산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

아두이노로 만드는 고감도 금속 탐지기 - 철/비철 금속 판별 기능 포함

부품 및 소모품

부품수량비고
아두이노 나노 R3 (Arduino Nano R3)1
표준 LCD 16x2 (백색/청색)1Adafruit 제품 기준
TL081 연산 증폭기 IC1LT1677, 741 등 대체 가능
스피커 (0.25W, 8옴)1
범용 NPN 트랜지스터1
탐색 코일 (Search coils)2송신/수신 코일 각각 1개
저항, 커패시터 세트1

앱 및 온라인 서비스

프로젝트 소개

이번에는 철(자성체)과 비철(비자성체) 금속을 판별할 수 있는 고감도 금속 탐지기를 만드는 방법을 소개합니다. 비교적 단순한 장치임에도 감도가 만족스러운 수준입니다.

본 프로젝트는 PCBgogo의 후원으로 진행되었습니다.

이 프로젝트는 2013년 아두이노 CC 포럼에서 David Crocker가 발표한 프로젝트의 연장선입니다. 당시 코드는 공개되었으나, 실제로 제작되어 작동하는 증거(사진 또는 영상)를 찾을 수 없어 직접 테스트해보기로 했습니다.

먼저 GitHub에 공개된 기본 코드로 기본 버전을 제작하여 장치의 기능을 확인한 후, 청각 신호(부저음)와 16x2 LCD 디스플레이에 탐지된 물체 종류(철/비철) 및 근접도를 막대 그래프로 표시하는 기능으로 코드를 업그레이드했습니다.

장치는 매우 단순하게 구성되며 몇 가지 부품만 필요합니다:

아두이노로 만드는 고감도 금속 탐지기 - 철/비철 금속 판별 기능 포함

작동 원리: VLF 유도 밸런스 방식

이 장치는 VLF(Very Low Frequency) 유도 밸런스(Induction Balance) 탐지 기술을 사용하며, 두 개의 동일한 코일(송신 코일과 수신 코일)로 구성됩니다. 모든 유도 밸런스 탐지기에서 코일 밸런스는 매우 중요합니다.

포텐쇼미터는 신호의 90도 위상 차 성분(작은 성분)을 제로(Zero) 조정하는 데 사용됩니다. (동위상 성분은 일반적인 IB 탐지기 방식대로 두 코일의 상대적 위치를 조정하여 널링합니다.)

각 코일은 지름 11cm 보빈에 0.5mm² 에나멜 동선 64턴을 D자 형태로 감고, 테이프로 감싼 뒤 알루미늄 호일로 차폐하고 주석 도금 동선으로 묶었습니다. (차폐가 단락 턴처럼 작동하지 않도록 작은 간격을 남겨두었습니다.) 그리고 플라스틱 플레이트에 타이랩으로 고정했습니다.

아두이노로 만드는 고감도 금속 탐지기 - 철/비철 금속 판별 기능 포함

공진 주파수 설정

온라인 계산기를 사용하거나 오실로스코프로 1차 코일-커패시터 회로의 병렬 공진 주파수를 측정합니다. 위 치수대로 제작했다면 정확히 7.64kHz가 나오므로 코드에 직접 값을 입력하면 됩니다. 다른 공진 주파수 값이 나온다면 코드의 다음 부분에서 적절히 조정해야 합니다:

#define TIMER1_TOP (249) // 주파수 미세 조정

성능 테스트 결과

영상에서 보시듯 결과가 놀랍게 좋습니다. 금속이 없을 때 장치는 완벽하게 안정적입니다. 탐지 범위도 꽤 넓어서, 지름 15cm 금속 뚜껑을 30cm 이상 거리에서 탐지합니다. 더 큰 금속 물체는 40-50cm 이상 거리에서도 탐지됩니다. 공중에서 작은 동전도 15cm 거리에서 탐지 가능합니다.

전원은 리튬 배터리 2개를 직렬 연결(7.4V)하여 아두이노 Vin 핀에 공급합니다. 소비 전류가 20mA를 넘지 않아 배터리 수명이 매우 깁니다. 영상에서 장치 제작 전 과정이 자세히 설명되어 있습니다.

이것은 예비 결과일 뿐입니다. 향후 Tx 코일 구동용 파워 MOSFET 트랜지스터를 추가하면 감도를 크게 향상시킬 수 있을 것으로 보이며, 이는 향후 영상에서 테스트하고 소개할 예정입니다.

코드

아두이노 코드

// 유도 밸런스 금속 탐지기
// CPU 16MHz, ADC 클럭 1MHz로 동작. 이 속도에서 ADC 해상도는 8비트로 감소.
// 타이머 1: 시스템 클럭을 약 256으로 나누어 62.5kHz 사각파 생성
// 타이머 0: 타이머 1 출력을 8로 나누어 7.8125kHz 송신 코일 구동 신호 생성
// ADC: 코일 구동 전압 주기당 8개 샘플링, 45도 간격 4개 위상 검출기 구현
// 타이머 2: 이어폰/헤드셋용 톤 생성

#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, 임베디드 ATmega328P 사용 시 0

// 디지털 핀 정의
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;

// 아날로그 핀 정의
const int receiverInputPin = 0;
const int encoderAPin = A1;
const int encoderBpin = A2;

// ISR에서만 사용하는 변수
int16_t bins[4];
uint16_t numSamples = 0;
const uint16_t numSamplesToAverage = 1024;

// ISR과 외부에서 공유하는 변수
volatile int16_t averages[4];
volatile uint32_t ticks = 0;
volatile bool sampleReady = false;

// 외부에서만 사용하는 변수
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; // 낮을수록 높은 감도. 코일 밸런스가 잘 맞으면 10도 사용 가능.

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;
  
  #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;
  
  // 타이머 1 설정
  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);
  
  // 타이머 0 설정
  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_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) {
    // 캘리브레이션 버튼 눌림
    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.write(' '); Serial.print(calib[i]);
      lcd.setCursor(0,1); lcd.print(' '); lcd.print(calib[4]); lcd.print(" ");
    }
    Serial.println();
  } else {
    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 temp = phase1; phase1 = phase2; phase2 = temp; }
    double phaseAverage = ((phase1 + phase2)/2.0) - phaseAdjust;
    if (phase2 - phaseAverage > 180.0) {
      if (phaseAverage < 0.0) phaseAverage += 180.0;
      else phaseAverage -= 180.0;
    }
    
    // 진단용 출력
    Serial.print(misses); Serial.write(' ');
    if (bin0 >= 0.0) Serial.write(' '); Serial.print(bin0, 2); Serial.write(' ');
    if (bin1 >= 0.0) Serial.write(' '); Serial.print(bin1, 2); Serial.write(' ');
    if (bin2 >= 0.0) Serial.write(' '); Serial.print(bin2, 2); Serial.write(' ');
    if (bin3 >= 0.0) Serial.write(' '); Serial.print(bin3, 2); Serial.print(" ");
    Serial.print(amp1, 2); Serial.write(' '); Serial.print(amp2, 2); Serial.write(' ');
    if (phase1 >= 0.0) Serial.write(' '); Serial.print(phase1, 2); Serial.write(' ');
    if (phase2 >= 0.0) Serial.write(' '); Serial.print(phase2, 2); Serial.print(" ");
    
    if (ampAverage >= 0.0) Serial.write(' '); Serial.print(ampAverage, 1); Serial.write(' ');
    lcd.setCursor(0,0); lcd.print(" "); lcd.print(ampAverage);
    lcd.setCursor(0,1); lbg.drawValue(ampAverage, max_ampAverage);
    if (phaseAverage >= 0.0) Serial.write(' '); Serial.print((int)phaseAverage);
    
    // 탐지된 물체 판별 및 알림
    if (ampAverage >= threshold) {
      // 코일 중심선에 수직으로 들었을 때:
      // - 비철 금속: 음의 위상 이동 (두꺼운 구리/알루미늄 -90도, 얇은 알루미늄 -30도)
      // - 철 금속: 0도 또는 작은 양의 위상 이동
      // 위상 이동이 -20도 이하면 비철로 판정
      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.write('!'); temp -= (threshold/2); }
    }
    Serial.println();
  }
  
  while (ticks - oldTicks < 8000) {}
}

LcdBarGraph 라이브러리

다운로드만 가능 (미리보기 없음)

회로도

아두이노로 만드는 고감도 금속 탐지기 - 철/비철 금속 판별 기능 포함

제조공정

  1. 예산 친화적 라즈베리 파이 아기 울음 감지기 DIY 가이드
  2. 아두이노로 만드는 레트로 누미트론 클락: 심플하고 신뢰성 높으며 에너지 효율적
  3. 아두이노 나노와 MPU-6050으로 실시간 자이로스코프 게임 만들기
  4. DIY ADXL335 지진 감지기: 저전력 3축 가속도계 프로젝트
  5. 아두이노와 노키아 5110 LCD로 만드는 DIY 전압계 - 단계별 가이드
  6. MobBob: 아두이노 로봇 직접 만들기, 안드로이드 스마트폰으로 매끄럽게 제어
  7. 아두이노 나노와 로터리 인코더로 만드는 정밀 측정 바퀴
  8. 아두이노, Yaler, IFTTT로 웹 연동 IoT 게이지 만들기
  9. Sharp GP2Y1010AU0F 센서로 정밀 공기질 모니터 만들기
  10. 아두이노 나노로 만드는 휴대용 가이거 계수기