STM32 FreeRTOS バイナリセマフォの動作を確認する

STM32 FreeRTOS バイナリセマフォの動作を確認する

以前、FreeRTOSについて記事を書きましたが今回はバイナリセマフォの動作について見ていきます。

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

PC:Windows10 OS
IDE: STM32CubeIDE Version1.3.0
Configurator: STM32CubeMX Version5.4.0
Board: STM32Nucleo-F401RE

マルチスレッドプログラムにおいて、複数のスレッド間で共有リソースを扱う場合には注意が必要です。
共有リソースとは複数のスレッドで扱うファイルやメモリ等を言います。

今回はシンプルな共有メモリ(変数)の取り扱いについて見てみましょう。

IDEのプロジェクトを作成する

File – New -STM32 Project を選択し、Boart SelectorタブからNUCLEO-F401REを選択しNextボタンを押します。

Project Nameは RtosSemaphore としておきます。
言語はCで良いので、そのままFinishを押します。

Initialize all peripherals with their default Mode? と聞いてくるので、Yesを押します。
This kinod of project is associated with the STM32CubeMx perspectiove.
Do you want to open this perspective now?と聞いてくるので、Yesを押します。

FreeRTOSを追加する

Pinout & Configuration – Categories の Middleware – FREERTOSをチェックします。
右側の FREERTOS Mode and Configuration の Mode , Interface から CMSIS_V1 を選択します。
これはOSのバージョンです。
どちらでも良いのですが、安定しているV1の方を使ってみます。

タスクとセマフォを追加する

Modeの下のConfigurationでTasks & Queuesを選択します。
上のTasksのAddボタンを押します。
小窓が出るので priority osPriorityNormalにかえてOKを押します。
これで2つのスレッドは同じ優先順位を持ちタイムスケジューラーによって処理が切り替わります。

次にTimers & Semaphoresを選択し、中段の Binary SemaphoreのAddボタンを押します。
小窓が出るのでそのままOKします。

次に Pinout & Configuration – Categories の System Coreの SYS – Timebase Source から TIM5 を選択します。

FreeRTOSでは Timebase Sourceは SysTick 以外を使うことが推奨されているからです。

ここで言うタスクはスレッドと同じ意味と考えて構いません。
この先はスレッドという言葉で統一します。

ビルドする

まずビルドしてエラーがないことを確認しておきます。

Project Explorerから Core – Src – main.c をダブルクリックして開きます。

私の環境では

272行目:StartDefaultTask()
290行目:StartTask02()

という2つのスレッドがつくられています。
これらの生成は126行目付近に書かれていて、138行目付近の osKernelStart()によって、これらのスレッドが動き始めます。

ボードをつないで動かしてみる

それでは NUCLEO-F401REボードをUSBケーブルでPCとつないで Run – Debug します。

デバッガが起動したら、2つのスレッドの for(;;)の行番号をダブルクリックしてブレークポイントを貼ります。

Run – Resume してプログラムを動かすとブレークします。
Resume(再開)は F8キーが便利です。

Resume してプログラムを動かすともうひとつのスレッドのブレークポイントでブレークし、どちらのスレッドも動作していることがわかりました。

コーディングする

変数a,b,c,dを追加し、2つのスレッドに以下のコードを実装します。
注意事項として osDelay(1); の部分は削除しておいてください。
なぜかというと、これをきっかけにスレッドが切り替わってしまうためです。

(そのために共有メモリーのアクセス時にスレッドが切り替わらなくなり不具合がわかりにくくなる)

/* USER CODE BEGIN 0 */
int a,b,c,d;
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */
  /* Infinite loop */
  for(;;)
  {
    a++;
    b++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a != d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END 5 */ 
}

void StartTask02(void const * argument)
{
  /* USER CODE BEGIN StartTask02 */
  /* Infinite loop */
  for(;;)
  {
	a++;
	c++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a != d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END StartTask02 */
}

ブレークポイントを貼って実行する

2つのスレッドに以下のコードを実装して、if (a != d)のところ2箇所にブレークポイントを貼って実行します。
aの値が100000を超えたところでブレークします。

この時 a の値は dと一致しないことを確認します。

こちらの環境では a = 100001, d = 112356 でした。かなり違います。

a の値は b + c の値である d と同じになりそうです。

どうしてこのようなことが起こるのでしょうか?

a++の部分ですが、C言語では1行ですが、実際はアセンブラで複数行に渡って命令が書かれています。

Window - Show View - Disassembly でC言語のコードが逆アセンブルされて表示されます。
a++の部分は以下のようになっています。

ldr r3,[pc,#64]
ldr r3,[r3,#0]
adds r3,#1
ldr r2,[pc,#60]
str r3,[r2,#0]

メモリーの値をレジスタr3に持ってきて +1 してから書き戻す処理になっています。
それで、この処理の途中にスレッドの切り替えが起こるとカウントミスが発生します。

例えばひとつ目のスレッドがメモリーから持ってきた値が5だったとします。
1つ加えて6になり、メモリーに保存する前にふたつ目のスレッドに切り替わったとします。
そうするとふたつ目のスレッドがメモリーから持ってきた値も5です。
1つ加えて6になり、メモリーに保存します。
その後、ひとつ目のスレッドに切り替わってメモリーに6を保存することになります。
本来であれば7になるべきところでした。

そこで今回のような共有リソースにアクセスするためにバイナリセマフォを使ってみます。

バイナリセマフォを使う

セマフォにはカウンティングセマフォとバイナリセマフォがあります。

例えがあまりよくありませんが、駐車場をイメージしてみてください。
5台駐車できるところは最大値5のカウンティングセマフォで、たった1台しか駐車できないところはバイナリセマフォのイメージです。

共有ファイルは1度に1箇所からしかアクセスできないようにしなければなりません。
このような場合にはバイナリセマフォを使います。

リソースが使用中の場合には待ち続け、空いたら使うことができるようになります。
そしてリソースを使い終わったら空いたことを知らせてあげる必要があります。

こうして1台分の駐車スペースを使うスレッドで共有します。

待ちには以下の関数を使います。
osSemaphoreWait(myBinarySem01Handle, osWaitForever);

第1引数にはセマフォのハンドルを指定し、第2引数には待ち時間を指定します。
WaitForeverなので、ずっと待つという値です。

空きを知らせるには以下の関数を使います。
osSemaphoreRelease(myBinarySem01Handle);

同じく第1引数にはセマフォのハンドルを指定します。

セマフォを使ったコードは以下のようになります。
共有メモリのアクセス部分を Wait と Release で挟みます。


void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */
  /* Infinite loop */
  for(;;)
  {
  osSemaphoreWait(myBinarySem01Handle, osWaitForever);
  a++;
  osSemaphoreRelease(myBinarySem01Handle);
  b++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a != d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END 5 */ 
}

void StartTask02(void const * argument)
{
  /* USER CODE BEGIN StartTask02 */
  /* Infinite loop */
  for(;;)
  {
  osSemaphoreWait(myBinarySem01Handle, osWaitForever);
  a++;
  osSemaphoreRelease(myBinarySem01Handle);
  c++;
    if (100000 < a)
    {
    	d = b + c;
    	if ( a != d)
    	{
    		b = c;
    	}
    }
  }
  /* USER CODE END StartTask02 */
}

この後ビルドして実行すると a と d が等しくなることが確認できました。
いかがでしたか。今回はごく一般的なバイナリセマフォの扱い方を紹介しました。

FreeRTOSカテゴリの最新記事