お知らせ:この記事はJLCPCBの提供でお送りしています。
少し間が空いてしまいましたが、今回は現在制作中のESP32+FPGAオーディオ基板を使ったPCM音源のESP32側のソフトウェアの話をしたいと思います。
ESP32が担当する処理
まずはESP32側で行っている処理の全体像を紹介します。上の図は、前回の記事でも紹介した、ESP32+FPGAオーディオ基板の構成図です。この図の通り、メインのMCUであるESP32には、
- PCと通信するためのUSB-UART IC
- MIDI対応機器と通信するためのMIDIレシーバ回路
- 音源機能を実装するためのFPGA
が接続されています。基本的にはこれらの回路を動かすための機能をESP32に実装していくことになります。また、上記に加えて、今回はFPGA上の回路を簡単にするため、FPGAの先に接続されているSPI Flashへの書き込み機能もESP32が担当する設計としています。以下で順にどのような実装をしているか説明していきます。
PCとの通信機能
この基板はUSB-UART変換ICのCH340K(WCH社)を経由して、PCとシリアル通信できるようになっています。今回はESP32へのファームウェア書き込みと、デバッグ用のログの出力という、いたって普通の用途に使っています。ログ出力については一応、printf()ではなくESP-IDFの標準ログライブラリを使うようにしています。
ちなみに、今回は後述する通り、FPGAのコンフィギュレーション1もESP32から行っているため、FPGAのコンフィギュレーションデータもESP32のFlashに書き込んでいます。コンフィギュレーションデータはESP-IDFに入っているSPIFFSライブラリを使ってESP32上のプログラムから読み出せる形式で書き込んでいます。この手のプログラム本体以外のデータをプログラム本体と一緒にマイコンのFlashに書き込もうとすると、普通だとまあまあ面倒だったりする(そしてとりあえず超巨大な const uint8_tの配列が出来上がったりする)のですが、ESP-IDFとSPIFFSの組み合わせだとCMakeLists.txtに1行書くだけで特定のフォルダの中身をプログラム書き込み時に一緒にFlashに書き込むようにできたりして、なかなか便利です。
MIDIデータの受信
今回実装するPCM音源を外部から制御できるように、この基板には電子楽器の演奏データの送受信に使われるMIDI2規格に対応したMIDIレシーバ回路を実装しています。MIDIレシーバ回路と書くと大げさですが、基本的にはシリアル通信をフォトカプラで一度受けてからESP32に入力しているだけです。
フォトカプラから入ってくるMIDIの信号は31250bpsのシリアル信号なので、ESP32上のソフトウェアとしてはまずESP32のUARTを初期化します。その後はひたすら受信したデータを解釈して、音源機能を担うFPGAを制御する処理を実装するのみとなります。
受信したMIDIメッセージを解釈するMIDIパーサについては、今回は過去作品であるFM音源を作った時に書いたMIDIパーサを使いまわしています。
FM音源を作る際にせっかくだからと流用しやすいようにと、C++を使って真面目に書いていたのが活用できた形です。今のところソースは公開していないですが、ざっくり下記のような設計になっています。
- 入力データを解釈するステートマシンと各MIDIメッセージに対応した仮想関数のハンドラをドライバ部として実装
- アプリケーション側は必要なハンドラ関数だけをオーバーライドして使う
このような設計とすることで、アプリケーション側のソースには必要なハンドラ関数だけしか出てこないように実装できるので、アプリケーション側について、MIDIの基本的な機能から順に対応させていくよう実装していったときに、その時点では無駄なソースコードがあまり目に入らないように実装できます。また、当たり前ではありますが、ドライバ側はそのままほかのMIDIを使うプロジェクトに流用できるので便利です。
MIDI規格は80年代の電子楽器の非力なCPUでも対応できるように設計されているため比較的シンプルな通信規格ですが、うっかり適当にパーサを作ると変なバグを作りこんで難儀することがあるので、一度しっかり書いてできるだけ使いまわすか、ありもののライブラリを使うのがオススメです。MIDIにはランニングステータスという、特定の条件下で送信するデータの一部を省略できるルールがあるのですが、実は私はこのランニングステータスが適用される「特定の条件」3を長いこと勘違いしていて、以前、あるMIDIデータを流し込んだ時にごくまれに音が鳴らなくなるというバグを作りこんでしまったことがあったのでした。
FPGAの制御
「PCとの通信機能」のところで触れたように、この基板ではFPGAのコンフィギュレーションもESP32から行う設計になっています。
一般的にFPGAのコンフィギュレーションはJTAG4の信号線か、コンフィギュレーション用のメモリを接続するピンを使ってFlashメモリから行いますが、今回使用しているiCE40 Ultra(Lattice Semiconductor社)は一般的なSPIの信号線を使ってコンフィギュレーションをすることができます。また、コンフィギュレーションが完了した後はコンフィギュレーション用のピンを書き込んだ回路の入出力ピンとして使えるようになっています。この特徴を使い、コンフィギュレーション用の信号線と、FPGA上のPCM音源を制御するための信号線を共通化して、ESP32とFPGAを接続する配線の数を減らしています。
後ほど説明するESP32からSPI Flashにデータを書き込む機能も含めた3つの機能は、基本的にはSPIバスを使って通信を行います。そのため、ESP32側のソフトウェアではまず、SPIのペリフェラルを初期化します。ESP-IDFでは、SPIバス全体の初期化の他、その先に接続されるデバイスごとの設定を行ってからSPIを使うお作法になっているので、
- FPGAをコンフィギュレーションするための設定
- FPGA内のPCM音源回路を制御するための設定
- FPGAの先に接続されているSPI Flashを読み書きするための設定
をそれぞれ、違うデバイスに対する設定として定義し、初期化しています。
コンフィギュレーション
FPGAのコンフィギュレーションについては、あらかじめESP32のFlashメモリに書き込まれているコンフィギュレーションデータをSPIFFSライブラリを使って読み出し、iCE40のテクニカルノートに記載の手順でひたすらFPGAにそのデータを送り込むことによって行います。今回の環境では、電源投入時に自動的にコンフィギュレーション処理が走るような実装としています。
PCM音源の制御
FPGA内のPCM音源は回路の簡単化のために、MIDIを直接解釈できるような設計にはなっていません。MIDI規格の発音を開始するためのメッセージであるノートオン・メッセージを受信した際、ESP32側のソフトウェアではMIDIパーサの出力を受けて下記のような処理を行い、その結果をSPIバスを経由してFPGAに送信しています。
空きスロットの探索
FPGA内のPCM音源は現在のところ16個の音を同時に鳴らすことができる設計になっています。ある1つの音を鳴らすための処理を便宜上「スロット」とカウントすると、今回のPCM音源は16スロットの発音能力があるということになります。
ここで、ある音がすでに鳴っているときに、別の音を鳴らしたいというノートオン・メッセージが来た場合、もしすでに使用しているスロット数が16に満たない場合は、空いているスロットに新しく来たノートオン・メッセージを割り当てます。
一方、すでに16個のスロットをすべて使いきっている場合、すでに使用中のスロットを何らかの基準で選び、新しく来たノートオン・メッセージに対して割り当て直す必要があります。今回は空きスロットがない場合、LRU(Least Recently Used)方式、つまり、今発音している中で最も発音開始が早かったスロットを選んで新しく来たノートオン・メッセージに割り当てています。
以前作ったFM音源では、この再割り当てのルールが若干異なり、空きスロットがない場合はまず、各スロットの発音状況を調査し、「発音中かつ、ノートオフ・メッセージはまだ来ていない状態」「発音中だが、すでにノートオフ・メッセージを受信している」の2状態を確認しています。ノートオフ・メッセージは鍵盤楽器で言うところの鍵盤を離す動作を意味するMIDIメッセージです。したがって、これをすでに受信しているのであれば、音が出ていたとしても残響音のような比較的小さな音である可能性が高いといえます。過去作のFM音源では、これを考慮して、後者の状態のスロットを優先して再割り当てに回す処理が入っています。後者の状態のスロットがない場合は、今回の音源と同様にLRUによる再割り当て処理が行われます。
今回のPCM音源で上記の発音状況を考慮した再割り当て処理を入れていないのは、まだ発音状態をFPGAから読み出せるようにしておらず、「発音中だが、すでにノートオフ・メッセージを受信している」の状態が確認できないためです。現状では、ノートオフ・メッセージを受信した場合、そのメッセージと対応したスロットは空きスロットとして扱うようにしています。
発音データの指定
ノートオン・メッセージを割り当てるスロットが確定した後は、ノートオン・メッセージの内容に基づいて、FPGAに接続されているSPI Flashメモリのどの場所のデータを再生すればよいか、どのくらいの長さのデータを再生するのか、また再生スピードはどのくらいにすればよいかを決定します。このデータについては、SPI Flashメモリに書き込むデータをPC上で生成するときに、事前に計算してテーブルとして持つ実装としています。この辺のツールの紹介も追い追い書こうと思います。
発音開始指示
発音するための設定データが求まった後は、それらをFPGAに対して送り、最後にFPGA内のレジスタに発音開始フラグを立ててやることで、無事に発音が開始されます。
SPI Flashの書き込み
長々とPCM音源の制御について書いてしまいましたが、最後にFPGAの先に接続されているSPI Flashメモリへの書き込み処理についても説明しておきます。
今回使用しているSPI Flashメモリは128Mbit、つまり16Mbyte品になります。これが2つ、基板上に搭載されているため、基板1枚当たり32MbyteのSPI Flashメモリが搭載されていることになります。ESP32のRAMや、内蔵Flashメモリはこれだけのデータをまとめて保持しておくだけの容量がないため、SPI Flashメモリに書き込む内容は何かしらの方法で外部から少しずつ受け取り、順にSPI Flashメモリに書いていく実装とする必要があります。今回はESP32が持つWiFi通信機能を利用し、ESP32がサーバ、PCがクライアントとなるような形で、PCからSPI Flashメモリに書き込むファイルを送り込むような実装としています。USB-UARTで数Mbyteのファイルを送ると結構な時間がかかりますが、WiFiを使っているため、そこそこ高速にファイル転送ができています。
FPGA上には、外部のピンの制御によって、ESP32からのSPIバスの信号をSPI Flashメモリに「素通し」する回路を入れてあるので、SPI Flashメモリ自体の制御手順はいたって普通のものになっています。
余談:謎のエラー
実は今回の基板はすんなり立ち上がらず、テスト用のファームウェアを書きこもうとした際に”A fatal error occurred: Serial data stream stopped: Possible serial noise or corruption.”というエラーが出てしまって、デバッグに苦労したのでした。
エラーメッセージを信じて、USB-UART周りの問題かと思ってあれこれ試行錯誤したのですが、なんと起動時に動作モードを決めるために使われるStrapping Pinの設定ミスでした。
上の表はESP32のデータシートから引用したものです。GPIO2の設定は
- SPI Boot(通常動作)モードの時は不問(Don’t-care)
- Download Boot(書き込み)モードの時は”0″
とあります。しかし今回はうっかりGPIO2を拡張用のI2Cバスに割り当てていたため、何も考えずにプルアップしていました。そのためDonwload Bootモード時の挙動がおかしくなっていたようです。自分が悪いのは間違いないのですが、エラーメッセージと実際の原因がちょっと違っていて、少し苦労しました。
GPIO2のプルアップをしていたのはESP32のすぐ左隣にある赤丸の位置の小さい(1005:1.0mm x 0.5mm)の抵抗だったので、せっかくJLCPCBさんで実装していただいたのですが、泣く泣く取り外したのでした…
まとめ
今回はESP32+FPGAオーディオ基板でPCM音源を作るにあたり、MCUであるESP32側に実装しているソフトウェアの内容について紹介しました。次回はFPGA側の実装内容について紹介する予定です。お楽しみに!
- FPGAに回路情報をロードすること
- Musical Instrument Digital Interface
- わかる人向けに書いておくと、単純にステータスバイトが来たら無条件でランニングステータス用のステータスバイトを保持する変数を更新する実装をするとコケるのです…(詳細はMIDI1.0規格書を読んでね!)
- Joint Test Action Group:基板やLSI内部の検査用の規格の通称。FPGAはJTAGを転用したコンフィギュレーション機能を持つことが多いです。