임베디드 장비에 많이 사용되는 각종 센서들을 사용해보고 이들의 특징과 관련 API 에 대해 알아보겠다.
조광센서(LDR)
빛의 양에 따라 저항값이 바뀐다. 아래 그림처럼 회로를 꾸미는데, 스위치 자리에 대신 LDR 센서를 사용한다.
빛을 쪼일 수록, LED 불이 환하게 켜지고, 어두울수록 불이 꺼진다.
빛의 양에 따라 LED 깜박거리기
아래 그림과 같이 회로를 만든다.
코드는 아래와 같다.
const int LED = 13; int val = 0; // 아날로그 입력 핀(A0) 지정 void setup() { pinMode(LED, OUTPUT); } void loop() { val = analogRead(0); ---- 1 digitalWrite(LED, HIGH); delay(val); digitalWrite(LED, LOW); delay(val); }
아두이노의 회로기판을 보면, 아날로그 입력 핀(A0~A6)이 준비되어 있다. 이들 핀들은 핀에 전압이 있고 없고를 알려줄 뿐 아니라, 전압이 있는 경우에는 그 값을 읽어 알려주는 기능을 가진다. analogRead() 함수를 통해 가능하다.
- A0 핀의 전압 값을 읽어들인다.
LDR 센서는 밝을 수록, 저항값이 작아지고 어두울 수록 커진다. 따라서 밝다면, 전압이 커질테고(val 값이 높음), 어둡다면, 전압이 작아질 것이다(val 값이 낮음).
결국, 밝아질수록 깜박이는 횟수가 적어지고, 어두울수록 깜박임이 심해진다.
아날로그 입력 값으로 정한 밝기만큼 LED 밝기 설정
앞에서 만든 회로를 그대로 둔 상태에서 아래 회로를 추가로 만든다.
아래는 코드다.
const int LED = 9; int val = 0; void setup() { pinMode(LED, OUTPUT); } void loop() { val = analogRead(0); analogWrite(LED, val/4); ---- 1 // digitalWrite(LED, val/4); ---- 2 delay(10); }
밝을 수록, LED 도 밝아지고, 어두울 수록, LED 도 어두워진다.
- LDR 센서로 부터 읽은 값만큼 아날로그 출력을 내보낸다.
- 주석을 풀면 어떻게 될까? 아날로그와 디지털 출력의 차이는 디지털은 High 또는 Low 밖에는 지정할 수 없다는 점이고, 아날로그는 0~255 값을 넣을 수 있다는 것이다.
온도센서
온도에 따라 저항 값이 변하는 센서를 이용해서 온도를 측정해보겠다. 온도를 측정하는 여러가지의 센서들이 있지만 여기서는 정확도는 떨어지지만 가장 저렴하기로 알려진 NTC-10KD-5J 를 사용한다.
일반적으로 망간, 니켈, 코발트, 철, 구리 등의 천이금속 산화물을 2~4 종 혼합하여 어떤 형상으로 성형한 후에 1100 ~ 1400 도의 고온 소결한 복합 산화물 반도성 소자이다.
NTC(Negative Temperature Coefficient Thermistor) 서미스터는 온도가 높아지면 저항이 낮아진다.
온도센서 회로의 이해
온도센서는 사용하긴 전에 알아두어야 할 사항이 있다. 단순히 저항값을 읽어서는 우리가 원하는 섭씨 온도를 바로 알수는 없다.
온도를 측정하는 방법은 특정 온도 시에 저항값이 얼마 인지를 기준으로 특정 온도에서 저항값이 변하면 이 비율만큼 온도를 측정하는 방식이다.
NTC-10KD-5J 의 저항값은 25도 에서 10K 옴이다. 온도회로는 센서와 센서의 저항과 같은 10K 옴을 직렬로 연결하고 연결된 부분에서 출력전압을 이용하는 전압 분배회로이다. R1 = 10,000 옴, 입력전압(V in) = 5V, 출력전압(V out) 은 K-아두이노의 ADC 에 의해 측정하여 알 수 있고, 서미스터(R2) 저항값은 계산으로 구할 수 있다(아래 그림 참조).
온도 계산은 Steinhart-Hart 방정식을 이용한다(아래 그림 참조).
여기서 T는 절대온도(K), R은 측정된 저항 a,b,c는 서미스터 제조업체에서 제공한 상수이다. 여기서 사용하는 파라미터 a=0.001129148, b=0.000234125, c=8.76741E-08 이다.
Steinhart-Hart 방정식에 의해 계산된 온도(T)는 절대(Kelvin)온도이며 간단한 식에 의하여 섭씨(Celcius)온도로 변환할 수 있다.
'섭씨(Celcius)온도 = 절대(Kelvin)온도 - 273.15'
회로도
소스코드
double r1 = 10000.0; // 직렬 연결 저항 double vin = 5.0; // 입력 전압 double A = 0.001129148; double B = 0.000234125; double C = 0.0000000876741; double adcraw; // ADC 값 double vout; // V out double rth; // 서미스터의 저항 double kel; // 절대온도 double cel; // 섭씨온도 void setup() { Serial.begin(9600); } void loop() { adcraw = analogRead(0); // 센서의 아날로그 값을 읽어들임 vout = adcraw / 1024 *vin; // 온도센서의 ADC 값을 이용하여 센서의 출력전압을 계산 rth = (r1 * vout) / (vin - vout); // 센서회로의 직렬저항, 입력전압, 출력전압을 이용하여 서미스터의 저항 계산 kel = ste(rth); // 방정식에 서미스터의 저항값을 넘겨 절대온도 계산 cel = kel - 273.15; // 절대온도를 섭씨온도로 변환하는 식 delay(100); Serial.println(cel); } double ste(double r) // 방정식에 의해 절대 온도 계산 사용자 함수 { double logr = log(r); double logr3 = logr * logr * logr; return 1.0 / (A+B * logr + C * logr3); // 방정식의 계산결과를 반환함 }
온도는 약 +- 0.5 도의 정밀도를 가진다.
피에조(Piezo) 부저
흔히 스피커 또는 압전 센서(진동을 감지하는)로 알려져 있다. 센서의 정밀도에 따라 가격차가 크다.
여기서는 피에조 센서를 이용해서 기본적인 동작을 확인한다.
소리를 들어보자
회로도
소스코드
void setup() { pinMode(7, OUTPUT); } void loop() { digitalWrite(7,1); delayMicroseconds(1137); digitalWrite(7,0); delayMicroseconds(1137); }
프로그램을 올리는 순간, 깜짝 놀랄지도 모르겠다. 삐~ 소리가 들린다면, 정상이다.
진동을 감지해보자
진동을 감지할 때마다 LED 가 켜지도록 해보자. 마이크 용도로도 사용이 가능하다고 해서 피에조 센서를 가까이 두고 소리를 질러보았다. 하지만, 꿈쩍도 하지 않았고, 결국 손으로 피에조 센서를 때리자, 감지한 센서 값이 나왔다.
회로도
소스코드
const int sensorPin = 0; const int ledPin = 13; const int threshold = 1; // 감도값을 너무 크게 주면 제대로 불이 켜지지 않는다 void setup() { pinMode(ledPin, OUTPUT); Serial.begin(9600); } void loop() { int value = analogRead(sensorPin); Serial.println(value); if(value >= threshold) { digitalWrite(ledPin, HIGH); delay(200); digitalWrite(ledPin, LOW); } }
소리 만들기
앞에서 단순히 삐~ 소리만 들었다면, 간단하게 소리를 만들어보자.
회로도
소스코드
void setup() { pinMode(6, INPUT); } void loop() { if(digitalRead(6) == 1) { tone(7, 262,250); // D7 에 주파수 262Hz 를 250ms 동안 출력한다. '도' 에 해당하는 주파수 delay(325); // 여러 소리를 출력하는 경우 음의 구분을 위해 대기하는 시간, 보통 주기의 130% 를 대기한다. 250(duration) * 1.3 = 325 tone(7, 330,250); delay(325); tone(7, 392,500); delay(325); } noTone(7); delay(500); }
tone(pin, frequence, duration)
지정한 주파수를 지정한 시간동안 출력하는 함수. 출력파형은 50% 듀티의 구형파이다. tone(pin, frequence) 형태도 가능
- pin : 구형파를 출력할 핀(포트번호)
- frequence : 출력할 주파수 Hz
- duration : 출력되는 기간(ms -1/1000c 초)
noTone(pin)
tone() 함수에 의해 발생한 주파수를 멈추게 한다.
- pin : 구형파 출력을 중단할 핀(포트번호)
noTone(7) : 7번 핀의 주파수 출력을 중지함
멜로디 만들기
학교종이 땡땡땡을 들어보자.
피에조 부저로 들어가는 주파수를 조정함으로서 여러가지 음계를 나타낼 수 있다.
각 음계를 정의한 헤더 파일(pitches.h)을 추가해야 한다. 학교 종이 땡땡땡의 음계는 다음과 같다.
'솔솔라라솔솔미 솔솔미미레 솔솔라라솔솔미 솔솔레미도'
솔 | NOTE_G4 |
라 | NOTE_A4 |
미 | NOTE_E4 |
레 | NOTE_D4 |
도 | NOTE_C4 |
쉼표 | PRESET(=0) |
소스코드
#include "pitches.h" // 음계 헤더 파일 #define PRESET 0 // 쉼표 음계 지정 int melody[] = {NOTE_G4,NOTE_G4,NOTE_A4,NOTE_A4,NOTE_G4,NOTE_G4,NOTE_E4, NOTE_G4,NOTE_G4,NOTE_E4,NOTE_E4,NOTE_D4, PRESET, NOTE_G4,NOTE_G4,NOTE_A4,NOTE_A4,NOTE_G4,NOTE_G4,NOTE_E4, NOTE_G4,NOTE_G4,NOTE_D4,NOTE_E4,NOTE_C4,PRESET}; // 박자 수를 저장하는 배열, 음표와 쉼표의 박자 수를 32분 음표를 1로 하여 그 배수로 입력한다 int durations[] = { 8,8,8,8,8,8,16, 8,8,8,8,24,8, 8,8,8,8,8,8,16, 8,8,8,8,24,8}; // 음표 : 8 = 4분 음표/쉼표, 16 = 2분 음표/쉼표, 24 = 점2분 음표/쉼표 int thisNote; //배열 변수에서 현재의 음 Index 값을 나타내는 변수 int dur; // 출력되는 시간을 저장하는 변수 int pauseNotes; // 음 구별을 위한 대기시간 변수 void setup() { pinMode(6, INPUT); } void loop() { if(digitalRead(6) == 1) { for(thisNote = 0; thisNote < 26; thisNote++) // 음표의 수 만큼 반복 { // 현재 음의 출력되는 시간을 계산. 32분 음표는 1초를 32 등분 한 것이므로, 음 출력시간 = 1000ms/32 * 박자수로 계산한다 dur = 1000/32 * durations[thisNote]; // 음표 길이계산 = 1000 / 32 * 32 음표의 배수 tone(7, melody[thisNote], dur); // 음 출력 pauseNotes = dur * 1.3; // 구분을 위한 시간 = 음표 * 1.3 delay(pauseNotes); noTone(7); // 소리 중지 } } }
MP3 파일 재생(?)하기
재생(?) 이라고 표현한 것은 있는 그대로의 MP3 파일을 재생하지는 못하기 때문이다. 아두이노가 처리할 수 있는 한계가 있고, 이를 출력할 수 있는 한계가 분명히 있다.
현재 내가 가진 것은 아두이노와 피에조 부저 뿐이다. 이런 열악한 상황에서 MP3 파일을 재생하기 위해서는 몇 가지 조치가 필요하다. 우선 아두이노가 처리할 수 있는 수준으로 눈높이를 낮춰야 한다.
실제 몇가지 MP3 파일을 가지고 테스트해본 결과, 음질이 좋을 수록, 파일 크기가 클수록, 컴파일 후 아두이노 자체에 업로드가 안되는 문제가 발생했다.
따라서 여기서 테스트 해볼 파일은 옛날 윈도우3.1 이 시작할 때의 소리 파일이다.
들어보면 정말 단순하다.
MP3 to RAW 변환
먼저 이 파일을 RAW 파일 형태로 바꿔야 한다. 이를 위해서 프로그램이 필요한데, audacity 라는 프로그램을 사용한다. 'apt-get install' 명령어로 간단하게 설치할 수 있다.
이 프로그램을 실행시켜,
파일을 연다.
'파일 → 내보내기' 선택한다. 파일 타입을 '기타 압축 파일' 을 선택하고 아래 '옵션' 을 누른다. 비압축 내보내기 설정에서 '헤더 : RAW(header-less)', '인코딩 : Unsigned 8 bit PCM' 을 선택한다.
변환을 하면, 'win31.raw' 파일이 생성된다.
RAW to C 변환
이제 C 코드로 변환할 차례다. 이를 위해 이번에는 프로세싱(processing)을 사용해야 한다.
프로세싱을 실행시켜, 아래의 코드를 입력한다.
byte pcmData[] = loadBytes("win31.raw"); String header = ""; header += "const int SOUNDDATA_LENGTH = " + pcmData.length; header += ";\n\n"; header += "const unsigned char soundData[] PROGMEM = {\n"; header += " "; for (int i = 0; i < pcmData.length; i++) { header += pcmData[i] + ", "; if(i % 16 == 15) { header += "\n"; header += " "; } } header += "\n"; header += "};\n"; saveBytes("PCMData.h", header.getBytes());
그리고 저장한다. 파일이름은 'pcm' 으로 하겠다. 기본 저장 경로는 'sketchbook/pcm/pcm.pde' 이다. 앞서 만든 win31.raw 파일을 이곳으로 복사한다.
이제 실행하자. 'RUN(→)' 버튼을 누르자. 아무 일도 안일어나는 것 같지만, 10초간 기다리자. 작은 정사각형 창이 떳다면, 성공이다. sketchbook/pcm 디렉토리에 'PCMData.h' 파일이 생성된 것을 볼 수 있다.
이 파일을 보면, 수많은 배열 변수로 선언된 것을 볼 수 있다.
const int SOUNDDATA_LENGTH = 20896; const unsigned char soundData[] PROGMEM = { -128, 127, -128, 127, -128, 127, -128, -128, 127, -128, 127, -128, 127, -128, 127, -128, -128, -128, -128, 127, -128, 127, -128, -128, -128, -128, -128, 127, -128, -128, 127, -128, 127, -128, -128, -128, 127, -128, 127, -128, -128, -128, -128, -128, -128, 127, -128, 127, -128, 127, -128, -128, -128, -128, 127, -128, 127, -128, 127, -128, -128, 127, 127, -128, 127, -128, 127, -128, -128, 127, -128, -128, 127, -128, 127, -128, 127, -128, -128, -128, -128, 127, -128, -128, -128, -128, -128,
아두이노 IDE 에 입력
아두이노 IDE 를 실행시킨다. 먼저 아래의 코드를 입력한다.
#include <stdint.h> #include <avr/interrupt.h> #include <avr/io.h> #include <avr/pgmspace.h> #define SAMPLE_RATE 8000 #include "PCMData.h" const int buttonPin = 8; const int ledPin = 13; int speakerPin = 11; volatile uint16_t sample; byte lastSample; int lastButtonState = LOW; // This is called at 8000 Hz to load the next sample. ISR(TIMER1_COMPA_vect) { if (sample >= SOUNDDATA_LENGTH) { if (sample == SOUNDDATA_LENGTH + lastSample) { stopPlayback(); } else { // Ramp down to zero to reduce the click at the end of playback. OCR2A = SOUNDDATA_LENGTH + lastSample - sample; } } else { OCR2A = pgm_read_byte(&soundData[sample]); } ++sample; } void startPlayback() { digitalWrite(ledPin, HIGH); // Set up Timer 2 to do pulse width modulation on the speaker // pin. // Use internal clock (datasheet p.160) ASSR &= ~(_BV(EXCLK) | _BV(AS2)); // Set fast PWM mode (p.157) TCCR2A |= _BV(WGM21) | _BV(WGM20); TCCR2B &= ~_BV(WGM22); // Do non-inverting PWM on pin OC2A (p.155) // On the Arduino this is pin 11. TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0); TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0)); // No prescaler (p.158) TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10); // Set initial pulse width to the first sample. OCR2A = pgm_read_byte(&soundData[0]); // Set up Timer 1 to send a sample every interrupt. cli(); // Set CTC mode (Clear Timer on Compare Match) (p.133) // Have to set OCR1A *after*, otherwise it gets reset to 0! TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12); TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10)); // No prescaler (p.134) TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10); // Set the compare register (OCR1A). // OCR1A is a 16-bit register, so we have to do this with // interrupts disabled to be safe. OCR1A = F_CPU / SAMPLE_RATE; // 16e6 / 8000 = 2000 // Enable interrupt when TCNT1 == OCR1A (p.136) TIMSK1 |= _BV(OCIE1A); lastSample = pgm_read_byte(&soundData[SOUNDDATA_LENGTH-1]); sample = 0; sei(); } void stopPlayback() { // Disable playback per-sample interrupt. TIMSK1 &= ~_BV(OCIE1A); // Disable the per-sample timer completely. TCCR1B &= ~_BV(CS10); // Disable the PWM timer. TCCR2B &= ~_BV(CS10); digitalWrite(ledPin, LOW); digitalWrite(speakerPin, LOW); } void setup() { pinMode(buttonPin, INPUT); pinMode(ledPin, OUTPUT); pinMode(speakerPin, OUTPUT); // startPlayback(); } void loop() { // while (true); int buttonState = digitalRead(buttonPin); if(lastButtonState == LOW && buttonState == HIGH) { startPlayback(); } lastButtonState = buttonState; delay(10); }
이제 앞서 생성했던, PCMData.h 파일을 추가한다. 추가하는 방법은 '스케치 → 파일추가' 를 선택하면 된다. 이제 컴파일하고 아두이노에 다운로드 하자.
스위치 버튼을 누르면, win31.mp3 로 들었던 소리가 어렴풋이 나마 들릴 것이다.