タイトルにある通り、先日ArduinoNanoを使用して小型MP3プレイヤーを作りました。
僕は毎晩寝る前に決まったストレッチ動画を見るというのがルーティーンになっているのですが、スマホで毎回動画を再生するのが面倒だなと思っていたのです。そこでArduinoNanoを使用してストレッチ音声1曲だけを流す専用のプレイヤーを作りたいと思うようになりました。家の隅に常時置いておいて、ストレッチがしたくなったときにワンボタンで再生できるようにしたいのです。
当初はどのように製作してよいか分かりませんでしたが、ChatGPTの助けを借りつつ、いくつかトラブルもありながら、無事完成させることが出来ました。
製作に必要なもの
そもそも電子工作でどの部品を使えばMP3が再生できるか分からなかったので、早速ChatGPTに相談します。
何度か往復した後、以下の部品を使えばよいということが分かってきます。
MP3データの読み取りですが、DFPlayerという外部モジュールを使えばSDカードからデータを読み取り、スピーカーに音声を出力できるそうです。当初は家に余っていたESP32を使って、いざというときにはWi-Fiで操作できるようにしたかったのですが、ESP32って3Vしか出力できないんですね。今回使用するスピーカーが5V駆動なのでESP32では出力が足らず。ということで5V出力ができるArduinoNanoを使うことにしました。
難解なDFPlayer制御コード
とりあえず見切り発車的に各アイテムを購入。購入したはいいものの制御するためのコードをどのように書けば分からなかったので、とりあえずChatGPTに「ボタンを押したら曲が再生されるプログラム」を出力してもらいました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
#include <SoftwareSerial.h> // ソフトウェアシリアル機能を使うためのヘッダを読み込む #include <DFRobotDFPlayerMini.h> // DFPlayer Mini制御ライブラリのヘッダを読み込む static const uint8_t DF_RX_PIN = 10; // Arduinoが受信するピン番号(DFPlayerのTXから配線する) static const uint8_t DF_TX_PIN = 11; // Arduinoが送信するピン番号(DFPlayerのRXへ配線する) static const uint8_t START_BTN_PIN = 9; // 再生開始用のスイッチを接続するデジタルピン番号(内部プルアップで使用) SoftwareSerial dfSerial(DF_RX_PIN, DF_TX_PIN); // DFPlayer通信用のSoftwareSerialインスタンスを生成する DFRobotDFPlayerMini dfp; // DFPlayer制御用オブジェクトを生成する bool started = false; // 初回再生が開始済みかどうかを示すフラグ(押下でtrueにして以後は無視する) void printDFPDetail(uint8_t type, int value) { // DFPlayerからのイベント詳細をシリアルモニタに表示する関数を定義する switch (type) { // イベント種別に応じて分岐処理を行う case TimeOut: // タイムアウトイベントのとき Serial.println(F("[DFP] Timeout")); // タイムアウトを示すメッセージを出力する break; // 該当ケースの処理を終了する case WrongStack: // スタック不整合イベントのとき Serial.println(F("[DFP] WrongStack")); // 不整合を示すメッセージを出力する break; // 該当ケースの処理を終了する case DFPlayerCardInserted: // SDカードが挿入されたとき Serial.println(F("[DFP] Card Inserted")); // 挿入を示すメッセージを出力する break; // 該当ケースの処理を終了する case DFPlayerCardRemoved: // SDカードが取り外されたとき Serial.println(F("[DFP] Card Removed")); // 取り外しを示すメッセージを出力する break; // 該当ケースの処理を終了する case DFPlayerCardOnline: // SDカードが認識されたとき Serial.println(F("[DFP] Card Online")); // 認識完了を示すメッセージを出力する break; // 該当ケースの処理を終了する case DFPlayerUSBInserted: // USB挿入イベント(未使用) Serial.println(F("[DFP] USB Inserted")); // 参考としてUSB挿入メッセージを出力する break; // 該当ケースの処理を終了する case DFPlayerUSBRemoved: // USB取り外しイベント(未使用) Serial.println(F("[DFP] USB Removed")); // 参考としてUSB取り外しメッセージを出力する break; // 該当ケースの処理を終了する case DFPlayerUSBOnline: // USB認識イベント(未使用) Serial.println(F("[DFP] USB Online")); // 参考としてUSBオンラインメッセージを出力する break; // 該当ケースの処理を終了する case DFPlayerPlayFinished: // 再生完了イベントのとき Serial.print(F("[DFP] Play Finished: ")); // 再生完了の見出しを出力する Serial.println(value); // 完了したトラック番号などの値を出力する break; // 該当ケースの処理を終了する case DFPlayerError: { // エラーイベントのとき Serial.print(F("[DFP] Error: ")); // エラー見出しを出力する switch (value) { // エラーコードに応じて詳細を表示する case Busy: Serial.println(F("Busy")); break; // デバイスビジー状態を表示する case Sleeping: Serial.println(F("Sleeping")); break; // スリープ状態を表示する case SerialWrongStack: Serial.println(F("Serial Wrong Stack")); break; // シリアル不整合を表示する case CheckSumNotMatch: Serial.println(F("Checksum Not Match")); break; // チェックサム不一致を表示する case FileIndexOut: Serial.println(F("File Index Out")); break; // ファイル番号範囲外を表示する case FileMismatch: Serial.println(F("File Mismatch")); break; // ファイル不整合を表示する case Advertise: Serial.println(F("Advertise")); break; // 広告関連エラー(未使用)を表示する default: Serial.println(F("Unknown")); break; // 不明エラーを表示する } // エラーコード分岐の終わりを示す break; // エラーケースの処理を終了する } // エラーケースのブロック終端を示す default: // 未定義のイベント種別のとき Serial.print(F("[DFP] Type=")); // 種別出力の見出しを出力する Serial.print(type); // 種別の数値を出力する Serial.print(F(", Value=")); // 値出力の見出しを出力する Serial.println(value); // 値を出力して改行する break; // デフォルトケースの処理を終了する } // switch文全体の終わりを示す } // printDFPDetail関数の終わりを示す void setup() { // 起動時の初期化処理を定義する Serial.begin(115200); // デバッグ用シリアル通信を開始する delay(300); // 電源安定や周辺初期化のため少し待機する pinMode(START_BTN_PIN, INPUT_PULLUP); // 再生開始スイッチを内部プルアップ入力で設定する(未押下=HIGH/押下=LOW) dfSerial.begin(9600); // DFPlayerとのシリアル通信を9600bpsで開始する delay(200); // DFPlayer側の受信準備を待つため少し待機する Serial.println(F("[SYS] Init DFPlayer...")); // DFPlayer初期化開始のログを出力する bool ok = dfp.begin(dfSerial, true, true); // DFPlayerを初期化する(ACK有効・モジュールリセット有効) if (!ok) { // 初期化に失敗した場合の分岐を行う Serial.println(F("[ERR] DFPlayer init failed. Check wiring, 5V, and SD.")); // 配線や電源やSDを確認するよう促す while (true) { delay(1000); } // 致命的エラーのため無限ループで停止する } // 初期化成否チェックのブロック終端を示す dfp.outputDevice(DFPLAYER_DEVICE_SD); // 再生デバイスとしてSDカードを選択する dfp.EQ(DFPLAYER_EQ_NORMAL); // イコライザを標準設定にする dfp.volume(22); // 音量を0〜30の範囲で22に設定する(必要なら調整する) Serial.println(F("[SYS] Ready. Waiting for button on D9")); // 自動再生せずD9ボタン待ちであることを表示する // dfp.play(1); // 自動再生は行わないためこの行はコメントアウトしておく } // setup関数の終わりを示す void loop() { // メインループ処理を定義する if (!started) { // まだ一度も再生を開始していない場合のみボタン監視を行う if (digitalRead(START_BTN_PIN) == LOW) { // ボタンが押下状態(LOW)かを確認する delay(20); // チャタリング対策として短時間待機して安定化を図る if (digitalRead(START_BTN_PIN) == LOW) { // 依然として押下状態なら有効な押下とみなす started = true; // 初回再生開始済みフラグを立てて今後は監視しないようにする Serial.println(F("[SYS] Button pressed. Start playing #1")); // 再生開始ログを出力する dfp.play(1); // 1曲目(0001.mp3)の再生を開始する while (digitalRead(START_BTN_PIN) == LOW) { delay(10); } // 押しっぱなし対策として離すまで待機する } // 押下確定後の処理ブロック終端を示す } // ボタン状態チェックのブロック終端を示す } // 未開始時監視ブロックの終端を示す if (dfp.available()) { // DFPlayerからイベントが届いているかを確認する uint8_t type = dfp.readType(); // イベント種別を読み出す int value = dfp.read(); // 付随する数値(曲番号など)を読み出す printDFPDetail(type, value); // イベントの詳細をシリアルに表示する if (type == DFPlayerPlayFinished) { // 再生完了イベントである場合の処理を行う Serial.println(F("[SYS] Next track")); // 次の曲へ進む旨をログに出力する dfp.next(); // DFPlayerに次トラック再生コマンドを送る } // 再生完了時の処理ブロック終端を示す } // イベント処理ブロック終端を示す delay(10); // ループ間隔を空けてCPU負荷とノイズを抑える } // loop関数の終わりを示す |
・・・
・・
・
長いし、どのコードが何やってるかサッパリ分からん。
とりあえずサンプルで出してもらったコードに対して一行一行個別に追加質問していくことでなんとか出力してもらったコードについては理解できました。
構想書を作成する
大まかにDFPlayerで制御できることが分かったところで、構想書を作成します。
今回何をやりたいのか、何を試してみたいのか、そういった道筋を示したものを用意しないと行き当たりばったりになってしまうので僕は電子工作をやる際には必ず作るようにしています。
今回やりたいこと
- ArduinoNanoを使用したMP3プレイヤー
- ボタン一つで再生制御を行う
- 省電力化のためにスリープモードを導入する
- 音量については不揮発メモリに保存する
「ボタン一つで再生や停止を制御するのであれば状態遷移がいるな」とか「処理は可読性やメンテナンス性の向上のためにモジュールは細かく分割したほうがいいな」と考えて構想書を作成します。
そして出来上がったのが以下のペライチドキュメント。
設計/実装上の困難
ChatGPT5に不正確な情報を流される
2025年9月現在、ChatGPTがVer4からVer5にアップデートされたのですが、これがどうもプログラミングコードに関しては不正確な情報を出力することが度々あり、そこで色々と苦労をさせられました。
ArduinoNanoでFreeRTOSを使用して定周期タスク制御を行おうとして、タスク制御用のコードを出力してもらったのですが、それが何度コンパイルしてみてもエラーが出る。Arduinoを使用してFreeRTOSを使う場合は#include <Arduino_FreeRTOS.h>を使わないとダメなんですよね。
まさかこんな単純なヘッダファイルのインクルードミスをChatGPTが引き起こすとも思わず、「なんでエラーが起きるんだ!?」と何時間も時間を浪費してしまいました。
ボタン(スイッチ)制御の切り分け方
今回はひとつのボタン(スイッチ)で色んな制御を行おうと目論んでいたわけですが、いざ自分が設計/実装するとなると意外と難しい。「ボタンを素早く2回押しで音量変更」と言っても、「普通に1回ボタンを押す」と「素早く2回押す」をどう切り分ければよいのか。
ChatGPTに相談してみたところ、やたら難しいロジックを考案されたので、こちらが理解できずにChatGPT案は却下。色々と考えを巡らした結果、「1回目のボタン押下から1秒以内に再度ボタンが押されたら『素早く2回押し』と判定する」というロジックとしました。
スリープに入らない
今回挑戦してみたかったスリープ制御。
開発の現場ではよく聞く制御ですが、自分は担当領分ではなかったため傍からいつも眺めていただけですが、自分でも一度動かしてみたいということで挑戦してみました。最終的には電池でこの小型MP3プレイヤーを駆動させたかったので、なるべく省電力にしたかったのです。
スリープ制御については全く分かっていなかったので、ここでもChatGPTに相談します。
スリープに入るとRAMで保持していたデータはリセットされてしまうかと思っていたのですが、ChatGPTによると保持されるとのことでした。また一度スリープに入ると復帰するのにそれなりに時間がかかるのかなとも思っていたのですが、ChatGPTに出力してもらったサンプルコードを試してみると一瞬で復帰したので驚きました。これなら細かくスリープに入って電力を節約しても特にデメリットはなさそうです。
ところがサンプルコードを動かしているときは良かったものの、このMP3プレイヤー制御を織り込んでみると、一度スリープに入ってから復帰が出来ない!
これもChatGPTに丸ごとソースを渡して解析してもらったところ、スリープに入る直前に以下の通信を止めないと、復帰できないことがあると言われてしまったのです。
- Arduino⇔PCのシリアル通信
- Arduino⇔DFPlayerのDFシリアル通信
なのでスリープに入る直前で以下のコードを追加して、通信を停止することで解決ができました。
1 2 |
Serial.end(); // シリアル通信を止める dfSerial.end(); // DFPlayer通信を止める |
無事完成
そこまで複雑なロジックではなかったものの、自分でMP3プレイヤーの制御を考えるとなると意外と時間がかかって、トータルで20時間ぐらいかけてしまった気がします。
当初は電池によるArduinoNanoへの電源供給を目論んでいましたが、ArduinoNanoは電源供給方法がUSB Type-Cしかないので簡単にはいかず、ここは諦めました。代わりにモバイルバッテリーを使って電源供給を行うようにしており、今では毎日寝る前のストレッチに使っています。
いやー今まで作ってきた電子工作の中でも一番有用なアイテムになりましたね。
本当は完成版のソース一式を出したかったのですが、今回僕が作ったソフトは現場のコーディングルールにゴリゴリ従ってコードを書いているので表には出せないものとなりました…(同じ職場の人に見られたら一発で同業だとバレるので)
電子工作のススメ
ChatGPTのようなAIが登場した昨今、電子工作へのハードルは劇的に下がっており、特に駆け出しエンジニアの方にとってはぜひ挑戦してほしいアクティビティだなと思っています。
電子工作をやるメリット
- プログラムに対する理解が深まる
- 自己肯定感が高まる
- 今後の転職の際の武器になる
プログラムに対する理解が深まる
やっぱり自分の手で動かして失敗してみて/成功してみてという経験は非常に重要だと思うんですよ。
それなりに歴史のある開発現場だと過去の失敗事例がコーディングルールとして体系付けられているので、ルールに従えば大きく失敗することなくプログラムを作れてしまいます。ですが「なぜそのルールが存在しているのか」というところに思いを馳せれている状態で開発をやれているか否かってソフトの品質や、本人の仕事の楽しさに大きく影響します。
僕自身の経験で言えばお恥ずかしい話ですが、エンジニアになって1年ぐらいが経過して初めて電子工作に挑戦して、その段になってやっとヘッダファイルが何のために必要になってくるか理解できました。列挙体も「なんでこれ使うんだろ。別にdefineマクロで良くない?」と思っていたところ、自分で作ったプログラムがそこそこ大きくなって手戻り的にあとから各所に修正をいれる際に「あー! こういうときに修正不要になるから列挙体ってあるんだ!!」と腹落ちすることが出来ました。
そういった実感を得てからは実装でのミスが大幅に減りました。
自己肯定感が高まる
自分が元々文系大学上がりの事務職人間だったこともあって、こういうプロダクト(というにはあまりにも小規模なアイテムですが)を生み出せると自己肯定感が高まります。
Arduinoを使用してMP3プレイヤーを作るなんて10年前には思いつきもしませんでしたし、それを実行して完成にまで持っていけるなんて自分でも驚いています。
今後の転職の武器になる
電子工作をやるメリットって割とこれが一番デカいかなーと思っています。
転職の面接の場で自分のスキル感を伝えるのって非常に難しいのですが、こうやって自分が作成した電子工作をポートフォリオにまとめて面接に持っていくとすごく喜ばれるんですよね。
何のために電子工作を行ったかであるとか、その電子工作を行うことでどんな学びがあったかも簡単にまとめられると非常に良いです。僕の場合でいうと今回は「いつも聞いているストレッチ音声を手軽に流せるようにしたい」ですし、電子工作を行う上で「ボタン制御の方法」「スリープの動作の理解」「不揮発メモリ(EEPROM)の使い方」が理解できました。
以上がArduinoNano/DFPlayer/マイクロスピーカーで小型MP3プレイヤーを作った報告となります。
ChatGPTを使いこなせば非常に簡単に電子工作が出来てしまうので駆け出しエンジニアの方は是非挑戦してみてください!
-
-
転職の面接ではパワポで自分の強みをプレゼンしろ!
口下手が故に転職の面接でうまく自己PRができない 「○○について聞いてくれていたらもっとうまく喋れたのに」と面接後によく後悔する 自己PRをうまく行う方法について知りたい 今回はこのような疑問にお答え ...
続きを見る