EZ-KIT lite BF533には4チャンネル入力、6チャンネル出力のオーディオコーデックAD1836Aが乗っています。この便利なチップとBF533はマルチチャンネルSPORT接続によって通信します。ADSP-BF533の性能を持ってすれば非常に低速とはいえ、オーディオ信号はダブルバッファDMAを使わないと満足に音が鳴りませんので意外に扱いが難しいものです。そこでTOPPERS/JSPを使ってこの機能をタスクに閉じ込めてみました。
ユーザーアプリケーションはデータキューを使ってオーディオ信号をやりとりします。ダブルバッファ制御はユーザーからは見えないようになっています。これによってユーザーによるオーディオ信号の取り扱いは非常に簡単になります。
初期化から始まってダブルバッファの制御まですべてがrun_ad1836_taskという名前のITRON4タスクで行われます。
AD1836Aは、外部から12.288MHzのクロックを入力することにより、内部でビット・クロックとサンプル・クロックを生成します。サンプル・クロックは96KHzまで可能ですがデフォルトでは48KHzです。また、96KHzでは、入力のうち2チャンネルが使用できなくなります。
AD1836Aの初期化には二つのステップを踏みます。
~PD/RST端子は、EZ-KIT Lite BF533の場合、FLASHデバイスのポートエクステンダーに接続されています。そこで以下のコードによって幅1mSのリセット・パルスを作りだします。
*pFLASHA_PORTA_DIR |= 0x01; // CODEC リセット信号を出力に *pFLASHA_PORTA_OUT &= 0xFE; // CODEC リセットをアサート tslp_tsk( 1 ); // リセット=Lパルスを作る *pFLASHA_PORTA_OUT |= 0x01; // CODEC リセットをデアサート tslp_tsk( 1 ); // リセットからの回復時間を与える
~PD/RSTのデアサート後に1mS待っているのは、コーデックが内部リセット処理を完了するのを待つためです。
リセットを終えたらAD1836Aに初期化パラメータ群を送ります。この引数はEZ-KIT Lite BF533のものをそのまま使いました。
初期化パラメータは定数を送り込むだけですので比較的簡単なプログラムで送り込むことが出来ます。全体の流れとしては
となります。
//-------------------------------------------------------- // SPI設定 // // PF4はAD1836のSPI SSピン *pSPI_FLG = FLS4; // SPIビットレート = SCLK/(2*SPIBAUD) :およそ 2MHz *pSPI_BAUD = 32; // SPI DMA 書き込みによるトリガ, 16bit データ, MSB first, マスタ *pSPI_CTL = TIMOD | SIZE | MSTR; //-------------------------------------------------------- // SPI DMA 設定 // // 16bit データ、メモリから読み出し、終了後割り込み *pDMA5_CONFIG = WDSIZE_16 | DI_EN; // CODEC 初期化文字列のアドレス *pDMA5_START_ADDR = sCodec1836TxRegs; // DMA 転送カウント *pDMA5_X_COUNT = sizeof( sCodec1836TxRegs ) / sizeof( short ); // DMA 増分 *pDMA5_X_MODIFY = sizeof( short ); //-------------------------------------------------------- // SPI 開始 // DMA 開始 *pDMA5_CONFIG |= DMAEN; // SPI 開始 *pSPI_CTL |= SPE;
SPIポートの設定は、これもEZ-KIT Liteのサンプルを使用しています。EZ-KIT Liteと大きく違うのは、SPIによる通信の終了を待つ部分です。SPI DMA転送の終了は割り込みを引き起こします。この割り込みは次のようなユーザー割り込みハンドラによってキャッチされます。
void spi_handler(void) { *pDMA5_IRQ_STATUS = DMA_DONE; // SPI DMA割り込みをクリア ssync(); isig_sem( spi_sem ); }
割り込みハンドラは、割り込み源であるDMAの割り込みビットをクリアしたあと、セマフォ spi_sem を使って割り込みが起きたことをシグナルします。ここでセマフォIDを定数ではなく変数で渡していることに注意してください。AD1836Aのコントロールソフトはライブラリとして供給します。ライブラリ化はユーザー・アプリケーションのコンフィグレーションよりも前に行われますので、その時点ではセマフォのIDはわかりません。セマフォIDの決定を遅らせるために、ID定数ではなく変数の値を使っています。変数の初期化はアプリケーションの責任です。
割り込みハンドラからのシグナルを受けて、本体タスクは次へ進みます。
//-------------------------------------------------------- // SPI 終了 // DMA送出終了を待つ wai_sem( spi_sem ); // DMA終了はSPI送信終了ではないので、ちょっと待つ tslp_tsk( 1 );
wai_sem()から帰った後も1m秒待っていますが、これは割り込み終了がSPI送信終了と一致しないためです。DMA終了割り込みはDMAコントローラが送信バッファのフェッチをすべて終了したときに発生します。そのため、フェッチしたデータは割り込み時点ではまだDMA内部のFIFOに残っており、送信されていません。この送信を待つため、きめうちで1mSのスリープを行っています。
SPIによる初期化が完了するとAD1836Aはオーディオ信号の変換を開始します。AD1836内部には複数のADCとDACが集積されていますので、これらの信号を全部取り出そうとすると、複数のI2S回線を使うか、マルチチャンネル通信を使うしかありません。
ここではマルチチャンネル通信を使います。AD1836AのデータシートではAUXモードと呼ばれています。このモードでは、1スロットにオーディオ信号をひとつ割り当て、8スロットで1サンプルを転送します。例えば受信(ADC)の場合、左右2チャンネル、計4チャンネルありますので、8スロットのうち4スロットを使って1サンプルを転送します。残りのスロットは使いません。そして1サンプルで1フレームを構成します(図1)
図1 スロット構造
マルチチャンネル設定では送信と受信のフレームは同期して転送されます。
シリアルポートの設定もEZ-KIT Liteのサンプルをそのまま使っています。RCR/TCRはそれほど難しくなく、ポイントはマルチチャンネル関係でしょう。SPORTは通信回線として使えるよう、かなり複雑な設定を許しています。そのため、今回のようなシンプルな設定だと迷ってしまいます。
//-------------------------------------------------------- // SPORT0 設定 // マルチチャンネルの場合、常に SPORTx_yCR1.LATFS = 0 // Sport0 受信設定 // 外部クロック, 外部同期信号, MSBファースト // 32-bit データ *pSPORT0_RCR1 = RFSR; *pSPORT0_RCR2 = 31; // データ長 32 // Sport0 送信設定 // 外部クロック, 外部同期信号, MSBファースト // 32ビットデータ *pSPORT0_TCR1 = TFSR; *pSPORT0_TCR2 = 31; // データ長 32 // フレームあたり8スロットのデータを送受で使う *pSPORT0_MTCS0 = 0x000000FF; *pSPORT0_MRCS0 = 0x000000FF; // マルチチャンネル設定 *pSPORT0_MCMC1 = 0x0000; // オフセット = 0, ウインドウサイズ = 8 *pSPORT0_MCMC2 = 0x1000 | MCMEN | MCDRXPE | MCDTXPE; // MFD = 1;
送信、受信ともコーデック側のデータはダブルバッファで扱います。これによってDMAがコーデックとの間でバッファ上のデータをやり取りする間、他方のバッファをプロセッサが処理できます。二つのバッファは完全に分離されていますので、両者を同時に行っても問題は起きません。
ダブルバッファ処理をアプリケーションから隠すため、この部分はrun_ad1836_task内部で全部行います。外部とのやり取りはデータキューを使います。処理を行うループは先頭で、SPORT0からの受信割込みを待ちます。割込みはSPI同様ハンドラで処理され、セマフォを使って通知されます。したがって割り込み待ちにはwai_sem()を使います。同期を取るのは受信割込みであることに気をつけてください。送信割り込みはSPIのところでも説明したようにDMA完了で発生し、その時点で送信は完了していません。それに対して受信割り込みは、バッファへの書き込み完了が確実に受信完了の後に発生するため、同期点として安全です。
while( 1 ){ int sample, slot, bufTx, bufRx; struct CODEC_BUFFER * BufToBeTransmit; // 受信DMA終了割り込みと同期 wai_sem( sport0_sem);
その次はダブルバッファ処理で一番のポイントであるバッファの割り出しです。プロセッサはDMAが使っていないバッファしか使えませんので、DMAコントローラを調べることになります。今回はDMAデスクリプタをそれぞれのバッファにひとつずつ割当て、バッファ上の転送が終わるごとに割り込みをかけています。したがって割込みと同期を取った後にDMAx_NEXT_DESC_PTRを調べれば、DMAが次に転送に使うデスクリプタがわかります。次に転送するということは現在使われておらず、そのデスクリプタに関連したバッファ上で信号処理を行えるということです。
バッファは送受にありますので、判定もそれぞれに対して行います。
// プロセッサが使ってよいバッファを割り出す bufTx = ( &tDescA == *pDMA2_NEXT_DESC_PTR) ? 0 : 1; bufRx = ( &rDescA == *pDMA1_NEXT_DESC_PTR) ? 0 : 1;
バッファが決まったらすぐにコーデックからの受信データをデータキューでアプリケーションに向けて送信します。データキューは深さが1です。そのため、キューに書き込んだ直後にタスク切り替えが起きるとは限りません。キューを待っているタスクの優先度が、ran_ad1836_taskより高ければ切り替えが発生しますが、そうでなければサービスコールから戻ってきて次に進みます。
つぎはデータキューによって送信データがアプリケーションから送られてくるのを待ちます。
// 受信したデータをデータキューで送出 snd_dtq( codec_rx_dtq, (VP_INT) &RxBuffer[bufRx] ); // 送信するデータをデータキューで受け取り rcv_dtq( codec_tx_dtq, (VP_INT *)&BufToBeTransmit );
アプリケーションから送られてきたデータは、送信バッファにコピーします。コピー先の送信バッファはループの先頭で検出したものです。
// 送信データを送信バッファにコピー for ( sample = 0; sample < SAMPLE_PER_INTR; sample++ ) for ( slot =0; slot < 8; slot ++ ) TxBuffer[bufTx].data[sample][slot] = BufToBeTransmit->data[sample][slot]; }
ダブルバッファの制御をrun_ad1836_taskに閉じ込めたので、アプリケーションはとても簡単です。この例はTalk Throughですので、データキューで送り込まれてきた受信データを送信データとして送り返しています。
static struct CODEC_BUFFER tx_buf; void talkthrough_task(VP_INT extinf) { struct CODEC_BUFFER * rx_buf; act_tsk( CODEC_TASK ); while( 1 ) { int sample, slot; rcv_dtq( CODEC_RX_DTQ, (VP_INT*)& rx_buf ); for ( sample = 0; sample < SAMPLE_PER_INTR; sample++ ) for ( slot =0; slot < 8; slot ++ ) tx_buf.data[sample][slot] = rx_buf->data[sample][slot]; snd_dtq( CODEC_TX_DTQ, (VP_INT) & tx_buf ); } }
統計的プロファイラによると、600MHzのBlackfinで97.25%がアイドル時間でした。この97.25%はアプリケーションで自由に使えます。
2.75%はTOPPERS/JSP、Talkthroughによるコピー、ダブルバッファ操作によって使用されています。16.5MIPSに相当します。かつてはVSELPが14MIPSといわれていました。いい時代になりましたねぇ。
ソースコードをダウンロードできるようにしておきます。ビルドするときには _applications\talkthrough\talktrough.dpg を開いてください。
ダウンロード : talkthrough.zip 434KB