아두이노에 DFplayer 모듈을 이용하여 사운드 기능을 추가하게 되면 말하는 알람시계 또는 인체 감지센서를 이용한 안내 방송, MP3 플레이어등 아두이노를 이용하여 할 수있는 기능성을 확장하게 된다.
이글에서는 DFplayer 모듈의 특성을 살펴보고 연결 및 테스트 방법을 알아보자.
예제에 사용된 DFPLAYER 구입처: 알리익스프레스
https://s.click.aliexpress.com/e/_DDFgoR5
Part 1. DFplayer의 주요 기능과 테스트
DFplayer 메뉴얼
DFplayer 특징
Supports MP3 and WAV decoding. // mp3 파일과 wav 파일 재생
Supports FAT16 and FAT32 file system. // SD카드 포멧 유형 : FAT16 또는 FAT32
24-bit DAC output and supports dynamic range 90dB and SNR 85dB. // 이어폰 잭 연결
Supports AD key control mode and UART RS232 serial control mode. // 로컬 버튼 이용 제어와 시리얼 통신 이용제어
Supports maximum 32GB micro SD card and 32GB USB flash drive. // 저장장치로 SD카드나 USB 연결 flash drive 사용
Supports maximum 3000 audio files in the root directory of the storage device. // 음원 저장 방식에 따라 가능
Supports maximum 99 folders, and each folder can store 3000 audio files // 음원 저장 방식에 따라 가능
(only first 255 files is valid when the serial command is sent to play one of the audio files in the folder).
Supports advertisement insertion. // 음원 재생중 끼워넣기 재생, 음원 저장 방식에 따라 가능
Supports random playback. // 무작위 재생 -> 같은곡 반복 가능성 높다 (순수하게 랜덤 값으로 재생되는 듯함)
Built-in a 3 watts amplifier that can direct drive a 4ohms/8ohms 3 watts speaker. // 3와트 증폭기 내장
30 levels adjustable volume, and 6 levels adjustable EQ. // 볼륨 조절 30단계, 6가지 EQ 모드
저장 방식
DFplayer에서 사운드 파일을 재생을 제어하기위해 저장규칙을 잘 이해하고 그 규칙에 맞게 사운드 파일을 저장장치(SD/USB)에 저장해야만 정상적으로 제어 할 수 있게 되며, 사운드 파일의 재생은 저장장치에 저장된 파일의 트랙 번호에 의해 제어된다.
1. 폴더 규칙
A. 저장방식 1
폴더명: 01 ~ 99까지 각 폴더별 파일갯수 1 ~ 255
파일명: 숫자 3자리 001 ~ 255 반드시 포함하는 파일명으로, 최대 파일갯수 255개 까지 각 폴더에 저장된 파일 인식
예) 폴더 파일명
01 / 001.mp3, 002.wav, 003 시크릿a.mp3,,,,,, 255 yayayat.mp3
02 / 001.mp3, 002.wav, 003 시크릿a.mp3,,,,,, 255 yayayat.mp3
B. 저장방식 2 (저장방식 1에 추가로 ADVERT 폴더 사용)
폴더명: 01 ~ 99까지 각 폴더별 파일갯수 1 ~ 255
파일명: 숫자 3자리 001 ~ 255 반드시 포함하는 파일명으로, 최대 파일갯수 255개 까지 각 폴더에 저장된 파일 인식
ADVERT 폴더: mp3 또는 wave파일의 이름은 4자리 이하 숫자로만 저장되어야만 한다. 1 ~ 3000
예) 폴더 파일명
01 / 001.mp3, 002.wav, 003 시크릿a.mp3,,,,,, 255 yayayat.mp3
02 / 001.mp3, 002.wav, 003 시크릿a.mp3,,,,,, 255 yayayat.mp3
ADVERT / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
C. 저장방식 3
mp3 폴더 사용: 저장방식 1의 01 ~ 99 폴더 대신 mp3 폴더 하나만 사용하고 사운드 파일 3000개 제어 가능
mp3 또는 wave파일의 이름은 4자리 이하 숫자로만 저장되어야만 한다.
예) 폴더 파일명
mp3 / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
D. 저장방식 4 (저장방식 3에 추가로 ADVERT 폴더 사용)
mp3 폴더 사용: 저장방식 1의 01 ~ 99 폴더 대신 mp3 폴더 하나만 사용하고 사운드 파일 3000개 제어 가능
mp3 또는 wave파일의 이름은 4자리 이하 숫자로만 저장되어야만 한다.
ADVERT 폴더: mp3 또는 wave파일의 이름은 4자리 이하 숫자로만 저장되어야만 한다. 1 ~ 3000
예) 폴더 파일명
mp3 / 0001.mp3/wav, 0002.mp3/wav,0003 시크릿a.mp3/wav, .......3000.mp3/wav.
ADVERT / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
E. 저장방식 5
폴더명: 01 ~ 15, 각 폴더별 파일갯수 1 ~ 3000
파일명: 숫자 4자리 0001 ~ 3000 반드시 포함하는 파일명으로, 최대 파일갯수 3000개 까지 각 폴더에 저장된 파일 인식
예) 폴더 파일명
01 / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
02 / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
F. 저장방식 6 (저장방식 5에 추가로 ADVERT 폴더 사용)
폴더명: 01 ~ 15, 각 폴더별 파일갯수 1 ~ 3000
파일명: 숫자 4자리 0001 ~ 3000 반드시 포함하는 파일명으로, 최대 파일갯수 3000개 까지 각 폴더에 저장된 파일 인식
ADVERT 폴더: mp3 또는 wave파일의 이름은 4자리 이하 숫자로만 저장되어야만 한다. 1 ~ 3000
예) 폴더 파일명
01 / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
02 / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
ADVERT / 0001.mp3/wav, 0002.mp3/wav, 0003 시크릿a.mp3/wav, .......3000.mp3/wav.
2. 트랙 번호의 지정
DFplayer에서 사운드 파일의 인식 및 제어를 하기 위해서는 트랙 번호가 사용된다. 하지만 트랙번호를 사용자가 임의로 지정할 수는 없고 컴퓨터에서 SD 또는 USB 저장장치로 사운드 파일을 저장시 저장된 순서에 따라 자동으로 지정되게 된다. 저장되는 순서는 폴더1 파일 오름차순 -> 폴더2 -> ..... -> ADVERT 파일 오름차순이다.
만약 5개의 사운드 파일 "0001.mp3, 0002.mp3, 0003.mp3, 0004.mp3, 0005.mp3"를 SD카드에 저장한다고 할 때 모두 한번에 저장하는게 아니라 두번에 걸쳐서 저장한다고 가정해 보자. (보통 한번에 저장할 것이지만)
첫번째에 "0001.mp3, 0002.mp3, 0004.mp3, 0005.mp3"를 저장하고 두번째에 "0003.mp3"를 추가하여 저장하면 mp3 파일의 트랙 번호는 아래와 같이 정해지게 될 것이다.
0001.mp3 - 트랙 1
0002.mp3 - 트랙 2
0004.mp3 - 트랙 3
0005.mp3 - 트랙 4
0003.mp3 - 트랙 5
상기처럼 이미 사운드 파일이 저장되어 있는 폴더에 사운드 파일을 추가할 경우 트랙 번호가 의도대로 지정되지 않을 수 있다. 특히 ADVERT 폴더를 사용한다면 주의를 필요로 한다. 트랙 번호가 순서대로 지정되도록 하는 가장 좋은 방법은 사운드 파일이 추가되거나 순서가 변경되었을 경우 저장장치의 데이터를 모두 지우고 전체 파일을 통채로 다시 저장장치에 복사해야만 엉뚱한 사운드 파일이 재생되는 오류를 방지할 수 있다. 또한, 파일이름에서 사용한 숫자는 저장장치 저장 시 저장 순서(오름차순)를 정할뿐 아니라 폴더별로 파일을 제어하기 위해서도 사용된다. 이렇게 정해진 트랙번호와 폴더내의 파일 번호에 의해서 사운드 파일의 제어를 할 수 있게 된다.
DFplayer 시리얼통신 프로토콜
Format: $S / Ver. / Length / CMD / Feedback / Para_MSB / Para_LSB / Check_MSB / Check_LSB / $O
$S (0x7E): Start byte
Ver.: Version 0xFF by default
Length: Number of byte from version info to Check_LSB, 0x06 고정(checksum not counted)
CMD: Command Code
Feedback: 0x01: Need feedback--send confirmation back to MCU; 0x00: No need feedback
Para_MSB: Most significant byte of parameter
Para_LSB: Least significant byte of parameter
Check_MSB: Most significant byte of checksum - 사용안해도 된다.
Check_LSB: Least significant byte of checksum - 사용안해도 된다.
$O (0xEF): End byte
Length: 06 -> Ver. 부터 Para_LSB까지 자신을 포함한 바이트 수 6 byte, “FF 06 09 00 00 02”
Checksum (2 bytes) = 0xFFFF–(Ver.+Length+CMD+Feedback+Para_MSB+Para_LSB)+1
데이터의 정확성을 검사하기 위한 용도로 사용되는 합계. 오류 검출 방식의 하나이다. 대개는 데이터의 입력이나 전송 시에 제대로 되었는지를 확인하기 위해 입력 데이터나 전송 데이터의 맨 마지막에 앞서 보낸 모든 데이터를 다 합한 합계를 따로 보내는 것이다. 데이터를 받아들이는 측에서는 하나씩 받아들여 합산한 다음 이를 최종적으로 들어온 검사 합계와 비교하여 착오가 있는지를 점검한다.
[네이버 지식백과] 검사합 [checksum, 檢査合] (IT용어사전, 한국정보통신기술협회)
필자는 위 공식을 이용하여 Checksum을 구하고 데이터를 전송하여 보았으나 Checksum 에러가 반환되어 DFplayer를 콘트롤 할 수 없었다. 많은 시간을 투자하여 확인해보고 인터넷상에서 검색도 해 보았으나 나와 같이 에러가 발생하여 문의한 글들을 발견할 수 있었지만 자세한 설명 및 예제는 찾을 수 없었고, "Length 설명의 0x06 (checksum not counted)" 이부분에서 문제가 있는듯 하지만, Checksum을 사용하지 않아도 명령어를 인식 한다고 하니 사용하지 않기로 한다.
상기의 프로토콜중 $S, Ver., Length, $O는 7E, FF, 06, EF 값이고 그 값이 변하지 않는다.
아래처럼 빨간색 4 byte 영역 CMD, Feedback, Para_MSB, Para_LSB 부분만 명령어 형식에 맞게 데이터를 수정해서 시리얼 통신으로 1byte 씩 총 8byte를 보내주면 제어를 할 수 있다.
“7E FF 06 09 00 00 02 EF”
Feedback값으로 01로 수정하여 DFplayer로 전송하면 데이터 수신 후 에러가 없다면, DFplayer 모듈은 수신에 대한 응답으로 "7E FF 06 41 00 00 00 xx xx EF"을 보내주게 된다.
DFplayer는 시리얼 통신을 통한 제어를 위해 프로토콜을 사용한다. 블로그의 시리얼통신 포스트를 읽어 보았다면 프로토콜의 사용이 익숙할 것이다. 프로토콜의 사용 및 개념 파악하고자 한다면 아래 글을 참조하기 바란다. 프로토콜은 DFplayer 뿐만아니라 대부분의 장치들의 제어에 필수적으로 사용되게 된다. 각 장치의 메뉴얼에서 제공하는 각자의 프로토콜 규칙을 이해하고 숙지해야만 제대로 제어할 수 있다.
아두이노 - 안드로이드를 이용한 무선 원격제어 그리고 시리얼 통신 - 3편
DFplayer 명령어
DFplayer를 제어하기 위한 명령어중에는 아래에서 언급된 mp3 파일의 저장 방식에 따라 사용가능 여부가 결정되는 명령어 코드 들이 있어서 어떤 저장방식을 사용할지를 먼저 결정하고 난후 해당 저장방식에 맞는 명령어를 사용하여야 한다.
|
* 황색 숫자 자리는 제어 명령에 영향을 미치지 않는다. "00"이 아니라 다른 숫자가 있어도 상관없다.
DFplayer 응답 코드
이벤트 발생시 DFplayer는 아래 상황에 따라 시리얼 통신으로 해당 코드를 전송한다.
1. 모듈 초기화시
USB flash drive online: 7E FF 06 3F 00 00 01 xx xx EF
SD card online: 7E FF 06 3F 00 00 02 xx xx EF
PC online: 7E FF 06 3F 00 00 04 xx xx EF
USB flash drive and SD card online: 7E FF 06 3F 00 00 03 xx xx EF
2. 재생 종료시
1st track is finished playing in USB flash drive: 7E FF 06 3C 00 00 01 xx xx EF
2nd track is finished playing in USB flash drive: 7E FF 06 3C 00 00 02 xx xx EF
1st track is finished playing in SD card: 7E FF 06 3D 00 00 01 xx xx EF
2nd track is finished playing in SD card: 7E FF 06 3D 00 00 02 xx xx EF
3. ACK 값 01에 대한 응답
Module returns ACK: 7E FF 06 41 00 00 00 xx xx EF
4. 에러 발생시
7E FF 06 40 00 00 01 xx xx EF - Module busy(초기화 진행중 - 200ms 소요)
7E FF 06 40 00 00 02 xx xx EF - Currently sleep mode(대기 모드 상태)
7E FF 06 40 00 00 03 xx xx EF - Serial receiving error(시리얼 데이터가 프로토콜에 맞지 않음)
7E FF 06 40 00 00 04 xx xx EF - Checksum incorrect(체크썸 오류)
7E FF 06 40 00 00 05 xx xx EF - 재생명령이 트랙 범위를 벗어난 경우
7E FF 06 40 00 00 06 xx xx EF - 재생명령 트랙 번호가 없는경우
7E FF 06 40 00 00 07 xx xx EF - Insertion error(끼워넣기 재생은 음원재생중일 때에만 실행됨)
7E FF 06 40 00 00 08 xx xx EF - SD card reading failed(SD 읽기 오류)
5. 저장장치 연결 및 해제시
USB flash drive is plugged in: 7E FF 06 3A 00 00 01 xx xx EF
SD card is plugged in: 7E FF 06 3A 00 00 02 xx xx EF
USB cable connected to PC is plugged in: 7E FF 06 3A 00 00 04 xx xx EF
USB flash drive is pulled out: 7E FF 06 3B 00 00 01 xx xx EF
SD card is pulled out: 7E FF 06 3B 00 00 02 xx xx EF
USB cable connected to PC is pulled out: 7E FF 06 3B 00 00 04 xx xx EF
아두이노 및 스피커 연결
시리얼 통신 TTL 레벨이 3.3V 일경우 MCU TX와 DFplayer RX에 직접 연결해도 되지만 TTL레벨이 5V일경우(아두이노의 경우 VCC 5V, TTL 전압 역시 5V) 1K 저항을 TX와 RX 사이에 추가해주어야 한다.
VCC로 5V를 사용하지만 TTL 전압이 3.3V라면 저항을 추가할 필요가 없다.(예로 블루투스 모듈 연결에서 확인 할 수 있다)
TTL레벨이 5V인 MCU에서 저항을 사용하지 않고 연결할 경우 데이터 전송에는 문제가 없으나 스피커를 통해 화이트 노이즈가 심하게 발생하게 된다. (사용 불가할 정도임)
Note: When using a 5V MCU, please attach a 1K resistor between the TX and RX ports.
DFplayer 스피커 연결
상기 그림에서 처럼 SPK1과 SPK2에 스피커의 +와 -를 연결해주면 되고 극성 상관없이 연결해도 사운드 출력은 문제없이 잘 된다. SPK1과 SPK2 단자에서 나오는 사운드 신호는 내장된 3W 증폭기를 거쳐서 출력되고 모노 사운드만을 지원한다.
관련 명령어 코드
Audio amplification 출력증폭 | 10 | 00 | 01 | 0F | MSB=1: amplifying on, LSB: set gain 0-31 |
* 상기 명령을 전송하면 에러 코드가 반환되고 어떠한 변화도 없다. 아마도 amplifying on과 gain 31로 고정된 듯 싶다.
별도의 외부 전원 5V 밧데리가 없다면 아두이노의 전원 출력 5V를 이용하면 된다.
DFplayer 스피커 두개 연결 (Not recommandable)
인터넷에서 DFplayer 스피커 단자에 스피커 두개를 연결한 예제를 찾아 볼 수 있었다. 스피커 두개의 GND를 DFPLAYER GND와 공통으로 연결해주고 스피커의 +를 SPK1과 SPK2에 연결시켜 주면 두개의 스피커에서 사운드 출력은 되나 스피커와 모듈 둘 다에서 엄청난 발열이 발생하게 된다. 두개의 스피커에서 소리는 나오지만 모노로만 출력되고 스피커를 한개만 연결한 경우와 비교해 사운드가 더 작아진 느낌 마저 든다. 만약 상위 기능인 스테레오로 연결이 가능했다면 모듈 제작사에서 적극적으로 홍보를 했을 것이다. 스테레오 사운드는 이어폰 연결을 위한 DAC R과 DAC L 단자를 이용하여 사용가능하다.
이어폰 잭 연결
DFplayer는 모듈이 동작할 때 모듈 자체에서 white noise를 만들어 내고 있으며, SPK1과 SPK2 단자를 통한 모노 사운드 출력시에는 거의 느끼지를 못했다. 하지만 DAC R과 DAC L 단자를 이용할 경우 white noise가 심하게 노출 되는데 사운드 파일이 재생중일 때는 white noise의 크기가 작아져 거의 들리지 않고 일시정지시 최대로 들리고 정지시는 재생시보다는 크게 들린다. 이 때문에 메뉴얼상 이어폰 소켓과 DAC핀 사이에 저항 100옴을 추가하라고 되어 있는 것 같다. 하지만 220옴을 연결한 상태에서도 white noise가 심하여 1K 옴 저항을 추가해주어 white noise를 거의 없앨수 있었다. 하지만 추가된 저항의 크기에 비례하여 이어폰에 출력되는 소리는 작아지게 된다.
100옴 ~ 1K옴 사이의 저항을 연결해보고 적당한 크기의 저항을 결정하면 될 것 같다.
DAC R과 DAC L 단자를 통해 출력되는 사운드는 스테레오를 지원한다. 음원이 스테레오를 지원한다면 이어폰의 왼쪽과 오른쪽이 다르게 들리는 것을 확인할 수 있다.
white noise 제거 및 감소를 위한 방법
1. 음원자체에 노이즈를 포함한 경우 음원 변경
2. GND 연결이 확실하게 되지 않은 경우 - 브레드 보드를 사용할 경우 납땜 연결보다는 불안전하다.
노이즈가 심하다면 GND에 연결된 핀을 움직여보자
3. 사운드 출력쪽 저항을 추가해 보고 저항의 크기를 변경해 보자
4. IO_1핀과 IO_2핀 사이의 GND핀도 GND에 연결해 보자.
5. 아두이노를 이용할 경우 DFplayer의 GND를 아두이노 GND핀에 직접 연결해 보자.
[arduino] - 아두이노 - DFPlayer DAC_R, DAC_L 단자 노이즈 제거 방법
스테레오 사운드 테스트 음원
https://kozco.com/tech/soundtests.html
Note: Between headphone and module, a 100R resistor can be added to make the limiting .
관련 명령어 코드
Set DAC | 1A | 00 | 00 | 00 | 00 | 오디오잭 사용유무 설정: 01 = off, 00 = on , defalt: 01 |
기본값이 00 = on 이다.
로컬 버튼 이용 제어
DFplayer는 프로토콜을 이용하지 않고 자체 제어용 핀을 통한 제어도 지원한다.
관련핀은 IO 1, IO2, ADKEY1, ADKEY2, GND이다. 각 핀과 GND 사이에 버튼을 위치 시켜 버튼에 의해 단락이 발생하면 전압 강하를 인지하고 작동하도록 되어 있는것 같다. 간단하게 테스트 하는 방법으로는 GND에 점퍼선을 연결하고 단자를 IO 1번핀에 짧게 단락 시키면 다음곡 재생, 길게 단락 시키면 볼률감소, IO 2번핀에 짧게 단락 시키면 이전곡 재생, 길게 단락 시키면 볼륨이 증가되는것을 확인할 수 있다.
추가로 ADKEY1, 2를 이용하여 아래와 같이 저항별로 구분된 버튼을 연결하게 되면 각각의 전압강하 정도를 인식하고 해당 명령이 실행되는 것 같다.
이글에서는 상기의 내용에 관해 살펴보지는 않겠다. 해당 사항은 메뉴얼을 참조하기 바란다.
블루투스 모듈 이용 DFplayer 테스트 및 제어
우선 SD 카드에 MP3 파일 또는 WAV 파일을 저장하는 방법으로 앞에서 설명한 B. 저장방식 2 (저장방식 1에 추가로 ADVERT 폴더 사용)를 따르겠다.
테스트 파일
테스트 파일을 SD카드에 저장하면 아래와 같은 폴더 및 파일들이 SD카드에 위치해 있을 것이다.
B. 저장방식 2 (저장방식 1에 추가로 ADVERT 폴더 사용)
폴더명: 01 ~ 99까지 각 폴더별 파일갯수 1 ~ 255
파일명: 숫자 3자리 001 ~ 255 반드시 포함하는 파일명으로, 최대 파일갯수 255개 까지 각 폴더에 저장된 파일 인식
ADVERT 폴더: mp3 또는 wave파일의 이름은 4자리 이하 숫자로만 저장되어야만 한다. 1 ~ 3000
저장시 생성된 트랙번호
폴더 01
001 canon_guitar.mp3 - 루트 트랙 넘버 1
002 LRMonoPhase4.wav - 루트 트랙 넘버 2
003 터미네이터2 오프닝.mp3 - 루트 트랙 넘버 3
004 미션임파서블배경음악.mp3 - 루트 트랙 넘버 4
005 수퍼마리오.mp3 - 루트 트랙 넘버 5
006 스타크레프트.mp3 - 루트 트랙 넘버 6
폴더 02
005 통화중.MP3 - 루트 트랙 넘버 7
015 풀벌래,새소리.mp3 - 루트 트랙 넘버 8
017 새벽닭.wav - 루트 트랙 넘버 9
026 큰파도.MP3 - 루트 트랙 넘버 10
027 다듬이.MP3 - 루트 트랙 넘버 11
029 아프리카 리듬.mp3 - 루트 트랙 넘버 12
폴더 ADVERT
0001 k1.mp3 - 루트 트랙 넘버 13
0002 k2.mp3 - 루트 트랙 넘버 14
0003 k3.mp3 - 루트 트랙 넘버 15
0004 k4.mp3 - 루트 트랙 넘버 16
0005 k5.mp3 - 루트 트랙 넘버 17
트랙 넘버 사용 제어
Specify playback(root) | 03 | 00 | 03 | E8 | 폴더 상관없이 전체 트랙 번호중 특정 번호 재생 03E8: 1000번째 트랙 |
파일 넘버 사용 제어
Specify a track in a folder | 0F | 00 | 0B | 64 | 0B: 11폴더, 64: 파일번호 100번 재생 |
폴더 02의 "026 큰파도.MP3"를 재생하고자 한다면 아래의 두가지 방법을 사용할 수 있다.
트랙 넘버 사용: “7E FF 06 03 00 00 0A EF” // 0x0A: 10
파일 넘버 사용: “7E FF 06 09 00 02 1A EF” // 0x02: 02폴더, 0x1A: 26
안드로이드 앱을 이용한 테스트
DFplayer 제어를 위한 프로토콜을 이용하여 테스트할 수 있도록 프로그램된 안드로이드 앱을 사용해 DFplayer의 작동상황 및 특성들을 파악해 보자.
테스트용 결선도
블루투스 2.0 모듈 HC-06 또는 BLE AT-09 - 블루투스 모듈의 TTL 전압은 3.3V이므로 저항을 추가할 필요는 없다.
아래 그림에는 HC-06이 연결되어 있다. 그자리에 BLE AT-09를 연결해주면 BLE 모듈을 사용할 수 있다.
이어폰 소켓을 사용하여 DAC R 및 DAC L에 결선을 해준다.
3W 미만의 모노 스피커를 SPK1과 SPK2에 연결해 주었다.
아래와 같이 연결한 상태에서 음원을 재생한다면 스피커와 이어폰 둘다에 소리가 출력되게 된다.
안드로이드 앱
블루투스 연결을 통해 DFplayer에 명령어를 전송하고 DFplayer의 응답을 확인할 수 있도록 프로그램된 앱이다.
DFcontroller
https://play.google.com/store/apps/details?id=com.appybuilder.dfcontroller.DFcontroller
블루투스 연결
DFcontroller basic 앱은 블루투스 2.0모듈, BLE 모듈, ESP32의 블루투스와 연결할 수 있다.
블루투스 2.0모듈을 사용할 경우 우선 스마트폰에 블루투스 모듈을 등록시켜주어야 한다.
이미 등록이 되어 있다면 아래 과정(1 ~ 4)은 건너 뛰어도 된다.
1. 스마트폰의 설정에서 블루투스 아이콘을 클릭하여 블루투스를 켜고 블루투스 기기를 검색한다.
2. 연결하고자 하는 모듈이 표시되면 해당 모듈을 클릭한다.
3. 모듈에 설정된 비밀번호를 입력하고 확인을 클릭한다. 아두이노 모듈 HC-06의 경우 초기 비밀번호는 0000 또는 1234이다.
4. 등록이 완료되면 등록된 디바이스 항목에 해당모듈이 표시된다.
모듈 등록이 완료 되었으면 DFcontroller basic 앱을 실행시키고 Bluetooth 2.0 항목이 표시된 상태에서 왼쪽의 Bluetooth 버튼을 클릭하고 등록된 모듈을 선택해주면 안드로이드 앱과 블루투스 모듈이 연결 된다.
블루투스 BLE 모듈을 사용 할 경우 상기의 블루투스 2.0의 스마트폰에 모듈을 등록시켜주는 절차(2 ~ 4)가 생략된다.
스마트폰의 블루투스를 켜고(1번) 앱을 실행시켜 앱에서 연결만 시켜주면 된다.
블루투스 BLE, AT-09와 연결하고 테스트 해보겠다.
블루투스 4.0 BLE 모듈은 블루투스 2.0과 같이 블루투스 모듈을 스마트폰에 등록하는 절차(상기 1 ~ 4번)없이 안드로이드 앱 상에서 바로 연결해야 한다.
블루투스 버전을 클릭한 뒤 Bluetooth 4.0을 선택하고 Bluetooth 버튼을 클릭, 블루투스 리스트에서 해당 블루트스를 선택한다.
연결이 되면 블루투스 BLE의 경우 모듈에 설정된 이름이 앱에 표시되고 블루투스 2.0 모듈의 경우 Connected라고 표시된다.
******* DFcontroller basic 앱은 플레이스토어에서 삭제 하였습니다. ********
아래글의 command 테스트는 하실수 없습니다. 참고로만 읽어주세요.
연결이 된 후에 CMD의 Select the command를 클릭하고 명령어 리스트에서 원하는 명령어를 선택하면 프로토콜 CMD 항목에 값이 입력된다. 프로토콜 항목은 직접 입력도 가능하며 입력시에는 반드시 HEX 값으로 입력해야 원하는 값으로 제어를 할 수 있게 된다.
아래 그림은 다음곡 재생의 명령어를 보내는 화면이다. 이 때 ACK의 값을 01로 설정하고 Send를 클릭하면 DFplayer에서 보내는 응답을 Return value 항목에 표시하게 된다.
사운드 볼륨을 1씩 증가하거나 감소시키지 않고 특정값 10으로 변경해보자.
CMD에서 Volume: 06을 선택하고 DEC to HEX convertor의 LSB 항목에 10을 입력하고 Convert 버튼을 클릭하면 프로토콜의 MSB와 LSB 항목에 HEX 값으로 변경된 값이 입력되고 Send 버튼을 클릭하면 볼륨이 조절된것을 확인 할 수 있다.
현재 볼륨값을 DFplayer에 질의하고 응답을 받아보자.
CMD에서 Current volume: 43을 선택하고 Send 버튼을 클릭하면 DFplayer에서 응답으로 보내온 데이터를 Return value 항목에서 확인 할 수 있다.
7E FF 06 43 00 00 0A xx xx EF // 현재 볼륨값 10
또한 현재 재생되고 있는 음원의 재생이 종료될 경우 DFplayer 종료 표시값을 두번 연속해서 보내는데, 블루투스 2.0과 연결된 상태에서는 아드로이드 앱에 한번만 표시되고 블루투스 BLE 연결시에는 두번 연속 보내는 데이터가 그대로 표시된다. (오류가 아니며, DFplayer에서 두번 보내는 것이다)
7E FF 06 3D 00 00 01 xx xx EF + 7E FF 06 3D 00 00 01 xx xx EF // 트랙번호 1번 종료
7E FF 06 48 00 00 00 EF // Query tracks 수 in SD -> 7E FF 06 48 00 00 11 xx xx EF // 트랙 수 11(HEX) = 17
7E FF 06 4F 00 00 00 EF // Query folders 수 in storage -> 7E FF 06 4F 00 00 03 xx xx EF // 폴더 수 3(HEX)
7E FF 06 4E 00 00 01 EF // Query tracks 수 in folder number 1 -> 7E FF 06 4E 00 00 06 xx xx EF // 1번 폴더: 6
아두이노와 DFplayer 연결
DFplayer를 아두이노에 연결하고 블루투스를 통해 DFplayer를 제어하고 테스트 해보자.
아래 스케치를 아두이노에 업로드하고 DFcontroller basic App으로 테스트 해보자. 아두이노에 스케치 업로드시에는 상기 첨부된 그림처럼 블루투스 TX핀과 아두이노 RX핀을 분리한 상태에서 업로드 해야만 한다. 이 때 만약 안드로드앱과 블루투스 모듈이 연결되어 있는 상태라면 안드로이드 앱에서 블루투스 연결을 끊어주고 업로드 하자. 연결된 상태에서 업로드 하게되면 아두이노 TX를 통해 아두이노에 업로드되는 스케치 데이터가 그대로 블루투스를 통행서 안드로이드 앱으로도 전송되게 되는데 전송이 완료될때까지 앱이 반응을 안하게 된다. 업로드가 완료된 후 다시 연결 시켜주면 아두이노에서 소프트 웨어 시리얼과 하드웨어 시리얼을 동시에 사용할 수 있다.
#include <SoftwareSerial.h>
#define DF_rxPin 2 // DFplayer RX -> arduino 2
#define DF_txPin 3 // DFplayer TX -> arduino 3
SoftwareSerial dfSerial(DF_txPin, DF_rxPin); // (RX, TX)
void setup() {
Serial.begin(9600); // bluetooth Serial
dfSerial.begin(9600); // DFplayer Serial
}
void loop() {
if (dfSerial.available()) {
Serial.write(dfSerial.read()); // send to android via bluetooth
}
if (Serial.available()) {
dfSerial.write(Serial.read()); // send to DFplayer
}
}
앞에서 살펴본 블루투스 모듈을 직접 DFplayer에 연결한 경우와 작동상 어떠한 차이도 없다. 이 스케치에서 아두이노는 단지 블루투스와 DFplayer간의 시리얼 통신을 통해 들어온 데이터를 중계하는 역할만 하게된다.
테스트 영상
블루투스 BLE 연결 / 블루투스 2.0 연결
Play Next “7E FF 06 01 00 00 00 EF”
Increase volume “7E FF 06 04 00 00 00 EF”
Decrease volume “7E FF 06 05 00 00 00 EF”
Specify volume “7E FF 06 06 00 00 1E EF” // 1E: 볼륨 30
Query current volume “7E FF 06 43 00 00 00 EF”
Pause “7E FF 06 0E 00 00 00 EF”
Play “7E FF 06 0D 00 00 00 EF”
Play Next “7E FF 06 01 00 00 00 EF”
Insert an advertisement “7E FF 06 13 00 00 01 EF”
Insert an advertisement “7E FF 06 13 00 00 04 EF”
Stop “7E FF 06 16 00 00 00 EF”
DFplayer에 블루투스 모듈을 직접연결하고 실행한 테스트
아두이노에 DFplayer와 블루투스 모듈을 연결하고 실행한 테스트
시리얼 모니터 이용 제어
안드로이드 앱을 통하지 않고 아두이노 시리얼 모니터에서 명령어를 입력하여 DFplayer를 제어해 보도록 하자.
명령어 데이터 헥사값 8자리를 저장하기 위해 부호 없는형(unint8_t) 배열을 만들어 주고 고정된 값인 헤더와 종료문자 0x7E, 0xFF, 0x06 , 0xEF를 해당 위치에 지정해 준다.
uint8_t df_command[] = {0x7E, 0xFF, 0x06, 0x00, 0x00, 0x00, 0x00, 0xEF};
명령어 전송을 위해 입력값을 갖는 사용자 함수를 만들어 필요한 곳에서 명령어 데이터의 변동값 부분을 입력하고 순차적으로 시리얼 통신을 통해 DFplayer로 데이터를 전송하도록 해주자.
void send_command(uint8_t cmd, uint8_t ack, uint8_t msb, uint8_t lsb) {
df_command[3] = cmd;
df_command[4] = ack;
df_command[5] = msb;
df_command[6] = lsb;
for(int i = 0; i < sizeof(df_command); i++){
dfSerial.write(df_command[i]);
}
}
아래 스케치를 업로드하고 play, stop, pause, previous, next등의 텍스트 명령어를 시리얼 모니터에 입력해보자. 이 때 명령어 종료문자로 '\n'(새 줄)을 사용했으므로 아두이노 시리얼 모니터의 전송옵션을 "새 줄"로 반드시 변경하자.
코드상 종료문자의 확인은 아래 두 코드에서 볼 수 있고 시리얼 통신이 이해가 잘 안된다면 이전 강좌를 살펴보기 바란다.
if(c != '\n')
String s = Serial.readStringUntil('\n');
볼륨을 조절하고자 할경우 시리얼 모니터에서 "@볼륨" 입력하고 전송하면 된다.
예) @10 -> 볼륨 10, @25 -> 볼륨 25
#include <SoftwareSerial.h>
#define DF_rxPin 2 // DFplayer RX -> arduino 2
#define DF_txPin 3 // DFplayer TX -> arduino 3
SoftwareSerial dfSerial(DF_txPin, DF_rxPin); // (RX, TX)
String s =""; // 시리얼 모니터 텍스트 제어용 변수
// DFplayer command 변수
uint8_t df_command[] = {0x7E, 0xFF, 0x06, 0x00, 0x00, 0x00, 0x00, 0xEF};
void send_command(uint8_t cmd, uint8_t ack, uint8_t msb, uint8_t lsb) {
df_command[3] = cmd;
df_command[4] = ack;
df_command[5] = msb;
df_command[6] = lsb;
for(int i = 0; i < sizeof(df_command); i++){
dfSerial.write(df_command[i]);
}
}
void setup() {
Serial.begin(9600); // Serial monitor
dfSerial.begin(9600); // DFplayer Serial
send_command(0x06, 0, 0, 10); // volume 초기값
Serial.print("Volume: "); Serial.println(10);
}
void loop() {
if(Serial.available()) { // 시리얼 버퍼에 데이터 있으면
if(Serial.peek() == '@') {
String s = Serial.readStringUntil('\n');
s.remove(0, 1); // '@' delete
int vol = s.toInt();
send_command(0x06, 0, 0, vol); // Specify volume
Serial.print("Volume: "); Serial.println(vol);
s = "";
}
char c = Serial.read();
Serial.println(c, HEX);
if(c != '\n') { // 볼륨조절 @번호 이후 0xFF 값이 전송됨, 원인 모름
if(c != 0xFF) s += c; // 0xFF일경우 명령어 스트링에 저장방지
} else {
if(s == "play"){
send_command(0x0D, 0, 0, 0); // Play, to DFplayer
Serial.println("Play");
s = "";
}
else if(s == "stop"){
send_command(0x16, 0, 0, 0); // Stop, to DFplayer
Serial.println("Stop");
s = "";
}
else if(s == "pause"){
send_command(0x0E, 0, 0, 0); // Pause, to DFplayer
Serial.println("Pause");
s = "";
}
else if(s == "previous"){
send_command(0x02, 0, 0, 0); // Play Previous, to DFplayer
Serial.println("Play Previous");
s = "";
}
else if(s == "next"){
send_command(0x01, 0, 0, 0); // Play Next, to DFplayer
Serial.println("Play Next");
s = "";
}
else {
Serial.println(s);
s = "";
}
}
}
}
이번엔 상기 예제에서 사용했던 블루투스 BLE모듈 'AC-09' 대신 블루투스 2.0 모듈 HC-06을 연결하고 arduino bluetooth controller PWM 안드로이드 앱을 통해 mp3 플레이어처럼 제어해 보겠다.
arduino bluetooth controller PWM - 아두이노 원격제어 안드로이드 앱 버전 3.5 다운로드
arduino bluetooth controller PWM 매뉴얼
arduino bluetooth controller PWM 안드로이드 앱이 블루투스 2.0(HC-06) 및 4.0 BLE(AT-09) 모듈, ESP32 모듈과 wifi모듈을 모두 지원하므로 필요에 따라 통신모듈을 아두이노에 연결하여 사용하면 된다.
아래 스케치 파일을 아두이노에 업로드 해주자.
아두이노에 스케치를 업로드 했다면 arduino bluetooth controller PWM을 실행한뒤 아두이노를 통해 DFplayer를 제어하기 위해 아래와 같이 앱의 버튼 설정을 해주자.
버튼 10, 11, 12번은 사용되지 않으므로 "uncheck"를 해주어 버튼이 표시되지 않도록 해주고, 버튼 라벨 항목에서 이름도 그림과 같이 변경해 주자.
Export 버튼은 클릭하면 현재 설정된 버튼라벨이 저장되게 되고 Import 버튼을 클릭하면 저장된 파일을 읽어 저장된 이름을 불러오게 된다. 버튼 이름을 백업할 때 사용하는 기능이다.
check 박스중 "echo text color"는 echo 값이 수신되었을 경우 버튼 이름의 색깔을 변경해주는 기능으로 기본값은 "check"이지만 이번 예제에서는 echo값이 수신되어도 이 기능을 사용하지 않을 것이므로 "uncheck" 해주자.
"echo text color" 설정은 arduino bluetooth controller PWM 앱 버전 2.7에 추가된 기능이다.
Previous, Next, AD stop 버튼은 푸쉬버튼으로 사용하고, DFplayer 모듈에서 볼륨 조절은 0 ~ 30 사이이므로 PWM Max 값을 30으로 설정하고 EQ도 0 ~ 5까지이므로 5로, 현재 예제의 ADVERT 폴더를 제외한 폴더 갯수가 2개 이므로 2로 설정해 주자.
설정을 완료하고 블루투스 아이콘을 클릭해서 모듈에 맞게 연결 해주자. 연결이 되면
arduino bluetooth controller PWM - Not Connected 항목이 arduino bluetooth controller PWM으로 변경된것을 볼 수 있다.
이제 각각의 버튼을 클릭하여 작동 상태를 확인해 보면 된다.
ADVERT 기능을 사용하기 위해서는 특정 파일 번호를 입력해야 되는데 아두이노에 업로드된 코드상에 시리얼 통신으로 들어오는 데이터중 '\n'문자(줄바꿈)를 인식하고 ADVERT 명령을 실행하도록 되어 있다.
트랙 재생중 끼워넣기를 하고자 한다면 아래 그림처럼 텍스트 입력창의 맨 오른쪽 엔드 버튼을 NL('\n'-줄바꿈, New Line)로 변경하고 텍스트 입력창에 ADVERT 트랙번호를 입력하고 send를 클릭하면 작동하게 된다.
DFplayer 명령어중 폴더내 특정 트랙을 제어할 수 있는 명령어가 있는데 이를 이용하기 위해서는 엔드 버튼을 NL로 한 상태에서 "f폴더,파일이름"를 입력하고 send를 클릭하면 폴더내 특정 파일을 재생할 수 있다.
예) f1,10 -> 1번 폴더, 파일이름 010.mp3 재생, 트랙 번호가 아니다.
f2.255 -> 2번 폴더, 파일이름 255.mp3 재생
예제 음원 파일의 2번 폴더의 "027 다듬이.mp3" 파일을 재생할 경우 "f2,27"을 입력해야 한다.
// advert command 변수
String AD = "";
void general_use() {
if(Serial.peek() == 'f') { // 폴더내 특정 트랙 재생 텍스트 명령 처리
String s = Serial.readStringUntil('\n');
s.remove(0, 1); // 'f' delete
String msb = s.substring(0, s.indexOf(","));
String lsb = s.substring(s.indexOf(",") + 1, s.length());
int msb_val = msb.toInt();
int lsb_val = lsb.toInt();
send_command(0x0F, 0, msb_val, lsb_val); // Specify a track in a folder
send_pin_echo(21); // Play button status
send_pin_echo(40); // Stop button status
send_pin_echo(50); // Pause button status
send_pin_echo(70); // Repeat button status
send_pin_echo(90); // Shuffle button status
send_pwm_slide(3, 0); // Repeat folder slide status
Serial.print("folder: ");Serial.print(msb); Serial.print(" track: "); Serial.print(lsb);
}
else {
char text = Serial.read(); // read text value from App, AD 재생 명령 처리
if(text >= 48 && text <= 57) { // income text == number, 0 ~ 3000 for advert
AD += text;
}
else if(text == '\n'){
int ad_num = AD.toInt();
uint8_t ad_lsb = ad_num;
uint8_t ad_msb = ad_num >> 8;
send_command(0x13, 0, ad_msb, ad_lsb); // Insert an advertisement
AD = "";
}
else {
Serial.write(text); // print text to App
}
}
}
상기 코드는 ADVERT 명령시 숫자만 입력하면 되므로 사용이 편리하지만 숫자와 문자 'f'를 포함하는 텍스트는 사용할 수 없게 된다. "hi", "hello"는 숫자와 'f'가 없으므로 사용할 수 있다.
상기 코드를 아래 코드로 변경해보자
void general_use() {
if(Serial.peek() == '@') { // 폴더내 특정 트랙 재생 텍스트 명령 처리
String s = Serial.readStringUntil('\n');
s.remove(0, 1); // '@' delete
if (s.indexOf(",") != -1) {
String msb = s.substring(0, s.indexOf(","));
String lsb = s.substring(s.indexOf(",") + 1, s.length());
int msb_val = msb.toInt();
int lsb_val = lsb.toInt();
send_command(0x0F, 0, msb_val, lsb_val); // Specify a track in a folder
send_pin_echo(21); // Play button status
send_pin_echo(40); // Stop button status
send_pin_echo(50); // Pause button status
send_pin_echo(70); // Repeat button status
send_pin_echo(90); // Shuffle button status
send_pwm_slide(3, 0); // Repeat folder slide status
Serial.print("folder: ");Serial.print(msb); Serial.print(" track: "); Serial.print(lsb);
}
else {
int ad_num = s.toInt();
uint8_t ad_lsb = ad_num;
uint8_t ad_msb = ad_num >> 8;
send_command(0x13, 0, ad_msb, ad_lsb); // Insert an advertisement
}
}
else {
char text = Serial.read();
Serial.write(text); // print text to App
}
}
변경을 하고 스케치를 업로드 하게되면 텍스트 명령어가 바뀌게 된다.
ADVERT 명령: @번호
폴더내 파일 재생 명령: @폴더,파일이름
단점은 ADVERT 명령시 @를 추가해주어야 한다는 것이고 장점은 코드가 좀더 단순해지고 텍스트 입력란에 '@'를 포함하지 않는 텍스트를 자유롭게 입력할 수 있다는 것이다. 편리한 코드를 적용하면 되겠다.
상기 스케치중 주요함수를 살펴보겠다.
// 블루투스 앱 상태표시 관련 변수
uint8_t pin_echo[] = {0xF0, 0, 0xF1};
void send_pin_echo(uint8_t pin_val){
pin_echo[1] = pin_val;
for (int i = 0; i < 3; i++){
Serial.write(pin_echo[i]);
}
}
상기 코드는 아두이노에서 DFplayer에 보내는 명령어에 따라 관련된 안드로이드 앱의 버튼 상태를 표시 하기 위해 사용하는 함수 이다. 만약 Stop 버튼이 눌려진 상태에서 Play버튼을 클릭한다면 Stop 버튼은 off 상태로 변경되어야 한다. 이렇게 서로 상호 작용해야만 하는 버튼의 상태 명령을 보내기 위한 코드이다.
아래와 같이 사용한다.
send_pin_echo(21); // Play button status, Play 버튼 on
send_pin_echo(40); // Stop button status, Stop 버튼 off
send_pin_echo(50); // Pause button status, Pause 버튼 off
send_pin_echo(70); // Repeat button status, Repeat 버튼 off
send_pin_echo(90); // Shuffle button status, Shuffle 버튼 off
uint8_t pwm_slide[] = {0xF4, 0, 0, 0, 0xF1};
void send_pwm_slide(uint8_t slide, uint16_t pwm_val) {
pwm_slide[1] = slide;
if (pwm1 < 256) {
pwm_slide[2] = pwm_val;
pwm_slide[3] = 0;
}
else {
pwm_slide[2] = pwm_val;
pwm_slide[3] = pwm_val >> 8;
}
for (int i = 0; i < 5; i++) {
Serial.write(pwm_slide[i]);
}
}
상기 코드는 앱상의 PWM 슬라이드 버튼의 상태를 표시하기 위한 것이다.
send_pwm_slide(3, 0); // Repeat folder slide status, 3번 슬라이드, 값 0
// DFplayer command 변수
uint8_t df_command[] = {0x7E, 0xFF, 0x06, 0x00, 0x00, 0x00, 0x00, 0xEF};
void send_command(uint8_t cmd, uint8_t ack, uint8_t msb, uint8_t lsb) {
df_command[3] = cmd;
df_command[4] = ack;
df_command[5] = msb;
df_command[6] = lsb;
for(int i = 0; i < sizeof(df_command); i++){
dfSerial.write(df_command[i]);
}
}
상기 코드는 DFplayer에 명령을 보내기 위한 코드이고 매뉴얼상 프로토콜에 따라 작성되었다.
send_command(0x0D, 0, 0, 0); // Play, to DFplayer
나머지 코드들은 이전 강좌에서 다룬 안드로이드를 이용한 아두이노 무선 원격제어 FULL 코드를 이용하고 DFplayer 메뉴얼상의 명령어에 맞게 적용해준 것이다. 시리얼 통신 송수신에 관한 코드가 이해가 안된다면 이전 강좌를 살펴보기 바란다.
https://postpop.tistory.com/12
DFplayer 모듈은 random playback 기능을 제공한다.
Set random playback(root) | 18 | 00 | 00 | 00 | 전체 파일 대상 무작위 재생 |
하지만 이기능은 순수하게 randeom() 함수를 사용하는것 같은데, 문제는 중복된 숫자가 빈번하게 나올수 있다는 것이고, 또한 저장장치 root 폴더의 전체 트랙번호를 대상으로 한다는 것이다. 이 얘기는 이미 재생된 트랙이 다시 재생될 수 있을 뿐만아니라 ADVERT폴더가 있을경우 ADVERT 폴더내의 트랙도 재생된다는 것이다. 이는 mp3 플레이어의 셔플 기능과 비슷한 것 같으나 다른 기능이다.
상기의 DFplayer 제어코드는 안드로이드 앱의 Shuffle 버튼을 클릭할 경우 DFplayer에서 지원하는 random playback 기능을 사용하도록 코딩 되어 있다. DFplayer의 random playback 기능을 사용하지 않고 아두이노에서 Shuffle 기능을 코드로 구현해 보도록 하겠다.
Shuffle 이라함은 1 ~ 10 까지 번호가 매겨진 10장의 카드가 있을경우, 이를 무작위로 섞은 다음 순서대로 한장씩 보여주는 것이다. 10장의 카드가 모두 보여지는 동안 번호는 순서에 상관없이 무작위로 나오지만 같은 번호는 나올 수가 없다.
셔플 기능을 구현 하기 위해서는 randeom() 함수를 이용해야 한다. 아두이노 참조사이트의 random() 함수 페이지를 살펴보자.
https://www.arduino.cc/reference/ko/language/functions/random-numbers/random/
random() [난수]
설명
random 함수는 의사 난수를 만들어 낸다.
문법
random(max),
random(min, max)
아래 코드를 아두이노에 업로드하면
void setup() {
Serial.begin(9600);
for (int i = 0; i < 10; i++) {
uint8_t temp = random(0, 9);
Serial.print(temp);
}
}
void loop() {
}
시리얼 모니터에 "4752486881" 과 같이 난수가 생성되게 된다. 하지만 아두이노의 리셋 버튼을 클릭해보면 앞에 나왔던 값과 똑같은 값이 출력되게 된다. 이 얘기는 아두이노 내부적으로 조건에 따라 난수 생성 시퀀스가 정해져 있다고도 볼 수 있다. 조건이 바뀌지 않으면 생성된 난수의 값이 바뀌지 않는다는 얘기이다. random 함수를 사용하는 이유는 매번 다른 값을 만들어 내기 위함인데 리셋이 되면 앞서 리셋되었던 때의 난수와 같은 값만을 생성하게 되면서 사용 할 수가 없게된다.
아두이노 참조 사이트의 random 함수 예제아래 주의와 경고 항목을 살펴보자.
주의와 경고
If it is important for a sequence of values generated by random()
to differ, on subsequent executions of a sketch, use randomSeed()
to initialize the random number generator with a fairly random input, such as analogRead()
on an unconnected pin.
랜덤()에 의해 생성된 값의 시퀀스가 달라지는 것이 중요한 경우, 사용되지 않는 아날로그 핀의 Read() 값을 이용하는 randomSeed() 함수를 사용하여 의사 난수 초기값을 생성시켜 random() 함수를 실행시키세요. 정도로 해석 되겠다.
상기 코드에 아래 코드를 추가해 주자.
randomSeed(analogRead(0)); // analogRead(0)의 값이 매번 다르다는것을 추정해 볼 수 있다.
void setup() {
Serial.begin(9600);
randomSeed(analogRead(0));
for (int i = 0; i < 10; i++) {
uint8_t temp = random(0, 9);
Serial.print(temp);
}
}
void loop() {
}
아두이노의 리셋 버튼을 클릭할 때마다 매번 다른 번호가 출력되는 것을 확인 할 수 있다.
랜덤 함수는 주사위나 빙고 게임을 코딩할 때에도 사용된다.
버튼을 클릭하면 주사위 1 ~ 6의 번호가 나오도록 하는 코드는 매우 간단하다.
random(1, 6); // 결과값이 중복되어도 상관없다.
빙고게임은 제시되는 숫자가 중복되면 안된다.
5 x 5 배열 빙고게임
게임참여자 두명이 5 x 5 배열에 1 ~ 25 까지의 숫자를 임의대로 넣고, 그 숫자는 중복되지 않아야 한다.
제시되는 숫자에 따라 숫자에 해당하는 칸을 지워나가고, 지워진 칸이 연속한 5칸이 되면 한줄이되고 5개의 줄이 먼저 생성되는 참여자가 이기는 게임이다.
이 때 제시되는 숫자역시 순서는 상관없으나 중복되지 않아야 한다.
아두이노를 이용하여 5 x 5배열의 빙고 게임을 한다는 것은 아두이노를 이용하여 숫자를 제시한다는 것이다.
코드의 핵심은
1. 버튼을 누르면 정해진 범위내의 숫자를 무작위로 만들어 낸다.
2. 앞서 나온 숫자와 중복되지 않아야 한다.
1번은 random(1, 25)함수를 이용하면 쉽게 구현할 수 있다.
2번은 25개의 배열을 정의하고 첫번째 버튼을 눌러서 나온 숫자를 배열 인덱스 0번에 저장한 뒤 참여자에게 숫자를 제시하고, 두번째 버튼을 눌렀을 때 나온 숫자가 인덱스 0번의 값과 같은지 검사하여 같으면 다른 값이 나올때 까지 랜덤 함수를 실행하고 다른 값이 나오면 배열 인덱스 1번에 저장하고 참여자에게 숫자를 제시한다. 같은 방식으로 순서대로 숫자의 중복검사를 하면서 숫자를 저장하고 참여자에게 숫자를 제시하면 된다.
보통 25개 배열의 칸이 모두 채워지기 전에 게임이 끝나게 되는데, 25개 배열의 칸을 오두 채운다고 가정해보자.
첫번째 값을 생성하고 두번째 값을 생성할때 첫번째 값과 두번째 값이 같을 확률이 굉장히 낮지만 24개의 숫자를 생성한 상태에서 마지막 값과 24개의 값이 같을 확률은 굉장히 높게된다. 마직막 남은 숫자가 xx이라면 random()함수를 통해 숫자가 나올때마다 배열 24개의 숫자와 비교하여 모두 중복되지 않으면 xx값이 확정되고, 25번째 배열에 저장되어 종료되겠지만 배열 24개의 숫자와 비교중 중복된 값이 나오면 다시 random()함수를 실행하고 중복안된 값이 나올때가지 무한 검증 및 숫자를 생성하게 된다.
이렇게 채워진 25개의 배열은 Shuffle의 트랙번호로 사용된다. Shuffle 버튼을 클릭하면 25개 배열 인덱스 0부터 순서대로 24까지 증가시키면서 해당 인덱스의 배열값을 음원 재생용 트랙번호로 DFplayer에 전송하여 셔플의 기능에 맞게 재생되도록 하는 것이다. 하지만 상기의 방식으로는 25개의 정도의 배열이라면 큰 무리가 없으나 100개 이상의 배열에 중복되지 않는 숫자를 채우는 것에는 우리가 인지할 수 있는 시간이 소요되고, 또한 완료되는 시간도 일정치 않게된다.
아래 스케치를 업로드 하고 시리얼 모니터에서 배열이 완성된 시간을 확인해 보자.
// shuffle 변수
uint16_t shuffle[650] = {0, }; // 450: 동적 메모리 54% 사용, 650: 73%
uint16_t shuffle_tracks = 100; // 셔플용 트랙수
unsigned long int atime; // shuffle_num 함수 실행시간 확인 변수
void shuffle_num() {
shuffle[0] = random(1, shuffle_tracks+1);
uint16_t track_count = 1;
bool duplecate = false;
while (track_count < shuffle_tracks) {
uint16_t temp = random(1, shuffle_tracks+1);
for (int i = 0; i < track_count; i++) { // 중복검사
if (shuffle[i] == temp) {
duplecate = true;
break;
}
else if (i == track_count -1 && shuffle[i] != temp) { // 마지막
duplecate = false;
}
}
if (duplecate == false) {
shuffle[track_count] = temp;
track_count++;
}
}
}
void setup() {
Serial.begin(9600);
randomSeed(analogRead(0));
atime = micros();
shuffle_num();
Serial.println(micros() - atime);
Serial.println("shuffle");
for (int i = 0; i < shuffle_tracks; i++) {
Serial.print(shuffle[i]); Serial.print(",");
}
}
void loop() {
}
uint16_t shuffle[650] = {0, }; // 450: 동적 메모리 54% 사용, 650: 73%
상기 변수는 셔플용 배열을 정의한 것이다. DFplayer의 매뉴얼에 3000트랙 이하를 사용할 것을 권장하고 있다. 배열의 크기를 3000까지 하고 싶으나 아두이노의 메모리 문제로 그렇게 할 수는 없다.
배열크기를 450으로 정의하는 것만으로 동적메모리 약 54%를 차지하고 650에서 73%를 차지하여 거의 한계에 다다른다.
동적메모리 74% 이상 될경우 경고 메세지가 출력된다. 실제 제어용 코드의 메모리 사용 용량까지 고려하여 최대 배열 크기를 결정해야 할 것이다.
uint16_t shuffle_tracks = 100; // 셔플용 트랙수
셔플에 사용할 총 트랙수를 정의하는 변수이다. 앞선 셔플용 배열의 크기를 넘어서지 않는 값을 임의대로 입력하여 테스트 해보자.
아래 값은 트랙수에 따른 소요시간(마이크로초)을 5회에 걸쳐서 수집한 것이다.
uint16_t shuffle_tracks = 100; // 트랙수: 100
91496 = 91.496 밀리초 = 0.09초
50420
70388
100760
75164
uint16_t shuffle_tracks = 650; // 트랙수: 650
1581196 = 1581.196 = 1.58초
2080644
1759004
1724564
2007024
트랙수가 증가함에 따라 코드의 완료시간이 급격히 상승하는 것을 볼 수 있고 코드가 완료된 시간이 일정하지 않다. 트랙수가 600개 정도 되면 1초 ~ 2초 정도 소요된다고 볼 수 있는데 이정도면 실 사용에 문제가 발생하게 된다. 셔플 버튼을 누르고 1초 ~ 2초 뒤에 트랙이 재생되기 시작할 것이다.
결과는 비슷하면서도 일정한 시간, 실제로 적용하는데 무리가 없을 시간내에 완료되도록 하기위해서는 앞에서 언급한 "1 ~ 10 까지 번호가 매겨진 10장의 카드가 있을경우, 이를 무작위로 섞은 다음 순서대로 한장씩 보여주는 것"을 구현하면 된다.
1. 숫자의 범위가 정해져 있다. -> 트랙 갯수
2. 트랙 갯수에 맞는 배열이나 더 큰 배열이 있다.
3. 배열에는 인덱스 순서대로 범위내의 숫자가 순서대로 들어가 있다.
1 ~ 25의 숫자면 인덱스0 = 1, 인덱스1 = 2 .......
4. 배열의 숫자를 무작위로 섞는다.
4-1. 랜덤함수로 중복되지 않는 숫자 범위 내의 두개의 배열 인덱스 번호를 생성 시킨다.
4-2. 각각의 배열 인덱스 번호의 숫자를 서로의 배열에 교차 저장 시킨다.
4-3. 상기의 작업을 배열 갯수만큼 실행 시킨다.
이전 방법과 큰 차이점은 같은 숫자의 검증 자체를 하지 않는 다는 것이다. 인덱스 번호의 생성시 두 인덱스의 중복 유무만 검사하게 된다.
// shuffle 변수
uint16_t shuffle[650] = {0, }; // 450: 동적 메모리 54% 사용, 650: 73%
uint16_t shuffle_tracks = 650; // 셔플용 트랙수
unsigned long int atime; // shuffle_num 함수 실행시간 확인 변수
void shuffle_num() {
uint16_t track_count = 0;
uint16_t sh_count = 0;
uint16_t track_num = 0;
while (track_count < shuffle_tracks) {
uint16_t temp = track_count + 1;
shuffle[track_count] = temp;
track_count++;
}
while (sh_count < shuffle_tracks) {
uint16_t index1 = random(0, shuffle_tracks);
uint16_t index2 = random(0, shuffle_tracks);
while (index1 == index2) {
index1 = random(0, shuffle_tracks);
index2 = random(0, shuffle_tracks);
}
uint16_t temp1 = shuffle[index1];
uint16_t temp2 = shuffle[index2];
shuffle[index1] = temp2;
shuffle[index2] = temp1;
sh_count++;
}
}
void setup() {
Serial.begin(9600);
randomSeed(analogRead(0));
atime = micros();
shuffle_num();
Serial.println(micros() - atime);
Serial.println("shuffle");
for (int i = 0; i < shuffle_tracks; i++) {
Serial.print(shuffle[i]); Serial.print(",");
}
}
void loop() {
// put your main code here, to run repeatedly:
}
uint16_t shuffle_tracks = 100; // 트랙수: 100
18460 = 18.460 밀리초 = 0.02초
18812
18264
18640
18448
uint16_t shuffle_tracks = 650; // 트랙수: 650
118840 = 118.840 밀리초 = 0.12초
118452
118688
118488
118688
코드가 완료되는 시간이 많이 줄었을 뿐만 아니라 완료되는 시간들간의 차도 거의 없어졌다. 이제 아두이노에 두코드를 업로드하고 트랙수를 조정해 가면서 트랙번호가 잘 섞이는지 확인해 보면된다.
셔플 순서를 생성하는 코드는 완성되었다. 하지만 상기 코드에서는 셔플용 트랙수를 직접 입력해서 업로드 해야만 작동하게 된다. SD카드의 파일 갯수에 변동이 생길때마다 트랙수를 입력하고 아두이노에 업로드하는 것은 상당히 번거롭게 된다.
셔플용 트랙수를 사용자가 직접 입력해서 결정하지 않고 아두이노가 실행될 때 DFplayer에 SD카드의 셔플용 트랙수를 질의를 하고 그 응답 값을 저장해 놓았다가 Shuffle 버튼이 클릭될 경우 그 값을 트랙 번호로 사용하여 셔플 코드를 실행하고 재생하도록 해보자.
셔플용 트랙수를 결정하기 위해서는 ADVERT 폴더의 유무에 따라 결정되게 된다. ADVERT 폴더가 없다면 root폴더의 전체 트랙수가 셔플용 트랙수가 되어 전체 트랙수를 질의하고 그 값을 사용하면 될것 이지만 ADVERT 폴더가 있다면 전체 트랙수에서 ADVERT폴더의 트랙수를 빼줘야 한다. 하지만 DFplayer 프로토콜중 ADVERT 트랙수를 질의하는 항목이 없어서 질의를 할 수 가 없다. 즉 ADVERT 폴더의 트랙수를 빼주는 방법은 사용할 수가 없다.
DFplayer 프로토콜에서 지원하는 질의 항목을 이용하고 계산에 의해 셔플용 트랙수를 산출해야만 한다.
1. SD카드내의 폴더 갯수를 질의 // ADVERT폴더도 갯수에 포함된다.
폴더 1, 폴더 2, 폴더 ADVERT가 있을 경우 : 3
2. 상기 수신값 3을 이용하여 각 폴더마다 트랙 갯수를 질의 // 번호로된 폴더만 정상 응답
폴더 1 질의 : 6
폴더 2 질의 : 6
폴더 3 질의 : 폴더 3은 없어서 에러코드 수신
3. 정상수신된 폴더내 트랙갯수를 모두 합산 : 셔플용 트랙 갯수
DFplayer 매뉴얼중 폴더 트랙 갯수를 질의 설명
3.7.3 Query number of tracks in a folder
Query number of tracks in folder 01 :: "7E FF 06 4E 00 00 01 FE AC EF"
Query number of tracks in folder 11 :: "7E FF 06 4E 00 00 0B FE A2 EF"
If the folder queried is empty without any files, the module will report an error,
and the data "7E FF 06 40 00 00 06 FE B5 EF" will be returned
질의용 cammand는 "4E" 이고 정상 수신 할경우 응답으로 "4E"가 수신되고 LSB자리에 1 ~ 255의 값이 헥사값으로 수신된다.
에러발생할 경우 응답으로 "40"이 수신된다.
시리얼 통신을 통해 들어오는 데이터를 받는 변수로 char를 많이 사용하게 되는데, 데이터 송신측 프로토콜에 따라 char 변수가 적합하지 않을 수 있다. 아두이노 참조 사이트의 char 변수 설명을 살펴보자.
char [자료형]
설명
문자 값을 저장하는 1바이트 메모리를 차지하는 자료형. 문자 리터를은 작은 따옴표 안에, 이렇게 : 'A' (여러 문자 - 문자열 - 큰 따옴표: "ABC").
그러나 문자는 숫자로 저장된다. 특정 인코딩을 ASCII chart 에서 볼 수 있다. 이것은 문자에 산술 연산이 가능하다는 것을 뜻하는데, 여기서 문자의 아스키 값이 쓰인다 (e.g. 'A' + 1 은 66 값을 가지는데, 왜냐면 대문자 A의 ASCII 값은 65이기 때문). 문자가 어떻게 숫자로 변환되는지에 대한 자세한 것은 Serial.println
레퍼런스를 보시오.
char 자료형은 부호있는 형이므로, 그것은 -128 에서 127 까지 숫자를 인코드함을 뜻한다. 부호없는 1바이트 (8비트) 형은 byte 을 쓰시오.
char 자료형은 부호있는 형이고 -128 에 127 까지 숫자를 인코드한다고 했다. 아스키코드는 0 ~ 127까지 이므로 아스키 코드 번호내의 숫자를 통신에 사용한다면 문제없이 사용할 수 있고 문자로 출력할 수 있다. 하지만 시리얼 통신에서는 1byte (8bit) 단위로 송수신하는데 프로토콜에 따라 -128 ~ 127 범위(부호 있는형) 또는 0 ~ 255 범위(부호 없는형)을 사용하게 된다. 아래와 같이 변수에 char 자료형을 선언하고 시리얼 통신 데이터를 수신할 경우 프로토콜이 부호 있는형(-128 ~ 127 범위)을 사용한다면 문제없지만 프로토콜이 부호없는 형(0 ~ 255 범위)을 사용하고 있다면 0 ~ 127 까지는 문제없이 수신 할 수 있으나 127을 넘어서는 값을 수신할 경우 자료형이 마치 32bit 인것 처럼 작동하여 FFFFFF가 추가되는 현상이 발생하게 된다.
char temp = Serial.read(); <---------- 126 (7E) 255 (FF) 162 (A2)
Serial.println(temp, HEX);
7E
FFFFFFFF
FFFFFFA2
127을 넘어서는 값에 대해 아래처럼 코드를 추가하여 필요없는 FF FF FF값을 제거 시켜도 되지만
char dump = Serial.read(); <---------- 126 (7E) 255 (FF) 162 (A2)
uint8_t temp = dump // 32bit(FF FF FF A2) -> 8bit (A2) : FF FF FF 버림
Serial.println(temp, HEX);
7E
FF
A2
수신용 변수의 자료형과 전송되어온 데이터의 데이터 범위가 서로 맞지않아 발생하는 것이므로 전송측 프로토콜이 시리얼통신을 하기위해 부호있는 형의 값을 사용하는지 부호없는 형의 값을 사용하는지 파악하여 시리얼 데이터를 받는 자료형을 그에따라 결정해주면 간단하게 해결 할 수 있다.
상기의 예처럼 126 (7E) 255 (FF) 162 (A2) 값이 들어 온다는 것에서 127의 범위를 넘어서는 부호 없는 형이 사용된다는 것을 알 수 있다. 따라서 자료형을 부호 없는 형인 byte나 uint8_t를 사용해주면 해결되게 된다. 필자는 부호없는 형의 사용에 있어 uint8_t 형식을 선호한다. uint8_t, uint16_t, uint32_t 처럼 bit수가 바뀔때 혼동을 줄여주고 가독성도 좋기 때문이다.
uint8_t temp = Serial.read(); <---------- 126 (7E) 255 (FF) 162 (A2)
Serial.println(temp, HEX);
7E
FF
A2
아래 스케치를 아두이노에 업로드 해보자.
시리얼 모니터에 수신된 데이터 값이다.
7E FF 6 4F 0 0 3 FE A9 EF
folder_num: 3
folder_count: 1
7E FF 6 4E 0 0 6 FE A7 EF
folder_tracks[folder_count-1]: 6
folder_count: 2
7E FF 6 4E 0 0 6 FE A7 EF
folder_tracks[folder_count-1]: 6
folder_count: 3
7E FF 6 40 0 0 6 FE B5 EF
folder_tracks[folder_count-1]: 0
6,6,
wrong_track: 1
folder_num: 2
shuffle_tracks: 12 // 필요로 하는 값이다.
시리얼 모니터 출력 값
7E FF 6 4F 0 0 3 FE A9 EF
folder_num: 3
folder_count: 1
7E FF 6 4E 0 0 6 FE A7 EF
folder_tracks[folder_count-1]: 6
folder_count: 2
7E FF 6 4E 0 0 6 FE A7 EF
folder_tracks[folder_count-1]: 6
folder_count: 3
7E FF 6 40 0 0 6 FE B5 EF
folder_tracks[folder_count-1]: 0
6,6,
wrong_track: 1
folder_num: 2
shuffle_tracks: 12
7,11,4,8,12,6,9,5,1,10,3,2,
이제 Custom Shuffle용 코드는 완성되었다. arduino bluetooth controller PWM 제어코드에 추가하여 실행해 보자.
Custom Shuffle의 작동 방식은 배열에 저장된 값을 이용해 특정 트랙의 재생명령을하고 재생이 종료될 때 수신되는 반환값을 인식하고 다음 배열 값을 이용하여 트정 트랙이 재생명령을 하게 된다. 여기서 주의할 점은 앞선 테스트에서 확인했듯이 트랙의 재생이 완료되었을때 DFplayer는 "7E FF 06 3D 00 00 01 xx xx EF + 7E FF 06 3D 00 00 01 xx xx EF" 이렇게 같은 값을 두번 전송하게 된다.
두번 수신되는 값을 카운터(df_code_count)를 이용하여 한번만 작동되도록 하여야만 Custom Shuffle 실행시 배열을 건너뛰어 재생되는 오류를 방지 할 수 있다.
else if (df_val == 0x3D && shuffle_play == true) {
df_code_count++;
if (df_code_count == 2){ // 셔플 두번 실행 방지
shuffle_play_track();
df_code_count = 0;
}
}
관련 글
[arduino] - DFplayer - 아두이노 사운드 모듈
[arduino] - NodeMcu(ESP8266)에서 DFplayer를 제어하는 코드
[arduino] - ESP32(DevKit)로 DFplayer 제어하기
[arduino] - 안드로이드 앱 DFcontroller를 이용하여 DFplayer 제어
[arduino] - 아두이노시계 예제, ESP01 WiFi 이용 시간 동기화 하기
[arduino] - 아두이노 말하는알람시계 예제 - DFPlayer
[arduino] - 말하는알람시계 - 블루투스 연결 및 시간 동기화, DFPlayer 제어
[arduino] - NodeMcu - 말하는 알람시계, wifi이용 시간 동기화 및 DFPlayer 원격제어
arduino bluetooth controller PWM - 아두이노 원격제어 안드로이드 앱 버전 3.5 다운로드
arduino bluetooth controller PWM 매뉴얼
'Arduino' 카테고리의 다른 글
안드로이드 앱 DFcontroller를 이용하여 DFplayer 제어 (0) | 2019.08.16 |
---|---|
ESP32(DevKit)로 DFplayer 제어하기 (0) | 2019.08.14 |
ESP32 Serial 통신 라이브러리, software Serial? (0) | 2019.08.14 |
NodeMcu(ESP8266)에서 DFplayer를 제어하는 코드 (0) | 2019.08.14 |
아두이노 - 디지털 도어락 예제, delay() 함수 대체 방법 (14) | 2019.05.27 |
Esp8266 NodeMcu 및 ESP32 Dev Module, stack 오류 (0) | 2019.05.25 |
ESP32 - Dev Module 와이파이 이용 원격제어(soft AP, wifi) (0) | 2019.05.21 |
ESP8266 - NodeMcu 1.0 와이파이 이용 원격제어(soft AP, wifi) (0) | 2019.05.21 |