Arduino M0 Pro を GDB でデバッグしよう

投稿者: | 2017/09/11

Tried source code level debugging of Arduino M0 Pro sketch by GDB.

Arduino は使いやすく、組込プログラミングのツールとして普及していますが、Arduino Sketch でプログラミングする際の一番のネックは、標準ではデバッガが用意されていないことではないでしょうか。いわゆる「printf() デバッグ」は可能ですが、大きなプログラムを Eclipse 上の GDB(GNU Debugger)などでデバッグした経験をお持ちの方には、いろいろと歯がゆいところだと思います。

なお、「結論から教えろ」という方は、後半の「実際にやってみる」からお読みください。しばらく前振りが続きます。スミマセン。

前振り

Arduino Sketch とは、C++ をベースとした、皆様におなじみの

void setup()
{
    // ...
}

void loop()
{
    // ...
}

というプログラミング言語(環境)のことです。

以前、Adafruit のこちらの記事で、旧 Atmel の Atmel Studio を使うと Sketch のデバッグが可能という話を読んで試したことがあるのですが、Microsoft Visual Studio 風の操作環境に慣れないこともあり、うまく行きませんでした。Atmel Studio を使わないとうまくいかないのか、と思っていたのですが、「普通に GDB でデバッグできるよ」と書いていらっしゃる記事を見つけ、目から鱗が落ちました。確かに、Arduino Sketch は .ino という拡張子のコードを C++ に変換して実行している訳ですから、技術的には可能かなあ、とは思っていたのですが。(言い訳終わり)

なお、今回の記事は「人様の記事」を後追いして日本語で書き下しただけという点で、プロの端くれとしては後ろめたいのですが、参考になる方も多いのではないかと思い、書いてみることにした次第です。

元記事はこちらです。非常に参考になりました。ありがとうございました。So far, I didn't imagine we can source-level debug Arduino M0 Pro by GDB.  The article was so helpful for me.  Thank you.

ちゃんと仁義を切りましたよ。 🙂

評価環境

今回は、私のメイン環境である Mac OS X 上で試してみました。元記事を書かれた方は Linux で試しているようですが、あまり OS には依存しなそうなので、arm-none-eabi-gdb と OpenOCD が動く環境であれば、Windows でも使えるのではないか、と思います。(元記事の方もそう書いています。)

ここで Mac OS X を使った一つの理由として、(私の主要開発環境であることに加えて)普段からデバイスメーカーに冷たく扱われがちな Mac OS X でも組込ソフトウェア設計ができることを強調したい、ということもあります。

さて、必要なブツです。

  • パソコン
    (今回は Mac です。)
  • Arduino M0 Pro
    スイッチサイエンスさんなどで購入できます。Arduino Uno に比べるとやや高価ですが、性能がそれだけ高く、また外付けデバッガ(ハードウェア)無しで  GDB デバッグできるのは大きな利点だと思います。)

なお、SEGGER J-Link などをお持ちのプロフェッショナルな方もあると思いますが、OpenOCD というソフトを使うと、ほぼ同じことができると思います。

実際にやってみる

まずは、必要なソフト(以下)をインストールします。

  • 最新版の Arduino IDE
  • GDB
  • OpenOCD

Arduino IDE のインストールと設定

最初に IDE です。ここからインストールします。(Web Editor ではありませんよ。)

なお、Arduino IDE をインストールしただけでは Arduino M0 Pro 対応になりません。メニューの「ツール → ボード → ボードマネージャ」を起動し、右上の検索窓に m0 pro と入力して検索し、Arduino SAMD Boards パッケージをインストールしてください。

こうすると、IDE のメニューの「ツール → ボード」から、Arduino M0 Pro (Programming Port) を選べるようになります。ここで選んだ通り、USB ケーブルはボードの Programming Port のほうに繋ぎます。

ここまでできたら、手始めにサンプルプロジェクト Blink をビルドして、Arduino M0 Pro 上で動くことを確認しておきましょう。「ファイル → スケッチ例 → 01.Basics → Blink」ですね。続いて、メニューの「スケッチ → マイコンボードに書き込む」を実行します。これは、後ほど GDB でデバッグするときに重要なので、忘れずに実行しておきます

これが動いたら、後の作業のために C++ の中間ソースコードと実行ファイル(ELF ファイル)をコピーしておきましょう。通常は、Arduino IDE はこれを裏でこっそりやっていて、ユーザーには中間ソースコードを見せてくれませんが、コピーすることはできます。(なぜコピーするかというと、Arduino IDE 終了時に中間ソースコードや ELF が消されてしまうからです。)

このためには、Arduino IDE 環境設定の変更が必要です。メニューから「Arduino → Preferences...」を開き、「より詳細な情報を表示する」から「コンパイル」を選んでおきます。この状態で「スケッチ → 検証・コンパイル」をすると、ログ表示に次のような結果が得られるはずです。末尾の部分が重要です。

(前略)
Linking everything together...
"/Users/yokoyama/Library/Arduino15/packages/arduino/tools/arm-none-eabi-gcc/4.8.3-2014q1/bin/arm-none-eabi-gcc" "-L/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897" -Os -Wl,--gc-sections -save-temps "-T/Users/yokoyama/Library/Arduino15/packages/arduino/hardware/samd/1.6.16/variants/arduino_mzero/linker_scripts/gcc/flash_with_bootloader.ld" "-Wl,-Map,/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/Blink.ino.map" --specs=nano.specs --specs=nosys.specs -mcpu=cortex-m0plus -mthumb -Wl,--cref -Wl,--check-sections -Wl,--gc-sections -Wl,--unresolved-symbols=report-all -Wl,--warn-common -Wl,--warn-section-align -o "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/Blink.ino.elf" "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/sketch/Blink.ino.cpp.o" "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/core/variant.cpp.o" -Wl,--start-group "-L/Users/yokoyama/Library/Arduino15/packages/arduino/tools/CMSIS/4.5.0/CMSIS/Lib/GCC/" -larm_cortexM0l_math -lm "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/core/core.a" -Wl,--end-group
"/Users/yokoyama/Library/Arduino15/packages/arduino/tools/arm-none-eabi-gcc/4.8.3-2014q1/bin/arm-none-eabi-objcopy" -O binary "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/Blink.ino.elf" "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/Blink.ino.bin"
"/Users/yokoyama/Library/Arduino15/packages/arduino/tools/arm-none-eabi-gcc/4.8.3-2014q1/bin/arm-none-eabi-objcopy" -O ihex -R .eeprom "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/Blink.ino.elf" "/var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897/Blink.ino.hex"
最大262144バイトのフラッシュメモリのうち、スケッチが9856バイト(3%)を使っています。

太字で示した /var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897 というディレクトリを、どこかにコピーしてください。以下に例を示します。(なお、実際にタイプする部分をアンダーラインで示します。以下同様)

$ cp -Rp /var/folders/dv/sqr27wds7ls4nfh6fdc01tlh0000gn/T/arduino_build_344897 ~/

あ、言い忘れましたがシェル(Bash)の基本的な操作はできることを前提としています。。。

このディレクトリの中を見ると、次のようなファイルがあることが分かります。

  • Blink.ino.elf(ELF 形式の実行ファイル)
  • sketch/Blink.ino.cpp(C++ に変換されたソースコード)

GDB のインストール

次に GDB です。Mac OS X でしたら、Homebrew でインストールするのが一番簡単だと思います。Homebrew をインストールしたら、以下のコマンドでインストールできます。

$ brew cask install gcc-arm-embedded

この Adafruit の記事も参考にしてください。実行すると、/usr/local/bin の下に arm-none-eabi-gdb というコマンド(GDB)がインストールされるはずです。

OpenOCD のインストール

そのままでは、GDB は SAM D21 マイコン(Arduino M0 Pro に載った ARM マイコン)と「お話し」できないので、同じボード上に載った EDBG というチップを介してデバッグをするためのツール OpenOCD をインストールします。

これも Homebrew であれば、

$ brew install openocd

としてインストールできます。

次に、OpenOCD に「Arduino M0 Pro とお話しする方法」を伝えるための「構成ファイル(.cfg)」が必要です。これは、ここから入手できます。内容(のうち肝心なところ)はこんな感じです。このファイル arduino_zero.cfg を、先ほどのディレクトリにコピーしておきます。(実際には、OpenOCD と GDB は個別に実行するので、どこにコピーしても良いです。)

source [find interface/cmsis-dap.cfg]
set CHIPNAME at91samd21g18
set ENDIAN little
source [find target/at91samdXX.cfg]

実際のデバッグ

では、実際にデバッグをしてみましょう。まず最初に OpenOCD を起動します。これは、GDB の立場から見ると gdbserver と呼ばれるリモートデバッグ用のソフトウェアです。OpenOCD はフロントプロセスとして動作するので、ターミナルのウィンドウは個別に開いておいたほうが良いでしょう。

OpenOCD 用のウィンドウで起動します。

$ openocd -f ~/arduino_build_344897/arduino_zero.cfg
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "swd". To override use 'transport select '.
none separate
adapter speed: 400 kHz
cortex_m reset_config sysresetreq
Info : CMSIS-DAP: SWD  Supported
Info : CMSIS-DAP: JTAG Supported
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : CMSIS-DAP: FW Version = 01.1F.0118
Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 1 TDO = 1 nTRST = 0 nRESET = 1
Info : CMSIS-DAP: Interface ready
Info : clock speed 400 kHz
Info : SWD DPIDR 0x0bc11477
Info : at91samd21g18.cpu: hardware has 4 breakpoints, 2 watchpoints

のように表示されれば、正しく動作しています。動かない場合は、ケーブルの接続等を確認しましょう。USB ケーブルは、ボードの Proramming ポートのほうに正しく接続されているでしょうか。

次に GDB を、ターミナルの別のウィンドウから起動します。

$ cd ~/arduino_build_344897
$ arm-none-eabi-gdb
GNU gdb (GNU Tools for ARM Embedded Processors 6-2017-q2-update) 7.12.1.20170417-git
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin10 --target=arm-none-eabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)

このようになれば OK です。

それでは実際にデバッグしてみましょう。まずは GDB に、リモートのターゲットへ OpenOCD 経由で接続することを伝えます。OpenOCD は、デフォルトで TCP ポート 3333 番で待機しているので、target コマンドを使い

(gdb) target remote :3333
Remote debugging using :3333
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x00000000 in ?? ()

とします。

ここで、デバッグする対象のソフトウェアは既にマイコン上のフラッシュメモリに書き込まれているのですが、GDB はそのソフトウェア(バイナリファイル)とソースコードの関係を理解していませんので、「シンボル情報」というものを与えてあげます。シンボル情報は ELF ファイルに書き込まれています。(フラッシュメモリには、シンボル情報は書かれていません。)

(gdb) file Blink.ino.elf
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from Blink.ino.elf...done.

質問が出たら、y と入力して先に進みます。

次に、マイコンをリセットします。

(gdb) monitor reset halt
target halted due to debug-request, current mode: Thread
xPSR: 0x81000000 pc: 0x000028f4 msp: 0x20002c00

これは、そういうものだと覚えてください。詳しいことを知りたい方は、こちらで勉強してください。

次に、loop() 関数にブレークポイントを張ってみましょう。つまり、マイコンが loop() 関数の先頭まで来たら停止するようにする訳です。まずソースコードを確認します。

(gdb) list loop
26        // initialize digital pin LED_BUILTIN as an output.
27        pinMode(LED_BUILTIN, OUTPUT);
28      }
29
30      // the loop function runs over and over again forever
31      void loop() {
32        digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
33        delay(1000);                       // wait for a second
34        digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
35        delay(1000);                       // wait for a second

32行目を実行する前に停止したいので、次のようにします。

(gdb) break 32
Breakpoint 4 at 0x4124: file /Applications/Arduino.app/Contents/Java/examples/01.Basics/Blink/Blink.ino, line 32.

そして実行します。実行するには continue コマンドですが、GDB ではコマンドを略してタイプすることができ、continue の代わりに c で大丈夫です。やってみましょう。

(gdb) c
Continuing.

Breakpoint 4, loop ()
    at /Applications/Arduino.app/Contents/Java/examples/01.Basics/Blink/Blink.ino:32
32        digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)

となって停止したでしょうか。

GDB で直前のコマンドを再実行するにはリターンキーを押します。リターンキーを押すたびに、ボード上の LED が点滅するのが確認できましたか?

ちなみにこの Blink プログラムでは変数を使っていませんが、変数を表示するには print コマンドが使えます。たとえば変数 v を表示したければ、print v というコマンドで変数の値を確認できます。また、スタックトレースを表示するには where というコマンドが使えます。これで、プログラムを効率的にデバッグできるようになりましたね。その他の詳しい使い方については、GDB の参考書を見てください。(おいおい)

GDB を抜けるには、quit コマンドです。別ウィンドウで動いている OpenOCD は、コントロール C で停まります。

なお、GDB のコマンドラインデバッグに慣れない方も多いと思いますので(私も慣れない!)、次回は Eclipse 上でデバッグしてみたいと思います。突然ですが、今日はここまで。

おしまい。