STM32でNUTTXを使ってみる

Task Control Interfaces


NuttxのAPIはこ ちらに 詳しく公開されています。
Linuxのアプリを開発したことがある方ならば、同じ関数が使えることに気が付くと思いますが、
Task Control InterfacesのAPIはかなり趣が違っています。
また、タスクスケジュールのやり方(スケジューリングポリシー)がRTOS独特のルールに従っています。
NuttxにはFIFO(FCFSと呼ばれることもあります)、ラウンドロビン(RRと呼ばれることも有ります)、散発スケジューラの3つのスケ ジュールポリシーが有ります。

Nuttxのスケジュールポリシーについては、こ ちらにドキュメントが公開されていて、デフォルトのスケジュールポリシーはFIFOで、
ラウンドロビンと散発スケジューラはオプションと書かれていますが、
なぜか、STM32のデフォルトのスケジュールポリシーはラウンドロビンとなっていて、以下のルールに従います。

条件@
優先度の一番高いタスクが実行可能になったら、これよりも低い優先度のタスクが実行中であっても
優先度の一番高いタスクに実行権が移る。

条件A
優先度の一番高いタスクが実行中になったら、自分で実行権を放棄しない限り、動き続ける。
実行権を他のタスクに横取り(pre-empted)されることはない。
条件@に従い、自分よりも優先度の高いタスクが実行可能になったら、実行権は剥奪される。

条件B
優先度の一番高いタスクが複数あるときは、一定の時間間隔で優先度の一番高いタスクが同時に実行される。
時間間隔はCONFIG_RR_INTERVALの値に従う。

この動作を確認するためのサンプルアプリを公開しています。
こちらの手順でapps/examplesディレクトリに追加してくださ い。
以下の手順でサンプルアプリをファームに組み込みます。
$ make clean
$ make menuconfig







ここまで変更したらひたすらExitで抜けます。
最後に以下の画面で[.config]を上書きします。


新しい[.config]を使ってファームウェアをビルドし、新しいファームウェアをマイコンに書き込みます。
nshに接続するとtask_testアプリが追加されています。


psコマンドを実行すると、スケジュールポリシーがラウンドロビンになっていることが分かります。
Idle Taskは何も処理するタスクが無いときにCPUを単純に消費するタスクで、優先度が0(最低)のタスクで、これだけはFIFOで動きます。
nsh> ps
  PID GROUP PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK   STACK COMMAND
    0     0   0 FIFO     Kthread N-- Ready              00000000 001000 Idle Task
    2     2 100 RR       Task    --- Running            00000000 002000 nsh_main

引数無しで実行すると、タスクプライオリティの標準値、最大値、最小値を表示します。
nsh> task_test
Task Control Interfaces example
sched_get_priority_std=100
sched_get_priority_max=255
sched_get_priority_min=1
CONFIG_VERSION_MAJOR=12
CONFIG_VERSION_MINOR=3
CONFIG_VERSION_PATCH=0
task_test Finish

F4 Discoveryでusbnshを使っているときは、syslogの出力先がRAMLOGになっているのでdmesgを使う必要が有ります。
nsh> task_test
nsh> dmesg
Task Control Interfaces example
sched_get_priority_std=100
sched_get_priority_max=255
sched_get_priority_min=1
CONFIG_VERSION_MAJOR=12
CONFIG_VERSION_MINOR=3
CONFIG_VERSION_PATCH=0
task_test Finish



タスクの実行はtask_create APIを使います。
task_fork関数はそのラップ関数です。

task_forkの1番目の引数はタスク名
task_forkの2番目の引数はタスクの優先度(プライオリティ)
task_forkの3番目の引数はタスク内の繰り返し処理回数
task_forkの4番目の引数はタスク内の待ち時間

タスク名や優先度を変えて、複数のタスクを起動するので、処理を共通化しています。



test1の引数で実行すると、以下のコードを実行し、以下の表示となります。
各タスクは同じ優先度で起動し、10000000回の繰り返し処理で待ち時間はありません。
    task_fork("myTask1", prio_std, 10000000, 0);
    task_fork("myTask2", prio_std, 10000000, 0);

system_timerの値は起動時からの通算Tick数(1Tick=10m秒)です。
nsh> task_test test1
task_create name:myTask1 priority:100
task_create name:myTask2 priority:100
task_test Finish
myTask1 start PID:5 loop:10000000 wait:0 system_timer:1065
myTask2 start PID:6 loop:10000000 wait:0 system_timer:1085
nsh> myTask1 end PID:5 system_timer:1159
myTask2 end PID:6 system_timer:1174

このときタスクの実行権限は以下のように遷移します。
myTask1もmyTask2も並列で動きます。

Prio
@
A
B
C
D
E
F
---
G
H
I
nsh
100
獲得→放棄


獲得→放棄


獲得→放棄

task_test
100

獲得→終了








myTask1
100


獲得→剥奪

獲得→剥奪


獲得→終了
myTask2
100



開始→剥奪

獲得→剥奪


獲得→終了

@入力完了により実行権を放棄
Atask_testの開始と終了
BmyTask1の開始。一定時間動くが、同じ優先度のタスクが他にも有るので実行権を剥奪
CmyTask2の開始、一定時間動くが、同じ優先度のタスクが他にも有るので実行権を剥奪
D一定時間入力が何もないので実行権を放棄
E一定時間動くが、同じ優先度のタスクが他にも有るので実行権を剥奪
F一定時間動くが、同じ優先度のタスクが他にも有るので実行権を剥奪
以降DからFを繰り返す
G一定時間入力が何もないので実行権を放棄
H一定時間動いて終了
I一定時間動いて終了



test2の引数で実行すると、以下のコードを実行し、以下の表示となります。
各タスクは同じ優先度で起動し、100回の繰り返し処理で1回ごとに1マイクロ秒のsleepを行います。
sleepを行うことで実行権を放棄します。
    task_fork("myTask1", prio_std, 10000000, 0);
    task_fork("myTask2", prio_min, 10000000, 0);
    task_fork("myTask3", prio_max, 10000000, 0);

nsh> task_test test2
task_create name:myTask1 priority:100
task_create name:myTask2 priority:1
task_create name:myTask3 priority:255
myTask3 start PID:12 loop:10000000 wait:0 system_timer:10845
myTask3 end PID:12 system_timer:10899
task_test Finish
myTask1 start PID:9 loop:10000000 wait:0 system_timer:10899
myTask1 end PID:9 system_timer:10953
nsmyTask2 start PID:11 loop:h10000000 wait:0 system_timer:10954
> myTask2 end PID:11 system_timer:11008

このときタスクの実行権限は以下のように遷移します。
myTask3が一番プライオリティが高いので、myTask3は優先的に実行され、他のタスクは待たされます。

Prio
@
A
B
C
D
E
F
---
G
H
I
J
K
nsh
100
獲得→放棄



獲得→放棄

獲得→放棄
獲得→放棄

task_test
100

獲得→剥奪
獲得→終了








myTask1
100




獲得→剥奪
獲得→剥奪

獲得→終了


myTask2
1











獲得→剥奪 獲得→終了
myTask3
255


獲得→終了










@入力完了により実行権を放棄
Atask_testの開始。MyTask3の開始により実行権を剥奪
BmyTask3の開始と終了
CmyTask3の終了により実行権を獲得し終了
DmyTask2の開始。一定時間動くが、同じ優先度のタスクが他にも有るので実行権を剥奪
E一定時間入力が何もないので実行権を放棄
F一定時間動くが、同じ優先度のタスクが他にも有るので実行権を剥奪
以降EからFを繰り返す
G入力が何もないので実行権を放棄
H一定時間動いて終了
I入力が何もないので実行権を放棄
JmyTask2の開始。一定時間動くが、優先度の高いタスクが他にも有るので実行権を剥奪
以降IからJを繰り返す
K一定時間動いて終了



test3の引数で実行すると、以下のコードを実行し、以下の表示となります。
各タスクは優先度を変えて起動し、10000000回の繰り返し処理で待ち時間はありません。
ただし、sleep()により一時的に実行権を放棄しています。これによりmyTask1が先に終了します。
    task_fork("myTask1", prio_std, 10000000, 0);
    sleep(1); // wait 1Sec
    task_fork("myTask2", prio_min, 10000000, 0);
    task_fork("myTask3", prio_max, 10000000, 0);

nsh> task_test test3
task_create name:myTask1 priority:100
myTask1 start PID:14 loop:10000000 wait:0 system_timer:23832
myTask1 end PID:14 system_timer:23887
task_create name:myTask2 priority:1
task_create name:myTask3 priority:255
myTask3 start PID:17 loop:10000000 wait:0 system_timer:23933
myTask3 end PID:17 system_timer:23987
task_test Finish
nsmyTask2 start PID:15 loop:h10000000 wait:0 system_timer:23988
> myTask2 end PID:15 system_timer:24042

このときタスクの実行権限は以下のように遷移します。

Prio
@
A
B
C
D
E
F
G
H
---
I
nsh
100
獲得→放棄





獲得→放棄


task_test
100

獲得→放棄
獲得→剥奪

獲得→終了




myTask1
100


獲得→終了








myTask2
1






獲得→剥奪
獲得→放棄
獲得→終了
myTask3
255




獲得→終了







@入力完了により実行権を放棄
Atask_testの開始とSleepによる実行権の放棄
BmyTask1の開始と終了
CSleepからの復帰で実行権を獲得。MyTask3の開始により実行権を剥奪
DmyTask3の開始と終了
EmyTask3の終了により実行権を獲得し終了
FmyTask2の開始。一定時間動くが、同じ優先度のタスクが他にも有るので実行権を剥奪
G一定時間入力が何もないので実行権を放棄
H一定時間動くが、高い優先度のタスクが他にも有るので実行権を剥奪
以降GからHを繰り返す
I一定時間動いて終了



test4の引数で実行すると、以下のコードを実行し、以下の表示となります。
各タスクは優先度を変えて起動し、10000000回の繰り返し処理で待ち時間はありません。
ただし、sleep()により一時的に実行権を放棄しています。これによりmyTask1が先に終了します。
    task_fork("myTask1", prio_min, 10000000, 0);
    sleep(1); // wait 1Sec
    task_fork("myTask2", prio_max, 10000000, 0);
    task_fork("myTask3", prio_max, 10000000, 0);

nsh> task_test test4
task_create name:myTask1 priority:1
myTask1 start PID:20 loop:10000000 wait:0 system_timer:150123
myTask1 end PID:20 system_timer:150177
task_create name:myTask2 priority:255
myTask2 start PID:21 loop:10000000 wait:0 system_timer:150224
myTask2 end PID:21 system_timer:150278
task_create name:myTask3 priority:255
myTask3 start PID:22 loop:10000000 wait:0 system_timer:150279
myTask3 end PID:22 system_timer:150333
task_test Finish
nsh>

このときタスクの実行権限は以下のように遷移します。

Prio
@
A
B
C
D
E
F
G
H
I
nsh
100
獲得→放棄






獲得
task_test
100

獲得→放棄
獲得→剥奪

獲得→剥奪
獲得→終了

myTask1
1


獲得→終了







myTask2
255




獲得→終了





myTask3
255






獲得→終了



@入力完了により実行権を放棄
Atask_testの開始とSleepによる実行権の放棄
BmyTask1の開始と終了
CSleepからの復帰で実行権を獲得。myTask2の開始により実行権を剥奪
DmyTask2の開始と終了
EmyTask2の終了で実行権を獲得。myTask3の開始により実行権を剥奪
FmyTask3の開始と終了
GmyTask3の終了で実行権を獲得し終了



STM32のデフォルトのスケジュールポリシーはラウンドロビンとなっていますが、以下の変更でスケジュールポリシーをFIFOに変更することが できます。







FIFOのスケジュールポリシーは、条件Bだけが変わります。

条件@
優先度の一番高いタスクが実行可能になったら、これよりも低い優先度のタスクが実行中であっても
優先度の一番高いタスクに実行権が移る。

条件A
優先度の一番高いタスクが実行中になったら、自分で実行権を放棄しない限り、動き続ける。
実行権を他のタスクに横取り(pre-empted)されることはない。
条件@に従い、自分よりも優先度の高いタスクが実行可能になったら、実行権は剥奪される。

条件B
優先度の一番高いタスクが複数あるときは、(時間的に)先に実行可能になったタスクが実行され、
(時間的に)後に実行可能になったタスクは待たされる。



FIFOのスケジュールポリシーに変更後、psコマンドを実行すると、スケジュールポリシーがFIFOになっていることが分かります。
nsh> ps
  PID GROUP PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK   STACK COMMAND
    0     0   0 FIFO     Kthread N-- Ready              00000000 001000 Idle Task
    2     2 100 FIFO     Task    --- Running            00000000 002000 nsh_main

こちらがラウンドロビンの時の結果です。
nsh> task_test test1
task_create name:myTask1 priority:100
task_create name:myTask2 priority:100
task_test Finish
myTask1 start PID:5 loop:10000000 wait:0 system_timer:1065
myTask2 start PID:6 loop:10000000 wait:0 system_timer:1085
nsh> myTask1 end PID:5 system_timer:1159
myTask2 end PID:6 system_timer:1174

こちらがFIFOの時の結果です。
同じ優先度のタスクが複数あるときは、(時間的に)先行するタスクが終了するまで、(時間的に)後続のタスクは待たされます。
nsh> task_test test1
task_create name:myTask1 priority:100
task_create name:myTask2 priority:100
task_test Finish
myTask1 start PID:5 loop:10000000 wait:0 system_timer:5165
myTask1 end PID:5 system_timer:5219
myTask2 start PID:6 loop:10000000 wait:0 system_timer:5219
myTask2 end PID:6 system_timer:5274
nsh>

このときタスクの実行権限は以下のように遷移します。

Prio
@
A
B
C
D
E

G
H
I
nsh
100
獲得→放棄


獲得




task_test
100

獲得→終了







myTask1
100


獲得→終了






myTask2
100



獲得→終了






@入力完了により実行権を放棄
Atask_testの起動と終了
Btask_testの終了により実行権を獲得し処理終了
CmyTask1の終了により実行権を獲得し処理終了



test5の引数で実行すると、以下のコードを実行し、以下の表示となります。
各タスクは同じ優先度で起動し、100回の繰り返し処理で1回ごとに1マイクロ秒のsleepを行います。
    task_fork("myTask1", prio_std, 100, 1);
    task_fork("myTask2", prio_std, 100, 1);

system_timerの値は起動時からの通算Tick数(1Tick=10m秒)です。
nsh> task_test test5
task_create name:myTask1 priority:100
task_create name:myTask2 priority:100
task_test Finish
myTask1 start PID:5 loop:100 wait:1 system_timer:1001
myTask2 start PID:6 loop:100 wait:1 system_timer:1002
nsh> myTask1 end PID:5 system_timer:1202
myTask2 end PID:6 system_timer:1202

このときタスクの実行権限は以下のように遷移します。
sleepによりmyTask1もmyTask2も、1回のループごとに実行権を放棄します。

Prio
@
A
B
C
D
E
F
---
G
H
I
nsh
100
獲得→放棄


獲得→放棄


獲得→放棄

task_test
100

獲得→終了








myTask1
100


獲得→放棄

獲得→放棄


獲得→終了
myTask2
100



開始→放棄

獲得→放棄


獲得→終了

@入力完了により実行権を放棄
Atask_testの起動と終了
BmyTask1の開始。一定時間動くが、sleepにより実行権を放棄
CmyTask2の開始、一定時間動くが、sleepにより実行権を放棄
D一定時間入力が何もないので実行権を放棄
E一定時間動くが、sleepにより実行権を放棄
F一定時間動くが、sleepにより実行権を放棄
以降DからFを繰り返す
G入力が何もないので実行権を放棄
H一定時間動いて終了
I一定時間動いて終了



この様にRTOSの世界では、タスクの優先度が非常に重要な要素となります。
仮に優先度の一番高いタスクが永久ループすると、他のタスクは全く動けなくなります。



APIの使い方に間違いがあるかもしれません。
こ ちらのドキュメントで確認してください。

ファームの書き込み方法と、nshへの接続方法はこちらに紹介しています。
STM32F4 Discovery
STM32F3 Discovery
STM32F103RB Nucleo
STM32F103 BluePill/BlackPill
STM32F103 VEボード
STM32F407 VGボード

続く...