====== 아두이노 그대로 따라하기 - 3.센서 사용하기 ====== 임베디드 장비에 많이 사용되는 각종 센서들을 사용해보고 이들의 특징과 관련 API 에 대해 알아보겠다. ====== 조광센서(LDR) ====== 빛의 양에 따라 저항값이 바뀐다. 아래 그림처럼 회로를 꾸미는데, 스위치 자리에 대신 LDR 센서를 사용한다. {{ :computer:embedded:switch.jpg |}} 빛을 쪼일 수록, LED 불이 환하게 켜지고, 어두울수록 불이 꺼진다. ===== 빛의 양에 따라 LED 깜박거리기 ===== 아래 그림과 같이 회로를 만든다. {{ :computer:embedded:ldr.jpg |}} 코드는 아래와 같다. 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 밝기 설정 ===== 앞에서 만든 회로를 그대로 둔 상태에서 아래 회로를 추가로 만든다. {{ :computer:embedded:led1.jpg |}} 아래는 코드다. 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) 저항값은 계산으로 구할 수 있다(아래 그림 참조). {{ :computer:embedded:temp.jpg |}} 온도 계산은 Steinhart-Hart 방정식을 이용한다(아래 그림 참조). {{ :computer:embedded:temp2.jpg |}} 여기서 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' ===== 회로도 ===== {{ :computer:embedded:temp.png |}} ===== 소스코드 ===== 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) 부저 ====== 흔히 스피커 또는 압전 센서(진동을 감지하는)로 알려져 있다. 센서의 정밀도에 따라 가격차가 크다. 여기서는 피에조 센서를 이용해서 기본적인 동작을 확인한다. ===== 소리를 들어보자 ===== ==== 회로도 ==== {{ :computer:embedded:piezo.fzz |}} ==== 소스코드 ==== void setup() { pinMode(7, OUTPUT); } void loop() { digitalWrite(7,1); delayMicroseconds(1137); digitalWrite(7,0); delayMicroseconds(1137); } 프로그램을 올리는 순간, 깜짝 놀랄지도 모르겠다. 삐~ 소리가 들린다면, 정상이다. ===== 진동을 감지해보자 ===== 진동을 감지할 때마다 LED 가 켜지도록 해보자. 마이크 용도로도 사용이 가능하다고 해서 피에조 센서를 가까이 두고 소리를 질러보았다. 하지만, 꿈쩍도 하지 않았고, 결국 손으로 피에조 센서를 때리자, 감지한 센서 값이 나왔다. ==== 회로도 ==== {{ :computer:embedded:piezo2.fzz |}} ==== 소스코드 ==== 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); } } ===== 소리 만들기 ===== 앞에서 단순히 삐~ 소리만 들었다면, 간단하게 소리를 만들어보자. ==== 회로도 ==== {{ :computer:embedded:melody.fzz |}} ==== 소스코드 ==== 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번 핀의 주파수 출력을 중지함 ===== 멜로디 만들기 ===== 학교종이 땡땡땡을 들어보자. 피에조 부저로 들어가는 주파수를 조정함으로서 여러가지 음계를 나타낼 수 있다. {{ :computer:embedded:d1.jpg |}} {{ :computer:embedded:d2.jpg |}} {{ :computer:embedded:d3.jpg |}} {{ :computer:embedded:d4.jpg |}} 각 음계를 정의한 헤더 파일(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 이 시작할 때의 소리 파일이다. {{ :computer:embedded:win31.mp3 |}} 들어보면 정말 단순하다. ==== MP3 to RAW 변환 ==== 먼저 이 파일을 RAW 파일 형태로 바꿔야 한다. 이를 위해서 프로그램이 필요한데, audacity 라는 프로그램을 사용한다. 'apt-get install' 명령어로 간단하게 설치할 수 있다. 이 프로그램을 실행시켜, {{ :computer:embedded:win31.mp3 |}} 파일을 연다. '파일 -> 내보내기' 선택한다. 파일 타입을 '기타 압축 파일' 을 선택하고 아래 '옵션' 을 누른다. 비압축 내보내기 설정에서 '헤더 : 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 #include #include #include #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 로 들었던 소리가 어렴풋이 나마 들릴 것이다. ---- {{indexmenu>:#1|skipns=/^(wiki|etc|diary|playground)$/ skipfile=/^(todays|about|guestbook)$/ nsort rsort}} ----