Arduino UNO R4のデバッグ・printfの行方の謎とprintlnの小技
printf 使える?
とりあえず乱暴にもこんなコードを試してみました。
コンパイルは…OK、おぉ、ってことは printf()
あるんだ! ダウンロードも OK だ。
シリアルモニタを確認っと…
あれっ…出ない!?…おかしいなぁ…。
念の為、オマジナイ Serial.begin(9600); while (!Serial);
を入れてみましたが、変わりません。
一体全体 printf()
の出力はどこに行っちゃったんでしょう?
デバッガで確認
printf()
の行方を探すため、トラ技 ARM ライタ でステップインしてみると、スカッ!と次の行に移動しちゃいます。どうやら空の関数っぽい挙動です。う〜ん…
- Arduino UNO R4 のソースコードデバッグは、以下をご参照ください。
ライブラリを試す
気を取り直し、IDE のライブラリマネージャーで「printf」を検索。シンプルなものから重厚なものまで工夫を凝らしたものが色々出てきます。大抵の場合は良く出来ていて、Sirial
以外のデバイスにもバインドできる感じに作られています。
1番シッカリしてそうなのは、公式サイトにも上がっている LibPrintf でしょうか。snprintf()
や vsnprintf()
も再現されています。
でも僕には帯にも襷にも長い感じ。
で、一番しっくりきたのが Qiita の記事 のコードです。ただ僕は、きっとどこかで printf()
が weak シンボル か virtual
として定義されているに違いないと読み、stdio.h
の I/F 仕様でコーディング&試してみました。案の定(定義が見つかっていないのでわかりませんが…)エラーも無く動作 OK です。
このコードはかつて LPC-1343 ボードで実装した sci.c からの移植です。Serial
専用だけど、僕的にはこれで十分です。
当然ですが、printf()
にステップイン出来ました。読みは正しかった様です
Minima と WiFi の違い!
Minima では、出力関数により低レベルな関数 Serial.write()
を使って OK でしたが、WiFi では、連続する printf()
間で前後の文字が欠落するという問題に遭遇したので、Serial.print()
を使わざるを得ず、処理時間もかなり異なります。GitHub にテストスケッチを置いたので、気になる方は試してみてください。
また WiFi では、初期化待ちの while(!Serial);
は機能せず、delay(1000);
で待つ必要がありました。どちらも HardwareSerial
という抽象クラスを継承していますが、Minima は 真偽を評価するオペレータがそれなりに実装されている のに対し、 WiFi では 単に true
を返しているだけ だからです。
Minima と違い WiFi の USB は、ARM との間に ESP32-S3-MINI-1-N8 を挟んでいるので、 このような違いが出るのだと思います。回路上の違いを十分検証せず、安易にプログラムを公開すると危険ということを思い知らされました。
printfの書式と動作確認
printf()
や scanf()
で押さえるべき書式文字列のフォーマットは次図の通り、%
に続き、フラグ、最小フィールド幅、精度、長さ修飾子、変換指定子で構成されます。
それぞれの厳密な仕様はそれなりのボリュームになり、ここには書ききれませんが、Linux 系や Mac であれば、シェルからコマンド man 3 printf
を打てば標準 C ライブラリの書式を調べられますし、簡潔にまとめてくれている次のサイトがオススメです。
今回は出力を検証するために、前者のサンプルコードを使わせてもらいました。どれも記事の記述通りになりましたし、浮動小数点もそれなりに(厳密に仕様通りか未検証という意味)フォーマットしてくれてます。
エスケープシーケンスのテスト
ついでに IDE のシリアルモニタ側テストとして、エスケープシーケンス も試してみました。
Tera Term 並の動作を期待したのですが、これはダメでした。不可視文字も含めてそのまま出てきちゃいます。
VSCode のターミナルなら対応している気もしますが、僕の Mac が古いためか(Late 2013)、環境構築がうまくいかず未確認です。せめて画面クリアや文字色の変更か反転ぐらいできると助かるんですけどネ
ライブラリ化
デバッグのたびにコピペするのも面倒なので、ライブラリ化しちゃいました。UNO R4 以外の動作確認ができないので R4 用としています → Releases
よかったら使ってやってください。
-
Release から最新版の
Source code (zip)
をダウンロードします。 -
Arduino IDE メニューの「スケッチ → ライブラリをインクルード → .ZIP形式のライブラリをインストール…」からダウンロードした .zip ファイルを読み込みます。
-
IDE メニューの「ファイル → スケッチ例 → カスタムライブラリのスケッチ例」からサンプル
printf
を試せます。
Serial.println() の小技
SRAM メモリの少ない時代の Arduino では 敬遠された らしい String
クラス ですが、ちょっとしたデバッグには 数値を文字列に変換 したり 他の文字列との足し算 ができるので、println()
との組み合わせがイイ感じです。僕はもっぱらコレですネ。
クラス内部 で引数(コンストラクタ)に応じて dtostrf()
や ultoa()
を選り分けているので、処理のオーバーヘッドや ヒープメモリ へのフットプリントもそれなりと思いますが、小数点以下の桁数指定や基数変換ができたりする多才な奴です。
Arduino の String
クラス は C++ の標準規格 に比べてメソッド数が少ないものの、ハードウェアに依存しない ArduinoCore-API レイヤに属しているので、どのアーキテクチャのボードでもそれなりに動くハズです。
printfの行方は?
さて冒頭の件に戻りますが、以下の2つの疑問が湧いてきます。
- なぜ空の
printf()
が定義されているのか - どこで定義されているのか
Arduino 新参者で UNO R4 しか持ってない僕には歴史的な経緯はわかりませんが、Arduino Playground - Printf の冒頭には「stdio および(それを出力先とする)printf() は Arduino 環境に組み込まれていません…」とハッキリ書かれています。
メモリが少なく、また多くのアーキテクチャを抱える Arduino が printf()
ではなく、より単純な print()
と println()
をベースラインにしたことは理解できます。
では「stdio を組み込んでいない」のはなぜか? 以下は私の勝手な解釈です。
OS が載るシステムでは、システムをオペレートするための標準入出力 I/F として、stdin
や stdout
、stderr
という抽象化された仮想の標準端末を必要とします。
一方デフォルトで OS が載らない Arduino ですが、Serial、LiquidCrystal、Ethernet や WiFi といったデバイスクラスは、Stream クラス から Print クラス を継承しているので、これらを元にすれば stdio を Serial
に決めることも可能だったハズです。
それでも stdio を一意に決めていないのは、標準入出力 I/F として物理層に何を載せるか、「決めるのはお前だゾ」と暗に言われているように僕には思えます。生粋の組込系的発想ですかね。
とはいえ、単純に未定義にしないのは親切心か、はたまた余計なお世話か?… 未だに見つからない定義箇所と共に謎は残ります。まぁ、どうでもイイことですが…
以上、中途半ばな謎解きと小技でした