PIC32MX220F032B (3) -- SPI/I2S クロックの検討 (3)

PIC32MX220F032B に接続した (ノンオーバーサンプリング) ディジタル・オーディオ DAC に約 440 Hz の正弦波を出力するプログラムを下に示します。
正弦波は DDA (Digital Differential Analyzer) アルゴリズムで発生させており、左右のチャンネルに 90° 位相の違う正弦波を出力しています。
MPLAB X で PIC32 用のプロジェクトを作成し、自動生成される「main.c」の内容を下のプログラム・リストの内容で置き換えてください。

/********************************************************/
/*  Files to Include                                    */
/********************************************************/
#include <plib.h>      /* use PIC32 peripheral lib      */
#include <stdint.h>    /* For uint32_t definition       */
#include <stdbool.h>   /* For true/false definition     */
//#include "system.h"  
//#include "user.h"    

// Work around
#if !defined(SPI_CONFIG_MCLKSEL)
  #define    SPI_CONFIG_MCLKSEL SPI_OPEN_MCLKSEL
#endif

int __attribute__((weak)) SpiChnTxBuffCount(SpiChannel chn)
{
  return((1 == chn) ? SPI1STATbits.TXBUFELM : SPI2STATbits.TXBUFELM);
}
// end of work around

// system clock frequency in Hz
#define SYS_FREQ 42336000UL // 42.336 MHz
// Configuration bits
// SYSCLK = (PRICLK=12.096[MHz]) / 3 * 21 / 2 = 42.336[MHz]
#pragma config FNOSC=PRIPLL,   FPLLIDIV=DIV_3
#pragma config FPLLMUL=MUL_21, FPLLODIV=DIV_2
// PBCLK = SYSCLK / 1 = 42.336 [MHz]
#pragma config FPBDIV=DIV_1
//             HS xtal,    Cock Sw En, FSCM En
#pragma config POSCMOD=HS, FCKSM=CSECME
// USBPLL = (PRICLK=12.096 [MHz]) / 3 * 24 = 96.768 [MHz]
#pragma config UPLLEN=ON, UPLLIDIV=DIV_3
#pragma config FWDTEN=OFF  // watchdog timer off
#pragma config FSOSCEN=OFF // secondary osc off
/********************************************************/
/* Global Variable Declaration                          */
/********************************************************/
const int fs_ind = 0; // 0 for 48 kHz, 1 for 44.1 kHz
const uint32_t fs_tab[2] = {48000UL, 44100UL};

// 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;

// use SPI2 for DAC output
const uint32_t DAC_SPI = SPI_CHANNEL2;

const uint32_t SPI_CONFIG_params =
             ( SPI_CONFIG_MSTEN    // master mode
             | SPI_CONFIG_FSP_HIGH // LRCK(SS) = 1 for L ch
             | SPI_CONFIG_ENHBUF   // use FIFO
             | SPI_CONFIG_DISSDI   // DISable SDI (and Rx)
             | SPI_CONFIG_CKP_HIGH // clock = _-_-_-_-
             );

const uint32_t SPI_CONFIG2_params =   
             ( SPI_CONFIG2_AUDEN     // AUDio mode ENable
             | SPI_CONFIG2_AUDMOD_RJ // right justified
             | SPI_CONFIG2_IGNTUR    // IGnore Tx UnderRun
             );

void set_dac_fs(uint32_t fs)
{
    SpiChnEnable(DAC_SPI, 0); // at first diable SPI
    if (48000UL == fs) { // fs = 48 kHz
// use REFCLK as SPI clock (MCLKSEL = 1)
// fs = 12.096 [MHz] / 3 * 24 / 31.5 / 2 / 32 = 48 [kHz]
      SpiChnConfigureEx( DAC_SPI,
                         ( SPI_CONFIG_params 
                         | SPI_CONFIG_MCLKSEL
                         ),
                         SPI_CONFIG2_params);
      SpiChnSetBrg(DAC_SPI, 0); // Baud rate divi = 1/2
    } else { // fs = 44.1 kHz
// use PBCLK as SPI clock (MCLKSEL = 0)
// fs = 12.096 [MHz] / 3 * 21 / 2 / 30 / 32 = 44.1 [kHz]
      SpiChnConfigureEx( DAC_SPI,
                         SPI_CONFIG_params,
                         SPI_CONFIG2_params);
      SpiChnSetBrg(DAC_SPI, 14); // Baud rate divr = 1/30
    } // if (48000UL == fs) { ...
    SpiChnEnable(DAC_SPI, 1); // now, enable SPI
} // void set_dac_fs()

/********************************************************/
/* Main Program                                         */
/********************************************************/
int32_t main(void)
{
  int32_t sin_acc = 0;
  int32_t cos_acc = 0x7f80; // initial radius

  DDPCONbits.JTAGEN = 0; // diable JTAG pins
  SYSTEMConfigPerformance(SYS_FREQ);
// REFCLK = (PRICLK=12.096 [MHz])/3*24/31.5 = 3.072 [MHz]
  OSCREFConfig( OSC_REFOCON_USBPLL,
               (OSC_REFOCON_OE | OSC_REFOCON_ON), 15);
  mOSCREFOTRIMSet(384); // trim = 384/512 = 0.75
// pin configurations
// to monitor REFCLK on PA4 (pin 12)
  mSysUnlockOpLock( PPSOutput(3, RPA4, REFCLKO) );
// DAC_BCLK = SCK2 = RB15 (pin 26, not assignable)
// DAC_LRCK = SS2  = RB9  (pin 18)
// DAC_SDAT = SDO2 = RB8  (pin 17)
  mSysUnlockOpLock( (PPSOutput(4, RPB9, SS2),
                     PPSOutput(2, RPB8, SDO2)) );
  INTConfigureSystem(INT_SYSTEM_CONFIG_MULT_VECTOR);
  ANSELA = 0;
  ANSELB = 0;
// setup SPI for the fs (48 kHz / 44.1 kHz)
  set_dac_fs(fs_tab[fs_ind]);
  while(1) {
    if (2 > SpiChnTxBuffCount(DAC_SPI)) {
// FIFO has room to store stereo sample
      SpiChnWriteC(DAC_SPI, cos_acc);
      SpiChnWriteC(DAC_SPI, sin_acc);
// update sin/cos value using DDA calculation
      cos_acc -= ((sin_acc * acc_mul[fs_ind]) >> acc_shift);
      sin_acc += ((cos_acc * acc_mul[fs_ind]) >> acc_shift);
    } // if
  } // while
} // int32_t main()

「Global Variable Declaration」というコメントの下の「fs_ind」を「0」に設定すると 48 kHz サンプリング、「1」に設定すると 44.1 kHz サンプリングの設定になります。
「PIC32UBL.exe」を使った書き込みでは、コンフィギュレーション・ワードを特別扱いせず、また、後から書き換えることもできないので、ソース・プログラム中に「#pragma config」ディレクティブを使って必要な分のコンフィギュレーションは全て記述しておく必要があります。
MPLAB X の PIC32 用プロジェクト作成では、コンフィグを記述する目的に「configuration_bits.c」というソース・ファイルが自動生成されますが、それは無視し、「main.c」の方にコンフィグを記述しています。
Microchip 提供の周辺ライブラリを使う形に書いてありますが、一部の記述が足りない部分があるようで、「Work around」と記してある部分で対応しています。
SPI/I2S モジュールの初期化には SpiChnOpenEx() 関数あるいは SpiChnConfigureEx() 関数を使うことができますが、SpiChnOpenEx() 関数では指定しなくても自動的に ON ビットを立てる動作が余計なので、指定された設定のみを行う SpiChnConfigureEx() 関数を使っています。
この設定のフラグのひとつに MCLKSEL ビットの設定を示す「SPI_CONFIG_MCLKSEL」があるのですが、これに対応する「SPI_OPEN_MCLKSEL」は定義されているのに、なぜか「SPI_CONFIG_MCLKSEL」の定義は抜けています。
シンボルに実際に定義されるビット・パターンは SPI_OPEN〜 でも SPI_CONFIG〜 でも同じなので、

#define SPI_CONFIG_MCLKSEL SPI_OPEN_MCLKSEL

として補っています。
また、SPI の送信 FIFO の中にあるデータの数を返す SpiChnTxBuffCount() という関数があるのですが、ヘッダ・ファイルでは定義されているのにオブジェクトのリンクの過程で、「そんな関数はない」と言ってきます。
ライブラリのソース (spi_chn_tx_buff_count_lib.c) を見てみると、関数本体の定義が

#ifdef _SPI_REV_5XX_
#endif

という条件コンパイルのディレクティブに囲まれていますが、実際にはエンハンスド・バッファ (FIFO バッファ) モードがないのは PIC32MX3XX/4XX シリーズなので、 #ifdef の部分は

#if defined(_SPI_REV_1XX_) | defined(_SPI_REV_5XX_)

または

#ifndef _SPI_REV_3XX_

となるべきだと思います。
また、「PIC32 Family Reference Manual」の「Section 23.Serial Peripheral Interface(SPI)」の「23.4.5.2 LEFT-JUSTIFIED MODE」および「23.4.5.3 RIGHT-JUSTIFIED MODE」の説明で、CKP = 0 として、ビット・クロック (SCK) の立ち上がりでシフト、立ち下りでサンプルするタイミング図が掲載されていますが、これは誤りだと思います。
これまでにマイコンに接続して使ったことのある

  • ROHM BU9480F
  • NEC μPD6376
  • TI/BB PCM1716
  • AKM AK4321

のいずれも、ビット・クロックの立ち下りでシフト、立ち上がりでサンプルというタイミングになっていました。
このタイミングで動作させるには、「23.4.5.1 I2S MODE」の項と同様に、CKP = 1 とする必要があります。
オーディオ・サンプルの L / R の同期を取るのに、送信アンダーランを無視するモードを使っています。
CONFIG2 のパラメタとして、SPI_CONFIG2_IGNTUR フラグを指定します。 (「IGNTUR」は「IGNore Transmit Under-Run」の意)
このモードでは、送信 FIFO が空になって送信すべきデータがない場合 (アンダーラン状態) には、代わりに「ゼロ」を送信します。
送信 FIFO にデータが書き込まれるとアンダーラン状態を抜けることになりますが、そのチェックは新しいオーディオ・フレームの開始のタイミング、つまり、次の L チャンネル・データを送信するタイミングまで引き伸ばされます。
アンダーラン状態でない場合には、L チャンネル送信タイミング、R チャンネル送信タイミングにそれぞれ 1 ワードずつ送信 FIFO からデータが読み込まれますが、いったんアンダーラン状態になると、以降のオーディオ・フレームの R チャンネル送信タイミングでの送信 FIFO 読み出しをしなくなります。
したがって、確実にアンダーランになっている状態で、すばやく L ch / R ch の順に送信データを送信 FIFO に書き込むと、そのチェックは次のオーディオ・フレームの頭の L ch 出力タイミングで行われるので、L ch データ / R ch データの順で送信されることが保証されます。
ここで「すばやく」というのは、L / R の書き込みタイミングの間隔がオーディオ・フレームの 1/2 以上にならないことという条件です。
書き込み間隔がそれ以上になると、L ch データが消費されてアンダーラン状態を抜けた後に R ch データが書かれないまま再びアンダーラン状態となる可能性があります。 そうすると L / R が逆転してしまいます。