FreeRTOSにおけるタスクスケジューリングの基礎
FreeRTOSでは、異なる優先度を持つタスク間では「優先度スケジューリング」が、同じ優先度を持つタスク間では「ラウンドロビン(タイムスライス)スケジューリング」が採用されます。ここでは、CMSIS-RTOS APIを用いたタスクの生成から、スケジューリングの挙動、およびタスクの待機状態への遷移までを実験を通じて確認します。
開発環境の準備とデバッグ出力
STM32CubeMXで新規プロジェクトを作成し、FreeRTOSを有効化します。また、ログ確認のためにUSART1を有効にしておきます。printf関数を利用可能にするため、usart.cにリターゲット用のコードを追加します。
#ifdef __GNUC__
#define DEBUG_UART_PRINTF int __io_putchar(int data_byte)
#else
#define DEBUG_UART_PRINTF int fputc(int data_byte, FILE *f)
#endif /* __GNUC__ */
DEBUG_UART_PRINTF
{
HAL_UART_Transmit(&huart1, (uint8_t *)&data_byte, 1, 0xFFFF);
return data_byte;
}
タスクの生成手順
CubeMXで自動生成されるデフォルトタスクに加え、手動でタスクを定義・生成する方法を解説します。CMSIS-RTOSでは、osThreadDefマクロでタスクのパラメータを定義し、osThreadCreate関数でインスタンスを生成します。
手動タスク定義の実装
freertos.c内にタスクハンドル、タスク関数のプロトタイプ宣言、および生成コードを記述します。
// タスクハンドルの宣言
osThreadId redLedTaskHandle;
// タスク関数のプロトタイプ宣言
void red_led_callback(void const * argument);
// タスク定義(名前、エントリ関数、優先度、インスタンス数、スタックサイズ)
osThreadDef(led_red, red_led_callback, osPriorityNormal, 0, 128);
// タスク生成
redLedTaskHandle = osThreadCreate(osThread(led_red), NULL);
続いて、タスクの実際の処理を記述します。ここでは、GPIOピンを反転させシリアル出力を行うために、簡易的なビジーウェイト遅延関数を用意します。
void busy_wait(uint32_t cycles)
{
for(uint32_t i = 0; i < cycles; i++)
{
for(uint32_t j = 0; j < 1000; j++);
}
}
void red_led_callback(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
printf("Red LED task is executing\r\n");
busy_wait(500);
}
}
ラウンドロビン(タイムスライス)スケジューリングの確認
デフォルトタスク(緑LED点滅、osPriorityNormal)と、上記で作成した赤LEDタスク(同じくosPriorityNormal)を同時に実行します。両方のタスクが同じ優先度であるため、FreeRTOSはタイムスライスに基づいてCPU時間を交互に割り当てます。その結果、両方のLEDが同時に点滅する挙動を確認できます。
ただし、シリアル出力を観察すると、文字列の出力途中でコンテキストスイッチが発生し、ログが途切れるや交錯する現象が見られます。これは、printfの実行中であってもタイムスライスが尽きれば他のタスクに切り替わるためです。
優先度ベースのプリエンプティブスケジューリング
次に、赤LEDタスクの優先度をosPriorityAboveNormalに引き上げて実行します。
osThreadDef(led_red, red_led_callback, osPriorityAboveNormal, 0, 128);
この状態では、優先度の高い赤LEDタスクがCPUを占有し続け、緑LEDタスクは一度も実行されなくなります。ビジーウェイトを用いている場合、タスクは常に「実行可能」状態にとどまるため、優先度の低いタスクに処理が回ることはありません。
osDelayによるCPUの明け渡し
ビジーウェイトの代わりに、FreeRTOSが提供するosDelayを使用すると挙動が変化します。
void red_led_callback(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
printf("Red LED task is executing\r\n");
osDelay(500);
}
}
osDelayを呼び出すと、呼び出したタスクは指定されたティック数の間「待機」状態へ遷移します。これにより、CPUリソースが解放され、優先度の低い緑LEDタスクが実行可能となり、両方のタスクが交互に動作するようになります。RTOSにおいては、単なる時間潰しではなく、他タスクへ実行権を譲渡するという意味でosDelayの利用が極めて重要になります。