2020年5月20日水曜日

STM32でDCCを作る方向で (15) DAC出力のために2 (一応音出た)

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

ーーーー
STM32でDCCを作る.    INDEXページへ

一応、、、ちゃんと音が出た.音を出すためのポイントを今回は述べる.
間違っていてもわたしは何の責任も負わないので素直に死んでくれ.


【条件、環境】
(target)
targetはNUCLEO-F207を使っているが、STM32のHAL/LLライブラリを使えば他のCPUへの移植は容易だと思う.最終的に使いたいのはSTM32F205またはSTM32F405である.
(USB)
NUCLEO-F207にはFullSpeed専用のUSBと、HighSpeedでもFSでも使えるUSBの2つ載っている.いまはUSB-HSを使っている.ただしモードはFSである.
NUCLEO-F207のプリント基板上にはUSB-HSのUSBコネクタはついてないのでUSB-HSのpinにコネクタを自分で取り付けた.でもsoftware的にはFSモードで動かしているので12Mbpsしか出ないので48kHz16bitで動作確認するに留める.

(HOST)
win10だと音楽再生アプリが動かない.デバドラの問題であろうが未解明.→win10のサウンドデバドラを更新とかなんとかしたら治ってしまった.
Linuxなら動く.
Linuxでaplay -D hw:1.0 xxxx.wavで音楽再生している.xxx.wavは48k16bitで作成しておく.

(IDE)
開発環境は、STM32CubeIDE Version: 1.3.0.
Peripheral制御はSTM32CubeMXに頼りっぱなし.
USB Audio Class 1.0のsample codeもSTM32CubeMXが吐いたものを使用.→こちら

(sample code)
同sample codeのデフォの機能は、、、
・デバイスディスクリプタが記述されていて、win10なりLinuxにUSB接続するとUAC 1.0 deviceとして認識してもらえる.ただしサンプリング周波数やbit数は16bit48kで決め打ち.
・受信したaudio streamの処理は、80 packet(80mSec分)をbufferに積んでくれるところまでやってくれる.
・ただし、HOSTアプリがaudio streamを出力してくれるかどうかは、上で触れたようにwin10だとなぜか知らないけどダメ.Linuxならaudio streamを出力してくれる.
・audio streamの仕様は、、、
    -EndPoint1を使う
    -1packetは、1ms毎のisochronous転送で送られて来る量(FSであるため1mSec)
    -48kHz 16bit 2chの場合は、1msのdata量=48000x2x2÷1000=192bytes
    -サンプリング周波数はSTM32CubeMXで変更可能
    -little endian    ←fuckだなぁ
・その先の処理、すなわちDACへの出力は自分で書く必要がある.

なお、現状はFullSpeedでUSBを動かしているので、packet周期は1mSecであるが、後日HighSpeedにしたら125uSec周期になる.


【自力でやること1   出力インターフェースの決定】
DACへ音声を出力するには通常はI2Sバスを使うだろう.しかし、STM32のI2Sは192kHz32bitが最大なのだ.384kHzは通らない.   ←残念だなぁ

そこでわたしはSPIを使うことにした.SPI1なら余裕で384kHzを通せる.ただしSPI3は192kHzが限界である.

ところが現状のTargetのSTM32F207-144だと、pinアサインの問題により、USB-HSとSPI1を同時に使えないんだ.  ←fuckだなぁ.   →こちら

なのでSPI3でaudio streamを出力する.(とりあえず実験のためだけ)

DACはLRCKを必要とする.I2SはLRCKを出力してくれる.
しかしSPIはLRCKを出力してくれない.どうするか?
外付けのFPGAでLRCKを生成する.

DACはPCM5102を使う.(とりあえず実験のためだけ)

そして超大事なこと.SPI/DACはmaster clockで動かすこと.これは鉄則だ.


【自分でやること2    hardware/peripheralの諸々】
(SPI)
STM32CubeMXで設定する.
・Transmit slave
・DMA追加
・DMA割り込みON
・DMAはCirculerにする  (じゃないと音がブツブツ途切れる)

Slave SPIのSCKには48kHzx2chx32bit=3.072MHzを与える必要がある.FPGAで生成する.(DACを32bit modeで動かすため)

(USB)
STM32CubeMXで設定する.USB-HSは「USB-OTG-HS」という名称なので注意してくれ.FSでもHSでもcodeはほとんど変わらないので、USB-OTG-FSでやりたければそうすればいいだろう.

(DAC)
PCM5102を使う.I2S 32bit modeである.
wavファイルとUSBが16bitなので不一致であるが、それはprogramで調整する.

(FPGA)
Xtalの源発振からbckとsckとLRCKを生成する役目.bck=sck=64LRCKの関係.
LRCKのbit同期は、再生開始の最初のdata bitで決める.
そのため、STM32から再生開始パルスを出してFPGAをre-startさせる.
verilog codeの公開はここでは割愛する.


【自分でやること3   STM32 buffer動作】
bufferの動作を考える.sample codeを崩してしまい、違う形にした.

ring bufferを2つ設ける.

(16bit buffer)
sample codeのデフォルトのbufferをそのまま使う.
USB packetが192bytesだと上で述べた.1mSec分である.
80 packet分のring bufferが在る.サイズは192x80=15360 bytesである.80mSec分のaudio streamを格納できる.格納物は、16bit LittleEndian LRchである.
HOSTから来るpacketは休みなくこのbufferに積まれる.

(32bit buffer)
わたしが追加したbuffer.
DACは32bit BigEndianである.
そのため、16→32bit変換と、LE→BE変換をしないといけない.
そこで2つ目のbufferを設ける.サイズは1つ目のbufferの2倍で30720 bytesだ.
格納物は、32bit BigEndian LRchである.
2つ目のbufferは、SPI DMAによって休みなく読まれ続ける.

1つ目のbufferには際限なく積まれる.2つ目のbufferは際限なく読まれる.1から2へ定期的にコピーしてやる.その仕組みをこの図で解説する.

①音楽再生の先頭からUSB packetが192bytesずつ16bit bufferに積まれてゆく
      ---wait 40mSec---
②半分まで積んだら、16bit buffer前半を32bit buffer前半にコピー
      ---wait 40mSec---
③全部積んだら、16bit buffer後半を32bit buffer後半へコピー
④SPI DMA起動.以後DMAはグルグル廻り続ける
      ---wait 40mSec---
⑤SPI DMA半分割り込みが発生する.16bit buffer前半を32bit buffer前半へコピー
      ---wait 40mSec---
⑥SPI DMA全部割り込みが発生する.16bit buffer後半を32bit buffer後半へコピー
      ---wait 40mSec---
以降は⑤と⑥の永久loop

問題がある.
積まれる速度はHOST CPU固有の速度だ.
読まれる速度はFPGA Xtal固有の速度だ.
この2者の速度はppmオーダーで食い違っている.
わたしの実機では30秒ぐらい経つとbufferが破綻する.
この破綻を避けるためのUSBの仕組みがfeedbackである.feedbackはsample codeに実装されていない.自力で実装しなくてはいけない.まだ実装していない.


【自分でやること4   STM32 programming】
1)buffer操作のstateを定義する
(usbd_audio.h)
/* Audio Commands enumeration */
typedef enum
{
  AUDIO_CMD_NONE = 0,
  AUDIO_CMD_START,      上図の②
  AUDIO_CMD_WAIT1,
  AUDIO_CMD_PLAY,          上図の③④
  AUDIO_CMD_WAIT2,
  AUDIO_CMD_FILL_FROM_TOP,    上図の⑤
  AUDIO_CMD_WAIT3,
  AUDIO_CMD_FILL_FROM_HALF,   上図の⑥
  AUDIO_CMD_WAIT4,
  AUDIO_CMD_STOP

} AUDIO_CMD_TypeDef;

2)SPI DMAの半分割り込みと完了割り込みcallbackを登録する
(main.c)
static void MX_SPI3_Init(void)   へ追加
HAL_SPI_RegisterCallback(&hspi3, HAL_SPI_TX_COMPLETE_CB_ID, TransferComplete_CallBack_HS);  DMA完了割り込み⑥

HAL_SPI_RegisterCallback(&hspi3, HAL_SPI_TX_HALF_COMPLETE_CB_ID, HalfTransfer_CallBack_HS);    DMA半分割り込み⑤

(stm32f2xx_hal_conf.h)
callbackを有効化する.
#define  USE_HAL_SPI_REGISTER_CALLBACKS         1U

3)DMA割り込みcallback関数にcodeを追加する
何をやりたいのかというと、、、
  ・if文は、packetが来ないならDMAを止める
  ・audio_cmdという変数は、buffer操作stateを進行させる

(usbd_audio_if.c)
extern SPI_HandleTypeDef hspi3;
extern uint8_t audio_cmd;
extern uint8_t packet_watch;

void TransferComplete_CallBack_HS(void)DMA完了割り込み⑥
   if(packet_watch==0) HAL_SPI_DMAStop(&hspi3); // No packet
   packet_watch=0;
   audio_cmd = AUDIO_CMD_FILL_FROM_HALF;
}

void HalfTransfer_CallBack_HS(void){    DMA半分割り込み⑤
   if(packet_watch==0) HAL_SPI_DMAStop(&hspi3); // No packet
   packet_watch=0;
   audio_cmd = AUDIO_CMD_FILL_FROM_TOP;
}

4)16bit buffer操作
音楽再生が終わるまで、ここはpacket毎の割り込みでcallされつづける.=SOF割り込み=1mSec
しかしこの関数の実質的な仕事は音楽再生の先頭だけだ.
16bit bufferに、②半分積まれたら32bit bufferへコピー、③全部積まれたら32bit bufferへコピー させるよう、buffer操作stateを進める.

(usbd_audio.c)
USBD_AUDIO_DataOut(*pdev, uint8_t epnum)   SOF割り込みでcallされる
{
  USBD_AUDIO_HandleTypeDef   *haudio;
  haudio = (USBD_AUDIO_HandleTypeDef *) pdev->pClassData;
  usbd_ptr = pdev;
ここでは引数のpointerをglobal変数に退避させるという変なことをやっている.深い処にある16bit bufferのアドレスをmain()に引き渡すためである.16bit bufferアドレスは動的に変化するのだっ!

  packet_watch=1;     packet在りの監視
  haudio->wr_ptr += AUDIO_OUT_PACKET;    write ptr更新

16bit bufferが半分に達したらbuffer操作stateを進める.
  if (haudio->wr_ptr == AUDIO_TOTAL_BUF_SIZE/2){
    if (audio_cmd == AUDIO_CMD_NONE){
      audio_cmd = AUDIO_CMD_START;
    }
  }

16bit bufferが全部埋まったらbuffer操作stateを進める.
  else if (haudio->wr_ptr == AUDIO_TOTAL_BUF_SIZE){
    haudio->wr_ptr = 0U;      write ptr topに戻す
    if (audio_cmd == AUDIO_CMD_WAIT1){
      audio_cmd = AUDIO_CMD_PLAY;
    }
  }

次のpacket受信の準備
  USBD_LL_PrepareReceive(pdev, AUDIO_OUT_EP, &haudio-> buffer[haudio->wr_ptr], AUDIO_OUT_PACKET);
}

5)buffer操作をするところ
buffer操作はmain()のpollingでやることにした.それには理由がある.buffer操作には2mSecぐらい要する.割り込みハンドラ内で2mSecも時間を喰うと、1mSec毎のpacket割り込みを取りこぼして死んでしまうからだ.(割り込み優先度はさておく)

(main.c)
while loop内でbuffer操作stateを監視し、16bit→32bit変換とLitterEndian→BigEndian変換を適宜行う.
while(1)  {
USBD_AUDIO_HandleTypeDef   *haudio;
uint8_t* buffer_ptr;
  haudio = (USBD_AUDIO_HandleTypeDef *) usbd_ptr->pClassData;
    switch(audio_cmd)  {

②の処理.32bit bufferの前半にコピーする.
      case AUDIO_CMD_START:
buffer_ptr = &(haudio->buffer[0]);  16bit bufアドレス
        for(int i=0;i<AUDIO_TOTAL_BUF_SIZE;i=i+8){ 32bit buf前半
                          16bit→32bit変換とLE→BE変換
     pbuf32[i+3]=0; // Lch 32bit bigendian (UPPER)
     pbuf32[i+2]=0;
     pbuf32[i+1]=*buffer_ptr++; // USB little endial (LOWER)
     pbuf32[i+0]=*buffer_ptr++; // USB little endial (UPPER)
     pbuf32[i+7]=0; // Rch 32bit bigendial (UPPER)
     pbuf32[i+6]=0;
     pbuf32[i+5]=*buffer_ptr++; // USB little endial (LOWER)
     pbuf32[i+4]=*buffer_ptr++; // USB little endial (UPPER)
     }
        audio_cmd = AUDIO_CMD_WAIT1;  wait state
      break;


③の処理.32bit bufferの後半にコピーする.
      case AUDIO_CMD_PLAY:
                    16bit bufferアドレスは半分進んだ場所とする
        buffer_ptr = &(haudio->buffer[AUDIO_TOTAL_BUF_SIZE/2]);
        for(int i=AUDIO_TOTAL_BUF_SIZE; i<AUDIO_TOTAL_BUF_SIZE*2;
            i=i+8)    32bit buf後半
        {
                       32bit変換、LEBE変換は同上
     }

                    PC8でFPGAをre-startする
     HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, 1);
                    ④ SPI DMAを32bit bufferで起動する
     HAL_SPI_Transmit_DMA(&hspi3, pbuf32,
                                     AUDIO_TOTAL_BUF_SIZE*2);
     HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, 0);
     audio_cmd = AUDIO_CMD_WAIT2;  wait state
      break;

⑤の処理.32bit bufferの前半にコピーする
      case AUDIO_CMD_FILL_FROM_TOP: // fill pbuf32 top to half
        buffer_ptr = &(haudio->buffer[0]);
      for(int i=0;i<AUDIO_TOTAL_BUF_SIZE;i=i+8)
        {
                       32bit変換、LEBE変換は同上
        }
        audio_cmd = AUDIO_CMD_WAIT3;  wait state
      break;


⑥の処理.32bit bufferの後半にコピーする
      case AUDIO_CMD_FILL_FROM_HALF: // fill pbuf32 half to end
        buffer_ptr = &(haudio->buffer[AUDIO_TOTAL_BUF_SIZE/2]);
        for(int i=AUDIO_TOTAL_BUF_SIZE; i<AUDIO_TOTAL_BUF_SIZE*2;
            i=i+8)
        {
                       32bit変換、LEBE変換は同上
        }
        audio_cmd = AUDIO_CMD_WAIT4;  wait state
      break;
      default:;
} // switch
  } // while


【残問題   feedback】
現状のSTM32 DCCには異なる2つの時間が流れている.

buffer readは、SPI DMAに時間支配されている.SPI DMAはSlave modeで動いているので、時間の源はFPGA上のXtal発振器である.

もう一方のpacket割り込みは1mSec毎にかかるが、HOSTに時間支配されている.HOSTの時間の源はマザーボード上のXtal発振器である.
両者は完璧に同期してなければならない.同期がずれるとaudio streamがbit落ちして音声にnoiseが出る.

しかし現状の両者は同期していない.30秒ぐらいで1pcket分ズレてしまうぐらいの不一致度だ.Xtalの周波数精度は数10 ppmオーダーだからそんなもんだろう.

同期させるには、USB deviceにfeedbackを実装する必要がある.
sample codeにはfeedbackは無いので、自分で実装する必要がある.

次の課題はそれかな.


#project folderをzipにしたものをupしておく.   →こちら
debug codeを追加してあったりとグダグダに汚いが文句は禁止だ.
間違っていても知らん.
再生終了処理と再開処理を実装してないので起動後一曲目しか動かない.これは仕様です.

かしこ

0 件のコメント:

コメントを投稿