[아두이노] [강좌] 25. tone()/noTone() 함수 (2) - 부저 실습
이번 시간엔 tone() 함수와 noTone() 함수를 이용해서 부저로 멜로디를 연주하는 실습을 해보자.
부저에는 굉장히 여러 가지가 있는데, 연결 핀이 2개인 것도 있고 3개인 것도 있다. 3개인 경우 Vcc, GND, Data 핀으로 이루어져 있을 것이고, 2개인 경우 GND, Data 핀으로 이루어진다.
위의 센서는 핀이 3개네. 표시된 대로 Vcc 핀은 5V에, GND 핀은 GND에 SIG라고 표시된 핀은 8번에 꽂자. 8번 맘에 안 들면 아무데나 꽂아도 됨. 아래 그림의 부저와 본인이 가진 부저가 다르겠지만 핀 연결은 대부분 비슷하니까 참고만 하자. (fritzing 프로그램에 부저 모듈이 저거밖에 없어서.ㅜㅜ)
그리고 스케치 툴의 “파일→예제→02.Digital→toneMelody” 예제 열기.
toneMelody.ino |
#include "pitches.h"
int melody[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4}; int noteDurations[] = {4, 8, 8, 4, 4, 4, 4, 4};
void setup() {: for (int thisNote = 0; thisNote < 8; thisNote++) { int noteDuration = 1000 / noteDurations[thisNote]; tone(8, melody[thisNote], noteDuration);
int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes);
noTone(8); } }
void loop() { } |
“#include” 구문은 지정한 헤더 파일을 컴파일 하기 전 소스에 추가하는 역할을 한다. 그럼 pitches.h 파일은 어디에 있나? 스케치 툴의 아래 부분을 보면 하나의 탭이 더 있는 것을 볼 수 있다.
"#include “pitches.h”" 구문을 사용했기 때문에 탭이 생긴 건 아니고, toneMelody.ino 파일과 같은 폴더 내에 pitches.h 파일이 있기 때문에 탭이 추가되어 나타난 것이다. #include 구문으로 추가할 수 있는 헤더 파일은 소스 파일(.ino)과 같은 위치에 있거나 아두이노에서 특별히 지정된 경로(libraries 폴더나 core 폴더 등)에 존재하는 파일이어야 하며, 만일 다른 경로에 있는 파일을 추가하고 싶다면 직접 경로를 적어줘야 에러가 발생하지 않는다.
헤더 파일(.h)에는 주로 클래스가 정의되어 있거나 함수가 선언되어 있는데, 다른 파일에 정의된 함수나 클래스를 사용하기 위해서는 반드시 그 함수나 클래스가 선언된 헤더 파일을 “#include” 구문으로 추가해줘야 한다. (digitalWrite() 함수 등 기본 함수는 내부적으로 이미 추가되어 있으므로 따로 헤더 파일을 찾지 않아도 된다.)
pitches.h 탭을 클릭 해보자.
뭔가 “#define” 해서 엄청 많이 있다. “#define” 구문도 처음 나왔나? ㅜㅜ?
“#define” 구문은 키워드를 지정하는 구문이라고 보면 된다. 하나만 살펴 볼까?
#define NOTE_C4 262 |
위 구문은 262이라는 값을 “NOTE_C4”라는 키워드로 사용하겠다는 뜻이다. 소스 상에서 “NOTE_C4”라는 키워드를 사용하면 그 값은 실제로 262를 의미한다. 예제 소스에서 “NOTE_C4” 키워드가 사용된 부분을 찾아보자.
int melody[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4}; |
melody 배열은 int 형의 배열이다. 배열의 첫 번째 값으로 ‘NOTE_C4’ 키워드가 사용되어 실제로는 262가 저장된다. 즉 위 배열의 의미를 풀어 쓰면 다음과 같다.
int melody[] = {262, 196, 196, 220, 196, 0, 247, 262}; |
아니, 이렇게 알아보기 쉬운 숫자를 놔두고 왜 키워드를 사용해!?
이유는 두 가지이다. 첫째, 이 값이 무엇을 의미하는지 코드 상에서 직관적으로 알 수 있게 하기 위함이고. 둘째, 똑 같은 값(변하지 않는 상수 값이어야 함)이 여러 번 사용될 경우 나중에 값이 수정되어야 할 때 쉽게 수정할 수 있게 하기 위함이다. #define 구문에 정의된 값만 고치면 되니까.
다시 소스로 돌아와서, 업로드 해보자.
굉장히 익숙하지만 제목을 들어본 기억이 없는 멜로디가 흘러나온다.
setup() 함수에서 tone() 함수를 사용하고 loop() 함수에서는 아무 동작도 하지 않으므로 전원을 넣은 후 한 번만 연주되고 더 이상 연주되지 않는다. 아쉽구로.
소스를 살펴보자.
int melody[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4}; int noteDurations[] = {4, 8, 8, 4, 4, 4, 4, 4}; |
NOTE_C4 키워드에 저장된 262라는 값은 사실 4옥타브 ‘도’ 음계의 주파수 값이다.
부저 센서는 주파수에 따라 음계가 다른 소리가 출력되는데, pitches.h 파일에 각 음계에 대한 주파수 값을 키워드로 지정해둔 것.
그래서 melody[] 배열에 출력할 음계의 주파수 값을 순서대로 저장했다. 그리고 noteDurations[] 배열에는 몇 분 음표인가를 저장하고 있네. 4분 음표, 8분 음표.
그리고 setup() 함수에서 for() 문을 이용해 각 음계를 차례로 출력한다.
for (int thisNote = 0; thisNote < 8; thisNote++) { int noteDuration = 1000 / noteDurations[thisNote]; tone(8, melody[thisNote], noteDuration); int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes); noTone(8); } |
noteDurations[] 배열에는 4분 음표, 8분 음표가 저장되어 있었고, 정수형 변수 noteDuration 값에는 실제로 출력될 시간이 계산되어 저장된다. 그리고는 tone() 함수를 이용해서 8번 핀에 thisNote 번째의 주파수(음계)를 계산된 시간만큼 출력.
음계 사이에 약간의 딜레이를 주기 위해 noteDuration의 1.3배만큼 delay() 하고, noTone() 함수를 호출한다.
사실 tone() 함수에서 설정한 시간보다 delay()에서 지연된 시간이 길기 때문에 자동으로 출력이 중단되어 noTone() 함수를 다시 호출할 필요는 없는데, 예제에서는 호출했네?
음계를 출력하기 위한 시간과 중간 딜레이 시간을 계산하기 위해 변수가 사용되고, 계산식이 사용되긴 했지만 사실 내용은 아주 쉬운 예제 소스다. 순서대로 주파수를 변경해가며 파형을 출력하는 예제. 끝!
하지만 이거만 하고 끝내기는 아쉽고, toneMultiple 예제나 toneKeyboard 예제는 다른 필요한 애들이 많으니까, 우리는 스위치 하나만 추가해서 스위치가 눌리면 멜로디가 계속 반복해서 나오고, 한번 더 누르면 꺼지는 예제를 만들어 볼까.
toneMelody 예제와 지난 강좌에서 배웠던 인터럽트를 활용해보자.
아두이노 보드에 스위치 추가!
위치만 바뀌었을 뿐 부저의 연결은 동일하며, 스위치가 추가되었다.
소스를 수정해볼까.
toneMelody 예제에서 melody[] 배열과 noteDurations[] 배열은 그대로 사용하고, 스위치를 위한 핀 번호와 멜로디 On/Off 상태를 저장하기 위한 변수를 추가하자.
int swPin = 2; boolean bMelodyOn = false; |
그리고 setup() 함수에서 멜로디가 출력되는 부분을 loop()로 옮기고 swPin 모드와 인터럽트를 설정하자. 인터럽트를 사용할 테니까.
void setup() { pinMode(swPin, INPUT_PULLUP); attachInterrupt(0, swInterrupt, FALLING); } |
그리고 swInterrupt() 함수를 정의해줘야겠지. 채터링 방지를 위해 delayMicroseconds() 구문을 추가했다. 2밀리초 정도니까 괜찮을거야.. 아마도..;;;
void swInterrupt() { delayMicroseconds(2000); if(digitalRead(swPin) != LOW) return ;
if(bMelodyOn) bMelodyOn = false; else bMelodyOn = true; } |
bMelodyOn 변수의 값을 확인해서 true면 false로, false면 true로 바꿔주고 있다. 멜로디가 나오는 중이면 멜로디를 끌 거고, 안 나오는 중이면 켤 거니까.
그리고 loop() 함수. setup()에 있었던 for() 구문을 loop() 함수로 옮기는데, 단 조건이 붙어야 한다. bMelodyOn 변수가 true일 경우에만 동작하도록.
void loop() { if(bMelodyOn) { for (int thisNote = 0; thisNote < 8; thisNote++) { int noteDuration = 1000 / noteDurations[thisNote]; tone(8, melody[thisNote], noteDuration);
int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes);
if(bMelodyOn == false) break; } } } |
for() 반복문의 마지막에 bMelodyOn 값을 확인해서 false일 경우, 즉 bMelodyOn이 true인 상태에서 스위치가 한번 더 눌려 false로 바뀐 경우 “break;” 하고 있다.
“break”는 break를 포함하고 있는 가장 가까운 반복문을 종료하는 명령어이다. 유의할 점은 반복문이 중첩되어 사용 중인 경우 break 구문과 가장 가까운 반복문만 종료된다는 점.
아무튼, 그래서 스위치가 한번 더 눌리면 반복문을 종료하고 멜로디 출력을 중단한다.
업로드 한 후 실행해보자.
아무 소리도 안 난다. 당연하지. 스위치를 눌러보자.
띠↗디↘디↘디↗디↘ (쉬고) 띠↗디↗ ♪♬~ 띠↗디↘디↘디↗디↘ (쉬고) 띠↗디↗ ♪♬~ 띠↗디↘디↘디↗디↘ (쉬고) 띠↗디↗ ♪♬~ 띠↗디↘디↘디↗디↘ (쉬고) 띠↗디↗ ♪♬~
계속 나온다. 스위치를 한번 더 눌러보자. 멈춘다.
짝짝짝~
이 것이 멜로디 박스의 간단한 버전이다. 여기에 멜로디 배열을 여러 개 더 추가하고(물론 다른 멜로디로), 스위치를 추가하거나 아니면 멜로디 번호를 저장하는 변수를 추가하여 더 많은 멜로디가 나오는 멜로디 박스를 만들 수 있을 것이다.
이건 여러분의 몫으로 남겨두면서, 오늘 실습은 여기서 끝~!
진짜 끝.