モダンOSのお砂場(76) UNO R4でFreeRTOS、ミューテックスを使う一手間

Joseph Halfmoon

前々回バイナリ・セマフォ、前回はカウンティング・セマフォでした。今回ミューテックスで気づきました。手元のFreeRTOS、デフォルトではミューテックス使えるようになってません。ミューテックスって言ったら排他制御業界(そんな業界あるのか?)の大スターじゃないかい。例によってNotification推しのためだろうけど。

※「モダンOSのお砂場」投稿順Indexはこちら

※Arduino IDE上で「スケッチ」形式のソースからFreeRTOSを使って実験してみてます。ターゲット機はArduino UNO R4 Minima。泣く子も黙る?ArmコアのルネサスRA4M1マイコン搭載機です

ミューテックス(ミューテックス・セマフォ)

前々回のバイナリ・セマフォで学んだ通り、Task間の同期に使うならバイナリ・セマフォで良いけれど、排他制御に使うのならばミューテックス使え、というのがFreeRTOSのマニュアルページのおすすめでした。その心を勝手に推測するとこんな感じ(バイナリ・セマフォでやったときのマズイ例)

    1. 優先度の低いタスクが「たまたま」Take成功してリソースを使っている
    2. より優先度の高いタスクがそのリソースを使おうとすると使用中なのでリソースを使えない
    3. 優先度の低いタスクは、より優先度の高いタスク(その中には上のタスクも含まれる)が走っているために、リソースを確保しているのになかなか実行が進まない。
    4. 何チンタラ走ってるんだ、バカ野郎(不適切な言葉づかいです)!何時になったら優先度の高いタスクがリソースを得られるのだ?

ということかと。しかしミューテックス使うと、「優先度の継承」なる技が発動、優先度の高いタスクがミューテックスをTakeしたときに、低いタスクがミューテックスを保持していたら低い方の優先度がTakeしたい高い方並みに昇格するのであります。さっさと済ませてこっちに譲れ、ということじゃね。

そんなチート的能力を秘めているらしいミューテックスですが、手元のFreeRTOSでは先発選手に名を連ねていなかったです。前2回で書いているとおり、FreeRTOS的には、Notificationを使えばもっと軽くセマフォ(ミューテックス含む)系APIで出来ることは皆出来てしまうので、そっちが「推し」ということなのだと思います。ただ、ミューテックスの場合は FreeRTOSConfig.h の以下のマクロすら 0 になってました。

configUSE_MUTEXES

これを 1 に変更しないことにはビルドすらできませぬ。また、ミューテックスの持ち主を知るための xSemaphoreGetMutexHolder() という関数もあるのですが、それを取り込むための以下のマクロなどは存在すらしていません。

INCLUDE_xSemaphoreGetMutexHolder

これもFreeRTOSConig.hに追加しました。上記の設定変更により、ようやく以下のソースコードがビルドできるようになります。

なお、Mutexに関するマニュアルの在処は以下です。

xSemaphoreCreateMutex

実験に使用したソース

Arduino IDEのスケッチ形式のソースです。今回は「存在しない理念上の」リソースを奪い合う?2個のTaskの動作の様子を、「ループ」タスクで観察します。Taskの初期設定プライオリティが異なるなどちょっと小細工もしています。

#include <Arduino_FreeRTOS.h>
#define LED_RED   (7)
#define LED_BLUE  (6)
#define LOOP_W    (1000)
#define TASK1_W   (2500)
#define TASK2_W   (1500)
#define PRIOBASE  (1)
#define ON        (0)
#define OFF       (1)
#define LONG_TIME (0xffff)

TaskHandle_t loop_task;
TaskHandle_t task1, task2;

SemaphoreHandle_t xSemaphore = NULL;

void initPins() {
  pinMode(LED_RED, OUTPUT);
  digitalWrite(LED_RED, OFF);
  pinMode(LED_BLUE, OUTPUT);
  digitalWrite(LED_BLUE, OFF);
}

void loop_thread_func(void *pvParameters)
{
  TaskHandle_t temp;
  while (1) {
    temp = xSemaphoreGetMutexHolder(xSemaphore);
    if (temp == task1) {
      Serial.println("Task1 has the mutex.");
    } if (temp == task2) {
      Serial.println("Task2 has the mutex.");
    } else {
      Serial.println("Not Task1 or Task2.");
    }
    vTaskDelay(LOOP_W);
  }
}

void task_func1(void *pvParameters)
{
  while (1) {
    if( xSemaphoreTake( xSemaphore, LONG_TIME ) == pdTRUE ) {
      digitalWrite(LED_RED, ON);
      vTaskDelay(TASK1_W);
      digitalWrite(LED_RED, OFF);
      xSemaphoreGive( xSemaphore );
    }
    vTaskDelay(TASK1_W);
  }
}

void task_func2(void *pvParameters)
{
  while (1) {
    if( xSemaphoreTake( xSemaphore, LONG_TIME ) == pdTRUE ) {
      digitalWrite(LED_BLUE, ON);
      vTaskDelay(TASK2_W);
      digitalWrite(LED_BLUE, OFF);
      xSemaphoreGive( xSemaphore );
    }
    vTaskDelay(TASK2_W);
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial) { }
  initPins();

  xSemaphore = xSemaphoreCreateMutex();

  if (xTaskCreate ( task_func1, "TASK1", 512 / 4, nullptr, PRIOBASE, &task1 ) != pdPASS) {
      Serial.println("Failed to create task1."); return;
  }

  if (xTaskCreate ( task_func2, "TASK2", 512 / 4, nullptr, (PRIOBASE + 1), &task2 ) != pdPASS) {
      Serial.println("Failed to create task2."); return;
  }

  auto const rc_loop = xTaskCreate (
      loop_thread_func, static_cast<const char*>("Loop Thread"), 512 / 4, nullptr, PRIOBASE, &loop_task
    );
  if (rc_loop != pdPASS) {
    Serial.println("Failed to create 'loop' thread");
    return;
  }

  Serial.println("Starting scheduler ...");
  vTaskStartScheduler();
  for( ;; ); /* Never! */
}

/* NEVER CALLED! */
void loop() {
}
実験結果

今回は、いつもは我関せずと吉例Lチカを続けている「ループタスク」に積極的な監視の約目を割り振りました。ループタスクの中で、以下の関数を使って

xSemaphoreGetMutexHolder() 

どちらがMutexを持っているのか(両者とも寝ているということもある)監視してシリアルモニタに出力してます。結果はこんな感じ。Mutex_Results

一応、Task1とTask2が排他しながら動いてるみたい。Mutexは意外な冷遇だったな。

モダンOSのお砂場(75) UNO R4でFreeRTOS、カウンティング・セマフォ へ戻る

モダンOSのお砂場(77) UNO R4でFreeRTOS、再帰的ミューテックス へ進む