モダンOSのお砂場(68) UNO R4、ルネサスRA4M1でFreeRTOS、優先順位

Joseph Halfmoon

前回はDelayに関するAPIでした。今回はTaskのPriorityを操作してみます。今までPriorityはみんな「1」みたいなテキトーな設定をしてきました。優先順位を設定する実験してみることで勉強になるんでないかい、という目論見。しかし、改めて FreeRTOSConfig.h を読んでみると思っていたのと違う?

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

FreeRTOS、taskのPriority

FreeRTOSでは、複数のtaskが並行に動作する(ように見える)わけですが、どのtaskを実行するのかはスケジューラ様に任されてます。簡単にまとめてしまうと、Ready状態にあるtaskの中で「一番優先順位の高いやつ」がRun(実行)状態になります。ReadyやらRunやらタスクの状態については以下の過去回で図を描いてますぞ。

モダンOSのお砂場(29) FreeRTOS、プリエンプティブなスケジューリング

御本家の以下のドキュメントもどうぞ。

Task States

さてこのように重要なTaskのPriorityですが、今までTaskを作るときにはテキトーに1などと書いてみな一緒にしてました。その心は、

    • どのTaskもタイムスライスを使いきることなく制御権を短時間で返納

するからみんな同じで良いよね。だいたいタスクの数も2個3個だし。そこで心を入れ替えて、Taskのプライオリティについて改めてまとめると以下のようです。

    1. プライオリティには0からconfigMAX_PRIORITES -1までの段階がある
    2. タスク生成時にプライオリティの初期値を与える
    3. プライオリティの数が多い方が優先度が高い
    4. スケジューラは次のタスクを選ぶとき、優先度が一番高いものを選ぶ
    5. 何もすることが無いときに走るらしい ILDEタスクのプライオリティは0である。
    6. 同一レベルに複数のタスクを置いても良い。
    7. 同一レベルにおかれたタスクを「ラウンドロビン」スケジュールすることもできるが設定による。
今回登場する各種の設定値については、例によって
FreeRTOSConfig.h
内に記述があります。今回 Arduino IDEのインストールパッケージ renesas_uno のArduino_FreeRTOSの設定値を覗いてみたところでは、
    • configMAX_PRIORITIESは5と設定されていた
です。つまりタスクのプライオリティは0から4まで。どこかでconfigMAX_PRIORITIESは最大32という記述を読んだ気がしてましたが、これは、configUSE_PORT_OPTIMISED_TASK_SELECTION が1のときの上限制限でした。Arduino環境のUNO R4向けのデフォルトでは5とされているみたいです。ただし、configUSE_PORT_OPTIMISED_TASK_SELECTION は0なので32以上の数を設定することもできそうですが、そのときはメモリ使用量が怖いです。
同一レベルに置かれたタスク間でのスケジューリングはラウンドロビンだと思っていたのですが、デフォルト値は違いました。configUSE_TIME_SLICINGのデフォルトは0になってます。つまり、勝手にラウンドロビンしてくれるわけではないので、同一レベル感では自主的に制御権を返納することが望ましい設定だと思います。configUSE_TIME_SLICINGを定義しないか1に設定すればラウンドロビンになるようです。
Priority制御のためのAPI

プライオリティはタスク生成時に初期設定するようになっていますが、2つあるAPIで読んだり書いたりすることが可能です。

    1. uxTaskPriorityGet()、タスクハンドルのプライオリティを返す
    2. vTaskPrioritySet()、 タスクにプライオリティを設定する
どちらも FreeRTOSConfig.h内でマクロを定義しないと使えないです。今回は、デフォルトで使える設定になってました。
実験用のソース

さて、今回の実験で明示的に作ったタスクは4個です。

TASK1~TASK3は、プライオリティにより実行順序が変わることを「体感」するためのTASKです。各TASKとも、実行を開始すると割り当てられた出力端子をHighにし、制御権を手放す前にLowにします。3個とも1ms毎に定周期で実行するように設定してます。外部で出力を観察していれば、いつどのTASKが活動しているのか分かる筈。3個のタスクとも初期のプライオリティは1ですが、以下のタスクにより変更されます。

LOOP TASKは、初期プライオリティは1のまま変わらず、10秒に1回実行される「ゆるゆる」タスクです。このタスクの中で、TASK1~TASK3のうち1個だけにプライオリティを+1します。つまり設定されたタスクは10秒間優先度が高くなりっぱなしっす。優先度をプラスするのは「ラウンドロビン」的な順番です。勿論、優先度があがっても10秒後には元の木阿弥、プライオリティは1に戻されます。

TASKの優先度の様子が以下に(TASK2の優先度を「盛っている」ときの様子。)TASKs
実験に使ったソースコード、Arduino環境向け「スケッチ」.ino 形式が以下です。なお以下でvTaskPrioritySet(tasks[taskidx], PRIOBASE + 1);のところをコメントアウトするとプライオリティを変更しないケースとなります。

#include <Arduino_FreeRTOS.h>
#define T3PIN   (7)
#define T2PIN   (6)
#define T1PIN   (5)
#define LWAIT   (10000)
#define NTASKS  (3)
#define PRIOBASE (1)

TaskHandle_t loop_task;
TaskHandle_t tasks[NTASKS] = {NULL, NULL, NULL};
const char taskname[NTASKS][6] = {"Task1", "Task2", "Task3"};
pin_size_t taskParam[NTASKS] = {T1PIN, T2PIN, T3PIN};

void initPins() {
  pinMode(T1PIN, OUTPUT);
  digitalWrite(T1PIN, 0);
  pinMode(T2PIN, OUTPUT);
  digitalWrite(T2PIN, 0);
  pinMode(T3PIN, OUTPUT);
  digitalWrite(T3PIN, 0);
}

void loop_thread_func(void *pvParameters)
{
  int taskidx = 0;
  while (1) {
    vTaskDelay(LWAIT);
    vTaskPrioritySet(tasks[taskidx], PRIOBASE);
    taskidx = taskidx < (NTASKS -1) ? taskidx + 1 : 0; 
    vTaskPrioritySet(tasks[taskidx], PRIOBASE + 1);
    for (int i=0; i<NTASKS; i++) {
      UBaseType_t prio = uxTaskPriorityGet(tasks[i]);
      Serial.print("Task# ");
      Serial.print(i);
      Serial.print(" Priority=");
      Serial.println(prio);
    }
  }
}

void task_func(void *pvParameters)
{
  TickType_t xLastWakeTime;
  const TickType_t xFrequency = 1;
  pin_size_t pinNumber = *(pin_size_t *)pvParameters;

  xLastWakeTime = xTaskGetTickCount();
  while (1) {
    vTaskDelayUntil(&xLastWakeTime, xFrequency);
    digitalWrite(pinNumber, 1);
    delayMicroseconds(100);
    digitalWrite(pinNumber, 0);
  }
}

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

  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;
  }

  TaskHandle_t xHandle = NULL;
  BaseType_t xReturned;
  for (int i=0; i<NTASKS; i++) {
    xReturned = xTaskCreate ( task_func, taskname[i], 512 / 4, (void *)&taskParam[i], PRIOBASE, &xHandle );
    if (xReturned != pdPASS) {
      Serial.println("Failed to create task.");
      return;
    } else {
      tasks[i] = xHandle;
    }
  }
  Serial.println("Starting scheduler ...");
  vTaskStartScheduler();
  for( ;; ); /* Never! */
}

/* NEVER CALLED! */
void loop() {
}
実機上での実行結果

外部端子をDigilent Analog Discovery2のロジアナ機能で観察しました。時間軸の1Divは1ms=FreeRTOSの1TICK時間です。HIGHになっているところが各タスクが走っている期間。

プライオリティを変更しないケース。

TASKviewNOCHANGE

上記の順番でずっと実行が続きました。同じレベルの中では生成順なのか順番が決まってるみたいね。

本題のプライオリティを変更するケース。

最初にTASK2が勝っているので、TASK2が優先度高くなってるみたい。

TASKview1

お次はTASK1が優先。プライオリティの変更が時々かかっているので順序も洗い替えされている?知らんけど。

TASKview2

思ったように順序を扱えるようになった?まだまだじゃのう。

モダンOSのお砂場(67) UNO R4、ルネサスRA4M1でFreeRTOS、Delay へ戻る

モダンOSのお砂場(69) UNO R4、ルネサスRA4M1でFreeRTOS、ヒープメモリ へ進む