※機能を追加した以下の記事もご参照ください。
rp2040で例外発生時にスタックトレースを表示する
1. はじめに
RP2040でソフトウェア開発をしているとき、プログラムが突然停止してしまった経験はありませんか? その原因の一つとして、Hard Faultが発生し割り込みを受け付けない状態で例外ハンドラから復帰しない状態になってしまうことがあります。
デバッグプローブ(Picoprobe など)を使用している場合、デバッガに制御が戻るため、例外が発生したことがすぐに分かります。しかし、デバッグプローブを使わずに開発していると、何が起こったのかを知る手段がありません。
そこで、RP2040のSDKが提供する「例外ハンドラ登録機能」を活用し、Hard Faultが発生した際にPC(プログラムカウンタ)などのレジスタ情報を表示し、問題の発生個所を特定できるようにしました。
また、Arduinoでは、Hard Fault発生時にPCの値からどこでエラーが起こったのかを特定する容易な方法が見つけられませんでした。一方、PlatformIO(earlephilhower版)では、esp32_exception_decoder を利用することで簡単にエラーの発生場所を特定できます。本記事では、その方法についても解説します。
2. Hard Faultとは
通常処理または例外処理中にエラーが発生したことによって、発生する例外です。Cortex-M0/M0+においては、以下のとうな要因で発生します。
- アラインメントエラー
- BKPT命令の実行(デバッガ未使用時)
- 未定義命令の実行
3. 作成するハンドラの処理内容
ハンドラの処理内容は以下のようになっています。
- 例外発生時の情報を取得する
- ユーザが指定した関数に制御を移す
Hard Fault発生時の情報表示はユーザが作成するようにしました。これはArduino環境およびpico-SDKのどちらの環境でも動作できるように考慮したためです。
例外から復帰させているのは、USBによるシリアル出力を可能とするためです。
レジスタ値の出力の他に、esp32_exception_decoderが解釈できるような情報も出力します。出力する情報はpcとlrのみですが、例外が発生した個所の特定に役立つかと思います。
4. 実装したコードとテストプログラム
実装したコード:
ヘッダファイル
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t psr;
uint32_t sp;
} fault_regs_t;
extern void registerHardfaultHandler(void (*func)(fault_regs_t *regs));
#ifdef __cplusplus
}
#endif
ハンドラ部
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <hardware/exception.h>
#if PICO_RP2040
#include <RP2040.h>
#else
#include <RP2350.h>
#endif
#include "HardFaultHandler.h"
static fault_regs_t regs;
static void (*handler)(fault_regs_t *regs) = NULL;
static void
exceptionHandlerEntry(void)
{
uint32_t *sp;
uint32_t lr = (uint32_t)__builtin_return_address(0);
if ((lr & 4) == 0) {
sp = (uint32_t *)(__get_MSP() + 4);
} else {
sp = (uint32_t *)__get_PSP();
}
regs.r0 = sp[0];
regs.r1 = sp[1];
regs.r2 = sp[2];
regs.r3 = sp[3];
regs.r12 = sp[4];
regs.lr = sp[5];
regs.pc = sp[6];
regs.psr = sp[7];
regs.sp = (uint32_t)sp;
if ((regs.psr &0x1f) != 0) {
// 例外処理中にfaultが発生」した場合は、
// 表示ができない可能性があるため、ここで停止させる。
for (;;) {
;
}
}
// 例外からの戻りアドレスを変更
sp[6] = ((uint32_t)handler) & 0xfffffffeU;
// レジスタの値を格納した構造体のアドレスを第一引数として渡す
sp[0] = (uint32_t)®s;
}
void
registerHardfaultHandler(void (*func)(fault_regs_t *regs))
{
exception_set_exclusive_handler(HARDFAULT_EXCEPTION, exceptionHandlerEntry);
handler = func;
}
テスト用のプログラム
#include <Arduino.h>
#include <stdlib.h>
#include <hardware/exception.h>
#include "HardFaultHandler.h"
static void __attribute__((noreturn))
showFaultStatus(fault_regs_t *regs)
{
Serial.printf("******** Hard fault (Core %d) ********\n", regs->core_num);
Serial.printf("r0: %08x r1:%08x r2:%08x r3: %08x\n", regs->r0, regs->r1, regs->r2, regs->r3);
Serial.printf("r12:%08x lr:%08x pc:%08x psr:%08x\n", regs->r12, regs->lr, regs->pc, regs->psr);
Serial.printf("sp: %08x\n", regs->sp);
Serial.printf("******** Halted ********\n\n");
Serial.printf("Backtrace: 0x%08x 0x%08x\n", regs->pc, (regs->lr) & ~0x00000001U);
Serial.printf("\n\n");
while (true) {
delay(5000);
}
}
void
setup()
{
while (!Serial)
{
;
}
delay(1000);
Serial.printf("build [%s %s]\n", __DATE__, __TIME__);
registerHardfaultHandler(showFaultStatus);
}
void
loop()
{
__breakpoint(); // 38行目
}
5. esp32_exception_decoderを使えるようにする
esp32_exception_decoderを使うため、platformio.iniに以下の行を追加してみます。
monitor_filters = esp32_exception_decoder
monitorを実行すると、以下の警告が表示されました。
Warning! Skipping unknown filters `esp32_exception_decoder`. Known filters are `colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time`
monitor_filtersで指定可能なフィルタは、以下のリンク先に説明があります。
pio device monitor
私の環境では、フィルタのファイルは以下の場所にありました。platformやpackageが異なると、フィルタの検索対象から除外されるようです。
"C:\Users\%USERNAME%\.platformio\platforms\espressif32\monitor\filter_exception_decoder.py"
PlatformIOのプロジェクト直下にmonitorというディレクトリを作り、その中にこのフィルを置くことで、上記警告は表示されなくなりました。
6. 実行結果
実行すると、以下のように例外発生時のレジスタ情報が表示されます。
******** Hard fault (Core 0) ********
r0: 100030c5 r1:100032e1 r2:d0000150 r3: 20001d9c
r12:20000275 lr:10003b63 pc:100032a0 psr:01000000
sp: 20041fe0
******** Halted ********
Backtrace: 0x100032a0 0x10003b62
#0 0x100032a0 in loop at C:\Users\%USERNAME%\.platformio\packages\framework-arduinopico/pico-sdk/src/rp2040/pico_platform/include/pico/platform.h:126
(inlined by) loop at src/main.cpp:38
#1 0x10003b62 in main at C:\Users\%USERNAME%\.platformio\packages\framework-arduinopico\cores\rp2040/main.cpp:150 (discriminator 1)
例外発生個所とその呼び出し元のソースファイル名と行番号が表示されました。
今までは、Hard Fault発生時にはBOOTボタンを押してリセットする必要がありました。しかしこの実装では、例外から復帰し割り込みが受け付けられる状態で待機しているため、通常の手順でフラッシュメモリを書き換えることができるようになっています。