ESP32用ヘルパーライブラリ その3 キュー

概要

ESP32用ヘルパーライブラリ その2 タスク
概要 前回はシリアルモニター経由でリセットなどがかんたんに操作できるラッパークラスを紹介しました。今回はマルチタスク操作になります。 ESP32のコア構成 ESP32はSOLOというシングルコアのものを除いて、2コアあります。ただしESP3...

前回はタスクでしたが、今回はタスクと組み合わせて利用することが多いキューになります。データの受け渡し機能になりますが、タスク間でのタイミング連動などにも利用されています。

キューとは?

FreeRTOSのキューにはいろいろな使い方があります。排他制御で使うミューテックスやセマフォなども内部的にはキューと同じような動作となります。

複数のデータを一度に受け渡す

一番メインの機能が複数のデータを受け渡すものになります。湿度と温度を定期的に取得するタスクと、別タスクでそのデータを表示するプログラムがあるとします。

float ondo;
float shitudo;

void updateTask(){
  while(1) {
    ondo = getOndo();

    // ここで表示タスクが動作して、ondoしか更新できていない状態で表示

    shitudo = getShitudo();

    delay(1000);
  }
}

マルチタスクで動作していると湿度はまだ更新していない状態で、温度だけ更新したデータを表示してしまう可能性があります。ある程度直近のデータを表示すればよい場合には問題ありませんが、このデータを使って何かを計算する場合には困ってしまいます。

typedef struct {
  float ondo;
  float shitudo;
} queue_data_t;

void updateTask(){
  while(1) {
    // データ取得
    queue_data_t data;
    data.ondo = getOndo();
    data.shitudo = getShitudo();

    // キューの送信
    queue1.send(&data);

    delay(1000);
  }
}

キューを利用すると、構造体などでデータの受け渡しが可能になります。そのため、その構造体のデータには中途半端なデータが入っていないことが保証されます。

データを保管しておく

キューには複数のデータを保管する機能もあります。これにより、30秒に1度データを取得して5分に一度Wi-Fiを使って送信する場合などに配列のかわりに使うことができます。

void updateTask(){
  while(1) {
    // データ取得
    queue_data_t data;
    data.ondo = getOndo();
    data.shitudo = getShitudo();

    // キューの送信
    queue1.send(&data);

    delay(30000);
  }
}

void sendTask(){
  while (1) {
    queue_data_t data;
    while(queue1.receiveNoWait(&data)){
      // キューに溜まっているデータを全部送信する
      sendData(data);
    }
    delay(300000);
  }
}

単純に保存しておくだけであれば配列でもよいのですが、マルチスレッドで追加と取得が確実に行えるのはキューを使ったメリットとなります。

タイミング同期

データを受信するまで待機する機能がキューにあります。

void waitTask() {
  while (1) {
    queue_data_t data;
    queue2.receiveWait(&data);
    sendData(data);
    delay(1);
  }
}

上記のように受信するまでスリープしながら待つことが可能です。この場合には受信した瞬間にこのタスクの動作が復帰します。ただし、タスクの優先順位もありますので受信待ちをするタスクは優先順位を高めにしておく必要があります。

これは入力待ちや、いつ受信するかわからないデータを最優先で処理をしたい場合によく利用される方法になります。

キュー受信方法

キューの受信は3種類の考え方があります。受信エラーはキューが空である場合となります。

ノンブロック(受信街をせずにエラー時は即時返却)

loop()関数などでデータがある場合だけ処理をしたい場合にはノンブロックで取得をして、空の場合には即時に終了する使い方がシンプルです。

タイムアウト付き受信

一定期間キューにデータ取得待ちをしますが、データがこない場合にはタイムアウトで終了します。定期的にデータが来るはずの場合に異常判定としてタイムアウトを設定しておくのは良いかと思いますが、リアルタイム性が落ちるので使い方が難しいです。

ブロック(タイムアウト無限待機)

データが来るまでスリープして待機しています。受信専用タスクを用意して、その中で利用するパターンが多いです。

キュー送信方法

キューの送信は1つ増えて、4種類の考え方があります。送信エラーはキューが一杯である場合となります。

ノンブロック(送信待ちをせずにエラー時は即時返却)

タイムアウトの時間を0に設定することで、送信できない場合にはエラーで即時返却されます。即時性が重要な場合にはキューが一杯の場合でもブロックしない運用が好ましいです。

とはいえ、キューが一杯のときにはそのデータが失われるので注意しましょう。

タイムアウト付き送信

1秒などのタイムアウト時間を設定して送信します。キューに空きがある場合には即時成功して終了。空きがない場合にはタイムアウトまで待機します。

一時的にデータが詰まっている場合にはタイムアウトを設定したほうがよい場合があります。ただし、ブロックされている時間はリアルタイム性が失われる可能性がありますので注意が必要です。慢性的にタイムアウトする状態であればタイムアウト分操作感が悪くなるだけです。

ブロック(タイムアウト無限待機)

排他制御で、あらかじめキューを一杯にしておき、ブロック状態で送信をすると他のタスクから取得するまで待機しているタスクができます。通常はブロック受信をしたほうがシンプルになるはずです。

上書き(ノンブロック)

メールボックスと呼ばれる最後のデータだけ保持するキューの使い方では、データを1つだけ保管できるように設定をしておき、上書き設定で送信することでキューの状態に影響せずに送信することが可能です。

EspEasyQueueではサポートしていません。要望があれば別クラスとしての実装をしたいと思います。

EspEasyQueueの使い方

#include "EspEasyQueue.h"
#include "EspEasyTask.h"

typedef struct {
  uint32_t time;
  uint8_t val;
} queue_data_t;

EspEasyQueue queue1;
EspEasyQueue queue2;
EspEasyTask task1;
EspEasyTask task2;

void loopTask1() {
  while (1) {
    queue_data_t data;
    // NoWait > process only what is in the queue
    while(queue1.receiveNoWait(&data)){
      Serial.printf("queue1 time=%d, val=%d\n",data.time, data.val);
    }
    delay(3000);
  }
}

void loopTask2() {
  while (1) {
    queue_data_t data;
    // Wait > Process queues in real time as they are added
    queue2.receiveWait(&data);
    Serial.printf("queue2 time=%d, val=%d\n",data.time, data.val);
    delay(1);
  }
}

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("Queue test");

  queue1.create(10, sizeof(queue_data_t));
  queue2.create(10, sizeof(queue_data_t));

  task1.begin(loopTask1);
  task2.begin(loopTask2);
}

void loop() {
    queue_data_t data;
    data.time = millis();
    data.val = random(0, 256);

    queue1.send(&data);
    queue2.send(&data);

    delay(1000);
}

Task1は3秒間隔で実行して、その時にあるデータすべてをノンブロック受信をして処理しています。Task2はブロック受信をして、受信した瞬間に処理をしています。この2種類が比較的よく使う使い方だと思います。

まとめ

クラスの使い方自体は単純なのであまり細かく解説していません。実際の処理もそれほど長くはないので、中身を実際に確認してみるとよいと思います。

どちらかというとこのクラスを使うよりは、このクラスの使い方を学んでもらって、コピペして自分の使いやすいクラスを作って貰えればなと思っています。

https://github.com/tanakamasayuki/EspEasyUtils/blob/master/src/EspEasyQueue.h

上記が該当クラスになります。なんとサンプルスケッチより短いです。

送信だけはsendFromISR()という、割り込みから利用する専用関数があります。割り込みはEspEasyUtilsだとすべて隠匿する方向ですので通常は使うことは無いはずですが、キューを時前で処理する場合には必要になるので覚えておいてください。

コメント