Arduinoで`printf()`

:::note info
『arduino-picoですと`Serial.printf()`が使えますよ』
って @jksoft さんに教えていただきました．　
ありがとうございます！

なのでピコなら，わざわざコレを使う必要はありませんね（´(ｪ)｀;
:::


# めんどくさいなぁ
さいきん，Arduino環境を触ることが多い．
「コードを書いて→ロジアナで動作を確認して→はい終わり」って時には問題ないのだけど，スクリーン表示が必要なときには`Serial`クラスのメソッド：`Serial.print()`や`Serial.println()`を組み合わせて表示を作ることになる． 

この`Serial.print()`でも16進で表示などもできるのだけど桁数指定や他の文字や文字列，変数との組み合わせを作ろうとすると手間がかかってしょうがない．これまではそのたびに，いろいろ誤魔化しながら書いていた．しかしいよいよ我慢がならなくなった． 

「そういう便利なのって，もう誰かが作ってるんだろうなぁ」と思いながら，Arduino IDEのLibrary Managerで「printf」を探してもC++のSTLとか高尚なのしか出てこなかったし．


# `printf()`はないけど`sprintf()`は動く 
`printf()`が無いのはしょうがない．でも`sprintf()`は？  
「ひょっとしたら動くんでないの？」と思って試してみたら動くではないか！   
```c
void setup() {
  Serial.begin(9600);
  while (!Serial)
    ;

  char s[80];

  sprintf(s, "%s\n%d, %d, %d", "Hello, world!", 11, 13, 17);
  Serial.println( s );
}

void loop() {
} 
```
これ↑↑の結果↓↓
```
Hello, world!
11, 13, 17
```
ついでに調べてみたら`vsnprintf()`もちゃんと存在してる．ならば`printf()`も作れる．  
そんなわけで[前に書いた古いコード](https://os.mbed.com/users/okano/code/SB1602E//file/baf578069dfc/SB1602E.cpp/)を掘り出してきて，printfするやつを作ってみた． 

# 闇に吸い込まれる 
「簡単にprintfできるぢゃないか！」ってことで`printf()`って関数を作って実行してみた．しかし結果が出てこない．確認のために`Serial.println()`と混ぜて使ってみるとSerial.printlnの内容だけ表示されてprintfのは表示されない．
なんかおかしい．少し考えて関数の名前を変えてみたらイケた．どうやら`printf`はすでに用意されていて，その「何もしない」方にリンクされてしまってるっぽい． 

なのでしょうがないので別の名前をつけることにした．何もいいのが思いつかなかったので適当に`zprintf`にした．

# いごいた！ 
適当に作った`zprintf()`だけど，コード量も少なくて簡単に動く．ライブラリ化するまでもないほどの小さいコード．これなら必要な時にコピペして使うのでいいかな． 
とりあえず`MAX_ZPRINTF_LENGTH`の指定と`zprintf()`の本体だけコピーして，プロトタイプ宣言が面倒ならプログラムの最初の方にペーストしておけばいいし．

```c 
//  プロトタイプ宣言
void zprintf(const char *format, ...);

//  プログラム本体(1/2 : setup)
void setup() {
  Serial.begin(9600);
  while (!Serial)
    ;

  zprintf("Hello, world!\n");
}

//  プログラム本体(2/2 : loop)
void loop() {
  //  繰り返しやらせたいことは特にないのでここは空っぽ
}

//  ここからが`zprintf`に関連する部分
#define MAX_ZPRINTF_LENGTH 80  // 一度に変換する最大文字数
void zprintf(const char *format, ...) {
  char s[MAX_ZPRINTF_LENGTH];
  va_list args;

  va_start(args, format);
  vsnprintf(s, MAX_ZPRINTF_LENGTH, format, args);
  va_end(args);

  Serial.print(s);
}
```
あたりまえだけど，これ↑↑がこう↓↓

```
Hello, world!
```
# なんでわざわざ作ったか 
キレイに表を作ったりするには，やっぱりフォーマット機能がないと不便． 

これはアスキーコード表だけど，たったこれだけを表示させるにもprintfのようなフォーマットができないとつらい．

```
  0 (0x00) ' ',  16 (0x10) ' ',  32 (0x20) ' ',  48 (0x30) '0',  64 (0x40) '@',  80 (0x50) 'P',  96 (0x60) '`', 112 (0x70) 'p',
  1 (0x01) ' ',  17 (0x11) ' ',  33 (0x21) '!',  49 (0x31) '1',  65 (0x41) 'A',  81 (0x51) 'Q',  97 (0x61) 'a', 113 (0x71) 'q',
  2 (0x02) ' ',  18 (0x12) ' ',  34 (0x22) '"',  50 (0x32) '2',  66 (0x42) 'B',  82 (0x52) 'R',  98 (0x62) 'b', 114 (0x72) 'r',
  3 (0x03) ' ',  19 (0x13) ' ',  35 (0x23) '#',  51 (0x33) '3',  67 (0x43) 'C',  83 (0x53) 'S',  99 (0x63) 'c', 115 (0x73) 's',
  4 (0x04) ' ',  20 (0x14) ' ',  36 (0x24) '$',  52 (0x34) '4',  68 (0x44) 'D',  84 (0x54) 'T', 100 (0x64) 'd', 116 (0x74) 't',
  5 (0x05) ' ',  21 (0x15) ' ',  37 (0x25) '%',  53 (0x35) '5',  69 (0x45) 'E',  85 (0x55) 'U', 101 (0x65) 'e', 117 (0x75) 'u',
  6 (0x06) ' ',  22 (0x16) ' ',  38 (0x26) '&',  54 (0x36) '6',  70 (0x46) 'F',  86 (0x56) 'V', 102 (0x66) 'f', 118 (0x76) 'v',
  7 (0x07) ' ',  23 (0x17) ' ',  39 (0x27) ''',  55 (0x37) '7',  71 (0x47) 'G',  87 (0x57) 'W', 103 (0x67) 'g', 119 (0x77) 'w',
  8 (0x08) ' ',  24 (0x18) ' ',  40 (0x28) '(',  56 (0x38) '8',  72 (0x48) 'H',  88 (0x58) 'X', 104 (0x68) 'h', 120 (0x78) 'x',
  9 (0x09) ' ',  25 (0x19) ' ',  41 (0x29) ')',  57 (0x39) '9',  73 (0x49) 'I',  89 (0x59) 'Y', 105 (0x69) 'i', 121 (0x79) 'y',
 10 (0x0A) ' ',  26 (0x1A) ' ',  42 (0x2A) '*',  58 (0x3A) ':',  74 (0x4A) 'J',  90 (0x5A) 'Z', 106 (0x6A) 'j', 122 (0x7A) 'z',
 11 (0x0B) ' ',  27 (0x1B) ' ',  43 (0x2B) '+',  59 (0x3B) ';',  75 (0x4B) 'K',  91 (0x5B) '[', 107 (0x6B) 'k', 123 (0x7B) '{',
 12 (0x0C) ' ',  28 (0x1C) ' ',  44 (0x2C) ',',  60 (0x3C) '<',  76 (0x4C) 'L',  92 (0x5C) '\', 108 (0x6C) 'l', 124 (0x7C) '|',
 13 (0x0D) ' ',  29 (0x1D) ' ',  45 (0x2D) '-',  61 (0x3D) '=',  77 (0x4D) 'M',  93 (0x5D) ']', 109 (0x6D) 'm', 125 (0x7D) '}',
 14 (0x0E) ' ',  30 (0x1E) ' ',  46 (0x2E) '.',  62 (0x3E) '>',  78 (0x4E) 'N',  94 (0x5E) '^', 110 (0x6E) 'n', 126 (0x7E) '~',
 15 (0x0F) ' ',  31 (0x1F) ' ',  47 (0x2F) '/',  63 (0x3F) '?',  79 (0x4F) 'O',  95 (0x5F) '_', 111 (0x6F) 'o', 127 (0x7F) ' ',
```
これを(ループ部分に限れば)，たったひとつの文だけ↓↓で済むからなぁ． 
これをその都度，ひとつひとつ組み合わせて文字列を作るのはかなりしんどいから．
```c 
#define COLS 8
#define ROWS (128 / 8)
...
..

  for (int y = 0; y < ROWS; y++) {
    for (int x = 0; x < COLS; x++) {
      int n = x * ROWS + y;
      zprintf("%c%3d (0x%02X) '%c',", x ? ' ' : '\n', n, n, isprint(n) ? n: ' ' );
    }
  }
...
..
```
# 制限
とまぁそうは言っても，いいことばかりではなくて悪いこともある．　
ひとつ目はサイズ，ふたつ目は変換できる変数の型の制限．

## プログラム・サイズ
"Hello, world!"を表示するだけの簡単なプログラム．
zprintfを使わなければ下のようなコードで済む． 
```c
void setup() {
  Serial.begin(9600);
  while (!Serial)
    ;

  Serial.println("Hello, world!\n");
}

void loop() {
} 
```
このコードをコンパイルしたあとフラッシュに書かれるサイズは**1488**バイト．
それに対してzprint([「いごいた！」の節](https://qiita.com/drafts/9afc45bb1beab9b7af4f/edit#%E3%81%84%E3%81%94%E3%81%84%E3%81%9F)のサンプルコード)を使うと**3046**バイト． 
これはターゲットをArduino UNO R3（ボードパッケージ:Arduino AVR Boards 1.8.6）にした場合． 

ではほかのターゲットでやってみるとどうなるか．下の表はその比較．　
（PicoではIDEのToolsメニューから最適化オプションを選べるようになっているけど，ここではデフォルトのサイズ最適化(ーOs)を使用）

|ターゲット|ボードパッケージ|zprintfなし<br/>[バイト]|zprintfあり<br/>[バイト]|
|---|---|---|---|
|Arduino UNO R3|Arduino AVR Boards<br/>1.8.6|1488|3046|
|Arduino UNO R4|Arduino UNO R4 Boards<br/>1.0.2|33692|35636|
|Raspberry Pi Pico|Raspberry Pi Pico/RP2040<br/>3.3.2|51980|52092|

上記を得たコンパイル時のメッセージはこんな感じだった．　
```
== UNO R3 ==
zprintfなし: Sketch uses 1488 bytes (4%) of program storage space. Maximum is 32256 bytes.
zprintfあり: Sketch uses 3240 bytes (10%) of program storage space. Maximum is 32256 bytes.

== UNO R4 ==
zprintfなし: Sketch uses 33692 bytes (12%) of program storage space. Maximum is 262144 bytes.
zprintfあり: Sketch uses 35636 bytes (13%) of program storage space. Maximum is 262144 bytes.

== Pico ==
zprintfなし: Sketch uses 51980 bytes (2%) of program storage space. Maximum is 2093056 bytes.
zprintfあり: Sketch uses 52092 bytes (2%) of program storage space. Maximum is 2093056 bytes.
```

R3では，そもそもストレージが小さく（31.5Kバイト）て，元のコードサイズも小さいからコード増加量のインパクトが大きい． 
それに対してR4やPicoでは増加率が小さくてストレージに対する影響も少ない． 

このことから，R3のような環境では，どのような表示をさせたいか，どれだけ楽をしたいかとストレージの余裕で決めればいいと思う． 
一方，R4やPicoなら最初からzprintfを使うのもアリ．ていうか，どうしてこれらの環境では標準ライブラリ内でprintfを使うようにしてくれてないんだろう？互換性のためかも知れないけど，今後，R4がボードの主流になってくる頃には，printfが公式ボードパッケージでサポートされることを期待する． 

## 浮動小数点は変換できない 
zprintfの中で使っている`vsnprintf()`が浮動小数点に対応してないとのことで`%f`を使うとその部分が「?」に置き換わる．これは別途対応しないといけないところ． 
でも当面，浮動小数まで細かい表示で出すことは無さそうだから，このままにしておく （´(ｪ)｀;

# 参考 
ここで用いたコード例は，全てこちらにも置いてあります 
https://gist.github.com/teddokano/f2a63ee5aa0a8c0afc1d273c47d67145