ソフト S/PDIF トランスミッタ (12)

今回はデータの「生成」側と「消費」側との「同期」の方法についての話です。
次のような不具合、

  • 送信中のデータを新しいデータで上書きしてしまう (オーバーラン・エラー)
  • 送信データが必要なタイミングまでに新しいデータが間に合わない (アンダーラン・エラー)

を生じないで連続的にデータを流し続けるためには、生成側の平均速度と消費側の平均速度が厳密に一致している必要があります。
これを実現するために、バッファ・メモリを用意し、「融通」の利く方の平均速度を制約の多い方の平均速度より速く設定しておき、バッファが一杯、あるいは空になったら融通の利く方がウェイト状態になることで両者の実質的な平均速度を一致させます。
今回のソフト S/PDIF トランスミッタで言えば、サンプリング周波数 48 kHz ではトランスミッタ出力は 6.144 bps の一定のレートで出力し続けなければなりません。
SPI/I2S モジュールにはデータ FIFO がないので、コンスタントに 384 kHz のレートで 1 ハーフワード (16 ビット・ワード) ずつ DMA 転送されます。
fs = 48 kHz のレートで 2 ch ステレオ 16 ビット・オーディオ・データは BMC に符号化されてサブフレーム 2 個分の 8 ハーフワードのデータになります。
したがって、データ生成側は 48 kHz より早い平均レートで 2 ch ステレオ・データを生成し、バッファ・メモリに空きがあったら書き込み、空きがなかったら空きができるまで待つという動作を繰り返します。
このバッファリングのメカニズムとして「ダブル・バッファ」あるいは「ピンポン・バッファ」と呼ばれる、バッファを「2 面」持つ方式を使用します。
STM32F4xx シリーズの (高機能版) DMA コントローラには、その名も「Double buffer mode」と呼ばれる動作モードがあり、転送元アドレス・レジスタ/転送先アドレス・レジスタを 2 組持ち、切り替えがハードウェアで自動で行われる機能を備えています。
しかし、この「ダブル・バッファ・モード」は F4xx 用ライブラリである STM32CubeF4 ではサポートされているものの、初期化 C コード・ジェネレータである STM32CubeMX ではサポートされていないので使用しませんでした。
その代わりに、他の F1xx シリーズなどの (低機能版) DMA コントローラにもある「Circular mode」と「Half-transfer reached」割り込みを利用しています。
「1/2 転送完了」割り込みは、DMA 転送が指定の回数の半分終了した時点で発生します。
つまり、DMA バッファの先頭からバッファの 1/2 まで (前半) のデータの転送が完了したことを示します。
ソフトウェア側で 1/2 転送完了割り込みを認識した時点では、すでに DMA ハードウェアはバッファの「後半」のデータの転送中であることが保証されるので、DMA バッファの「前半」のデータに対するアクセスは、「Transfer complete」割り込みが発生するまでの間は DMA ハードウェアのアクセスと競合しません。
同様にして「(全) 転送完了」割り込みが発生すると、バッファの「後半」のデータが転送ずみであることが分かるので、ソフトウェア側では (安心して) バッファの後半のデータをアクセスすることができます。
「Circular mode」をオンにしておくと、転送完了後はアドレス/転送回数レジスタを元の状態に戻して「頭」から DMA 転送を再開するので、上に述べたプロセスを繰り返します。
STM32CubeF4 の HAL (Hardware Abstraction Layer) ライブラリでは、割り込みハンドラ内で割り込み要因を特定したあと、各モジュールごとにまとめてユーザ・コールバック関数を呼び出す仕様になっています。
たとえば、SPI/I2S モジュールを I2S モードで使う場合には、1/2 転送完了時に
HAL_I2S_TxHalfCpltCallback() 関数
が呼ばれ、(全) 転送完了時には
HAL_I2S_TxCpltCallback() 関数
が呼ばれます。
ユーザ・コールバック関数は「登録」する方法ではなく、ライブラリ側では何もしないデフォルトの関数を「weak」シンボルとして定義しておき、必要ならばユーザが「普通の強さ」の同名の関数を定義することにより「オーバーライド」する方式を取っているため、関数の名前は「正確」に上に示した通りでなければなりません。
ソフト S/PDIF トランスミッタ・プログラムでのユーザ・コールバック関数を示します。

#define SPDIF_TXBUF_INDMAX (sizeof(spdif_txbuf)/sizeof(int16_t))

// S/PDIF TX buffer index structure
typedef struct tag_spdif_txind_t {
// room for output data (count in terms of halfword)
  volatile uint16_t room; 
// S/PDIF TX buffer pointer
  volatile uint16_t *ptr; 
} spdif_txind_t;

static spdif_txind_t spdif_txind;

// user callback for DMA half complete
void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{
// check for SPI/I2S device
  if (SPI3 == hi2s->Instance) { 
    spdif_txind.room = (SPDIF_TXBUF_INDMAX / 2);
// TX buffer pointer (the first half)
    spdif_txind.ptr  = &(spdif_txbuf[0]); 
  } //
} // void HAL_I2S_TxHalfCpltCallback()

// user callback for DMA (full) complete
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
// check for SPI/I2S device
  if (SPI3 == hi2s->Instance) {
    spdif_txind.room = (SPDIF_TXBUF_INDMAX / 2);
// TX buffer pointer (the second half)
    spdif_txind.ptr  = &(spdif_txbuf[SPDIF_TXBUF_INDMAX / 2]); 
  } //
} // HAL_I2S_TxCpltCallback()

ユーザ・コールバック関数は全 I2S モジュールに共通なので、S/PDIF 用として使っている SPI3/I2S3 モジュールであるかどうかを最初に調べています。
コールバック関数の中の処理は簡単で、バッファの空きを示す「spdif_txind.room」の値をハーフワード単位で表現した出力バッファ・サイズの 1/2 として、メイン側のプログラムにバッファに空きができたことを知らせます。
また、出力バッファへデータを書き込む位置を示すポインタ「spdif_txind.ptr」を DMA によって空いた側 (前半/後半) のアドレスに設定します。
メイン関数部分のコードの断片を下に示します。

int main(void)
{
  int16_t sfbuf[2];         // subframe data buffer
  int32_t sin_acc = 0;      
  int32_t cos_acc = 0x7e00; // initial radius
  int i;

. . . <中略> . . .

// init. SPDIF TX buffer for 1 block, fs=48 kHz, copy permitted, no built-in wave
  setup_spdif_txbuf(1, CH_STATUS_BITS, 0); 
  spdif_txind.room = 0; // assume buffer full at first
// start I2S transmission by DMA (automatic repeat by 'circular mode')  
  HAL_I2S_Transmit_DMA(&SPI3,        // handle of SPI/I2S for S/PDIF
                       spdif_txbuf,  // TX buffer (array of uint16_t)
                       SPDIF_TXBUF_INDMAX); // buffer size in 16-bit halfword
// Note: I2S configured for 16-bit data word / 16-bit frame per channel

  while (1) {
// sin_cos_freq = 48   [kHz] * 3775 / (2 * pi * 2^16) = 440.05 [Hz]
// sin_cos_freq = 44.1 [kHz] * 4108 / (2 * pi * 2^16) = 439.96 [Hz]
    const uint16_t acc_mul[2] = {3775, 4108};
    const uint32_t acc_shift = 16;

    if (0 < spdif_txind.room) {
      spdif_encode_subframe_data(sfbuf, 2); // put 2 subframes (L-ch/R-ch)
// update sin/cos value using DDA calculation
      cos_acc -= ((sin_acc * acc_mul[0]) >> acc_shift);
      sin_acc += ((cos_acc * acc_mul[0]) >> acc_shift);
      sfbuf[0] = cos_acc; // L-ch
      sfbuf[1] = sin_acc; // R-ch
    } // if (...
  } // while (1) { ...
} int main()

メインの無限ループ内で「spdif_txind.room」が 0 でない、つまり出力バッファに空きがある場合には、spdif_encode_subframe_data() 関数を呼び出して、2 ch ステレオ・オーディオ・サンプルに対応する 2 サブフレーム分のエンコードをして出力バッファに書き込みます。
その後で、DDA (Digital Differential Analyzer) アルゴリズムによる 440 Hz サイン波生成を行っています。
「spdif_txind.room」が 0 である、つまり出力バッファに空きがない場合には何もせず、無限ループを回るだけです。