STM32F4-Discovery 用FM音源プログラム -- TGSTM32F4 (2)

ボード上の外付けステレオ DAC (CS43L22) へ DMA を使用してオーディオ出力する簡単なテストプログラムが動くようになりました。 記事の最後にソースリストを示します。
単に、サンプリング周波数 48 kHz で周波数 440 Hz の音を、のこぎり波を左チャンネルから、三角波を右チャンネルから出力するだけです。
当初は、STMicro 提供のサンプル・プログラム「Audio_Playback_and_Record」(解説はアプリケーション・ノート AN3997) では DAC から出力されるのに、自作のテストプログラムでは、音どころか、MCLK や SCLK すら発生しない状態でした。
調べてみると、これは I2S 用の PLL の設定がされていないために、I2S モジュールへのクロック自体が供給されていないためと分かりました。
「Audio_Playback_and_Record」プログラムでは「main.c」から明示的に呼び出されるプログラム中には I2S PLL 設定のコードは含まれておらず、スタートアップ・プログラムから main() 関数の実行前に自動的に呼び出される「SystemInit()」関数からさらに「SetSysClock()」関数が呼び出され、その中で設定が行われていました。 (ファイルとしては「system_stm32f4xx.c」の中)
SetSysClock() 関数内では各種のクロックの設定を行っているので、その意味では I2S PLL の設定をするのにふさわしい場所ですが、FM 音源プログラムではサンプリング周波数の切り換えの必要があるので、後に示すソースリストのように、ユーザプログラム中に引っ張り出してきて、 I2S PLL クロックの設定を行うようにしました。
ただし、後に示すプログラムでは、I2S PLL を 86 MHz の固定値に設定するだけで、可変できるようにはなっていません。
プログラムのコンパイル方法は、まず、「Atollic TrueSTUDIO for STMicroelectronics STM32 Lite V2.3.0」で通常のように STM32F4-Discovery ボード用の新規 C プロジェクトを作成します。
そして、プロジェクト内の「Utilities / STM32_EVAL」フォルダ内の「stm32f4_discovery_audio_codec.h」ファイル中の各 #define に対しコメントの付け外しで、次のように設定します。

// #define I2S_INTERRUPT
// #define AUDIO_MAL_MODE_NORMAL
#define AUDIO_MAL_MODE_CIRCULAR
#define AUDIO_MAL_DMA_IT_TC_EN
#define AUDIO_MAL_DMA_IT_HT_EN
#define USE_DEFAULT_TIMEOUT_CALLBACK

この設定で、

  • I2S 送信を各サンプルごとの割り込みを使わずに DMA で行う
  • DMA は通常モードではなくサーキュラー・モードで行う
  • DMA の HalfTransfer 割り込みを使う
  • I2C 通信のタイムアウト・コールバック関数はデフォルトのものを使う

という動作モードを選択することになります。
STM32F10x などの従来からある DMA コントローラの「サーキュラー・モード」と、「HalfTransfer」割り込み機能を利用して「ダブル・バッファ」を実現しており、機能アップした STM32F4 の DMA コントローラがハードウェアで本来持っている「ダブル・バッファ・モード」を利用したものではありません。
次に、メニュー・バーの「Project / Properties」を選んで開くダイアログで、
C/C+ Build / Settings / Tool Settings / C Compiler / Miscellaneous / Other Options
のテキスト・ボックスに

-DHSE_VALUE=8000000

を追加します。
これは、Std Periph Driver のシンボル「HSE_VALUE」 (外部高速クロック周波数) のデフォルト値が 25 MHz となっているので、ボードに実装されているクリスタルの実際の値 8 MHz でオーバーライドするためです。
STM32F10x の場合には、デフォルト値が 8 MHz だったので、特に設定が必要なかったのですが、STM32F4xx では、イーサネットの PHY 用のクロックとして使う 25 MHz がデフォルト値となったので、設定ファイルに変更を加えるか、あるいは上のように、コンパイル時のオプションとして追加する必要があります。
本来、シンボルの値の定義を追加する専用の「場所」は用意されているのですが、Lite 版の統合環境では、変更不可能なエントリとなっているので、上のように、コンパイル時の追加オプションとして与えます。
最後に、「src」フォルダ内の「main.c」を下のソースリストと置き換えてコンパイルします。

/****************************/
/* Wave generation sample   */
/* using stereo DAC CS43L22 */
/* by DMA                   */
/* 2012/02/15               */
/****************************/

/* Includes */
#include "stm32f4xx.h"
#include "stm32f4_discovery.h"
#include "stm32f4_discovery_audio_codec.h"
#include <stdlib.h> // for abs() function
#include <string.h> // for bzero(), memcpy() function

/* PLLI2S_VCO = (HSE_VALUE Or HSI_VALUE / PLL_M) * PLLI2S_N
   I2SCLK = PLLI2S_VCO / PLLI2S_R */
// I2SCLK = 258 [MHz]  / 3 = 86 [MHz]
#define PLLI2S_N   258
#define PLLI2S_R   3

#define WAVEQUESIZE (256)
#define STEREO_MODE (1)
#define AUDIO_SAMPLES_PER_DMA_BUF (8)
// factor '2' for 'stm32f4_discovery_audio_codec.c/.h' V1.0.0 (19-Sep-2011)
#define DMA_BUF_SIZE_ADJ (2)
// factor '4' for 'stm32f4_discovery_audio_codec.c/.h' V1.1.0 (28-Oct-2011)
//#define DMA_BUF_SIZE_ADJ (4)

// DMA buffer for audio samples
// factor 2 for STEREO sample
// factor 2 for 16-bit (2-byte) data
#define DMA_BUF_BYTES (2*2*AUDIO_SAMPLES_PER_DMA_BUF)
typedef uint16_t dac_dma_buf_t[2*AUDIO_SAMPLES_PER_DMA_BUF];
dac_dma_buf_t dac_dma_buf[2]; // double buffer

// wave queue handling defines / functions
volatile int   wq_wr_ix, wq_rd_ix;
int16_t wave_que[WAVEQUESIZE][STEREO_MODE+1];

#define WAVE_QUE_HAS_ROOM (wq_wr_ix != wq_rd_ix)
#define UPDATE_QUE_INDEX(ix, n) (ix = ((ix + (n)) & (WAVEQUESIZE - 1)))
#define UPDATE_QUE_READ_IX(n)  UPDATE_QUE_INDEX(wq_rd_ix, n)
#define UPDATE_QUE_WRITE_IX(n) UPDATE_QUE_INDEX(wq_wr_ix, n)

__inline__ void ENQUEUE(uint16_t Lch, uint16_t Rch)
{
  wave_que[wq_wr_ix][0] = Lch;
  wave_que[wq_wr_ix][1] = Rch;
  UPDATE_QUE_WRITE_IX(1);
} // void ENQUEUE()

__inline__ void INIT_QUE_IX( void )
{
  wq_wr_ix = 0;
  wq_rd_ix = 0;
} // void INIT_QUE_IX()

//
// I2S PLL setup for 86 MHz MCLK
//
void stm32_PLLI2S_MCLK_setup()
{
// disable PLLI2S for parameter setting
  RCC->CR &= ~((uint32_t)RCC_CR_PLLI2SON);
/* PLLI2S clock used as I2S clock source */
    RCC->CFGR &= ~RCC_CFGR_I2SSRC;
/* Configure PLLI2S */
    RCC->PLLI2SCFGR = (PLLI2S_N << 6) | (PLLI2S_R << 28);
/* Enable PLLI2S */
    RCC->CR |= ((uint32_t)RCC_CR_PLLI2SON);
/* Wait till PLLI2S is ready */
    while((RCC->CR & RCC_CR_PLLI2SRDY) == 0) {}
} // stm32_PLLI2S_MCLK_setup()

int main(void)
{
  int32_t  tri_wave = 0;
  uint32_t Lch_data = 0;
  uint32_t Rch_data = 0;
  uint32_t ph_acc = 0; // phase accumulator
// phase accum increment for 440 Hz @ fs = 48 kHz
  uint32_t ph_inc = ((440 * 0x800000UL) / 48000) << 9;

  STM_EVAL_LEDInit(LED6);
  STM_EVAL_LEDOn(LED6); // Blue LED ON
// wave queue (FIFO) and DMA buffer setup
  INIT_QUE_IX(); // initialize queue indexes
  bzero(dac_dma_buf, sizeof(dac_dma_buf)); // clear DMA buffer
// I2S DAC (CS43L22) setup
  stm32_PLLI2S_MCLK_setup(); // set I2S PLL MCLK = 86 MHz
  EVAL_AUDIO_SetAudioInterface(AUDIO_INTERFACE_I2S); // select I2S interface DAC
  EVAL_AUDIO_Init(OUTPUT_DEVICE_BOTH, 83, I2S_AudioFreq_48k); // vol = 83, fs = 48kHz
// start CS43L22 stereo audio DAC playing by DMA
  EVAL_AUDIO_Play(dac_dma_buf[0],
            DMA_BUF_SIZE_ADJ*sizeof(dac_dma_buf));
/* Infinite loop */
  while (1)
  {
    if (WAVE_QUE_HAS_ROOM) {
      ENQUEUE(Lch_data, Rch_data);// put stereo data in queue
// saw and triangle wave generation
      tri_wave = abs((int32_t) ph_acc) - 0x40000000UL;
      Lch_data = (ph_acc   >> 16); // sawtooth wave
      Rch_data = (tri_wave >> 15); // triangle wave
      ph_acc  += ph_inc; // update phase accumulator
    } // if (WAVE_QUE_HAS_ROOM)
  } // while (1)
} // int main()

void fill_dma_buf(int n)
{
// copy audio sample data from wave_que[] to DMA buffer
  memcpy(dac_dma_buf[n], wave_que[wq_rd_ix], sizeof(dac_dma_buf[0]));
  UPDATE_QUE_READ_IX(AUDIO_SAMPLES_PER_DMA_BUF);
} // void fill_dma_buf()

// Callback used by stm32f4_discovery_audio_codec.c.
// Refer to stm32f4_discovery_audio_codec.h for more info.

void EVAL_AUDIO_TransferComplete_CallBack(uint32_t pBuffer, uint32_t Size){
// "1"-page of DMA buffer has transfered, fill it
  fill_dma_buf(1);
}

void EVAL_AUDIO_HalfTransfer_CallBack(uint32_t pBuffer, uint32_t Size){
// "0"-page of DMA buffer has transfered, fill it
  fill_dma_buf(0);
}

uint16_t EVAL_AUDIO_GetSampleCallBack(void){
  /* TODO, implement your code here */
  return 0;
}

これを実行すると、青色の LED (LED6) だけが点灯し、のこぎり波が左チャンネルから、三角波が右チャンネルから出力されます。
TureSTUDIO で STM32F4-Discovery 用のプロジェクトを新規作成すると、プロジェクト内にコピーされてくる「stm32f4_discovery_audio_codec.c / .h」のバージョンは
V1.0.0 (19-Sep-2011)
と古く、一方、STMicro の web サイトからダウンロードした「stm32f4discovery_fw」に含まれるファイルのバージョンは
V1.1.0 (28-Oct-2011)
となっており、両者は異なっています。
EVAL_AUDIO_Play() 関数の第二引数として渡す、オーディオ・データ・バッファのサイズの扱いが両者で違っており、使うライブラリのバージョンによってサイズに掛けている定数値「DMA_BUF_SIZE_ADJ」の値の定義を変更する必要があります。