新版FM音源プログラム (9)

STM32F4xx シリーズの DMAC は STM32F1xx シリーズの DMAC に比べて機能が拡張されており、ハードウェアで「ダブル・バッファ」機能をサポートしています。
これは、メモリ・ベース・アドレス・レジスタを「2 面」の DMA バッファに対応して 2 組持っていて、一方の「面」の設定での規定の回数の転送が終了すると、自動で「使用中の面」のレジスタと、「空きの面」のレジスタとの役割が入れ替わり DMA 転送を続けるようになっています。
メモリ・ベース・アドレス・レジスタは 2 組あるので、各面の DMA バッファのメモリ中のアドレスは任意に選べ、さらには DMA 実行中に空いている面のベース・アドレスを (オン・ザ・フライで) 書き換え可能です。
しかし、STM32F4-Discovery 用の BSP および STM32CubeFx のHAL ライブラリでは、このハードウェア・ダブル・バッファ・モードはサポートされていません。
HAL (Hardware Abstraction Layer) ライブラリでは、デバイス間のハードウェアの差異を吸収して統一的に扱うという性質上、一部の上位機種にしかない機能はサポートされず、各機種に共通する基本機能だけがサポートされています。
「基本機能」でも、1/2 転送完了 (Half Transfer Complete) 割り込みを利用して、シングル・バッファを (1/2 バッファ × 2) と見なしてダブル・バッファ機能を実現できます。
STM32F4xx と S/PDIF に関して、以前の記事、

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

でも同様の DMA の話題について説明しています。
また、PIC32MX と AC97 コーデックとの接続についても、同様の話題を記事

PIC32MX220F032B(5)-- AC97 コーデックを接続する(2)

で触れています。
FM 音源プログラムでは、処理量低減のため、エンベロープの計算とキャラクLCD へのデータ送出は 1 ms 間隔、ポルタメント処理と LFO 計算と SMF シーケンサの更新は 4 ms 間隔で行なっています。
つまり、4 ms 周期で 1 サンプル当たりの処理量は増減するので、それを平均化するためにオーディオ出力 FIFO (リング・バッファ) の容量を 4 ms 分のサンプルを保持できる量に選んでいます。
具体的には、サンプリング周波数 48 kHz では、FIFO バッファ容量を
4 [ms] × 48 [kHz] = 192 サンプル
以上としています。
DMA を使う場合と、使わない場合とを区別せず、この FIFO バッファは常に利用することとし、DMA バッファへの転送は DMA 割り込みコールバック関数内で行なうことにしました。

まず、前回示した "main.c" のユーザ記述部の「PV」(Private Variables) セクションに FIFO バッファとなる以下の内容を追加します。

/* USER CODE BEGIN PV */
. . . . . <中略> . . . . .

//   512 mono audio (16-bit wide) samples
// = 256 stereo audio samples 
// = 1024 bytes
#define DAC_FIFO_SIZE    (512)

int16_t dac_fifo_buf[DAC_FIFO_SIZE];

volatile int wr_ix = 0;
volatile int rd_ix = 0;

. . . . . <中略> . . . . .
/* USER CODE END PV */

DMA 割り込みのユーザ・コールバック関数名は、1/2 完了および全転送完了に対してそれぞれ、
BSP_AUDIO_OUT_HalfTransfer_CallBack()
BSP_AUDIO_OUT_TransferComplete_CallBack()
で、この通りの名前でないと、ユーザ・コールバック関数として扱われず、単にどこからも呼ばれない関数としてリンク時の最適化で削除されてしまいます。
ユーザ・コールバック関数の内容は次のようになっています。

/* USER CODE BEGIN 4 */
void BSP_AUDIO_OUT_TransferComplete_CallBack(void)
{
// fill the second half of DMA buffer
  memcpy(&(dac_dma_buf[1]), &(dac_fifo_buf[rd_ix]), DMA_BUF_BYTES);
  rd_ix += DMA_BUF_SIZE; // advance read index
  if (DAC_FIFO_SIZE <= rd_ix) { // wrap around
    rd_ix = 0;
  } // if
}

void BSP_AUDIO_OUT_HalfTransfer_CallBack(void)
{
// fill the first half of DMA buffer
  memcpy(&(dac_dma_buf[0]), &(dac_fifo_buf[rd_ix]), DMA_BUF_BYTES);
  rd_ix += DMA_BUF_SIZE; // advance read index
  if (DAC_FIFO_SIZE <= rd_ix) { // wrap around
    rd_ix = 0;
  } // if
} // void BSP_AUDIO_OUT_HalfTransfer_CallBack()

void BSP_AUDIO_OUT_Error_CallBack(void)
{
} // void BSP_AUDIO_OUT_Error_CallBack(void)
/* USER CODE END 4 */

全転送完了割り込みが発生すると、それは、DMA バッファの後半部分、つまり dac_dma_buf[1] 側のすべてのオーディオ・データの転送が完了したということですから、新しいデータによる書き換えが可能になったことを示します。
memcpy() 関数で、dac_fifo_buf 配列から dac_dma_buf[1] 配列に DMA_BUF_BYTES バイトのデータをコピーします。
その後は、FIFO バッファの読み出しインデクス rd_ix を DMA_BUF_SIZE だけインクリメントし、ラップ・アラウンド処理を行います。
DMA コントローラを circular mode に再設定してあるので、全転送完了後は、何もしなくても自動的に再度始めから DMA 転送を繰り返します。
circular mode にしない場合には、全転送完了後に BSP の API である BSP_AUDIO_OUT_ChangeBuffer() を呼び出して DMA を再起動する必要があります。
同様にして、1/2 転送完了割り込みが発生すると、それは、DMA バッファの前半部分、つまり dac_dma_buf[0] 側のすべてのオーディオ・データの転送が完了したということですから、新しいデータによる書き換えが可能になったことを示します。
memcpy() 関数で、dac_fifo_buf
配列から dac_dma_buf[0] 配列に DMA_BUF_BYTES バイトのデータをコピーします。
その後は、FIFO バッファの読み出しインデクス rd_ix を DMA_BUF_SIZE だけインクリメントし、ラップ・アラウンド処理を行います。
"main.c" 中の FIFO バッファへの書き込み側、つまり、オーディオ・データの「生産側」では下のリストのようにして FIFO バッファへ書き込みます。

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
. . . . . <中略> . . . . .

    if (rd_ix != wr_ix) { // FIFO has room

. . . . . < 1 ステレオ・オーディオ・サンプル発生 > . . . . .

      dac_fifo_buf[wr_ix++] = < L ch オーディオ・データ >;
      dac_fifo_buf[wr_ix++] = < R ch オーディオ・データ >;
      if (DAC_FIFO_SIZE <= wr_ix) {
        wr_ix = 0;
      } // if (DAC_FIFO_SIZE <= wr_ix) { ...
    } // if (rd_ix != wr_ix) { ...

. . . . . <中略> . . . . .
  } // while (1) { ...
  /* USER CODE END 3 */

(rd_ix == wr_ix) つまり、FIFO バッファの読み出しインデクスと書き込みインデクスが等しい場合に FIFO バッファが満杯というお約束なので、両者が等しくない場合には FIFO バッファに空きがあることを示しています。
それを if 文で判定して、空きがあれば新しいステレオ・サンプル 1 個分の計算を行い、FIFO バッファに書き込みます。
発生させるオーディオ・データがモノラルで、左右のチャネルに同じデータを送る場合には、L/R に対応して 2 つある dac_fifo_buf[] への書き込みの両方とも同じモノラル・データを使うことになります。
その後は書き込みインデクスのラップ・アラウンド処理を行います。
以上、長くなりましたが、STM32CubeMX で生成する初期化コードと、BSP を共存させる方法について述べてきました。
BSP ライブラリのコピーなど、下準備は必要ですが、前回と今回の記事で示したように、ユーザ・プログラム上で記述すべきコードの量は多くなく、手軽にオーディオ DAC 出力が利用できます。