USBプロジェクト - ファームウェアに立ち返る (7) [USB]
今日は、「SETUPトランザクションが到着したら、残りのデータは破棄すべきなのか」について考えるのココロだ~
SETUPトランザクションを常に優先すべきか
前回の記事に対するコメントで、SETUPトランザクションは、無視すべきではなく、到着時にバッファに残っているデータを破棄すべきとの指摘を受けました。 全ての場合において、本当にそうなのかと考えて、検証してみました。
このシーケンス図は、INコントロール転送を受けたデバイスが、DATA-INステージの処理に手間取って、次のSETUPステージを取りこぼした例です。 DATA-INステージでは、例によってRX0E=0としているので、SETUPステージは、無視されています。
もし、SETUPステージが常時受け付け可能で、これを優先して受け付けたとすると、 cpuは、STATUSステージの受信を認識することができません。 ところが、hostは、STATUSステージを送信し、ACKを受け取ることが出来ます。 つまり、cpuとhostで情報の不一致が起こることになります。
何が問題なのか
私が考えるに、 問題は、SETUPパケットのバッファとOUTパケットのバッファが同じところにあるからではないでしょうか。 もし、別々のバッファがあれば、OUTパケットの処理に手間取っても、別のバッファでSETUPパケットを受け取ることが出来ます。
ただし、その場合でも、OUTパケットとSETUPパケットのうち、どちらが先に到着したかによって処理が異なってくるので、「優先すべきパケットは、コッチだ。」フラグが欲しくなります。
バッファのデータは、さっさと引き取るべし
MC908JB16は、そういった構成になっていないので、とにかく、到着したトランザクションを遅滞無く処理していくことが必要になると思います。
- エンド・ポイントのデータは、さっさと一時バッファに引き取り、次のトランザクションに備える。
- 割り込みハンドラ内で処理をしない。
割り込みハンドラ内でも、「待った」のきかないSETUPを確実に処理するために、全ての割り込み処理が終わるまで、割り込みハンドラを抜けない工夫も必要なのではないでしょうか。
USB_isr(void) { for (;;) { if (UIR1_RXD0F) { // EP0 OUT packet : continue; } if (UIR1_TXD0F) { // EP0 IN packet : continue; } if (UIR1_RXD1F) { // EP1 OUT packet : continue; } : break; } }
これ以上は、処理速度の速いエンジンかプロセッサを持ってくるしかないでしょうか。
付録 : USBプロジェクト索引
- USBプロジェクト - ファームウェアに立ち返る (1)
- USBプロジェクト - ファームウェアに立ち返る (2)
- USBプロジェクト - ファームウェアに立ち返る (3)
- USBプロジェクト - ファームウェアに立ち返る (4)
- USBプロジェクト - ファームウェアに立ち返る (5)
- USBプロジェクト - ファームウェアに立ち返る (6)
- USBプロジェクト - ファームウェアに立ち返る (7)
- USBプロジェクト - ファームウェアに立ち返る (8)
- USBプロジェクト - ファームウェアに立ち返る (9)
- USBプロジェクト - ファームウェアに立ち返る (10)
- USBプロジェクト - ファームウェアに立ち返る (11)
- USBプロジェクト - ファームウェアに立ち返る (12)
- USBプロジェクト - ファームウェアに立ち返る (13)
- USBプロジェクト - HIDデバイス(1)
- USBプロジェクト - HIDデバイス(2)
- USBプロジェクト - HIDデバイス(3)
- USBプロジェクト - 複合デバイスを考えた
- USBプロジェクト - HIDデバイス(4)
- USBプロジェクト - HIDデバイス(5)
参考文献
USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)
- 作者: インターフェース編集部
- 出版社/メーカー: CQ出版
- 発売日: 2006/07
- メディア: 単行本
USBターゲット機器開発のすべて―各種USBコントローラの使い方と基本ソフトウェアの作成法 (TECHI―Bus Interface)
- 作者:
- 出版社/メーカー: CQ出版
- 発売日: 2005/08
- メディア: 単行本
>DATA-INステージでは、例によってRX0E=0としているので、SETUPステージは、無視されています。
DATAステージでRX0E=0としているので、デバイスはこのトランスファのSTATUSステージでOUTトランザクションに対してNAKを返し、トランスファが完了しません。このトランスファが完了するまでは、ホストは次のSETUPを出しません。
そもそもRX0E=0は本当に必要でしょうか?
コントロール読み込み転送では、SETUPを受けた直後にデバイス側の処理が始まります。処理に時間がかかる場合はここでしっかり時間を使って転送すべきデータをすべて用意しておきます。つまり、リクエストの実行はホストへのデータ転送を除いてすべて終了した状態に持っていくわけです。ホストはDATAステージでINトランザクションを繰返して待っています(IN-NAK)。
一旦DATAステージの転送が始まると、転送データが複数のINトランザクションに渡っていても、STATUSステージも含めてノンストップでいくという実装にすれば良いでしょう。つまり、STATUSステージで止めるのではなく、DATAステージで止めておくわけです。そうすれば、RX0E=0でOUT-NAKしなくても問題ありません。
むしろOUTエンドポイントは常に開けておいて、データステージでOUTを見たらプロトコル違反でSTALLして良いでしょう。
8.5.3 Control Transfers (usb_20.pdf p226)
All the transactions in the Data stage must be in the same direction (i.e., all INs or all OUTs).
このリクエストをSTALLする場合は、DATAステージのINトランザクションに対してSTALLがかかります。このトランスファはこの時点で打ち切りとなり、ホストは次に新たなSETUPを送ってきます。
>もし、別々のバッファがあれば、OUTパケットの処理に手間取っても、別のバッファでSETUPパケットを受け取ることが出来ます。
そういうエンジンも確か1つだけ見たことがありますが(うーん、どれだったか思い出せない)、大抵はこのエンジンのようにIN/OUTの2つのバッファで、SETUPとOUTは共用です。でも、もう少し工夫を凝らしてあります。
>ただし、その場合でも、OUTパケットとSETUPパケットのうち、どちらが先に到着したかによって処理が異なってくるので、「優先すべきパケットは、コッチだ。」フラグが欲しくなります。
その意味でもSETUPとOUTが共用になっているんでしょう。SETUPデータをMCUが落とすまでOUTトランザクションはNAKされます。
>エンド・ポイントのデータは、さっさと一時バッファに引き取り、次のトランザクションに備える。
>割り込みハンドラ内で処理をしない。
まさにその通りです。
デフォルトエンドポイントに関しては、
- 割り込み処理はエンドポイントとファームウェア上のバッファとのデータ交換を行い、フラグを立てる
- - OUTエンドポイント(Rx) ---> SETUPデータ、OUTバッファ
- - INエンドポイント(Tx) <--- INバッファ
- main loop 内のタスク(co-operative multitask)、もしくはRTOSのタスクでSETUPデータを処理し、リクエストを実行
というのが、本格的な実装です。リクエストを実行している間にも頻繁にフラグをチェックし、新たなSETUPを見たらそのリクエストの処理を中断します。
ところで、このMCUでコントロール書き込み(OUT)転送は必要ですか?
コントロール書き込み転送を実装しないという選択肢もあると思うんですが。そうすると、そこそこ話が簡単になります。
それぞれSETUPの後で転送を待たせておくと、
データ無しコントロール転送:STATUSステージ(IN)
コントロール読み込み転送:DATAステージ(IN)
となり、NAKするのはINエンドポイントのみで、RX0E=0でOUTエンドポイントをNAKする必要が無くなります。
コントロール書き込み転送も、DATAステージを一気に受けてSTATUSステージで待たせておけばIN-NAKになりますが、すべてSETUPの後で転送を待たせておく方が制御構造が簡単です。
コントロール書き込み転送であるリクエストは実はそんなにありません。
標準デバイスリクエスト
- Set_Descriptor 使いません。
HID
- Set_Report( Output )、Set_Report( Feature )
- Set_Report( Output )はインターラプトOUTエンドポイントの方がいいですね。
CDC (Communication Device Class)(ごまかしてインターラプトEPで)
- Send_Encapsulated_Command、Set_Line_Coding
プリンタ
- 特になし
Still Image (ごまかしてインターラプトEPで)
- 特になし
マスストレージ (まじで!?)
- 特になし
バルクエンドポイントを要求するクラスは、実はごまかしてインターラプトエンドポイントで代用できます。
Tsuneo
by Tsuneo (2008-03-27 06:44)
Tsuneoさん、今日も、ありがとうございます。
> DATAステージでRX0E=0としているので、
これは、間違えました。正しくは、
「STATUS-OUT終了後にRXD0F=1となっているので次のSETUPが無視される」
です。別のタイミング図と混同してしまっていました。
「DATAステージで待たせろ」というのは、具体的には「全データがそろうまで、 TX0E=0 としておいて host を待たせる。」という意味だと解釈しました。なるほど、そうすると、マイコンは、STATUS-OUTの到着まで、データ送信に専念することができるのですね。
すると、「エンド・ポイントにデータを積んで、TX0E=1 にする」仕事は、割り込みハンドラの外で実行すべきなのかな?いよいよ、RTOSか?
> ところで、このMCUでコントロール書き込み(OUT)転送は必要ですか?
昨日、SETUPトランザクションが上書きする状況を突き詰めて考えてみて、「コントロールOUT転送が無ければ良い」という所に行き着くのだと思いました。EP0/EP1を使って、HIDデバイスを作る場合には、SET_REPORTリクエストが必須ですが、EP2をインタラプトOUTに追加するのであれば、コントロールOUT転送は、全く出てこないのですね。
これまで、仕様など深く考えずにいた頃には、「全部、コントロール転送で済ましちゃえ」と軽く考えていたのですが、非力なマイコンには、コントロール転送の完全制御は難しいらしいことがわかってきました。「コントロールOUT転送非実装」で考えます。
それでも、MC908JB16の場合には RXD0F==1 で SETUP パケットが無視されるので、 STATUS-OUT に対して遅滞なくRXD0Rで応答する必要はあります。なるほど、RXD0Fが邪魔なわけが理解できました。
そろそろ、HIDデバイスを仕上げなくては。
by noritan (2008-03-27 10:09)
>そろそろ、HIDデバイスを仕上げなくては。
実はこのコントロール転送が、デバイスのUSBスタックの実装で一番面倒な部分です。リクエストパーサーは面倒に見えても、各リクエストでかなり定型的で一つ二つリクエストを実装すればすぐこつがつかめます。この山を越えれば、後はらくちんです。
これまでの議論でコントロール転送の実装のアウトラインが見えてきたことと思います。と同時に、細かな例外的処理もいくつかと、このエンジン特有の要件も出てきました。まずは、コントロール転送の骨格のコードを書いて、それから一つ々々例外的処理を加味していけば良いでしょう。
コントロール転送の骨格 - 要はバッファの処理ですね、転送だけに。
[ USBインターラプトルーチン ]
- SETUP: エンドポイントからSETUPデータに転送
- INエンドポイント: フラグ、カウンターを見て、INバッファからエンドポイントにデータを転送
- OUTエンドポイント:(フラグを見て、エンドポイントからOUTバッファにデータを転送)
それぞれ、転送終了後ステート変数あるいはフラグの調整
[ USBタスク (main loopもしくはRTOS)]
- SETUPステージ
- - SETUPデータのパーサー(リクエスト分岐)
- - リクエスト実行
- - - コントロール読み込み転送で、転送データをバッファに詰める(開始アドレスをセット)
- DATAステージ
- - INエンドポイントに最初のパケットを転送(これで次からINエンドポイントのインターラプトが入る)
- STATUSステージ
- - データ無しコントロール転送でINエンドポイントにゼロレングス・パケット
INバッファは、ポインタで指定して臨機応変に切り替えられるようにしておくと良いでしょう。そうすると固定データ(デスクリプタなど)は、ポインタに開始アドレスをセットするだけとなります。
main loopでco-operative multitask を組むか、本格的にRTOSを使うか、これまたある程度好みの問題でしょう。USBの処理に関してはmain loopタスクで充分です。後はその上に何を乗せるかで判断が分かれるでしょう。それぞれに得失がありますから、結局最後は好みの問題ということです。
コントロール転送のステートマシンもしくはデシジョンツリー
- - SETUPステージでステートマシン、フラグの初期化
- - DATAステージ、STATUSステージ
- - - コントロール転送の種類による区別
- - - DATAステージをさらに実行フェーズと転送フェーズに分離するかどうか
このあたりはステートマシンにするか、フラグによるデシジョンツリーにするか、双方の混在にするか、好みの問題でしょう。
- ステートマシンだと排他ロジックが自動的に組み込まれますが、インターラプトルーチンにもステート分岐が入ります。
- フラグだとインターラプトルーチンは軽いけれど、排他ロジックがコードのあちこちに撒き散らされます。
ステート変数は結局一度にon/off、比較できるフラグのかたまりですから。大抵は混在でやってますね。
[ 例外的処理 ]
1) NAKing
- USBインターラプトルーチンで、SETUPを見たらINエンドポイントをNAKing(リクエスト中断対策)
- データ無しコントロール転送:STATUSステージの終了でINエンドポイントをNAKing
- コントロール読み込み転送:DATAステージの終了でINエンドポイントをNAKing
- これでデータ無しコントロール転送とコントロール読み込み転送は、USBタスクから解除されるまで待ち状態に入る。
2) STALLのサポート
- STALLの開始 ISTALL0とOSTALL0をセット
- STALLの終了 エンジンがSETUPを受けると自動的にISTALL0とOSTALL0を落とす
- コントロール転送のステートマシンの調整
- - STALLを発行すると、コントロール転送はそこで中断。
- - 次のSETUPでステートマシンが初期化。
3) ゼロレングス・パケット(ZLP)
コントロール読み込み転送ではDATAステージでZLPが必要になる場合があります。
最初の入門編をやっていた頃にZLPについてちゃんと解説しておけば良かったんですが、ここでコントロール読み込み転送にからめてやってしまいましょう。
トランスファ(DATAステージは実はほぼ独立したトランスファ扱いです)の終了は、
a) 期待されるバイト数が転送されたとき
あるいは
b) ショートパケット(ZLPを含む)が転送されたとき
となっています。
a) 期待されるバイト数が転送されたとき
コントロール読み込み転送ではSETUPデータのwLengthフィールドであらかじめ転送量がデバイスに通知されています。従って、DATAステージでちょうどこの量を転送するなら、DATAステージの最後にはZLPは付けません。
コントロール読み込み転送は「期待されるバイト数」が明らかなので比較的納得しやすいのですが、実はこの「期待されるバイト数」が判っているのかどうか判定に苦しむ場合も多々あります。それでZLPを付ける付けないという試行錯誤が始まるわけです。
b) ショートパケットが転送されたとき
ショートパケットとは、そのエンドポイントのwMaxPacketSize (デフォルトエンドポイントはbMaxPacketSize0) よりパケットのデータ長が短いものを指します。wMaxPacketSize = 8 なら、データ長 = 0..7がショートパケットです。で、データ長 = 0のものがZLPです。
ショートパケットは、トランスファを途中で打ち切る時に使用します。
コントロール読み込み転送では、SETUPデータのwLengthに満たない転送量でDATAステージを打ち切る場合です。
例えば、WindowsはGet_Descriptorでデスクリプタを要求するとき、いきなり255バイト寄越せと言ってきます。たいていのデスクリプタは255バイトもないので、デスクリプタのサイズで転送を打ち切ります。デスクリプタのサイズがちょうどbMaxPacketSize0の整数倍ならば、ショートパケットで終了するためにZLPを付加しなければなりません。
この処理はGet_Descriptorに限らずすべてのコントロール読み込み転送に共通です。
実際の転送量 = min( 指定されたデータのサイズ, wLength )
if ( (実際の転送量 < wLength) && (実際の転送量 % bMaxPacketSize0 == 0) ) ZLPを付加;
4) リクエスト実行途中のSETUPによる処理の中断
これはしっかりサポートするのはかなり面倒なので、中断される処理の全体がほぼ仕上がってから最後に実装するのが良いでしょう。
これで抜けはないかな。何かあったらまたポストします。
Tsuneo
by Tsuneo (2008-03-27 17:56)