2020年5月1日金曜日

STM32でDCCを作る方向で (8) I2Sの具合はどうかな?

告知です.
コミケ99にて当社のDDC/DACを頒布いたします.
  日付   2021年12月31日(金) 東地区 テ-40b  東5ホール
  サークル名    bangflat
コミケにお越しの際はお立ち寄りいただけますとありがたいです.
商品紹介ページを作りました.

ーーーー
前回は、SPIを試した.  →問題点は解決した.SPI有望.


今回は、I2Sを試してみようと思う.もっとも、STM32のI2Sは32bit384kHzには非対応なので使う見込みはないのだけれど.

STM32 datasheetのI2Sの項によると、32bit192kHz(約12Mbps)が上限である理由は、APBのclock 45MHzが律速になっていると匂わせる文言がある.
だが矛盾というか不思議なこともある.I2Sのhardwareは、SPIのhardwareにちょいと追加回路を乗せてI2Sとして運用しているっぽいのだ.そのためにSPIが接続されているバスもAPBである.なのに、SPI1の最高レートは30Mbpsなんだよなぁ、なんでだろ?

解説: ARM社がライセンスしているAMBAというバス規格があって、AMBAの中に性能の松竹梅でAXI,AHB,APBがあるみたいよ.AHBは高速道路、APBは一般道という位置づけだ.AXIは新幹線みたいなものなんじゃね?      ←昨日得た知識より

解説2:STM32F2xxやSTM32F4xxにはSPI1/SPI2/SPI3があり、高速/中速/中速である.ぶら下がっているAPBが違うためだ.サンプリング周波数の最高は384/192/192kHzに相当する.なのだが、I2S対応が、無し/あり/あり なのである.SPI1のI2Sを使いたいが無いのである.残念.
下記ではSPI2=I2S2を例示している.

-----
さて、I2Sである.
STM32CubeMXでI2Sを組み込む方法については割愛する.32bit192kHz masterで組み込んだ.

DMAしかやる気ないので、500ms毎にDMAがI2Sへdata出力するcodeをmain.cに追記する.
  while (1)  {
  while (HAL_I2S_GetState(&hi2s2) != HAL_I2S_STATE_READY){}
  HAL_I2S_Transmit_DMA(&hi2s2, txbuf1, 2);
  HAL_Delay(500);
  }
txbuf1にはこんなデータを入れておく.
uint16_t txbuf1[] = {0xAA00,0xAA00,0xFF00,0xFF00};
これは何を意図しているのかというと、MSB firstの32bit Lch/Rchを模擬している.オシロで観測しやすいbit patternを選んだ.uint16_tである理由は、HAL_I2S_Transmit_DMA()の第2引数のcastがそうなっているから.

↓この結果、I2Sのpinには下記の波形が出る.32bit Lch/Rchが出てくる.期待通り.

↓HAL_I2S_Transmit_DMA()の末尾の引数は転送する量である.上の例だと2にしてある.2の単位は何なのだろうか? I2S設定のData and Frame Format=32bitに対応していると推測する.つまり、32bit dataを2ヶという意味だ.

↓STM32CubeMXのI2S設定に要注意だ.I2SのDMA設定にDataWidthがある.ここをHalfWordにする必要がある.txbuf1がuint16であることに呼応した設定と思われる.

以上から、単一のbufferに置かれた32bit audio dataをDMAで円満に送信できるようである.(後段にFPGAを設けるので少しぐらいの処理であればFPGAでやれる)

-----
次に確認したいのは、送信の繋ぎ目がどうなるのかである.audio streamがFIFOの継ぎ目で隙間が開いてしまったら音が途切れてしまうからだ.
最初に試してみるcodeはこんな風に2度送信するやり方...
  while (1)  {
  while (HAL_I2S_GetState(&hi2s2) != HAL_I2S_STATE_READY){}
  HAL_I2S_Transmit_DMA(&hi2s2, txbuf1, 2);
  while (HAL_I2S_GetState(&hi2s2) != HAL_I2S_STATE_READY){}
  HAL_I2S_Transmit_DMA(&hi2s2, txbuf1, 2);
  HAL_Delay(500);
  }
↓これがI2Sに現れる信号だ.見事にスプリットおります.繋ぎ目が開いております.ダメであります. 

繋ぎ目を開けずにパーペキに乗り切るにはどうしたらよいのか?
2つ目の送信を終了確認せずにブッコむのを試してみるなり...
  while (1)  {
  while (HAL_I2S_GetState(&hi2s2) != HAL_I2S_STATE_READY){}
  HAL_I2S_Transmit_DMA(&hi2s2, txbuf1, 2);
  HAL_I2S_Transmit_DMA(&hi2s2, txbuf1, 2);
  HAL_Delay(500);
  }
↓はいっ、2つ目のDMAは無視されました~.ダメであります.

困っちまったな、、、、こちらのページによると、、、
 ・1度のDMAで送信できるのは64k個まで
 ・それより長いならDMA完了割り込みで継ぐ
と書かれてます.ふ~ん、やってみよう.

、、、、いろいろとやってみた.どうやら以下のカラクリのようだ.

↓I2SのDMAをcirculer modeに設定する.
↓while()の前でHAL_I2S_Transmit_DMA()を起動すると、txbuf1がグルグルと無限参照されつづける.一度起動したDMAは止めない限り永久に回り続ける.これで少なくとも隙間は無くせる.

次に、txbuf1を半分消化したら割り込みがかかるようにする.
STM32CubeMXが生成したcodeだと、txbuf1のHALFとFULLの2か所で割り込みがかかるようになっている.いまはHALFだけでいいので、FULLを禁止すればいい.DMAのCRレジスタを直接操作する.
HAL_I2S_Transmit_DMA()の後で(前だとダメだった)
hdma_spi2_tx.Instance->CR &= 0xFFFFFFEF; // clear (bit3:TCIE)
レジスタ直接操作とは荒療治だが、HAL libraryでどうやったら良いのか不明なので今はこれで先へ進む.

さらに、割り込みルーチンに、on the flyでtxbuf1を書き換えるcodeを追加する.txbuf1先頭をAA/88で切り替える.
void DMA1_Stream4_IRQHandler(void) {
  txbuf1[0]=sig;
  if(sig==0x8800)      sig=0xAA00;
  else if(sig==0xAA00) sig=0x8800;
  HAL_DMA_IRQHandler(&hdma_spi2_tx);
}

↓これが成果である.
N回目   →AA00AA00FF00FF00を送信している.
N+1回目 →8800AA00FF00FF00を送信している.
つまり、txbuf1をon the flyで書き換えることにより、I2Sから間断無く任意のstream dataを出力する目途が立った、ということだ.これにて原理確認終了.
実際のaudio再生では、64k個のbuffer領域の後半領域を読んでいる間に前半領域を書き換えるような実装になるだろう.DMAさん頑張ってね~

でもでも、on the flyでbufferを書き換えるのって気持ち悪い.別の手段は無いのだろうか?
あるにはある.DMAにはdouble buffer modeというのがある.
しかしSPIやI2SやI2Cにはdoble buffer modeは使えないようだ.理由は、APBに接続されたperipheralにはbouble buffer modeが無いらしい.datasheetにはそう受け止められるように書かれている.APBごときの速度ならdoubleは不要という設計なのだろう.


次回は同様の検討をSPIでやってみたい.
上手くいきそうだったらDACから音を出してみようかね.

かしこ


0 件のコメント:

コメントを投稿