ESP32用同期管理ライブラリ ESP32SyncKit

概要

FreeRTOSのタスクなどを管理するESP32TaskKitやESP32AutoTaskを紹介してきましたが、タスクだけでは細かい制御ができません。キューやミューテックスなどの同期関連の機能と組み合わせることでより便利に使えると思います。

今回はタスクと同時に利用することになる同期についてのライブラリと仕組みの説明になります。

同期とは?

FreeRTOSだと同期系の仕組みとして、メッセージを保存しておき順番に処理をするためのキュー、通知のみを行いデータは送付しないタスク通知、利用可能なリソースを管理するセマフォ、特定のリソースを占有するときに使うミューテックスがあります。

内部的な実装はキューを理解していればあとは使い方の問題なのですが、最初にある程度大まかに用語を把握していたほうが、理解が早いと思います。

キューとは?

データをリストに追加して、追加順で処理をするための処理になります。リストの件数や、追加や取り出しの方法などにより若干使い方が変わります。

01_basic_autotask

ESP32SyncKit::Queue<int> q(8);

void LoopCore0_Normal()
{
  static int counter = 0;
  if (!q.send(counter++))
  {
    Serial.println("[Queue] send failed");
  }
  delay(500);
}

void LoopCore1_Normal()
{
  int value = 0;
  if (q.receive(value))
  {
    Serial.printf("[Queue] core=%d, received=%d\n", xPortGetCoreID(), value);
  }
}

一番シンプルな例となります。int型で8個のリストを作成し、タスク間でメッセージを送信しています。sendでメッセージを送信し、receiveで受信します。8個まではメッセージが保存されていますので、非同期でデータの送付が可能です。

02_taskkit_producer_consumer

ESP32SyncKit::Queue<int> q(8);

void setup()
{
  Serial.begin(115200);

  producer.startLoop(
      []
      {
        static int value = 0;
        if (!q.send(value++, 1000))
        {
          Serial.println("[Queue/TaskKit] send failed");
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "producer"},
      500);

  consumer.startLoop(
      []
      {
        int v = 0;
        if (q.receive(v))
        {
          Serial.printf("[Queue/TaskKit] core=%d, received=%d\n", xPortGetCoreID(), v);
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "consumer", .priority = 2, .core = tskNO_AFFINITY});
}

01と同じ処理をESP32TaskKitで実装した場合です。

03_raw_dualcore_queue

ESP32SyncKit::Queue<int> q(8);

void sender(void * /*pv*/)
{
  int counter = 0;
  for (;;)
  {
    if (!q.send(counter++, 1000))
    {
      Serial.println("[Queue/raw] send failed");
    }
    delay(500);
  }
}

void receiver(void * /*pv*/)
{
  int v = 0;
  for (;;)
  {
    if (q.receive(v))
    {
      Serial.printf("[Queue/raw] core=%d, received=%d\n", xPortGetCoreID(), v);
    }
  }
}

void setup()
{
  Serial.begin(115200);
  xTaskCreatePinnedToCore(sender, "sender", 4096, nullptr, 2, nullptr, 0);
  xTaskCreatePinnedToCore(receiver, "receiver", 4096, nullptr, 2, nullptr, 1);
}

こちらも01と02と同じ処理をタスクライブラリを使わない場合の書き方です。タスクライブラリは好みなので必ずしも使う必要はありません。FreeRTOSを直接呼び出す方がタスクの使い方としては多いとは思います。

04_taskkit_loop_nonblock

constexpr uint32_t kQueueDepth = 8;
constexpr uint32_t kSendIntervalMs = 500;

ESP32SyncKit::Queue<int> q(kQueueDepth);
ESP32TaskKit::Task producer;

void setup()
{
  Serial.begin(115200);

  producer.startLoop(
      []
      {
        static int value = 0;
        if (!q.send(value++, 0))
        {
          Serial.println("[Queue/TaskKit] send failed");
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "producer", .priority = 2},
      kSendIntervalMs);
}

void loop()
{
  int v = 0;
  while (q.tryReceive(v))
  {
    Serial.printf("[Queue/loop] core=%d, received=%d\n", xPortGetCoreID(), v);
  }

  delay(1);
}

送信はタスクですが、受信はloopで行うこともできます。従来のプログラムに組み込むときにはこのような形式が多いと思います。タイマーでの定期実行でI2Cなどからデータを取得し、loopで一括処理を行います。

このときtryReceive関数を呼び出しています。これは必ずしもデータがあるわけではない場合にブロックせずに即失敗する関数となります。whileループをしていますので、データがある場合のみ全件処理する場合の使い方です。

05_irq_gpio_send

#define BUTTON_PIN 0

constexpr uint32_t kQueueDepth = 8;

ESP32SyncKit::Queue<int> q(kQueueDepth);
volatile bool gSendFailed = false;

void IRAM_ATTR onButton()
{
  static int counter = 0;
  if (!q.trySend(counter++))
  {
    gSendFailed = true;
  }
}

void setup()
{
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButton, FALLING);
}

void loop()
{
  if (gSendFailed)
  {
    Serial.println("[Queue/ISR] send failed (queue full)");
    gSendFailed = false;
  }

  int v = 0;
  while (q.tryReceive(v))
  {
    Serial.printf("[Queue/loop] core=%d, got %d (remaining=%lu)\n",
                  xPortGetCoreID(), v, static_cast<unsigned long>(q.count()));
  }

  delay(1);
}

送信はタスクでなくて割り込みからも可能です。GPIO割り込みでメッセージを送信している例となります。割り込みの場合には内部で小数点計算などを実行すると例外でパニックになる場合などがあり、基本は割り込み中は最低限の処理だけを実施し、キューなどの同期機能を利用して通常タスクに処理を渡すことになります。

FreeRTOSの場合には割り込み中にキューの送信をするためには、通常の関数ではなく割り込み中の送信関数を利用する必要がありますが、ESP32SyncKitだと割り込み中を自動判定して内部で切り替えて送信してくれます。

送信失敗をこのサンプルでは処理していますが、このように割り込み中に利用するグローバル変数にはvolatileをつけるなど制限があるので、グローバル変数を利用せずにキューのみで処理をして、送信失敗なども起こらないタイミング設計にすることがおすすめです。

06_taskkit_send_fail_clear

#include <ESP32SyncKit.h>
#include <ESP32TaskKit.h>

#define CLEAR_PIN 0

constexpr uint32_t kQueueDepth = 4;
constexpr int kSpamBatch = 2;
constexpr uint32_t kSpamPeriodMs = 200;

ESP32SyncKit::Queue<int> q(kQueueDepth);
ESP32TaskKit::Task spammer;
volatile bool gSendFailed = false;

void setup()
{
  Serial.begin(115200);
  pinMode(CLEAR_PIN, INPUT_PULLUP);

  spammer.startLoop(
      []
      {
        static int value = 0;
        bool failed = false;
        for (int i = 0; i < kSpamBatch; ++i)
        {
          if (!q.trySend(value++))
          {
            failed = true;
            break;
          }
        }
        if (failed)
        {
          gSendFailed = true;
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "spam", .priority = 2},
      kSpamPeriodMs);
}

void loop()
{
  Serial.println("[Queue/loop] start loop iteration =====================================");

  static int lastLevel = HIGH;
  int level = digitalRead(CLEAR_PIN);
  if (lastLevel == HIGH && level == LOW)
  {
    if (q.clear())
    {
      Serial.println("[Queue/loop] cleared queue on GPIO LOW");
    }
  }
  lastLevel = level;

  if (gSendFailed)
  {
    Serial.println("[Queue/TaskKit spam] send failed (queue full)");
    gSendFailed = false;
  }

  int v = 0;
  while (q.tryReceive(v))
  {
    Serial.printf("[Queue/loop] core=%d, got %d (remaining=%lu)\n",
                  xPortGetCoreID(), v, static_cast<unsigned long>(q.count()));
  }

  delay(500);
}

loopを500msでループさせることで、わざと送信失敗を発生させた例です。基本的に送信失敗は起こらないタイミングにするか、失敗しても特殊処理をしない設計にしたほうがシンプルになります。

07_send_to_front

  producer.startLoop(
      []
      {
        static int value = 0;
        static uint32_t tick = 0;
        ++tick;

        const bool urgent = (tick % kUrgentInterval == 0);
        int payload = urgent ? -1000 - static_cast<int>(tick) : value++;

        if (urgent)
        {
          // en: Push urgent item to the front so it is received next
          // ja: 緊急データを先頭に積んで次に受信されるようにする
          if (!q.sendToFront(payload, 0))
          {
            Serial.println("[Queue/front] sendToFront failed");
          }
        }
        else
        {
          if (!q.send(payload, 0))
          {
            Serial.println("[Queue/front] send failed");
          }
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "front-producer", .priority = 2},
      kNormalPeriodMs);

5回に1度、sendToFrontを利用してキューの先頭に追加しているパターンです。通常はキューは送信順に並んでいますが、sendToFrontを利用することで割り込むことが可能です。

08_mailbox_overwrite

  producer.startLoop(
      []
      {
        static int value = 0;
        if (!mailbox.overwrite(value++))
        {
          Serial.println("[Queue/mailbox] overwrite failed");
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "mailbox-producer", .priority = 2},
      kProducePeriodMs);

キューの使い方でメールボックスと呼ばれるものがあります。キューを1件のみ保存する状態にしてoverwrite関数を利用することで常に最新情報のみを処理する機能となります。

最後の状態のみ表示したい場合にはこのような使い方も可能です。

09_taskkit_struct_payload

struct SensorPacket
{
  uint32_t seq;
  float temperatureC;
  uint32_t timestampMs;
};

ESP32SyncKit::Queue<SensorPacket> q(kQueueDepth);

キューは任意の構造体などを送信することも可能です。FreeRTOSのキューを直接利用する場合には若干面倒なのですがライブラリを利用するとテンプレート機能を利用して、型チェックが可能になっています。

実際のところintなどの値よりも、メッセージ用の構造体を宣言して送信することが多いと思います。

通知とは?

通知はキューからメッセージ送信機能を除いたものです。割り込みがあったなどの通知には変数の送信は不要ですのでより単純な通知機能が利用可能です。

01_counter_autotask

#include <ESP32AutoTask.h>
#include <ESP32SyncKit.h>

ESP32SyncKit::Notify n;

void setup()
{
  Serial.begin(115200);
  ESP32AutoTask::AutoTask.begin();
}

void LoopCore0_Normal()
{
  if (!n.notify())
  {
    Serial.println("[Notify/counter] notify failed");
  }
  delay(1000);
}

void LoopCore1_Normal()
{
  static uint32_t count = 0;
  uint32_t drained = 0;
  while (n.take(0))
  {
    ++drained;
    Serial.printf("[Notify/counter] core=%d, got event %u\n", xPortGetCoreID(), ++count);
  }
  delay(2000);
}

void loop()
{
  delay(1);
}

キューとあまり変わらないのですが、送信元はnotify関数で受信側がtake関数で待ち受けます。この例だとtake(0)で0ms間受信待ちをするので、ノンブロックでデータがあるだけ処理するwhileループになっています。

02_bits_taskkit

#include <ESP32TaskKit.h>
#include <ESP32SyncKit.h>

constexpr uint32_t kBitRxReady = 1 << 0; // RX ready 用ビット
constexpr uint32_t kBitTxDone = 1 << 1;  // TX done 用ビット
constexpr uint32_t kBitNoise = 1 << 2;   // 未使用・ノイズ用ビット(無視される想定)

// ja: Notify ビットモード(TaskKit タスクで利用)
ESP32SyncKit::Notify evt;
ESP32TaskKit::Task producer;
ESP32TaskKit::Task consumer;

void setup()
{
  Serial.begin(115200);

  // ja: TaskKit 送信タスク(優先度2)。200 ms 周期の2フェーズ状態機械
  producer.startLoop(
      []
      {
        enum class Phase
        {
          SendRx,
          SendTx
        };
        static Phase phase = Phase::SendRx;
        static uint8_t cooldown = 0; // 200 ms 単位で間引き

        if (cooldown > 0)
        {
          --cooldown;
          return true;
        }

        if (phase == Phase::SendRx)
        {
          // ja: まず RX ready をセット
          if (!evt.setBits(kBitRxReady))
          {
            Serial.println("[Notify/bits] setBits RX failed");
          }
          phase = Phase::SendTx;
        }
        else
        {
          // ja: 次に TX done をセットし、両ビットが揃った状態にする
          if (!evt.setBits(kBitTxDone))
          {
            Serial.println("[Notify/bits] setBits TX failed");
          }
          // ja: マスク外ビットは waitBits で無視されることを示すため、未使用ビットも立てる
          (void)evt.setBits(kBitNoise);
          phase = Phase::SendRx;
          cooldown = 3; // 次の RX ready まで 600 ms 空ける
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "bits-setter", .priority = 2},
      200);

  // ja: TaskKit 受信タスク(優先度2)。デフォルト 1 ms 周期でビット待ち
  consumer.startLoop(
      []
      {
        static uint32_t lastMs = 0; // 前回受信時刻を保持
        // ja: RX+TX の2ビットが揃うまで待機。マスクしたビットだけを評価し、他のビットは無視。復帰時にクリア。
        if (evt.waitBits(kBitRxReady | kBitTxDone, true, true))
        {
          uint32_t nowMs = millis();
          uint32_t deltaMs = (lastMs == 0) ? 0 : (nowMs - lastMs);
          lastMs = nowMs;
          Serial.printf("[Notify/bits] core=%d, RX ready & TX done @ %lu ms (delta=%lu ms)\n",
                        xPortGetCoreID(),
                        static_cast<unsigned long>(nowMs),
                        static_cast<unsigned long>(deltaMs));
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "bits-waiter", .priority = 2});
}

void loop()
{
  delay(1);
}

非常にわかりにくい利用方法なのですが、bitを利用して条件管理をすることができます。送信側はsetBitsで個別のbitを立てる処理を行います。受信側はwaitBitsを利用して指定したbitが上がっている場合だけ受信可能です。

複数の条件があり、全部揃ったときだけ処理をしたい場合に利用する機能です。実際のところハードウエア系の非同期処理とかで使ったりしますが、一般的な用途ではあまり使うことはないと思います。

03_raw_event_bits

#include <Arduino.h>
#include <ESP32SyncKit.h>

constexpr uint32_t kBitSensor = 1 << 0;
constexpr uint32_t kBitTimeout = 1 << 1;

ESP32SyncKit::Notify evt(ESP32SyncKit::Notify::Mode::Bits);

void producer(void * /*pv*/)
{
  for (;;)
  {
    if (!evt.setBits(kBitSensor))
    {
      Serial.println("[Notify/raw] setBits sensor failed");
    }
    delay(300);
    if (!evt.setBits(kBitTimeout))
    {
      Serial.println("[Notify/raw] setBits timeout failed");
    }
    delay(700);
  }
}

void consumer(void * /*pv*/)
{
  for (;;)
  {
    if (evt.waitBits(kBitSensor | kBitTimeout))
    {
      Serial.printf("[Notify/raw] core=%d, bits=0x%02lx\n",
                    xPortGetCoreID(), (unsigned long)(kBitSensor | kBitTimeout));
    }
  }
}

void setup()
{
  Serial.begin(115200);
  xTaskCreatePinnedToCore(producer, "setBits", 4096, nullptr, 2, nullptr, 0);
  xTaskCreatePinnedToCore(consumer, "waitBits", 4096, nullptr, 2, nullptr, 1);
}

void loop()
{
  delay(1);
}

02をシンプルにして、FreeRTOSのタスクで利用しているパターンです。

04_taskkit_takeall_loop

#include <ESP32TaskKit.h>
#include <ESP32SyncKit.h>

ESP32SyncKit::Notify n;
ESP32TaskKit::Task producer;

void setup()
{
  Serial.begin(115200);

  producer.startLoop(
      []
      {
        for (int i = 0; i < 3; ++i)
        {
          if (!n.notify())
          {
            Serial.println("[Notify/takeAll] notify failed");
          }
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "takeall-producer", .priority = 2},
      400);
}

void loop()
{
  static uint32_t total = 0;
  uint32_t got = n.takeAll(1000);
  if (got > 0)
  {
    total += got;
    uint32_t nowMs = millis();
    Serial.printf("[Notify/takeAll] got batch=%lu (total=%lu) @ %lu ms\n",
                  static_cast<unsigned long>(got),
                  static_cast<unsigned long>(total),
                  static_cast<unsigned long>(nowMs));
  }
}

通知にはカウンタモードと呼ばれる機能があります。notify関数で通知を行い、受信時にtakeAll関数で受信することで、未受信の通知件数を取得することができます。

たとえば音を3回鳴らしたい場合などに、3件送付しておくことで受信側で何回鳴らせばいいのかを把握することができます。キューで鳴らす回数をいれて送信する場合でも同じですので、どちらを利用するかは状況により異なります。個人的にはなるべくキューを利用したほうがシンプルだとは思っていますが、機能的には通知の方が変数がない分軽いです。

セマフォ

セマフォは同時に4同時接続が可能な場合に4と設定することで、リソース管理をすることができる機能になります。実際のところ同時利用は1に制限される事が多く、その場合には次に紹介されるミューテックスとほぼ同じ役割となりますが、セマフォは最初に制限数のカウンタをセットして、そのリソースの利用開始するときにカウントダウン、利用が終わったらカウントアップすることでリソースの許可を行っています。

ただこのライブラリではバイナリセマフォと呼ばれる同時カウントが1に固定されたセマフォのみをサポートしています。バイナリセマフォでは、同時利用を避けるよりはそのリソースが現在利用可能かを制御する使い方がメインとなります。デフォルトは利用不可で、許可を与えると使えるようになります。

01_autotask_start_signal

#include <ESP32AutoTask.h>
#include <ESP32SyncKit.h>

ESP32SyncKit::BinarySemaphore startSignal;

void setup()
{
  Serial.begin(115200);
  ESP32AutoTask::AutoTask.begin();
}

void LoopCore0_Normal()
{
  if (!startSignal.give())
  {
  }
  delay(500);
}

void LoopCore1_Normal()
{
  if (startSignal.take())
  {
    Serial.printf("[BinarySemaphore] core=%d, millis=%lu take!\n", xPortGetCoreID(), static_cast<unsigned long>(millis()));
  }
}

void loop()
{
  delay(1);
}

take関数で許可が得られるまでブロックしてまっています。give関数が定期的に呼ばれるので、呼ばれた瞬間にtake関数のブロッキングが完了してその処理が動きます。

02_taskkit_button_isr

#include <ESP32TaskKit.h>
#include <ESP32SyncKit.h>

constexpr int kButtonPin = 0;

ESP32SyncKit::BinarySemaphore buttonSem;
ESP32TaskKit::Task handlerTask;

void IRAM_ATTR onButton()
{
  if (!buttonSem.give())
  {
  }
}

void setup()
{
  Serial.begin(115200);
  pinMode(kButtonPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(kButtonPin), onButton, FALLING);

  handlerTask.startLoop(
      []
      {
        if (buttonSem.take())
        {
          Serial.printf("[BinarySemaphore] core=%d, button pressed\n", xPortGetCoreID());
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "button", .priority = 2});
}

void loop()
{
  delay(1);
}

GPIO割り込みでgive関数が呼び出されていて、別タスクのtake関数に返却する例です。ほぼ通知と同じ動きとなります。こちらも本来は割り込みから送信するためには別関数を内部で使い分けて呼び出しています。

03_raw_one_shot

#include <Arduino.h>
#include <ESP32SyncKit.h>

ESP32SyncKit::BinarySemaphore sem;

void signaler(void * /*pv*/)
{
  delay(500);
  if (!sem.give())
  {
    Serial.println("[BinarySemaphore/raw] give failed");
  }
  vTaskDelete(nullptr);
}

void waiter(void * /*pv*/)
{
  if (sem.take())
  {
    Serial.printf("[BinarySemaphore/raw] core=%d, got signal\n", xPortGetCoreID());
  }
  vTaskDelete(nullptr);
}

void setup()
{
  Serial.begin(115200);
  xTaskCreatePinnedToCore(signaler, "signaler", 4096, nullptr, 2, nullptr, 0);
  xTaskCreatePinnedToCore(waiter, "waiter", 4096, nullptr, 2, nullptr, 1);
}

void loop()
{
  delay(1);
}

01とほぼ同じでFreeRTOSのタスクを利用した例です。こちらも通知でもよいとは思います。

ミューテックス

ミューテックスは同時にリソースを利用するのを防ぐための仕組みとなります。たとえばI2Cなどのアクセスは複数タスクから同時に実行すると失敗しますので、ミューテックスを利用して同時実行を制限するか、I2C専用のタスクを作成してそこにキューを利用して依頼をする形にするのがおすすめです。

01_autotask_shared_serial

#include <ESP32AutoTask.h>
#include <ESP32SyncKit.h>

ESP32SyncKit::Mutex serialMutex;

void setup()
{
  Serial.begin(115200);
  ESP32AutoTask::AutoTask.begin();
}

void printSafe(const char *msg)
{
  ESP32SyncKit::Mutex::LockGuard lock(serialMutex);
  Serial.println(msg);
}

void LoopCore0_Normal()
{
  printSafe("[Mutex/AutoTask] core0 says hello");
  delay(200);
}

void LoopCore1_Normal()
{
  printSafe("[Mutex/AutoTask] core1 says hi");
  delay(300);
}

void loop()
{
  delay(1);
}

LockGuardという機能を利用して、そのブロックを抜けるまでは排他ロックをするサンプルです。シリアル出力なども複数のタスクから送信すると混ざることがあるのですが、排他ロックをすることできれいに送信可能です。

02_taskkit_sensor_bus

  sensorTask.startLoop(
      []
      {
        static bool holding = false;
        static uint32_t releaseAtMs = 0;
        static uint32_t startMs = 0;

        if (!holding)
        {
          if (busMutex.lock())
          {
            holding = true;
            startMs = millis();
            releaseAtMs = startMs + kSensorHoldMs;
            Serial.printf("[Mutex/TaskKit] sensor locked (hold %lu ms) @ %lu ms\n",
                          static_cast<unsigned long>(kSensorHoldMs),
                          static_cast<unsigned long>(startMs));
          }
          else
          {
            Serial.println("[Mutex/TaskKit] lock failed");
          }
        }
        else if ((int32_t)(millis() - releaseAtMs) >= 0)
        {
          if (!busMutex.unlock())
          {
            Serial.println("[Mutex/TaskKit] unlock failed");
          }
          holding = false;
          uint32_t now = millis();
          Serial.printf("[Mutex/TaskKit] sensor unlocked @ %lu ms (held %lu ms)\n",
                        static_cast<unsigned long>(now),
                        static_cast<unsigned long>(now - startMs));
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "sensor", .priority = 2},
      kMutexLoopIntervalMs);

LockGuardを利用しない場合には明示的にlock関数とunlock関数を呼び出して排他制御を行います。FreeRTOSの場合には明示的呼び出ししかありませんので、lock関数とunlock関数をかならず対応して呼び出して排他制御を管理します。

03_raw_buffer_guard

#include <Arduino.h>
#include <ESP32SyncKit.h>

ESP32SyncKit::Mutex bufMutex;
int sharedCounter = 0;

void writer(void * /*pv*/)
{
  for (;;)
  {
    {
      ESP32SyncKit::Mutex::LockGuard lock(bufMutex);
      if (lock.locked())
      {
        sharedCounter++;
        Serial.printf("[Mutex/raw] writer: %d\n", sharedCounter);
      }
      else
      {
        Serial.println("[Mutex/raw] writer lock failed");
      }
    }
    delay(300);
  }
}

void reader(void * /*pv*/)
{
  for (;;)
  {
    {
      ESP32SyncKit::Mutex::LockGuard lock(bufMutex);
      if (lock.locked())
      {
        Serial.printf("[Mutex/raw] reader: %d\n", sharedCounter);
      }
      else
      {
        Serial.println("[Mutex/raw] reader lock failed");
      }
    }
    delay(500);
  }
}

void setup()
{
  Serial.begin(115200);
  xTaskCreatePinnedToCore(writer, "writer", 4096, nullptr, 2, nullptr, 0);
  xTaskCreatePinnedToCore(reader, "reader", 4096, nullptr, 2, nullptr, 1);
}

void loop()
{
  delay(1);
}

FreeRTOSのタスク呼び出し例です。複数タスクから呼び出すグローバル変数は本来volatileを利用する必要がありますが、自前で排他制御を行うことで不要になっています。ただあまりサンプルとしては好ましくない利用方法だと思います。

カウントアップは通知やキューなどを利用して送信し、カウントアップする場所を特定タスクに固定することで、複数タスクからカウンタを更新するのを防ぐような方がよいと思います。

04_taskkit_lockguard_timeout

  waiterTask.startLoop(
      []
      {
        // ja: ループ遅延前に解放されるようガードのスコープを限定
        {
          ESP32SyncKit::Mutex::LockGuard guard(busMutex, kWaiterTimeoutMs);
          if (guard.locked())
          {
            Serial.printf("[Mutex/LockGuard] waiter got lock (timeout=%lu ms), doing %lu ms work\n",
                          static_cast<unsigned long>(kWaiterTimeoutMs),
                          static_cast<unsigned long>(kWaiterWorkMs));
            delay(kWaiterWorkMs); // 保持中の短い擬似作業
          }
          else
          {
            Serial.printf("[Mutex/LockGuard] waiter lock timeout (timeout=%lu ms)\n",
                          static_cast<unsigned long>(kWaiterTimeoutMs));
          }
        }
        return true;
      },
      ESP32TaskKit::TaskConfig{.name = "waiter", .priority = 2},
      kWaiterLoopMs);

ミューテックスも他のタスクが長時間占有している場合に完了するまで待つのではなく、タイムアウト処理をしたい場合があります。kWaiterTimeoutMsでタイムアウトが可能で0をしているとロックできなかった場合には即時失敗が返ったり、短い時間だけ待つなどの使い分けが可能です。

まとめ

いろいろな処理を紹介してみましたが、似たような機能が多いです。基本はキューを使いながらたまにミューテックスぐらいで問題無いことが多いですが、用語のと目的に差を認識していることは重要だと思います。

また、このライブラリの説明やサンプルを理解することによって、上記のFreeRTOS自体の把握がしやすくなっていると思います。ESP32TaskKitもESP32SyncKitもFreeRTOSを使いやすく薄くラッパーしたライブラリとなりますので、慣れてくるとFreeRTOSの関数を直接呼び出したほうが使いやすいことはあると思います。

この一連のライブラリ群もこれを使ってほしいというよりは、FreeRTOS全体の理解度をあげるために準備した面が大きいです。ぜひ素のFreeRTOSも触ってみてもらいたいと思います。

コメント