How To Program An Arduino Based Metal Detector

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 18

instructables

How to Program an Arduino Based Metal Detector

by JorBi

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)
How to Program an Arduino Based Metal Detector: Page 1
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).

How to Program an Arduino Based Metal Detector: Page 2


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)“.
How to Program an Arduino Based Metal Detector: Page 3
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();

How to Program an Arduino Based Metal Detector: Page 4


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
How to Program an Arduino Based Metal Detector: Page 5
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.

How to Program an Arduino Based Metal Detector: Page 6


// 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.

How to Program an Arduino Based Metal Detector: Page 7


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.

How to Program an Arduino Based Metal Detector: Page 8


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

How to Program an Arduino Based Metal Detector: Page 9


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

How to Program an Arduino Based Metal Detector: Page 10


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.

How to Program an Arduino Based Metal Detector: Page 11


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

How to Program an Arduino Based Metal Detector: Page 12


#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.

How to Program an Arduino Based Metal Detector: Page 13


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

How to Program an Arduino Based Metal Detector: Page 14


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

How to Program an Arduino Based Metal Detector: Page 15


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.
How to Program an Arduino Based Metal Detector: Page 16
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.

How to Program an Arduino Based Metal Detector: Page 17


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

Download
http://www.instructables.com/ORIG/F28/0JW0/JCUUP6Y8/F280JW0JCUUP6Y8.ino

How to Program an Arduino Based Metal Detector: Page 18

You might also like