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

前々回はMRT(Multi-rate Timer)を使いましたが、今回はSystickタイマーのサンプルを例に動かしてみます。SystickタイマーはCortex-M0+内にあるタイマーで、1チャンネルしかありません。通常このタイマーはOSのタイマーに使われるという記述がどっかにありました。

Systickサンプルプログラム

Blinkyのサンプルをインポートした時と同様に、今度はSystickサンプルをインポートします。メインプログラムはsysticktest.cとなっています。まずはこれを動作させると、赤色のLEDが点滅するように動きます。早速全体を見てみましょう。

今までソースを見ていれば、大体想像は付きますね。LEDの点滅のところはGPIOInit関数でGPIOの初期化を行い、7pinを出力に切り替えて、whileループの中で出力をON/OFFしています。Systickについては、Systick_Config関数で初期化を行い、delaySysTick関数でSystickタイマーを使用しています。

(systicktest.c)

#define SYSTICK_DELAY       (SystemCoreClock/100)

:

int main (void) 
{
  SystemCoreClockUpdate();

  /* Called for system library in core_cmx.h(x=0 or 3). */
  SysTick_Config( SYSTICK_DELAY );
  
  GPIOInit();
  
  /* Set port 0_7 to output */
  GPIOSetDir( 0, 7, 1 );

  while (1)  /* Loop forever */
  {
    delaySysTick(10);
    GPIOSetBitValue( 0, 7, 0 );
    delaySysTick(10);
    GPIOSetBitValue( 0, 7, 1 );
  }
}

Systickタイマー

Systickタイマーの初期化

Systickタイマーの初期化はSystick_Config関数で行っています。引数はSYSTICK_DELAY = SystemCoreClock/100と前々回出てきたように、10msのタイマーを作成していると予想されます(というかそうなっています)Systick_Configの関数を見てみます。

(core_cm0plus.h)

__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk)  return (1);      /* Reload value impossible */

  SysTick->LOAD  = ticks - 1;                                  /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Systick Interrupt */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk;      /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */
}

最初のマスクを一旦飛ばして、レジスタへの代入を見てみます。微妙に名前が違いますがここに出てくる対応は以下です。 ユーザーズマニュアルp180

  • Systick->LOAD : SYST_RVR
  • Systick->VAL : SYST_CVR
  • Systick->CTRL : SYST_CSR

f:id:tomo_watanabe:20131014234129p:plain

SYST_RVRレジスタにタイマーの設定値をセットします(あれ?なんで-1してんだろ?)SYST_CVRレジスタに値を書き込むことでSystickタイマのクリアが行われます。

f:id:tomo_watanabe:20131014235116p:plain

SYST_CSRにはCLKSOURCE, TICKINT, ENABLEに1を書き込むことで、クロックソースをシステムクロック、割り込み有効、タイマーを有効としています。

f:id:tomo_watanabe:20131014235452p:plain

NVIC_SetPriority関数は、このSystickの割り込みの優先度を設定しているようですが、ここを深堀りするとCortex-M0+の内容まで追うことになるので置いておきます。この辺のコードはARMが提供しているので、普通はいじる必要性がないものと思われます(もちろんカリカリチューニングするなら必要かも)

割り込みハンドラ

GPIOInit関数とGPIOの操作については、やってることはだいたいわかると思うので割愛します。Systickの初期設定ができたので、今度はタイマーの割り込み処理を見ると、TimeTickをカウントアップしてるだけでした。これはMRTと同じですね。違うのは割り込み判断のレジスタ操作をしていない(1チャンネルしかないので必要ない)ことです。これで、Systickタイマーがこのサンプルの場合、10msで割り込みが入りTimeTickがカウントアップされることがわかりました。

/* SysTick interrupt happens every 10 ms */
void SysTick_Handler(void)
{
  TimeTick++;
}

ウェイト関数

残る関数はdelaySysTick関数です。文字通りdelay(ウェイト)を行う関数です。処理の前後はSystickのタイマーの有効と無効を行っています。

void delaySysTick(uint32_t tick)
{
  uint32_t timetick;

  /* Clear SysTick Counter */
  SysTick->VAL = 0;
  /* Enable the SysTick Counter */
  SysTick->CTRL |= (0x1<<0);

  timetick = TimeTick;
  while ((TimeTick - timetick) < tick);
  
  /* Disable SysTick Counter */
  SysTick->CTRL &= ~(0x1<<0);
  /* Clear SysTick Counter */
  SysTick->VAL = 0;
  return;
}

ウェイトの部分は、timetickとTimeTickの差分だけwhileを回しています。10msの単位でTimeTickがカウントアップされるので設定された時間で抜けるという処理です(・・・これだとCPUブロックするけど、いいのかな本当に(^^; まぁSleepじゃなくてWaitだからヨシとしましょう)

weakシンボル

これでひと通りの動作がわかりました。もう一つweakシンボルという概念を知っておかないと、ちゃんとハンドラの設計ができません。weakシンボルはweakシンボルで解説されています。C++でのオーバーロードに近い機能で、weakと宣言された関数は、デフォルト動作を記述でき、もし同名の関数があった場合はそちらが優先されてリンクされるようです。このサンプルの場合「void SysTick_Handler(void)」がそれにあたります。デフォルトがどこになるかと言うと

(cr_startup_lpc8xx.c)

#define WEAK __attribute__ ((weak))
:
WEAK void SysTick_Handler(void);
:
__attribute__ ((section(".after_vectors")))
void SysTick_Handler(void)
{
    while(1)
    {
    }
}

このように宣言され、デフォルトでは永久ループに入るようになっています。試しにサンプルの方のSystick_Handler関数をコメントアウトしてビルドしてみてください。ビルドが通ります(動かすと永久ループに入ってデッドロックします)

今回は以上で、次はUARTをやります。やっと実践的な感じになってきました。しかし、どのサンプルも微妙な実装だなぁ・・・(;´Д`)

LPCXpressoとLPC800-MAXでマイコンを学ぶ(番外編:提供されているコードについて)

LPCXpressoを触っていて、気になったことがあるので書いておきます。LPC812にはARMのCortex-M0+のコアとNXPの周辺モジュールの2つが合わさったマイコンです。ブロック図はユーザーズマニュアルp7にあります。

f:id:tomo_watanabe:20131013235759p:plain

サンプルプログラムを見てて思ったのは「あれっ?これ品質的に全然ダメなんじゃね・・?」という箇所がパラパラと見かけられたことです。例えば前エントリのMRTは0チャンネルしか実装が無く、引数にチャンネル番号増やすだけでいいはずなのになんでだろう?と思って見ていったら、MRTのコードの先頭に以下の記述を発見!

/****************************************************************************
 *   $Id:: lpc8xx_mrt.c 5543 2012-10-31 02:19:19Z usb00423                     $
 *   Project: NXP LPC8xx multi-rate timer(MRT) example
 *
 *   Description:
 *     This file contains MRT timer code example which include timer 
 *     initialization, timer interrupt handler, and related APIs for 
 *     timer setup.
 *
 ****************************************************************************

"exmaple"・・・えっ!?仮にも公式で"lpc_driver_lib"っていうプロジェクトファイルなのに?ちょっとマテ、他のコードもか?と思って調べてみました。exampleという記述があるドライバは以下の通り(exmapleとはいえ、そこそこ品質のいいのもあるかも。調べてないけど)

  • MRT(lpc8xx_mrt.c)
  • Clock Configuration(lpc8xx_clkconfig.c)
  • CMP(lpc8xx_comp.c)
  • CRC(lpc8xx_crc.c)
  • GPIO(lpc8xx_gpio.c)
  • NMI(lpc8xx_nmi.c)
  • PMU(lpc8xx_pmu.c)
  • WDT(lpc8xx_wdt.c)
  • WKT(lpc8xx_wkt.c)

正式にサポートされているコードは

  • I2C(lpc8xx_i2c.c)
  • UART(lpc8xx_uart.c)
  • SPI(lpc8xx_spi.c)

ARMコアのCortex-M0+側のコードについては正式にサポートされているようです。というわけでLPC側のコードのほとんどはサンプルドライバということになります。まぁ確かにドライバはそれぞれの事情に合わせて作りこむ必要がある部分ではありますが、もう少し頑張って欲しい感じがします...(;´Д`) まぁレジスタマクロがあるだけマシなのかも...

これは、誰かがライブラリを作成して公開するなどしていかないと、なかなか流行らないかもしれませんね...ただでさえ、組み込みエンジニアは外に出てこないのでノウハウの共有ができないです(´ε`;)ウーン…

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を使ったタイマーと、今回説明できなかったハンドラの補足をしようと思います。

LPCXpressoとLPC800-MAXでマイコンを学ぶ(その5:PLLを使用してクロックアップ!)

LPC812は最大動作クロックは30MHzなので、今回はデフォルトの12MHzを30MHzで動作するように設定を変更してみます。

動作クロックの変更を行うには

前回のエントリのクロック・ジェネレータを参照しながら考えてみます。

f:id:tomo_watanabe:20131012211624p:plain

目的は「system clock」を30MHzにすることです。さて入力クロックの種類ですが、

  • IRC oscillator
  • XTALIN, XTALOUT
  • CLKIN
  • watchdog oscillator

が選択できるようですがクロックジェネレータには制約があります。

f:id:tomo_watanabe:20131012230344p:plain

watchdog oscillatorは入力周波数が低すぎて使えません。CLKINは回路図上でクロックがそもそも接続されていません。XTALIN, XTALOUTですが回路図で見ると...

f:id:tomo_watanabe:20131012230654p:plain

実装はされているのですが、ジャンパーで実は接続されていません。したがって使えるのはIRC osciilator(内部発振器)だけとなります。さて12MHzの内部発振器で30MHzを作るにはどうすればよいでしょう。答えはPLL回路で逓倍・分周することで作ることができます。このあたりもユーザーズマニュアルp50に記述があります。

f:id:tomo_watanabe:20131012231528p:plain

今回はPLLを使ってSystem clock=30MHzを作りたいので、Main clock = 60MHzを作れということです。その場合

  • M divider value : 5
  • P divider value : 2

というのがあります。これがPLL回路の設定値となります。この表の少し上に色々と記述があり、PLL回路の制約条件が書いてあります。

f:id:tomo_watanabe:20131012234850p:plain

整理するとこういうことになります。

f:id:tomo_watanabe:20131012234428p:plain

今、PLL入力 = 12MHz、Main clock = 60MHz、System clock = 30MHzなので、SYSAHBCLKDIV = 2となります。MとPについては

f:id:tomo_watanabe:20131013004323p:plain

この式から、

  • M = 60MHz / 12MHz = 5
  • FCCO = 2 × P × 60MHz (156MHz < FCCO < 320MHz)なので、 P = 2

具体的に設定してみる

前述の設定を具体的に設定してみます。system_LPC8xx.cの設定値を変更します。変更するレジスタ

  • SYSPLLCTRL : PLLのMとPを設定する
  • MAINCLKSEL : メインクロック選択をPLL outに設定する
  • SYSAHBCLKDIV : システムクロックをメインクロックから1/2して生成する
#define CLOCK_SETUP           1
#define SYSOSCCTRL_Val        0x00000000      // Reset: 0x000
#define WDTOSCCTRL_Val        0x00000000      // Reset: 0x000
//#define SYSPLLCTRL_Val        0x00000041      // Reset: 0x000
#define SYSPLLCTRL_Val        0x00000024      // Reset: 0x000 M=5, P=2
#define SYSPLLCLKSEL_Val      0x00000000      // Reset: 0x000
//#define MAINCLKSEL_Val        0x00000000      // Reset: 0x000
#define MAINCLKSEL_Val        0x00000003      // Reset: 0x000 PLL select
//#define SYSAHBCLKDIV_Val      0x00000001      // Reset: 0x001
#define SYSAHBCLKDIV_Val      0x00000002      // Reset: 0x001 1/2

ただし注意があります。デフォルトではSYSAHBCLKDIV = 1なのでクロック生成を前段から行っていくと、メインクロック60MHzを出力した瞬間、システムクロックも60MHzとなってしまいます。そのため、まず最初にSYSAHBCLKDIV = 2として、あらかじめシステムクロックをメインクロックの1/2に設定しておくほうが安全です。コードは以下のように修正します。

  LPC_SYSCON->SYSAHBCLKDIV  = SYSAHBCLKDIV_Val; <-- 安全のため、先に1/2しておく

  LPC_SYSCON->SYSPLLCLKSEL  = SYSPLLCLKSEL_Val;   /* Select PLL Input         */
  LPC_SYSCON->SYSPLLCLKUEN  = 0x01;          /* Update Clock Source      */
  while (!(LPC_SYSCON->SYSPLLCLKUEN & 0x01));     /* Wait Until Updated       */
#if ((MAINCLKSEL_Val & 0x03) == 3)                /* Main Clock is PLL Out    */
  LPC_SYSCON->SYSPLLCTRL    = SYSPLLCTRL_Val;
  LPC_SYSCON->PDRUNCFG     &= ~(0x1 << 7);        /* Power-up SYSPLL          */
  while (!(LPC_SYSCON->SYSPLLSTAT & 0x01));        /* Wait Until PLL Locked    */
#endif

#if (((MAINCLKSEL_Val & 0x03) == 2) )
  LPC_SYSCON->WDTOSCCTRL    = WDTOSCCTRL_Val;
  LPC_SYSCON->PDRUNCFG     &= ~(0x1 << 6);        /* Power-up WDT Clock       */
  for (i = 0; i < 200; i++) __NOP();
#endif

  LPC_SYSCON->MAINCLKSEL    = MAINCLKSEL_Val;     /* Select PLL Clock Output  */
  LPC_SYSCON->MAINCLKUEN    = 0x01;            /* Update MCLK Clock Source */
  while (!(LPC_SYSCON->MAINCLKUEN & 0x01));       /* Wait Until Updated       */

//  LPC_SYSCON->SYSAHBCLKDIV  = SYSAHBCLKDIV_Val; <--- コメントアウト

動作確認をしてみる

動作確認はprintfを使ってSystemCoreClockを表示してみます。

f:id:tomo_watanabe:20131013012106p:plain

設定通り30MHzになっていることが確認できました。ここでレジスタをIDE上で確認できる方法を紹介します。ブレークした状態で、左のプロジェクトウィンドウから「Peripherals+」のタブを選択し、Peripheralから今の場合「SYSCON」をチェックします。この状態で右下のコンソールウィンドウの「Memory」タブを選択すると各レジスタの現在の値が表示されます。

f:id:tomo_watanabe:20131013012814p:plain

レジスタが設定通りになっていることが確認できます。このレジスタの確認はGPIOその他でも使えますし、その場で「Value」の値を変更することもできるのでデバッグにも使えます。


前回と今回はスタートアップルーチンや、動作クロックの設定について調べてみました。普段ArduinoのようなPICマイコンではこのようなことを気にする必要はありません。しかしプロトタイプ以上のことをやったり、製品に組み込むマイコンの選択をコストなどを考えると、組込みマイコンを使用する必要が出てきます。ここでの方法や基本的な考え方はどのマイコンでもほとんど同じなので、基本さえ押さえておけば応用は可能でしょう。

LPCXpressoとLPC800-MAXでマイコンを学ぶ(その4:スタートアップルーチン)

スタートアップルーチンを見てみる

Blinkyのサンプルアプリはmai.cのmain関数から始まっているのですが、ここでここまでに至る起動時のスタートアップルーチンを見てみようと思います。スタートアップルーチンでは、起動時の重要な初期化やデフォルト設定を行っています

main関数はどこから呼ばれるか?

main.cのmain関数はどこからコールされているか調べてみますと、cr_startup_lpc8xx.cからコールされていることがわかります。 正確には"_REDLIB_"が有効なので、Redlib内の__mainの後にmainがコールされています。またmain関数を実行し、万が一抜けてきた場合にはwhileの永久ループに入るようになっていることがわかります。

(cr_startup_lpc8xx.c)

#if defined (__REDLIB__)
    // Call the Redlib library, which in turn calls main()
    __main() ;
#else
    main();
#endif

    //
    // main() shouldn't return, but if it does, we'll just enter an infinite loop
    //
    while (1) {
        ;
    }
}

このmain関数をコールしている関数はResetISRという関数です。これはリセットハンドラで、リセットされるとここへジャンプしてきます。つまりこのRestISRがプログラムの開始となります。

SystemInit関数

ResetISRではSystemInit関数をコールしてシステムの初期化を行い、その後main関数を実行するようになっています。ResetISRの最初ではベクターテーブルをFlashからSRAMへ移動したり(Cortex-M0+の機能)、data領域の初期化などを行っています。

SystemInitの中を見ていきましょう。ifdefで無効になっている部分を取り除くと以下のようになります。

(system_LPC8xx.c)

void SystemInit (void) {
  volatile uint32_t i;

  /* System clock to the IOCON & the SWM need to be enabled or
  most of the I/O related peripherals won't work. */
  LPC_SYSCON->SYSAHBCLKCTRL |= ( (0x1 << 7) | (0x1 << 18) );

  LPC_SYSCON->SYSPLLCLKSEL  = SYSPLLCLKSEL_Val;   /* Select PLL Input         */
  LPC_SYSCON->SYSPLLCLKUEN  = 0x01;          /* Update Clock Source      */
  while (!(LPC_SYSCON->SYSPLLCLKUEN & 0x01));     /* Wait Until Updated       */

  LPC_SYSCON->MAINCLKSEL    = MAINCLKSEL_Val;     /* Select PLL Clock Output  */
  LPC_SYSCON->MAINCLKUEN    = 0x01;          /* Update MCLK Clock Source */
  while (!(LPC_SYSCON->MAINCLKUEN & 0x01));       /* Wait Until Updated       */

  LPC_SYSCON->SYSAHBCLKDIV  = SYSAHBCLKDIV_Val;
}

各行を解説していきます。これはユーザーズマニュアルと突き合わせて確認できます。

SWM(Switch Matrix)、IOCON(I/O configuration)のブロックにクロックを供給して有効にする。
LPC_SYSCON->SYSAHBCLKCTRL |= ( (0x1 << 7) | (0x1 << 18) );

#define SYSPLLCLKSEL_Val      0x00000000 // Reset: 0x000
:
System PLLのクロックソース選択。上記の値からIRCを選択
LPC_SYSCON->SYSPLLCLKSEL  = SYSPLLCLKSEL_Val; <----- 1
System PLLのアップデート
LPC_SYSCON->SYSPLLCLKUEN  = 0x01;
while (!(LPC_SYSCON->SYSPLLCLKUEN & 0x01)); 

#define SYSPLLCLKSEL_Val      0x00000000 // Reset: 0x000
:
メインクロックソース選択。上記の値からIRC Oscillator(内部発振器を使用)
LPC_SYSCON->MAINCLKSEL    = MAINCLKSEL_Val; <----- 2
メインクロックのアップデート
LPC_SYSCON->MAINCLKUEN    = 0x01;
while (!(LPC_SYSCON->MAINCLKUEN & 0x01));

#define SYSAHBCLKDIV_Val      0x00000001 // Reset: 0x001
:
システムクロックの分周比の設定。上記の値から1
LPC_SYSCON->SYSAHBCLKDIV  = SYSAHBCLKDIV_Val; <----- 3

動作クロックの設定

さて何をやってるんでしょうね。なんとなくクロック周りの初期設定を行っていることはわかります。上記の設定は下記の図の設定を行っています。下記はクロックジェネレータのブロック図ですが、前述の1〜3は①〜③の設定を行っています。

f:id:tomo_watanabe:20131012211624p:plain

この図との対応は、SYPLLCLKSELではIRC oscillatorを選択、次段のMAINCLKSELでは前段のPLL出力ではなく、IRC oscillatorを選択、それをSYSAHBCLKDIVで分周してシステムクロックとしています。つまり、

  • main clock : IRC oscillator
  • system clock : main clock / 1

となります。システムクロック=メインクロック=内部発振器のクロック数です。では内部発振器のクロックはいくつになっているでしょうか。ユーザーズマニュアルp5に書いてあります。

f:id:tomo_watanabe:20131012212651p:plain

したがって、システムクロックは12MHzで動作することになります。LPC812は30MHzで動作可能なのですが、初期値は抑えていますね。では、実際に12MHzで動作していることを確認してみます。main関数の最初のSystemCoreClockUpdate関数でそれを確認できます。SystemCoreClockUpdate関数の最後にSystemCoreClockへの代入箇所があるので、その後にブレークポイントを張ってブレークしたところで確認します。※下記スクリーンショットスクリーンショットの作成の都合上、代入前になっているので注意。実際に確認するのは戻り出口の場所で

f:id:tomo_watanabe:20131012213916p:plain

確かに12MHzになっています。実際のコード上での設定はどこでやっているかというとSystemCoreClockUpdate関数の下記になります。

  switch (LPC_SYSCON->MAINCLKSEL & 0x03) {
    case 0: /* Internal RC oscillator */
      SystemCoreClock = __IRC_OSC_CLK;  <-------- ここで一旦決定される
      break;
    case 1: /* Input Clock to System PLL */
      switch (LPC_SYSCON->SYSPLLCLKSEL & 0x03) {
          case 0: /* Internal RC oscillator */
            SystemCoreClock = __IRC_OSC_CLK;
            break;
          case 1: /* System oscillator */
            SystemCoreClock = __SYS_OSC_CLK;
            break;
          case 2: /* Reserved */
            SystemCoreClock = 0;
            break;
          case 3: /* CLKIN pin */
            SystemCoreClock = __CLKIN_CLK;
            break;
      }
      break;
    case 2:  /* WDT Oscillator  */

:
:
    下記でLPC_SYSCON->SYSAHBCLKDIV = 1
    SystemCoreClock /= LPC_SYSCON->SYSAHBCLKDIV;

SystemInitでLPC_SYSCON->MAINCLKSEL = 0と設定しているので、一旦システムクロックは_IRC_OSC_CLKになります。その後LPC_SYSCON->SYSAHBCLKDIVの値で分周しますが、設定では1なのでシステムクロックはそのまま_IRC_OSC_CLKとなります。

/*----------------------------------------------------------------------------
  Define clocks
 *----------------------------------------------------------------------------*/
#define __XTAL            (12000000UL)    /* Oscillator frequency             */
#define __SYS_OSC_CLK     (    __XTAL)    /* Main oscillator frequency        */
#define __IRC_OSC_CLK     (12000000UL)    /* Internal RC oscillator frequency */
#define __CLKIN_CLK       (12000000UL)    /* CLKIN pin frequency              */

__IRC_OSC_CLKは定義から12MHzとなっているので、システムクロックが12MHzということになります。以上がスタートアップルーチンです。スタータップで主に必要なことは下記となります。

  • 動作クロックの設定
  • クロックを供給するブロックの設定

LPCXpressoとLPC800-MAXでマイコンを学ぶ(その3:LEDの動作(GPIO)を調べる)

サンプルアプリ(Blinky)のLEDの動作を調べてみる

これで、ひと通りデバッグ等ができるようになりました。実際のコードを解析しつつ、ARM Cortex-M0というかLPC800シリーズのアーキテクチャを見て行きたいと思います。まずは前半を飛ばして、わかりやすいLEDの制御部から見ていきます。以下の方法は、僕がこういったマイコンの動作とアーキテクチャを理解する上で確認していく過程をほぼそのまま書いている感じです。

LEDはGPIOで制御

LEDはLPC800-MAXのボード上に配置してあり、LPC812と接続してあります。接続を確認するためにLPC800-MAXの回路図を確認します。

p2にある下記の部分がLEDの回路になります。この回路図から

  • PIO0_16 : BLUE
  • PIO0_17 : GREEN
  • PIO0_7 : RED

ということがわかります。

f:id:tomo_watanabe:20131009215242p:plain

Blinky(main.c)のソースコードを見ると

  /* Set port p0.7 to output */
  GPIOSetDir( 0, 7, 1 );

  /* Set port p0.16 to output */
  GPIOSetDir( 0, 16, 1 );

  /* Set port p0.17 to output */
  GPIOSetDir( 0, 17, 1 );

となっていますので、正しいことがわかります。この関数を見てみると、きちんとAND, ORでレジスタ制御されています。dirはoutput=1と設定されています。

(lpc8xx_gpio.c)

void GPIOSetDir( uint32_t portNum, uint32_t bitPosi, uint32_t dir )
{
  if( dir )
  {
        LPC_GPIO_PORT->DIR0 |= (1<<bitPosi);
  }
  else
  {
        LPC_GPIO_PORT->DIR0 &= ~(1<<bitPosi);
  }
  return;
}

GPIOレジスタの確認

さて、この関数の「LPC_GPIO_PORT」が指している場所を探してみると、、、

(LPC8xx.h)

#define LPC_GPIO_PORT_BASE    (0xA0000000)
:
#define LPC_GPIO_PORT         ((LPC_GPIO_PORT_TypeDef  *) LPC_GPIO_PORT_BASE  )
:

「LPC_GPIO_PORT」は「LPC_GPIO_PORT_BASE」へのポインタになっていて、LPC_GPIO_PORT_BASE = 0xA0000000となっています。これを確認してみます。LPC800シリーズのページのLPC81xユーザーズマニュアルを調べます。p10にメモリマップが載っています。これによると0xA0000000〜0xA0004000までがGPIOに割り当てられています。

f:id:tomo_watanabe:20131009235224p:plain

LPC_GPIO_PORT_TypeDefをコード上で見ると下記のように定義されています。

(LPC8xx.h)

typedef struct {                            
  __IO uint8_t B0[18];                   /*!< (@ 0xA0000000) Byte pin registers port 0 */
  __I  uint16_t RESERVED0[2039];
  __IO uint32_t W0[18];                  /*!< (@ 0xA0001000) Word pin registers port 0 */
       uint32_t RESERVED1[1006];
  __IO uint32_t DIR0;                          /* 0x2000 */
       uint32_t RESERVED2[31];
  __IO uint32_t MASK0;                                  /* 0x2080 */
       uint32_t RESERVED3[31];
  __IO uint32_t PIN0;                          /* 0x2100 */
       uint32_t RESERVED4[31];
  __IO uint32_t MPIN0;                                   /* 0x2180 */
       uint32_t RESERVED5[31];
  __IO uint32_t SET0;                         /* 0x2200 */
       uint32_t RESERVED6[31];
  __O  uint32_t CLR0;                         /* 0x2280 */
       uint32_t RESERVED7[31];
  __O  uint32_t NOT0;                                    /* 0x2300 */

} LPC_GPIO_PORT_TypeDef;

GPIOのポートの定義はユーザーズマニュアルのp88から

f:id:tomo_watanabe:20131009235252p:plain

これに対応していることになります。この表からDIR0のReset Valueは0、つまり起動時の初期値はINPUTであることがわかります。BlinkyではDIR0の対応するbitに1(=OUTPUT)をORで書き込んでいることになります。

またGPIOへの書込みはSET0, CLR0, NOT0を利用することがわかります。SET0のReset Valueは0なので、BlinkyでGPIOSetDir関数をセットした直後の状態は

  • OUTPUT
  • 初期値: 0

となるので、該当するGPIOのLEDは「点灯」することになります。これを実機上で確認するにはGPIOSetDir関数までステップ実行させてみると確認できます。下記はPIO0_7(RED)のDIRをセットした直後の状態です。

f:id:tomo_watanabe:20131009223241j:plain

点灯する理由は回路図を確認することでわかります。回路図ではLEDはアノードコモンを使用しており、3.3vにプルアップされています。したがってOUTPUTで0が書き込まれると、電流が流れ込みLEDが点灯することになります。消灯する場合は1を書き込むことで3.3vで電位が同じになり、電流が流れないので消灯することになります。

GPIOへの書込み

設定が理解できた所で書込みですが、これは上記で書いたとおりになっています。main.cのコードを見るとここで行っています。

  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 );
        }

単純にGPIOSetBitValue関数で、点灯時は0, 消灯時は1をセットしています。セットする関数はGPIOSetDirと同じようになっています。ここではSET/CLRを使用していますね。

(lpc8xx_gpio.c)

void GPIOSetBitValue( uint32_t portNum, uint32_t bitPosi, uint32_t bitVal )
{
  if ( bitVal )
  {
        LPC_GPIO_PORT->SET0 = 1<<bitPosi;
  }
  else
  {
        LPC_GPIO_PORT->CLR0 = 1<<bitPosi;
  }
  return;
}

GPIOの初期設定

飛ばしてしまっていたのですが、GPIOの初期設定も見ておきます。

  /* Enable AHB clock to the GPIO domain. */
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<6);

先ほどのGPIOで探したのと同じ要領で「LPC_SYSCON」と「SYSAHBCLKCTRL」を探します。これは「System configuration」の「System clock control」レジスタです。(ユーザーズマニュアルp26, p35)そして6bit目に1を書き込んでいます。

f:id:tomo_watanabe:20131009235347p:plain

この表からGPIOへのクロック供給をEnableにしてGPIOブロックを有効にしていることがわかります。


さすがにArduinoとはひと味もふた味も違う難解さですね(^^; GPIO一つ動かすだけでもこれだけの調査と理解が必要になります。次回はMRT(Multi rate timer)とかクロック周りを見ていこうかと思います。

LPCXpressoとLPC800-MAXでマイコンを学ぶ(その2:デバッグでprintf文を使う)

普通にオンラインデバッグができますが、一点だけprintf文を使おうとしてハマりました。

デバッグでprintf文を使う

前回のBlinkyプロジェクトにprintf文を書いてビルドすると以下のようにリンカでエラーになってしまいます。

f:id:tomo_watanabe:20131009211938p:plain

調べたところ解決策がこちらの「趣味エンジニアリング」に書いてありました。

プロジェクトウィンドウから「Blinky」のプロジェクトを右クリック→「Properties」→「C/C++ Build」→「Settings」で右のビューの「MCU Linker」→「Target」を選択します。

「Use C Library」が「Redlib(none)」となっていますので、これを「Redlib(semihost)」に変更します(下図参照)

f:id:tomo_watanabe:20131009213149p:plain

これでビルドすると・・・

f:id:tomo_watanabe:20131009213319p:plain

無事ビルドが通るようになりました。ただしこの設定はプロジェクトごとに必要みたいなので注意が必要です。

実際に実行すると「Console」ウィンドウにprintfの内容が表示されるようになります。

f:id:tomo_watanabe:20131009213814p:plain

BlinkyのGPIOレジスタの設定についても書こうと思いますが、別途エントリを分けます。