タクテク
昭和歌謡好き大学生の雑記
プロフィール

プロフィール画像

T.Ueda

電子機器と昭和歌謡を愛する理系大学生

ARMマイコンの割込みを理解しよう

日付:2023/03/03

1:そもそも「割込み」とは?

 筆者はエンベデッドシステムスペシャリストの資格を持っているので肩書上は「組み込みシステム開発のスペシャリスト」ですが、実態はfor文の構文すら怪しいプログラミング超初心者なんですよね。というわけで前回のARMマイコンでのPWM出力の学習に引き続き、今回は組み込み開発では欠かせない割込み処理について、マニュアルを見ながらステップバイステップで学習していきます。

 そもそも「割込みって何?」という話ですが、これはゲーム制作のような一般的なプログラミングでは意識されることの少ないハードウェアレベルでのプログラミング特有の概念なのでおさらいもかねて軽く説明しておきます(割込みといってもいろいろありますが、今回は外部入力による割込みを対象としています)。

マイコンとスイッチが接続された回路

 例えば、こんな感じでマイコンに接続されたスイッチが押されたことを読み取りたい場合を想定します。人間だったら「そんなもん見ればわかるだろ」と言いたいことですが、残念ながらコンピュータは決められた動作を行うだけでそんなに都合よく動作してくれないわけです。

 まず考えられるのは定期的にボタンが押されているかを確認する処理を行うという方法。いわゆる「ポーリング」というやつで、頻繁にボタンの状態を確認する動作を行えばボタンが押されたことを判定できるというわけです。
 でも、これってスイッチが押されているか否かに関わらず頻繁に「スイッチを確認する処理」が入るので全く賢くない方法ですよね。

割込みコントローラ

 そこで出てくるのが割込みです。これは、マイコン内部にある割込みコントローラというハードウェアが割込み信号を受信すると、CPUに割込み信号を送り、その名の通り現在実行中の処理に割り込んで特別な処理を実行させるという機能。これならスイッチの状態監視をハードウェアがやってくれるのでポーリングのように無駄な処理が必要ないわけです。

 それ以外にも例えばこんな感じで、割込みはさまざまな場面で応用されるので組み込み開発には必須の知識です。
  • 他の機器から信号を受信したら応答する(UART割込みとか)
  • 異常な電圧が発生したらシステムを緊急停止する(外部割込みの応用)
  • 所定の時間が経過したらある処理を行う(タイマ割込み)

 ただし、割込みはハードウェアレベルの処理であるためハードウェアを意識した特殊なプログラミングが要求されます。割込みコントローラの制御方法はマイコンによって異なるのでマイコンごとにプログラミングする必要がありますし、割込みは様々な要因で発生するのでそれらを把握しつつ優先度を適切に設定する必要があります。正直言うとやりたくないのですが、組み込み開発では避けて通れない道なのでいよいよ実際のマイコンを用いて割込み処理のプログラミングを行っていきます。

2:「スイッチが押された」ことを検出するまでの長い道のり

 それでは、実際にプログラミングしていきましょう。今回は前回と同様にARM Coretex-M0を搭載したLPC1114をターゲットデバイスとして、スイッチが押される度にLEDがオン/オフするというプログラムを書いていきます。「いや、そんなもんスイッチとLEDをつなげば終わりじゃん」という話なんですが、あくまでテスト用プログラムということで…

 外部割込みを使ったサンプルプログラムがなかったので、今回も前回同様ユーザーマニュアル(LPC111x/LPC11Cxx User manual)を読み進めていきます。ユーザーマニュアルは500ページ超えなのでキーワードで検索をかけていくわけですが、今回のキーワードはこんな感じ。

  • Interruption
  • IRQ(Interruption ReQuest)
  • NVIC(Nested Vectored Interrupt Controller)
  • GPIO

InterrputionやIRQはそれぞれ割込み、割込み要求のことですが、ARMマイコン特有の単語としてNVICがあります。

NVIC CMSIS

 これは「ネスト可能割込みコントローラ」の略称で、正直どういう意味なのかはよくわからない(複数の割込みをネストできるってこと?)ですが、単に割込みコントローラの商品名みたいなもんと思っておけばOKです。ユーザーマニュアルによればCMSIS(ARMの共通ライブラリ)を使うことで割込みの有効・無効や優先順位等を簡単に設定できることがわかります。

 例えば、ある割込みを有効にする場合は

NVIC_EnableIRQ(IRQn_Type IRQn)

と書けばOKなんですが、次は「IRQn_Type IRQn」ってなに?という話になります。

NVIC IRQNum

 これもARM特有のもので、マニュアルによれば「わかりやすくするために様々な割込み要因につけられた番号」のことで、きちんと表にまとめられています(ユーザーマニュアルpp71より引用)。今回スイッチはPIO0_9ポートに接続したので、PIO0のInterruption、即ち割込み番号31を有効にすればよいことがわかります。

 じゃあ

NVIC_EnableIRQ(31)

と書けばいいじゃないかと言いたくなるところですが、これだと可読性(「31って何?」ってなる)も移植性(別のマイコンに変えたときに「31」じゃなくなったらプログラム中のすべての「31」を探し出して変更しないといけない)も最悪です。

CMSISライブラリ

 そこで、ライブラリを確認してみると割込み番号がまとめられていることがわかりました。これを使うと

NVIC_EnableIRQ(EINT0_IRQn)

と書くことができます。こうすればなんとなく意味が分かりますし、マイコンが変わった際もライブラリだけ変えればOKなわけです。これでPIO0による外部割込みが有効化されたわけですが、次はPIO0による外部割込み発生条件の設定を行っていきます。

3:GPIO割込み設定

 ここまでで何をしたかというとPIO0による外部割込みを受け付けるように設定しただけで、まだ外部割込み自体の設定は行っていないわけです。で、これの設定方法を見つけるのにかなり手間取ったのですが、GPIOの章に説明がありました。

GPIO 設定

 割込み関連のレジスタが7個もありますが、今回必要なものを1つずつ設定していきます。

 まずは、割込みを発生させる条件を設定するGPIO interrupt sense(GPIOnIS)レジスタ。これはエッジトリガかレベルトリガかを設定するレジスタですが、今回はスイッチが押されたという「変化」を検知したいのでエッジトリガに設定。エッジトリガの場合は該当するビットをゼロにするわけですが、面倒なので全部ゼロに。先ほどと同様にライブラリを使って次のように記述しました。

LPC_GPIO[0] -> IS = 0x00;

GPIO interrupt event register(GPIOnIEV)立上がりと立下がりのどちらに反応するかを決定するレジスタ。今回スイッチはプルアップされていて押されるとGNDになるので立下り検出(0)に設定します。

LPC_GPIO[0] -> IEV = 0x00;

 そして最後にGPIO interrupt mask register(GPIOnIE)。これは各ポートに対応するビットを0にするとそのポートからの割込みがマスクされる(無効になる)レジスタで、PIO0_9のみ有効にしたいのでビット演算を使って以下のように9ビット目のみを1に設定しました。

LPC_GPIO[0] -> IE = 1 << 9;

これでやっとPIO0_9のダウンエッジで割込みが発生するようになったわけです。次は割込みハンドラの実装です。

4:止まらない割込み

 長い道のりでしたが、最後に実際に割込みが発生した際の処理を受け持つ特殊な関数、割込みハンドラを実装していきます。とは言ってもライブラリ化されているので以下の関数を宣言するだけです。

void PIOINT0_IRQHandler(void)

この専用の関数を記述することで、割込みが発生すると強制的にこの関数に切り替わります。で、この関数の中にスイッチが押された時に実行してほしい処理を記述するわけですが、ここからがトラブル続きでした。最終的にできた割込みハンドラがこんな感じ。

void PIOINT0_IRQHandler(void)
{
    LPC_GPIO[0] -> IC = 1 << 9;
    LPC_GPIO[0] -> IE = 0x00;
    GPIOSetDir(2, 0, 1); // RED P2_0 out
    GPIOSetValue(2, 0, !GPIOGetValue(2,0));
    UARTSend_Str("Interrupt\n\r");
    Wait_N_Ticks(50);
    LPC_GPIO[0] -> IE = 1 << 9;
}
                    

 デバッグ用にシリアル通信を適当に突っ込んでいることは気にしないとして、全体として何をやっているかというとチャタリングを防止するために割込みを検知したらいったん割込みをマスクし、しばらく待つという処理を行っています。

 一行目の「IC」レジスタが何かということなんですが、これは「GPIO interrupt clear register」といってエッジ検知回路をリセットするためのレジスタで、ここに0を書き込んでリセットしないとエッジ検知回路が動作し続けて割込みが止まらなくなります。最初はこれに気づかずにかなり悩みました。

 そして、何より問題だったのが「Wait_N_Ticks(50);」という行。これはサンプルプログラムについていた関数を流用していて「タイマ割込み50回分待つ」という処理を行っています。つまり、割込み処理の中で割込みを使っているわけです(SysTick割込みという内蔵タイマ関連の割込み)。これが何を意味するのかというと優先順位を正しく設定しないとちゃんと動かないわけです。

 具体的にどういうことかというと、この「Wait_N_Ticks(50);」関数はタイマ割込み50回分待つという処理を行うわけですが、ここで優先順位がPIO0割込み>タイマ割込みだった場合、PIO0の割込みハンドラ実行中はタイマ割込みが実行されず、いつまで経ってもタイマ割込みが発生しないので「Wait_N_Ticks(50);」の処理が永遠に終わらなくなってしまいます。
 そこで、割込みの優先順位を0~3で設定する「NVIC_SetPriority関数」を使い、PIO0割込みの優先順位を下げたら正常に動作しました。最終的なコードはこんな感じ。

#ifdef __USE_CMSIS
#include "LPC11xx.h"
(省略)
#endif

int main(void)
{
    //Initialize
    (省略)
    //Interruption setting (PIO0_9 down edge)
    LPC_GPIO[0] -> IS = 0x00;
    LPC_GPIO[0] -> IEV = 0x00;
    LPC_GPIO[0] -> IE = 1 << 9;
    NVIC_SetPriority(SysTick_IRQn,0);
    NVIC_SetPriority(EINT0_IRQn,3);
    NVIC_EnableIRQ(EINT0_IRQn);
    while(1)
    {

    }
    return 0;
}

void PIOINT0_IRQHandler(void)
{
    LPC_GPIO[0] -> IC = 1 << 9;
    LPC_GPIO[0] -> IE = 0x00;
    GPIOSetDir(2, 0, 1); // RED P2_0 out
    GPIOSetValue(2, 0, !GPIOGetValue(2,0));
    Wait_N_Ticks(50);
    LPC_GPIO[0] -> IE = 1 << 9;
}
                    

 このプログラムで注目すべきは、main関数は無限ループに入っているのにスイッチが押されたことを検知できる点です。つまり、他の処理に専念しながらもスイッチが押された時だけ自動的に処理が切り替わるので、ポーリングより効率的なわけです。

動作確認

 まあ、こんなにいろいろ設定した結果が「スイッチを押すたびにLEDがオンオフを繰り返すだけのプログラム」なんですが、割込みに対する恐怖心(なんか違う意味に聞こえる)はかなり払拭された気がします。