読者です 読者をやめる 読者になる 読者になる

LPCXpressoとLPC800-MAXでマイコンを学ぶ(その6:タイマーを使う)

今回はBlinkyのサンプルに戻って、残っているタイマーの動作を確認したいと思います。

LPC800シリーズのタイマー

LPC800シリーズにはタイマーが2種類あります。

  • Cortex-M0+内にあるSystick timer, 24bit 1チャンネル
  • LPCシリーズとしてのMRT(Multi-rate Timer), 31bit 4チャンネル

BlinkyではMRTを使ってLEDを点滅させています。サンプルの書き方としては「?」なところもあるのですが、とりあえず見ていきます。

全体の動作

LEDを点滅させている部分は以下の箇所です。

extern uint32_t mrt_counter;

:

  init_mrt(0x8000);

  while (1)  /* Loop forever */
  {
    /* I/O configuration and LED setting pending. */
    if ( (mrt_counter > 0) && (mrt_counter <= 200) )
    {
        GPIOSetBitValue( 0, 7, 0 );
    }
    if ( (mrt_counter > 200) && (mrt_counter <= 400) )
    {
        GPIOSetBitValue( 0, 7, 1 );
    }
    if ( (mrt_counter > 400) && (mrt_counter <= 600) )
    {
        GPIOSetBitValue( 0, 16, 0 );
    }
    if ( (mrt_counter > 600) && (mrt_counter <= 800) )
    {
        GPIOSetBitValue( 0, 16, 1 );
    }
    if ( (mrt_counter > 800) && (mrt_counter <= 1000) )
    {
        GPIOSetBitValue( 0, 17, 0 );
    }
    if ( (mrt_counter > 1000) && (mrt_counter <= 1200) )
    {
        GPIOSetBitValue( 0, 17, 1 );
    }
    else if ( mrt_counter > 1200 )
    {
        mrt_counter = 0;
    }
  }

init_mrt関数でMRTを初期化して、mrt_counterの値をチェックして点滅間隔を調整していることがわかります。この時点でmrt_counterが当たり前のようにextern変数ってどうなのよ?って感じはしますが気にしないことにします(^^;

MRT(Multi-rate Timer)の初期化

init_mrt関数を見ていきましょう。

(lpc8xx_mrt.c)

void init_mrt(uint32_t TimerInterval) 
{
  /* Enable clock to MRT and reset the MRT peripheral */
  LPC_SYSCON->SYSAHBCLKCTRL |= (0x1<<10);
  LPC_SYSCON->PRESETCTRL &= ~(0x1<<7);
  LPC_SYSCON->PRESETCTRL |= (0x1<<7);

  mrt_counter = 0;
  LPC_MRT->Channel[0].INTVAL = TimerInterval;
  LPC_MRT->Channel[0].INTVAL |= 0x1UL<<31;
    
  LPC_MRT->Channel[0].CTRL = MRT_REPEATED_MODE|MRT_INT_ENA;
    
  /* Enable the MRT Interrupt */
  NVIC_EnableIRQ(MRT_IRQn);

  return;
}

まず最初にSYSAHBCLKCTRLレジスタの10bit目に1を書込み、MRTブロックへのクロック供給を行っています。

LPC_SYSCON->SYSAHBCLKCTRL |= (0x1<<10);

次の2行でPeripheral reset control registerでMRTブロックのリセットを行っています。ユーザーズマニュアルp28にあります。

f:id:tomo_watanabe:20131013223521p:plain

f:id:tomo_watanabe:20131013223530p:plain

0を書き込んでから、1を書き込んでブロックにリセットを掛けています。

  LPC_SYSCON->PRESETCTRL &= ~(0x1<<7);
  LPC_SYSCON->PRESETCTRL |= (0x1<<7);

グローバル変数mrt_counterを初期化して、タイマー値を設定しています。ここでMRTは4チャンネルあるはずなのですが、なぜか0チャンネル固定になっています。MRTはユーザーズマニュアルp162から

f:id:tomo_watanabe:20131013224135p:plain

なお、マニュアルにはちゃんと4チャンネル分の記述があります。Time interval registerにタイマーの設定値を書込み、31bit目にタイマーを即時開始するように、LOADビットを書き込んでいます。ユーザーズマニュアルp163

f:id:tomo_watanabe:20131013224423p:plain

  mrt_counter = 0;
  LPC_MRT->Channel[0].INTVAL = TimerInterval;
  LPC_MRT->Channel[0].INTVAL |= 0x1UL<<31;

次にControl registerにリピートモードに設定し、割り込みを有効にしています。

f:id:tomo_watanabe:20131013224718p:plain

  LPC_MRT->Channel[0].CTRL = MRT_REPEATED_MODE|MRT_INT_ENA;

最後にMRTの割り込みを有効にして、タイマー割り込みが入るようにしています。

  /* Enable the MRT Interrupt */
  NVIC_EnableIRQ(MRT_IRQn);

このNVIC_EnableIRQ関数は何をやっているのでしょう。IRT_IRQnを調べてみると

typedef enum IRQn
{
/******  Cortex-M0 Processor Exceptions Numbers ***************************************************/
  Reset_IRQn                    = -15,    /*!< 1 Reset Vector, invoked on Power up and warm reset*/  
  NonMaskableInt_IRQn           = -14,    /*!< 2 Non Maskable Interrupt                           */
  HardFault_IRQn                = -13,    /*!< 3 Cortex-M0 Hard Fault Interrupt                   */
  SVCall_IRQn                   = -5,     /*!< 11 Cortex-M0 SV Call Interrupt                     */
  PendSV_IRQn                   = -2,     /*!< 14 Cortex-M0 Pend SV Interrupt                     */
  SysTick_IRQn                  = -1,     /*!< 15 Cortex-M0 System Tick Interrupt                 */

/******  LPC8xx Specific Interrupt Numbers ********************************************************/
  SPI0_IRQn                     = 0,        /*!< SPI0                                             */
  SPI1_IRQn                     = 1,        /*!< SPI1                                             */
  Reserved0_IRQn                = 2,        /*!< Reserved Interrupt                               */ 
  UART0_IRQn                    = 3,        /*!< USART0                                            */
  UART1_IRQn                    = 4,        /*!< USART1                                            */
  UART2_IRQn                    = 5,        /*!< USART2                                            */
  Reserved1_IRQn                = 6,        /*!< Reserved Interrupt                               */    
  Reserved2_IRQn                = 7,        /*!< Reserved Interrupt                               */
  I2C_IRQn                      = 8,        /*!< I2C                                              */
  SCT_IRQn                      = 9,        /*!< SCT                                              */
  MRT_IRQn                      = 10,       /*!< MRT                                              */ 

割り込み番号として、MRTは10番に割り当てられていることがわかります。さてNVIC_EnableIRQの中は、

__STATIC_INLINE void NVIC_EnableIRQ(IRQn_Type IRQn)
{
  NVIC->ISER[0] = (1 << ((uint32_t)(IRQn) & 0x1F));
}

引数として割り込み番号をNVICのISER[0]に設定をしています。NVICレジスタはユーザーズマニュアルp14、ISERはp15にあります。

f:id:tomo_watanabe:20131013225648p:plain

f:id:tomo_watanabe:20131013225821p:plain

つまり、NVICのInterrupt Set Enable registerに、MRTの割り込み許可を行っています。NVICとは「Nested Vector Interrupt Controller」で要は「割り込みコントローラ」です。ここではそれ以上はいいでしょう。

これで、タイマーを設定し、タイマー値がカウントダウンして0になった時に、MRTの割り込みが動作することになります。

割り込みハンドラ

次に割り込みが起こった時の動作を確認します。割り込みハンドラの実装は

(lpc8xx_mrt.c)

void MRT_IRQHandler(void)
{  
  if ( LPC_MRT->Channel[0].STAT & MRT_STAT_IRQ_FLAG )
  {
    LPC_MRT->Channel[0].STAT = MRT_STAT_IRQ_FLAG;  /* clear interrupt flag */
    mrt_counter++;
  }
  return;
}

タイマー値がカウントダウンされ0になると、この割り込みハンドラが呼ばれます。全体としてはグローバル変数のmrt_counterをインクリメントしているだけです。さて詳細に見て行きましょう。

まずは、MRTの0チャンネルのStatus registerに割り込みフラグが立っているかチェックしています。

f:id:tomo_watanabe:20131013231858p:plain

if ( LPC_MRT->Channel[0].STAT & MRT_STAT_IRQ_FLAG )

割り込みフラグが立っていたら、割り込みフラグをクリアします。INTFLAGに1を書き込むことでクリアになります。クリア後にカウンタを+1して終わりです。

LPC_MRT->Channel[0].STAT = MRT_STAT_IRQ_FLAG;  /* clear interrupt flag */
mrt_counter++;

LEDの点滅動作

MRTの動作がわかりました。init_mrt関数で設定した値がカウンタとして働き、カウントダウンされ、0になるとmrt_counterが+1されていることになります。したがって、サンプルプログラムの動作は0x8000(32768) × 200のタイミングで点滅をするようになっていて、mrt_counterが1200を超えると0にリセットされ、繰り返すことになります。

注意:mrt_counterがグローバルで初期化がinit_mrt内で行われるのに、メインプログラムで0にクリアするとか、サンプルとして良くない例です。こういうプログラムは書かないようにしましょう(^^;

タイミングを変えてみる

それではタイマーのタイミングを変更してみましょう。こんな感じです。

//  init_mrt(0x8000);
  init_mrt(SystemCoreClock / 100);

mrt_counterが200で点滅することになるので、これだと2秒おきに点滅することになります。??よくわかりませんね。解説します。タイマーは結局、クロック周波数に同期して動いています。さてSystemCoreClock = 12MHzでした。これは1秒間の振動数です。つまり120000を設定すると、1秒のタイマーになります。ということは100で割っているので10msのタイマーを設定していることになります。点滅のカウントは200ですので、10ms × 200 = 2s ということになります。実際のタイマーはこのように10msや1msといった単位で作成しますので、このような設定の仕方になります。

次回は、MRTではなくSystickを使ったタイマーと、今回説明できなかったハンドラの補足をしようと思います。