前回はタスクの一時停止と再開でしたが、その横で割り込みも使ってました。しかし割り込みとタスクの関係性は薄かったデス。そこで今回は、タスク側とも「分かり合える」スケジューラのタイムスタンプをQueueを使って送信することで割り込み発生タイミングをタスク側に伝えてみたいと思います。
※「モダンOSのお砂場」投稿順Indexはこちら
※Arduino IDE上で「スケッチ」形式のソースからFreeRTOSを使って実験してみてます。ターゲット機はArduino UNO R4 Minima。泣く子も黙る?Armコアのルネサスマイコン搭載機です。
Task Utilities
FreeRTOSの task.h 内で定義されている関数については、以下にマニュアルページがあります。
上記の関数類を駆使すれば、TASKの挙動をいろいろ明らかにすることができるのですが、使うに際してはいろいろと問題もありです。負担が多い割にはデバッグ時に使う目的などの関数が多いので、多くがOFFになっていたりします。ちょっと手元のArduino版のFreeRTOSConfig.hを調べたところ以下のようなマクロがみなOFFってました。
#define configUSE_TRACE_FACILITY (0) #define configUSE_STATS_FORMATTING_FUNCTIONS (0) #define INCLUDE_uxTaskGetStackHighWaterMark (0) #define INCLUDE_eTaskGetState (0)
上記をONにしてちょっと「ディープな」関数を使うのはまた今度ということで、今回はコンフィギュレーションを変更せずに使用できる以下の関数を使ってみたいと思います。
xTaskGetTickCountFromISR()
FreeRTOSのスケジューラが内部に持つ Tickタイマのカウント値を読み出す関数です。スケジューラの管理下にあるTaskはすべてTickタイマによって管理されているので、この値が分かれば「お互いにタイミングが分かりあえる」というものです(まあ、1ms単位だけれども。)そして上記の関数は、ISR(Interrup Service Routine)の中から呼び出し可能デス。ここ重要ね。勿論、ISRからではなくTaskから呼び出し可能な関数も用意されてます。
割り込みハンドラのタイミングをQUEUEでTASKに伝達
第66回でタスクからタスクにQUEUE経由で通信してみることをしてみました。今回もQUEUEを使ってみますが、ISRからの通信はISR専用です。
xQueueSendToBackFromISR
使い方はTask用の関数と大差ないのでまあ、いいかという感じっす。
実験に使用したソース
例によって、前回使用したソースのチョイ変です。QUEUEを一本追加して、その中をISRからタスクに向かってTickCountが流れるようにしています。そしてTaskをサスペンド、リジュームする際にその「引き金をひいた」タイミングとしてタスク側で表示してます。
#include <Arduino_FreeRTOS.h> #define NTASKS (3) #define LED_RED (7) #define LED_BLUE (6) #define LED_GREEN (5) #define BUTTON (3) #define LOOP_W (100) #define LED_W (200) #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] = {LED_RED, LED_BLUE, LED_GREEN}; volatile int suspended; const uint8_t queueSize = 5; QueueHandle_t msgQueue; volatile int errCount; void initPins() { pinMode(LED_RED, OUTPUT); digitalWrite(LED_RED, 0); pinMode(LED_BLUE, OUTPUT); digitalWrite(LED_BLUE, 0); pinMode(LED_GREEN, OUTPUT); digitalWrite(LED_GREEN, 0); } void loop_thread_func(void *pvParameters) { TickType_t ts; int oldsuspended = suspended; while (1) { vTaskDelay(LOOP_W); if (oldsuspended != suspended) { Serial.print("OLD: "); Serial.print(oldsuspended); Serial.print(" NEW: "); Serial.println(suspended); if (oldsuspended < 3) { vTaskResume(tasks[oldsuspended]); } if (suspended < 3) { vTaskSuspend(tasks[suspended]); } oldsuspended = suspended; } while (xQueueReceive(msgQueue, (void *)&ts, 0) == pdTRUE) { Serial.println(ts); } } } void task_func(void *pvParameters) { TickType_t xLastWakeTime; pin_size_t pinNumber = *(pin_size_t *)pvParameters; const TickType_t xTimeInc = pinNumber * LED_W; while (1) { vTaskDelay(xTimeInc); digitalWrite(pinNumber, 0); vTaskDelay(LED_W); digitalWrite(pinNumber, 1); } } void selTask() { TickType_t tstamp; BaseType_t xHigherPriorityTaskWoken; suspended = suspended < NTASKS ? suspended + 1 : 0; tstamp = xTaskGetTickCountFromISR(); xHigherPriorityTaskWoken = pdFALSE; if (xQueueSendToBackFromISR(msgQueue, &tstamp, &xHigherPriorityTaskWoken) != pdTRUE) { errCount++; } } void setup() { Serial.begin(115200); while (!Serial) { } initPins(); pinMode(BUTTON, INPUT_PULLUP); suspended = NTASKS; attachInterrupt(digitalPinToInterrupt(BUTTON), selTask, FALLING); errCount = 0; msgQueue = xQueueCreate(queueSize, sizeof(TickType_t)); 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() { }
実験結果
上記のコードをビルドして走らせ、Arduino IDE付属のシリアルモニタで受信した実行結果が以下に。
前回同様、手動でボタンを押す度に、サスペンドされていたOLDタスクがリジュームされ、NEWタスクが今度はサスペンドされるようになってます。番号0,1,2は実体タスクありでぞれぞれがLEDを異なる周期でLチカしてます。サスペンドされるとLチカは該当タスクの管理下のLEDのみ停止します。ここまでは前回と同じです。
今回は上記の動作に加えてボタンを押したタイミングを割り込みハンドラからTickCounter値でもらって表示するようになっています。
スケジューラのTickCounterはArduino UNO R4のFreeRTOSのデフォルト設定では、1ms毎カウントで32ビット幅です。1か月程度であればラップしない筈なので、日々のカウントであればOK(何が?)じゃないかと。知らんけど。