Raspberry Pi用Speaker pHATをMicroPythonで鳴らしてみる

Raspberry PiのベアメタルMicroPythonで、PWMクラスを作って音声出力の実験ができたので、同様に音声出力ができる「I2S」についてもベアメタルプログラミングで鳴らす実験をしてみました。

I2SはPCMデータを送受信する規格です。音声信号を出力するには、I2Sに対応した音声信号用のDACが必要です。

今回は、手持ちのPimoroni Speaker pHATを使いました。
これはI2Sでデータを受信できるモジュールです。Adafruitの紹介ページによると、MAX98357AというDAC兼デジタルアンプのチップを使用しています。
このモジュールは他にI2C経由で制御できるLEDレベルメータも搭載されていますが、今回はI2Sの機能だけを使用します。

I2Sとは

I2S(Inter-IC Sound)とは、デジタル音声信号を送受信するためのシリアルインタフェースです。

名前はI2Cと似ていますが、I2Sは信号線の構成的にはむしろSPIに近いです。クロックと送信用と受信用の線が独立しており、クロックに合わせてデータが1ビットずつ転送されます。
さらに、フレーム同期信号があります。ステレオ音声信号は左右チャネルがありますので、チャネルの切り替えにフレーム同期信号を用います。
このほか、受信したデータをDA変換するタイミングを正確に制御するためのマスタークロックがありますが、これを必要としないデバイスも多いようです。また、Raspberry Piではマスタークロックは外部へ出力できません。

上の図はI2S信号の例です。一番上がデータ送信用のクロック(BCLK)、2番目が左右チャンネルを表すフレーム同期信号、3番目がデータです。

この例では、左右チャンネルそれぞれ32bitを使用しています。
データは上位ビットから送信され、例えば受信側が16bitしか使わない場合は下位16bitは読み飛ばせば済むようになっています。

フレーム同期信号は0のときLch、1のときRchで、この信号の周波数はDACのサンプリング周波数と同一になります。上記の例は22KHzとなっています。

I2Sも含めたPCMデータのシリアル伝送には、音声信号のサンプリングレートや量子化ビット数、信号の位相やタイミングでいろいろなバリエーションがありますので、それにあわせた設定が必要になります。
バリエーションについては以下のページがよくまとまっています。

オーディオに使用される周波数について(サンプリング周波数、PCM,DSDなど) | マルツセレクト

PCMフォーマットの変換について

Raspberry PiのI2S

Raspberry Piでは、I2Sで入力・出力を行うことができ、クロックの内部供給・外部供給も選択できます。
フレームあたりのビット数は8bit~32bitを選択可能です。
データは送受信それぞれFIFOが用意されており、ポーリング/割り込み/DMAのいずれかでCPUとデータをやり取りすることができます。
以下がブロック図です。図の右側が外部への出力信号です。(CLK:クロック、FS:フレーム同期、DIN:入力、DOUT:出力)

GPIOにI2S信号を出力するには、Alternate Functionの設定が必要になります。利用できるピンは以下の2通りですが、GPIOコネクタに出力されているのはALT0系統のみです。

ALT0: 18(CLK)、19(FS)、20(DIN)、21(DOUT)
ALT2: 28(CLK)、29(FS)、30(DIN)、31(DOUT)

今回は出力の実験ですので、18、19、21を使用します。

また、関連するレジスタは以下のようになっています。ベースアドレスは0x20203000です。
レジスタがたくさんあるように見えますが、実際に使うのは
・CS_A
・FIFO_A
・MODE_A
・TXC_A
の4つです。_AはAudioの略でしょうか?
残りのレジスタはデータ受信や割り込み、DMAなどの設定用で、今回は使用しません。

I2S用クロック信号の生成

クロックはクロックマネージャを用いて生成します。クロックマネージャはPWMで用いたものと同じ構成で、レジスタのアドレスのみが異なり、0x20101098になります。

生成するクロックの周波数はサンプリングレートとフレームあたりのビット数で異なってきます。例えば、CDの音声信号は44.1KHz/16bit/2chですから、

44100 * 16 * 2 = 1411200 Hz

となります。I2Sの場合、16bitの伝送でも32bit分のスペースを使う場合もありますので、その場合はこの2倍で約2.82MHzのクロックが必要となります。

残念ながら、Raspberry Piのハードウェアはこのような周波数を綺麗に生成することができません。
ソースとして用意されている信号は19.2MHz、216MHz、500MHz、1GHzであり、この周波数は44.1KHzでは割り切れないからです。

以前紹介したMASH機能を使えば擬似的にその中間の周波数も生成できますが、クロックに大きなジッタが発生することになりますので、DACによってはエラーが起こる可能性もあります。
MASH機能を使わない場合は、周波数が微妙にずれることになりますので、その分再生する音声の周波数が微妙に高くなったり低くなったりすることになります。

ソースおよび必要となる各周波数を因数分解すると、以下のようになります。

・44,100 = 10^2 * 7^2 * 3^2
・48,000 = 10^2 * 4^2 * 3

・19,200,000 = 10^5 * 8^2 * 3
・216,000,000 = 10^6 * 2^3 * 3^3
・500,000,000 = 10^8 * 5

ちょっと計算してみれば、うまく整数分の1にできるケースはほとんどないことが分かります。

今回の実験では、19.2MHzを75分周して256KHzを生成し、16bit * 2ch * 8KHzとして利用することにしました。
MicroPythonではそれほど高速にFIFOにデータを送り込めませんので、実験用には十分です。

MAX98357Aについて

今回使用するMAX98357AというDACですが、データシートからI2S関連のスペックを拾うと以下のようになります。

・サンプリング周波数:8kHz, 16kHz, 32kHz, 44.1kHz, 48kHz, 88.2kHz, 96kHz
(11.025kHz, 12kHz, 22.05kHz and 24kHzは利用不可)
・量子化ビット数:16/24/32ビット
・クロック(BCLK)の極性:BCLKのrising edgeでデータ読取
・フレーム同期クロックの極性:LchがLow

今回は前述のように8KHz/16bitで使用します。設定レジスタ等はなく、入力されてくるクロックに近いサンプリング周波数が自動で選択されるようです。量子化ビット数はフレーム同期信号で自動的に決定されます。

ステレオ入力モノラル出力ですが、今回購入したSpeaker pHATはLRどちらのチャンネルの音も出力されるようです。

音声信号を出力してみる

I2Sに関するレジスタの解説は、毎度おなじみのマニュアルの8章(P.119~)にあります。
I2Sのレジスタを操作する前に、まずクロックおよびGPIOのAlternate Functionの設定をしておきます。
ついでにレジスタのアドレスも定義しておきます。

Raspberry PiのI2Sハードウェアには、以下の図のような細かい設定項目があります。
図(今回はDINは関係ありません)の上から順に、意味と設定値は以下の通りです。

・FSLEN 1チャネル分のビット数(16)
・FLEN 1フレーム分のビット数(32)
・CH1WID、CH2WID 各チャネルのビット数(16)
・CH1POS、CH2POS 各チャネルは何クロック目からデータが始まるか(1, 17)

最後のCH1POS=1は、フレーム開始後1クロック置いてからデータが始まることを示します。これがI2Sの標準的な方式ですが、フレーム開始直後からデータを開始する(CH1POS=0)設定も可能です。

I2Sのデータ送信は、今回はポーリングモードを使いました。バッファに空きがあるかどうかフラグをチェックして、空きがあればFIFOにデータを書き込む、最も単純な手法です。

ポーリングモードの具体的なレジスタ操作方法はマニュアルの8.4.1に書かれています。
上記の設定およびポーリングモードの設定を行う処理が以下です。コメントはマニュアルの説明文です。

これで、I2Sでデータを送信する準備ができました。
送るデータは符号付き16bit整数(-32768~32767、0x8000~0x7fff)です。今回はノコギリ波を用意しました。
配列に格納して読み出しながら送ると、速度が間に合わない場合があるので、以下のコードではFIFOへの書き込み部分を展開しています。

CS_Aレジスタのbit 2を1にするとデータの送信が始まります。
実行させると、Speaker pHATスピーカーから音が出ます。(「ブー」というだけですが・・・)
1波形に17サンプル使用しているので、8000 / 17 = 470Hzくらいの音になります。

コメント