技術サポートに所属する、エンジニア2年目の私。組込み開発を学ぶため、ESP32搭載のロボットドッグを相棒にすることになった。名前は【だいきち】(以下、組込み犬)だ。
組込み犬のサンプルコードを自分好みに変え、目の前では仕様どおりに動いている。でも、プログラムの中身も本当に正しいのだろうか?
内部の動きを確認しようにも、ESP32のArduino環境では、ブレークポイントやステップ実行を手軽には使えない。printfログでも動作は確認できるが、関数の流れや変数の変化を追うのは大変だ。
そこで、ハートランド・データの動的テストツール「DT+Trace」を使うことにした。DT+Traceは、ソフトウェアを止めずに、実行経路やタイミングをリアルタイムに可視化できるツールだ。
本記事では、ESP32(Arduino環境)へのDT+Traceの導入から、実行経路の見える化、C0カバレッジ計測までをまとめてみた。
CONTENTS
前提

- ESP32をコアとしたロボットドッグキットで、組込み開発を学習
- サンプルコードを変更し、自分好みの動作にカスタマイズして、動作検証をする
- サンプルコードの理解やコード変更には、生成AIを活用
組込み犬を作ることになったきっかけや開発の様子は、下記のブログで紹介しているので、気になる方はぜひご覧ください。
組込み犬開発による新人エンジニア成長記

ESP32搭載のロボットドッグを題材に、新人エンジニアが組み立てから機能追加、動作検証までに挑戦!生成AIを活用しながら、組込み開発を通じて成長していく様子を紹介します。
ブレークポイントを使って、デバッグができない!?
デバッガがないし、printfもしんどい
IDEでビルドして、ESP32にプログラムを書き込み、動かしてみると期待動作をしていそうではある。
でも、AIと相談して作られたプログラムは、正しく動いているのだろうか?
まずは、センサで検出した値に応じて、正しく関数が実行されているのかを確認しようと思う。
…でも、Arduino環境って、デバッガがないんですね、専用ハードウェアが必要っぽい。
とりあえず、もっとも標準的で、どのマイコンでも使える、ログ出力コードとなるprintf文でIDEのコンソールに出力するデバッグ手法を試してみる。
ブレークポイントによる停止やステップ実行ができず、「今どの関数を実行しているのか」「想定した分岐へ入っているのか」の確認が、簡単にはできない。
変数値の変化履歴は見られるが、「変数値がどう変化しているか」のタイミングを確認するのも難しい。

問題事象の原因調査では、ログ出力コードの追加、ビルド、書き込み、動作・ログの確認までの一連の作業を何度も繰り返すことになる。
また、ログ出力コードが増えていくと、今度は後片付けも大変になってしまう。
不要になったログを削除するためにコード全体を見直し、仕込んだ箇所を探し回ることもあった。
動作確認には使えるものの、関数の呼び出し順序や変数の変化を追いかけることにも限界があり、思ったようにデバッグが進まない。
もっと簡単に動作を見える化したい!
そこで、普段の業務で扱っているDT+Traceを、自分の開発でも使ってみることにした。
実行経路の見える化で動作の正しさを確認する
ESP32(Arduino環境)へのDT+Trace導入

通信方法とテストポイントの設定
ESP32マイコンは、ポート数も多くないので、空きポートが1ポートあれば使える「UART通信」を使用。
※DT+Traceは、最大3Mbpsの高速UART通信に対応しています。
次に、printfの代わりとなる、テストポイントを自動挿入。
その際、自分でカスタマイズしたファイル(main.cppやswitch.cpp)と、動作関数がまとめられているファイル(Motion.cppなど)を対象に選び、すべての関数の入口・出口に設定した。
トレース用ドライバの修正
続いて、UARTの設定/処理をするソースファイルとヘッダファイルを追加し、DT+Traceのトレース用ドライバをArduino環境向けにカスタマイズした。
- CファイルをC++ファイルに変更する
- 「.c」→「.cpp」に変更する
- 使用予定の関数をextern “C”で宣言する
extern "C"{
void _TP_BusOut( DT_UINT addr, DT_UINT dat );
void _TP_MemoryOutput( DT_UINT addr, DT_UINT dat, void *value, DT_UINT size );
void _TP_EventTrigger( DT_UINT addr, DT_UINT dat );
}
extern “C”を付けることで、これらの関数をCの形式でリンクするようにコンパイラに指示している。
キャスト変更
void*(なんでもポインタ)は、そのままunsigned char*へ暗黙変換できない。そのため、unsigned char *p = value;と書くとエラーが出る。
そこで、明示的にキャストするよう修正した。
- サンプル:
unsigned char *p = value; - 変更後:
unsigned char *p = (unsigned char *)value;
API変更
サンプルで使われていた割込み制御APIは、Texas Instrumentsマイコン向けのライブラリ(TivaWare/StellarisWare)のものだったため、Arduino環境のAPIへ変更。
- CPU全体の割込みを有効にする関数
- サンプル:「IntMasterEnable()」
- 変更後:「interrupts()」
- 割込みを禁止にする関数
- サンプル「IntMasterDisable()」
- 変更後「noInterrupts()」
DT+Traceのプロジェクト・Bridgeアプリの設定
ドライバの修正後、次の手順でDT+TraceとBridgeアプリを設定し、データを取得した。
- DT+のプロジェクト作成

- テスト対象のソースコード登録
- Bridgeアプリの設定

- 3Mbps対応USB-UART変換ケーブルでBridgeアプリが起動しているPCとターゲットを接続
- DT+Traceでテスト実行し、データを取得する
Arduino特有の注意点

DT+には[.ino]ファイルを登録できないため、[.cpp]に変更する必要がある。もともとの[.ino]ファイルの説明欄にも「main.cpp」と記載があり、中身はC++で書かれている。

一方Arduino IDEでは、[.ino]ファイルをダブルクリックして対象のソースコード全てを起動するため、空の[.ino]ファイル(ダミーファイル)を用意する必要がある。
組込み犬の骨格動作を見える化するための仕込みは、これでバッチリ!
さっそくプログラムがどのように動いているか、正しい処理の流れになっているのかを確認してみる。
見える化した結果
printfデバッグよりも処理の流れを追いやすいし、変数値の変化を処理の流れと合わせて確認もできることがわかった。
▼動作検証でのターゲットの挙動
距離センサの検出距離に応じたモーター制御動作を見える化
▼関数の遷移

▼変数値の遷移

- 電源投入後に初期化処理が実行される
電源投入後のタイミングで、初期化処理を行う[setup]が実行されていることがわかる。 - 5cm以内に手をかざしてから、ダンス開始までに約2秒かかる
5cm以内に手をかざすと、途中で[checkApproachTrigger]が呼ばれる。その後、[checkHandGesture]から[danceDancing]が呼ばれるまでの時間は2,270msであり、約2秒後にダンスが始まっていることが確認できる(6,920ms-4,650ms=2,270ms)。 - 判定後、想定したダンス処理が実行される
ダンス関数[danceDancing]が実行され、体をひねる関数や足の角度を更新する関数などが呼ばれている。これにより、想定した処理の流れでダンスが実行されていることがわかる。 - 10cm~20cmの範囲を検出すると、対象へ近づく
[checkApproachTrigger]が呼ばれた後、対象へ近づくための[danceApproach]が実行されている。その中で、移動や歩行に関する関数が繰り返し呼ばれ、接近動作が実行されていることがわかる。 - 25cm~35cmの範囲を検出すると、ストレッチをする
[checkDistanceStretchTrigger]が呼ばれた後、ストレッチ関数[danceStretchSelf]が実行されている。その中で、足を滑らかに移動させる関数や足の角度を更新する関数などが呼ばれ、ストレッチが実行されていることがわかる。 - 一連の動作後に直立姿勢へ戻る
[standUp]が実行され、初期位置の直立姿勢へ戻っていることがわかる。
ダンス・接近・ストレッチの各動作中は、距離センサによる計測が停止していることも確認できた。
スイッチ操作に応じたモーター制御動作を見える化

- 電源投入後に初期化処理が実行される
電源投入後のタイミングで、初期化処理を行う[setup]が実行されていることがわかる。 - 短押しすると、お座りをする
短押しすると[shortPressAction]が実行され、お座り動作を行う[danceSitDown]へ遷移していることがわかる。 - ダブルクリックすると、挨拶をする
ダブルクリックすると[doubleClickAction]が実行され、挨拶動作を行う[danceSayHello]へ遷移していることがわかる。 - 長押しすると、移動する
長押しすると[longPressAction]が実行され、移動処理を行う[move_any]へ遷移していることがわかる。 - 旋回中に短押しすると、旋回を中断してお座りをする
左右への旋回中に短押しすると、旋回動作が中断され、[shortPressAction]を経て[danceSitDown]へ遷移していることがわかる。
DT+Traceで変更箇所のカバレッジを計測
実行経路から動作を確認できたところで、次は変更箇所に確認漏れがないか、C0カバレッジを計測していく。
今回変更した関数にテストポイントを自動挿入し、チェックボックスで不要なテストポイントを無効にした。計測対象となるステップに絞ってカバレッジを確認したところ、すべて100%になると想定していたが、一部が100%に達していないことがわかった。
▼C0カバレッジ結果

C0カバレッジ結果の考察
カバレッジが100%にならなかった箇所を確認したところ、要因は大きく2つに分けられた。
ケース①:仕様上、通過しない処理
[loopSecondary]と[task_MotionService]に関しては、関数の出口を示すFuncOutが通過していなかった。
コードを確認すると、関数の冒頭に[while]があり、設計上FuncOutには到達しない構造になっていた。つまり、これは不具合ではなく、仕様どおりの結果だった。

ケース②:削除し忘れた不要コード
[handleSwitch]と[isr_switch]に関しては、どちらも割込み処理に関係する関数で、割込み処理自体は問題なく動作しているので通過するはず…。

しかし、何度やっても通過しない。
コードの確認を行ったところ、[isr_switch]はマスクされており、どこからも呼び出されていないことが判明!

実装中に、[isr_switch]による割込み処理を実行していたが、仕様を満たす処理にならなかったため、[handleSwitch]をmain.cppのループ処理で呼び出す仕様に変更していた。しかしその際に、不要になったコードを削除し忘れていた…。
現在の動作には影響しないが、将来的に誰かがこの処理を使い、意図しない不具合を生む可能性があった。
コードを修正して再計測
不要なコードを削除し、仕様上通過しないテストポイントは、DT+Traceで「非通過」に設定して計測対象から除外した。そして、再度計測した結果、カバレッジはすべて100%に!

C0カバレッジを計測したことで、未確認のコードだけでなく、実装途中で残っていた不要なコードも発見できた。コード変更による実装漏れがないことを確認し、将来の不具合につながる可能性も減らすことができた。
見える化の価値
目視によるシステムの挙動と、実際のソフトウェアの動きを確認できていることは違う。
生成AIの支援で実装は想像以上に早く終わった。
だが、「動いているように見える」状態から「確認できている」状態にするまでが、今回いちばん深かった。
未通過は必ずしも不具合ではない。しかし呼び出し元を失った取り残しコードは、将来の不具合の種になる。
その切り分けを自分でできたことが、今回のいちばんの収穫だ。
ツールを入れて増えたのは、デバッグの手数でなく「説明できること」の量だった。
デバッガのない環境でも、リリースの根拠は自分の手で作れる。
上司の『これで大丈夫なの!?』に、今の私ならこれまでの結果を差し出して自信を持って答えられる。次はセンサや動作を追加し、パワーアップした推し犬【だいきち】で同じ検証をやる予定。
「動いていそう」に満足することなく、これからの私は「確認できたコード」しかリリースしない!
【 無料でみられる! 】動的テストツールDT+ デモ動画

ソフトウェア開発者のための動的テストツール「DT+」をご紹介する動画です。
ソースコードの実行によりログを収集し、多彩な解析機能によりソフトウェアの動作をこまかく見える化。たった数クリックの解析で、関数遷移や変数の変動、カバレッジをグラフィカルに表示します。本動画では、そんなDT+の導入手法から実際の解析の様子まで、基本的な使い方をデモンストレーションいたします。

