DFPlayerを使う-ソフトウェア編

DFPlayerを使う。今回はソフトウェア編です。前回作成した回路を、音楽プレイヤーとして動かすためのスケッチを作っていきます。シンプルで、コンパクトな音楽プレイヤー、題して「SIMPLE PLAYER」を作ります。恥ずかしながら、この手の制御系ソフトウェアは作ったことがありません。したがって、手探りで作り上げることになります。はたして、出来上がるのでしょうか?全く自身はありません。
SIMPLE PLAYERに求められる機能
失敗を繰り返しながら、思いつくままにスケッチを作るのは楽しい作業です。しかし、完成のイメージを定めておかないと、完成は遠い先になってしまいそうです。そこで、今回作るSIMPLE PLAYERに盛り込む機能を決めようと思います。盛り込む機能は以下のとおりです。
- MicroSD内の音声ファイルを再生する
- 一番目のファイルの再生を終えたら二番目のファイルを再生し、順次ファイルを再生する
- 最後のファイルの再生を終えたら一番目のファイルに戻って再生を続ける
- 随時イコライザの切り替えができる
- 電源が切られたときのイコライザ設定、再生中のファイルを保持する
- 電源ONで、前回電源OFF時のイコライザ設定に戻し、再生中だった曲から再開する
- 電源OFF中にMicroSDが入れ替えられた場合には、最初のファイルから再生する
以上の機能が実装できれば、ランダム再生や一曲リピートなども容易に追加できるはずです。
SIMPLE PLAYER開発:コマンドで躓く
SIMPLE PLAYERスケッチを書くにあたっては、DFPlayerのマニュアルを参照しました。しかし、マニュアルに書かれたとおりに動作しないことが結構ありました。仕方なく、GitHubにあるDFPlayerのライブラリを参照しました。すると、マニュアルと異なる部分をいくつか見つけました。例えば、MicroSDのファイル数を取得するコマンドは、こう書かれていました。



TFカード(中国ではMicroSDをTF cardと表記されることが多いです。)のファイル数取得は0x47と記載されています。しかし、GitHubにあるDFPlayerのライブラリを見ると0x48になっていました。



どうりで何度やってもうまくいかないわけです。マニュアルの参照を止め、ライブラリソースの解析から始めることにしました。SIMPLE PLAYERのスケッチ作成がこんなに困難だとは・・・。それにしても、マニュアルのミスに誰も気づいていないのでしょうか?
DFPlayerが気の遠くなるほど遅くて困った
MicroSDカードの入れ替えが検出されない場合は、電源ONで再生を開始します。しかし、電源ONと同時に再生コマンドを送っても、全く受け付けてくれません。SIMPLE PLAYERの完成が一気に遠ざかったように感じました。しかし、ここで挫折する訳にはいきません。そこで1秒程ディレイをかけてコマンドを送ることで、再生が開始されるようになりました。
しかし、試験を重ねていくと1秒のディレイでは足りないようになってしまいました。また、SIMPLE PLAYERの完成が遠ざかってしまいました。
一時はDFPlayerを壊してしまったのかとも思いました。しかし、再生可能となるまでの時間は、ファイル数で増減することが分かりました。そこで、SIMPLE PLAYERの起動ルーチンで、BUSY信号の監視をするようにしました。つまり、BUSY信号がOFFになるまで、ポーリングするようにしたわけです。
しかし、これもうまくいきませんでした。そこで、ロジックアナライザーを使って、BUSY信号のシーケンスを見て驚きました。なんと、DFPlayerの起動シーケンスで、2回のBUSYが発生していました。一度目は初期化プロセスでした。そして二回目のはMicroSDをシークして、ファイル数をカウントしているようでした。
二度目のBUSYは、最大で5秒程になることがテストの結果分かりました。SIMPLE PLAYERは起動の遅い音楽プレイヤーになることが決定的となりました。
DFPlayerにはハンドシェイク信号がありません。しかし、BUSY信号を使った、疑似的なハンドシェイクの必要性に気づけば、意外と使いやすいです。
SIMPLE PLAYER開発:何とか完成
ちいさなDFPlayerを使うのが、こんなに難しいとは思ってもいませんでした。しかし、GitHubにあったDFPlayerライブラリには随分助けられました。マニュアルの情報だけではSIMPLE PLAYERは完成しなかったでしょう。下の写真は、漸く動くようになったSIMPLE PLAYER稼働中の様子です。



素人が作った、恥ずかしいスケッチですが、備忘録の意味も含め、以下に置いておきます。
/* Play audio file with DFPlayer*/
#include <Wire.h> // OLED display
#include <Adafruit_SSD1306.h> // OLED display
#include <EEPROM.h> // EEPROM storage
#include <avr/sleep.h> // MCU power control
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
volatile uint8_t PIN_PORT;
volatile uint8_t PIN_NUM;
// DFplayer command structire
#define DFPLAYER_SEND_LENGTH 10
#define SB 0x7E //start byte
#define VER 0xFF //version
#define LEN 0x6 //number of bytes after "LEN" (except for checksum data and EB)
#define FEEDBACK 1 //feedback requested
#define NO_FEEDBACK 0 //no feedback requested
#define EB 0xEF //end byte
// DFplayer command
#define NEXT 0x01
#define PREV 0x02
#define SETTRACK 0x03
#define VOLUP 0x04
#define VOLDN 0x05
#define SETVOL 0x06
#define SETEQ 0x07
#define PBMODE 0x08
#define PBSRC 0x09
#define STANDBY 0x0A
#define NORMAL 0x0B
#define RESET 0x0C
#define PLAY 0x0D
#define PAUSE 0x0E
#define FOLDER 0x0F
#define VOLADJ 0x10
#define REPEAT 0x11
#define RAND 0x18
// DFplayer query commands
#define INIT_SRC 0x3F
#define GET_VOL 0x43
#define GET_EQ 0x44
#define GET_TRACKS 0x48
#define GET_TRACK 0x4B
// EEPROM addresses
#define ADDR_TRACKS 0x180
#define ADDR_TRACK 0x182
#define ADDR_EQ 0x184
#define ADDR_VOL 0x185
// variables for DFplayer
uint8_t sending[DFPLAYER_SEND_LENGTH] = { 0 };
uint8_t response[20];
uint8_t commandValue = 0;
uint8_t feedbackValue = 0;
uint8_t paramMSB = 0;
uint8_t paramLSB = 0;
uint8_t checksumMSB = 0;
uint8_t checksumLSB = 0;
uint16_t Tracks;
uint16_t Track;
uint8_t Folders;
bool Status; // false:PAUSE true:PLAYING
uint8_t Volume;
uint8_t Length;
uint8_t EQ;
// variables for PIN operation
volatile uint8_t P_B;
void setup() {
int i;
uint16_t TEMP_TRACKS;
uint16_t TEMP_TRACK;
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(SSD1306_WHITE); // Draw white text
display.clearDisplay();
display.setCursor(22, 14);
display.print("SIMPLE PLAYER");
display.display();
MCUCR &= ~(0 << PUD); // turn off pull up canceling bit
DDRB &= ~B00011111; // pin 8 to 12 in to read mode
PORTB |= B00001111; // pullup 8,9,19,11 except pin 12. because pin12 is active high
DDRB |= B00100000; // PIN 13 into output mode for debug
PCIFR &= ~B00000001; // clear interrupt flag
/*
PCICR &= ~B00000001; // disabling Pin change interrupt 0
PCMSK0 &= ~B00011111; // disabling PCINT 4::0
*/
PCIFR &= ~B00000001; // clear interrupt flag
PCICR |= B00000001; // enabling Pin change interrupt 0
PCMSK0 |= B00011111; // enabling PCINT 4::0
// start serial port for communication with DFplayer
Serial.begin(9600);
// reset DFPlayer
commandValue = RESET;
feedbackValue = NO_FEEDBACK;
paramMSB = 0;
paramLSB = 0;
findChecksum();
sendData();
// wait for bootup DFPlayer
busyWait();
// wait for complete scanning TF card
busyWait();
// get tracks
getTracks();
// wait for complete query
busyWait();
// read EQ setting from EEPROM
EQ = EEPROM.read(ADDR_EQ);
// set EQ
setEQ();
busyWait();
// read current tack and total tracks from EEPROM
TEMP_TRACK = (EEPROM.read(ADDR_TRACK) << 8) + EEPROM.read(ADDR_TRACK + 1);
TEMP_TRACKS = (EEPROM.read(ADDR_TRACKS) << 8) + EEPROM.read(ADDR_TRACKS + 1);
if (TEMP_TRACK > TEMP_TRACKS) TEMP_TRACK = 1;
// check SD card has exchanged
if (TEMP_TRACKS == Tracks) {
// set current track and start playing
Track = TEMP_TRACK;
playTrack();
Status = true;
} else {
// reset current track
Status = false;
Track = 1;
}
// get volume setting
getVolume();
// display current status
updateDisplay();
}
void loop() {
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // MCU power down and wait for PIN change interrupt
switch (PIN_NUM) {
case 8: // push PREV button D8
{
// previous command to DFplayer
if (Track > 1) Track--;
//commandValue = SETTRACK;
playTrack();
break;
}
case 9: // push PLAY/STOP button D9
{
if (Status == false) {
Status = true;
// play command to DFplayer
resumePlay();
} else {
Status = false;
// PAUSE command to DFplayer
pause();
}
break;
}
case 10: // push NEXT button D10
{
playNext();
busyWait();
break;
}
case 11: // push EQ button D11
{
// change EQ setting
EQ++;
setEQ();
updateDisplay();
break;
}
case 12: // incoming busy signal to D12
{
if (Status == true) {
playNext();
}
break;
}
}
PIN_NUM = 0;
delay(200); // avoid for chattering
}
/*
PIN change interrupt handler
*/
ISR(PCINT0_vect) {
PIN_PORT = ~((PINB & B00001111) | B11110000); // extract D8::D11 pin status
switch (PIN_PORT) {
case B00000001:
PIN_NUM = 8;
break;
case B00000010:
PIN_NUM = 9;
break;
case B00000100:
PIN_NUM = 10;
break;
case B00001000:
PIN_NUM = 11;
break;
default:
if ((PINB & B00010000) == B00010000) PIN_NUM = 12; // check D12 pin
else PIN_NUM = 0;
}
}
/*
wait for done the busy status
*/
void busyWait() {
uint8_t i;
for (i = 0; i < 255; i++) { // detect busy signal rizing
if ((PINB & B00010000) == 0) delay(1);
else break;
}
for (i = 0; i < 255; i++) { // detect busy signal falling
if ((PINB & B00010000) != 0) delay(10);
else break;
}
PCIFR &= ~B00000001; // clear interrupt flag
}
/*
calcurate checksum
*/
void findChecksum() {
uint16_t checksum = (~(VER + LEN + commandValue + feedbackValue + paramMSB + paramLSB)) + 1;
checksumMSB = checksum >> 8;
checksumLSB = checksum & 0xFF;
}
/*
send command to DFPayer
*/
void sendData() {
clearBuffer();
sending[0] = SB;
sending[1] = VER;
sending[2] = LEN;
sending[3] = commandValue;
sending[4] = feedbackValue;
sending[5] = paramMSB;
sending[6] = paramLSB;
sending[7] = checksumMSB;
sending[8] = checksumLSB;
sending[9] = EB;
Serial.write(sending, DFPLAYER_SEND_LENGTH);
}
/*
get response from DFPlayer
*/
uint8_t getResponse() {
uint8_t pointer;
uint8_t i;
// wait for filled buffer
for (i = 0; i < 200; i++) {
if (Serial.available() < 9) delay(10);
else break;
}
// read response code
pointer = 0;
while (Serial.available()) {
response[pointer] = Serial.read();
pointer++;
}
return pointer;
}
/*
Clear serial buffers(RX and TX)
*/
void clearBuffer() {
// clear RX buffer
while (Serial.available()) {
Serial.read();
delay(1);
}
// clear TX buffer
Serial.flush();
}
/*
play next file
*/
void playNext() {
if (Track == Tracks) Track = 1;
else Track++;
playTrack();
}
/*
play specified track
*/
void playTrack() {
// play specified track
commandValue = SETTRACK;
feedbackValue = NO_FEEDBACK;
paramMSB = Track >> 8;
paramLSB = Track & 0xFF;
findChecksum();
sendData();
busyWait();
getTrack();
updateDisplay();
}
/*
resume play
*/
void resumePlay() {
Status = true;
// resume plaing
commandValue = PLAY;
feedbackValue = NO_FEEDBACK;
paramMSB = 0;
paramLSB = 0;
findChecksum();
sendData();
// DO NOT WAIT BUSY SIGNAL HERE!!
}
/*
pause
*/
void pause() {
commandValue = PAUSE;
feedbackValue = NO_FEEDBACK;
paramMSB = 0;
paramLSB = 0;
findChecksum();
sendData();
// DO NOT WAIT BUSY SIGNAL HERE!!
}
/*
get VOLUME
*/
void getVolume() {
uint8_t i;
clearBuffer();
// get volume
commandValue = GET_VOL;
feedbackValue = NO_FEEDBACK;
paramMSB = 0;
paramLSB = 0;
findChecksum();
sendData();
if (getResponse() > 9) {
Volume = response[6];
}
}
/*
set EQ
*/
void setEQ() {
uint8_t i;
if (EQ > 5) EQ = 0;
commandValue = SETEQ;
feedbackValue = NO_FEEDBACK;
paramMSB = 0;
paramLSB = EQ;
findChecksum();
sendData();
EEPROM.update(ADDR_EQ, EQ);
delay(4); // wait for done EEPROM written
}
/*
get EQ
*/
void getEQ() {
// get EQ
commandValue = GET_EQ;
feedbackValue = NO_FEEDBACK;
paramMSB = 0;
paramLSB = 0;
findChecksum();
sendData();
if (getResponse() >= 9) {
EQ = response[6];
}
}
/*
get current track
*/
void getTrack() {
EEPROM.update(ADDR_TRACK, (Track >> 8));
delay(4); // wait for done EEPROM written
EEPROM.update(ADDR_TRACK + 1, (Track & 0xFF));
delay(4); // wait for done EEPROM written
}
/*
get tracks
*/
void getTracks() {
// get tracks
commandValue = GET_TRACKS;
feedbackValue = NO_FEEDBACK;
paramMSB = 0;
paramLSB = 0;
findChecksum();
sendData();
if (getResponse() >= 9) {
Tracks = (response[5] << 8) + response[6];
EEPROM.update(ADDR_TRACKS, (Tracks >> 8));
delay(4); // wait for done EEPROM written
EEPROM.update(ADDR_TRACKS + 1, (Tracks & 0xFF));
delay(4); // wait for done EEPROM written
}
}
/*
get current track,tracks,volume,eq and display
*/
void getStatus() {
// get volume
getVolume();
// get EQ
getEQ();
// get current track
getTrack();
// get tracks
getTracks();
// Display status
updateDisplay();
}
/*
update OLED display
*/
void updateDisplay() {
// update display
display.clearDisplay();
display.setCursor(0, 0); // Start at top-left corner
display.print("Track:");
display.print(Track);
display.setCursor(64, 0);
display.print("Tracks:");
display.print(Tracks);
display.setCursor(0, 16);
display.print("EQ:");
switch (EQ) {
case 0:
display.print("NORMAL");
break;
case 1:
display.print("POP");
break;
case 2:
display.print("ROCK");
break;
case 3:
display.print("JAZZ");
break;
case 4:
display.print("CLASSIC");
break;
case 5:
display.print("BASS");
break;
}
display.setCursor(64, 16);
display.print("VOL:");
display.print(Volume);
display.display();
}