Cortex-M4で使用されているNVICの仕組みを調べてみました。今回は、KinetisマイコンTWR-KE18Fを使用して、ボード上にあるタクトスイッチが押されたときに割り込み処理がどうやって呼ばれるのかを見てみます。
TWR-KE18Fは、以前、モータドライバの使用方法を説明したときに登場しましたが、別にCortex-M4を搭載したマイコンであれば、基本的なやり方はあまりかわりません。
割り込み処理って?
英語だと、interruptです。これの対義がpollingになります。今回のタクトSWの例だと、下の絵のような感じです。何かの信号の状態変化が起きた時に特定の処理をさせたいよ、といった場合は、割り込み処理のほうがポーリング処理よりも効率的です。
とりあえず、サンプルコード
ボタンを押すと赤色LEDがひかるコードのサンプルです。他のところはさておいて、肝心なところは、色付きの部分に注目します。他は派手に無視。
#include <stdio.h>
#include "board.h"
#include "peripherals.h"
#include "pin_mux.h"
#include "clock_config.h"
#include "MKE18F16.h"
#include "fsl_debug_console.h"
#include "fsl_port.h"
#define BOARD_LED_GPIO BOARD_LED_RED1_GPIO
#define BOARD_LED_GPIO_PIN BOARD_LED_RED1_PIN
void BOARD_SW3_IRQ_HANDLER(void)
{
GPIO_PortToggle(BOARD_LED_GPIO, 1u << BOARD_LED_GPIO_PIN);
GPIO_PortClearInterruptFlags(BOARD_SW3_GPIO, 1U << BOARD_SW3_GPIO_PIN);
}
int main(void) {
BOARD_InitBootPins();
BOARD_InitBootClocks();
BOARD_InitBootPeripherals();
BOARD_InitDebugConsole();
PORT_SetPinInterruptConfig(BOARD_SW3_PORT, BOARD_SW3_GPIO_PIN,
kPORT_InterruptFallingEdge);
EnableIRQ(BOARD_SW3_IRQ);
/* Enter an infinite loop, just incrementing a counter. */
while(1) {
}
return 0 ;
}
BOARD_SW3_IRQ_HANDLERの関数が、ボタンを押す毎に実行されて、LEDを制御してるっていうのは何となくわかります。でも、こんな関数一体どこで定義されてるのか、不明です。これを追っかけていきます。
NVICとはなんだ?
Nested Vector Interrupt の略です。Cortex-M4にはNVICという割り込み処理用のコントローラが内蔵されてます。そのNVICはベクターテーブルとよばれるテーブルを持っているんですが、そのテーブルが、割り込みの要因ごとに実際の例外処理が記述されているコードの先頭アドレスの情報を持っています。
NVICはそのテーブルに基づいて、割り込み要求があったときに、例外処理を実行するため、分岐先のアドレスをPCにわたします。このとき、各割込みには、優先順位がついていて、NVICは、同時に複数の割込み要求があったときには、その優先順位に従って、調停して、分岐先を決めてくれます。
割込みのリストはきまってる
割込み処理で肝心なとこは、何がどうなった時に割り込み処理を実行するという点ですが、これは、コアとマイコンの仕様できまってます。今回はTWR-KE18Fを使いますが、マニュアルでは、下のようにつらつらと下のようなリストが書かれています。
途中略
途中略
リストをみてみると、右欄のSource descriptionに割り込みさせるときの動作が書いてありますね。pin detect とか wake upとか。
つまるところ、このリストは、こんな時に割り込み処理を行える仕様になっていますという説明をしています。
今回つかったSW(SW3)は、PTD6というポートにつながってるので、リスト内の赤で塗ってる箇所、Pin Detect (PORT D)という割り込みを使えばいいってことがわかります。リストの中身は下のような感じでした。
0x0000_0138 Vector #78 IRQ #62 NVIC non-IPR #1 NVIC IPR #15
なんでしょうか?これは。
まず、Vector ナンバは、割込み処理の種類の番号みたいです。リストの最初から番号が振ってあります。右のIRQも似てますが、ちょっと数値が違ってます。
割込みの要因にはコア側で定義されているものとチップ側で定義されているものがあり、二つを混同しないように、IRQでは、コア側をマイナス(上のリストはNXPのものなので、数字が書いてませんが、マイナスで定義されます)、一方でチップ側の割り込みについては、IRQをプラスで表示しています。
なので、IRQはVectorナンバのチップ側の割り込み要因をナンバリングしたものと理解できそうです。IRQの数値は実際、サンプルコード中に下のように使われています。
EnableIRQ(BOARD_SW3_IRQ); // BOARD_SW3_IRQ -> 62
こんなふうにAPIを利用すれば、EnableIRQという関数にさっきのリストから有効にしたい割り込みを見つけて、そのIRQ番号を引数として、関数に渡すだけで使えるようです。
ちなみに、リストの左にあるアドレス0x0138はなんでしょか。
それぞれの割り込みには、それぞれ固有のアドレスをもっています。リスト見ればわかりますが、実は、こんな関係が、あります。
参照アドレス = IRQの数値 × 4 + 64
実際に、IRQを62にして、計算すれば、アドレスはリストに明記されてある0x138にちゃんとなりました。
実際にそこのアドレスになにが書かれているでしょうか、みてみます。
0x0000_044Dと書かれていました。
これ何かというと、ソースコードをみると、割り込み時によばれる関数の先頭アドレス(0x0000_044C)になっていることがわかります。
下の画像が、その証拠です。
分かりにくいですが、サンプルプログラムにあったBOARD_SW3_IRQ_HANDLERという関数の正体はPORTD_IRQHandlerでその関数のポインタは0x44cになっています。
ただ、0x44Dと0x44Cでは、LSBのビットが異なります。これは、Cortex-M4の仕様によるもので、例外処理をARMかThumbモードで実行されるかを指定するために、LSBが使われるためです。
なので、CPUはSWボタンが押されると、実際は、0x044Cのアドレスに分岐することになります。
こんな感じで、NVICはベクタテーブルという割込みリストを持っていて、それぞれの割込みに対応したメモリアドレスを持っています。そのアドレスには、割込み処理を実際に記述している関数の先頭アドレスを格納していて、割込み要因に合わせて、CPUに分岐先を指示するって感じで機能しています。
ちなみに実際のコードでは、ベクタテーブルは下のように関数ポインタの配列として、定義されています。これは大体、マイコンのスタートアップ時の処理のなかで定義されているようです。
関数ポインタの配列の定義はこんな感じでやります。
void (* const hoge [] ) (void) = { } // 関数ポインタ 配列の定義の仕方
そして、下が実際のコード、リストにあった割り込み要因が実際に定義されてます。PORTD_IRQHandlerも途中でちゃんと、定義されてました。
void (* const g_pfnVectors[])(void) = {
// Core Level - CM4 ここからコア
&_vStackTop, // The initial stack pointer
ResetISR, // The reset handler
NMI_Handler, // The NMI handler
HardFault_Handler, // The hard fault handler
MemManage_Handler, // The MPU fault handler
////////////////////////// 途中略 /////////////////////////////
PendSV_Handler, // The PendSV handler
SysTick_Handler, // The SysTick handler
// Chip Level - MK64F12 ここからチップ
DMA0_IRQHandler, // 16 : DMA Channel 0 Transfer Complete
DMA1_IRQHandler, // 17 : DMA Channel 1 Transfer Complete
DMA2_IRQHandler, // 18 : DMA Channel 2 Transfer Complete
DMA3_IRQHandler, // 19 : DMA Channel 3 Transfer Complete
DMA4_IRQHandler, // 20 : DMA Channel 4 Transfer Complete
////////////////////////// 途中略 /////////////////////////////
PORTA_IRQHandler, // 75 : Port A pin detect interrupt
PORTB_IRQHandler, // 76 : Port B pin detect interrupt
PORTC_IRQHandler, // 77 : Port C pin detect interrupt
PORTD_IRQHandler, // 78 : Port D pin detect interrupt
PORTE_IRQHandler, // 79 : Port E pin detect interrupt
////////////////////////// 途中略 /////////////////////////////
ENET_Transmit_IRQHandler, // 99 : Ethernet MAC Transmit Interrupt
ENET_Receive_IRQHandler, // 100: Ethernet MAC Receive Interrupt
ENET_Error_IRQHandler, // 101: Ethernet MAC Error
}; /* End of g_pfnVectors */
割込みの有効化の方法
まずは、API関数を使う方法。簡単です。IRQの番号を引数に渡すだけ。
EnableIRQ(BOARD_SW3_IRQ); // BOARD_SW3_IRQはIRQ番号
次にレジスタを直接たたく方法。ISERというレジスタに1をセットすれば、割込みを有効化できます。ISERのレジスタ(32bit)は複数ありますが、リストにあるNVIC non-IPRをみて、何番のレジスタを使えばいいかわかります。今回は1なので、NVIC_ISER1を使います。
次にどこのビットを立てるかですが、ビットはIRQの0から順に定義されてるだけなので、IRQを32で割ったあまりのビットを立てます。つまり、
NVIC->ISER[1]=1UL<<(BOARD_SW3_IRQ % 32);
でOKです。または、ビットシフトをうまく使って、
NVIC->ISER[(((uint32_t)BOARD_SW3_IRQ ) >> 5UL)]
= (uint32_t)(1UL << (((uint32_t)BOARD_SW3_IRQ ) & 0x1FUL));
でも、できます。
割込みの無効化の方法
DiableIRQという関数があるので、それにIRQ番号を渡します。
DisableIRQ(BOARD_SW3_IRQ); // BOARD_SW3_IRQはIRQ番号
別の方法はICERレジスタを使います。設定方法はISERと同じです。
NVIC->ICER[1]=1UL<<(BOARD_SW3_IRQ % 32);
もちろん、これでもいいです。
NVIC->ICER[(((uint32_t)BOARD_SW3_IRQ) >> 5UL)]
= (uint32_t)(1UL << (((uint32_t)BOARD_SW3_IRQ) & 0x1FUL));
割込みの有効・無効のレジスタは、ISERとICERで別々に定義されてます。ISERに0を書き込んでも無効にはならないし、逆もしかり、ICERに0を書き込んでも割り込みは有効化されません。
プライオリティの変更
プライオリティは、0~15までの16段階で設定できます。数値が低い方が優先順位は高くなります。優先順位の設定はNVIC_SetPriorityを使えば、簡単に設定できます。
設定されている優先順位はNVIC_GetPriorityで、確認できます。
uint8_t priority;
priority=NVIC_GetPriority(BOARD_SW3_IRQ);
PRINTF("NVIC Priority = %d\r\n",priority);
NVIC_SetPriority(BOARD_SW3_IRQ,5U);
priority=NVIC_GetPriority(BOARD_SW3_IRQ);
PRINTF("NVIC Priority = %d\r\n",priority);
実行結果は下のとおりです。デフォルトのLv0からLv5に変更しました。
サンプルケース
最初のサンプルコードを編集して、SWボタンを押すと、無限ループにはいるようにコードを変更します。
void BOARD_SW3_IRQ_HANDLER(void)
{
PRINTF("test\r\n");
// オレンジLED
GPIO_PortToggle(BOARD_LED2_GPIO, 1u <<
BOARD_LED2_GPIO_PIN);
GPIO_PortClearInterruptFlags(BOARD_SW3_GPIO, 1U <<
BOARD_SW3_GPIO_PIN);
while(1){}
}
Lowpowerタイマーを使用して、赤色のLEDを点滅させるコードを追加します。
void LPTMR0_HANDLER(void)
{
LPTMR_ClearStatusFlags(LPTMR0, kLPTMR_TimerCompareFlag);
lptmrCounter++;
if (lptmrCounter % 500 ==0)
{
GPIO_PortToggle(BOARD_LED_GPIO, 1u << BOARD_LED_GPIO_PIN);
}
__DSB();
__ISB();
}
下が結果です。当たり前ですが、何もしなければ、赤色LEDは点滅しますが、SWをおすと、無限ループにはいり、上のLowpowerタイマの例外処理は実行されなくなり、LEDは点滅しなくなります。
NVIC_SetPriority(BOARD_SW3_IRQ,5U)で、SWのプライオリティを下げた時の動作が以下です。
Preemptionにより、SWの例外処理中にも優先度の高いLowpowertimerの割り込み処理が優先されて、Lチカが止まることがなくなりました。
割り込み処理の説明は、とりあえず、こんなところです。
それでは、Happy NVIC 🙂
教えてくれてありがとう