PIC32MM CPUシステム

平成31年 07月

    2. システムソフトウェア P-SYSTEMの概要

 P-SYSTEMはPIC32MM CPU上でマルチタスク動作を行うためのシステムソフトウェアです。
対象を小規模な組み込み装置に絞り込むことで市販のRTOSのように膨大なライブラリを理解する手間を掛けることなく利用できます。
 タスクの優先度管理は特に何も指示しなければIO処理の待ち時間がなるべく短くなるように調整されます。
従ってプログラマはIOアクセスの詳細を気にすることなく、本来の装置制御に集中することが出来ます。

実装している機能は下記になります。
 ・ 非プリエンプティブ方式のマルチタスク機能(最大で250個のタスクを管理)
 ・ タスク優先度に応じた実行管理
 ・ UARTによるエラー出力と端末によるコマンド操作
 ・ UART, TIMER, SPI, I2C, F-ROM, RTCなどのデバイスドライバ
 ・ F-ROMへのログ書き込みと端末コマンドによるログの読み出し機能
 ・ Queue, 整数型printfなどのソフトウェア関数
 ・ 緊急停止を補助するfatalError関数

例えば以下のような機能は実装していません。
 ・ プリエンプティブ方式のマルチタスク機能
 ・ リソースの占有管理を行うセマフォ機能(キュー機能で代用します)
 ・ 動的に追加・削除が可能なタスク管理機能(タスクは静的にのみ定義されます)

 これらの機能はシステムの規模が大きくなってくると必要なのは確かですが、小規模限定なら無くてもさほど不自由しません。

 また、CPU機能についても全ての機能をP-Systemと一緒に使用できるわけではありません。
以下は使用できない機能の一例です。
 ・ 多重割り込み機能(割り込み処理関数内での処理を最小化することで必要性は下がります)
 ・ ADコンバータ機能(大半のピンがデジタル機能前提で基板設計されています)
 ・ USBポート



 1.タスク優先度管理の概要

 タスク優先度はkTaskPriority_tという型で表現されます。
 タスクの優先度には下記があります。優先度の高い方から順に記述します。
仮に同じ優先度のタスクが複数ある場合はタスク番号の小さい方から順に実行されます。

 ・kTaskPriority_t::own, kTaskPriority_t::own_once
 これらは全く同じです。機能的な意味合いからはown_onceが本来ですが、定義のバランスからownも同じ
機能として定義だけしてあります。
 これらの優先度は最優先で実行されます。その間は他の優先度のタスクは全て待たされます。
つまり、これらの優先度のタスクが一つでもある限り、それ以外のタスクは実行されません。
非常にワガママな要求でそれを受け入れるしかないという意味からown(オーナーの意味)と名付けました。
 しかし、この状態が続くと他のタスク実行が妨げられるため、これらの優先度は一度きりです。
この優先度を持つタスクを実行後、一旦処理を中断した(つまりタスクスライスの実行が終了した)時点でも
優先度が同じ、つまりタスク内で自身の優先度を変更していなければシステムが強制的に優先度を後述する
kTaskPriority_t::normalに変更します。これにより特定のタスクがCPUを占有することは出来なくなっています。
自タスク内で他の優先度に変更した場合はそちらが優先します。(own, own_onceへの変更は無効です)

 この優先度の主たる用途は割込み処理関数からメイン側のタスクに処理を要求する時に可能な
限り速いレスポンスを実現するために使用します。

 ・kTaskPriority_t::high, kTaskPriority_t::high_once
 この優先度は後述するkTaskPriority_t::normalに対して実行頻度が高いのが特徴です。
具体例を示した方が分かりやすいので、具体例で示します。タスク数5個で優先度が下記の場合、
 優先度highのタスク番号 2,3
 優先度normalのタスク番号 0,1,4
実行順序は以下のようになります。タスク番号0を優先度normalの実行開始とした場合、
2-3-0-2-3-1-2-3-4-2-3-0-.......

 つまり優先度normalのタスクを1つ実行する前に優先度highのタスクも各1回実行されます。
 high_onceは優先度highとして1回だけ実行後、自タスクによる優先度の変更が無ければ
システムによって優先度をnormalに変更します。
ただし、優先度own_highとは異なり自タスク内で自身の優先度を再びhigh_onceに設定することは
有効です。これを連続的に行った場合は事実上、優先度highと同等です。

 この優先度の主たる用途はIOポートをポーリングで処理することを想定しています。
優先度high_onceの目的は優先度highは必要な処理が終了した時点で優先度をnormalに
戻し忘れることが多いので、便宜的に追加してあります。
 当然、個々のタスクの負荷量に応じてタスクの処理が一巡する時間を調整する目的でも使えます。

 ・kTaskPriority_t::normal
 基本の優先度です。タスク番号0から順番に実行していきます。
タスク番号の最後まで実行が終わると再び0番から実行を開始します。

 ・kTaskPriority_t::sleep
 この優先度のタスクは実行されません。実行する必要が出てくるまで待機させるのが目的です。

 ・kTaskPriority_t::disable
 実行されないという意味ではsleepと同じです。
違いは一旦優先度をdisableに設定したタスクは他の優先度に変更できません。
タスクの動作を以降、完全に禁止する目的で用意されています。



 2.マルチタスクの概要

 P-Systemでのマルチタスクは非プリエンプティブ方式です。
コルーチン方式と呼んだ方が分かりやすいかもしれません。
コルーチンのルールは簡単です。
  1. コルーチンを内包するC言語の関数をA()とする。
  2. 関数A()を実行時、関数内でコルーチンの開始を示すCOStart()に出くわすまではC言語のルールに従う。
  3. 最初のCOStart()ではそのままC言語のルールで動作し、コルーチンの中断を示すCoSusp()まで継続する。。
  4. CoSusp()に出くわした段階で、関数A()からreturnする。返り値が必要な場合はCoSusp()に引数として渡せばよい。
  5. 次に再び関数A()が実行された場合、COStart()に出くわすまでは従来と同じ。
  6. COStart()に出くわすと、次の動作は直前に実行したCoSusp()の次から実行される。
  7. つまり見かけ上、直前の中断位置の次から処理を継続しているように見える。CoSusp()は複数個あって良い。
  8. コルーチン開始後、コルーチンの終了を示すCoFinish()に到達すると、直前の実行位置情報は破棄される。
  9. つまり、次回関数A()を実行時は手順3.から処理が行われる。
  10. CoFinish()後はC言語のルールに従う。
 コルーチン方式のマルチタスクではタスクの切り替え処理をプログラムによって記述する必要があります。
その意味では面倒が増えるように見えるのですが、タスク切り替えを自動で行うプリエンプティブ方式にも
多くの弱点があります。小規模向けの組み込み装置を前提にした場合は以下のような点が気掛かりです。

 ・ タスク毎に専用のスタック領域を確保する必要があるため、タスク数の上限がRAM容量で制限される。
 ・ IOなどのリソースはほぼ全てを所有権管理する必要がある。このコード記述もそれなりに負担が大きい。

 コルーチン方式では少しのルールに従うことで上記の問題を回避できます。



 3.タスクの概要

 ユーザーのプログラムはタスクとして記述します。タスクはクラスCTaskBaseを継承する必要があります。
P-Systemでは全てのタスクをCTaskBaseとして扱います。

 ユーザープログラムでは必要に応じて下記関数を定義します。
(本来は他にも必要に応じて記述する関数がありますが、話を単純化して説明しています。)
 ・  virtual デストラクタ
 ・  virtual bool init();
 ・  virtual void exec();

  関数virtual bool init()は関数virtual void exec()の実行開始前に一度だけ呼び出しされます。
処理の必要が無ければ記述しなくてかまいません。
関数がfalseを返すと後述の関数fatalError()を呼び出して停止します。
 関数virtual void exec()はユーザープログラム本体です。
マルチタスク動作させるので通常はコルーチンとして記述します。

 タスクの種類としてシステムタスクとユーザータスクの二種類があります。
両者の違いは後述する関数fatalError()が呼び出された後の動作で、ユーザータスクは
全て優先度をkTaskPriority_t::disableにされます。つまり、以降実行されなくなります。

 3.1 実際にタスクを作ってみる

 一部を擬似コードとして実際にタスクを書いてみます。
作成するタスクはCPUの入力ポート1で開始指示処理関数execShori()を実行し、その後処理終了を知らせる
入力ポート2の値を確認して終了する単純なものです。

ヘッダファイルは以下のようになります。
現時点では個々の機能の詳細を説明できていないので、名前空間やインクルードパス, 初めて出てくるクラス名などの
細部については無視してください。




ソースファイルは以下のようになります



 関数init()の目的は文字通りタスクの初期化部を実行部exec()と分離することです。
関数exec()はコルーチンのおかげで処理手順がそのままプログラムの手順になっています。
ここで注目して欲しいのは、コルーチンは多段にネストして記述できる点です。
このように記述することで複雑な処理も複数の処理手順の集合として記述できます。

 また、関数execShori()には後述する関数fatalError()が二つ使用されています。
関数fatalError()はプログラムの継続動作が出来なくなったとき、引数に識別用の文字列を
与えて呼び出します。個別に意味のある文字列を与えるのが本来ですが、単なる識別用ですので
"1", "2", "3", .....といった「固定文字列」でもかまいません。

 タスクをシステムに登録するにはP-Systemのタスクテーブルに新規のタスクを追加しますが、
ここでは詳細には触れません。



 4.関数fatalError()の概要

 関数fatalError()はプログラムの継続動作が出来ないときに呼び出す専用の関数です。
割り込み処理関数の中からでも呼び出し可能です。
 ・ if文でelse節に分岐することはありえないとき
 ・ switch文でdefault節に分岐することはありえないとき
 ・ ハードウェアの故障を検出して、その後の処理を継続できないとき
のような時にその箇所にfatalError()を組み入れます。


 4.1 関数fatalError()の出力例

 先のタスクCTaskShoriで関数fatalError("1")が実行された場合、stderrには下記のメッセージが出力されます。
yy/mm/dd hh:mm:ss fataError at ソースファイル.cpp execShori 1
Trace nest TaskNo [タスク番号n]
    ソースファイル.cpp exec CoroutineNest
    ソースファイル.cpp execShori CoroutineNest

 出力の意味は次の通りです。
 1行目:yy/mm/dd hh:mm:ss は日時。"fataError at"以降はエラーの発生したソースファイル上の位置を示します。
      ソースファイル.cppの関数execShori内で識別文字"1"の場所でエラーが発生した。
 2行目:エラーの発生したタスク番号
 3行目以降:コルーチンの呼び出し状況を上位から書き出しています。
        識別文字の"CoroutineNest"はコルーチンの開始を意味しています。
        ソースファイル.cppの関数execにてコルーチンの開始
        ソースファイル.cppの関数execShoriにてコルーチンの開始


つまり、タスク番号nのタスクでコルーチンを2回経由した関数execShori内の識別子"1"の
位置でエラーが発生したことを追跡できます。
 可能であれば、同様の内容がログとしてフラッシュROMにも記録されます。

 これまではプログラムに関数fatalError()が現れ実行されたときの表示でした。
類似のエラーとしてCPUの例外が発生したときも上記に類似したエラー表示を行います。
相違点名は一行目のエラーの発生箇所でソースコード上の位置としては特定できないため、
PC(プログラムカウンタ)値とエラー要因によって表示します。



 4.2 関数fatalError()の処理概要

 この関数の大まかな処理内容は以下の通りです。
  1. グローバル割り込み制御を禁止にする。
  2. 多重にfatalError()が発生した場合はシステムを強制的にリセットしてLEDにエラー状態を表示後、無限ループにする。
  3. stderrが使用できない状況ならシステムを強制的にリセットしてLEDにエラー状態を表示後、無限ループにする。
  4. システムが安全な状態となるように入出力などを再設定する。
  5. P-Syststemが提供するユーザーサービス機能を停止する。
  6. グローバル割り込み制御を許可する。
  7. stderrを介してエラー情報を出力する。
  8. システムタスクの実行を再開出来ないなら、LEDにエラー状態を表示後、無限ループにする。
  9. ユーザータスクの優先度を全てkTaskPriority_t::disableとして以降実行されないようにする。
  10. システムタスクの実行を再開する(F-ROMへのログ書き込みタスクを含む)
 実際の処理内容はもう少し複雑ですが、全体としては外部にエラー情報を出力できない状態ではシステムをリセット後、
LEDにエラー表示し無限ループにする。そうでなければシステムを安全な状態に再設定してstderrにエラー情報を出力する。
ここでシステムタスクの継続動作が出来なければこの段階でLEDにエラー表示し無限ループにする.
そうでなければシステムタスクのみが動作する環境でタスク動作を再開します。この場合のみ無限ループには移行しません。
このシステムタスクの一つとしてログ出力が処理されます。

システムタスクはユーザーによる静的な追加が出来ます。
 ・ 通信ポートのみで接続されている装置ユニットに緊急停止を送信するタスク
 ・ 緊急停止後に周辺装置への停止処理を実行するタスク
などの処理をシステムタスクとして登録しておき、関数fatalError()が実行されたときに
これらのタスクを起動させるなどの用途が考えられます。

 手順4.の処理内容は明らかにユーザーシステムごとに処理内容が変わってきます。
このためシステム側では定義せず、関数::library::setSafe()としてユーザーが定義する
必要があります。ここで処理すべき内容については雛形のソースリストを添付してありますので、
それを元に修正します。
通常は、システムをリセットした状態が装置として安全な状態となるように装置が設計されているので
 ・ システムの出力をリセット状態と一致する値に再設定する。但し、P-Systemが占有しているUartとタイマを除く。
 ・ ユーザーが使用している割り込み許可を全て禁止にする。但し、P-Systemが占有しているUartとタイマを除く。
とします。
 なお、システムタスクにユーザー独自のタスクを追加している場合は、その追加タスクの要求に合わせた変更が
必要になります。





PIC32MM目次へ  前へ  次へ