RP2040/RP2350とデュープレックスI2S
はじめに
Raspberry Pi Pico / Pico 2に使用されているRP2040/RP2350にはI2S IOが無い。そこで、RasPi Pico extra SDKをはじめとして、PIO(Programmable IO)による実装が行われている。
PIOとPIOによるI2S実装に関しては以下が詳しい。
ざっと見たところ、RasPi Picoに対するI2Sインターフェースは出力だけの実装ばかりが見つかる。RP2040はFPUを持っていないため、信号処理向きではないことを考えれるとこれは妥当である。
一方でRP2350はFPUを持っているので信号処理に使うことも考えられる。
そこで、すでに実装例があるかどうかは別として、PIOの理解を深めるためにデュープレックスI2Sの実装について考えてみる。
なお、RP2040/RP2350のデータシートにはPIOの応用例として3-pin I2Sと書いている。あまり考えずに3pinとしたのか、デュープレックス実装が不可能であるのかは調べる必要がある。
マスターかスレーブか
I2Sデバイスには二つの種類がある。
- マスター : BCLKとWS信号を出力する。
- スレーブ : BCLKとWS信号を入力する。
要するに タイミングを自分で作るか他者に作らせるかになる。参考にすべき次の2つはいずれもマスターである。
マスターにする場合
マスターにする利点はPIOのSide-Set機能を使うことができる点にある。Side-SetはPIOの命令を実行しながら同じサイクルでGPIOピンを制御する機能である。これはある種のVLIWだと思えばよい。PIO 1命令とSide-Set 2信号の合計3命令を並列実行するVLIWアーキテクチャと言える。
マスターにする欠点は、RP2040のクロック周波数に制限が発生することである。PIOの命令サイクルはRP2040のクロックの整数分の1に制限される。そのため、48kHzや96kHzといった標準的なFs(サンプル周波数)を使いたければRP2040のクロック周波数に妥協が必要になる。これはRasPi Picoをそのまま使えないことを意味する。
(追記)PIOのクロック周波数は分数分周器によって任意周波数に設定できる。しかしながら、分数分周器を使えばPIOクロックにジッターが購入し、これはそのままI2Sクロックのジッターになる。そのため、48kHzや96kHzといった標準的なFs(サンプル周波数)を使いたければRP2040のクロック周波数をI2SのBCLK周波数の整数倍にするような妥協が必要になる。これはRasPi Picoをそのまま使えないことを意味する。
逆に、Fsを標準的なものにする必要が無いのなら、RP2040のクロックはデフォルトでよい。
スレーブにする場合
スレーブにする利点は、クロックの制限がなくなることだ。
一方で、欠点はプログラムが長くなることだ。 Raspberry Pi PicoのPIOプログラミング(I2Sインターフェースの実装)では4PIO命令サイクルで1BCLK周期を作っている。これをデュープレックスにすると、おそらく6PIO命令サイクルになる。ところが、スレーブの場合はWAIT命令でBCLKの両エッジを待つため、おそらくは8PIO命令サイクルが必要になる。加えて、WSのエッジも待たなければならない。
32命令分しかロードできないPIOで実装できるか否かは疑問が残る。
FIFOとの読み書き
PIOはFIFOからの読み出し、FIFOへの読み出しを自動で行う機能(Autopull/Autopush)があるが、これには注意が必要である。
例えばFIFOからの読み出しの場合、仮にPIOのステートマシンを動かし始めてからDMAを起動するとタイミングによっては次のようなことが起きる。
- 左チャンネルデータをpullするタイミングでFIFOが空である。
- PIOはFIFOからデータを取り出すのを諦めて、Out Shiftレジスタからごみの値(all-0?)を送信する。
- プログラムがDMAを起動し、DMAが左チャンネルデータをFIFOに書き込む。
- 右チャンネルデータをpullするタイミングでFIFOから左チャンネルデータを読み込んでしまう。
これを避けるには
- 送信DMAを先に起動する
- 送信FIFOに2ワード分のデータが溜まるまで送信しない
と言った方法が考えられるが、それらが実装可能か否かは調査する必要がある。
左右チャンネルのアライメント
前のコメントに書いた通り、DMAとPIOのステートマシン起動タイミングによっては左右チャンネルのアライメントがずれる可能性がある。これに関して検討してみた。
送信FIFOに2ワード分のデータが溜まるまで送信しない
可能である。SHIFTCTRL_PULL_THRESHによって、送信FIFOに十分なデータが溜まるまでステートマシンをストールできる。
同様なことがSHIFTCTRL_PUSH_THRESH によって受信FIFOでも可能である。
送信DMAを先に起動する
こちらのほうが簡単に思える。
FIFOのクリア
PIOのFIFOはリセット後には空であることが補償されている。一方で、ステートマシンを停止して再起動する際にFIFOを空にする方法は提供されていないように思える。
これはAudio CODECの動作モードを変更する際などに問題になる。こういった場合、CODECだけではなくI2Sコントローラ、アルゴリズムなども入れ替えになるからである。FIFOの中に残されたデータは左右チャンネルの撮り違いや、予期せぬ送受ディレイなどの問題になりうる。
PIOでI2Sを組み、それを再起動する場合、FIFO内部の送信データをフラッシュするPIOステートマシンを間に挟んだ方がよいかもしれない。
スレーブ動作にする場合のWS同期
スレーブ動作にする場合、WS同期に関しては2つの方法が考えられる。
- 最初にWS同期をとった後は、BCLKで決め打ちで送受信を行う。
- 1ワード送受信ごとにWS同期を行う。
1を採用した場合、BCLKのグリッチにより大きな雑音が入る。この雑音は継続する。
2を採用した場合、BCLKのグリッチにより大きな雑音が入るが、WS同期によりすぐに収束する。LR同期がずれる可能性があるが、これは雑音よりもいい。ただし、これはプログラムが非常に大きくなる。32命令には入らない可能性が極めて大きい。
I2S回線が長くなることはあまり考えにくいので、グリッチは入らないものとして考えて1を採用するより手はないと思われる。
結論
以下の表のようになる。
特徴 | マスター | スレーブ |
---|---|---|
クロック品質 | 標準Fsからずれるか、ジッター混入 | 良い(CODEC供給) |
クロックのグリッチ耐性 | 考えなくてよい | あきらめる |
Fs変更 | クロックデバイダ変更が必要 | FIFOフラッシュが必要 |
データ長変更 | PIO再プログラム | PIO再プログラム |
RP2350のクロック周波数は150MHzであり、Fs=48kHzのときに1,500命令/ch/サンプルしか実行できない。これは超高品質を望む性能ではないので、ジッターの混入については気にしなくてよいのではないかと考えられる。
同様の理由で、小規模電子回路におけるI2Sのジッター混入リスクは無視して良いと思われる。
FIFOフラッシュの手間を考えると
- Fs固定の場合はスレーブの方が気が楽(CODECとマイコンで設定を合わせなくてよい)。
- Fsを動的に変更する場合はマスターの方が楽。
スレーブ動作の利用手順
起動手順
1.FIFOは空であると仮定する。
2. 送受信DMA / 受信割り込み をイネーブルにする。
3. スレーブI2S PIOを起動する。
この場合、PIOのアセンブリコードにはI2SのWSに対する同期機能が必須であることに注意。
終了手順
以下の手順は終了後に再起動することを見込んだ手順である。この手順のあとは送受のFIFOがからである必要がある。
- 送受信DMAを停止する。
- 送信FIFOを監視し、空になるまで待つ。
- スレーブI2S PIOを停止する。
- 受信FIFOを監視し、空でなければ空読みする。
DMAではなく割り込み・ポーリングの場合も同じ。
データ転送
DMAの場合
送信DMA割り込みは使わない。受信DMA割り込みでハードウェアとソフトウェアの同期をとる。受信DMA割り込みが発生した時点で、受信DMAバッファは埋まっているが、送信DMAバッファも同時に空である。したがって送信DMA割り込みは必要ない。
送信DMAの状況は受信DMAよりFIFOの深さの分だけ進んでいることに注意。これはDMAバッファサイズの小ささに制限があることを意味する。
また、送信DMAが先行しているということは、送信バッファを埋めるための時間は、DMAバッファ長よりもFIFOの長さだけ短いということになる。例えば、DMAバッファ長が48サンプル、FIFOの深さが4サンプルとすると、送信データは受信割り込みから44サンプル分の時間で埋めなければならない。
これが嫌ならば、DMAを始める前に送信FIFOを0で埋める方法が使える。
割り込み方式
基本的にDMAと同じで、違うのはプログラムが直接FIFOをアクセスすることである。FIFOの割り込み閾値をいじって2ワード(1ステレオ・サンプル)毎に受信割り込みを入れることにすれば、処理は容易になる。
ポーリング方式
割り込みを使わずFIFOをポーリングする際も、2ワード(1ステレオ・サンプル)そろうのを待ってから処理をし、2ワードの処理結果を送信FIFOに書き込むとよい。
ポーリングとPicoのバックグラウンド処理
サンプルプログラムをポーリングで実装したが、大きな雑音が入ったり左右のチャンネルがひっくり返ったりする。
オシロスコープで見る限りCODECからの信号はきれいで、このような問題が起きる理由がわからない。PIOのアセンブリ・コードもPicoのポーリング動作もそのような不安定性が入り込む隙は無い。PIOのアセンブリコードの大規模書き換えも行ったが同じ。
考えられる問題を全部潰して途方に暮れた後、寝っ転がっている時にふと思い当たった。バックグラウンドで何かしているのではないか。
そこでmain()関数でI2Sの開始前に1000mSのスリープを入れたところ、あっさり問題が解消した。そしてさらに、CMakeLists.txtの
pico_enable_stdio_usb(${PROJECT_NAME} 1)
をコメントアウトするとスリープ無しでも問題が解消した。
結論として、STDIOをUSB経由で使うような設定ではmain()関数の最初の1秒付近に割り込みを使ったバックグラウンドの初期化作業があるのだと思われる。
USBがバックグラウンド処理を使うことに不思議はないので、これは不注意だった。