今回はバイナリセマフォを使ってみます。
( FreeRTOS V1 で行ったバイナリセマフォの動作を V2 で再確認してみます )
投稿時の開発環境を記しておきます。
PC:Windows10 OS
IDE: STM32CubeIDE Version1.6.0
Configurator: STM32CubeMX Version6.2.1
Board: STM32Nucleo-F401RE
プロジェクトを作成する
IDEを起動し、File- New – STM32 Project を選択し、Target Selection ウィンドウが出たら Board Selector タブを選択し Boards List から NUCLEO-F401RE を選択し Next ボタンを押します。
Project 名に F401RtosV2BinarySemaphore と入力し、Finishボタンを押します。
Initialize all peripherals with their default Mode ? と聞いてくるので Yesを押します。
RTOSを使えるように設定する
Pinout & Configuration – Categories – Middleware – FREERTOS を選択し、
Mode の Interface で CMSIS_V2 を選択します。
その下の Configuration – Tasks and Queues タブを選択し、 Addボタンを押します。
New Task のウィンドウが出てくるので、上から2つ目の Priority を osPritrityNormal に変更し OK ボタンを押します。
これで優先順位が同じ2つのスレッドが作成されることになります。
次に Timers and Semaphoresタブを選択し、中段 Binary Semaphores のAddボタンを押します。
New Binary Semaphore のウィンドウが出たら OKボタンを押します。
タイムベース ソースを変更する
Categories – System Core – SYS を選択し、SYS Mode and Configuration – Mode で
Timebase Source を Systick から TIM10 に変更しておきます。
FreeRTOSでは Systick 以外を使うことが推奨されているからです。
ビルドしてエラーがないことを確認しておきます。
ビルドする
まずビルドしてエラーがないことを確認しておきます。
Project Explorerから Core – Src – main.c をダブルクリックして開きます。
私の環境では
292行目:StartDefaultTask()
310行目:StartTask02()
という2つのスレッドがつくられています。
これらの生成は143行目付近に書かれていて、157行目付近の osKernelStart()によって、これらのスレッドが動き始めます。
ボードをつないで動かしてみる
それでは NUCLEO-F401REボードをUSBケーブルでPCとつないで Run – Debug します。
デバッガが起動したら、2つのスレッドの osDelay(1);の行番号をダブルクリックしてブレークポイントを貼ります。
Run – Resume してプログラムを動かすとブレークします。
Resume(再開)は F8キーが便利です。
Resume してプログラムを動かすともうひとつのスレッドのブレークポイントでブレークし、どちらのスレッドも動作していることがわかりました。
コーディングする
main.c で以下の記述を確認しておきます。
変数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 = 195753 でした。かなり違います。
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台分の駐車スペースを使うスレッドで共有します。
待ちには以下の関数を使います。
osSemaphoreAcquire(myBinarySem01Handle, osWaitForever);
第1引数にはセマフォのハンドルを指定し、第2引数には待ち時間を指定します。
WaitForeverなので、ずっと待つという指定です。
空きを知らせるには以下の関数を使います。
osSemaphoreRelease(myBinarySem01Handle);
同じく第1引数にはセマフォのハンドルを指定します。
セマフォを使ったコードは以下のようになります。
共有メモリのアクセス部分を Acquire と Release で挟みます。
void StartDefaultTask(void const * argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
osSemaphoreAcquire(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(;;)
{
osSemaphoreAcquire(myBinarySem01Handle, osWaitForever);
a++;
osSemaphoreRelease(myBinarySem01Handle);
c++;
if (100000 < a)
{
d = b + c;
if ( a != d)
{
b = c;
}
}
}
/* USER CODE END StartTask02 */
}
この後ビルドして実行すると a と d が等しくなることが確認できました。
いかがでしたか。今回はごく一般的なバイナリセマフォの扱い方を紹介しました。