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

STM32F4 の SPI/I2S モジュールには、オーバーサンプリング DAC 用のマスター・クロック (MCLK) を発生させる機能があります。
しかし、そのオーバーサンプリング比は 256 倍 (256 fs) に固定で、また、回路の制限により、SPI/I2S モジュールへのクロック (I2SCLK) は、さらにその 4 倍以上が必要になります。
この I2SCLK は、外部からのクロック入力と、内蔵の I2S 専用の PLL クロックとの、どちらかを選ぶことができます。
MCLK を発生させる場合の SPI/I2S のクロック系統を図にしたものを下に示します。

CPU コアや、バス (AHB/APB) のクロックを発生させる PLL と同様の機能を持つ別の PLL が I2S 専用に用意されています。
基準周波数は両方の PLL に共通で、HSI (内部高速クロック, 16 MHz) あるいは HSE (外部高速クロック) を 1/PLLM に分周して作成しています。
この基準周波数は 1 MHz 〜 2 MHz の範囲に制限されています。
RTC ブロックでは、通常の 32.768 kHz クロックの他に、HSE クロックを分周して得られた 1 MHz クロックを使用することができます。
そのため、HSE を 1 MHz の整数倍の周波数に選んでおけば、RTC 用の 32.768 kHz クリスタルを実装しなくても RTC ブロックを機能を活かせることになります。
実際の STM32F4-Discovery ボードでは、HSE に 8 MHz のクリスタルを使用し、PLLM = 8 として 1 MHz の PLL 基準周波数を発生させています。
これを、コアクロック用 PLL では PLLN = 336 倍して VCO 出力周波数として 336 MHz を発振させ、PLLP= 2 分の 1 にして 168 MHz を CPU コアに供給しています。
さらに、VCO 周波数 336 MHz を PLLQ = 7 分の 1 にして 48 MHz を得て、(フルスピード以下の) USB 回路のクロックとして供給しています。
この辺の PLL の設定は、デフォルトの C スタートアップ・ルーチンの中で自動的に行われており、各部分が最高速で動作する設定になっているので、通常は気にする必要はありません。
I2S 専用の PLLI2S については、デフォルトのスタートアップ・ルーチンでは設定されませんから、処理をスタートアップ・ルーチン内に追加するか、ユーザ・プログラム中でケアする必要があります。
また、サンプリング周波数を可変にする場合には、SPI/I2S モジュール内の自由度はあまり大きくないので、PLLI2S の設定自体を変更して、I2SCLK を変化させる必要があります。
上の図では、I2S のサンプリング周波数 fs = 48 kHz を得るための設定の具体的な数値を書き込んであります。
各パラメータの値は自由に選べるわけであなく、下のような制限があります。

192 <= PLLI2SN <= 432
2 <= PLLI2SR <= 7
0 <= I2SODD <= 1
4 <= (2*I2SDIV + I2SODD) <= 511
I2SCLK <= 192 [MHz]

ここで、PLLI2SN の制限は、実際は PLL VCO の発振周波数制限で、192 MHz から 432 MHz の間に選ぶ必要があり、基準周波数 1 MHz の条件では、PLLI2SN に対して上の表のような制限になるということです。
このような制限のもと、目的のサンプリング周波数から、誤差が最小になる最適なパラメータ値を選び出すプログラムを作成しました。
プログラムは後で示しますが、いくつかのサンプリング周波数について求めた結果を下に示します。

Target_fs  PLLN   PLLR  I2SDIV I2SODD Real_fs [Hz] err [ppm]
--------- ------ ------ ------ ------ ------------ ---------
   22050    429      4      9      1   22049.75329    -11.19
   24000    424      3     11      1   24003.62313    150.96
   32000    213      2      6      1   32001.20192     37.56
   35000    224      5      2      1   35000.00000      0.00
   36000    258      4      3      1   35993.30357   -186.01
   40000    256      5      2      1   40000.00000      0.00
   44100    271      6      2      0   44108.07227    183.04
   45000    288      5      2      1   45000.00000      0.00
   46000    212      3      3      0   46006.94401    150.96
   48000    258      3      3      1   47991.07143   -186.01
   49516    355      7      2      0   49525.66895    195.27
   49716    280      2      5      1   49715.90909     -1.83
   49749    382      5      3      0   49739.58333   -189.28
   50000    192      3      2      1   50000.00000      0.00
   60000    384      5      2      1   60000.00000      0.00
   64000    344      3      3      1   63988.09487   -186.02
   88200    271      3      2      0   88216.14551    183.06
   96000    344      2      3      1   95982.14286   -186.01
  100000    256      2      2      1  100000.00000      0.00

この表は、STM32F4xx のリファレンス・マニュアル「RM0090」のセクション 25.4.4、ページ 694、Table 100. と同様の形式となっていますが、最後の誤差を ppm 単位で表示している部分が異なっています。
35 kHz、40 kHz、45 kHz、50 kHz、60 kHz、100 kHz といったサンプリング周波数については誤差ゼロとなっていますが、ディジタル・オーディオとして標準的な 32 kHz、44.1 kHz、48 kHz といった周波数については、おおむね ±200 ppm 程度の誤差となっています。
民生用 SPDIF のクラス 2 の許容サンプリング周波数偏差は ±1000 ppm ですから、それに比べると誤差は小さく、SPDIF トランスミッタを外付けして、外部 SPDIF DAC に送信する場合でも、外部 DAC でロック可能な範囲と言えます。
上の表で 49 kHz 台の周波数が 3 種ありますが、これは「本物」の FM 音源チップで使われているサンプリング周波数です。
49716 Hz に対して誤差が -1.83 ppm となっていますが、これは、NTSC カラー・サブキャリア周波数の 3.5795454 MHz や 14.31818 MHz の原発振を使う「古典的」な FM 音源チップ (YMF262 など) のサンプリング周波数 49715.90909 Hz を、目的のサンプリング周波数を Hz 単位の整数で表現したために生じた誤差で、実際の周波数は誤差ゼロで実現できています。
49516 Hz は、OPL4 (YMF278B) などの、44.1 kHz 系の原発振 (33.8688 MHz など) を使う FM 音源の場合のサンプリング周波数 49515.789 Hz を Hz 単位に整数化したものです。
49749 Hz は、DS-1 (YMF740F) などの、48 kHz 系の原発振 (24.576 MHz など) を使う FM 音源の場合のサンプリング周波数 49748.988 Hz を Hz 単位に整数化したものです。
44.1 kHz 系、48 kHz 系、ともに誤差の絶対値は 200 ppm 程度で、誤差ゼロのカラー・サブキャリア系に比べて悪くなっています。
最後にプログラムを下に示します。 各パラメータを許容範囲内で変化させ、総当りで誤差を計算し、誤差が最小になる場合のパラメータを表示します。
I2SDIV、I2SODD の計算は、Std_Periph_Driver 中の「I2S_Init()」関数の中で使われている計算式を、そっくりそのまま抜き出して使っています。
Cygwin 上の gcc で書いてありますから、特に環境には依存しないはずです。

//
// i2spll_conf.c : I2S PLL configuration
//                 for STM32F4
//
// 2012/02/20 pcm1723
//

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define I2SPLLR_MIN (2)
#define I2SPLLR_MAX (7)

#define I2SPLLN_MIN (192)
#define I2SPLLN_MAX (432)

#define I2SDIV_MIN (2)
#define I2SDIV_MAX (511)

uint32_t pllm = 8;
uint32_t HSE_VALUE = 8000000UL;

int32_t fs_tab[] = {
  22050,
  24000,
  32000,
  35000,
  36000,
  40000,
  44100,
  45000,
  46000,
  48000,
  49516,
  49716,
  49749,
  50000,
  60000,
  64000,
  88200,
  96000,
  100000
}; // int fs_tab[]

int main(int argc, char* argv[])
{
  int pllr, plln;
  int r_opt, n_opt, d_opt, o_opt;
  int i, fs;
  uint32_t fvco, i2sclk, tmp;
  uint32_t i2sdiv, i2sodd;
  double err, err_min, fs_opt;
  int n = (sizeof(fs_tab) / sizeof(int32_t));
  
  if (1 < argc) { // argument found
    if (argc > n) { // too many arguments
      argc = (n + 1); // limit to table entries
    } // if (argc > n) ...
    for (n = 1; n < argc; n++) {
// override table content by the argument  
      sscanf(argv[n], "%ld", &(fs_tab[n-1])); 
    } // for 
    n--;
  } // if (1 < argc) ...
  printf("Target_fs  PLLN   PLLR  I2SDIV I2SODD Real_fs [Hz] error [ppm]\r\xa");  
  printf("--------- ------ ------ ------ ------ ------------ -----------\r\xa");
  for (i = 0; i < n; i++) {
    fs = fs_tab[i];
    err_min = 9999;
    for (plln = I2SPLLN_MIN; plln <= I2SPLLN_MAX; plln++) {
      for (pllr = I2SPLLR_MAX; pllr >= I2SPLLR_MIN; pllr--) {
        fvco   = plln * (HSE_VALUE / pllm); // VCO output freq
        i2sclk = fvco / pllr; // i2sclk to SPI/I2S module
	if (192000000UL >= i2sclk) { // i2sclk limited to 192 MHz
// following calculations quoted from I2S_Init() function	
/* Compute the Real divider depending on the MCLK output state, with a floating point */
/* MCLK output is enabled */
          tmp = (uint16_t)(((((i2sclk / 256) * 10) / fs)) + 5);
/* Remove the floating point */
          tmp = tmp / 10; 
// calculate fs error
	  err = (((double)i2sclk / 256.0 / tmp / fs) - 1.0); 
/* Check the parity of the divider */
          i2sodd = (uint16_t)(tmp & (uint16_t)0x0001);
/* Compute the i2sdiv prescaler */
          i2sdiv = (uint16_t)((tmp - i2sodd) / 2);
/* Get the Mask for the Odd bit (SPI_I2SPR[8]) register */
          i2sodd = (uint16_t) (i2sodd << 8);
// check if minimum error value found	
          if (1 < i2sdiv) { // i2sdiv[7:0] == 0000000X forbidden
            if (fabs(err) < fabs(err_min)) { // minimum error found
  	      r_opt = pllr;
	      n_opt = plln;
	      d_opt = i2sdiv;
	      o_opt = i2sodd;
	      fs_opt = fs * (1.0 + err);
	      err_min = err;
	    } // if (err < err_min) ...
	  } // if (1 < i2sdiv)
	} // if (192000000UL >= i2sclk) 
      } // for (pllr
    } // for (plln
    printf("%8d %6d %6d %6d %6d %13.5f   %7.2f\r\xa", 
            fs, n_opt, r_opt, d_opt, o_opt>>8, fs_opt, 1e6*err_min);
  } // for (i = 0;
  return (0);
} // int main()

引数なしで実行すると、初期値付き配列の「fs_tab」中に設定されているサンプリング周波数についての表を出力します。
サンプリング周波数を Hz 単位の整数として引数に与えると (複数可)、そのサンプリング周波数についてのパラメータを表示します。
fs_tab
配列を「上書き」する形で実現しているので、fs_tab[] 配列の要素数までの引数を受け入れます。