PSoC5LP Prototyping Kit (21) --- SPDIF_Tx と DMA (9)
DFB (Digital Filter Block) のハードウェア DMA / 割り込み要求は「出力データ・レディ」の条件により発生するので、データの流れとは逆向きに後段から前段へとデータ転送要求を出す流れとは一致しません。
「入力データ・エンプティ」のようなハードウェア信号は存在していないので、DSP プログラム上の対処と、外部ハードウェアなしでは入力側/出力側とも DMA で直結するのは難しいと思います。
そんなわけで、簡単のため、DFB についてはソフトウェアでデータを入力し、計算結果が得られるまでポーリングして待つ方式にしました。
main() 関数での処理を文章で表現したものを下に示します。
int main() { 各種初期化 DMA / SPDIF_Tx / Filter スタート Filter にダミー・データ書き込み for(;;) { // 無限ループ if (dac_fifo に空きがある) { if (Filter の両チャネルともデータ・レディ) { Filter からフィルタ済みデータを読み出し dac_fifo に書き込み Filter の入力ステージング・レジスタに「素」のデータ書き込み 次のオーディオ・サンプル・データ計算 } // if (両 ch レディ) } // if (fifo 空き) 他の仕事 } // for(;;) { ... } // int main()
普通に、
- サンプル・データ発生
- DFB 入力ステージング・レジスタへ書き込み
- DFB 出力データ・レディになるまで待つ
- DFB 出力ホールディング・レジスタから読み込み
- SPDIF_Tx への FIFO バッファに書き込み
というシーケンシャルな手順にすると、サンプル・データ発生のための処理時間と、DFB ハードウェアでの処理時間とを足したものが全体での処理時間となってしまいます。
DFB ハードウェアでの処理時間は BUS_CLK 49 MHz で 3 μs 程度ですが、その影響を少なくするために、次のような処理手順としました
- 無限ループの始め
- (FIFO に空きあり) かつ (DFB 出力データ・レディ) なら以下を実行
- 他の処理
- 無限ループの終わり
DFB での計算はハードウェアで行なわれるので、DFB 入力レジスタ書き込によりフィルタ計算をスタートさせてから次のサンプル計算を実行すれば、両者の計算は「パラレル」に進行し、合計の実行時間としては単純な和ではなく、どちらか長くかかった方に揃えられます。
上のプログラムで、無限ループに入る前に DFB にダミー・データを書き込んでフィルタ計算をスタートさせているのは、ループ内でまず出力データ・レディのチェックから始めているので、最初に出力データが存在している状態でないと動き出さないためです。
次に実際のプログラムを示します。 まず、#define の一部です。
// phase accumlator bit allocation #define PHACC_FRAC_BITS (12) #define PHACC_INT_BITS (12) // 4K point sine table #define PHACC_INT_MASK ((1UL << PHACC_INT_BITS) - 1) #define PHACC_BITS (PHACC_INT_BITS + PHACC_FRAC_BITS) // parameters for linear frequency sweep #define SWEEP_SEC (180) #define SWEEP_MIN_Hz (10) // max sweep freq default to fs/2 #define PHINC_MAX (1UL << (PHACC_BITS-1)) #define PHINC_MIN (((SWEEP_MIN_Hz * 2 * PHINC_MAX)/FS_org)) // oversampling factor (4) #define OVS (4) // original sampling frequency (12 kHz) #define FS_org (12000) // oversampling frequency (48 kHz) #define FS_ovs (OVS * FS_org)
main() 関数の内容です。
int main() { uint32_t samp = 0; // initialize FIFO write/read index dac_fifo.wr_ix = 0; dac_fifo.rd_ix = 0; // set linear frequency sweep parameters ph_inc = PHINC_MIN; // minimum freq (10 Hz) // frequnecy increment delta_inc = (uint32_t)(PHINC_MAX / ((double)FS_org * SWEEP_SEC)); // start components DMAconfig(); SPDIF_Tx0_Start(); SPDIF_Tx0_EnableTx(); Filter0_Start(); // enable data ready flag of HOLDING register A, B Filter0_SetInterruptMode(0x03); ISR_tx0_StartEx(ISR_tx0_InterruptEx); CyGlobalIntEnable; /* Uncomment this line to enable global interrupts. */ // kick DFB hardware Filter0_Write16(Filter0_CHANNEL_A, samp); // L ch Filter0_Write16(Filter0_CHANNEL_B, samp); // R ch for(;;) { if (dac_fifo.rd_ix != dac_fifo.wr_ix) { // FIFO has room // check for digital filter output ready by polling if (Filter0_IsInterruptChannelA() && Filter0_IsInterruptChannelB()) { // both L/R data ready? // read filtered stereo sample from DFB HOLD reg A/B, then write to FIFO dac_fifo.buf[dac_fifo.wr_ix++] = (int16_t)Filter0_Read16(Filter0_CHANNEL_A); // L ch dac_fifo.buf[dac_fifo.wr_ix++] = (int16_t)Filter0_Read16(Filter0_CHANNEL_B); // R ch // wrap around FIFO write index if (DAC_FIFO_SIZE <= dac_fifo.wr_ix) { dac_fifo.wr_ix = 0; } // if (DAC_FIFO_SIZE <= ... // write new stereo audio sample to DFB STAGE reg A/B Filter0_Write16(Filter0_CHANNEL_A, (uint16_t)samp); // L ch Filter0_Write16(Filter0_CHANNEL_B, (uint16_t)(samp >> 16)); // R ch // update stereo audio sample with oversampling samp = upsample(OVS); } // if (Filter0. ... } // if (dac_fifo.rd_ix != ... } // for(;;) { ... } // int main()
DFB にアップサンプルの機能はなく、常にアップサンプルされたサンプリング周波数での計算を行なう必要があります。 その上で、元のサンプリング周波数での入力サンプル系列に「0」を挿入してアップサンプルした系列を入力する必要があります。
アップサンプルおよび (元の fs での) サンプル発生のようすを図示したものを下に示します。
upsample() 関数では、4 回に 1 回だけ gen_sample() 関数を呼び出して元の fs での新しいサンプルを得て値を返し、残りの 3 回は「0」を返します。
ステレオ 2 ch 対応ですが、32 ビット整数の下位 16 ビット、上位 16 ビットに L / R サンプル・データを詰め込んでいます。
gen_sample() 関数では、目的のアプリケーションに合わせたサンプル列を発生させます。
ここでは、「リニア周波数スイープ正弦波」を発生させて、WaveSpectra などで周波数特性を観測するのを目的にしています。
サイン波テーブルのインデクスを供給するフェーズ・アキュムレータの ph_acc の増分である ph_inc の値は出力される周波数に比例しています。 ph_inc の値をリニアに増加させれば出力周波数もリニアに増加するので、定数 delta_inc を増分として ph_inc を増加させています。
サイン波テーブルのインデクスに π/2 相当のオフセットを加えて「コサイン波」を発生させ、L / R チャネル間で 90° 位相の異なる正弦波としています。
実際のプログラムを下に示します。
uint32_t gen_sample( void ) { uint32_t samp; uint32_t phh; ph_acc += ph_inc; // update phase acc ph_inc += delta_inc; // updata phase inc (freq sweep) if (PHINC_MAX <= ph_inc) { // reached to max freq? ph_inc = PHINC_MIN; // set to min freq if so } // if (PHINC_MAX <= ph_inc) ... // calc sine table index (L ch) phh = (ph_acc >> PHACC_FRAC_BITS); // extract integer part phh &= PHACC_INT_MASK; // reduce to 0..4095 samp = (uint16_t)stab[phh]; // L ch sine data // calc table index for cosine (R ch) phh += (1UL << (PHACC_INT_BITS-2)); // add pi/2 offset phh &= PHACC_INT_MASK; // reduce to 0..4095 samp |= (stab[phh] << 16); // combine L/R return(samp); } // uint32_t gen_sample() uint32_t upsample(int ovs) { static int upsamp_phase = 0; uint32_t samp; if (upsamp_phase++) { // insert '0' samp = 0; } else { // sample update timing samp = gen_sample(); } // if (upsaple_phase++) { ... upsamp_phase %= ovs; // modulo by oversampling factor return(samp); } // uint32_t upsample()
周波数スイープが元の fs でのナイキスト周波数 (fs/2) に達すると、周波数を最低周波数 10 Hz に戻します。 スイープ 1 周期は 180 秒 (3 分) に設定しています。