R8. 2016/06/21
Arduino用RTOS
マイコンの使い方に慣れ、マイコンを実用的な制御装置として応用する段階になると、インターロック処理やプロセス制御のみならず、通信やユーザーインターフェース機能の実装について考え始めるようになります。
簡単な制御装置であれば、一連の処理を一筆書きの形で繰り返し実行すれば対応できますが、通信のタイムアウト処理やPID制御などの処理が増えてくると、ソフトウェアが煩雑になり、コーディングに時間がかかる上、デバッグにも時間がかかるようになります。
この問題は、それぞれの処理が複雑であるというよりは、複数の処理を同時に動かすための仕組みがうまく導入できていないために生じていることが少なくありません。
つまり、それぞれの処理があたかも独立して動いているがごとく見せる仕組み、すなわち、マルチタスク動作に対応したRTOSが導入できれば、これらの問題は解決できます。
とはいえ、RTOSの導入と聞くと、身構えてしまう方も少なくないと思います。
そこで、RTOSの役割とArduinoで利用できる簡素なRTOSの実装とその応用について本書に記しました。本書は、実用的なソフトウェアを作成するには避けて通れない内容が記載されていると思いますので、皆様のお役に立てれば幸いです。
(1) RTOSとは
OSには、あらかじめ定義されたタスクを一定時間内に連続実行するノンプリエンティブ方式から、WindowsやLinuxの様に複数のタスクがそれぞれ干渉せず、同時実行されるプリエンティブ方式のマルチタスクOSまで様々な方式があります。
広い意味で考えると、順序実行のタスクスケジューラーを装備し、応答時間が保証された制御プログラムは、RTOS的な発想を持つプログラムと解釈することができます。
RTOSとマルチタスクOSの違いは曖昧ですが、タスク切り替えの応答時間が規定されており、複数のタスク実行に対応しているOSがRTOSと一般に呼ばれており、I-TRON(国産OS)が有名で、多くの産業用機器に実装されています。
(2) タスクの状態遷移
一般的なRTOSの場合、タスクの実行は、Waiting (待ち状態), Ready (実行待ち状態), Running (実行状態), Suspend (強制終了状態), Dormant(休眠状態) などの状態に分類されます。
しかし、小規模な組み込みマイコンに実装した場合、RunningとReady、SuspendとWaitingの違いが区別しにくいため、DormantとWaiting、Readyの3条件に着目して扱います。
(3) タスクの実行
タスクの実行について考えた場合、Readyになったプログラムをあらかじめ定義した順番で呼び出す方法がもっとも簡単な形となります。
この方法では、実行する必要のないプログラムの呼び出しもチェックされますし、優先順位の高いプログラムが必ずしも最適に実行されるとは限りませんので、CPU時間の有効活用という点で、改善の余地があるのも事実です。
そこで、同時実行可能なタスク数が増えて来た場合は、Queue (待ち行列)を設け、待ち行列に格納されたプログラムを実行する方式に変えることで、システムの安定性を保ちながらプログラムの実行効率が最適になるように考慮された、様々なRTOSが提供されています。
(4) 制御機器におけるRTOSの役割
制御機器には、ANDやOR、タイマーなどを組み合わせて利用するインターロック処理プログラム(シーケンサラダー)、温度制御などを行うためのPID制御などのプロセス制御(図形言語プログラム)、通信処理プログラム、HMI処理などが実装されます。
一般にインターロック制御はモータのON / OFF制御に利用されますので、100ms以下の応答が求められます。
温度制御のようなプロセス制御では、s関数をz変換し、離散処理形として扱うことが求められますので、プログラムの実行間隔が長くても、その実行間隔が正確であることが求められます。
通信処理では1ms程度の応答やタイムアウト処理が求められます。
キー操作やLCD表示の場合は、データの更新は50〜200msですが、データを1〜10ms程度の間隔でゆっくりとバースト転送するような処理があります。
これらの処理機能は、一般に下記のように分類されます。
a. 常時実行される処理
OSの基本機能や高速応答が必要とされる処理
b. 一定周期で実行される処理
PLC(Programmable
Logic Controller)やPID制御など、時間管理が重要な処理
c. イベント実行(タスクの動作や割り込みによって実行管理されるもの)
キー入力割り込み、UART割り込み処理などに関連し、従属関係を持つ処理がこれに該当します。
(5) ポーリング処理における問題点
制御機器の製作において、他の機器から応答を待つような処理でポーリング処理を記述することは多いと思いますが、他の機器から応答が無いと、いつまでもその状態に留まってしまい、次の処理に進めなかった、他の処理に変更しようとしてもうまく応答しなかったという経験をされた方は多いと思います。
他の機器からの応答を待つ場合、処理開始からの所定の時間が経過したとき、無応答として、次の処理に進むような「タイムアウト処理」を記述するのが一般的です。
タイムアウト処理を確実に行うためには、時計機能を応用しますが、時計機能を使うと、時刻変更によって計時処理が誤作動しますので、制御機器ではこのような処理は使われず、一定周期でON/OFFするクロックのエッジの数を数えてカウントダウン処理するようなタイマー機能やタイマー割り込みを使うのが基本となります。
これらの処理は、特定の処理に特化したコーディングで実現することも可能ですが、複数の処理が必要になると、システムリソースの制限を受けること、複雑性が増すことから、コーディングが難しくなる傾向があります。
しかし、RTOS機能に実装されているSleep命令を使えば、指定した時間が経過したとき、RTOSがタイムアウト発生時に自動的にタイムアウト処理タスクを起動させますので、タイムアウト処理の記述が簡略化されます。
(6) 割り込み処理の問題点
通信処理では、1文字受信する毎に受信割り込みが自動的に受信バッファに受信した文字を格納するような処理が行われます。
そして、受信した文字列を解析し、コマンドに応じて、制御機器内蔵メモリーの情報書き換える処理や送信元にレスポンス応答を返すような処理が求められます。
制御機器では、このような通信処理と機器制御を交互に実行するのがもっとも基本的なやり方となりますが、通信電文100byteを9600bpsで対応させる場合、電文が正常に受信されるまで待つようなコーディングを行うと、100msの間、制御処理が停止することになります。
また、通信電文を逐次解析実行しているような場合、送信元が間違ったコマンドを発行していた場合、受信処理が停止してしまうこともありますので、通信処理においては、タイムアウト処理とエラー処理を完全な形で実装し、これらをバックグラウンドでイベント実行させるような手法が用いられます。
このような処理を記述しようとした場合、タイマー割り込みを応用し、機器制御や受信電文の解析処理を定期的にスタートさせたいと思うようになります。
しかし、割り込み処理の中でこのような処理を記述すると、通信割り込みの動作を阻害させることになりますので、タイマー割り込み処理にかかる時間が延びる可能性があります。
例えば、割り込み処理にかかる時間が1msを超えると、ハードウェアFIFOを持たないUARTの場合、9600bps通信でも文字の取りこぼしが発生するようになります。
この様な場合、複数のタスクを並列動作させることができるというRTOSの特徴をうまく生かし、UART通信のようなハードウェア割り込みを最速で実行しつつ、受信した文字列の解析処理のように優先順位の低い処理をメインルーチンの中で行うという機能分担を行って、システムのパフォーマンスを向上させることができます。
(1) RTOS (void)
クラスライブラリの名称。
RTOSライブラリーをincludeすると、Rtosという名称のインスタンスが生成されます。
RTOS_MAXTASKに利用するタスクの最大登録数を設定します。(標準=8)
RTOS_ADJUSTに計時の時刻調整値を登録します。(標準=130us)
(2) Loading (Task Id, Timer Setting, Function)
Loading命令によりタスクの登録が行われ、RTOS機能が使えるようになります。
登録は、指定されたTask Idの状態がRTOS_UNLOADEDの場合に実行されます。
Task Id=0の場合は、タスクの新規登録扱いとなり、最大管理数の範囲で新規登録できます。
Timer Setting = 0の場合は、Event Driven扱いとなり、タスクは生成されますが、他の処理からBegin, Sleepなどの命令でタスク状態が起床されない限り、当該タスクの実行は行われません。
Timer Setting = 1の場合は、常時Readyのタスクとなり、毎スキャン実行されます。
Timer Setting > 2の場合は、指定時間周期[us]で当該タスクがWaitingからReadyに切り替わります。この値が極端に短いとCPU負荷が上がりますので、最低待ち時間を定義し、極端に短い間隔で当該タスクがReadyに変わらないようにしています。
Functionには、( ) を除いた関数名を記述します。
(3) Unloading (Task Id)
指定されたタスクの登録を解除します。
Unloading処理が行われると、指定タスクの状態が次のように初期化されます。
TimerSet ---> RTOS_EVENTDRIVEN : 0
Counter -----> RTOS_EVENTDRIVEN : 0
Flag ----------> RTOS_UNLOADED : 未登録状態
Task ---------> Rtos_Hook : ダミー関数
(4) Timer (Task Id, Timer Setting)
指定されたタスクの状態とタイマー設定を変更します。
Timerはus単位で時間設定できますが、値の更新はTimer 0割り込みの周期で行われますので、64*256/(F_CPU)で分解能(2.048ms at 8MHz)が決まります。
RTOSのタスク登録の際、タイマー設定値を下記のように設定すると、イベント実行、毎スキャン実行、一定周期実行の選択が可能です。
Timer Setting = 0の場合:タスクの状態はDormantに変わります。
Timer Setting = 1の場合:常時Readyのタスクに遷移し、毎スキャン実行されます。
Timer Setting > 2の場合:指定周期[us]で当該タスクがWaitingからReadyに遷移
ReadyのタスクはWaitingに遷移し、指定時間後より起動します。
(5) IdNum (void)
現在実行中のタスク番号を返す関数
(6) Begin (Task Id)
指定したタスクをReadyに遷移させます。
Suspend(強制待ち状態)のタスクを即ReadyにするResumeと同じ機能になります。
Waiting Flagは変化させません。
(7) End (Task Id)
指定したタスクのReadyとWaiting両方のフラグをクリアし、Dormant(休止状態)状態に遷移させます。
Dormant状態のタスクは、一定周期実行が設定されている場合でも、再起動しない状態になります。
(8) Sleep (Task Id, Timer Setting)
指定したタスクの状態を指定時間Waiting状態に遷移させます。
Sleepの場合、Waiting時間の精度は保証されますが、Sleepコマンドを使って一定周期実行タスクのような動きをさせることはできますが、実行間隔は設定時間に処理時間が加算された形になりますので、定時性が保証されません。
Dormant(休止状態)のタスクは、Sleep命令により起床させることができます。
Sleep命令を事項すると一定周期実行は終了します。
(9) Suspend (Task Id)
指定したタスクのReady Flagをクリアし、Waiting状態に遷移させますが、Dormant状態のタスクをWaitingにすることはできません。
Running状態のタスクを休止する場合は、Suspend(0)を実行します。この命令を実行しないと、タスク実行は一旦終了しCPUリソースは一旦OSに返されても、ReadyフラグがEnable状態にあるため、次のスキャンで再び実行されます。
一定周期で1回実行するタスクの場合は、処理終了でSuspend(0)を実行し、CPUリソースを解放する形にしてください。
(10) Resume (Task Id)
指定したタスクをDormantの状態からWaiting状態に遷移させますが、Ready状態のタスクをWaiting状態に遷移させることはできません。
(11) Continue (Task Id)
指定したタスクを一定間隔で繰り返し実行するためのフラグをEnableに変えます。
但し、EVENTDRIVEN指定されていた場合を除きます。
(12) Reference (Task Id)
指定したタスクの状態を返します。
RTOS_DORMANT = 0x00 // Dormant状態
RTOS_WAITING = 0x01 // Waiting状態
RTOS_READY = 0x02 // Ready状態
一定周期実行中のタスクの場合、
= 0x03 // ReadyかつWaiting状態
RTOS_UNLOADED = 0x80 // 未定義
RTOS_INVALID = 0xC0 // 無効なTask Id
(13) Control (void)
RTOSのタスクディスパッチャプログラム本体。
この関数の呼び出しを止めると、RTOS機能はすべて停止しますので、この関数をloop()に記述し、常時実行されるようにして下さい。
main.cppを改変し、loop()をRTOSのタスクとして登録することも可能です。この場合は、Arduino.hに#include "RTOS.h" を追加し、RTOS.cppとRTOS.hをmain.cppと同じフォルダに格納し、main.cppの中でloop()をタスクに登録する記述を追加してください。
(14) ScantimeMax (void)
RTOSのスキャンタイムの最大実行時間を確認することができます。
数値は5秒毎に更新されます。
(15) ScantimeAvg (void)
RTOSのスキャンタイム平均時間を確認することができます。
数値は5秒毎に更新されます。
(1) Flagの管理
bit 0 : Waiting Flag
bit 1 : Ready Flag
bit 2 : Continue Flag
bit 7 : Unloading Flag
※bit 0, 1 の組み合わせ
0x00 : Dormant状態 タスクは休止状態(関数ポインタは登録された状態)
0x01 : Waiting状態 タイマーカウントダウンが行われる
0x02 : Ready状態 Runningに遷移可能な状態
0x03 : Ready状態で且つWaiting状態 現在実行中のタスクを終了しても、
タイマーカウントアップ後、Ready状態に遷移する
一定周期実行タスクはこのような状態になります
0x8x : Task Unloading状態
bit 0, bit 1 の状態によらず、タスクに関する処理は行われない
関数ポインタは登録されていない
※bit 2がONの時、WaitingからReadyへの遷移でWaiting Flagをクリアしません。
(2) Runningのタスクが通常終了した場合
タスクの状態はRunningからReady状態に遷移した形になりますので、次のスキャンで再実行されます。
高速実行が必要とされない場合は、タスク実行を終了する直前にSleepやSuspend, End命令などを実行し、Ready Flagをクリアすることにより、CPUリソースを簡単に解放(他のタスク実行に振り向ける)することができます。
一般的なRTOSでは、すべてのタスク実行がWaitingになりCPUがIdle状態になった場合、CPUを省電力モードに移行させる様な処理が記述されますが、このRTOSでは省電力モードへの移行は行われません。
(3) Readyになったタスク
Waiting、Dormant のタスクはReadyになるまで実行されず、Ready状態のタスクが複数ある場合は、実行順序が来たら、順次実行されます。
このRTOSはノンプリエンティブタイプですので、特定のタスクがCPU時間を長時間占有すると他のタスク実行が停止しますので、CPUリソースは長時間占有せず短時間で解放し、次の処理にCPUリソースを明け渡すことが重要です。
(4) Timer設定と一定周期実行タスク
Timer設定が0の場合
一定周期実行タスクとして扱われません。
Timer設定が1の場合、毎スキャン実行されます。
最も優先順位の高い処理にだけ割り当ててください。
Timer設定が2以上の場合、
設定した時間毎にWaitingからReady状態に遷移します。
一定周期実行処理はWaitingからReady状態に遷移した瞬間から次のタイマーカウントダウンが始まりますので、タスクがWaitingからReadyに切り替わる周期が比較的安定するという特徴があります。
これに対して、Sleep命令を組み合わせて同じような動きを実現した場合は、休止設定時間に処理遅延時間が加算された値でタスクの状態がWaitingからReadyに遷移しますので、定時性は保証されません。
(5) タイマーカウントアップ後
Timer設定が0(Event Driven指定)のタスク
Continue flagがOffですので、Ready = true, Waiting = falseになります
Timer設定が1以上の場合、
Continue flagがOnの場合、Ready = true, Waiting = trueとなりますので、一定周期で指定したタスクが実行されます。
一般的なRTOSではReadyかつWaitingという状態遷移は定義されません。
言うなれば、1階層のQueuingバッファを持ちReady状態で、次のRequestを受け付けているようなイメージになります。
(1) プリエンティブマルチタスクの問題点
a. RAMの消費量が増加する
Free RTOSはAVRマイコンで利用できるプリエンティブマルチタスク機能を持ったRTOSですが、プリエンティブマルチタスク機能を使う場合は、タスク毎にCPUのレジスタ待避領域を設けると共にスタック領域に上限値を設け、その範囲内でコーディングを行う形になります。
この結果、シングルタスク実行の場合よりもサブルーチンの中で利用できるスタック変数の使用制限が厳しくなる上、グローバル変数領域やヒープメモリー領域が減少しますので、見つけにくいバグの発生確率が上昇します。
b. タスクスイッチに伴う弊害
プリエンティブマルチタスクの場合、RTOSのタスクスイッチ機能によって、複数のタスクを同時にRunning状態に遷移させることが可能で、設定したタイムスライス時間でタスクの実行(CPUリソースの利用)が途中で切り替えられます。
しかし、Arduinoで共通に利用されるミドルウェアやI/Oサービスは、RTOSと連動した作業変数の適切な待避処理やセマフォ(Semaphore)によるリソースへの排他アクセスには対応していませんので、複数のタスクから同時に実行要求(呼び出し)が入ると誤作動する場合があります。
プリエンティブマルチタスクでは、このような問題が普通に起こりますので、この問題の本質を理解し、適切に対応することが求められますが、容易ではありません。
c. ノンプリエンティブマルチタスクとは
タスク処理の処理が完全に終了するまで、他のタスクに実行が切り替わらないタイプのRTOSは「ノンプリエンティブタイプ」に分類されます。
ノンプリエンティブタイプの場合、複数のタスクを同時にRunning状態に遷移させることはできませんが、タスクスイッチ機能が大幅に簡素化されます。
さらに、タスクの優先実行制御機能を省略し、Ready状態になったタスクを順次実行する方式を採用することで、Task Queuing処理の実装が省略できますので、小規模システムに適した簡素なRTOSとすることができます。
d. ノンプリエンティブタイプにおけるセマフォ(排他実行)の管理
ノンプリエンティブタイプの場合でも、ミドルウェアやI/Oサービスに対するアクセスについては、同時利用できるリソースの制限を受けます。
しかし、プリエンティブタイプの場合とは異なり、タスク実行が途中で切り替わることはなく、ミドルウェアやI/Oサービスの呼び出しが再帰実行(リエントラント)になることはありませんので、コーディングの難易度は格段に下がります。
(2) ATmega328でどうやってRTOS機能を実現するか?
AVRマイコンで行うような制御機能の多くは、一定時間間隔で繰り返し実行されることが多いこと、状態遷移の考え方で処理内容を分析した場合、複数のステージ(スレッド)に分解できることが多い。
スレッドの実行時間上限を、例えばTs[ms]以内などのような形で規定すれば、タスクが複数(n個)存在しても、RTOSのタスク実行周期はTs×n[ms]スキャンに制限できることになります。
タスクの処理をTt[ms]以内に行う場合は、Tt[ms]周期でタスクをWaitingからReady状態に遷移させ、スレッドの実行はTs[ms]以内に終了させ、他のタスクでもCPUリソースが順番に利用できるような処理方法を採用すればよいことになります。
通信伝文の解析のようにTt時間が問われない場合は、Ts[ms]毎にWaitingからReady状態に遷移させて通信状態の監視を行うというやり方を採用します。
このような手法を採用することによって、ノンプリエンティブタイプのRTOSで疑似マルチタスク処理が実現できます。
(3) タスク優先順位
タスクディスパッチャの機能は、各タスクの優先順位付けを無くし、登録順番毎に順序実行するような作り込みにすることで簡素化できます。
しかし、このやり方では、各タスクが順番に実行されてしまいますし、プロセス制御で必要な一定周期毎の制御は実現できません。
一般的なRTOSでは、タイマー割り込みとメールボックス(メッセージ伝送)機能を使うことによってタスクの状態を遷移させる手法が採用されていますが、複数のタスクを異なる周期毎に実行させる場合、タイマー割り込みルーチンを都度作成する必要があり、扱いが面倒です。
そこで、各タスクが一定周期でWaitingからReadyに遷移させるタイマー割り込みに似た仕組みを標準的に設けることによって、一定時間間隔でのタスク実行に対する時間精度を高める仕組みを導入しました。さらに、高速応答が求められるような優先度の高いタスクに対しては、短い周期でReadyになる様に設定し、優先度の低いタスクは、Waiting時間が長くなるようにユーザープログラムを実装して且つ、スレッドの実行時間に上限値を設けてコーディングすることで、システムとして適切に動作するように配慮しています。
(4) ATmega328に実装するRTOS機能
上述の検討より、ノンプリエンティブタイプでかつQueueを用いたタスク実行優先順位管理を省略し、タスクの一定周期実行機能を追加したRTOS機能をATmega328に実装し、Arduinoで利用できるRTOS機能として仕上げています。
基本的には、ATmega168(ROM 16KB、RAM 1KB)でも動作しますが、ROM / RAM容量ともに不足ぎみですので、RTOSを実装し、複数の機能を集積し、実用的な機器制御に応用するのは難しいと推測されます。
(5) RTOSで想定する状態遷移
小規模組み込みシステムの場合、Windows, Linux OSとは異なり、タスク生成といってもプログラムが動的にローディングされることは無く、プログラムはすでにローディングされた状態で使われますし、タスク実行の終了によってメモリー上から消滅することもありません。
さらに、ノンプリエンティブタイプの場合、Running状態のタスクは1つしかありませんので、RTOSの状態遷移は、SuspendとWaiting、Ready(Running)の3つの状態を定義すれば良く、この間を行き来できるように命令セットを実装する形になります。
また、マルチタスク動作といっても、各タスク(スレッド)で占有するCPU時間の上限をコーディング時にプログラマーがあらかじめ決めて適切にコーディングするような疑似マルチタスク動作となりますので、各状態遷移を適切に管理するには、プロセスのタイムスケジューラーに対する理解が必要です。
どうしてもプリエンティブなRTOS機能がどうしても必要な場合は、Arduinoでの利用に際して大幅なリソース制限は生じると思いますが、Webで公開されている情報は多いうえ、ソースが公開されており、ライセンスフリーであるFree RTOSをご利用ください。
Arduinoは、もともとシングルタスク実行を想定した作り込みになっていますが、delay処理を正確に行うために、Timer 0割り込みを使ったus単位の計時処理が使われています。
8 / 16MHzの場合、8 / 4 usの分解能が得られますので問題ありませんが、これ以外のCPU周波数だと丸め誤差が生じ、正しいus値がシステムから戻ってきません。
このため、20MHz動作に対応させる場合は、カウンター設定値の補正演算が必要となります。
標準状態では、8個のタスク実行まで登録できます。
ArduinoはC++がバックグラウンドで動いていますので、関数ポインタが使えますので、ユーザーがSetup()関数の中で、必要なタスクを登録することが可能です。
タスクに登録した関数は変更することもできます。
■登録の例(main.cppの改変例の場合)
0:自分自身を表す番号
1:ユーザー定義プログラム1(loop関数を登録するという使い方もあります)
2:ユーザー定義プログラム2
3:ユーザー定義プログラム3
4:ユーザー定義プログラム4
5:ユーザー定義プログラム5
6:ユーザー定義プログラム6
7:ユーザー定義プログラム7
■main.cppの改変例
この例の場合、loopの動きは改変前と変わりませんが、Rtos.Timer命令を使うことによって、実行条件を変更することが可能です。
int main(void) {
init();
Rtos.Loading(0, RTOS_DOEACHSCAN, loop);
initVariant();
#if defined(USBCON)
USBDevice.attach();
#endif
setup();
for (;;) {
Rtos.Control(); if (serialEventRun) serialEventRun();
}
return 0;
}
![]() |
※Event DrivenのタスクがReady / Running状態でSuspend実行した場合、
Dormant状態に遷移します。
※Running状態のタスクは同時に1つしかないため、ReadyとRunningは同じ
状態として扱われます。
Running状態のタスクがreturn命令で終了した場合、Preemptとして扱われます。
※二重待ち状態は定義されていません
■2016/06/21
1,Arduino版マイナーチェンジ
RX63マイコン用ソフトをArduino(ATmega328)に再移植
RX63マイコン(CS+)でも一部修正すれば動作します。
2,ESP8266での動作確認
Arduino版がそのまま動作することを確認しました。
3,Home Pageで公開
使って頂けるかわかりませんが取り敢えず公開
一通りバグはつぶしたつもりですが、
このプログラムの使用に伴い発生した損害に対する責任は、一切負わないこととします。
プログラムは自由に改変並びに配布頂いてかまいません。
ご利用頂いた際は、メールを頂けると幸いです。
■2015/05/06
1,Arduino用簡易RTOS機能実装
AVRマイコンではTimer0を使い、us / ms単位の計時処理を行って比較的正確に計時できる仕組みが採用されています。
この機能は、RTOSの実装に不可欠な処理ですので、Arduinoの場合、Loop処理にちょっと手を加えるだけで、2年前に開発したRTOS機能(C言語版)が簡単に実装可能であることがわかりました。
実際には、LPC1114用Arduino版としてC++のプログラムを作り、これをATmega328用に移植した形になっています。
しかしながら、作成したプログラムは、ほとんど手を加えずに利用できました。
Arduino版は、クラスライブラリと関数ポインタを使うことによって、タスクが動的に登録できるようになっていますので、過去に製作したC言語版より使いやすい物に仕上がっています。
■2015/05/13
1,タスクを未登録状態にする概念の取り入れ
C++では、関数ポインタが使えるため、タスク設定を解除することもできる。
逆に言えば、タスク管理フォルダのような形でリソース管理できれば、休止状態にあるタスクを未登録に変え、別のタスクが割り付けられるような仕組みを設けることができる。
この場合、登録すべきタスク(関数)は、ROMにリンクされた形となっているので、同時利用されるタスクの数が少なければ、ディスパッチャが管理すべきリソースを削減することができるので、スキャン実行処理の高速化とRAM使用量の削減が可能になる。
2,タスクを未登録にできる条件
Suspendは強制待ち状態への遷移、Exitは休止状態への遷移、としているので、SuspendのタスクについてUnload(無負荷)することで管理対象から外す形にする。
Unloadされたタスクについては、Flagレジスタのbit 7をEnable(RTOS_UNLOADED)することで識別する。
Loading処理では、Unloadedされたタスク番号の自動再利用は現在のところ実装されていない。
3,タスク生成に関する概念の変更
unloadできると言うことは、従属タスクが動的にloading(生成)できることになる。
従属タスクを生成した親タスクは、正しい手続きでunloadすることが求められるため、従属タスクを生成した場合は、タスク番号を必ず保持し、当該番号のタスクを間違いなくunloadするような形をとる。
タスク番号は、タスク名ではなく数字で管理される。
4,動的なタスク生成と消滅の効果
動的なタスク生成と消滅機能を使うと、タイムアウト処理のように実行頻度の高くない処理において、タスクハンドラの数を削減し、RAM使用量を削減することができる。
ソフトウェアによっては、動作設定をスイッチで切り替えて使うことがあるので、動的なタスク生成の仕組みがあると、タスク番号を固定せず、動作設定に応じて適宜動的にタスク生成すればよい形にできるので、より汎用的な使い方が可能になる。
■2015/05/23
1,バグFIX
(1) Sleep実行についてC言語版の動作と異なる動きになっていることがわかった。
SleepでWaiting状態のカウンター減算終了後、Event drivenのタスクは、Exit後、再起動しないのが基本であるが、フラグ管理の方法に問題があり、カウントアップ後、Suspendしない限り、タスクが連続稼働状態に遷移してしまうことがわかった。
Sleepで定時再起実行を許可するのは、Timer設定が定時再起動になっている場合だけで、Event Driven 設定の場合は、再起動しないことが基本となる様に修正した。
(2) Event Driven設定におけるバグ
Sleep処理でタイムアウト判定した後、ハングアップ状態になることが判明し、原因を調査した。
調査の結果、Event Driven設定でWaitingからReadyに遷移した場合、カウンター設定値の調整処理を行っているが、Event Driven処理ではこの処理が不要であるにもかかわらず実施していたため、長時間ループ処理になってしまっていたことが原因と判明したので修正した。
2,RTOSコマンドの見直し
標準的なRTOSとは状態管理の手法、コマンド名称が異なっているようだったので、できるだけ標準的な処理に近づけるよう処理の詳細を見直した。
この見直しでWaitingとReady、Unloaded、Invalidフラグの管理方法を変更したほか、状態遷移コマンドの見直しも行った。
■2015/06/27
1,20MHz動作時に計時誤差が生じる問題を解消
不具合の原因は、Arduinoのmicros() 関数が、F_CPU = 8 / 16MHzを想定しており、これ以外の周波数だと、64/(F_CPU / 1000000L)の計算結果が整数値にならないため、計時誤差の原因になっていることが判明しました。
8MHz:64/(F_CPU / 1000000L) = 8.0us(デフォルト)
16MHz:64/(F_CPU / 1000000L) = 4.0us(デフォルト)
20MHz:64/(F_CPU / 1000000L) = 3.2us
18.432MHz:64/(F_CPU / 1000000L) = 3.472us
対策として、Timer関数でタイマー設定値を登録する際、F_CPUの値で誤差補正する形とし、実行速度が下がらない方法で対策することとした。
2,時刻計算方法の見直しと最小スキャン時間
PLC機能を実装した際、1スキャン時間が5msよりも延びている状況が確認されたため、タスクディスパッチャ処理を見直すこととした。
RTOSの計時はus単位で行えるように設計されており、micros()関数は4usの分解能で値を返す仕様になっていますが、計時分解能はtimer 0 割り込み周期に大きく依存性した実装になっています。
このため、最小スキャン時間は
8MHz:(64 * 256) / (F_CPU / 1000000L) = 2.048ms
が妥当と考えられますので、これより短い時間設定が指定された場合は、この値に修正する形とします。
さらに、カウントダウン計算(32bit演算)の実行頻度を減らし、実行速度の改善を図ることにしました。
3,カウントダウン方向の変更
これまでは、カウントダウン方式で対応してきましたが、初期値を0に変更し、カウントアップする方法に変更しました。
この変更に伴い、Continue Flagを新たに設け、より分かりやすいプログラム構造に変更しました。
戻る