LPC1114FN28/102 (5)

LPC1114FN28 の内蔵タイマを利用した PWM DAC および外付けディジタル・オーディオ DAC でオーディオ信号を発生させるプログラムを作りました。
プログラムのソースは記事の最後で示します。
外付けディジタル・オーディオ DAC は、いつもの ROHM BU9480F です。 (サンプリング周波数は 48 kHz)
LPC11xx シリーズは、内蔵モジュールの数と機能をぎりぎりまでカットしてローコスト化を図っているので、ARM7TDMI コアの LPC23xx シリーズや Cortex-M3 コアの LPC17xx シリーズには内蔵されている I2S モジュールがありません。
そのため、SSP (SPI) とタイマの組み合わせでシリアル DAC とインターフェースしているので、BU9480F 以外では出力信号にジッタを生じる可能性があります。
プログラムを簡単にするため、DDA (Digital Differential Analyzer) アルゴリズムを利用して、周波数が約 440 Hz で位相が 90° 異なる正弦波を 2 波発生させ、それぞれを L ch と R ch とに出力しています。
DAC と接続する回路図を下に示します。

簡単のため、ISP (ブートローダ) によるフラッシュ書き込みのためのシリアルの接続や、リセットスイッチの配線などは省略してあります。
16 ビット・ディジタル・オーディオ DAC の BU9480F 出力を「WaveSpectra」で観測した結果を下に示します。

2 次高調波のレベルは -80 dB 程度で、全高調波歪み率としては 0.01 % 程度になっています。
PWM 出力に 3 次バタワース特性 LC フィルタ (カットオフ周波数約 20 kHz) をかけた結果を下に示します。

2 次高調波のレベルは -40 dB 程度で、全高調波歪み率としては 1 % 程度になっています。
PWM 出力の分解能は 10 ビット弱ですが、正弦波のデータ自体はディジタル・オーディオ DAC に出力するものと同じです。
歪み率が下がらないのは、

  • Atmel AVR では「Fast PWM」
  • STMicro STM32F では「Edge-aligned PWM」
  • NXP LPC23xx/LPC17xx(PWM)、LPC11xx では「Single edge PWM」

と呼ばれている、PWM 波形の立ち上がり/立ち下りエッジのうち一方のエッジの位相は固定で、もう一方のエッジの位相が変化することでパルス幅を変えている方式の PWM だからです。
この方式では、各 PWM 周期内の「H」レベルの部分の「重心」位置がパルス幅により変位するので、サンプリング・タイミングが変動することと等価となり、振幅方向の「歪み」となって現れます。

  • Atmel AVR では「Phase correct PWM」
  • STMicro STM32 では「Center-aligned PWM」
  • NXP LPC23xx/LPC17xx(PWM) では「Double edge PWM」

と呼ばれている、PWM 波形の立ち上がり/立ち下り両エッジの位相を変化させ重心位置の位相を一定に保つ方式の PWM では歪みを少なくすることができます。
しかし、LPC11xx のタイマ (TMR16B/TMR32B) では、LPC23xx/LPC17xx の PWM モジュールとは違って「Double edge PWM」のモードは削除されており、Single edge のモードしかありません。
また、マッチ・レジスタのシャドウ・レジスタは省略されており、マッチ・レジスタへの書き込みは直ちに影響を与えます。
そのため、マッチ・レジスタを更新する時点でのタイマ・カウンタ値よりも大きな値の比較値に対しては、その PWM 周期内でマッチが成立しますが、更新する時点でのタイマ・カウンタ値よりも小さな比較値では、その PWM 周期内ではマッチが成立せず、不正な PWM 値となり、出力波形に「グリッチ」(glitch) が生じます。
ここでは簡単のため、出力データの振幅を小さくした上で「ゲタ」をはかせることにより、比較値が一定値を下回らないようにしています。
具体的には、16 ビット 2 の補数のオーディオ・データ -32768 〜 32767 の範囲を PWM 値として本来は 0 〜 999 の範囲にマッピングするのを、ゲタをはかせて 92 〜 999 の範囲にマッピングしています。
最後にソース・リストを示します。
(2012/10/05 追記: グローバル変数dac_update_flag」、「dac_buf[]」、「pwm_buf[]」の宣言に「volatile」を追加しました。)
このプログラムは Keil/ARM MDK-Lite uVision4 上で作成したものです。
IAR、LPCXpresso などの他の環境では試していません。
トランジスタ技術 2012 年 10 月号付属の DVD-ROM には Keil/ARM MDK-Lite uVision V4.53 が収録されていますが、それはインストールせず、以前購入した Nuvoton NUC120-SDK に付属してきて既にインストールずみの V4.20 を使用しています。
もちろん、そのままでは新しいチップである LPC1114 には対応していませんが、トラ技付属 DVD-ROM に収録されている「MDK-ARM_NXP_LPCxxxx.exe」の実行でデバイス・データベースが追加され、正常に使えるようになりました。
トラ技付属 DVD-ROM に収録されているサンプルプログラム集「MyARM_sample.exe」を実行し、解凍されてできた「MDK_sample」フォルダ内の「main.c」の内容を下のプログラムに置き換え、「Project.uvproj」をダブルクリックして uVision4 を起動してビルドできます。
オブジェクトの HEX ファイル名は「Project.hex」になります。
「MDK_sample」では、内蔵 RC オシレータ (IRC) を使うように「system_LPC11xx.c」中で

//#define SYSPLLCLKSEL_Val    0x00000001  // Reset: 0x000
#define SYSPLLCLKSEL_Val    0x00000000 // Reset: 0x000

と定義してあるので、12 MHz の水晶振動子を付けなくても動作はします。
外部に付けた水晶振動子を生かすには、上の部分のコメント部分と生きている部分とを逆にします。 これは、結局、「system_LPC11xx.c」に手を入れて IRC を使うようにしているのを、元に戻すことになります。

/*******************************************************/
/* DACtest : External digital audio DAC (ROHM BU9480F) */
/*           (stereo 16-bit right justified format)    */
/*           and PWM DAC using internal Timer/Counter  */
/*           test for LPC1114FN28/102                  */
/*           with Keil/ARM MDK-lite uVision4 IDE       */
/*                                                     */
/* 2012/10/02 created by pcm1723                       */
/*******************************************************/
#include "lpc11xx.h"

#define IOCON_fields(func,mode,hys,admode) \
    ( ( 0x07 & func        ) \
    | ((0x03 & mode)   << 3) \
    | ((0x01 & hys)    << 5) \
    | (    1           << 6) \
    | ((0x01 & admode) << 7) \
    )

#define IOCON_I2Cfld(func,i2cmode) \
    ( ( 0x07 & func         ) \
    | (    1            << 7) \
    | ((0x03 & i2cmode) << 8) \
    )

#define DAC_fs 48000UL
#define CPUCLK_Hz 48000000UL
#define PCLK_Hz CPUCLK_Hz
#define LRCK_divisor (PCLK_Hz / (2*DAC_fs))

// for LRCK generation

#define LRCK_IRQHandler TIMER16_1_IRQHandler
#define LRCK_IRQn       TIMER_16_1_IRQn
#define LRCK_TMR        LPC_TMR16B1
#define LRCK_MRn        3
#define LRCK_MR         MR3
#define LRCK_MATn       0
#define LRCK_MAT        MR0
#define LRCK_IR_bitmask (1 << LRCK_MRn)
#define LRCK_EM_bitmask (1 << LRCK_MATn)

// for PWM DAC

#define PWM_divisor    (2*LRCK_divisor)
#define PWM_tweak      46
#define PWM_mul        (LRCK_divisor-PWM_tweak)
#define PWM_shift      15
#define PWM_ofs        (LRCK_divisor+PWM_tweak)
#define PWM_IRQHandler TIMER32_1_IRQHandler
#define PWM_IRQn       TIMER_32_1_IRQn
#define PWM_TMR        LPC_TMR32B1
#define PWM_MRn        2
#define PWM_MR         MR2
#define PWM_MATLn      1
#define PWM_MATRn      3
#define PWM_MATL       MR1
#define PWM_MATR       MR3
#define PWM_IR_bitmask (1 << PWM_MRn)

volatile int dac_update_flag = 0;
volatile int32_t dac_buf[2] = {0, 0};
volatile int32_t pwm_buf[2] = {0, 0};

void LRCK_IRQHandler ( void )
{
  if (LRCK_IR_bitmask & LRCK_TMR->IR) { // LRCK edge occurred,
    LRCK_TMR->IR |= LRCK_IR_bitmask;    // clear match interrupt
    if (LRCK_EM_bitmask & LRCK_TMR->EMR) { // Lch out timing (LRCK = EM0 == 1)
      PWM_TMR->PWM_MATL= pwm_buf[0]; // PWM DAC L ch
      PWM_TMR->PWM_MATR= pwm_buf[1]; // PWM DAC R ch
      LPC_SSP0->DR = dac_buf[0]; // output L ch for serial DAC
    } else {                     // Rch out timing (LRCK = EM0 == 0)
      LPC_SSP0->DR = dac_buf[1]; // output R ch for serial DAC
      dac_update_flag = 1; // ready for next audio sample
    } // if (LRCK_EM_bitmask & ...
  } // if (LRCK_IR_bitmask & ...
} // void LRCK_IRQHandler()

void LPC11xx_IOCON_setup( void )
{
  register LPC_IOCON_TypeDef *p = LPC_IOCON;

//                           F  M  H  A
//                           U  O  Y  /
//                           N  D  S  D
//                           C  E
  p->PIO0_6   = IOCON_fields(2, 0, 0, 1);  // Pin 6: SCK0  for DAC_SCLK
  p->PIO0_8   = IOCON_fields(1, 3, 1, 1);  // Pin 1: MISO0
  p->PIO0_9   = IOCON_fields(1, 3, 0, 1);  // Pin 2: MOSI0 for DAC_SDAT

  p->R_PIO1_2 = IOCON_fields(3, 0, 0, 1);  // Pin11: CT32B1_MAT1 for PWM_Lch
  p->PIO1_4   = IOCON_fields(2, 0, 0, 1);  // Pin13: CT32B1_MAT3 for PWM_Rch
  p->PIO1_6   = IOCON_fields(1, 2, 1, 1);  // Pin16: RXD (pull-up)
  p->PIO1_7   = IOCON_fields(1, 0, 0, 1);  // Pin15: TXD
  p->PIO1_9   = IOCON_fields(1, 0, 0, 1);  // Pin18: CT16B1_MAT0 for DAC_LRCK

  p->SCK_LOC  = 2; // select SCK0 at PIO0_6/SCK0 (Pin 6)
} // void LPC11xx_IOCON_setup()

void LPC11xx_SYSCON_setup( void )
{
  register LPC_SYSCON_TypeDef *p = LPC_SYSCON;

// at first, enable AHB clock for IOCON module
  p->SYSAHBCLKCTRL |= (1 << 16); // IOCON
// configure I/O pins for modules
  LPC11xx_IOCON_setup();
// enable AHB clock for modules
  p->SYSAHBCLKCTRL |= (  0
//                    | (1 <<  5) // I2C
//                    | (1 <<  7) // CT16B0
                      | (1 <<  8) // CT16B1
//                    | (1 <<  9) // CT32B0
                      | (1 << 10) // CT32B1
                      | (1 << 11) // SSP0
                      | (1 << 12) // UART
//                    | (1 << 13) // ADC
//                    | (1 << 15) // WDT
                      );
// de-assert RESET for modules
  p->PRESETCTRL |= (1 << 0); // SSP0                           ;
} // void LPC11xx_SYSCON_setup()

void LPC11xx_GPIO_setup( void )
{
} // void LPC11xx_GPIO_setup()

void LPC11xx_Timer_setup( void )
{
  register LPC_TMR_TypeDef *p = LRCK_TMR;

// LRCK timer setup

  p->TCR  = (1 << 1);  // reset timer
  p->TCR  =  0;        // disable timer
  p->PR   =  0;        // clock prescale = 1/1
  p->CTCR =  0;        // count at PCLK rising edge
  p->LRCK_MR  = LRCK_divisor-1; // 48 [MHz] / 500 =  96 [kHz]
  p->LRCK_MAT = 1;     // match at (TC == 1)
  p->EMR  = (3 << (4 + 2*LRCK_MATn)); // MAT0 toggle on match0
  p->MCR  = (3 << (3 * LRCK_MRn));  // reset/interrupt at match3

// PWM timer setup

  p = PWM_TMR; // PWM timer register block pointer

  p->TCR  = (1 << 1);  // reset timer
  p->TCR  =  0;        // disable timer
  p->PR   =  0;        // clock prescale = 1/1
  p->CTCR =  0;        // count at PCLK rising edge
  p->PWM_MR   = (PWM_divisor - 1); // 48 [MHz] / 1000 =  48 [kHz]
  p->PWM_MATL = (PWM_divisor >> 1); // initial duty for L ch 
  p->PWM_MATR = (PWM_divisor >> 1); // initial duty for R ch
  p->PWMC = ( (1 << PWM_MATLn) // set PWM mode for L ch (MR1)
            | (1 << PWM_MATRn) // set PWM mode for R ch (MR3)
            );
  p->MCR  = (2 << (3 * PWM_MRn));  // reset at match2

  LRCK_TMR->TCR |= (1 << 0); // start LRCK timer
  PWM_TMR->TCR  |= (1 << 0); // start PWM  timer
} // void LPC11xx_Timer_setup()

void LPC11xx_NVIC_setup( void )
{
// enable timer interrupt for LRCK generation
  NVIC_EnableIRQ(LRCK_IRQn);
} // void LPC11xx_NVIC_setup()

void LPC11xx_SSP0_setup( void )
{
  register LPC_SSP_TypeDef *p = LPC_SSP0;
   
  LPC_SYSCON->SSP0CLKDIV = 1;  // SSP0 clock = CCLK/1
// set control registers
  p->CR0 = (       0
           |   (16-1)       // 16 bit xfer
           | (     0  << 4) // SPI mode
           | (     0  << 6) // CPOL = 0
           | (     0  << 7) // CPHA = 0
           | (  (8-1) << 8) // SCK0 = 48 [MHz] / (2 * 8) = 3 [MHz]
           );
  p->CR1  = 0;  // no loopback, SPI disabled, MASTER mode 
  p->CPSR = 2;  // clock prescaler = 1/2 
  p->CR1 = (1 << 1);  // start SSP0 
} // void LPC11xx_SSP0_setup()

void LPC11xx_periph_setup( void )
{
  LPC11xx_SYSCON_setup();
  LPC11xx_GPIO_setup();
  LPC11xx_Timer_setup();
  LPC11xx_SSP0_setup();
  LPC11xx_NVIC_setup();
} // void LPC11xx_periph_setup()

int main( void )
{
  int i;
  int32_t sin_acc = 0;
  int32_t cos_acc = 0x7f80; // initial radius
// sin_cos_freq = 48 [kHz] * 59 / (2 * pi * 2^10) = 440.2 [Hz]
  const int acc_mul   = 59;
  const int acc_shift = 10;

  LPC11xx_periph_setup();
  for (;;) { // infinite loop
    if (dac_update_flag) {
      dac_update_flag = 0;
// sin/cos generation by DDA (Digital Differential Analyzer)
      cos_acc -= ((sin_acc * acc_mul) >> acc_shift);
      sin_acc += ((cos_acc * acc_mul) >> acc_shift);
// new audio sample value
      dac_buf[0] = sin_acc;
      dac_buf[1] = cos_acc;
      for (i = 0; i < 2; i++) { // calculate PWM duty value
        pwm_buf[i]= PWM_ofs + (int16_t)((dac_buf[i] * PWM_mul) >> PWM_shift);
      } // for (i = 0; ...
    } // if (dac_update_flag) { ...
  } // for (;;) { ...
} // void main()