2023年9月19日火曜日

BlackFinを使ってみるなり(23) ADC/DACをI2S接続詳細

BlackFin BF706評価ボードに、TIのADC PCM1808を取り付けて、loopbackでDACでモニタする機能を実装しました.
 PCM1808 96kHz 24bit 秋月で売ってるもの

DACはPCM510xです.

BF706へのADC/DACの接続の考え方はこちらで書きました.
骨子は、、、
・ADCがmasterになります.ADCがBCK,LRCK,DATAを出力
・DACはslaveです.BCK,LRCK,DATAを入力される
・BF706のI2Sは、SPORT-AがDAC用、SPORT-BがADC用
・SPORT-AはBCKを入力される、LRCK,DATAを出力する→DACへ
・SPORT-BはBCK,LRCK,DATAをADCから入力される
CrossCoreのIOアサイン画面の様子です.SPORT1を使いました.

PCM1808の回路はこんなです.

ーーーー
BF706のfirmwareについて以下で説明します.key pointは、
・SPORTをI2S modeに設定する
・I2SをDMAで動かす
・Buffer関連のeventは割り込みのcallback関数で受け取る
・Tx Buffer(DAC)とRx Buffer(ADC)がある
・Bufferを上手く操作してADC→DACへloopbackする
・DAC Lchに内部発生信号を出力し、それをADC Rchで変換し、DAC Rchにそのまま出力する、というloopback動作をさせています

project folder詰め合わせはこちらです.(zip)
たぶん何かしらbugってます.免責でよろしく.

ではprogramの説明でぇす.なにから説明しましょうかねぇ?

SPORTをI2S modeに設定する
ライブラリの使いこなしです.

void h_wavegen_I2S_start(void)  ←この関数の中身です

int div = (100000000 / paramL.bck) + 0.5 - 1;
これはBCK周波数をSPORTに発生させるための分周比を計算します.SPORTのclockは100MHzです.サンプリング周波数96kHzとすると、BCKはその32bit 2chで64倍ですから6.144MHzです.分周比=100/6.144≒16と計算されます.なんつってこのdiv使わないんですがね.

まずはSPORT1-B Rx ADC用の設定です.
adi_sport_Open(1, ADI_HALF_SPORT_B, ADI_SPORT_DIR_RX,  ADI_SPORT_I2S_MODE,  DeviceMemory_B, ADI_SPORT_DMA_MEMORY_SIZE, &hDeviceB);
まずはopenです.引数がいろいろあります.
1: SPORT1の意味
ADI_HALF_SPORT_B: SPORT1-Bの意味
ADI_SPORT_DIR_RX: 受信の意味.すなわちADC.
ADI_SPORT_I2S_MODE: I2Sの意味
DeviceMemory_B: callback関係のwork memoryだけどとくに使ってない
ADI_SPORT_DMA_MEMORY_SIZE: 同work memoryのsizeで、ヘッダファイルの定義を踏襲する.こいつをNULLとかにすると静かに暴走するので要注意です.
hDeviceB: openしたハンドラのポインタ

adi_sport_ConfigData(hDeviceB, ADI_SPORT_DTYPE_ZERO_FILL, 31u, false, false, false );
31という数字は、ADCから32bitのサンプリングデータを受け取る意味.PCM1808は24bit ADCですが、I2Sは32bitでinterfaceしますので要注意.

adi_sport_ConfigClock(hDeviceB, div,false,false,false);
I2Sのclockを外部入力BCKにします.
1つめのfalseが外部入力BCKを使う意味です.
外部入力BCKを使うので、内部分周比のdivは意味ないです.0にするのもアレなのでなんとなくdivを引数に与えています.

adi_sport_ConfigFrameSync(hDeviceB, 31u, true,  false, false, false, false, false);
LRCKの設定をします.ここにも31という数字が登場します.
最初のfalseがLRCK入力の意味です.

adi_sport_EnableDMAMode (hDeviceB, true); DMA好きです

adi_sport_RegisterCallback(hDeviceB, SportCallbackB, NULL);
callback関数の登録.

adi_sport_SubmitBuffer(hDeviceB, nBufferRx0, SIZE_OF_RX_BUFFER);
受信準備.受信バッファとサイズを与えています.受信満タンになると割り込みがかかります.

state_sportB = H_SPORTB_ADC_TO_BUF0; 受信ステータス
adi_sport_Enable(hDeviceB, true); 受信開始

以上がSPORT1-B RxにADCを接続する設定です.

次にDACの設定をする前に、300uSec delayを入れました.
理由は、Tx Buffer処理とRx Buffer処理が時間的に近接してしまい、同時にcallback関数が呼ばれてややこしくなったら嫌だなと思ったからです.
h_delay_us(300);
なんで300uSecなのか?
Bufferサイズを512BYTEにしてあります.512/8=64個.96kHzサンプリングだと64個は666uSecに相当するので、666uSec毎にBuffer割り込みがかかる計算です.その中間で300uSecです.
delayにはTimer割り込みを使いました.その説明は後述します.

つぎはSPORT1-A Tx DAC用の設定です.
adi_sport_Open(1,ADI_HALF_SPORT_A, ADI_SPORT_DIR_TX,  ADI_SPORT_I2S_MODE,  DeviceMemory_A, ADI_SPORT_DMA_MEMORY_SIZE, &hDeviceA);
openはADCの設定と似たよなもの.

adi_sport_ConfigData(hDeviceA, ADI_SPORT_DTYPE_ZERO_FILL, 31u, false, false, false );
dataはADCの設定と同じかな.

adi_sport_ConfigClock(hDeviceA, div, false, false, false);
BCKはADCの設定と同じかな.

adi_sport_ConfigFrameSync(hDeviceA, 31u, true, true, false, false, false, false);
DACのLRCKはI2Sが出力します.

adi_sport_EnableDMAMode (hDeviceA, true); DMAオン、ADCと同じ

adi_sport_RegisterCallback(hDeviceA, SportCallbackA, NULL);
callback関数登録.ADCと似たよなもん.

adi_sport_SubmitBuffer(hDeviceA, nBufferTx0, SIZE_OF_TX_BUFFER);
送信準備.送信バッファとサイズを与えています.送信済で割り込みがかかります.

txbuf01=0; 送信ステータス
adi_sport_Enable(hDeviceA, true); 送信開始


delay timer
one shot timerでdelayを作りました.
これもcallback関数でdelay終了を検知しています.

設定方法.
adi_tmr_Open(2, Timer2CBMEM, ADI_TMR_MEMORY, Timer2Handler, 0, &phCTimer2);
Timer2をopenします.Timer2Handlerがcallback関数名.

adi_tmr_SetMode(phCTimer2, ADI_TMR_MODE_SINGLE_PWMOUT);
PWMじゃないけど、PWMのSINGLE SHOT modeにします.

adi_tmr_SetIRQMode(phCTimer2, ADI_TMR_IRQMODE_DELAY);
delay countが済んだら割り込みがかかるようにします.

次にcallback関数です.
void Timer2Handler(......){
timer2ongoing=false;
return;
}
やってることはtimer2ongoingをいじってるだけ.

次にdelay関数本体です.
void h_delay_us(int us){
 ①adi_tmr_SetWidth(phCTimer2, 1);
 ②adi_tmr_SetDelay(phCTimer2, 100*us); // clk=100MHz
 timer2ongoing=true;
 ③adi_tmr_Enable(phCTimer2, true);
 ④while(timer2ongoing);
 ⑤adi_tmr_Enable(phCTimer2, false);
}
①②Timer2にdelay時間設定をする.②だけじゃなくて①も必要.
③Timer2スタート
④callback関数がtimer2ongoingをfalseにするまでwait
⑤Timer2停止


バッファ処理
バッファはRx2本、Tx2本の計4本あり、各バッファは512BYTESです.
96kHzサンプルですので、666uSec毎にバッファが消費されます.
バッファの動作イメージ図.

Rx callback関数
はしょってキモのところだけ抜き出しました.
ADC受信バッファが満タンになったらここへ飛んできます.
case文で、満タンになったのはBUF0かBUF1かで処理を分けていますが、いずれにせよやってることは2つです.
・ステータスの切り替え(PROCESSING_BUF0とか)
・次の受信をkick-offしている
void SportCallbackB(........){
 switch(state_sportB) {
  case H_SPORTB_ADC_TO_BUF0:
   state_sportB = H_SPORTB_PROCESSING_BUF0;
   adi_sport_SubmitBuffer(hDeviceB, nBufferRx1, SIZE_OF_RX_BUFFER);
   break;
  case H_SPORTB_ADC_TO_BUF1:
   state_sportB = H_SPORTB_PROCESSING_BUF1;
   adi_sport_SubmitBuffer(hDeviceB, nBufferRx0, SIZE_OF_RX_BUFFER);
   break;
  }
}

Tx callback関数
DAC送信バッファが空になったらここへ飛んできます.
次のDAC dataを用意するのが仕事です.実測で30uSecかかっています.
void SportCallbackA(......){
 fract32 *pBufTx, *pBufRx; バッファのポインタ

前回のターンで用意済のバッファを送信起動します.
 adi_sport_SubmitBuffer(
  hDeviceA,
  txbuf01==0 ? nBufferTx1 : nBufferTx0,
  SIZE_OF_TX_BUFFER
  );

ここからバッファコピー.
 // Lch
 pBufTx = txbuf01==0 ? nBufferTx0: nBufferTx1;  コピー先バッファ
 pBufRx = state_sportB==H_SPORTB_PROCESSING_BUF0 ? nBufferRx0 : nBufferRx1;  コピー元バッファ

DAC信号選択がADC loopbackだった場合に限り、バッファコピーを行う.
 if(paramL.waveform==H_WAVEGEN_ADC){
  for(int i=0; i<SIZE_OF_TX_BUFFER; i=i+8){
   *pBufTx = *pBufRx;
   pBufTx += 2;
   pBufRx += 2;
  }
 }
バッファには、偶数word(32bit)がLch、奇数word(32bit)がRchの順で並んでいるので、for文は8づつ増やし、コピーポインタは2づつ増やします.

DAC信号選択が内部関数のsin波等である場合は、バッファコピーをせず、関数出力をTxバッファに積むのみ.
 else {
  for(int i=0; i<SIZE_OF_TX_BUFFER; i=i+8){
   *pBufTx = h_wavegen(H_WAVEGEN_LCH);
   pBufTx += 2;
  }
 }

 // Rch   Lchと似てるので省略

 // state update
 txbuf01 = txbuf01==0 ? 1 : 0;  Txバッファの切り替え
 state_sportB = state_sportB==H_SPORTB_PROCESSING_BUF0 ? H_SPORTB_ADC_TO_BUF1 : H_SPORTB_ADC_TO_BUF0; Rxバッファの切り替え

ここでは2つのバッファを交互にWR切り替えして使っていますが、BlackFinには「ring buffer」という自動的にグルグル回り続ける便利なバッファがあるみたいです.それの使い方をまだ知らないのですが、いずれそっちに移行するべきだと思います.
「programming reference manual」の中の「Automatic Circular Addressing」で検索すると出てきました.CPUのindex registerに開始と終了アドレスを設定するとその間を自動的に循環してくれるそうです.ライブラリで提供されてるのかどうかは知りませんが、フツー提供されてると思うんです.

ーーーー
DAC/ADC loopbackのキモは上記のとおりです.

この他に、UART,Timerなども制御しています.SPIは挫折しました.UARTはたしか115200bpsだったかな.
UART経由で信号を変えられます.
 a l 60  Lch amplitude 60%の意味
 a r 89  Rch amplitude 89%の意味
 w l s  Lch sin波の意味
 w l u  Lch 上昇三角波の意味
 w r l  Rch Loopbackの意味


やっと信号が通ったので、信号処理を始められます.

22へ     24へ

かしこ

0 件のコメント:

コメントを投稿