1. 概要
APIのパラメーターチェックなどでassertを使うことがあります。しかしPlatformIOとrp2040(フレームワーク:Arduino)を使用したプログラムにおいては、不便なことがあります。それはassertion-failが発生した時、エラーメッセージがUART0のみに出力されることです。
UART0にシリアルモニタを接続するか、デバッガー(J-Linkなど)を接続している場合は、何が起こったかを把握できます。しかし、USBシリアル接続では何も表示されません。
このため、USBシリアル接続のみでエラー発生時の情報を取得する方法を実装してみました。
2. assertの実装
assertは、ファイルC:\Users\%USERNAME%\.platformio\packages\toolchain-gccarmnoneeabi@1.90201.191206\arm-none-eabi\include\assert.hの16~17行付近に、以下のように定義されています。
# define assert(__e) ((__e) ? (void)0 : __assert_func (__FILE__, __LINE__, \
__ASSERT_FUNC, #__e))
assertは、関数__assert_func()
を呼び出しています。
次に関数__assert_func()
の定義を確認します。以下の様に定義されています。
USBシリアルに出力するためには、以下の3点を変更する必要があります。
- 割り込みハンドラから呼び出された場合は、既存の処理を行う →①
- 出力先を標準出力からUSBシリアルに変更する →②
- USBシリアル出力処理が実行できるよう、
_exit()
を呼び出さず1、delay()
を入れた無限ループとする →③
※これにより、USB経由のリセットも有効となり、PlatformIOからの書き込みができるようになるはずです。
// incorrect warning from GCC 6
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsuggest-attribute=format"
void __assert_func(const char *file, int line, const char *func, const char *failedexpr) {
weak_raw_printf("assertion \"%s\" failed: file \"%s\", line %d%s%s\n",
failedexpr, file, line, func ? ", function: " : "",
func ? func : "");
_exit(1);
}
上位のソースは、以下のリンク先にあります。
https://github.com/raspberrypi/pico-sdk/blob/f396d05f8252d4670d4ea05c8b7ac938ef0cd381/src/rp2_common/pico_runtime/runtime.c#L262-L271
3. 代替機能の実装
先の関数__assert_func()
は、libmbed.aに入っています。libmbed.aに入っているオブジェクトファイルはすべてリンク対象となるため、同名の__assert_func
という関数を追加するとシンボルが重複してしまいます。このためgnu ldが持っている--wrapオプションを使用します。
platformio.iniに以下の行を追加します。
build_flags = -Wl,--wrap=__assert_func
これにより、もともと存在している_assert_funcは、__real___assert_funcという名前で呼び出すことができます。一方、新たに作る関数は__wrap___assert_funcという名前で定義しておきます。
関数__assert_func()を以下のように実装します。
#include <Arduino.h>
#include <platform/mbed_critical.h>
extern "C" {
extern void __real___assert_func(const char *file, int line, const char *func, const char *failedexpr);
}
extern "C" void
__wrap___assert_func(const char *file, int line, const char *func, const char *failedexpr)
{
char buf[128];
if (core_util_is_isr_active()) { // 割り込み処理中かを判断
__real___assert_func(file, line, func, failedexpr); // ①既存の処理を呼び出す
} else {
snprintf(buf, sizeof(buf), "assertion \"%s\" failed: file \"%s\", line %d%s%s\n",
failedexpr, file, line, func ? ", function: " : "",
func ? func : "");
Serial.print(buf); // USBシリアルに出力②
for (;;) { // 無限ループ③
delay(1000);
}
}
}
4. 動作確認
以下のようなテストプログラムを作成し、実行してみます。
#include <Arduino.h>
void
setup()
{
while (!Serial) {
;
}
}
void
loop()
{
assert(false);
}
実行すると、以下のようにメッセージが表示されました。
assertion "false" failed: file "src\main.cpp", line 14, function: void loop()
この状態で、PlatformIOから書き込みができるることも確認できました。
5. 課題
rp2040はArmv6-Mアーキテクチャなので、アラインされていないデータをアクセスすると、アボートが発生します。このときにもエラー発生時の情報を出力できる方法を検討していこうと思っています。
-
_exit()を呼び出すと、割り込み禁止状態で停止してしまうためです。 ↩