LPCXpressoとLPC800-MAXでマイコンを学ぶ(その9:I2Cでスイッチコントロール(GPIO))
おそらく今回でLPCXpressoは最後になりますが、I2Cを使ってみます。LPC812は少ピンなのでGPIOが少ないのですが、Arduinoと同じようにGPIOやADCを載せるために、I2Cバスにデバイスを接続して拡張しています。
I2Cのサンプルプログラムを動かす
まずはサンプルプログラムを動かしてみます。サンプルは例のごとく、import project(s)からNXP_LPC8xx_SampleCodeBundle.zipを選択して「I2C」をインポートします。メインはi2ctest.cです。しかしこのサンプルコードは、このLPC800-MAXでは動かないのでLPC800-MAXのGPIO制御用のI2Cのコードを作成しました。
このコードをi2ctest.cを削除して使用します。このコードを動かすと、SW2のスイッチを押すと赤色のLEDが光るようになっています。 LEDを光らせているのは、最初の方にやったGPIO制御ですが、SW2のスイッチ制御にI2Cを使用しています。
LPC800-MAXのI2C
I2Cデバイス
LPC800-MAXでは2つのデバイスがI2Cバスに接続されています
- PCA9672PW : GPIOエキスパンダ
- PCF8591T : ADC / DAC
今回はこのうちのGPIOエキスパンダを使用しています。PCA9672PWの資料はこちらにあります。
I2Cアドレス
GPIOエキスパンダのI2Cアドレスを調べてみます。回路図に書いてあります。
I2C Addrs = 0b01000(A1)(A0); Default Addrs = 0b0100011
ということなので、0x23になります。I2Cバスは7bitアドレスを採用していて、下位1bitでRead/Write制御するようになっています。そのためこの場合は左へ1bitシフトし、b01000110/Write, b01000111/Readとなり、それぞれ0x46/Write, 0x47/Readとなります。GPIOへのアクセスは8bitでRead/Writeする形になります(AND書込みなので、どこかのポートだけ制御したい場合は、読み込んでからORして書込む必要あり)
また回路図を見るとP0-P7がGPIOになっており、P7がSW2に接続されていることがわかります。IOEXPAN4, IOEXPAN5はArduinoのDigitalピンとして出ています。
なお、基板上では「XP_4」「XP_5」となっていますが、シルクが間違っていますので注意して下さい。
サンプルプログラムの動作
サンプルのプログラムではI2Cの初期化を行い、GPIOエキスパンダのIOEXPANを操作します。動作確認はテスタなどで当たってもらえばわかります。その後、P7のスイッチを監視するようにしていて、スイッチが押されるとLEDを点灯する。というプログラムです。
I2Cのピンアサインの設定
コードの最初のところでI2Cへのピンアサインを行っています。この辺は以前にも出てきたピンマルチの設定です。
- SDA : PIO0_10
- SLA : PIO0_11
/* * Initialize I2C pin connect */ /*connect the I2C SCL and SDA sigals to port pins(P0.10-P0.11)*/ regVal = LPC_SWM->PINASSIGN7 & ~(0xFFUL<<24); LPC_SWM->PINASSIGN7 = regVal | (10 << 24); /* P0.10 is I2C SDA, ASSIGN0(31:24) */ regVal = LPC_SWM->PINASSIGN8 & ~(0xFF<<0); LPC_SWM->PINASSIGN8 = regVal | (11 << 0); /* P0.11 is I2C SCL. ASSIGN0(7:0) */ regVal = LPC_IOCON->PIO0_10 & ~(0x3<<8); LPC_IOCON->PIO0_10 = regVal | (0x2<<8); /* Enable Fast Mode Plus */ regVal = LPC_IOCON->PIO0_11 & ~(0x3<<8); LPC_IOCON->PIO0_11 = regVal | (0x2<<8); /* Enable Fast Mode Plus */ /* Enable I2C clock */ LPC_SYSCON->SYSAHBCLKCTRL |= (1<<5); /* Toggle peripheral reset control to I2C, a "1" bring it out of reset. */ LPC_SYSCON->PRESETCTRL &= ~(0x1<<6); LPC_SYSCON->PRESETCTRL |= (0x1<<6);
後半の部分はI2Cブロックへのクロックの供給とリスタートです。
I2Cの初期化
/* For master mode plus, if desired I2C clock is 1MHz (SCL high time + SCL low time). If CCLK is 36MHz, MasterSclLow and MasterSclHigh are 0s, SCL high time = (ClkDiv+1) * (MstSclHigh + 2 ) SCL low time = (ClkDiv+1) * (MstSclLow + 2 ) Pre-divider should be 36000000/(1000000*4)-1 = 9-1 = 8. If fast mode, e.g. communicating with a temp sensor, Max I2C clock is set to 400KHz. Pre-divider should be 36000000/(400000*4)-1 = 22.5-1 = 22. If standard mode, e.g. communicating with a temp sensor, Max I2C clock is set to 100KHz. Pre-divider should be 36000000/(100000*4)-1 = 90-1 = 89. */ I2C_MstInit(LPC_I2C, I2C_FMODE_PRE_DIV, CFG_MSTENA, 0x00);
今回はLPC812はI2Cマスターとして動作するので、その初期化を行います。I2Cバスプロトコルには3種類あります。今回アサインされているPIO0_10, PIO0_11は全てのモードをサポートしています。この設定ではFast-modeでの動作設定を行っています。またマスターモードをイネーブルにセットしています。
- Standard-mode :
- Fast-mode :
- Fast-mode Plus :
その後にI2Cがアイドルかどうかチェックしている箇所があったり、バッファを用意しています。
GPIOの初期化(LEDの初期化)
最初の頃にやったGPIOの設定です。ここでは赤色LEDを使うために初期化しています。
/* GPIO初期化 */ GPIOInit(); GPIOSetDir( 0, 7, 1 ); // RED GPIOSetBitValue( 0, 7, 1 ); // RED OFF
GPIOエキスパンダの操作
GPIOエキスパンダに一度0を書き込んでから、B0を書き込んで確認しています。0x46/Write, 0x47/Readです。
/* Clear GPIO = 0 / GPIO = 0xB0 */ uint8_t data; I2CMasterTXBuffer[0] = 0x00; I2C_MstSend( LPC_I2C, 0x46, (uint8_t *)I2CMasterTXBuffer, 1 ); I2C_MstReceive( LPC_I2C, 0x47, &data, 1 ); printf("GPIO: %x\n", data); I2CMasterTXBuffer[0] = 0xB0; I2C_MstSend( LPC_I2C, 0x46, (uint8_t *)I2CMasterTXBuffer, 1 ); I2C_MstReceive( LPC_I2C, 0x47, &data, 1 ); printf("GPIO: %x\n", data);
0xB0を書き込んでいる理由は、b10110000で、P7 = HIGH, P5 = HIGH, P4 = HIGHに設定するためです。これでArduino端子のDigital 5, 6がHIGHになります。 またP7をHIGHにすることで、SW2の押下でP7がGND(=0)になるのを検知できるようになります。
スイッチの取得
永久ループ内でレジスタを読みだしています。0x80と比較して、つまりb1000000と比較して1(=HIGH)なら、押されていないのでLEDを消灯、0(=GND,LOW)ならスイッチが押されているのでLEDを点灯するようにしています。
while (1) { I2C_MstReceive( LPC_I2C, 0x47, &data, 1 ); if ( data &= 0x80 ) { GPIOSetBitValue( 0, 7, 1 ); // RED OFF } else { GPIOSetBitValue( 0, 7, 0 ); // RED ON } }
まとめ
とりあえずLPC800-MAXを使ったLPCXpressoでの組み込みマイコンはこれくらいになります。Arduinoでもマイコンでもだいたい
- GPIO
- UART
- I2C
がだいたいできれば、あとはなんとかなるんじゃないかと思います。LPC812単体の価格ですが@200以下で買えます。ArduinoはUnoで@3000くらいしますので、一桁価格が違うことになります。もちろん、LPC812単体ではダメで、周辺回路やデバッグ回路を作成することになりますので、LPC812の場合は100台以上の量産向けという選択になります。
こんな感じになるでしょうか。ある程度の量産を視野にいれる場合、このようなマイコンへの習熟やコスト感が後々重要になってきます。
# 僕自身は回路屋さんじゃないので実際の量産基板設計とかはできないのですが、回路図を読むことはできるので、ソフトウェア開発を行うためのハードの要件を回路屋さんと詰めることができます。ユーザーズマニュアルを読めることと、回路図を読めることで、ソフトウェア開発者としての幅が広がります。まぁ需要があるかどうかは置いておきますが(^_^;)
LPCXpressoとLPC800-MAXでマイコンを学ぶ(その8:UARTでPCと接続)
今回もサンプルを使ってUARTの動作を確認していきます。
UARTのサンプルプログラムを動かす
まずはサンプルプログラムを動かしてみます。サンプルは例のごとく、import project(s)からNXP_LPC8xx_SampleCodeBundle.zipを選択して「UART」をインポートします。メインはuarttest.cです。このサンプルプログラムはUARTでPCのターミナルソフトなどと接続して、PCのからの入力をループバックでエコーを返すプログラムです。それにはPCとLPC800-MAXを接続する必要があるので接続図を示します。
LPC800-MAXの0pinをシリアルアダプタのTX, 1pinをRX, GNDを接続します。注意としてはLPC800-MAXは3.3v駆動なので、USBシリアルアダプタを使う場合、3.3v対応のものを使うことです。PCのターミナルソフトではボーレートを9600に設定しておきます。あとはお決まりの、データビット8,、ストップビット1、 パリティ無し、フローコントール無しです。この状態でdebugビルドで動かしてみます(redlib(semihost)に変更しないとエラーになるので注意)
緑のLEDが点灯します(これはおまけに記述)。一方PCのターミナルには"Hello World!"が表示されます。ターミナルの方に文字を入力するとエコーバックされて文字が表示されます。
UARTの動作を見る
UARTのピンアサインの設定
コードは#ifでいくつかのブロックになっていますが、これはRXとTXのUARTブロックとピンアサインの話なので#if 1のとこだけ見ます。
/* connect the UART0 TXD abd RXD sigals to port pins(P0.4-P0.0)*/ regVal = LPC_SWM->PINASSIGN0 & ~( 0xFF << 0 ); LPC_SWM->PINASSIGN0 = regVal | ( 4 << 0 ); /* P0.4 is UART0 TXD, ASSIGN0(7:0) */ regVal = LPC_SWM->PINASSIGN0 & ~( 0xFF << 8 ); LPC_SWM->PINASSIGN0 = regVal | ( 0 << 8 ); /* P0.0 is UART0 RXD. ASSIGN0(15:8) */ regVal = LPC_SWM->PINASSIGN0 & ~( 0xFF << 16 ); LPC_SWM->PINASSIGN0 = regVal | ( 12 << 16 ); /* P0.12 is UART0 RTS, ASSIGN0(23:16) */ regVal = LPC_SWM->PINASSIGN0 & ~( 0xFFUL << 24 ); LPC_SWM->PINASSIGN0 = regVal | ( 13 << 24 ); /* P0.13 is UART0 CTS. ASSIGN0(31:24) */
この部分はUARTのRX, TX, RTS, CTSのピンアサインを行っています。
LPC800-MAXの回路図で見ると、TX, RXはそれぞれDigitalピンとしての1pin, 0pinに割りあたっています。ちょうどこのピン配置はArduinoと同じ配置にしてあります(当たり前だけど)
ここではPIOにUARTの端子を割り付けるという、いわゆるピンマルチの設定を行っています。LPC800シリーズでは機能の割にピン数が少ないため、デフォルト状態ではすべてGPIOに設定されていますが、これを内部のピンマルチ機能を使って、使いたい機能ブロックのピンをGPIOに割りつけるようになっています。通常ピンマルチは制約が多いのですが、LPC800シリーズではかなり自由に割り付けられるようになっていて、Switch Matrix機能と呼ばれています。詳しいことはLPC800シリーズ - 少ピンパッケージ、スイッチ・マトリックス内蔵 Cortex-M0+に書いてあります。設定を見てみます。ユーザーズマニュアルp122-p124にSwitch Matrixのレジスタ設定があります
Pin assigne register 0を見ると、該当するビット範囲(ワード設定)にPIOの番号を書き込むことでアサインされるようです。TXDをPIO0_4にアサインするには4を[7:0]で書き込みます。その他のピンアサインも同様に行っています。
UART通信の初期化
初期化でUARTのピンアサインを行ったので、UARTの初期化を行います。
UARTInit(LPC_USART0, 9600);
UARTInit関数です。UARTのコードもARMが提供しているところなので詳細は省きます。まぁ普通に使う分にはそのままでいいんじゃないかと。(厳密にはボーレートの生成クロックの選択など、シビアな設定がありますが)lpc8xx_uart.cで提供されている関数で、ボーレートと使うUARTのチャンネルを設定します。USART(非同期および同期が使える)はLPC812では2チャンネル、もしくは3チャンネル用意されています(パッケージで違う)あれれ?フローコントロールとか、パリティの設定が無いなーと思ったら・・・
(lpc8xx_uart.c) void UARTInit(LPC_USART_TypeDef *UARTx, uint32_t baudrate) { uint32_t UARTSysClk; UARTTxEmpty = 1; UARTClock_Init( UARTx ); UARTSysClk = SystemCoreClock/LPC_SYSCON->UARTCLKDIV; UARTx->CFG = DATA_LENG_8|PARITY_NONE|STOP_BIT_1; /* 8 bits, no Parity, 1 Stop bit */ // UARTx->CFG = DATA_LENG_7|PARITY_NONE|STOP_BIT_1; /* 7 bits, no Parity, 1 Stop bit */ // UARTx->CFG = DATA_LENG_8|PARITY_NONE|STOP_BIT_2; /* 8 bits, no Parity, 2 Stop bit */ // UARTx->CFG = DATA_LENG_8|PARITY_EVEN|STOP_BIT_1; /* 8 bits, even Parity, 1 Stop bit */ // UARTx->CFG = DATA_LENG_8|PARITY_ODD|STOP_BIT_1; /* 8 bits, odd Parity, 1 Stop bit */
コメントアウトされてる...orz UARTも詳細を自分で設定する場合は、このドライバ書き直す必要がありますね...(;´Д`)
受信・送信処理
実際の受信・送信処理は下記の部分です。UARTSend関数で"Hello World!"をUARTで送信して、永久ループで受信待ちになっています。送信はUARTのチャンネルと、送信データ、サイズです。
UARTSend(LPC_USART0, (uint8_t *)"Hello World!\r\n", 14); for ( i = 0; i < 0x10000; i++ ); while ( 1 ) { if ( UARTRxCount && !RxErrorCount ) { LPC_USART0->INTENCLR = RXRDY; /* Disable RXRDY */ UARTSend( LPC_USART0, (uint8_t *)UARTRxBuffer, UARTRxCount ); UARTRxCount = 0; LPC_USART0->INTENSET = RXRDY; /* Re-enable RXRDY */ } } }
受信ですが、これも実装が微妙な気がしますが(゜.゜)・・・UARTRxCountは受信データ数なので、受信データがあって、かつエラービットが立ってない場合処理を行います。
- 受信割り込みクリア
- 受信バッファの内容を、そのまま送信
- 受信データサイズクリア
- 受信割り込み有効
という流れになっています。これで、受信した(PCからの入力)データがエコーバックされる動作となっています。しかし、この実装はドライバで使う変数をここで処理するのはあまり良くないですね...(;´Д`) なんかサンプルコードがこんなのばっかなんだけどいいのかな。
おまけ。なぜLEDが点灯するのか?
さて、UARTの動作は理解できたかと思います。ひとつあれ?っと思う部分があったので補足します。LPC800-MAXでこのサンプルプログラムを走らせると、LEDが緑に点灯するんですね。GPIOの設定した覚えないのに...というわけで、そこを調べてみます。実はBlinkyの時からあった、この最初のデバッグに使うよ。という部分が答えでした。
/* Config CLKOUT, mostly used for debugging. */ regVal = LPC_SWM->PINASSIGN8; regVal &= ~( 0xFF << 16 ); regVal |= ( 17 << 16 ); LPC_SWM->PINASSIGN8 = regVal; /* P0.17 is CLKOUT, ASSIGN(23:16). */ CLKOUT_Setup( CLKOUTCLK_SRC_MAIN_CLK );
何をやってるのか調べてみると、CLCKOUTを特定のピンに外部出力していました。このプログラムの場合、PIO0_17にCLCKOUT、おそらく12MHzを出力しています。LPCXpressoとLPC800-MAXでマイコンを学ぶ(その3:LEDの動作(GPIO)を調べる)でやりましたが、LPC800-MAXのボードはPIO0_17は緑のLEDに接続していました。ここにCLCKOUTが出力されたことでPWMとして機能するため、緑色に点灯するというわけです。具体的にやっていることは
- Switch MatrixのレジスタでCLCKOUTピンをPIO0_17に設定
- CLCKOUTをメインクロックの12MHzに設定
CLKOUTは右真ん中あたりにあります。CLKOUT_Setup関数では、CLKOUTのソースクロックを設定します。関数内ではソースクロックの選択と、CLKOUTのためにCLKOUTDIVで分周しています。ただし、ちょっと不思議なのはCLKOUTDIVは初期設定が0なのでDisableのはずなのにLEDが点灯しているんですよね。はて
その後にCLKOUTDIV = 1を書き込むことで、12MHzが出力されるということのはずなのですが、オシロスコープがあればこの辺確かめられるはずだけど持ってない...orz
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
SYST_RVRレジスタにタイマーの設定値をセットします(あれ?なんで-1してんだろ?)SYST_CVRレジスタに値を書き込むことでSystickタイマのクリアが行われます。
SYST_CSRにはCLKSOURCE, TICKINT, ENABLEに1を書き込むことで、クロックソースをシステムクロック、割り込み有効、タイマーを有効としています。
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にあります。
サンプルプログラムを見てて思ったのは「あれっ?これ品質的に全然ダメなんじゃね・・?」という箇所がパラパラと見かけられたことです。例えば前エントリの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にあります。
0を書き込んでから、1を書き込んでブロックにリセットを掛けています。
LPC_SYSCON->PRESETCTRL &= ~(0x1<<7); LPC_SYSCON->PRESETCTRL |= (0x1<<7);
グローバル変数mrt_counterを初期化して、タイマー値を設定しています。ここでMRTは4チャンネルあるはずなのですが、なぜか0チャンネル固定になっています。MRTはユーザーズマニュアルp162から
なお、マニュアルにはちゃんと4チャンネル分の記述があります。Time interval registerにタイマーの設定値を書込み、31bit目にタイマーを即時開始するように、LOADビットを書き込んでいます。ユーザーズマニュアルp163
mrt_counter = 0; LPC_MRT->Channel[0].INTVAL = TimerInterval; LPC_MRT->Channel[0].INTVAL |= 0x1UL<<31;
次にControl registerにリピートモードに設定し、割り込みを有効にしています。
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にあります。
つまり、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に割り込みフラグが立っているかチェックしています。
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で動作するように設定を変更してみます。
動作クロックの変更を行うには
前回のエントリのクロック・ジェネレータを参照しながら考えてみます。
目的は「system clock」を30MHzにすることです。さて入力クロックの種類ですが、
- IRC oscillator
- XTALIN, XTALOUT
- CLKIN
- watchdog oscillator
が選択できるようですがクロックジェネレータには制約があります。
watchdog oscillatorは入力周波数が低すぎて使えません。CLKINは回路図上でクロックがそもそも接続されていません。XTALIN, XTALOUTですが回路図で見ると...
実装はされているのですが、ジャンパーで実は接続されていません。したがって使えるのはIRC osciilator(内部発振器)だけとなります。さて12MHzの内部発振器で30MHzを作るにはどうすればよいでしょう。答えはPLL回路で逓倍・分周することで作ることができます。このあたりもユーザーズマニュアルp50に記述があります。
今回はPLLを使ってSystem clock=30MHzを作りたいので、Main clock = 60MHzを作れということです。その場合
- M divider value : 5
- P divider value : 2
というのがあります。これがPLL回路の設定値となります。この表の少し上に色々と記述があり、PLL回路の制約条件が書いてあります。
整理するとこういうことになります。
今、PLL入力 = 12MHz、Main clock = 60MHz、System clock = 30MHzなので、SYSAHBCLKDIV = 2となります。MとPについては
この式から、
- 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を表示してみます。
設定通り30MHzになっていることが確認できました。ここでレジスタをIDE上で確認できる方法を紹介します。ブレークした状態で、左のプロジェクトウィンドウから「Peripherals+」のタブを選択し、Peripheralから今の場合「SYSCON」をチェックします。この状態で右下のコンソールウィンドウの「Memory」タブを選択すると各レジスタの現在の値が表示されます。
各レジスタが設定通りになっていることが確認できます。このレジスタの確認は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は①〜③の設定を行っています。
この図との対応は、SYPLLCLKSELではIRC oscillatorを選択、次段のMAINCLKSELでは前段のPLL出力ではなく、IRC oscillatorを選択、それをSYSAHBCLKDIVで分周してシステムクロックとしています。つまり、
- main clock : IRC oscillator
- system clock : main clock / 1
となります。システムクロック=メインクロック=内部発振器のクロック数です。では内部発振器のクロックはいくつになっているでしょうか。ユーザーズマニュアルp5に書いてあります。
したがって、システムクロックは12MHzで動作することになります。LPC812は30MHzで動作可能なのですが、初期値は抑えていますね。では、実際に12MHzで動作していることを確認してみます。main関数の最初のSystemCoreClockUpdate関数でそれを確認できます。SystemCoreClockUpdate関数の最後にSystemCoreClockへの代入箇所があるので、その後にブレークポイントを張ってブレークしたところで確認します。※下記スクリーンショットはスクリーンショットの作成の都合上、代入前になっているので注意。実際に確認するのは戻り出口の場所で
確かに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ということになります。以上がスタートアップルーチンです。スタータップで主に必要なことは下記となります。
- 動作クロックの設定
- クロックを供給するブロックの設定