Arduino Proのヒント:Print可変長関数vsnprintf()を使用したSerial Print関数の改善


APDahlen Applications Engineer

このたびはArduino Proシリーズ製品をお買い上げいただき、誠にありがとうございます!おそらく図1のようなPortenta H7を購入されたことかと思います。

この新しいパワーをどのように活用しますか?ArduinoのSerialライブラリ、具体的にはSerial.println()関数から始めましょう。

ArduinoのSerial.println()関数の問題点は何でしょうか?

おそらく、Serial.println()関数をやめて、メッセージ全体を1行で印刷できる洗練されたフォーマッタに切り替える時が来たのでしょう。この由緒ある関数は20年近くも私たちの役に立ってきましたが、プロ用機器ではもっと良いものができるはずです。

具体的には、Serial.println()の上に複数のSerial.print()関数を積み重ねる必要がなくなるため、プログラムの乱雑さを減らすことができます。これにより、プログラムが明瞭になり、高度な機能に集中できるようになります。例えば、次の行を印刷するために、Serial.print()関数とSerial.println()関数を何回呼び出す必要があるでしょうか。

Variadic functions are cool! 
 	Runtime is 762 seconds, and this message has been printed 1520 times.

プログラムにもよりますが、関数呼び出しは5回くらいです。もっといい方法があります。

図1: キャリアボードに取り付けたArduino Portenta H7の写真

ArduinoのSerialライブラリのSerial.println()関数を改善するにはどのようにすればよいでしょうか?

改良されたソリューションの中心は、vsnprintf()と呼ばれる便利な関数です。古くて安全でない類似のsprintf()や、改良されたsnprintf()をご存じかもしれません。C言語の標準ライブラリ(stdio.h)にあるすべてのprintf関数は、フォーマットされた文字列と、その文字列に挿入する引数のリストを受け入れます。以下に例を示します。

sprintf(buffer, "Vin: %.2fV, Vout: %.2fV, Gain: %.2f", v_in, v_out, gain);
Serial.println(buffer);

結果は以下のようになります。

Vin: 2.00V, Vout: 5.00V, Gain: 2.50

この例では、フォーマットされた文字列は「Vin: %.2fV, Vout: %.2fV, Gain: %.2f」であり、引数は「v_in、v_out、gain」です。バッファは文字配列であり、フォーマットされた文字列を格納します。そののち、このバッファからArduinoのSerial.println()関数に送られます。

技術的なヒント: sprintf()関数は安全ではないので好ましくありません。気をつけないと、割り当てたバッファを越えて書き込みを続け、それによってプログラム内の他の変数が壊れてしまいます。例えば、50のバッファサイズを割り当てたにもかかわらず、sprint()に100の長さの文字列を送ると、マイクロコントローラのメモリ内において、次の50バイトが上書きされ、これによりプログラムの重要な部分が壊れてしまいます。

経験上、この種のエラーのトラブルシューティングは困難です。断続的なバッファの上書きによるバグのトラブルシューティングにかかる時間を考えれば、snprintf()で余分にかかる事前の労力は妥当なものです。

メッセージフォーマッタの追加によるArduinoのシリアルライブラリの改善

先に進む前に、SerialライブラリがArduinoに深く組み込まれていることを認識してください。コアライブラリとして、SerialライブラリはすべてのArduino製品でマイクロコントローラ向けのUART(Universal Asynchronous Receiver-Transmitter)の設定を処理します。そのため、Serialライブラリを捨ててしまうのは危険です。

その代わり、Serialライブラリの上にフォーマッタを追加します。具体的には、sprintf()のようなフォーマットコマンドを使いますが、少し工夫します。Serialと一緒にvsnprintf()関数を使います。

完全な解決策を検討する前に、この代表的なArduinoの.inoファイルで私たちの解決策がどのように機能するのか見てみましょう。具体的には、Serial.println()関数の上に積み重ねられた少なくとも4つのSerial.print()ステートメントを置き換えるlogger.print()ステートメントを学びます。

#include "Logger.h"
Logger logger(200);

void setup() {
  logger.begin(19200);
}

void loop()  {
  static uint16_t loopCnt = 0;
  uint32_t now = millis();

  logger.print("Variadic functions are cool! \r\n \tRuntime is %lu seconds, and this message has been printed %u times.\r\n\n", now / 1000, loopCnt);

  loopCnt++;
  delay(500);
}

技術的なヒント: sprintf()関数と snprintf()関数は、ほとんどすべての最新のCおよびC++コンパイラのstdio.hにあるコアルーチンです。これらの関数はprintf()と異なり、stdout(標準出力)に直接出力するのではなく、フォーマットされた文字列を文字バッファに格納します。

sprintf()ファミリのフォーマッ タについては、何百冊もの本や何万ものウェブ上の資料を参照してください。%d, %i, %f, \n, and \tなどの一般的なフォーマッタをメモ用紙に書いておくことをお勧めします。

サポートするヘッダファイル(.h)は以下のとおりです。

#ifndef LOGGER_H
#define LOGGER_H

#include <Arduino.h>

class Logger {
public:
    Logger(uint8_t bufferSize = 200); // Custom buffer size
    void begin(uint32_t baudRate = 9600);
    void print(const char* format, ...);

private:
    bool initialized;
    char* tempBuffer;    // Dynamic buffer (allocated in constructor)
    uint8_t bufferSize;  // Size of the buffer
};

#endif // LOGGER_H

技術的なヒント: logger.print()メソッドの存在を知っていれば、他の「.cpp」ファイルでもアクセスできます。loggerオブジェクトへのextern参照を必ずincludeしてください。

サポートするlogger.cppのコードは、以下に記述されています。

#include "Logger.h"
#include <stdarg.h>

/**********************************************************************
 *
 * Constructor for the message logger
 *
 * Assume that the Logger object is permanent—no destructor required.
 *
 *********************************************************************/
Logger::Logger(uint8_t bufferSize)
  : initialized(false), bufferSize(bufferSize) {
  tempBuffer = new char[bufferSize]; 
  memset(tempBuffer, 0, bufferSize); 
}

/*******************************************************************
 *
 * Initialize the object with desired baud rate
 *
 ******************************************************************/
void Logger::begin(uint32_t baudRate) {
  Serial.begin(baudRate);
  while (!Serial) {}
  initialized = true;
}

/******************************************************************************************************************************************************************
 *
 * Print formatted message
 * 
 * Accept a string as if formatted using the old school sprintf. The vsnprintf() function provides length protection based on bufferSize.
 *
 * This is a variadic solution where the ellipsis indicate a variable number of arguments are passed to the method.
 *
 * Example usage: 
 * 
 *   logger.print("Variadic functions are cool! \r\n \tRuntime is %d seconds, and this message has been printed %d times.\r\n\n",  now / 1000, loopCnt);
 *
 *****************************************************************************************************************************************************************/
void Logger::print(const char* format, ...) { // A variadic function
  if (initialized) {
    va_list args;                                     // Declare a va_list buffer to hold the arguments.
    va_start(args, format);                           // Parse out the arguments.
    vsnprintf(tempBuffer, bufferSize, format, args);  // Prepare the string and place it in tempBuffer.
    va_end(args);                                     // clean up.
    Serial.print(tempBuffer);                         // Print the formatted message.
  }
}

おわりに

この技術概要で説明しているフォーマッタ処理オブジェクトは、Arduinoコードの乱雑さを低減する合理的な方法を提供します。Serial.print()関数やSerial.println()関数のスタックの代わりに、1行のコードで済みます。これは、プロ用製品で使用するプロフェッショナルなヒントであり、実際には、ほとんどのArduinoマイクロコントローラで動作します。

これは、ほんの第一歩です。Loggerユーティリティは、他のシリアルポートに拡張されるかも しれません。例えば、EthernetポートやローカルのSDカードにメッセージを記録することができます。

この情報が役に立ったら、「いいね!」をお願いします。また、このアイデアをあなたのプロジェクトにうまく取り入れるために、どのように修正したかも教えてください。

ご健闘をお祈りします。

APDahlen

関連情報

以下のリンクから、関連する情報や 有益な情報をご覧ください。

著者について

Aaron Dahlen氏、LCDR USCG(退役)は、DigiKeyでアプリケーションエンジニアを務めています。彼は、技術者およびエンジニアとしての27年間の軍役を通じて構築されたユニークなエレクトロニクスおよびオートメーションのベースを持っており、これは12年間教壇に立ったことによってさらに強化されました(経験と知識の融合)。ミネソタ州立大学Mankato校でMSEEの学位を取得したDahlen氏は、ABET認定EEプログラムで教鞭をとり、EETプログラムのプログラムコーディネーターを務め、軍の電子技術者にコンポーネントレベルの修理を教えてきました。彼はミネソタ州北部の自宅に戻り、このような記事のリサーチや執筆を楽しんでいます。




オリジナル・ソース(English)