====== 아두이노 그대로 따라하기 - 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}}
----