Arduino Nanoで作る高感度金属探知機 — 鉄/非鉄金属の識別機能付き
必要なパーツと材料
| パーツ | 数量 | 備考 |
|---|---|---|
| Arduino Nano R3 | 1 | |
| 16x2キャラクタLCD (白文字/青背景) | 1 | Adafruit標準品など |
| TL081 オペアンプIC | 1 | LT1677や741でも代用可 |
| スピーカー (0.25W, 8Ω) | 1 | |
| 汎用NPNトランジスタ | 1 | スピーカー駆動用 |
| サーチコイル (送信/受信用) | 2 | 自作: 11cm径ボビンに0.5mm²エナメル線64ターン、D字型巻き |
| 抵抗、コンデンサ各種 | 1式 | 回路図参照 |
| スイッチ3個、ポテンショメータ、電池 (リチウム2直列 7.4V推奨) | 各1 |
使用するアプリ
- Arduino IDE
プロジェクトの概要
今回は鉄 (強磁性体) と非鉄金属 (銅、アルミなど) を識別できる高感度な金属探知機をArduino Nanoで製作します。比較的シンプルな構成ながら、満足のいく感度が得られます。
このプロジェクトは PCBgogo のスポンサードです: https://www.pcbgogo.com/promo/from_MirkoPavleskiMK
ベースになったのはDavid Crocker氏が2013年にArduino CC Forumで公開したプロジェクトです。GitHub上のコードを見つけましたが、実際に動作する完成品の写真や動画が見当たらなかったため、まずは基本版を組んで動作確認を行い、その後以下の機能を追加して改良しました:
- 検出時の音声信号 (トーン出力)
- 16x2 LCDへの表示: 検出物質の種類 (Ferrous/Non-ferrous) と近接度を示すバーグラフ

構成部品と仕組み
この探知機は VLF (超低周波) インダクションバランス方式 を採用しています。同一仕様の2つのコイル (送信コイルと受信コイル) を使用し、コイルバランスの調整が極めて重要です。ポテンショメータで90度位相差成分をゼロ調整し、コイルの相対位置調整で同位相成分をヌルにします (典型的なIB検出器の手法)。
各コイルは以下の仕様で自作:
- ボビン外径: 11cm
- エナメル線: 0.5mm²、64ターン、D字型巻き
- テープ巻き後、アルミ箔でシールド (すずめっき銅線で結束、短絡ターンにならないよう微小ギャップを確保)
- プラスチックプレートにタイラップで固定

共振周波数の設定
一次コイルとコンデンサの並列共振周波数をオンライン計算機やオシロスコープで求めます。上記仕様通りであれば 7.64 kHz になります。コード内の以下の定数で微調整可能です:
#define TIMER1_TOP (249) // 周波数微調整用
性能と実測値
- 金属がない状態では完全に安定
- 直径15cmの金属蓋を30cm以上の距離で検出
- より大きな金属物体は40〜50cm以上で検出
- 小さなコインを空中で15cmの距離で検出
- 電源: リチウム電池2直列 (7.4V) をArduinoのVinに接続、消費電流20mA以下で長時間駆動可能
詳細な製作過程は動画で確認できます。今後、送信コイル駆動用にパワーMOSFETを追加することで感度を大幅に向上できる可能性があり、別動画で検証予定です。
ソースコード
メインのArduinoスケッチと、LCDバーグラフ表示用ライブラリ (LcdBarGraph) が必要です。完全なコードは以下に掲載されています (長いため主要部分のみ抜粋):
Arduino メインスケッチ (抜粋)
// Induction balance metal detector
// CPU 16MHz, ADCクロック 1MHz, ADC分解能8bit
// Timer1: システムクロックを約256分周 → 62.5kHz矩形波 (ADCトリガ兼用)
// Timer0: Timer1出力を8分周 → 7.8125kHz (送信コイル駆動)
// 1サイクルあたり16 ADCクロック (実質13.5サイクル)、8サンプル取得
// 45度間隔の4相位相検波を実装 (第3高調波をキャンセル)
// Timer2: イヤホン/ヘッドセット用トーン生成
#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) // コイル共振に合わせて調整
#define USE_3V3_AREF (1) // USB給電時は1、スタンドアローン時は0
// ピン定義 (主要なもの)
const int encoderButtonPin = 2; // キャリブレーションボタン
const int earpiecePin = 3; // トーン出力 (OCR2B)
const int T0InputPin = 4;
const int coilDrivePin = 5; // 送信コイル駆動
const int LcdPowerPin = 8; // LCD電源/バックライト
const int T0OutputPin = 9; // Timer0入力用
const int receiverInputPin = 0; // アナログ入力 (受信アンプ出力)
// ... 以下ピン定義続く ...
// ISR用変数
int16_t bins[4]; // 4相分のADC蓄積値
uint16_t numSamples = 0;
const uint16_t numSamplesToAverage = 1024;
volatile int16_t averages[4]; // ISR→メインループへの受け渡し用
volatile uint32_t ticks = 0; // システムティックカウンタ
volatile bool sampleReady = false;
// メインループ用変数
int16_t calib[4]; // キャリブレーション値
volatile uint8_t lastctr;
volatile uint16_t misses = 0; // ISR遅延カウント (正常なら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); // ADCサンプルホールド遅延補正
float threshold = 5.0; // 検出閾値 (小さいほど高感度、10程度が実用的)
void setup() {
lcd.begin(16, 2);
pinMode(encoderButtonPin, INPUT_PULLUP);
digitalWrite(T0OutputPin, LOW); pinMode(T0OutputPin, OUTPUT);
digitalWrite(coilDrivePin, LOW); pinMode(coilDrivePin, OUTPUT);
cli(); // 割り込み禁止
// Timer0停止 (Arduinoコアの設定を上書き)
TCCR0B = 0; TIMSK0 = 0; TIFR0 = 0x07;
// ADC設定: Timer1オーバーフローで自動トリガ、チャネル0読み取り
#if USE_3V3_AREF
ADMUX = (1 << ADLAR); // AREFピン(3.3V)を基準、左詰め
#else
ADMUX = (1 << REFS0) | (1 << ADLAR); // AVcc基準
#endif
ADCSRB = (1 << ADTS2) | (1 << ADTS1); // Timer1オーバーフローでトリガ
ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | (1 << ADPS2); // 有効、自動トリガ、分周16 (1MHz)
DIDR0 = 1;
// Timer1設定: 分周1、位相補正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設定: クロックソース=T0ピン、高速PWM、TOP=7、OC0B出力
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);
}
// Timer1オーバーフロー割り込み: ADC読み取り + 4相位相検波蓄積 + ティックカウント
ISR(TIMER1_OVF_vect) {
++ticks;
uint8_t ctr = TCNT0;
int16_t val = (int16_t)(uint16_t)ADCH; // 上位8bitのみ使用
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) {
// キャリブレーションボタン押下時: 現在の位相検波出力を基準値として保存
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 {
// キャリブレーション値を減算
for (int i = 0; i < 4; ++i) averages[i] -= calib[i];
const double f = 200.0;
// 第3高調波キャンセル処理
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;
}
// デバッグ出力
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(" ");
Serial.print(ampAverage,1); Serial.print(' '); Serial.print((int)phaseAverage);
// LCD表示
lcd.setCursor(0,0); lcd.print(" "); lcd.print(ampAverage);
lcd.setCursor(0,1); lbg.drawValue(ampAverage, max_ampAverage);
// 鉄/非鉄判定と音声フィードバック
if (ampAverage >= threshold) {
if (phaseAverage < -20.0) { // 非鉄金属: 負の位相シフト (例: 銅/アルミ -90度、薄いアルミ -30度)
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.write('!'); temp -= (threshold/2); }
}
Serial.println();
}
// ループ周期制御 (約8000ティック = 128ms)
while (ticks - oldTicks < 8000) {}
}
LcdBarGraph ライブラリ
別途ダウンロードが必要です (プレビュー不可)。
回路図

製造プロセス
- 予算に優しいRaspberry Pi赤ちゃん泣き声検知器の自作 - DIYガイド
- Arduinoで作るレトロNumitron管時計:シンプル、信頼性高く、省エネ
- Arduino NanoとMPU-6050で作るリアルタイムジャイロスコープゲーム
- DIY ADXL335地震検知器 - 低消費電力3軸加速度センサープロジェクト
- ArduinoとNokia 5110ディスプレイで自作電圧計:ステップバイステップガイド
- MobBob:Arduino製ロボットをAndroidスマホでシームレスに制御
- Arduino Nanoとロータリーエンコーダーで作る高精度測定ホイール
- Arduino、Yaler、IFTTTでWeb対応IoTゲージを構築する
- シャープGP2Y1010AU0Fセンサーで作る高精度空気質モニター
- Arduino Nanoで作る携帯型ガイガーカウンター
- Arduino NanoとWS2812 LEDストリップで作る1次元ポンゲーム - 完全DIYガイド