例外・フォルト処理(HardFault)
「HardFaultで固まった」の原因を数分で特定できるようになる。Cortex-Mの例外モデルとフォルトステータスレジスタ、スタックフレーム解析、復旧・リセットの定石を原理から押さえ、無限ループのデバッグから脱出できる。
- 1.Cortex-Mの例外は番号付きで管理され、フォルト(HardFault/MemManage/BusFault/UsageFault)もその一種。設定可能フォルトを有効化しなければ全てHardFaultへ昇格(escalation)する。
- 2.原因はフォルトステータスレジスタで特定する。CFSR(MMFSR/BFSR/UFSR)とHFSRでどの種別かを読み、有効ならMMFAR/BFARに違反アドレスが入る。
- 3.例外時にハードウェアが積む8ワードのスタックフレームからフォルトを起こした命令のPCを復元できる。復旧は困難なことが多く、安全側の設計としてログ保存後にリセットするのが定石。
「固まった」の中身を開ける
組み込み開発で最も心が折れるのが、動いていたファームウェアが突然無反応になり、デバッガで止めると見覚えのないアドレスの無限ループに落ちている——という状況です。その正体の多くが フォルト例外 です。ヌルポインタ参照、未整列アクセス、存在しない番地へのアクセス、スタックオーバーフローといったプログラムの不正が、Arm Cortex-M では例外という形でCPUに捕捉され、既定では HardFault_Handler へ飛びます。多くの雛形ではこのハンドラが while(1); になっているため、原因を何も残さず「ただ固まった」ように見えるのです。
本稿は、この不透明な停止を「どの種別のフォルトが、どの命令で、どのアドレスに触れて起きたか」まで分解する方法を、Cortex-M の例外モデルから追います。例外の入口である割り込み全般は /embedded/interrupt-handling-isr/ 、NVICを含むコア構成は /embedded/microcontroller-architecture/ を前提とします。
Cortex-Mの例外モデル
Cortex-M では、割り込み(IRQ)もシステム例外もフォルトも、すべて 例外番号 で一元管理されます。番号1がリセット、2がNMI、3がHardFault、そして4〜6が設定可能フォルト、11がSVCall、14がPendSV、15がSysTick、16以降が外部割り込みです。各例外にはベクタテーブル上のエントリ(ハンドラ先頭アドレス)と優先度があり、NVICが優先度に従って受理・ネストを裁きます。
フォルト系の例外は次の4つです。上位3つ(MemManage/BusFault/UsageFault)は 設定可能フォルト(configurable fault) と呼ばれ、SCB->SHCSR の対応ビットで個別に有効化できます。有効化しないと、これらは発生した瞬間に HardFault へ格上げされます。
| 例外 | 番号 | 主な原因 | 備考 |
|---|---|---|---|
| HardFault | 3 | 他フォルトの昇格、ベクタ読み出し失敗など | 常に有効。最高優先度の固定フォルト |
| MemManage | 4 | MPU保護違反、実行不可領域での命令実行 | MPU利用時に重要。SHCSRで有効化 |
| BusFault | 5 | 存在しない番地アクセス、バスエラー応答 | 不正ポインタや未実装ペリフェラル |
| UsageFault | 6 | 未定義命令、未整列アクセス、ゼロ除算 | DIV_0_TRP/UNALIGN_TRPで挙動制御 |
注意すべきは、Cortex-M0/M0+ にはそもそも設定可能フォルトが無く、あらゆるフォルトが HardFault だけで表現される点です。原因の細分化ができるのは M3/M4/M7/M33 などです。以降は主にこれらを前提に説明します。
昇格(escalation)という仕組み
なぜ多くのバグが種別を問わず HardFault に見えるのか。鍵が 昇格(escalation) です。設定可能フォルトは、次のいずれかの条件で HardFault へ引き上げられます。
- 対応する設定可能フォルトが
SHCSRで有効化されていない - そのフォルトが、自分と同じか高い優先度の例外ハンドラ実行中に発生した(フォルト処理の最中に別のフォルトが起きた等)
- 例外エントリ時のスタッキングやベクタフェッチ自体でバスフォルトが起きた
1つ目が実務で最も多い落とし穴です。起動直後に SHCSR の MEMFAULTENA/BUSFAULTENA/USGFAULTENA を立てておくだけで、次回以降フォルトが本来の種別のハンドラに入り、専用のステータスレジスタに原因が残るようになります。デバッグ時は必ずこれを有効化するのが第一歩です。
フォルトステータスレジスタで原因を特定する
種別と原因の一次情報が、システム制御ブロック(SCB)内の一連のステータスレジスタに刻まれます。中心が CFSR(Configurable Fault Status Register, 0xE000ED28) で、1つの32ビットレジスタが3つの領域に分かれています。
| レジスタ | アドレス | 内容 |
|---|---|---|
| CFSR | 0xE000ED28 | 設定可能フォルトの原因ビット群(下記3つを内包) |
| └ MMFSR | CFSR[7:0] | MemManage。IACCVIOL/DACCVIOL/MSTKERR/MMARVALID等 |
| └ BFSR | CFSR[15:8] | BusFault。IBUSERR/PRECISERR/IMPRECISERR/BFARVALID等 |
| └ UFSR | CFSR[31:16] | UsageFault。UNDEFINSTR/INVSTATE/UNALIGNED/DIVBYZERO等 |
| HFSR | 0xE000ED2C | HardFault。FORCED(昇格)/VECTTBL(ベクタ読出失敗) |
| MMFAR | 0xE000ED34 | MemManage違反アドレス(MMARVALID=1のとき有効) |
| BFAR | 0xE000ED38 | BusFault違反アドレス(BFARVALID=1のとき有効) |
読み方の原理はこうです。まず HFSR を見て FORCED が立っていれば、それは昇格された HardFault なので、真の原因は CFSR 側にあります。次に CFSR の3領域を調べ、どのビットが立ったかで種別と原因を絞ります。そして MMARVALID または BFARVALID が立っていれば、それぞれ MMFAR/BFAR に 違反したアドレスそのもの が入っています。これが得られれば「どの番地に触れて落ちたか」が確定します。
BusFault には PRECISERR(精密)と IMPRECISERR(不精密)があります。精密フォルトはフォルトを起こした命令で正確に例外へ入るため、スタックされたPCが原因命令を直接指します。一方、不精密フォルトはストアバッファ等でアクセスが遅延して完了するため、例外に入った時点のPCは原因命令とずれており、BFARの内容も無効になりがちです。不精密が出たら、書き込みバッファを疑い、疑わしい箇所の直後にデータ同期バリア(DSB)を挿んで精密化してから追うのが定石です。
スタックフレーム解析
原因アドレスと並んで重要なのが「どの命令が」です。Cortex-M は例外エントリ時、ハードウェアが自動で 8ワードのスタックフレーム を積みます。順に R0, R1, R2, R3, R12, LR, PC, xPSR で、この中の PC が、フォルトを起こした(あるいは次に実行するはずだった)命令のアドレス です。ここからソースの該当行を逆引きできます。
問題は、このフレームがどのスタックに積まれたかです。Cortex-M はスタックポインタを2本(メイン MSP とプロセス PSP)持ち、例外発生時にどちらが使われていたかは、ハンドラ入口で LR に入る EXC_RETURN 値のビット2で判別できます。RTOS ではタスクが PSP、ハンドラが MSP を使う構成が一般的なので、この判別を誤ると別のスタックを読んで見当違いのPCを拾います。
// HardFaultハンドラの入口(アセンブリ): 使用中のSPを引数に渡す
// EXC_RETURN のビット2が1ならPSP、0ならMSPが使われていた
void HardFault_Handler(void) {
__asm volatile (
"tst lr, #4 \n" // ビット2を検査
"ite eq \n"
"mrseq r0, msp \n" // 0: メインスタック
"mrsne r0, psp \n" // 1: プロセススタック
"b hardfault_report \n" // r0=フォルト時のスタックポインタ
);
}
// C側: 積まれた8ワードから各レジスタを取り出す
void hardfault_report(uint32_t *frame) {
uint32_t stacked_pc = frame[6]; // フォルト命令のアドレス
uint32_t stacked_lr = frame[5]; // 呼び出し元
uint32_t stacked_psr = frame[7];
uint32_t cfsr = *(volatile uint32_t *)0xE000ED28;
uint32_t hfsr = *(volatile uint32_t *)0xE000ED2C;
uint32_t bfar = *(volatile uint32_t *)0xE000ED38;
// stacked_pc をマップファイルと突き合わせれば原因行が判る
// cfsr のビットで種別、bfar/mmfar で違反アドレスを確定
save_crash_log(stacked_pc, stacked_lr, cfsr, hfsr, bfar);
system_reset();
}
stacked_pc の値を、ビルドで出力される マップファイル(.map)や逆アセンブル と突き合わせれば、どの関数のどの命令かが判ります。CFSR で種別、BFAR/MMFAR でアドレスが確定していれば、たとえば「PC=関数fooの某行、BFAR=0x00000000、DACCVIOL/PRECISERR」から『fooでヌルポインタに書き込んだ』と一意に結論できます。デバッガに繋げない量産機でも、この情報を不揮発領域に残せば現地の再現待ちなしに原因追及できます。
JTAG/SWDが繋がる開発段階では、ハンドラ内で __BKPT(0)(ブレークポイント命令)を実行すると、その場でデバッガが停止します。停止後にCFSR・HFSR・BFAR、そして上記スタックフレームをウォッチ式で開けば、リセットを挟まずに原因を観察できます。IDEによってはフォルト時のレジスタを一覧表示するフォルトアナライザ機能も備えます。
復旧するか、リセットするか
原因が判ったとして、その場で 復旧 できるかは別問題です。原則として、フォルトはプログラムの前提が崩れた状態であり、安全に処理を続行できる保証はありません。無理に元へ戻すと、破壊されたデータのまま走り続けてより深刻な誤動作を招きます。
例外的に復旧が現実的なのは、フォルト要因が局所的で、続行に必要な状態が壊れていないと確信できる場合だけです。たとえば、意図的にプローブした番地が未実装で BusFault になった、といったケースでは、スタックされた PC を次の命令へ進めて例外復帰させる手はあります。ただしこれは高度で危険な手法で、汎用の対策にはなりません。
HardFaultハンドラを空の while(1){} や、フラグだけ立てて return する実装にすると、原因が一切残らないまま停止するか、あるいは壊れた状態で暴走します。最低限、フォルトステータスとスタックPCを不揮発領域へ記録し、ウォッチドッグと連携して確実にリセットへ導くべきです。「とりあえず動かす」ためにフォルトを無視すると、フィールドでの再現困難な不具合として跳ね返ってきます。
そこで量産システムの定石が 「記録してからリセット」 です。ハンドラで CFSR・HFSR・違反アドレス・スタックPC を一時保持し、可能なら不揮発メモリ(内蔵フラッシュの専用セクタやバックアップレジスタ)へ書き出したうえで、ソフトウェアリセットを発行してクリーンな初期状態から再起動します。リセット後にログを吸い上げれば、フィールドでのフォルトも解析できます。
// SCB->AIRCR経由のソフトウェアリセット(要VECTKEYの書き込み)
#define SCB_AIRCR (*(volatile uint32_t *)0xE000ED0C)
void system_reset(void) {
__DSB(); // 先行するログ書き込みを完了させる
SCB_AIRCR = (0x5FAUL << 16) | (1UL << 2); // VECTKEY | SYSRESETREQ
for (;;) { } // リセットが効くまで待つ
}
リセットを最後の砦として担保するのが ウォッチドッグタイマ です。フォルトでハンドラすら正常に走らない、あるいはソフトリセットが失敗する事態でも、ウォッチドッグが再ロードされなければ一定時間後に強制リセットがかかります。リセット後は、リセット要因レジスタでウォッチドッグ由来かどうかを判定し、必要ならセーフモードで起動する、といった設計が堅牢です。ウォッチドッグの詳細は /embedded/watchdog-timer/ を参照してください。
未然に防ぐ設計
事後解析だけでなく、フォルトを起こしにくくする設計も重要です。BusFault の多くは不正ポインタやメモリマップ外アクセスに由来するため、ペリフェラルやメモリの正しい番地・アクセス幅を守ることが基本です(メモリマップの原理は /embedded/memory-mapped-io/ 参照)。UsageFault のうち未整列アクセスは、UNALIGN_TRP を立てておけば早期に検出でき、ゼロ除算も DIV_0_TRP で捕捉できます。
MPU(メモリ保護ユニット)を有効にすれば、スタック末尾の直下に非アクセス領域(ガードページ)を置いてスタックオーバーフローを MemManage フォルトとして即座に捕らえる、といった能動的な防御も可能です。これはヌルポインタ参照(0番地)を保護領域にして検出する用途にも使えます。
「全部HardFaultになる理由」は『設定可能フォルトをSHCSRで有効化していない、または高優先度ハンドラ中の再フォルトによる昇格(escalation)』。「原因の特定手順」は『HFSRのFORCED確認→CFSR(MMFSR/BFSR/UFSR)で種別→MMARVALID/BFARVALIDが立てばMMFAR/BFARで違反アドレス→スタックフレームのPCで命令』。「復旧方針」は『続行は原則危険。ステータスとPCを保存してからソフトリセット、最終保険にウォッチドッグ』と押さえます。
まとめ
Cortex-M のフォルトは、番号付き例外モデルの一部として体系立てられており、見かけ上の HardFault の裏には MemManage/BusFault/UsageFault という原因の細分化が用意されています。デバッグの起点は、設定可能フォルトを有効化して本来の種別へ落とすこと。そのうえで HFSR と CFSR で種別を、MMFAR/BFAR で違反アドレスを、スタックフレームの PC で命令を確定すれば、「ただ固まった」は「fooのヌルポインタ書き込み」まで分解できます。復旧は原則あきらめ、ログを残してからリセットし、ウォッチドッグで最後の砦を固める——この一連が、フィールドで壊れないファームウェアの土台になります。
組込み・IoT Article
例外・フォルト処理(HardFault)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
組み込み
比較で見る軸
難易度: advanced / カテゴリ: 組込み・IoT / タグ数: 6
導入後に効く点
原因はフォルトステータスレジスタで特定する。CFSR(MMFSR/BFSR/UFSR)とHFSRでどの種別かを読み、有効ならMMFAR/BFARに違反アドレスが入る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 組込み・IoT
- タグ数
- 6
判断チェックリスト
- 自社の用途が「組み込み / Cortex-M」に近いか確認する。
- 強みである「Cortex-Mの例外は番号付きで管理され、フォルト(HardFault/MemManage/BusFault/UsageFault)もその一種。設定可能フォルトを有効化しなければ全てHardFaultへ昇格(escalation)する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。