Introduction: How to Program an Arduino Based Metal Detector

This Instructable is about the programming of an Atmega328 based Arduino. It concentrates on the various aspects of the programming. This Instructable is meant as a reference for anyone trying to program an own detector. This Instructable is not about how to build a metal detector. For this see my other Instructables (using these approaches)

Why?

There are many great micro controller metal detectors available designs available (GoldPic, SurfPi, FoxyPi, Teemo DIY, TPIMD, FelezJoo and the TechKiwiGadgets designs here on Instructables). They use various micro controllers giving in most cases some explanation about the design, but not digging too deep into the programming, as some of them were programmed in assembler. As I did try to port some of the designs to Arduino my focus was mainly on the programming itself. During many hours of programming and testing various designs of Arduino based metal detectors I came to the point where I had a nice running detector (not too sensitive but working) and had a pretty decent code for the detector. But looking at the code, I realized there were so many tricks and ideas implemented over the time, that a combined Instructable about how to build the detector and how to program it would be way to complex. Especially if only looking for a detector to build nobody would dig too much into the code, the code itself being usable for many other designs as well. So I decided to create this Instructable about the programming itself. This Instructable will cover the various aspects of timing, signal analyzing, data processing, data output (e. g. LCD) and testing, using interrupts and fast analog reads.

Detector types

There are different types of metal detectors. The most common types are Beat Frequency Oscillation (BFO), Induction Balance (IB) and Pulse Induction (PI). BFO and IB types of detectors are using continuous oscillations, the PI type uses a single pulse and analyses what happens thereafter. So the PI type of detector gives some nice period of time after the pulse to acquire data, analyze them, do some filtering and sending data out to displays etc. That is the reason why the PI detector was my first choice to start with.

In the first versions, the timing and data acquisition was done as part of the loop() routine, creating some trouble in regard to exact timing etc. Later on, all the timing stuff etc. was transplanted into Interrupt Service Routines (ISR) so basically running in the background with only some data crunching and output routines in the loop().

From this point on, the approaches described here can be used for other detector designs as well

The current description focuses on PI detectors, as these were the circuits available and therefor could be tested in real life. Other designs like IB and BFO-ish will be tried out and documented at a later point of time.

For the PI detectors two basic designs were used:

  1. the usual decay curve design (in my case without an additional OpAmp)
  2. the LC trap design, where the main pulse triggers a series of decaying oscillations

I did some separate Instructables about both designs which will be updated with the described approaches.

Step 1: Basics - Timing, Timers, Interrupts...

General thoughts

The basic principal of a metal detector is, that the inductance / signal in a coil changes as a target comes near to the coil. The usual ways of identifying these changes is by measuring frequency shifts, decay times, change of voltage at a selected point(s) of time etc. Based on the capabilities of the ATmega328 based Arduinos these inputs can be measured by analog reads, digital reads or triggering external interrupts.

This Instructable will cover these inputs.

To get a sensitive detector, unfortunately the inputs will need to be measured quite fast. The ATmega running at 16MHz seems to be running fast, still in many cases this is too slow to use standard Arduino based routines. Various approaches „outside“ the standard Arduino routines will be given here to provide as much speed as possible

Timing, timing, timing…..

The crucial element of coil based metal detectors is the timing. Usually the signals are fast, so small errors in timing will lead to false readings making it difficult to obtain stable readings.

The standard features for timing for the Arduino are routines like millis(), micros(), delay() and delaymicroseconds(). For most applications these routines work fine, for metal detector designs they do not perform that well.

The resolution of millis() is 4µS according the reference. At the other hand, when looking at the oscillation frequency of the Arduino the delay between two pulses at 16MHz is 0,0625µS, which is 64 times the resolution. To obtain access to this 16MHz frequency for timing purpose, the most convenient way is to use the ATmega internal timers. There are in total three timers (timer0, timer1 and timer2).

The timers can be set to be running at full 16MHz or lower frequencies by prescalers (dividers). The timers have different mode to behave, but in the simplest mode they simply count from 0 to a given value, trigger an Interrupt Service Routine (ISR – a small routine) and restart counting. This mode of operation is called Clear Timer on Compare (CTC). During the counting of the timer, at any time of the program the counter value can be accessed and copied to a variable.

This is true for most of the time the Arduino is running. Unfortunately not only the timers trigger interrupts but many other events trigger interrupts as well. Therefore interrupts may occur at similar times, of even during the execution of an ISR. As each interrupt delays the execution of the current code this leads again to small misreadings or even completely missed values. To brings some order into this the ATmega itself treat the interrupts with different priority.

Vector 	Addr	Source  Interrupts definition
1	0x0000	RESET	External Pin, Power-on Reset, Brown-out Reset and Watchdog System Reset
2	0x0002	INT0	External Interrupt Request 0
3	0x0004	INT1	External Interrupt Request 0
4	0x0006	PCINT0	Pin Change Interrupt Request 0
5	0x0008	PCINT1	Pin Change Interrupt Request 1
6	0x000A	PCINT2	Pin Change Interrupt Request 2
7	0x000C	WDT	Watchdog Time-out Interrupt
8	0x000E	TIMER2_COMPA	Timer/Counter2 Compare Match A
9	0x0010	TIMER2_COMPB	Timer/Coutner2 Compare Match B
10	0x0012	TIMER2_OVF	Timer/Counter2 Overflow
11	0x0014	TIMER1_CAPT	Timer/Counter1 Capture Event
12	0x0016	TIMER1_COMPA	Timer/Counter1 Compare Match A
13	0x0018	TIMER1_COMPB	Timer/Coutner1 Compare Match B
14	0x001A	TIMER1_OVF	Timer/Counter1 Overflow
15	0x001C	TIMER0_COMPA	Timer/Counter0 Compare Match A
16	0x001E	TIMER0_COMPB	Timer/Coutner0 Compare Match B
17	0x0020	TIMER0_OVF	Timer/Counter0 Overflow
18	0x0022	SPI STC		SPI Serial Transfer Complete
19	0x0024	USART_RX	USART Rx Complete
20	0x0026	USART_UDRE	USART Data Register Empty
21	0x0028	USART_TX	USART Tx Complete
22	0x002A	ADC		ADC Conversion Complete
23	0x002C	EE		READY EEPROM Ready
24	0x002E	ANALOG COMP	Analog Comparator
25	0x0030 	TWI 		2-wire Serial Interface (I2C)
26	0x0032 	SPM READY 	Store Program Memory Ready

The goal in the programming of stable timing is therefor to use the highest prioritized interrupts as possible and to find ways of preventing other interrupts of occurring during the ISR. This leads to the first rule in dealing with interrupts: Keep the ISRs small! (.. I will be violating this rule on a regular basis…).

As the standard Arduino functions know about these timers as well, quite some of the routines and libraries are using them. This means, that if we are using the timers and changing their preset values, some of the standard routines do not work anymore. This needs to be kept in mind.

Delay, tone, sensor, stepper, PWM functions as well as communications might not work properly anymore.

Step 2: How to Use Timers

There is a great Instructable about how to use timers. I used this Instructable a reference, will only give some abstracts here. In case you want to look deeper into the timers, see the Instructable „Arduino Timer Interrupts: 6 Steps (with Pictures)“.

The basic principle is, that first of all you tell the timer0, timer1 or timer2

  1. how fast it should run (prescaler in register TCCR0B, TCCR2B, TCCR2B)
  2. to what value should be counted (value in register OCR0A, OCR1A, OCR2A)
  3. what to do at this point (settings in TCCR0A, TCCR1A, TCCR2A -> reset and restart, trigger a PWM signal etc…)
  4. set the counter to a given value to start with (value in TCNT0, TCNT1, TCNT2)
  5. enabling the relevant interrupts (setting in TIMSK0, TIMSK1, TIMSK2) → this will call the ISR

Then you define the ISR with the interrupt vector name e. g. ISR(TIMER0_COMPA_vect) and define what should be done during this ISR. Again, this code should be kept short, as this code can be interrupted by other interrupts at any time, which will corrupt values or lead to a crash of the program.

An additional portion of information is, that the timer0 and timer2 can count to 255, the timer1 can count to 65535.

How to use timers for the metal detector

As said previously, timing is crucial for metal detectors. So all the timing relevant stuff will be done by using timers.

Usually, there is an initial pulse provided to a coil. After that the reaction of a coil (could be the same coil or a different coil) is measured. During this period of measuring ALL OTHER interrupts should be prevented! Interrupt during this time, will either lead to slightly off-values, corrupted values, missed values.

I use the timers for 3 different purposes:

  1. timer0 for the main event (e. g. the pulse for a pulse induction detector)
  2. timer1 for data acquisition (e. g. frequency shift detection, timing of analog to digital conversion)
  3. timer2 for tone/volume generation.

timer0

The main cycle of a pulse induction (PI) detector consists of two phases. Fist there is the pulse to power the coil, then there is the silence where data acquisition takes place. In case of a PI detector the pulse has a normal duration of around 250µS the silence after the pulse should be enough for data acquisition, processing and updating outputs.

So timer0 is first set to a desired „event-speed“ at one hand providing a pulse near to 250µS and a usable silence time. For a 200Hz PI detector this would be a prescaler of 1024 (aka 16MHz / 1024 = 15.625kHz → 64µS per cycle) with a compare counter of „4“ for the pulse and a silence counter of „72“.

// set timer0 - taking care of the pulse to the coil and the pause between two pulses 
// Pulse composes of 4.672ms "off" (72) and 0.32ms "on"(4)  
// separate times for OCR0A will be set in the interrupt routine 
// Resulting frequency is 200Hz 
cli(); 
TCCR0A = 0;  // set entire TCCR0A register to 0 
TCCR0B = 0;  // same for TCCR0B 
TCNT0 = 0;  // initialize counter value to 0
// set compare match register to required pulse with 
OCR0A = 72;  // = (4672µS/(0.0625µS * 1024)) - 1 (must be <256) 
// turn on Clear Timer on Compare (CTC) mode 
TCCR0A |= (1 << WGM01); 
// Set CS01 and CS00 bits for 1024 prescaler 
TCCR0B |= (1 << CS02) | (1 << CS00); 
// enable timer compare interrupt 
TIMSK0 |= (1 << OCIE0A); sei();

As the signal is very unsymmetrical the trick is, to set the value 4 and 72 at each interrupt.

The ISR look basically like this (with a global volatile boolean "toggle"):

ISR(TIMER0_COMPA_vect){ 
if(toggle){ 
  cli(); 
  OCR0A = 72; 	// set for a long "off-time" -> next interrupt timer0 in 4.672ms 
  sei(); 
} 
else{ 
  cli(); 
  OCR0A = 4; // set for a short "on-time" -> next interrupt timer0 in 320µS 
  sei();
} 
toggle=!toggle; 
}

Of course during the if’s and the else’s there is some additional code for setting outputs as well.

timer1

As the timer1 has the highest counter value (65535) is can be used to measure “long” periods of time with high precision. At maximum speed (16MHz) the longest event before an overrun is 4.1ms. If the change in timing of an event is small, the overruns can be even ignored. The maximum timing resolution is then 0.0625µs! That is the background timer1 is used for the data-acquisition.

First of all he timer is set for maximum speed

cli(); 
TCCR1A = 0;             // set entire TCCR1A register to 0 
TCCR1B = 0;             // same for TCCR1B 
TCNT1 = 0;              // initialize counter value to 0 
// set compare match register 
OCR1A = timer1MaxValue; // just a value to start with -> set to a long period 
			// to prevent unwanted interrupt interference 
// turn on Clear Timer on Compare (CTC) mode 
TCCR1B |= (1 << WGM12); 
// Set CS10 no prescaler → running at full 16MHz 
TCCR1B |= (1 << CS10); 
// enable timer compare interrupt → calling the ISR 
TIMSK1 |= (1 << OCIE1A); 
sei();

To start the data acquisition the timer0 restarts the timer1 after every pulse by setting the counter of timer1 to 0 ( TCNT1 = 0) . Now there are two option for data acquisition:

  1. Waiting for an event to happen, and use timer1 to see when it happened, by reading the counter TCNT1.
  2. time an event like analog read at a specific time (using the preset compare value of timer1) after the pulse.

In the first case an ISR (e. g. analog comparator, or pin change) will read the TCNT1, e. g.

ISR(ANALOG_COMP_vect){		// for analog comparator @ pin D6 and D7 
  Toggles[toggleCounter]=TCNT1; 
  toggleCounter++;
}
 

In the second case the compare value OCR1A is set to value where the timer1 compare ISR will be called and an analog read will be performed. During the ISR the compare value OCR1A can be changed to a new value to repeatingly perform analog to digital conversion (ADC) cycles e. g.

ISR(TIMER1_COMPA_vect){ 
cli(); 
OCR1A = timer1PauseValue;  	// set the next interrupt to the value where a 
				// next ADC will give a usefull value 
ADCSRA |= (1 << ADSC);     	// ADSC -> start the cycle -> will be cleared 
				// after the conversion is done 
sei(); 
}

timer2

The easiest way of indicating targets is by sound. When searching for treasures, your eyes are normally focused on where to search. So they are not available to look to a display or LED. Using sound makes it easy to find the exact location when looking at the coil and listening simultaneously. To provide precision and some feeling for the target, the tone should change its volume according to the strength of the signal. So to my opinion there should be a volume modulation implemented for the speaker.

This is simply done by a 32kHz PWM signal to a speaker. A standard speaker is to slow to transform a 32kHz signal to a tone. The 32kHz is “interpreted” rather as an analog value. By providing an audible frequency where the “on”-pulses consist of a 32kHz signal with varying PWM proportions the volume of the audible tone van be varied.

Timer2 has the nice feature that it is the hard-wired PWM function to the pins D3 and D11

These pins can be activated to PWM by setting the bits in COM2x1 (COM2A1 and COM2B1) in the register TCCR2A. Register / output A is D11 and register / output B is D3. By setting the „how to behave“ to “fast PWM”, the PWM is set to drive the pin directly without needing any ISRs! The compare value OCR2A sets the PWM ratio (OCR2A=0 → 0% positive wave → 0V; OCR2A=255 → 100% positive wave→ 5V).

OCR2A and therefor the PWM ratio can be set at any place in the code to change the volume.

Now that we have the volume, we still need to create the audible tone/frequency. This can be done with one of the other timers in their ISR (e. g. setting OCR2A = “volume” after the data acquisition and setting OCR2A=0; when starting the pulse. That creates audible tone of 200Hz).

Step 3: Using Digital and Analog Pins

Analog reading, but really fast.

By using analogRead() it is possible to read a 10Bit value with good precision and stability. Unfortunately the maximum sampling rate is about 10kHz. This is partially due to some additional code in analogRead() partially due to the prescaler of the clock for the AD-Conversion.

Fortunately the Instructable „Girino - Fast Arduino Oscilloscope“ provides all the information about how to obtain higher sampling rates. Just by uncommenting the relevant line, the sampling of the Analog to Digital Conversion (ADC) speed can be set.

// ADCSRA |= (1 << ADPS2) | (1 << ADPS0);   // 32 prescaler for 38.5 KHz 
ADCSRA |= (1 << ADPS2);                   // 16 prescaler for 76.9 KHz 
// ADCSRA |= (1 << ADPS1) | (1 << ADPS0);  // 8 prescaler for 153.8 KHz

To use the ADC, there are three basic ways:

  1. free running mode – each time a conversion is finished the specific ISR is called.
  2. single conversion with interrupt – after a conversion is complete the ISR is called
  3. single conversion and „delaying“ until the value is available (like in analogRead())

Although the free running mode achieves definitively the most samples during a given time, I do not recommend to use it for metal detecting purpose. Why? Due to the low priority of the interrupt (see interrupt priority table): it is at priority 22! The first few readings will most likely be precisely timed. After that, other interrupts will start to interfere and delay the ADC slightly. Will lead to small but significant deviations in timing of the ADC, thus leading to deviations in the ADC values.

So what I really recommend to use is the timer1 triggered measurement by single conversion with interrupt.

So each ISR of timer1 triggers a ADC simply by setting

ADCSRA |= (1 << ADSC);

About 270 (theoretically 208) xtal cycles later, the “I-am-finished-with-the-Analog-to-Digital-Conversions” ISR(ADC_vect) is called, and the value can be read. At the prescaler of 16, only 8bit out of 10bit resolution can be used, as the lower two byte will not give precise values (see data sheet) at high speeds.

As the ADC will be done in the background on hardware-level, so this timeframe can be used to execute some commands during the ISR for storing the value and limiting the amount of measured values etc

For some applications the dynamics of the signal is far away from what should expected to deliver good results (seeing a complete wave during a single AD conversion). Still this seems not to be a big issue. This is most likely due to the internal sample and hold circuit inside the ATmega chip witch seems to work pretty well!

Serious Bit-Banging at digital ports

At some instances the outputs of the Arduino have to be set. This can easily been done by using digitalWrite(). Still the functions have some additional overhead code what makes them slow(er), and thus needing quite some cycles.

As some of the outputs are changes during ISRs, bit-banging the ports is a way better choice.

The basic functions are:

  • PORTD = PORTD | B00000100; // setting D2 high without changing other ports
  • PORTD = PORTD & B11111011; // setting D2 low without changing other ports
  • PIND = PIND | B00000100; // toggling D2 if low → high; if high → low

The allocation of the ports are bitwise

PORTD  D7    D6    D5     D4     D3     D2     D1(TXD) D0(RXD) 
PORTB  N.A.  N.A.  D13    D12    D11    D10    D9      D8 
PORTC  A0    A1    A2     A3     A4     A5     Reset   N.A.

For the powering of the coil the ports should be directly set to HIGH or LOW, for the tone output the PIND command (toggling) allows for a neat feature: Multi Tone Target Identification.

If we have two points during a cycle where we toggle the pin for the speaker we can decide if we toggle the pin at both points or just at one, depending on the target. By doing this we achieve a high tone (two toggles per cycle 200Hz) or a low tone (one toggle per cycle 100Hz).

Using pins for testing

Especially during experimenting with a newly developed circuit and new code it is very useful to drive an additional pin to indicate what is happening in the code. This can be used either to trigger an oscilloscope, or at some instances to show how long a certain routine in the code takes. By setting the pin high at the beginning of a routine and setting it low after it, it can be nicely seen if this piece of code interferes with interrupts or other signals or to compare which version of code is faster or slower. That became handy for the I2C problems.

Attached is a picture of the analog signal (red) and the signal of a pin indicating how long the AD conversion takes.

Step 4: Interfaces and Data Output

The main target of a detector is, to find a target and then give some information about the target to the user. One way of indicating a target is by using the speaker as explained at timer2. There are other ways, to be described.

Serial Output.

This is mainly used for testing and experimenting, but it is unbeatable!

Serial.print() and siblings are extremely fast (if set to Serial.begin(115200)). So while trying out your circuit and get a feeling for the readings, Serial.print() can be used to send serious amounts of data to the computer. If formatted in a decent way (e.g. „space“ between values, „returns“ between cycles) large amount of data can be transferred for later analysis by copying the outputs from the serial monitor to Excel or similar spreadsheet programs (I am using Libre Office).

I used this extensively, and one of the detector project will incorporate to use this to print data to a 16x2 LCD at a later point of time.

16x2 LCD

I think this is a good way to give some additional information about the target. This can incorporate the signal strength, but at the same time provide a menu to navigate through potential settings (sensitivity, auto balancing, power, discrimination).

There are two simple ways to drive a typical 16x2 LCD

  1. direct wiring with a 4 bit transferred
  2. using a I2C backpack

I know, that there are UART packpacks as well, but they are not so common as the I2C backpack.

The direct wiring is straight forward, but cumbersome, as many ports are used and some wiring is necessary. So the simplest way to use a 16x2 display should be using the I2C backpack. SHOULD BE!! Unfortunately there are some real topics here:

  1. it seems that it is using timer0 during start up
  2. I2C is incredibly slow!!!
  3. It is connected to A5 and A4.

1. During lcd.begin(16,2); apparently the delay() and siblings are used (funnily enough only there). This means, that if we like to use these timers for other purposes as we do, we have to call lcd.begin(16,2) prior to setting the timers for our purpose (did cost me a bit of time….). The other calls of the LCD do not use the timers or, can live at least with modified timer settings.

2. The biggest drawback was definitely the speed of I2C. During the first implementation of sending all the information to the LCD, the LCD would not show anything at all, it would just hang. I realized that sending 32 characters including a lcd.clear() was too much, so I reduced the amount to 2 characters. But even sending two characters took about 3 milliseconds, which is too long.

At 200Hz working frequency the cycle consists of a 300µs pulse and 4.7ms „silence. This „silence“ is used for data acquisition (about 2.5msin my case) and data crunching (1ms in my case) leaves about 1.1ms for the LCD output. Experiments showed that even one single character takes about 1.5ms. To solve this problem the routine Wire.setClock(400000) was quite useful.

Normally the I2C clock is set to 100kHz by default. By using Wire.setClock(400000) the clock can be set to 400kHz. The 100kHz default is set in the wire.begin(). This routine is called during lcd.begin(). So the Wire.setClock(400000) must be called after the lcd.begin() (…. again some hours frustration).

Still the issue had to be solved, that only one character could be send per cycle. To solve this, an array (16x2 characters → 32 characters in total) was created, being filled with all required information for display. This array is than read one character per cycle and send to the display.

3. the I2C is by default connected to the A4 and A5. What became apparent during testing is, that driving the I2C bus is destabilizing the voltage at the analog pins (probably to all pins, but the analog pins are sensitive as it effects the ADC). This leads to the timing issue, that all I2C transmissions should be well separated to any sensitive ADC cycles.

So by setting the I2C clock speed to 400kHz and sending out only one character per cycle the duration of communication to the LCD could be reduced to be finished before the next pulse was send to the coil. So A4 and A5 were nicely quite when starting the data-acquisition at the pin A0 after the pulse. The refresh rate of the LCD is therefor 200Hz/32 characters → 6.25Hz

Sound

As discussed in the step about the timers, timer2 is used to generate a dynamic sound output for the detector. A simple speaker connected via a 100 Ohm resistor to the port A (D11) or B (D3) give a nice feeling for the signal strength and type of target (if Multi Tone Target Identification is used). To achieve this the code is not realy nice, but doing the job.

At two places in the code, timed by the timer0 and eg after dataCrunching there are two snippets of code driven by the booleans "sound" and "pitch"

if((sound)&&(highPitch)) { // if the speaker should sound and at high pitch
if(OCR2A) OCR2A=0; else OCR2A=volume; }
if(sound){            	// if the speaker should make noise 
 if(OCR2A)
   OCR2A=0;
 else
   OCR2A=volume;  
}
else
  OCR2A=0;

Setting the booleans "sound" and "pitch" should be done at other places in the code as they are not time critical.

Same counts for the variable "volume" as an unsigned char or byte.

LED

As there is usually a LED present at D13 on all Arduino boards I use this LED aswel, setting the pin by bit-banging described in the last step. During testing this gives a good impression what is currently going on, for later use, the pin can be used to drive an external LED.

Attached are screenshots from different timing, made visible by an external pin. The initial pulse can be seen, than some wild oscillation. During the oscillation the data acquisition takes place. After the data acquisition the data crunchings starts (yellow signal set to high). At the end of all data crunching and data transfer the yellow signal is set to low. The pictures show the difference in duration of:

  1. only data crunching
  2. data crunching and sending out 50 values via Serial.print()
  3. data crunching and sending out ony 1 character to the LCD via I2C @ 100kHz

Step 5: Data Crunching

In an ideal world, the received signal would be crystal clear, and the smallest change compared to a reference would indicate a target. Unfortunately the world is not ideal and the signals in a metal detector are noisy and dirty (especially when using so few external components as I do). To filter out the relevant part of the data, some approaches were tried out to filter the received data and even some useful ones were found.

The first point of optimization is the circuit!

If the circuit and the coil are scrap, not the best filtering algorithm can help you.

There are some general rules, like using big condensers. Shielding the circuit will help as well and should be experimented with.

One thing what really makes things difficult are spikes in the signal. If the signal goes higher than 5V or lower than 0V there are some internal circuits protecting the ATmega from blowing up. This works only if the currents are kept low. For protection this works well, for the stability of the program this is not as good.

Having worked with quite „dirty“ signals, the code had hick-ups and the ADC cycles had major misreadings. This can lead to small delays in readings, missed readings, or plain unexplainable values. Optimizing the circuit is the first step to decent data.

Here some individual testing needs to be done for your circuit. Approaches are:

  • keeping connections short
  • using capacitors to stabilize the supply voltage
  • preventing large loads on Arduino pins
  • preventing high currents in proximity to sensitive parts / connections
  • preventing heating up/ cooling down of components
  • shielding components, connections, complete circuits
  • twisting wires
  • preventing moving / lose wires
  • knowing what you are doing…..

How to interpret measured values

The simplest approach to identifying changes to the measured signal is in comparing the signal to a reference value for the signal. If the measured value is different to this reference value then you found something.

Reality proves different.

In reality you will have small deviations in your readings.

These deviations are due to signal interference from the 50/60Hz signal from the main grid, high frequency signals from poorly shielded devices (AC-adaptors, computers), interference to other wireless signals (Wifi, GSM) or simply oscillations as a result from the circuit design. These interference can deviate from a reference value in such a magnitude, that the sensitivity of the detector can be rendered useless.

In the next parts different approaches explained for dealing with noisy signals.

Creating means

Creating a mean value over the last few values is a good point to start. This way small deviations in positive and negative direction will be leveled out. Depending on the magnitude and appearance of the noise the amount of values to create the average might be choosen differently

The easiest way is to define an array and fill your obtained values into this array, increment by each cycle. Each cycle you add up all the values, and compare them to the reference. Small deviations will be eliminated. This is easily implemented by a global counter which is incremented by each cycle. If it reaches a „maxValue“, it is set to 0.

Each cycle, a for-loop counts from 0 to the „maxValue“ to grate the sum. Dividing the sum by the maxValue can be done does not really makes sense. The reference value could compare directly to the sum.

  • Advantage: easy to implement, fast code, simple to read.
  • Disadvantage: small delay (which is probably not relevant), only usable for little noise
#define maxValue 20 
int valueCounter=0; 
int valueSamples[maxValue]; 

void dataCrunching(void ){
int i;
double average;
  valueSamples[valueCounter]=nextValue;
  valueCounter++;
  if(valueCounter>(maxValue-1))
    valueCounter=0;
  average=0;
 for(i=0; i<maxValue; i++)
   average=average+valueSamples[i];
}

Observing borders

if the signal is too volatile an other approach could be looking at the minimum or maximum values in an array. This is especially useful, if the signal has the tendency to deviate especially in one direction. Again an array is used, filled with one value per cycle. Each cycle the array is checked for the e. g. lowest value. This value is then compared to the reference value.

Creating e. g. the lowest value can be done, by giving a variable a high value to start with (e. g. 255) and then checking in the for-loop if the given array value is smaller

if(minValue<array[i])
minValue=array[i];

after the loop minValue has the smallest value of the array.

  • Advantage: even noisy signals can be analyzed pretty well
  • Disadvantage: array need to be large enough (can slow down reaction speed)
#define maxValue 20 
int valueCounter=0; 
int valueSamples[maxValue]; 

void dataCrunching(void){
int i;
int minValue;
  valueSamples[valueCounter]=nextValue;
  valueCounter++;
  if(valueCounter>(maxValue-1))
    valueCounter=0;
  minValue=32767;
  for(i=0;i<maxValue;i++){
    if(minValueminValue=valueSamples[i];
  }
}

Ignoring Values

Sometimes the there are some glitches in the signal or plain misreadings. If these values are added to the array, they will completely corrupt the analysis of the array. To prevent this, values which are „not trustworthy“ can be filtered out by setting „expectation-borders“. The simplest way of doing so, is to look at the average in the array and only accept values in a +/- range of the average. I do not recommend to do so! Why not? It might lead to the situation, that the average value get „stuck“. If the readings are slowly going up, and then suddenly back to „normal“, the average values will stuck with the high values, as the normal values might be out of the +/- range.

Better to use the reference value for this.

If the signal is outside of a +/- range in regard to the reference value in the array is just not replaced, the old value remains in the array. This leads to an array full of values in the same range. Comparing the average of the array to the reference value will be quite sensitive to small changes

  • Advantage: good way to filter out glitches and major misreadings, even small changes can be detected in a noisy signal.
  • Disadvantage: enough values need to be within the range, reference value and range should be chosen with care.
#define maxValue 20
int valueCounter=0;
int valueSamples[maxValue];
int limit=10;
int referenceValue;	// needs to be created somewhere in the program!!!

void dataCrunching(void){
int i;
int diff;
  diff=abs(nextValue-referenceValue)		// create the difference
  if(diff<limit)				// if difference larger than "limit"
    valueSamples[valueCounter]=nextValue;
  valueCounter++;
  if(valueCounter>(maxValue-1))
    valueCounter=0;
  average=0;
  for(i=0;i<maxValue;i++)
    average=average+valueSamples[i];
}

Average single values

One way of filtering out small deviations is to use the previous value multiply it by a factor add the new value and divide by the (factor+1).

  • Advantage: very easy to implement, filters out small noise
  • Disadvantage: ignores small changes, even if they are persistent (new value needs to deviate more than „factor“ to influence the result after the division)
#define maxValue 20
int valueCounter=0;
int valueSamples[maxValue];
int factor=3;

void dataCrunching(void){
int i;
double filterValue;
  filterValue=valueSamples[valueCounter];
  filterValue=filterValue*factor;
  filterValue=filterValue+nextValue;
  valueSamples[valueCounter]=filterValue/(factor+1);
  valueCounter++;
  if(valueCounter>(maxValue-1))
    valueCounter=0;
  average=0;
  for(i=0; i<maxValue;i++)
    average=average+valueSamples[i];
}

Using higher/lower counter

In this case the measured value would be compared to a previously created average. If the value is larger then the average a counter diffCounter would be incremented. If the value is smaller, the diffCounter would be decreased. If the diffCounter reaches a maximum value maxDiffCounter, the average is incremented and the diffCounter is set to 0. If the diffCounter goes below 0, the average is decreased and the diffCounter is set to maxDiffCounter.

Advantage: even very noisy signal can be analyzed.

Disadvantage: the distribution of high/lows should be „stable“, indications of deviations could be pretty slow

#define maxDiffCounter 30
int diffCounter=0;
int referenceValue;	   // needs to be created elsewhere in the program!!!
int sampleValue;
int average=0;

void dataCrunching(void){
int i;
double average;
  if(nextValue>referenceValue)		// if larger than the reference
    diffCounter++;			// increase the diffCounter
  else if(nextValue<referenceValue)	// if smalle than the reference
   diffCounter--;			// decrease the difCounter
  if(diffCounter>maxDiffCounter){	// maximum value reached
    average++;				// correct the average
    diffCounter=0;			// restart at 0
  }
  else if(diffCounter<0){		// minimum value reached	
    average--;				// correct the average
    diffCounter=maxDiffCounter;		// restart at maximum value
  }
}	

Creating reference Values

The easy part in the data crunching ware the different approaches to filter out noise. A different topic is how to create a reference value to measure against.

Again there are different approaches with different advantages and disadvantages. There are different ways to use an external input to obtain a reference value.

  1. using a push button, when pressed the recent value will be used as a reference
  2. having a dial to set a proper reference
  3. using push buttons +/- to set a reference value.

They are old school except for variant one, which can be used for pin pointing targets as well.

Still the more convenient way is to have an algorithm in place to create this value. I would create a first routine called calibrate() where I would have a “warm-up” of the circuit and then a period of time where I would create an average value with the minimum of filters set. Why minimum filters? The reference will have a start value of 0 or any other value. Except if the proximate value is know (stored) the real value can be either near or far from this initial value. So we need to move freely towards the real reference.

The second step is then to average the reference with all required filters just to find the “real” reference. At that point of time you will have a good reference.

Now we will have to deal with drift. Drift can occur due to temperature drift or to a change of the characteristics of the soil. So some kind adjusting of the reference value is useful.

My personal approach is to use a higher/lower (as decribed) counter for this. To prevent too fast changes of the reference value, the maximum counter value for changing the reference value can be set quite high (100-500) this compensates temperature. This maximum value for the counter could be set via a menu according to the users requirement

Using a push button approach for a pin pointing mode of operation would simply disable the adjustment of the reference value.


Step 6: Testing....

Testing

The best code remains theory until it is tested. As the testing took the main part of the design, at least I would like to share some experiences.

As explained earlier the Serial.print() stuff is pretty fast even for large amount of data. So during development I really recommend using the serial functions to print as many information to your computer as relevant. Here my recommendation is: Start with the raw data! If things are not running smoothly, try to get an impression what you code is dealing with. If you look at the filtered data, you might miss significant information. Her you might find glitches, drifting of signals or repeating pattern of deviations. The first step then would be looking at numbers, the second step would be looking at curves (created via spreadsheet programs like Excel or Libre Office). To get there, print the values of interest separated by “ “ (space) and separate the cycles with a new line (e. g. by using a Serial.prinln(“”)).

When going into filtering, these values can be analyzed by numbers as well, probably using additionally a LED as indication.

When working with analog signals an oscilloscope will come in handy. This is especially the case when the results are unexplainable. These results can be glitches of especially high or low values, repeating pattern of deviation. Single channel oscilloscopes can do a good job, my favorite approach is to use a two channel oscilloscope and use an additional pin for triggering the refresh.

Reasons for misreadings

during many hours of testing I came across different problems resulting in a malfunctioning of the detector.

Although the ATmega chip incorporates some basic pin protection it reacts funny when challenged. Over and under voltage voltage will lead to hick-ups in the code (stalling for a short period of time), a reboot of the chip or in worst case a dead pin. So make sure the pins are well protected. When using diodes to Vcc and GND, keep in mind, that they have a forward voltage of a few millivolts, thus creating small under or over voltages.

One other phenomenon is that AD conversion can be disturbed by over or under voltages leading to glitches (values being plain 255 where they should be different or deviating largely in single occasions).

Overlaying signals can destabilize readings. So when getting funny readings, try to touch different components to see if these patterns increase or not (ONLY IF DEALING WITH LOW VOLTAGES!). This might lead to components picking up the noise, needing some shielding or rearranging.

Lose wires can generate some noise as well, so do not move components during testing

Interfering interrupts will lead to either missed data (value skipped) or to additional delays in the ISR. Being aware of the priority of the interrupt can help, as higher priority ISR will not be interrupted by lower priority interrupts.

Accessing the same variables at the same time or changing register values while they are in use will lead to a total halt of the program. So keep an eye on the overall timing of the code when changing timing of the interrupts dynamically.

To find conflicts in timing, use an external pins to indicate when and how long a certain part of the code is running. This of coarse requires an oscilloscope. A different approach is to use the timer1 counter value to be simply printed to the serial monitor. Get the timer1 value at the start of a routine by calling:

start=TCNT1; 

and at the end of the routine:

end==TCNT1;

by sending the start and end value to the serial monitor, some feeling can be provided for the timing.

Conclusion

Although there are way better micro controllers to use for metal detectors (faster, more precise) the Arduino gives some great approaches. These approaches try to get a maximum of precision and/or speed. There are some basic ways to deal with noisy signals too. But one important thing remains: if the circuit is primitive or poor, the overall results will be too. At the other hand the initially mentioned available circuits (GoldPic, SurfPi, FoxyPi, Teemo DIY, TPIMD, FelezJoo and the TechKiwiGadgets designs here on Instructables) can be used in connection with an Arduino with the above approaches, replacing the intended micro controllers with you own code.

As a reference I attached the full code with the implementation of allmost all described features. The required circuit is the LC-Trap circuit with the A0 connected to D6. I will do a separate Instructable to explain the background to the circuit and code in detail.

Have fun.

P.S. If you found some usefull ideas for you project in this Instructabel, please be so nice and reference to this Instructable