Stream の解説と実装リファレンス

記事内には広告が含まれています。

【概要】

ArduinoにおけるStreamは、データの入出力(I/O)操作を行うための抽象基底クラスです。このクラスは、シリアル通信(Serial)、ネットワーク通信(EthernetClient)、ファイルシステム(File)など、異なる性質を持つハードウェアやリソースに対して統一的なインターフェースを提供します。開発者はStreamクラスのメソッドを介することで、下位の通信プロトコルやデバイスの差異を意識せずに、データの読み書きを行うことができます。

【仕様】

Streamクラスは、以下の主要なメソッドを定義しています。

  • available(): 受信バッファ内に存在する利用可能なバイト数を返す。戻り値はint型。
  • read(): バッファから1バイト読み取る。読み取れない(データがない)場合は-1を返す。戻り値はint型。
  • peek(): バッファの先頭にあるバイトを確認する。読み取り位置を移動させない。戻り値はint型。
  • write(byte): 1バイトのデータを送信する。成功したバイト数を返す。
  • write(const char*): 文字列を送信する。送信した文字数を返す。
  • print(Type data): データ型を解析し、人間が読み取り可能な形式に変換して送信する。数値や文字列の変換処理を含む。
  • println(Type data): print()の処理を行った後、改行コード(\r\n)を付加して送信する。
  • flush(): 送信バッファに残っているデータをすべて物理デバイスへ強制的に出力する。

【技術的特徴】

  • ポリモーフィズムの実現: Streamを継承することで、関数にStreamオブジェクトを引数として渡すことが可能になります。これにより、特定のハードウェアに依存しない汎用的なデータ処理ロジックを記述できます。
  • 内部バッファリング: Serialなどの具体的な実装では、CPUの処理速度とハードウェアの通信速度の乖離を埋めるためのFIFO(First-In, First-Out)バッファを内部に保持しています。
  • 非ブロッキング設計: 多くのStreamの実装において、available()やread()はバッファの状態を即座に返す非ブロッキングな動作を基本とします。ただし、物理的な送信(write)においては、ハードウェアバッファの空き状況に応じて挙動が異なる場合があります。

【注意点】

  • バッファオーバーフロー: 受信データの到着速度が処理速度を上回った場合、内部バッファが溢れてデータが欠落する可能性があります。高速な通信を行う際は、available()を確認しながら効率的に処理する実装が必要です。
  • ブロッキング動作の有無: write()やprint()メソッドは、ハードウェアの送信バッファが満杯になった際に、バッファが空くのを待つ「ブロッキング」動作が発生する実装と、即座に失敗を返す実装があります。使用する具体的なクラス(Serial, EthernetClient, File等)のドキュメントを確認する必要があります。
  • 割り込みコンテキストでの制約: Streamのメソッド(特にバッファ操作を伴うもの)は、ISR(割り込みサービスルーチン)内での呼び出しが安全でない、あるいは禁止されている場合があります。ISR内ではハードウェアレジスタへの直接アクセスを検討してください。
  • メモリ消費: Streamを継承するカスタムクラスを設計する場合、バッファサイズの設定がメモリ消費に直結するため、エンベデッド環境では慎重なサイズ設計が求められます。

【サンプルコード】

以下は、Streamクラスを継承して独自のバッファリング処理を実装する概念的な例です。

// Streamを継承したカスタムクラスの定義// 実際のプロジェクトでは、特定の通信プロトコルの抽象化に使用する\nclass MyDataBuffer : public Stream {
private:
  uint8_t _buffer[32];
  int _index = 0;

public:
  // 利用可能なバイト数を返す
  int available() const override {
    return 32 - _index;
  }

  // バッファから1バイト読み取る
  int read() override {
    if (_index >= 32) return -1;
    return _buffer[_index++];
  }

  // 1バイトをバッファに書き込む
  int write(uint8_t val) override {
    if (_index < 32) {
      _buffer[_index++] = val;
      return 1;
    }
    return 0;
  }

  // 文字列を書き込む(Streamのデフォルト実装を利用)
  int write(const char* str) override {
    int len = 0;
    while (*str != '
    _index < 32 && _buffer[_index++] = *str++;
    len++;
    return len;
  }

  // flushの実装(この例では何もしない)
  int flush() override {
    return 0;
  }
};

void setup() {
  Serial.begin(9600);
  MyDataBuffer myBuf;

  // Streamの共通インターフェースを使用して操作可能
  myBuf.write('H');
  myBuf.write('i');

  while (myBuf.available() > 0) {
    Serial.print(myBuf.read());
  }
}

void loop() {}

【活用例】

  • ユニバーサルロガー: Streamを引数に取るlog(Stream &s, const char* msg)関数を作成することで、シリアルモニタへの出力、SDカードへのファイル保存、ネットワーク経由の送信を、同一のロジックで切り替えることが可能になります。
  • 通信プロトコルの抽象化: TCP/IPやUARTなど、異なる物理層を抽象化して、上位のアプリケーションレイヤー(例:MQTTクライアントや独自のバイナリプロトコル)を実装する際の基盤として活用します。
  • データストリーミング: センサーデータを順次読み取り、特定の処理を経て別の出力先へ転送するパイプライン処理において、Streamインターフェースを統一言語として使用します。

コメント

タイトルとURLをコピーしました