ネクスティ エレクトロニクスの開発部隊による技術ブログになります。 当社取り扱い商材を活⽤したリファレンスボードを使ったソフトウェア開発の内容を発信します。
今回はこちらのブログ記事の続きとして、当社で開発したSTマイクロエレクトロニクス社製MCU搭載リファレンスボード(以下STM版リファレンスボード)とFreeRTOSを使用して、タスク間通信を行うソフトウェアを作成していきます。
1. この記事で作るもの(ゴール)
STM版リファレンスボード上でFreeRTOSを動かし、タスク間通信として
- キュー(Queue)で「データ」を渡す
- セマフォ(Binary Semaphore)で「合図(タイミング)」を渡す
を、LED4個の点滅だけで目視確認できる サンプルを作ります。
本記事は 割り込み連携なし(タスク同士のみ) で完結します。
2. 想定読者・前提
- IAR EWでビルド&書き込みができる
- FreeRTOSの「タスク」「Delay」「優先度」の雰囲気が分かる
- LEDは Active Low(GPIO=0で点灯)
3. 使用環境
4. サンプル全体の仕様(先に動きを決める)
4.1 LED割り当て(4つのLEDを何に使うか)
- LED1:Heartbeat(システム生存確認)
- LED2:Queue受信により点滅(データ駆動)
- LED3:Semaphore解放により点滅(イベント駆動)
- LED4:状態表示(キュー送信失敗時にトグル)
4.2 LED制御の約束事(Active Low)
Active Lowのため、GPIOの意味は以下です。
- 点灯:HAL_GPIO_WritePin(..., GPIO_PIN_RESET)
- 消灯:HAL_GPIO_WritePin(..., GPIO_PIN_SET)
このサンプルではmain.c内に以下のヘルパを用意しています。
static void LED_On(GPIO_TypeDef *port, uint16_t pin)
{
HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET);
}
static void LED_Off(GPIO_TypeDef *port, uint16_t pin)
{
HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET);
}
static void LED_Toggle(GPIO_TypeDef *port, uint16_t pin)
{
HAL_GPIO_TogglePin(port, pin);
}
また、GPIO初期化時の出力初期値を 全消灯(SET) にして、起動直後にLEDが点灯しっぱなしになるのを避けています。
HAL_GPIO_WritePin(GPIOK, GPIO_PIN_5|GPIO_PIN_4|GPIO_PIN_6|GPIO_PIN_3, GPIO_PIN_SET);
4.3 タスク構成(割り込みなし)
このサンプルは、CMSIS-RTOS2 API(cmsis_os.h)でタスク・キュー・セマフォを扱います。
- StartDefaultTask(LED1):Heartbeat
- PatternProducerTask:LED2用の点滅パターンを作ってキューへ送る
- LedDriverTask:キューを受信してLED2を点滅させる
- SyncTask:一定周期でセマフォをGive(合図)
- SemBlinkTask:セマフォをTakeしてLED3を点滅
4.4 期待する見え方(動作イメージ)
- LED1:500ms周期で点滅(常に動く)
- LED2:1秒ごとにパターンが変わり、点滅回数が 1→2→3→5… と変化
- LED3:700ms周期で短く点滅
- LED4:キュー送信が失敗した場合のみトグル(通常は変化しない想定)
5. キュー(Queue)でのタスク間通信:設計と実装
ここで言うキューは「 タスク間でデータ(メッセージ)を受け渡しする仕組み 」です。
言い換えると、
- 送信側タスクは「やってほしい内容」を メッセージ化 してキューに入れる
- 受信側タスクはキューからメッセージを取り出して 指示どおりに処理 する
という分業を作れます。
LED制御のような小さな処理でも、
- 点滅パターンを決めるロジック(上位)
- 実際にGPIOを叩くロジック(下位)
を分離でき、規模が大きくなったときに見通しが良くなります。
5.1 キューを使う判断基準(セマフォとの違い)
キューは基本的に「 データそのものを渡したい 」ときに使います。
キュー向き
- パラメータ(数値、構造体、コマンドIDなど)を渡したい
- 受信側に処理を順番に実行してほしい(順序性が欲しい)
- バッファリングしたい(溜めて後で処理したい)
セマフォ向き
- 「起きて処理して」という合図だけで十分(データを渡す必要が薄い)
- 同期や排他が目的
5.2 キューに流すデータ設計
LED2の点滅指示は、次の3項目で表現します。
- blink_count:何回点滅するか
- on_ms:点灯時間
- off_ms:消灯時間
typedef struct
{
uint16_t blink_count;
uint16_t on_ms;
uint16_t off_ms;
} LedBlinkMsg_t;
キューは深さ4で作成しています。
ここでの「深さ4」は、 未処理メッセージを最大4個まで溜められる という意味です。 深さを増やすほど「送信側が少し速くても吸収できる」一方で、メモリ消費は増えます。
g_led2Queue = osMessageQueueNew(4, sizeof(LedBlinkMsg_t), NULL);
5.2.1 ブロッキング時間とCPU負荷
RTOSのキューAPIは、多くの場合「空なら待つ(ブロック)」や「満杯なら待つ(ブロック)」ができます。
このサンプルでは、
- 受信側:osWaitForeverで待つ(=イベントが来るまで寝る)
- 送信側:ブロック時間0(=満杯なら諦める)
という方針にしています。
受信側がブロックできると、待っている間はCPUを消費しません(アイドルタスクが動くだけ)。逆に、ポーリング(whileで何度も見に行く)にすると無駄にCPU時間を使いがちです。
5.2.2 キュー満杯時の方針(落とす/待つ/上書き)
キューが満杯になるのは、典型的には次の状況です。
- 送信側が速すぎる
- 受信側が遅すぎる(処理が重い、優先度が低い等)
満杯時の方針はアプリ次第で、よくある選択肢は次の3つです。
- 落とす(このサンプル):ブロック時間0で送信し、失敗なら捨てる(LED4で可視化)
- 待つ:ブロック時間を設定し、空きができるまで待つ(確実に届けたいが、遅延が増える)
- 上書き:最新値だけが重要な場合は、別の仕組み(共有変数+排他/通知など)にする
LED点滅のようなデモでは「落としても致命的ではない」ので、失敗時はLED4で見える化するだけにしています。
5.3 実装:送信側(PatternProducerTask)
送信側は、いくつかの点滅パターンを順番にキューへ投入します。
- osMessageQueuePut() を使う
- 送信失敗(キュー満杯など)の場合はLED4をトグルして目視できるようにする
const LedBlinkMsg_t patterns[] = {
{ .blink_count = 1, .on_ms = 80, .off_ms = 120 },
{ .blink_count = 2, .on_ms = 80, .off_ms = 120 },
{ .blink_count = 3, .on_ms = 80, .off_ms = 120 },
{ .blink_count = 5, .on_ms = 40, .off_ms = 60 },
};
const osStatus_t st = osMessageQueuePut(g_led2Queue, &msg, 0U, 0U);
if (st != osOK)
{
LED_Toggle(LED4_GPIO_Port, LED4_Pin);
}
5.4 実装:受信側(LedDriverTask)
受信側は osMessageQueueGet() で待ち、受け取ったデータの内容通りにLED2を点滅させます。
if (osMessageQueueGet(g_led2Queue, &msg, NULL, osWaitForever) == osOK)
{
for (uint16_t i = 0; i < msg.blink_count; i++)
{
LED_On(LED2_GPIO_Port, LED2_Pin);
osDelay(msg.on_ms);
LED_Off(LED2_GPIO_Port, LED2_Pin);
osDelay(msg.off_ms);
}
}
ポイントは2つです。
- 受信待ちはブロック(osWaitForever)にしてCPUを無駄に回さない
- Active LowなのでON/OFFを取り違えない(ヘルパで吸収)
5.4.1 このサンプルで「点滅中にキューを見ない」理由
LedDriverTaskは「点滅中」に新しいメッセージを取りに行きません。 つまり、点滅処理が長いほど受信は遅くなり、キューが溜まりやすくなります。
この設計は、
- キューが溜まったらLED4で分かる(観察しやすい)
- 受信側が遅いとどうなるか(設計上のトレードオフ)が体験できる
という意図です。
実アプリで「常に最新コマンドだけ反映したい」なら、
- 点滅処理をさらに小さく分割して定期的にキューを見る
- あるいは「最新指示だけ保持する」構成にする
などの改善が考えられます。
6. セマフォ(Binary Semaphore)でのタスク間同期:設計と実装
6.1 セマフォで解決したいこと
セマフォは「今動いて良い」という 合図 として使います。
ここで使っているのは Binary Semaphore(0/1の状態) で、
- Release(Give)されると「合図が来た」
- Acquire(Take)すると「合図を受け取った」
という扱いになります。
6.1.1 バイナリセマフォの性質(イベントの積み残しに注意)
バイナリセマフォは「最大1」までしか保持できません。 そのため、Releaseが連続すると
- 2回Releaseしても、貯まるのは最大1回分
となり、「イベントを全部数えたい」用途には向きません。
このサンプルでは「LED3を時々点滅させたいだけ」で、 イベントが厳密に1回も漏れない必要がないため、バイナリセマフォがちょうど良いです。
※「イベント回数を落とさず数えたい」なら、Counting SemaphoreやQueue(イベントIDを入れる)が候補になります。
このサンプルでは、SemBlinkTaskを
- 何も起きていないときは寝かせる(ブロック)
- 合図が来たら短くLED3を点滅
という動作にします。
6.2 実装:Give側(SyncTask)
バイナリセマフォは以下で生成しています(初期カウント0)。
g_semBlink = osSemaphoreNew(1, 0, NULL);
SyncTaskは周期的にReleaseします。
ここではosDelay(700)で周期を作っていますが、
- 周期精度が重要ならosDelayUntil相当(CMSIS-RTOS2のosDelayUntil)を使う
- 複数の周期イベントが必要ならソフトウェアタイマを検討する
などが実務ではよくあります。
(void)osSemaphoreRelease(g_semBlink);
osDelay(700);
6.3 実装:Take側(SemBlinkTask)
SemBlinkTaskは osSemaphoreAcquire() で待ちます。
osWaitForeverでブロックしているため、合図が来るまでCPUを消費しません。 これは「待ち」の作り方として非常に重要で、RTOSを使うメリットの一つです。
if (osSemaphoreAcquire(g_semBlink, osWaitForever) == osOK)
{
LED_On(LED3_GPIO_Port, LED3_Pin);
osDelay(50);
LED_Off(LED3_GPIO_Port, LED3_Pin);
}
7. まとめ:キューとセマフォの使い分け
- キュー:データを渡す(何回点滅?何ms?などのパラメータ)
- セマフォ:合図を渡す(今やって良い、というタイミング)
LEDのような単純なI/Oでも、
- 指示(データ)
- タイミング(合図)
を分けて設計すると、タスク構成が読みやすくなります。
8. 付録
8.1 主要API(今回使ったもの)
本記事の実装はCMSIS-RTOS2 APIです。
- タスク作成:osThreadNew
- Delay:osDelay
- キュー:osMessageQueueNew / osMessageQueuePut / osMessageQueueGet
- セマフォ:osSemaphoreNew / osSemaphoreRelease / osSemaphoreAcquire
※目次にあるxQueueSendやxSemaphoreTakeはFreeRTOSネイティブAPIですが、CubeMXの設定によってはCMSIS-RTOS2のラッパAPIで実装する構成になります。今回はmain.cの実装に合わせてCMSIS-RTOS2で統一しています。
8.2 動作確認チェックリスト
- 電源投入直後、LEDが点灯しっぱなしになっていない(Active Low初期値が適切)
- LED1が500ms周期で点滅する
- LED2の点滅回数が周期的に変化する(キュー動作)
- LED3が約700ms周期で短く点滅する(セマフォ同期)
- LED4が常時点滅している場合、キュー送信が失敗している(キュー深さ不足、受信側が遅い等)
9. おわりに
今回はこちらのブログ記事の続きとして、FreeRTOSのキューとセマフォを使用したタスク間通信について解説しました。
LED4個という最小構成ではありますが、「データを渡す」「合図を渡す」というRTOS設計の基本を、コードと挙動を対応づけながら体感できる構成になっています。
実際の製品開発では、割り込み連携、DMA、複数I/O制御、通信スタックとの組み合わせなど、より複雑な要素が加わりますが、その土台となる考え方は今回のサンプルと変わりません。
ネクスティ エレクトロニクス開発部隊では、STマイクロエレクトロニクス社の製品開発や技術サポートを行っていますので、お気軽にお問い合わせください。









