Arduino FreeRTOSでLED点滅とボタン感知を実装した

# 概要

Arduino FreeRTOSの学習としてLED点滅とボタン感知の処理を実装した。

# 環境

- Windows11 Pro（23H2）
- Arduino 1.8.19
- FreeRTOS 11.1.0-3
- Arduino MEGA 2560 ver.3

# 参考

https://qiita.com/ikenohotori/items/8c6285812bc4177c2564

https://qiita.com/infinite1oop/items/aac04a70c1ed71cb83cc

# 実装する内容

- 2種類のLEDを別の周期で点滅させつつ、シリアルポートから入力した回数だけLEDを点滅させる
- 入力した分だけLED点滅させるタスクはシリアル入力で停止および復帰ができる
- ボタンを押すとミューテックスによる排他処理でシリアル出力する

# 回路図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601058/8ae28209-377b-4247-aab6-05f5f7d8ad32.png)

# 実装

```c
#include <Arduino_FreeRTOS.h>   // Arduino FreeRTOS ライブラリ
#include <queue.h>              // キューを使用するためのライブラリ
#include <semphr.h>             // FreeRTOSでセマフォを使用するためのライブラリ

#define LED1 7  //  LED1のポート
#define LED2 6  //  LED2のポート
#define LED3 5  //  LED3のポート
#define DIN_PIN 4 // ボタン押下による入力ピン

#define DEBOUNCE_DELAY 50 // チャタリング防止の待ち時間

// タスクハンドラを定義
TaskHandle_t TaskBlink1_Handler;
TaskHandle_t TaskBlink2_Handler;
TaskHandle_t TaskBlink3_Handler;
TaskHandle_t TaskButton_Handler;
TaskHandle_t TaskSerial_Handler;

QueueHandle_t InterTaskQueue; // キューを定義
SemaphoreHandle_t SerialSemaphore; // セマフォを定義

int LED2BlinkTime = 500;    // LED2に渡す点滅時間

// LED点滅とボタンとシリアル通信のタスクを定義
void TaskBlink1(void *pvParameters);
void TaskBlink2(void *pvParameters);
void TaskBlink3(void *pvParameters);
void TaskButton(void *pvParameters);
void TaskSerial(void* pvParameters);

void setup() {
    Serial.begin(9600);       // 9600bpsでシリアル通信を開始

    while (!Serial) {
        ; // シリアルポートの接続を待機
    }

    Serial.println("Start!");  // 開始文字列を表示
    pinMode(LED3, OUTPUT);    // LED3のピンを出力にする

    if ( SerialSemaphore == NULL )  // シリアル通信ミューテックス（セマフォ）がまだ作られていないことを確認
    {
        SerialSemaphore = xSemaphoreCreateMutex();  // シリアル通信を管理するセマフォ（ミューテックス）を作成
        if ( ( SerialSemaphore ) != NULL )  // ミューテックスが正常に作成されていれば
            xSemaphoreGive( ( SerialSemaphore ) );  // ミューテックスを解放してシリアルポートの利用を有効にする
    }

    // キューを作成する
    InterTaskQueue = xQueueCreate(1, sizeof(int)); // キューの長さ（キューに格納できるアイテムの数）とサイズ、最大で1個のint型データを格納

    if (InterTaskQueue != NULL) {   // キューが作成されている場合
        // LED1の点滅タスクを作成
        xTaskCreate(
            TaskBlink1      // 作成するタスク関数
            ,  "Blink1"     // タスクの名前
            ,  128          // タスクのスタックサイズを指定、使用されたスタックの最大量Stack Highwaterを基に調節する
            ,  NULL         // タスク関数に渡すパラメータ、タスクが開始したときにタスク関数のpvParameters引数として受け取る
            ,  2            // タスクの優先度、 (configMAX_PRIORITIES - 1) = 3が最大、0が最小
            ,  &TaskBlink1_Handler );   // タスクハンドラ

        // LED2の点滅タスクを作成
        xTaskCreate(
            TaskBlink2
            ,  "Blink2"
            ,  128
            ,  (void *)&LED2BlinkTime       // タスク関数にパラメータを渡す
            ,  3
            ,  &TaskBlink2_Handler );

        xTaskCreate(TaskBlink3, "Blink3", 128, NULL, 1, &TaskBlink3_Handler);   // LED3の点滅タスクを作成
        xTaskCreate(TaskButton, "Button", 128, NULL, 1, &TaskButton_Handler);   // ボタン押下時動作のタスクを作成
        xTaskCreate(TaskSerial, "Serial", 128, NULL, 0, &TaskSerial_Handler);   // シリアル通信のタスクを作成
    }
}

// 元のArduinoのループ関数
void loop()
{
    // 何も書かず、タスクで制御する
}

// シリアル通信のタスク関数
void TaskSerial(void* pvParameters){
    (void) pvParameters;    // pvParameterを使っていないことを明示的に示す

    while(1) { // 無限ループ
        while(Serial.available() > 0){              // シリアル通信が利用可能なら
            String str = Serial.readString();       // シリアルポートから入力を読み出す
            if (str[0] == 's') {                    // 押された文字が「s」なら
                vTaskSuspend(TaskBlink1_Handler);   // LED1点滅のタスクを止める
                Serial.println("Suspend!");         // 停止させたことを表示
            } else if (str[0] == 'r') {             // 押された文字が「r」なら
                vTaskResume(TaskBlink1_Handler);    // LED1点滅のタスクを復帰させる
                Serial.println("Resume!");          // 復帰させたことを表示
            } else {                                // 他の入力のとき
                int val = str.toInt();              // int型の値か確認
//                if (val >= 1 && val <= 10) {        // 1～10の範囲なら
                    Serial.print("N=");
                    Serial.println(val);            // 数値を表示
                    xQueueSend(InterTaskQueue, &val, portMAX_DELAY);  // キューを通してTaskBlink1に値を渡す、送信待機時間は無限（送信されるまで待つ）
//                } else {
                    // その他の入力なら何もしない
//                }
            }
            vTaskDelay(1);  // 1ティック=16msの間CPUを開放して、同じ優先度の他のタスクにも処理時間を渡す
        }
    }
}

// ボタン押下処理のタスク関数
void TaskButton(void *pvParameters)
{
    (void) pvParameters;            // pvParameterを使っていないことを明示的に示す
    static int lastState = HIGH;    // 最後のボタンの読み出し値
    int currentState;               // 現在のボタンの読み出し値

    pinMode( DIN_PIN, INPUT_PULLUP );   // ボタン接続ピンを入力プルアップ抵抗をオンにする
    while(1) {  // 無限ループ
        currentState = digitalRead( DIN_PIN );  // 現在ボタンの入力値を読み取る
        if (currentState != lastState) {        // 最後のボタン読み出し値と現在値が異なっていれば
            vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_DELAY));  // 50ms待つ
            if (digitalRead( DIN_PIN ) == currentState) {  // ボタンの読み出し値が変化していなければ確定
                if ( currentState == LOW ){            // 入力がLOW（ボタンが押下されている）のとき
                    // シリアル通信のためのミューテックス（SerialSemaphore）を取得しようとする
                    // 最大5ティック（約80ms）まで待つ
                    // pdTRUEが返れば取得成功、pdFALSEなら失敗
                    if ( xSemaphoreTake( SerialSemaphore, ( TickType_t ) 5 ) == pdTRUE ) {
                        Serial.println("Button Pushed");        // ボタンが押されたことをシリアルモニタに表示
                        vTaskDelay(1500 / portTICK_PERIOD_MS);  // 1.5秒待つ、他のタスクがすぐにSerialSemaphoreを取得してSerial.printlnが衝突しないようにするため
                        xSemaphoreGive( SerialSemaphore );      // SerialSemaphoreを開放して他のタスクがSerial.println()を実行できるようにする
                    }
                }
            }
        }
        vTaskDelay(1); // 1ティック=16msの間CPUを開放して、同じ優先度の他のタスクにも処理時間を渡す
    }
}

// LED1点滅のタスク
void TaskBlink1(void *pvParameters)
{
    (void) pvParameters;    // pvParameterを使っていないことを明示的に示す
    pinMode(LED1, OUTPUT);  // LED1のピンを出力にする

    while(1) {      // 無限ループ
        int count;  // 点滅回数
        // InterTaskQueueのキューが取得できたとき、値をcountに格納
        // キューの受信待機時間は無限（送信されるまで待つ）
        // pdTRUEが返れば取得成功、pdFALSEなら失敗
        if (xQueueReceive(InterTaskQueue, &count, portMAX_DELAY) == pdPASS) {
            for (int i=0; i<count; i++) {   // countの数だけループ
                // シリアル通信のためのミューテックス（SerialSemaphore）を取得しようとする
                // 最大5ティック（約80ms）まで待つ
                // pdTRUEが返れば取得成功、pdFALSEなら失敗
                if ( xSemaphoreTake( SerialSemaphore, ( TickType_t ) 5 ) == pdTRUE ) {
                    char buf[10];
                    sprintf(buf, "Blink: %d", i+1);          // 出力文字列を作成
                    Serial.println(buf);                // 点滅回数を表示
                    xSemaphoreGive( SerialSemaphore );  // SerialSemaphoreを開放して他のタスクがSerial.println()を実行できるようにする
                }
                digitalWrite(LED1, HIGH);                   // LED点灯
                vTaskDelay( 1000 / portTICK_PERIOD_MS );    // 1000ms待機をティックを使って行う
                digitalWrite(LED1, LOW);                    // LED消灯
                vTaskDelay( 1000 / portTICK_PERIOD_MS );    // 1000ms待機をティックを使って行う
            }
        }
    }
}

// LED2点滅のタスク
void TaskBlink2(void *pvParameters)
{
    int *weightTime = (int *)pvParameters;  // pvParameterの値を受け取る
    pinMode(LED2, OUTPUT);  // LED2のピンを出力にする

    while(1) { // A Task shall never return or exit.
        digitalWrite(LED2, HIGH);                       // LED点灯
        vTaskDelay( *weightTime / portTICK_PERIOD_MS ); // 受け取ったpvParameterの値をティックを使って待機時間とする
        digitalWrite(LED2, LOW);                        // LED消灯
        vTaskDelay( *weightTime / portTICK_PERIOD_MS ); // 受け取ったpvParameterの値をティックを使って待機時間とする
    }
}

// LED3点滅のタスク
void TaskBlink3(void *pvParameters) {
    (void) pvParameters;            // pvParameterを使っていないことを明示的に示す
    
    while (1) {         // 無限ループ
        digitalWrite(LED3, HIGH);               // LED点灯
        vTaskDelay( 300 / portTICK_PERIOD_MS ); // 300ms待機をティックを使って行う
        digitalWrite(LED3, LOW);                // LED消灯
        vTaskDelay( 300 / portTICK_PERIOD_MS ); // 300ms待機をティックを使って行う
    }
}
```

# 補足

## ティックについて

### **ティックの概要**

ティックとは、**システムの内部でカウントされる最小単位**のこと。コンピュータシステムでは、タイマーやクロック信号に基づいて一定の時間間隔でカウントが進んでいくが、その単位が「ティック」になる。ティックはいわゆる時計の「チク、タク」から来ているらしい。

### **ティックを使う理由**

リアルタイムOS（RTOS）やFreeRTOSでは、タスクの遅延やスケジューリングにティック単位を使うことで、システムが一定のタイミングで動作するように調整できる。

### **ティックの例**

たとえば、FreeRTOSの設定で **1ティック = 1ms** だとした場合、`vTaskDelay(1000)` は **1000ティック** の間、タスクを待機させることを意味する。ティックの周期が1msなので、これは **1000ms（1秒）** 待機させる。もしティックの周期が **10ms** と設定されていたら、`vTaskDelay(1000)` は **10秒** 待機することになる。

### Arduino FreeRTOSのティック設定

`C:\Users\ユーザー名\Documents\Arduino\libraries\FreeRTOS\src\FreeRTOSVariant.h` 

にある。

公式のリポジトリ見たい方はこちら https://github.com/feilipu/Arduino_FreeRTOS_Library/blob/master/src/FreeRTOSConfig.h

```c
// System Tick - Scheduler timer
// Use the Watchdog timer, and choose the rate at which scheduler interrupts will occur.

/* Watchdog Timer is 128kHz nominal, but 120 kHz at 5V DC and 25 degrees is actually more accurate, from data sheet. */

#ifndef portUSE_WDTO
    #define portUSE_WDTO        WDTO_15MS    // portUSE_WDTO to use the Watchdog Timer for xTaskIncrementTick
#endif

/* Watchdog period options:     WDTO_15MS
                                WDTO_30MS
                                WDTO_60MS
                                WDTO_120MS
                                WDTO_250MS
                                WDTO_500MS
                                WDTO_1S
                                WDTO_2S
*/

#if defined( portUSE_WDTO )

    #define configTICK_RATE_HZ  ( (TickType_t)( (uint32_t)128000 >> (portUSE_WDTO + 11) ) )  // 2^11 = 2048 WDT scaler for 128kHz Timer
    #define portTICK_PERIOD_MS  ( (TickType_t) _BV( portUSE_WDTO + 4 ) )
#else
    #warning "Variant configuration must define `configTICK_RATE_HZ` and `portTICK_PERIOD_MS` as either a macro or a constant"
    #define configTICK_RATE_HZ  1
    #define portTICK_PERIOD_MS  ( (TickType_t) ( 1000 / configTICK_RATE_HZ ) )
#endif

```

デフォルトでは `portTICK_PERIOD_MS=16` 、つまり1ティック=16ms、1秒=62ティックになっている。

## タスクハンドラについて

タスクハンドラ（`TaskHandle_t`型）は、タスクを識別するためのポインタで、タスクの状態を管理したり、他のタスクを操作するために使う。

### 主な使い道

1. **タスクの操作**
    
    タスクを一時停止（`vTaskSuspend`）したり、再開（`vTaskResume`）したりするのに使う。
    
    ```arduino
    vTaskSuspend(TaskHandle); // 指定したタスクを一時停止
    vTaskResume(TaskHandle);  // 指定したタスクを再開
    ```
    
2. **タスクの優先度変更**
    
    実行中のタスクの優先度を動的に変更する。優先度の高いタスクを優先して実行させたいときに使う。
    
    ```arduino
    vTaskPrioritySet(TaskHandle, newPriority); // タスクの優先度を変更
    ```
    
3. **タスクの情報取得**
    
    タスクが現在どれくらいメモリを使っているか、スタックが残りどれくらいあるかといった情報を取得するのに使う。
    
    ```arduino
      // スタックの使用量を取得
      UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(TaskHandle);
      
      Serial.print("Task Blink1 Stack Highwater: ");
      Serial.println(highWaterMark);
    ```
    
4. **タスクの削除**
    
    不要なタスクを削除（`vTaskDelete`）する。
    
    ```cpp
    vTaskDelete(TaskHandle); // タスクを削除
    ```
    

## タスクの優先度について

`FreeRTOSConfig.h` で設定されている。

```c
#define configMAX_PRIORITIES                4
（省略）
#define configTIMER_TASK_PRIORITY           ( configMAX_PRIORITIES-1 )
```

標準は優先度は0～3（0が最小）