組み込み
μT-Kernel

μT-Kernelタスク間通信(メールボックス)

はじめに

前回はタスク間通信をランデブでやってみたが,今回はメールボックスを使ってみる.

ってか,実は前回の手法は,デバイスドライバの作りによってはうまくいかないかもしれない.

ということが,記事を書いた後に発覚した.

ほ,ほら,この業界,動かす装置が無いのにプログラムだけどんどん先に作っておくとか,無茶なこともしないといけないし.
まあ,マーフィーの法則通りの話なんだけどね.

やりたいこと

想定する組込みシステムはこんな感じ.

+-----------------+
| Embedded System |
| +-------+  UART |     +----+
| | micom |<----------->| PC |
| +-------+       |     +----+
|    ^            |
|    |            |
| [Push Button]   |
+-----------------+

PCからUARTで組込みシステムにコマンドを送ると,その結果がUARTで返される.
組込みシステムに付いているプッシュボタンを押すと,マイコンは何らかの動作をして,その結果をUARTでPCに返す.

ただし,PCからのコマンド送信した後,返信をすべて受け取るまでプッシュボタンは押さない.プッシュボタンを押した後に返信をすべて受信するまでPCからコマンドを送信しない.PCからのコマンド送信とプッシュボタンは排他的にしか利用しないものとする.

問題点

UARTは送信と受信を同時にはできない.少なくとも,今使おうとしているマイコン内蔵のバッファは送受信共用となっているので,それは間違い無くそうなっている.

マイコンから見て,UARTのTxを動かすルートは2種類存在する.すなわち,PCからのRxを受信した後,もしくは,プッシュボタンの割込みを受け取った後である.Rxを受信した後は特に問題無いが,プッシュボタンの後では,Rxの受信待ち状態が続いているので,Txを動かすことができない.

この問題は後で解決する.

2つのものが1つのものを使おうとすると,必ず競合が発生するので,これも解決しなければならない.

方針

2個のことを同時には考えられないので,1個ずつ組み立てて,作っていく.

基礎

まず,PCとマイコンの接続を基礎としよう.すなわち,マイコンは受信待ち状態で動き始め,コマンドを受信して,実行する.実行し終わったら,結果を返信する.返信が終わったら,再び受信待ち状態に戻る.

受信と送信はやはりタスクを分ける.受信タスクから送信タスクへのタスク間通信には,メールボックスを使うこととしよう.

受信タスクは通常,受信待ち状態で停止し,コマンドを受信をしたらタスク処理を動かす.

送信タスクは通常,メールの受信待ち状態で停止し,メールを受信したらその内容を送信する.

付加する機能

基礎を作った上で,そこにプッシュボタンの動作を入れ込む.プッシュボタンの動作も1つのタスクとして実装する.そこからUARTの送信タスクで待ち状態になっているメールボックスにメールを送る.

しかしながら,UARTの受信タスクで受信待ち状態になっているので,これを一旦,解除せねばならない.解除は・・・特にデバイスドライバとしては用意されていないので,クローズして,もう一度オープンすることにする.待ち状態で外部からクローズが実行されると,待ちが解除されて,(ちゃんと作っていれば)I/Oエラーに何らかのエラーが返る.

I/Oエラーが返ってきたら,受信タスクをどこかの時点で再び受信待ちにする必要があるのだが,その前にオープンして,送信可能な状態にしないといけない.

この辺の切り替え処理は送信タスクで行うこととしよう.メールに細工をして,特定のメールを受け取ったときだけ,切り替え処理を行うこととしよう.

実装

概要

タスクは3つ,次のような優先順位で作る.

タスク 優先度
送信タスク 14
受信タスク 16
ボタンタスク 15

とりあえず,ボタンタスクを基準の優先度としておく.受信タスクはユーザインタフェースで遅延とかあんまり気にしなくても大丈夫なので,優先度を下げている.送信タスクは受信タスクよりも優先度を上げている.これは,メモリプールを使い切る前に受信したメールをどんどん処理していこうという意図がある.

UARTの送信と受信の排他制御にミューテックスを使う.これは,優先度継承モードにしておく.

イメージ

図にすると,大体こんな感じ.

             ++
             ++-----------------------+
              | UartSend              |
              |                       |
              |               (UartTx)|-->(To PC)
              |                   |   |
              +-------------------|---+
                     ^            |
                     |            |
                 [mailbox]        |
                   ^   ^       [mutex]
                   |   |          |
      +--(Reply)---+   |          |
      |             (Reply)       |
++    |           ++   |          |
++-------------+  ++--------------|---+
 | Button      |   | UartRcv      |   |
 |             |   |          (UartRx)|<--(From PC)
 |             |   |                  |
 +-----^-------+   +------------------+
      |
      +----------------------------------(push button)

ボタンタスクは,プッシュボタンが押されると,タスク処理が走って,その結果をメールボックスへ送信する.ボタンが押されたかどうかの判断は,ポーリングでやっても良いし,割り込みでセマフォとかイベントフラグとか使っても良いし,その辺はご自由に.

受信タスクはPCからコマンドを受け取ると,処理が走って,その結果をメールボックスへ送信する.

送信タスクは,メールボックスにメールが届いていたら,その内容をPCへ送信する.

UartのTxとRxはミューテックスを使って排他制御が行われる.

共有変数

#define DEVNM "rsa" /* UARTデバイス名 */
#define MSG_SIZE 16 /* メッセージサイズ */

LOCAL ID mpfid; /* 固定長メモリプールID */
LOCAL ID mbxid; /* メールボックスID */
LOCAL ID dd; /* UART デバイスディスクリプタ */
LOCAL ID mtxid; /* ミューテックスID */

/* メッセージの型定義 */
typedef struct {
    T_MSG msgque;
    W msgsize; /* メッセージサイズ */
    UB msgcont[MSG_SIZE]; /* メッセージ内容 */
} T_MSG_PACKET;

固定長メモリプールでは,T_MSG_PACKETのサイズを何個分か確保しておく.

送信タスク

送信タスクでは,メールを受信して,その内容をPCへそのまま流す.

送信と受信はミューテックスを使って,排他制御を行う.
送信タスクでは通常はメール受信待ち状態である.
送信スタートメールが届いてから,送信エンドメールが届くまでの間が,ミューテックスによってロックされる.

受信タスクがメールボックスに送ったパケットがいつ参照し終わって破棄してよくなったかという情報は,受信タスク側ではほとんど必要の無い情報である.参照し終わったかどうか,正確に分かるのは送信タスク側である.したがって,送信タスクは必ず受け取ったメッセージパケットをメモリプールへ返却する.

LOCAL void PCSendTsk( INT stacd, void *exinf )
{
    ER er, ioer;
    ID reqid;
    T_MSG_PACKET *msg;
    SZ asize;

    while(1) {
        /* メール受信待機 */
        er = tk_rcv_mbx(mbxid, (T_MSG*)&msg, TMO_FEVR);
        if(er < E_OK) {
            break;
        }

        if(msg->msgsize < 0) { /* 制御パケット */
            /* 制御パケット処理(後述) */
            /* ミューテックスのロック・アンロックはこの中で行われる. */
        }
        else { /* 返信パケット */
            reqid = tk_wri_dev(dd, 0, (void*)msg->msgcont, msg->msgsize, TMO_FEVR);
            er = tk_wai_dev(dd, reqid, &asize, &ioer, TMO_FEVR);
            if(er < E_OK) break;
        }

        /* メモリブロックの返却 */
        (void)tk_rel_mpf(mpfid, (void*)msg);
    }

    tk_exd_tsk();
}

受信タスク

受信タスクは,PCから来たコマンドを解析して,実行して,結果をメールで送信タスクへ送る.本来は,実行して結果を送る部分は別に専用タスクを作った方が良いのだが,今回はそこまでやらない.また,今回は必ず4文字の受信で何らかのアクションを起こすものとしている.この辺は仕様によっていろいろ変えていかなければならない.

受信タスクは通常,受信待ち状態である.ただし,受信待ちになる前に,ミューテックスを獲得しておく必要がある.受信待ちは永久待ちにしておく.永久待ちであっても,別のタスクからクローズは可能だ.

普通にPCからUART経由でコマンドを送信した場合だと,tk_wai_dev()は普通に正常終了して,ioerも正常で,次のコマンド処理を開始することになる.

受信待ち状態でクローズが発生すると,tk_wai_dev()は正常終了することになっている.そして,ioer(<E_OK)で中断したかどうかを取得できる.

ioer(<E_OK)を検出したならば,ただちにミューテックスを返却し,再び受信待ち状態へと戻る.

LOCAL void PCRcvTsk( INT stacd, void *exinf )
{
    ER er, ioer;
    ID reqid;
    SZ asize;
    T_MSG_PACKET *msg;
    UB buf[4];

    while(1) {
        er = tk_loc_mtx(mtxid, TMO_FEVR); /* ミューテックスロック */
        if(er < E_OK) break;

        /* UART受信待機 */
        reqid = tk_rea_dev(dd, 0, buf, 4, TMO_FEVR);
        er = tk_wai_dev(dd, reqid, &asize, &ioer, TMO_FEVR);

        if(er < E_OK) { /* これはデバイスドライバ由来のエラーで復旧できないのでタスクを終了させておく */
            break;
        } else if(ioer < E_OK) { /* I/Oエラーはこっち */
            er = tk_unl_mtx(mtxid); /* ミューテックスアンロック(強制) */
            if(er < E_OK) break;
            continue;
        }

        er = tk_unl_mtx(mtxid); /* ミューテックスアンロック(通常) */
        if(er < E_OK) break;

        /* 受信文字列の処理(省略) */
        /* msgのメモリブロック取得とデータ生成(省略) */

        /* msgを送信(後述) */
    }

    tk_exd_tsk();
}

ボタンタスク

ボタンタスクでは,プッシュボタンが押されると,処理が進むようになっている.ボタン押下の検出はポーリングでやるか,割り込みを使うか,あるいは,チャタリング除去をどうやって組むかについては,ここでは本質ではないので省略している.

LOCAL void ButtonTsk( INT stacd, void *exinf )
{
    T_MSG_PACKET *msg;

    while(1) {
        /* ボタンが押されるまで待機(省略) */
        /* ボタンが押されたときの処理(省略) */
        /* msgに結果が書かれる(省略) */

        /* msgを送信(後述) */
    }

    tk_exd_tsk();
}

送信処理

msgに返信するデータが入っている場合,送信処理は次のような手順で行う(エラー処理は省略).

    UartSendStart(); /* 送信開始 */
    tk_snd_mbx(mbxid, (T_MSG*)msg); /* msgをメールボックスへ送る */
    UartSendEnd(); /* 送信終了 */

UartSendStart()UartSendEnd()は,送信タスクのロックを確実にやるための処置である.具体的にはパケットのメッセージサイズに特定の値を入れて,送信タスクにメールを送るだけである.

/* 制御用はパケットのsizeを負の値にする. */
#define UART_SEND_START -1
#define UART_SEND_END -2

Inline LOCAL ER UartSendCtrl(W size)
{
    T_MSG_PACKET *msg;
    ER er;

    er = tk_get_mpf(mpfid, (void*)&msg, TMO_FEVR);
    if(er < E_OK) goto ERROR;

    msg->msgsize = size;

    er = tk_snd_mbx(mbxid, (T_MSG*)&msg);
    if(er < E_OK) goto ERROR; 

ERROR:
    return er;
}

Inline LOCAL ER UartSendStart(void)
{
    return UartSendCtrl(UART_SEND_START); /* 送信スタート */
}

Inline LOCAL ER UartSendEnd(void)
{
    return UartSendCtrl(UART_SEND_END); /* 送信エンド */
}

送信タスクで制御パケットを受け取った場合は次のような感じで処理する.

LOCAL void PCSendTsk( INT stacd, void *exinf )
{
    ER er;
    T_MSG_PACKET *msg;
    T_RMTX rmtx;

    while(1) {
        /* メール受信.msgが更新される.(省略) */

        if(msg->msgsize < 0) { /* 制御パケット */
           if(msg->msgsize == UART_SEND_START) { /* 送信開始 */
                /* ミューテックスがロックされてるかどうかを調べる */
                er = tk_ref_mtx(mtxid, &rmtx);
                if(er < E_OK) break;

                if(rmtx.htsk) { /* 受信タスクでロック中 */
                    /* クローズしてロックしてオープン */
                    (void)tk_cls_dev(dd, 0);
                    er = tk_loc_mtx(mtxid, TMO_FEVR);
                    if(er < E_OK) break;
                    dd = tk_opn_dev(DEVNM, TD_READ|TD_WRITE);
                    if(dd < E_OK) break;
                }
                else { /* ロックしてるタスク無し */
                    er = tk_loc_mtx(mtxid, TMO_FEVR);
                    if(er < E_OK) break;
                }
            }
            else { /* 送信完了 */
                er = tk_unl(mtxid);
                if(er < E_OK) break;
            }
        else {
            /* 返信(省略) */
        }
        /* メモリブロックの返却(省略) */
    }
    tk_exd_tsk();
}

送信スタートメールが届いたとき,既にロックされている可能性がある.これはミューテックスの状態を参照すれば判別できる.

ロックされていたならば,UARTの受信待ち状態が原因なので,一度UARTをクローズしてから,ミューテックスのロック要求を出す.こうすれば,受信タスクの優先度が上がるので,受信タスクはすぐに動いてアンロック処理が実行される.受信タスクでアンロック処理が実行されると,タスクの優先度は元に戻るので,送信タスク側ですみやかにロックされる.

もし,ロックされていない状態であったならば,UARTデバイスに対して特に何もすることなく,そのままミューテックスをロックするだけで良い.

送信エンドメールが届いた場合は,ミューテックスをアンロックするだけである.

受信タスクのI/Oエラー発生時の動作

受信タスクのI/Oエラー発生時の動作が気になる人がいるかもしれない.だが,安心して欲しい.これはちゃんと意図した順番に動いてくれる.送信タスクのクローズ処理から次のような順番で処理が走る.

  1. 送信タスクでUARTクローズを実行
  2. 送信タスクでロックを実行,アンロックされるまで待つ→ミューテックスの特殊効果発動!ロックしているタスク(受信タスク)の優先度が上がる
  3. 受信タスクの受信待ちでioerが発生
  4. 受信タスクでアンロック→受信タスクの優先度が元に戻る
  5. 送信タスクでロック成功
  6. 送信タスクでUARTオープン

このように,優先順位をちゃんと把握できれば,どの順番で処理が走るのかが分かる.

この後,受信タスクがいつ動くかは不定である.受信タスクよりも優先度の高いタスクすべてが同時待ち状態にならないと,受信タスクが動かないためである.しかしそれでも,送信タスクでアンロックされるまでは,受信タスクは最悪でもロック待ち状態までしか処理が進まないことが保証される.そして,受信タスクがロックに成功するのは,必ず,送信タスクがアンロックをして,メールの受信状態になった後である.

今後の検討と課題

結果の送信手順

tk_snd_mbx(mbxid, (T_MSG*)msg)で結果を送信する前後をUartSendStart()UartSendEnd()で挟む構造にしている.これら3つの手順を1個にまとめた関数UartSend()を次のように作ることもできる.

Inline LOCAL ER UartSend(T_MSG_PACKET *msg)
{
    UartSendStart();
    tk_snd_mbx(mbxid, (T_MSG*)msg);
    UartSendEnd();    
}

場合によってはこれでも良いのだが,複数のパケットを送りたいときに問題が発生する.

予め送るパケットの数が分かっているなら,引数msgを配列にして,引数に個数を付け加えれば良い.

しかし,何個のパケットを送れば良いのか分からない場合は,この構造では対処できない.

そういうわけで,ここは敢えて3個の関数にバラしている.

パケットの順番

受信タスクと,ボタンタスクで,送信開始,結果,送信終了の3個のパケットをばらばらにメールボックスへ送る箇所がある.今回は,PCからのコマンド処理とボタンの操作は排他的という制約とか,タスクの優先順位の制約とかがあるので,特に問題は起きないのだが,一般論としてかなり危うい.ここの改善策としては,パケットを1個にまとめる手法,タスクディスパッチを抑制しておく手法などが考えられる.

もし,ボタン操作でやりたいことが,PCからのコマンドと同等に扱えるもの,要はある特定のコマンドを実行するだけのものであるなら,別の手法もある.コマンド処理タスクを作って,そこから送信タスクへメールを送るようにする.図で描くと,大体,こんな感じ.

             ++
             ++-----------------------+
              | UartSend              |
              +-----------------------+
                     ^
                     |
                 [mailbox]        
                     ^
 ++                  |(Start,Reply,End)
 ++------------------|---------------+
  | Cmmand           |               |
  |                  |               |
  |[rendezvous]->[Proccess]->[Reply] |
  +---^---^---------------------|----+
      |   |   +-----------------+
      |   |   |                 |
      |   +-----(Command)--+    |
  (Command)   |            |    |
++    |       v     ++     |    v
++---------------+  ++------------------+
 | Button        |   | UartRcv          |
 +---------------+   +------------------+

こうしておけば,メールボックスへのルートは1つになるので,パケットの順番がおかしくなることはない.

コマンド処理タスクはランデブを使うことができる.ランデブ送信でコマンドは一旦キューに入るので,例えばPCからのコマンドの処理中であっても,プッシュボタンを押してコマンドを送ることができる.また,ランデブ要求を出したタスクはランデブが完了するまで止まるが,コマンドを多重に送信してしまうことを防ぐ効果がある.ただ,この辺はどういう仕様で実装するかにも関わってくることなので,事前にしっかりと検討しておくことが必要だろう.

コマンド処理タスクの優先度は受信タスクよりも上げておいた方が良いだろう.そうしておけば,ランデブのキューにコマンドが複数入っている場合に,受信タスクがミューテックスをロックする前にコマンド処理タスクが動くので,クローズ処理が何度も走らなくなる.

デバイスドライバの改良の可能性

送信と受信が同時にできないのは,GDIドライバのタスク処理が1個だけであるのが影響している.これは複数のタスクに分割しても良いことになっているようなので,デバイスドライバのレベルでちゃんと対応させることができれば,送受信はもう少し簡単になる可能性はある.ただし,ソフトウェアがどんなに対応できたとしても,ハードウェアで送受信バッファを共有しているならば,時間的に同時に送受信は不可能なので,そこは排他制御を行ったり,通信プロトコルを規定したりするなど,設計前に検討しておかなくてはならない.

思い切って,マイコンのUARTポートを2個使って,それぞれ受信専用,送信専用とするのもアリかもしれないが,基板の設計段階からそうしておかないといけないとか,PCと接続するポートはマイコンの書き込みにも使うから分けられないとか,実現のためのリスクは多そうだ.

まとめ

μT-Kernel2.0でタスク間通信をメールボックスを使って実現してみた.UARTの送信と受信を2個のタスクに分けて,ミューテックスで排他制御することで,できるだけ自由なタイミングで送受信できるようにした.ミューテックスを使う場合は,タスクの優先度がどのように変化するかちゃんと把握できていれば,どのような順番で動くかも分かって,デバッグもやりやすくなるだろう.

1個のメールボックスへ送信するタスクが複数ある場合の競合についても検討を行った.今回は様々な制約のおかげで特に問題は起きないが,一般論としては送信手順で送る3種のパケットの扱いについて,もう少し検討が必要そうである.