Construye un Detector de Metales Sensible con Arduino: Discriminación Férrico/No Férrico
Componentes y materiales
| Componente | Cantidad |
|---|---|
| Arduino Nano R3 | 1 |
| Pantalla LCD 16x2 (blanco sobre azul) | 1 |
| Amplificador operacional TL081 (o LT1677, 741) | 1 |
| Altavoz 0.25W, 8 ohmios | 1 |
| Transistor NPN de propósito general | 1 |
| Bobinas de búsqueda (ver construcción) | 2 |
| Resistencias y condensadores varios | 1 set |
Aplicaciones y servicios
- Arduino IDE – Para compilar y subir el firmware.
Descripción del proyecto
Este proyecto muestra cómo construir un detector de metales sensible capaz de discriminar entre materiales férricos y no férricos. La sensibilidad es notable para un dispositivo relativamente simple.
Está basado en el proyecto de David Crocker (Arduino CC Forum, 2013). Decidí probar su código porque no encontré evidencia (fotos/vídeos) de que alguien lo hubiera construido y funcionara bien.
Primero monté la versión básica con el código original de GitHub para verificar el funcionamiento, luego mejoré el firmware añadiendo:
- Señal acústica (buzzer/altavoz).
- Pantalla LCD 16x2 con información visual: tipo de metal (Férrico / No Férrico) y barra gráfica de proximidad.
Principio de funcionamiento: VLF Induction Balance
Utiliza tecnología VLF (Very Low Frequency) Induction Balance con dos bobinas idénticas:
- Bobina transmisora (Tx): genera campo electromagnético.
- Bobina receptora (Rx): detecta perturbaciones causadas por metales.
El equilibrio de bobinas es crítico. Un potenciómetro anula el componente de 90° fuera de fase; el componente en fase se anula ajustando la posición relativa de las bobinas (estilo IB clásico).
Construcción de las bobinas
- Forma: D-shape (media circunferencia).
- Diámetro del soporte: 11 cm.
- Vueltas: 64 vueltas de hilo esmaltado 0.5 mm².
- Protección: cinta aislante + pantalla de papel de aluminio unida con alambre estañado (dejar pequeño hueco para que no actúe como vuelta en cortocircuito).
- Montaje: bridas sobre placa plástica rígida.
Ajuste de frecuencia resonante
La frecuencia resonante paralela del circuito bobina-condensador debe determinarse (calculadora online u osciloscopio). Con las dimensiones indicadas, resulta 7.64 kHz exactos, valor ya puesto en el código.
Si tu frecuencia difiere, ajusta en el código:
#define TIMER1_TOP (249) // Ajuste fino de frecuencia
Resultados obtenidos
- Estabilidad perfecta sin metal presente.
- Tapa metálica 15 cm detectada a >30 cm.
- Objetos grandes detectados a 40-50 cm.
- Moneda pequeña detectada a 15 cm en aire.
Alimentación: 2 baterías Li-ion en serie (7.4 V) al pin Vin del Arduino. Consumo <20 mA → gran autonomía.
El vídeo (en el proyecto original) detalla la construcción completa.
Mejoras futuras
Se puede aumentar sensibilidad añadiendo un MOSFET de potencia para excitar la bobina Tx. Se probará en próximos vídeos.
Código
Firmware Arduino (C++)
// Induction balance metal detector
// CPU 16MHz, ADC clock 1MHz, resolución 8 bits
// Timer1: 62.5kHz square wave → Timer0 divide /8 = 7.8125kHz para bobina Tx
// 16 ciclos ADC por conversión, 8 muestras por ciclo de excitación
// 4 detectores de fase a 45° (cancela 3er armónico)
// Timer2: generación de tono para auricular
// Otras ratios Timer1 posibles (235+)
#include <LiquidCrystal.h>
#include <LcdBarGraph.h>
#define max_ampAverage 200
LiquidCrystal lcd(6, 7, 10, 11, 12, 13);
LcdBarGraph lbg(&lcd, 16, 0, 1);
#define TIMER1_TOP (259) // Ajuste fino frecuencia (ver texto)
#define USE_3V3_AREF (1) // 1 si alimentación USB (ruido en 5V), 0 si standalone con 3.3V
// Pines digitales
const int debugTxPin = 1;
const int encoderButtonPin = 2;
const int earpiecePin = 3;
const int T0InputPin = 4;
const int coilDrivePin = 5;
const int LcdRsPin = 6;
const int LcdEnPin = 7;
const int LcdPowerPin = 8;
const int T0OutputPin = 9;
const int lcdD4Pin = 10;
const int lcdD5Pin = 11;
const int LcdD6Pin = 12;
const int LcdD7Pin = 13;
// Pines analógicos
const int receiverInputPin = 0;
const int encoderAPin = A1;
const int encoderBpin = A2;
// Variables ISR
int16_t bins[4];
uint16_t numSamples = 0;
const uint16_t numSamplesToAverage = 1024;
// Compartidas ISR / main
volatile int16_t averages[4];
volatile uint32_t ticks = 0;
volatile bool sampleReady = false;
// Solo main
int16_t calib[4];
volatile uint8_t lastctr;
volatile uint16_t misses = 0;
const double halfRoot2 = sqrt(0.5);
const double quarterPi = 3.1415927/4.0;
const double radiansToDegrees = 180.0/3.1415927;
const float phaseAdjust = (45.0 * 32.0)/(float)(TIMER1_TOP + 1);
float threshold = 5.0; // Menor = más sensible. 10 usable con bobina bien equilibrada.
void setup() {
lcd.begin(16, 2);
pinMode(encoderButtonPin, INPUT_PULLUP);
digitalWrite(T0OutputPin, LOW);
pinMode(T0OutputPin, OUTPUT);
digitalWrite(coilDrivePin, LOW);
pinMode(coilDrivePin, OUTPUT);
cli();
TCCR0B = 0; TIMSK0 = 0; TIFR0 = 0x07;
// ADC trigger en Timer1 overflow
#if USE_3V3_AREF
ADMUX = (1 << ADLAR);
#else
ADMUX = (1 << REFS0) | (1 << ADLAR);
#endif
ADCSRB = (1 << ADTS2) | (1 << ADTS1);
ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | (1 << ADPS2);
DIDR0 = 1;
// Timer1: phase correct PWM, TOP=ICR1
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = (1 << WGM12) | (1 << WGM13) | (1 << CS10);
TCCR1C = 0;
OCR1AH = (TIMER1_TOP/2 >> 8); OCR1AL = (TIMER1_TOP/2 & 0xFF);
ICR1H = (TIMER1_TOP >> 8); ICR1L = (TIMER1_TOP & 0xFF);
TCNT1H = 0; TCNT1L = 0;
TIFR1 = 0x07;
TIMSK1 = (1 << TOIE1);
// Timer0: clock=T0, fast PWM, TOP=7
TCCR0A = (1 << COM0B1) | (1 << WGM01) | (1 << WGM00);
TCCR0B = (1 << CS00) | (1 << CS01) | (1 << CS02) | (1 << WGM02);
OCR0A = 7; OCR0B = 3; TCNT0 = 0;
sei();
while (!sampleReady) {}
misses = 0; sampleReady = false;
Serial.begin(19200);
}
// ISR Timer1 overflow: muestreo ADC + timekeeping
ISR(TIMER1_OVF_vect) {
++ticks;
uint8_t ctr = TCNT0;
int16_t val = (int16_t)(uint16_t)ADCH;
if (ctr != ((lastctr + 1) & 7)) ++misses;
lastctr = ctr;
int16_t *p = &bins[ctr & 3];
if (ctr < 4) { *p += val; if (*p > 15000) *p = 15000; }
else { *p -= val; if (*p < -15000) *p = -15000; }
if (ctr == 7) {
++numSamples;
if (numSamples == numSamplesToAverage) {
numSamples = 0;
if (!sampleReady) {
memcpy((void*)averages, bins, sizeof(averages));
sampleReady = true;
}
memset(bins, 0, sizeof(bins));
}
}
}
void loop() {
while (!sampleReady) {}
uint32_t oldTicks = ticks;
if (digitalRead(encoderButtonPin) == LOW) {
// Calibración: guarda offsets actuales
for (int i = 0; i < 4; ++i) calib[i] = averages[i];
sampleReady = false;
Serial.print("Calibrated: ");
lcd.setCursor(0,0); lcd.print("Calibrating... ");
for (int i = 0; i < 4; ++i) {
Serial.print(' '); Serial.print(calib[i]);
lcd.setCursor(0,1); lcd.print(' '); lcd.print(calib[i]); lcd.print(" ");
}
Serial.println();
} else {
// Resta calibración
for (int i = 0; i < 4; ++i) averages[i] -= calib[i];
const double f = 200.0;
double bin0 = (averages[0] + halfRoot2 * (averages[1] - averages[3]))/f;
double bin1 = (averages[1] + halfRoot2 * (averages[0] + averages[2]))/f;
double bin2 = (averages[2] + halfRoot2 * (averages[1] + averages[3]))/f;
double bin3 = (averages[3] + halfRoot2 * (averages[2] - averages[0]))/f;
sampleReady = false;
double amp1 = sqrt(bin0*bin0 + bin2*bin2);
double amp2 = sqrt(bin1*bin1 + bin3*bin3);
double ampAverage = (amp1 + amp2)/2.0;
double phase1 = atan2(bin0, bin2) * radiansToDegrees + 45.0;
double phase2 = atan2(bin1, bin3) * radiansToDegrees;
if (phase1 > phase2) { double t=phase1; phase1=phase2; phase2=t; }
double phaseAverage = ((phase1 + phase2)/2.0) - phaseAdjust;
if (phase2 - phase1 > 180.0) {
if (phaseAverage < 0) phaseAverage += 180.0; else phaseAverage -= 180.0;
}
// Diagnóstico serie
Serial.print(misses); Serial.print(' ');
Serial.print(bin0,2); Serial.print(' ');
Serial.print(bin1,2); Serial.print(' ');
Serial.print(bin2,2); Serial.print(' ');
Serial.print(bin3,2); Serial.print(" ");
Serial.print(amp1,2); Serial.print(' ');
Serial.print(amp2,2); Serial.print(' ');
Serial.print(phase1,2); Serial.print(' ');
Serial.print(phase2,2); Serial.print(" ");
if (ampAverage >= 0) Serial.print(' ');
Serial.print(ampAverage,1); Serial.print(' ');
lcd.setCursor(0,0); lcd.print(" "); lcd.print(ampAverage);
lcd.setCursor(0,1); lbg.drawValue(ampAverage, max_ampAverage);
if (phaseAverage >= 0) Serial.print(' ');
Serial.print((int)phaseAverage);
// Clasificación y alerta
if (ampAverage >= threshold) {
if (phaseAverage < -20.0) {
Serial.print(" Non-ferrous");
lcd.setCursor(0,0); lcd.print("NonFerous ");
} else {
Serial.print(" Ferrous");
lcd.setCursor(0,0); lcd.print("Ferrous ");
}
float temp = ampAverage;
int thisPitch = map(temp, 10, 200, 100, 1500);
tone(3, thisPitch, 120);
while (temp > threshold) { Serial.print('!'); temp -= threshold/2; }
}
Serial.println();
}
while (ticks - oldTicks < 8000) {}
}
Librería LcdBarGraph
Disponible en el repositorio del proyecto (solo descarga, sin vista previa).
Esquemático
Proyecto patrocinado por PCBgogo.
Proceso de manufactura
- Construya un Detector de Llanto de Bebé Económico con Raspberry Pi – Guía DIY
- Construye un reloj Numitron retro con Arduino: Simple, fiable y eficiente energéticamente
- Construye un Juego Giroscópico en Tiempo Real con Arduino Nano y Sensor MPU-6050
- Detector de terremotos DIY con ADXL335 – Acelerómetro 3 ejes de baja potencia
- Construye un voltímetro DIY con Arduino y pantalla Nokia 5110 – Guía paso a paso
- MobBob: Robot Arduino Impreso en 3D, Controlado por Smartphone Android
- Construir una rueda medidora DIY precisa con Arduino Nano y encoder rotativo
- Crear un medidor IoT web con Arduino, Yaler e IFTTT
- Construye un monitor de calidad del aire preciso con el sensor Sharp GP2Y1010AU0F
- Contador Geiger Portátil con Arduino Nano: Construye tu Detector de Radiación
- Juego Pong 1D con Arduino Nano y tira LED WS2812: Tutorial paso a paso