ESP32 RTOSでセマフォを使ってみる (Arduino)

ESP32 RTOSでセマフォを使ってみる (Arduino)

今回も Arduino のフレームワークで RTOS を使います。
今回はセマフォを使ってみます。

この記事は JTAG でデバッグすることを前提にして書いています。
環境構築については こちら をご覧になってください。

投稿時の開発環境を記しておきます。

PC:
Windows10 OS

開発ボード :
ESP32-DevKitCーVE
(Soc : ESP32-D0WD-V3)

デバッガー(H/W):
FT2232D

デバッガー (S/W) :
Visual Studio Code + PlatformIO + Arduino Framework

FreeRTOS

こちら が本家ですが、ESP32用としては このへん を見るのが良さそうです。

セマフォとは

セマフォとはRTOS環境下において複数のタスクが動作する中で、共有資源に対して同時アクセスしても問題が起こらないようにするしくみのひとつです。
このようなしくみを排他制御と言います。

バイナリーセマフォとカウンディングセマフォがありますが前者の方が良く使われているようです。

(勉強不足のせいか私はカウンディングセマフォの実例を見たことがありません)

というわけで今回はバイナリーセマフォを使います。

プロジェクトをつくる

VSCodeで、もし使っていたプロジェクトを開いていたら、File – Close Folder して閉じておきます。

その後にVSCodeからPlatformIOをOpenします。

以下の内容でプロジェクトを新規に作成します。

Name : ESP32A-RTOS-Semaphore
Board : Espressif ESP32 Dev Module
Framework : Arduino Framework

Name : ESP32A の “A” は Framework (Arduino Framework)の頭文字を示しています。

(後から見てわかるように、Arduinoを使うことを明示しています)

platformio.ini に以下の4行を追加して、 Ctrl + s で保存しておきます。

COM[8]の8の部分はデバイスマネージャーのポート(COMとLPT)で Silicon Labs CP210x から始まるCOMの番号を記述します。

upload_port はプログラムをアップロードする時に使うCOMポートを指定します。
monitor_port は printf()出力をモニターするCOMポートを指定します。

debug_tool = minimodule
upload_port = COM[8]
monitor_port = COM[8]
monitor_speed = 115200

今回使うセマフォに関連するAPI

xSemaphoreCreateBinary()

機能:セマフォを作成する
引数:なし
戻り値:作成されたセマフォへのハンドルが返る。つくられない場合にはNULLが返る

xSemaphoreTake()

機能:セマフォを獲得する
第1引数:セマフォへのハンドル
第2引数:セマフォが使えるようになるまでの待機時間
戻り値:作成されたセマフォへのハンドルが返る。つくられない場合にはNULLが返る

xSemaphoreGive()

機能:セマフォを開放する
第1引数:セマフォへのハンドル
戻り値:セマフォが解放されたかどうか

コーディングする

main.cppを以下のようにコーディングします。

Task0(), Task1()内の xSemaphoreTake()と xSemaphoreGive() はコメントにしておき、まずはセマフォなしの動きを確認します。

#include <Arduino.h>

TaskHandle_t taskHandle[2];
SemaphoreHandle_t xSemaphore;

#define IO25 25
#define IO26 26

int a, b, c, d, e = 0;

void Task0(void *args) {

  SemaphoreHandle_t semaphore = (SemaphoreHandle_t)args;

  digitalWrite(IO25, HIGH);
  while (true) {
    //    xSemaphoreTake(semaphore, portMAX_DELAY);
    a++;
    //    xSemaphoreGive(semaphore);
    b++;

    digitalWrite(IO25, LOW);
    digitalWrite(IO25, HIGH);

    d = b + c;
    if (1000000 <= d) {
      digitalWrite(IO25, LOW);
      printf("Task0 : a = %d, b = %d, c = %d, d = %d\r\n", a, b, c, d);
      vTaskSuspend(taskHandle[0]);
    }
  }
}

void Task1(void *args) {

  SemaphoreHandle_t semaphore = (SemaphoreHandle_t)args;

  digitalWrite(IO26, HIGH);
  while (true) {
    //    xSemaphoreTake(semaphore, portMAX_DELAY);
    a++;
    //    xSemaphoreGive(semaphore);
    c++;

    digitalWrite(IO26, LOW);
    digitalWrite(IO26, HIGH);

    e = b + c;
    if (1000000 <= e) {
      digitalWrite(IO26, LOW);
      printf("Task1 : a = %d, b = %d, c = %d, e = %d\r\n", a, b, c, e);
      vTaskSuspend(taskHandle[1]);
    }
  }
}

void CreateTasks(void *args) {

  xTaskCreatePinnedToCore(Task0, "Task0", 4096, xSemaphore, 2, &taskHandle[0],
                          APP_CPU_NUM);
  xTaskCreatePinnedToCore(Task1, "Task1", 4096, xSemaphore, 2, &taskHandle[1],
                          APP_CPU_NUM);

  while (true) {
    ;
  }
}

void setup() {

  pinMode(IO25, OUTPUT);
  pinMode(IO26, OUTPUT);
  digitalWrite(IO25, LOW);
  digitalWrite(IO26, LOW);

  xSemaphore = xSemaphoreCreateBinary();
  xSemaphoreGive(xSemaphore);
  xTaskCreatePinnedToCore(CreateTasks, "CreateTasks", 4096, NULL, 2,
                          &taskHandle[0], PRO_CPU_NUM);
}

void loop() {
}

ビルドして実行する

ビルドしてエラーがないことを確認して、メニューから Run – Start Debugging (またはF5キー)すると少ししてから

loopTaskWDTEnabled = false; の行で停止します。

もう一度 F5キーを押すとプログラムを実行します。

その後アクティビティバーのPlatformIOのアイコンを選択してツリー表示させて Monitor を選択します。

するとTERMINAL に実行結果が表示されます。

なぜか d (またはe) の値と a が一致してしまうこともあるのですが、異なる値になればセマフォを使わない場合の目的を果たしています。

なぜ異なるのか、その理由がお分かりになりますか?

カウントミスする動作

a++ は1つの命令に見えますが実際は違います。

aはRAMに格納されている変数ですから、aをインクリメント(+1)する動作は次のようになります。

r <- a (レジスタr に RAM上の a の値を読む) …(1)
inc r (レジスタrの値を+1する) …(2)
r -> a (RAM 上のa にレジスタrの値を書く) …(3)

今仮に a の値が 100 だったとします。
Task0()が(2)の動作中でたまたまタスク切り替えが起こったとします。
Inc r で rの値は101 になりました。

その後Task1()でも rにaの値を読むと r は100です。
そのままTask1()が動くので rの値を+1してaに書きます。
この時点でaの値は101です。
Task1()の処理は1msecが経過するまで続きます。
(今回のコードにおいてタスク切り替えはタイムスライスにより1msec毎に発生します)

その間 aの値は増え続けます。

やがて1msecが経過してTask0()で(3)の動作が行われます。
A の値は 101 です。
先ほど切り替わった後のTask1()で加算されたaの値が全て無効になってしまいました。

以上の理由から切り替わる場所が悪いと、aの値が思うように増えない現象が起きます。

このように共有の資源(この場合a)に対して複数の処理が同時に実行されると破綻をきたす部分をクリティカルセクションと言います。

以上の理由で複数のタスクから共有の資源に対するアクセスする場合には排他処理が必要になります。

セマフォを実装する

Task0(), Task1()内のクリティカルセクションに対してセマフォを実装します。

// xSemaphoreTake()と
// xSemaphoreGive()の // をはずすことでセマフォを実装することができます。

これによりセマフォがクリティカルセクション a の排他制御を行ってくれます。

xSemaphoreTake()の方はセマフォを獲得しに行き、他のタスクで使われている場合には待たされます。
そして使用可能になったらクリティカルセクションの処理を行います。
処理を終えたらxSemaphoreGive()でセマフォを開放します。
セマフォを使うことで a へのアクセスが重ならなくなるのでカウントミスがなくなります。

ビルドして実行すると、 d(またはe) と a の値が大きく異なることはなくなります。

いかがでしたか?
セマフォをうまく使うことができましたか。

Semaphoreカテゴリの最新記事