割り込みベクタテーブルの仕組み

「ハンドラを書いたのに割り込みが発生しない」「ブートローダ経由で起動するとアプリケーションの割り込みが動かない」。どちらもベクタテーブルの理解で解決できる定番トラブルです。

この記事ではCortex-Mを題材に、(1) ベクタテーブルの構造、(2) 配置のルール、(3) VTORレジスタによる再配置、(4) ブートローダからアプリケーションへ安全に遷移する手順を解説します。なお、Cortex-Mの「例外(Exception)」を総称として用い、外部割り込み(Interrupt)とシステム例外(NMI・各種Faultなど)を含めて説明します。

ベクタテーブルとは:飛び先アドレスの一覧表

ベクタテーブルは、リセット・フォルト・各割り込みといった例外の要因ごとに「ハンドラの開始アドレス」を並べた表です。Cortex-Mではアドレスを書くだけでよく(ジャンプ命令を書く方式の従来コアとは異なります)、例外が発生するとハードウェアが自動的に該当ハンドラへ分岐します。

※Cortex-Mでは、テーブルに格納するハンドラアドレスのbit0がThumb状態を示すため1になっています(実アドレスはbit0を0として解釈されます)。誤ってbit0に0を設定すると、例外遷移時にINVSTATE(UsageFault/HardFault相当)となり、ハンドラ先頭でFaultに落ちます。

もうひとつCortex-M特有の重要な仕様が、テーブルの先頭ワードです。先頭はリセットハンドラではなく「メインスタックポインタ(MSP)の初期値」で、リセットハンドラのアドレスは2番目に置かれます。テーブルのサイズは「16(システム例外分)+外部割り込み本数」ワードです。この構造は後述するブートローダからのジャンプ処理でそのまま使います。

どこに配置されるのか

リセット直後、プロセッサは0x00000000(アドレス0)のベクタテーブルを参照します(多くのマイコンでは、ブート時にフラッシュ先頭が0x00000000へエイリアスされるよう構成されています。実際のフラッシュのアドレスは、ベンダによって0x08000000などの場合があります)。テーブルの実体は、スタートアップコード内に配列として定義され、リンカスクリプトで先頭セクションに配置されるのが一般的です。「自分のプロジェクトのテーブルがどこで定義され、どのアドレスに置かれているか」は、スタートアップコードとmapファイルで一度確認しておくとよいでしょう(「ブートローダとスタートアップコードの実装」「mapファイルの読み方」参照)。

「ハンドラを書いたのに呼ばれない」の正体

スタートアップコードでは、全割り込みハンドラが弱シンボル(weak)として共通のDefault_Handler(多くは無限ループ)に紐づけられています。ユーザが正しい名前で関数を定義すると、リンク時にそちらが優先されて差し替わる仕組みです。つまり関数名を1文字でも間違えると、エラーにならないままDefault_Handlerへ分岐し続けます。「割り込みは発生しているのにハンドラに来ない」「いつの間にか無限ループで止まっている」ときは、まずベクタテーブル定義の正式なハンドラ名と自分の関数名を突き合わせます。

VTORで再配置する

ベクタテーブルの参照先は、VTOR(Vector Table Offset Register)で変更できます。リセット直後のVTOR値は仕様上0x00000000ですが、マイコンのブート構成によっては実効的な参照先が異なる場合があるため、使用するデバイスのリファレンスマニュアルで確認してください。代表的な用途は次の3つです。

  • ブートローダ構成:フラッシュ後方に置いたアプリケーション側のテーブルへ切り替える(本記事のメインユース)。
  • RAMへの再配置:実行時にハンドラを差し替えたい場合や、フラッシュ停止中も割り込みを動かしたい場合に、テーブルをRAMへコピーしてVTORを向ける。
  • 複数ステージ構成:実行ステージごとに別のテーブルを持たせる。

注意点が2つあります。

  • 注意点1:再配置先のアライメント VTORで指定できるアドレスは下位ビットに制約があり、少なくとも2ⁿバイト境界に整列させる必要があります(nはコアによって異なります)。VTORのアライメント要件はコア/実装に依存するため、必ず使用するデバイスのリファレンスマニュアルに従ってください。迷った場合は、テーブルサイズ以上かつ、多くの環境で安全側となる少なくとも256バイト境界に整列させます。たとえば外部割り込みが32本の構成なら、エントリ数は16+32=48ワード(192バイト)となるため、0x100境界に整列させます。
  • 注意点2:コアによるVTORの有無 Cortex-M0にはVTORがありません。Cortex-M0+は実装オプションのため、搭載の有無は使用するチップのデータシートとリファレンスマニュアルで必ず確認してください。対策として、VTORがない場合はSRAMリマップ機能などベンダ固有の代替手段を使います(各マイコンベンダから実装事例が公開されている場合があります)。

ブートローダからアプリケーションへ遷移する定石

ブートローダがアプリケーションを検証したあと、アプリケーションへ制御を移す処理は次が定石です。ベクタテーブル先頭の構造(MSP初期値+リセットベクタ)をそのまま利用します。※下記は最小構成例です。本番の実装ではコードブロック後の注意事項を必ず確認してください。

typedef void (*app_entry_t)(void);
void jump_to_application(uint32_t app_base)
{
    uint32_t sp = *(volatile uint32_t *)(app_base);       /* 先頭: MSP初期値    */
    uint32_t pc = *(volatile uint32_t *)(app_base + 4u);  /* 2番目: リセット先  */
    __disable_irq();              /* 遷移中の割り込みを止める           */
    /* ブートローダで使ったペリフェラルと割り込み要因をここで後処理する */
    SCB->VTOR = app_base;         /* ベクタテーブルをアプリケーション側へ切り替え */
    __DSB(); __ISB();             /* VTORの反映を保証(環境に応じて)            */
    __set_MSP(sp);                /* スタックポインタを初期化                    */
    ((app_entry_t)pc)();          /* アプリケーションのリセットハンドラへ        */
}

割り込みを止めずに遷移すると、切り替えの途中で割り込みが入り、不整合な状態のままハンドラへ分岐してハードフォルトに陥る可能性があります。割り込み禁止、後処理、VTOR設定、MSP設定、ジャンプという順序を守り、割り込みの再許可はアプリケーション側の初期化でおこないます。また、RTOS動作中のタスクから遷移する場合は、プロセススタック(PSP)ではなくメインスタック(MSP)に戻してから遷移する必要がある点にも注意します。後処理では、__disable_irq() に加えてSysTickの停止、NVIC(Nested Vectored Interrupt Controller)の割り込みの無効化(Disable)と保留(Pending)のクリアを確実におこない、再初期化はアプリケーション側で実施すると安全です。

※このコードは最小構成例です。MPU・キャッシュ・クロックの設定、使用中のペリフェラル、RTOSの有無によって追加の処理が必要になります。また、__set_MSP() を呼んだあとのC関数呼び出しは、コンパイラが生成するコードがスタックを参照する可能性があります。本番の実装では、MSPを書き換えたあとのジャンプをインラインアセンブリの分岐命令で記述することが推奨されます。

※アプリケーション側のリセットベクタは、通常Thumbビット(bit0=1)込みで格納されているため、そのまま関数ポインタとして呼び出せます。環境によってはジャンプ先アドレスのbit0の扱いに注意が必要です。

トラブル時のチェックリスト

  • まず疑うべきはVTORの切り替え忘れと、ハンドラ名の不一致(weak→Default_Handler)です。 アプリケーションの割り込みが動かない:VTORの切り替え忘れ。ブートローダのテーブルを向いたままになっていないか。
  • アプリケーション起動直後にハードフォルト:割り込み有効のまま遷移/アプリケーション側リンカのROM開始アドレスと書き込みアドレスの不一致。
  • 特定の割り込みだけ来ない:ハンドラ名のタイプミスでDefault_Handler行き。
  • 再配置後に動作が不安定:再配置先のアライメント不足。

まとめ

ベクタテーブルは「先頭がスタック初期値、2番目がリセット」という構造と、VTORによる再配置の2点を押さえれば、ブートローダ構成もファームウェア更新も自信を持って設計できます。OTA更新を実装する際の土台になる知識です。

※Cortexは、Arm Limitedの登録商標です。

組み込みソフトの世界 トップへ戻る