14.1.1.4. ファームウェアのデバッグ

オンハードウェアデバッグとは、プロセッサを停止し、Cソースにブレークポイントを設定し、シングルステップ実行を行い、変数、メモリ、レジスタ、ペリフェラルを検査することを意味します。すべてVS Code内から行えます。これには3つのものが必要です。デバッグビルドSWDデバッグプローブ(Segger J-Link)、そしてJ-Link GDBサーバーに対して arm-none-eabi-gdb を駆動する Cortex-Debug 拡張機能です。

14.1.1.4.1. デバッグ用にビルドする

ターゲットは常に DEBUG=1 で再ビルドしてください:

make -j$(nproc) TARGET=<TARGET> DEBUG=1

リリース(DEBUG=0)イメージは -O2 でコンパイルされます。デバッガでは多くの変数について <optimized out> が表示され、インライン化された関数は呼び出し元に統合され、ステップ実行は予測できない形で飛び回ります。DEBUG=1 では -Og -ggdb3 でビルドされ、カメラ上で起動しつつデバッグ可能になります。デバッガに指定するELFは次のとおりです:

build/<TARGET>/bin/firmware.elf

(Alif AE3の場合は、高性能コアである build/OPENMV_AE3/bin/firmware_M55_HP.elf をデバッグします。)

14.1.1.4.3. VS Code Cortex-Debugのセットアップ

リポジトリ内に .vscode/launch.json を作成します。最も単純なケース、つまりVS Code、J-Link、ビルドがすべて 同じ Linux / macOSマシン上にある場合は servertype: "jlink" を使用します。これによりCortex-Debug自身がJ-Link GDBサーバーを起動します:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "OpenMV J-Link",
      "type": "cortex-debug",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf",
      "servertype": "jlink",
      "device": "STM32H743VI",
      "interface": "swd",
      "runToEntryPoint": "main",
      "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin",
      "gdbPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin/arm-none-eabi-gdb"
    }
  ]
}

ボードに合わせて executabledevice を変更してください(上の表を参照)。F5 を押すとビルド・フラッシュ・実行が行われ、main で停止します。

Tip

デバッグを開始するたびに自動的に再ビルドするには、.vscode/tasks.json にビルドタスクを追加し、起動構成から "preLaunchTask" で参照します。例えば、make -j$(nproc) TARGET=OPENMV4 DEBUG=1 を実行する "build-firmware" という名前のタスクと、上記の構成に "preLaunchTask": "build-firmware" を追加すると、F5 で再ビルド、フラッシュ、デバッガの起動が1ステップで行われます。

警告

Cortex-Debugには arm-none-eabi-gdb が必要です。これはSDKの ~/openmv-sdk-<version>/gcc/bin に同梱されていますが、デフォルトでは PATH 上に ありません。そのため、デバッグは "GDB executable 'arm-none-eabi-gdb' was not found" で失敗します。これを修正するには、上記のように armToolchainPath / gdbPath を設定するか、~/openmv-sdk-<version>/gcc/binPATH に追加します(その場合 printenv PATH でそれが一覧に表示されるはずです)。

14.1.1.4.4. ペリフェラルレジスタビュー(SVD)

Cortex-DebugにCMSIS SVD ファイルを指定すると、ペリフェラルレジスタ(タイマー、DMA、カメラインターフェースなど)を名前とビットフィールド単位でデコードしたビューが得られます:

"svdFile": "/path/to/STM32H743.svd"

STM32とMIMXRTの場合、SVDはST / NXPのCMSISパックまたはCortex-Debug SVDレジストリから入手します。Alif SVDはファームウェアリポジトリの lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/ にベンダリングされています(AE3 HPコアには ..._CM55_HP_View.svd を使用します)。

14.1.1.4.6. gdbrunnerによるコマンドラインデバッグ

組み込みターゲットに対してGDBセッションを手作業でセットアップするのは5ステップの作業です。正しいデバイス、ポート、インターフェースフラグを指定して1つのウィンドウでJ-Link / ST-Link GDBサーバーを起動し、Waiting for GDB connection が表示されるのを待ち、2つ目のウィンドウで arm-none-eabi-gdb を実行し、target remote localhost:<port> と入力し、gdbにELFを指定します。gdbセッションが終了したら、サーバーウィンドウを終了させるのを忘れないようにします。gdbrunner は、これらすべてを1つのフォアグラウンドコマンドにまとめる小さなCLIです。OpenMV SDKのPython環境に同梱されているため、インストールするものはありません。通常のエントリポイントはファームウェアリポジトリの make debug ターゲットです:

make -j$(nproc) TARGET=<TARGET> DEBUG=1 debug

これはボードの構成からデバッガ引数(J-Linkデバイス名、および必要に応じてST-Link外部フラッシュローダー)を使ってgdbrunnerを実行し、SDKの arm-none-eabi-gdb がすでに PATH 上にある状態にします。デフォルトのバックエンドはJ-Linkです。make DEBUGGER=STLINK debug を使えば、代わりにST-Linkプローブで動作します。

gdbrunnerは直接呼び出すこともできます(SDKの外部では pip install gdbrunner):

gdbrunner jlink --device STM32H743VI build/OPENMV4/bin/firmware.elf
gdbrunner stlink build/OPENMV4/bin/firmware.elf
gdbrunner qemu --machine mps2-an500 build/MPS2_AN500/bin/firmware.elf

最初の位置引数でサーバーバックエンド(jlinkstlinkqemu)を選択し、残りはそのバックエンドに転送されます。デフォルト値はOpenMVカメラで動作するように設定されています。gdbrunner --help でバックエンドごとの完全なフラグ一覧が表示されます。各バックエンドの引数テーブルはJSON駆動(src/gdbrunner/backends.json)なので、新しいサーバーの追加はコードではなく設定の編集で済みます。

gdbrunnerがコマンドライン作業のために行うこと:

  • 1つのプロセス、クリーンなライフサイクル。 サーバーが起動し、ポートが開くとgdbがアタッチし、gdbが終了するとサーバーがクリーンに終了します。セッションを生き延びる孤児の JLinkGDBServer も、管理すべき2つのターミナルもありません。

  • STM32CubeProgrammerの自動検出。 stlink バックエンドはSTM32CubeProgrammerツールを通常のインストール場所(~/STM32CubeProgrammer//opt/st/、STM32CubeIDEプラグインツリー)から検索するので、長い --cube-prog パスを毎回入力する必要がありません。SDKは独自のコピーを ~/openmv-sdk-<version>/stcubeprog/bin にバンドルしています。システムインストールが存在しない場合は --cube-prog をそこに指定してください。

  • プロジェクトごとのgdbinitを尊重。 カレントディレクトリの .gdbinit-ix で読み込まれ、ユーザー全体の ~/.gdbinit を上書きします。そのため、プロジェクトごとのgdbスクリプト(プリティプリンタ、ボード固有のマクロ、ブレークポイントセット)は作業ディレクトリに存在させるだけで適用されます。make debug はリポジトリのルートから実行されるので、そこにある .gdbinit が適用されます。

  • ドライラン。 --dryrun は実行せずにサーバーコマンドを表示します。呼び出しをラッパースクリプトに適応させたり、IDEの起動構成にコピーしたり、gdbrunnerが構成している引数を確認したりするのに便利です。

  • サーバー出力の表示。 --show-output はサーバーのstdout / stderrを表示し続けます。デフォルトではこれを抑制します(gdbのUIをきれいに保つため)。サーバー自体が誤動作している場合はこのフラグを切り替えてください。

  • QEMUバックエンド。 qemu-system-arm は、ボードを接続せずにファームウェアビルドをデバッグします。MPS2_AN500 ターゲットはボード構成でこのバックエンドを選択するので、make TARGET=MPS2_AN500 DEBUG=1 debug はQEMUの mps2-an500 マシン向けにビルドし、プラットフォーム非依存のコード、つまりカメラ固有のペリフェラルに触れないすべてのコードを、飛行機の中でもステップ実行できます。(qemu-system-arm はホストにインストールするもので、SDKの一部ではありません。)

ブレークポイントのガター表示とペリフェラルレジスタビューを伴うソースレベルのステップ実行には、上記のVS Code Cortex-Debugセットアップの方が優れたツールです。gdbrunnerは、コマンドラインで完結するすべての作業に適したツールです。

14.1.1.4.7. デバッガの使用

セッションが実行中(プロセッサが main で停止している状態)になったら:

  • ブレークポイント -- Cの行の横のガターをクリックするか、Debug Consoleで break <file>:<line> / break <function> を実行します。Cortex-Mコアには少数の ハードウェア ブレークポイントコンパレータがあります(M7 / H7では通常6~8個、M55では8個)。フラッシュ上のコードでこれを超えると静かに失敗するので、アクティブなブレークポイントの数は控えめに保ってください。

  • ステップ実行 -- F10 ステップオーバー(next)、F11 ステップイン(step)、Shift+F11 ステップアウト(finish)、F5 続行。命令レベルのステップ実行はDebug Consoleでの stepi / nexti です。

  • 変数 / ウォッチ / コールスタック -- VariablesCall Stack ペインはローカル変数とバックトレースを表示します。式は Watch に追加します。ソース内で変数にカーソルを合わせるとその値が表示されます。<optimized out> と表示されているものは、DEBUG=1 ビルドではないことを意味します。

  • ウォッチポイント(データブレークポイント) -- watch <expr> は変数が書き込まれたときに停止し、rwatch は読み込み時、awatch はいずれの場合にも停止します。Cortex-MのDWTユニットは約4個のハードウェアウォッチポイントをサポートします。誰が 変数を破壊したのかを捕捉するのに非常に有用です。

  • レジスタとペリフェラル -- Cortex Registers ビューはコアレジスタを表示します。svdFile が設定されていると、Peripherals ビューはすべてのペリフェラルレジスタとビットフィールド(DMA、タイマー、カメラ / CSIインターフェース、XSPIなど)をデコードします。ドライバがなぜ誤動作しているのかを見る最速の方法です。

  • メモリ -- Cortex-Debugのメモリビューアまたはgdbの x/ を使って、フレームバッファ、DMAバッファ、構造体を直接検査します。

  • 停止せずにprintf(SWO/RTT) -- タイミングに敏感な問題には、Seggerの RTT または SWO が、ターゲットの実行中にほぼゼロオーバーヘッドの printf を提供します。DEBUG_PRINTF=1 でビルドし、Cortex-Debugの rttConfig(RTT)または swoConfig(SWO、コアクロックが必要)を追加します。これは、ブレークポイントが観測しようとしているタイミングを変えてしまうような場合に適したツールです。

  • 切断 -- launch セッションでの Stop はターゲットを停止します。attach セッションでの Disconnect はカメラを実行したままにします。その後、カメラを電源再投入して通常動作に戻してください。

14.1.1.4.8. デバッグの落とし穴

  • 最適化で消えた変数。 すべてが <optimized out> と表示される場合、DEBUG=0 でビルドしています。DEBUG=1 で再ビルドしてください。

  • "GDB executable not found" -- SDKの gcc/binPATH 上にありません。armToolchainPath / gdbPath を設定してください。

  • "Cannot connect" / 誤ったメモリマップ -- device 名が間違っているか欠落しています。表の正確な文字列を使用してください。

  • ブレークポイントが静かにヒットしない -- フラッシュ常駐コード上のハードウェアブレークポイントが多すぎます。減らしてください。

  • ソースパスが一致しない(DockerでビルドしたELF) -- Dockerの build-firmware-dev ターゲットでビルドする(コンテナ内外で同じ絶対パスになる)か、gdbの set substitute-path を設定してください。