はじめに
μT-Kernel2.0で簡単なシリアル通信ソフトを作ってみる.
ここではタスク間通信(ランデブ)をメインとしたいので,それ以外の本質的で無い部分は大胆に省略する.
仕様
想定するハードウェア
想定するハードウェアはこんな感じか.
+------------------------------+
| Embedded System |
| +----------+ UART +-----+ | serial +--+
| |Peripheral|<======>|micom|<==========>|PC|
| +----------+ +-----+ | +--+
+------------------------------+
マイコンと周辺機器がUARTで接続されている.別にI2CでもSPIでも何でも良くて,通信方式はここでは重要ではない.
PCからマイコンに対してシリアル通信が可能である.実際にはUART--USBだったり,UART--RS232CだったりのICが入るが,そこも本質ではないので省略.
当然ながら,使うマイコンにMMUは無いし,シングルコアだ.組み込み業界ではよくある,普通のマイコンだ.
やりたいこと
ユーザはPCからマイコンへコマンドを送信すると,マイコンは受け取ったコマンドを周辺機器へ転送する.
周辺機器はコマンドを受信すると,処理を行い,結果をマイコンに返す.
マイコンは結果をそのままPCへ出力する.
コマンドはSTX
から始まり,ETX
で終わる.コマンド長は制御文字を入れて16文字以内ということにしよう.
周辺機器の状態遷移は,コマンド受信待機→コマンド受信→処理→結果を返信→コマンド受信待機
を繰り返す.
マイコンはコマンドの受信待機中にコマンドを送信しなければならない.
周辺機器の返信は,必ずACK
で終了するが,何文字あるかは分からない.
作ってみる
設計の方針
とりあえず,ランデブ使えば何とかなる.ランデブは,サーバ--クライアントモデルに基づくタスク間通信の手段である.
デバイスドライバとかまで書いてるときりがないので,ここでは省略.
こんな感じ?
タスクの起動とか,優先度とか,デバイスの定義とか,細かい部分は全部省略.
定義
#define PR_CALPTN 0x00000001 /* 周辺機器用 */
#define PC_CALPTN 0x00000002 /* PC用 */
#define STX 0x02
#define ETX 0x03
#define ACK 0x06
LOCAL ID porid; /* ランデブ用ID */
LOCAL ID pcid; /* PC用通信デバイスID */
周辺機器用サーバタスク
マイコンと周辺機器の通信を行う.通信自体はデバイスドライバに任せる.
サーバはランデブ受付け中の状態で動き始める.
クライアントからコマンドを受け取ると,サーバは周辺機器へそのコマンドを送信する.
周辺機器からの返答はPCへ送信する.
周辺機器からACK
を受け取ると,ランデブ返答をして,再びランデブ受付け中の状態になる.
LOCAL void PeripheralSeverTsk( INT stacd, void *exinf )
{
ER er;
INT cmsgsz; /* ランデブ受付けメッセージサイズ */
INT rmsgsz; /* UART受信メッセージサイズ */
SZ asize;
RNO rdvno;
UB cmd[16], rcv[16];
while(1) {
/* ランデブ受付け待ち */
cmsgsz = tk_acp_pol(porid, PR_CALPTN, &rdvno, cmd, TMO_FEVR);
if(cmsgsz < E_OK) break;
/* 受信したコマンドcmdを周辺機器へ送信(省略) */
while(1) {
/* 周辺機器から1文字ずつで受信(省略) */
/* 受信バッファ:rcv,受信サイズ:asize */
/* PCサーバへ転送 */
rmsgsz = tk_cal_por(porid, PC_CALPTN, rcv, asize, TMO_FEVR);
if(rmsgsz < E_OK) break;
/* ACKで周辺機器からの返答は終了 */
if( rcv[0] == ACK ) {
break;
}
}
/* ランデブ返答 */
er = tk_rpl_rdv(rdvno, rcv, 1);
if(er < E_OK) break;
}
}
PC用サーバタスク
ランデブで受信した文字をそのままPCへ送信するだけの簡単なお仕事.
LOCAL void PCSeverTsk( INT stacd, void *exinf )
{
ER er;
INT cmsgsz; /* ランデブ受付けメッセージサイズ */
RNO rdvno;
UB rcv[16], snd[16];
while(1) {
/* ランデブ受付け待ち */
cmsgsz = tk_acp_pol(porid, PC_CALPTN, &rdvno, rcv, TMO_FEVR);
if(cmsgsz < E_OK) break;
/* 受信したメッセージrcvをPCへ送信(省略) */
/* ランデブ返答 */
er = tk_rpl_rdv(rdvno, snd, 16);
if(er < E_OK) break;
}
}
クライアントタスク
ランデブでコマンド送信を周辺機器サーバ依頼し,返答を待つ.周辺機器サーバから返答があったら,周辺機器から結果に基づいた,何らかの処理を行う.
LOCAL void PCClientTsk( INT stacd, void *exinf )
{
ER er;
INT rmsgsz; /* ランデブ返答メッセージサイズ */
INT ct = 0;
UB rcv, cmd[16];
while(1) {
do {
/* PCから1文字ずつで受信(省略) */
/* 受信バッファ:rcv */
/* コマンド文字列生成 */
cmd[ct++] = rcv;
} while(rcv != ETX); /* ETXでコマンド受信完了 */
/* 周辺機器サーバにコマンドを渡す */
rmsgsz = tk_cal_por(porid, PR_CALPTN, cmd, ct, TMO_FEVR);
if(rmsgsz < E_OK) break;
/* 周辺機器の返答に基づく処理 */
ct = 0; /* カウントのリセット */
}
tk_exd_tsk();
}
コマンド文字列生成のあたりは,タスク間通信とはあまり関係が無いので,かなり省略.実際にはctの値がcmdのサイズを越えないように制限したり,ちゃんとSTX
を検出したり,やらないといけないことはいろいろある.
動作確認
PCからコマンドを送信すると,周辺機器からの返答がPCに表示される.正しく動いていそうだ・・・・・・本当に?
もう一度,PCからコマンドを送信すると,おそらく無反応だ.
動かない原因
デバッガを動かして,どういう動きをしているか見てみると,周辺機器サーバで,ACK
の検出がうまく動いていないことが分かる.PCでは確かにACK
が受信できていたにも関わらず.
答えは簡単.ACK
の検出の直前にある,ランデブ呼び出し(tk_cal_por()
)が原因だ.
ランデブ呼び出しで指定するメッセージバッファは入出力で共用となので.ランデブ先から復帰したときに,バッファはまず間違いなく,送信とは別の値に書き換わっている.
呼び出し先のPCサーバを確認してみると,tk_rpl_rdv()
で,snd
配列を返答している.ランデブ呼び出し後のrcvバッファを参照しても,ACK
なんて入っているわけがないのだ.
デバッグ
修正の方法はいくつかあると思うが,今回は周辺機器サーバ内だけで直す.
と言っても,PCへ送信される前の値を保存しておくだけなので,わざわざ書くほどのことでもない.
PC用サーバ内で,snd
を初期化もせずに返答に使っているという別の問題があるのだが,ここではその修正はやらない.
LOCAL void PeripheralSeverTsk( INT stacd, void *exinf )
{
/* 宣言(省略) */
UB history; /* 追加 */
while(1) {
/* ランデブ受付け待ち(省略) */
/* 受信したコマンドを周辺機器へ送信(省略) */
while(1) {
/* 周辺機器から1文字ずつで受信(省略) */
history = rcv[0]; /* 受信した文字を保持 */
/* PCへ転送(省略) */
/* ACKで周辺機器からの返答は終了 */
if( history == ACK ) {
break;
}
}
/* ランデブ返答(historyを返す) */
er = tk_rpl_rdv(rdvno, &history, 1);
if(er < E_OK) break;
}
}
これで,ACK
が正しく検出できるようになった.
修正すべき点は本当にこれだけだろうか?
今後の課題と検討
ランデブ返答
今回のソースでは,周辺機器サーバのランデブ返答はACK
しか入らないのであまり意味が無いが,通常は何らかの意味のある返答を行うべきだろう.じゃないと,わざわざランデブを使う意味が無くなってしまう.
例えば,温度センサがつながっているのであれば,返答には温度の値を入れれば良いだろう.
周辺機器からの返信をPCへ転送
今回のソースで周辺機器サーバは,周辺機器からの返信を1文字ずつPCへ転送している.これは16文字だか32文字だかある程度貯めておいてから,まとめてPC用サーバへ転送する方が効率的だろう.わざわざ書くほどのことでも無いが,こんな感じか.
LOCAL void PeripheralSeverTsk( INT stacd, void *exinf )
{
/* 宣言(省略) */
INT ct;
UB cmd[16], rcv[16], history;
while(1) {
/* ランデブ受付け待ち(省略) */
/* 受信したコマンドcmdを周辺機器へ送信(省略) */
ct = 0;
while(1) {
/* 周辺機器から1文字ずつで受信(省略) */
/* 受信バッファ:history */
rcv[ct++] = history;
if( ct == 16 || history == ACK ) { /* 16文字受信かACKの受信で,PC用サーバにまとめて転送 */
rmsgsz = tk_cal_por(porid, PC_CALPTN, rcv, ct, TMO_FEVR); /* PCサーバへ転送 */
if(rmsgsz < E_OK) break;
if( history == ACK ) break; /* ACKで周辺機器からの返信は終了 */
ct = 0;
}
}
/* ランデブ返答(省略) */
}
}
うまくいきそうな気がするけれど,実は別の問題が発生する可能性がある(後述).
PC用サーバタスク
PC用サーバを作らずに,周辺機器用サーバのように1個にまとめてしまう手が無くはない.PC用クライアント内の「周辺機器の返答に基づく処理」で,直接PCに返答を送信する手法だが,バッファをどうするかの問題があるので,別途検討する必要があるだろう.
実は,マイコンと周辺機器の関係と,マイコンとPCの関係は似ているようでちょっと違う.周辺機器が何か返信をするのは,必ず,コマンドを送信した後である.これは,周辺機器の仕様1として決められていることとする.一方,マイコンからPCに向けて何か出力するのは,本当にPCからコマンドを受け取ったときだけなのか?
他のタスク処理の結果をPCへ出力したいかもしれない.その場合,出力シーケンスがPCClientTsk
の奥の方に書かれていると,そこを使うのは基本的には無理だ2.
PCからの受信タスクと,PCへの送信タスクは分けておいた方がいろいろ使えそうな気がする.
周辺機器サーバとPC用サーバのタスク間通信
PC用サーバタスクはランデブで設計するのが適切だったか?という問題がある.PC用サーバタスクはPCに情報を流すだけなので,情報を流し終わっても,送信終了以上の意味がある情報を返答できない.また,周辺機器サーバにとっても,PCに向けてメッセージが送信し終わったかどうかという情報は重要ではない.そうなると,単にメールボックスか,メッセージボックスか,返答を必要としない別の仕組みを使った方が良いかもしれない.当然,通信速度とかタスクの優先度とか,ちゃんと考えて作らないと,PCになかなか返信が表示されない現象とか,バッファがあふれるとか出てきそうなので,注意が必要である.
今回はランデブを使っているので,呼び出し側タスクは返答が来るまで待機状態になる.今回の場合だと,呼び出した順番通りにタスクが動いてくれるので,タスクの優先順位をあまり気にする必要がないというメリットがある3.が,PCへの送信中に周辺機器からの受信処理ができないので,効率的で無いというデメリットもある.周辺機器側の通信速度がPC側の速度よりも速かったり,上記のように何文字かまとめてPCへ送信のために長時間,周辺機器からの受信処理ができない状態が発生したりすると,通信ドライバ側の受信バッファがあふれる可能性が高くなる.