USBプロジェクト - ファームウェアに立ち返る (1) [USB]
これまで、「HIDプロトコルを実装したサンプル・プロジェクト」を見よう見まねで使ってきました。 BLOGのコメントにかなり理解の助けになる情報を書いてもらいましたので、 ここで、このサンプル・プロジェクトがUSBデバイス、または、HIDデバイスとしてふさわしいかどうか検証しながら整備していこうと思います。 誤解などありましたら、どしどしコメントをください。 まずは、メモから。
USBの基本:パケット
USBケーブルを流れるすべてのデータは、パケットと呼ばれる単位で表現されます。
MC908JB16では、パケットの処理部分は、ハードウェアで行われ、ファームウェアで関与することはまずありません。
USBの基本:トランザクション (Transaction)
パケットには、 TOKEN, DATA, HANDSHAKE の三つの種類があります。 トランザクションは、これら三つのパケットからなるデータ(payload)送受のための最小単位です。 トランザクションは、 TOKEN パケットの種類によって SETUP, DATA, STATUS の三つの種類があります。
トランザクションは、ホストしか開始することが出来ません。 そのため、USBデバイスは、ホストから到着するトランザクションを延々と待つのが仕事になります。
MC908JB16の場合、トランザクションの終了により割り込みが発生します。 ファームウェアは、到着したトランザクションの種類により、適切な処理を行います。
割り込みが発生するのは、トランザクションの終了時です。 このため、ホストからデータを受け取るときには、受け取ったデータに従った処理を行う事になります。 ところが、ホストにデータを送る場合には、トランザクションの開始前に送信すべきデータをあらかじめバッファに入れておく必要があります。 そのためには、ホストから送られてくるトランザクションを解析して、次に要求されるであろうデータを予測し、しかるべきバッファに配置しておく必要があります。
USBの基本:ステージ (Stage)
トランザクションには、 SETUP, DATA, STATUS の三つの種類があります。 この種類の事をステージと呼んでいます。
USBの基本:トランスファ (Transfer)
トランスファは、任意の長さのデータを送受信するための通信単位です。 トランスファには、三つの種類があり、コントロール、インタラプト、バルクと呼ばれています。
コントロール・トランスファでは、 SETUP, DATA, STATUS のすべてのステージが使用されます。
SETUPステージでは、これから開始されるデータ転送に関する情報がホストから送られてきます。 MC908JB16の場合、 SETUP トランザクションが到着したことを示すフラグSETUPがあり、 SETUP トランザクションに特有の「これから開始される通信の予測」処理が始まります。
if(UIR1_RXD0F) { // any data received via EP0? if(USR0_SETUP) { // SETUP packet? do_setup(); // go to setup } else { // or a normal packet ep0_rxd(); // receive it } }
サンプル・プロジェクトの場合にも到着したトランザクションの種類によって処理が分かれているのがわかります。
SETUP ステージで次のデータの送受信方向が予測できるので、来るべき DATA ステージに備える事ができます。 DATA ステージでホストにデータを送信する必要がある場合には、 SETUP ステージの処理の中でバッファにデータを準備する必要があります。
最後の STATUS ステージでは、送受信されるデータがありません。 そのため、ハードウェアが適切(適当?)に処理を行います。
残る二つのトランスファ、インタラプトとバルクは、 DATA ステージだけで構成されます。 ホストが開始するトランザクションに対して単一方向にデータを送受信します。 サンプル・プロジェクトでは、エンド・ポイント1がインタラプト・トランスファとして実装されています。
if(UIR1_TXD1F) { // has EP1 sent Data? ep1_txd(); }
処理の内容は、要求に応じてデータをバッファに入れているだけです。
SETUPトランザクションでの処理
SETUP トランザクションでは、8バイトのデータが送信されてきます。 サンプル・プロジェクトでは、setup_bufferという構造体で表現されます。 SETUP トランザクションでの処理は、これから始まる一連のコントロール・トランスファがどんな種類のトランスファであるかを判断することです。 そのためのパラメータが、bmRequestTypeとbRequestの二つのフィールドに入っています。
typedef struct { byte bmRequestType; // Characteristics (Direction,Type,Recipient) byte bRequest; // Standard Request Code iword wValue; // Value Field iword wIndex; // Index or Offset Field iword wLength; // Number of Bytes to transfer (Data Stage) } setup_buffer;
bmRequestTypeの5,6ビット目には、USBとして備えるべき機能であるか、HIDとして備えるべき機能であるかという大分類情報が書かれています。 また、bRequestを検査することによって、トランスファの詳細を特定することが出来ます。
対応 | bmRequestType[6:5] | bRequest | 種別 |
---|---|---|---|
YES | 00 | 0x01 | CLEAR_FEATURE |
YES | 00 | 0x05 | SET_ADDRESS |
YES | 00 | 0x06 | GET_DESCRIPTOR |
YES | 00 | 0x09 | SET_CONFIGURATION |
NO | 01 | 0x01 | GET_REPORT |
YES | 01 | 0x09 | SET_REPORT |
HIDでは、必須とされているGET_REPORTは、実装されていません。 コメントに寄せられた情報によると、Windowsでは、インターフェース・デスクリプタにインタラプト・トランスファが宣言されていたら、GET_REPORTは、発行されないのだそうです。 まあ、今の所は、Windows専用ということにしておきましょう。
トランスファの種類によって処理を分ける部分の記述は、以下の通りです。 未知のトランスファに出会ったら、STALL宣言をすることによって、受け付けられなかったことを示します。
if((SetupBuffer.bmRequestType & 0x60) == 0x00) { // Standard Request Decoder: switch(SetupBuffer.bRequest) { case CLEAR_FEATURE: // 1 clearFeature(); break; case SET_ADDRESS: // 5 setAddress(); break; case GET_DESCRIPTOR: // 6 getDescriptor(); break; case SET_CONFIGURATION: // 9 setConfiguration(); break; default: forceSTALL(); break; } // endswitch } else if((SetupBuffer.bmRequestType & 0x60) == 0x20) { // Class request switch(SetupBuffer.bRequest) { case SET_REPORT: // 9 setClassReport(); break; default: forceSTALL(); break; } // endswitch } else { forceSTALL(); // Unknown request }
またまた、長くなったので続く…
付録 : 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
- メディア: 単行本
お言葉に甘えて、容赦なく訂正します。
USB通信の基本構造は、
トランスファ ー トランザクション ー パケット
という階層構造となります。つまり、
ー パケットが集まってトランザクション
ー トランザクションが集まってトランスファ
>MC908JB16では、パケットの処理部分は、ハードウェアで行われ、ファームウェアで関与することはまずありません。
MC908JB16に限らず、通常MCU上のUSBエンジン(SIE:Serial Interface Engine)はパケットのみならず、トランザクションのレベルまでを扱います。
>トランスファには、三つの種類があり、コントロール、インタラプト、バルクと呼ばれています。
アイソクロナスを含めて4つです。
>USBの基本:ステージ (Stage)
ステージは、コントロール転送(Control Transfer)でのみ適用されます。
インタラプト、バルク、アイソクロナス転送にはステージはありません。
えーと、直すのが面倒になってきたので、この辺で(おいおい)。後は細かいことなので。
入門書は大抵このあたりから説き起こすわけで、またそれを読む人もこういう構造的な部分から入ろうとします。が、実はこれがUSBでは結構面倒な部分で、多くの人がここですでに挫折するようです。しかし、実際には私のようなマニアックな人間以外はあまり用がない概念です。
この中で最も重要なのは、バルクとインターラプトにおける「トランスファ」を具体的に理解することです。あとは、エニュメレーションやクラス特有のリクエストを実装しようという人は、「コントロール転送でそれぞれのステージが何をやっているか」を理解しておけば良いでしょう。他はまあ、こんな用語もあったなぐらいで良いかと。
と言ったからには、何か書かなきゃ、ですね。後で投稿しますね。
Tsuneo
by Tsuneo (2008-03-15 21:21)
ようこそ、いらっしゃいました。
ありがたく、訂正をお受けします。
実は、資料を読んでもステージという階層があるのか無いのかピンとこなかったのです。ステージは、階層構造に入れちゃいけなかったのですね。
アイソクロナスは、MC908JB16の仕様書に出てこない概念なので、あえて入れていません。
ファームウェアで扱う単位がトランザクションに相当しているとはっきり理解できたのも、ごく最近のことでして、トランザクションの中身やパケットの構造が延々と書いてあるマイコンの仕様書に翻弄された感があります。
最近は、よっぽど大量のデータを送受信するのでなければ、コントロール転送だけで十分なのではないかと考えています。RS-232Cの1200baudで十分だった世代ですね。
マイコン側のファームウェアは、簡単に手を抜くことが出来るのに、PC側のプログラムは、あれやこれや利害関係を解決しなくてはならないので面倒です。もっと簡単にならないですかね。
by noritan (2008-03-15 22:54)
>最近は、よっぽど大量のデータを送受信するのでなければ、コントロール転送だけで十分なのではないかと考えています。
うーん、それはどうかと。
MC908JB16はロースピードUSBデバイスで、バルクが使えないのでかなり不満です。このスピードの通信なら、USB-シリアルケーブルをUARTでつないだ方が、ファームウェアもホストアプリもよほど簡単ですから。
>RS-232Cの1200baudで十分だった世代ですね。
私も「究極の」8-bit、MC6809が懐かしい世代です。富士通のFM-8やトラ技別冊のFORTH特集をありありと連想してしまうわけで。
>PC側のプログラムは、あれやこれや利害関係を解決しなくてはならないので面倒です。もっと簡単にならないですかね。
Setup-API まわりとか面倒ですが定型的なので、私自身は昔書いたコードをコピペでちょいちょいと直してお茶をにごしています。本格的に書くのはカリカリにスピードチューニングする必要がある時ぐらいですね。
ホストアプリのサンプルもJ.Axelsonの USB Central(http://www.lvr.com/usb.htm)を始めとして結構ありますし。また、使用しているMCUのメーカーのサンプルで物足りなければ、他のメーカーのサンプルもちょっといじってやればすぐ使えます。例えば、CypressのCyUSBデバイスドライバは、INFファイルのVID/PIDを合わせ込めば、どこのメーカーのUSB MCUでもつながります。もちろんEZ-USB特有のファームウェアダウンロード機能は駄目ですが、コントロール、インターラプト、バルク、アイソクロナスの通常の通信は問題なく使えます。で、上位のCyAPIとかSuiteUSB.NET がそのまま使えるわけです。こういった流用は、商用にするのでなければ許される範囲でしょう。(ネットに出さなければ判らない :-))メーカーは面白く無いでしょうが。
"USB Developer's uStudio" on Cypress
http://download.cypress.com.edgesuite.net/design_resources/reference_designs/contents/cy4604___usb_developer_s_ustudio_19.exe
"SuiteUSB.NET 2.0 - USB Development tools for Visual Studio 2005 and .NET 2.0/3.0 (2)" on Cypress
http://download.cypress.com.edgesuite.net/design_resources/reference_designs/contents/suiteusb_net_2_0___usb_development_tools_for_visual_studio_2005_and__net_2_0_3_0_19.msi
Tsuneo
by Tsuneo (2008-03-16 00:19)
では、インターラプトとバルクにおける「トランスファ」について
なお、用語については重要なもの以外はわざといいかげんに書いてますのであまりつっこまないように(遊び心です)。
まず、インターラプト転送とバルク転送というのはほとんど同じものです。MCUのファームウェア上でも、PCの(上位)デバイスドライバ、ホストアプリ上でも全く同じ手順(レジスタ、関数)を用いて扱います。従って、どちらかについて習熟すれば、他方もすでに手の内にあるわけです。お得ですね。
では、違いは何かというと、インターラプト転送は定期的に行われるのに対し、バルク転送は余った時間を利用して行われるというスケジュールの違いです。USB (Universal Serial Bus)はその名の通りバスですから、多くのデバイスが1本のバスにぶらさがります。ホストPCは時間を細かく区切ってこれらのデバイスと通信します。そのためにはスケジュールが必要ですね。
インターラプト転送では、エニュメレーションの時にこのスケジュールに定期的な予約を入れます。デバイスは、エンドポイント・デスクリプタの bInterval フィールドを使ってホストに予約を入れるわけです。
HID のIN方向のインターラプト転送はこの予約をフルに使って、デバイスドライバが常時定期的な転送を行います。
HID のOUT方向のインターラプト転送は、ホストアプリが WriteFile などで呼び出したときにのみ予約を行使します。
使っても使わなくてもスケジュールは確保されているわけです。
また逆に、転送はすべてスケジュールに縛られます。だから再送の場合もすぐにはできずに定時を待つわけですね。
なお、USBでINとかOUTとか呼ぶときには、常にホスト中心です。
ー ホストに入るのが IN
ー ホストから出るのが OUT
インターラプト転送の他に、アイソクロナス転送もエニュメレーション時にスケジュールに定期的な予約を入れます。
コントロール転送用に常に10%のスケジュールが予約されます(フルスピード・ロースピードの場合)。
こうして残ったスケジュールがバルク転送に割り当てられます。
と書いてくると、バルク転送は残りかすのような印象を受けるかもしれませんが、そこは残り物に福があり、大抵の場合スケジュールはすかすかですから、普通はバルク転送が最も多くのスケジュールを確保できます。またバルク転送には各エンドポイントで転送が必要な時にのみ動的にスケジュールが割り当てられます。つまり、たくさんのバルクエンドポイントがバスに接続していても、実際に転送を行っているエンドポイントにのみ、均等にスケジュールが割り当てられるわけです。
うーん、なかなか本題のトランスファにたどり着きませんね。
このスケジューリングは転送速度のチューニングに必要なので、まあ我慢してやって下さい。
また後で。
Tsuneo
by Tsuneo (2008-03-16 00:33)
では、インターラプトとバルクにおける「トランスファ」の続き
ようやく本題に入ります。
繰り返しますが、USB通信の基本構造は、
トランスファ ー トランザクション ー パケット
という階層構造となります。つまり、
- パケットが集まってトランザクション
- トランザクションが集まってトランスファ
ここで、
- PCのホストアプリで扱うのはトランスファ内のデータの部分、
- MCUのファームウェアで扱うのはパケット内のデータの部分だけです。
というわけで、以下の記述では、いさぎよく
- トランスファといえばそのデータ部分、
- パケットといえば、パケット内のデータの部分
- トランザクションについては忘れる
ということで進めてしまいます。
ホストアプリが WriteFile や ReadFile でやりとりしているのがトランスファです。
WriteFile が送り出したデータは、デバイスドライバの中でパケットに分割されます。分割の単位は、OUTエンドポイント・デスクリプタの wMaxPacketSize フィールドでデバイスが指定します。MC908JB16はロースピードデバイスなので、普通は wMaxPacketSize を8バイト目一杯に設定しますね。こうやって分割されたパケットをホストコントローラが順次デバイスに送り出します。デバイスのファームウェアはOUTエンドポイントでこれらのパケットを受け、つなげてトランスファを再構成するわけです。
例えば、30バイト のトランスファは、wMaxPacketSize = 8 バイト だと
8 + 8 + 8 + 6
と4つのパケットに分割されます。最期のパケットだけ短くなります(ショートパケット)。
ちょっと待って、いつもHIDで1度に8バイトまでしか送ってないよ!
と思われるかもしれませんが、実はHIDでも一度の WriteFile や ReadFileで8バイト以上のデータが受け渡しできます。レポートデスクリプタで ReportSize( ビット ), ReportCount( バイト ) を使って100バイトでも200バイトでも大きなサイズのレポートを指定して下さい。HIDでは4kバイトまでにしておいた方が無難だと思いますが (PCIバスのページサイズ)。
もちろんこういった大きなデータは、デバイス側にはパケットに分割されて届きますから、ファームウェアは大きなバッファを別に用意してその上でパケットをつなげてトランスファに戻すか、頭から処理できるなら届いた端からどんどん処理してしまいます。ホストの送り出しスピードが速くてファームウェアの処理が間に合わない場合は、以前のコメントで触れたように、エンドポイントバッファからパケットを読み出さないで残しておけば、ホストはバッファが空くまで再送を繰り返して待っています(NAKing)。
さて、逆方向の ReadFile では WriteFile とちょうど反対の操作が行われます。
ファームウェアはトランスファをパケットに分割してINエンドポイントからホストに送り出します。もちろん分割の単位はそのエンドポイントの wMaxPacketSize ですね。ホストPCではデバイスドライバがパケットをつなげてトランスファを再構成し、ReadFile に渡します。
IN方向でも、ホストの読み出しスピードが速すぎてデータが用意できない場合は、エンドポイントバッファを空にしておけばホストを待たせることができます(NAKing)。
以上がインターラプトとバルクの「トランスファ」です。
なんだ、たいしたことないじゃん、
と思って頂ければしめたものです。
あとは、ゼロ・レングス・パケット(ZLP, Zero-Length Packet)を説明すれば「トランスファ」は終了ですね。
Tsuneo
by Tsuneo (2008-03-16 06:45)
ふむふむ、全然たいしたこと無いじゃん。と、思ってしまいました。
そうすると、ホストが、インタラプトINエンド・ポイントからデータを受け取る時には、結局コントロール転送の出る幕なしということになりますよね。じゃあ、「僕はHIDデバイスだよ」と宣言しておいても、もちろん、レポート・サイズが固定であるという制約は受けるでしょうが、インタラプト・トランスファで好き勝手が出来るということですか。
HID宣言をしておいて、インタラプトOUTエンド・ポイントを装備することも出来ちゃうのかな?
bIntervalで予約するのは、トランスファの間隔でしょうか、トランザクションの間隔でしょうか?もし、トランスファ間隔の時間を示しているのだとすれば、バンド幅を上げるためにレポートサイズを大きくすると効果があるはずです。
と、考えてUSBの仕様書を探すと "Interval for polling endpoint for data transfers" とありました。つまり、トランスファ間隔を示すから、「レポートサイズを大きくする効果あり」ですよね。
by noritan (2008-03-16 11:59)
>ふむふむ、全然たいしたこと無いじゃん。と、思ってしまいました。
やった!ねらい通りですね。
サンプルもろくになかった昔はともかくとして、実は今時のUSBなんて簡単なんですよ。
既存のサンプルをいじり倒すことを前提に、ファームウェアとホストアプリから見えている範囲に話の筋を付けるための多少のバックグランドを混ぜるとこんなもんです。
で、もう少しこの調子でやっていいですか?
>そうすると、ホストが、インタラプトINエンド・ポイントからデータを受け取る時には、結局コントロール転送の出る幕なしということになりますよね。
HID ではレポートの種類により、それぞれ流すパイプが定められています。
Inputレポート:Get_Report( Input )、インターラプトINエンドポイント
Outputレポート:Set_Report( Output )、インターラプトOUTエンドポイント
Featureレポート:Get_Report( Feature )、Set_Report( Feature )
- Get_Report、Set_Reportはコントロール転送によるクラスリクエスト
Inputレポートは、Get_ReportクラスリクエストとインターラプトINエンドポイントの双方で流せますが、
- 転送速度
- ファームウェアでの処理の重さ
を考慮するとインターラプトINエンドポイントに軍配があがります。
>レポート・サイズが固定であるという制約は受けるでしょうが、
ReadFile や WriteFile はいわば生のレポートをやりとりしています。
ホストアプリとファームウェアで、レポートのフォーマットが統一されていれば、先頭からデータを詰め込んで、余った部分は適当なパディングでレポート長をそろえておけば良いわけです。というわけで、汎用性をねらわないかぎりレポートのフィールドを細かく指定する必要はありません。
また、レポートIDを使って同じ種類の複数のレポートをレポートデスクリプタで定義できます。
例えば
- レポートID 1:8バイトのInputレポート - ステータス用
- レポートID 2:100バイトのInputレポート - データ用
>bIntervalで予約するのは、トランスファの間隔でしょうか、トランザクションの間隔でしょうか?
実はトランザクションの間隔です。上記の簡略化した解説に沿えば、パケットの間隔ということになります。
うーむ、上記の解説は、そのあたりが今ひとつの出来でしたね。
Tsuneo
by Tsuneo (2008-03-16 15:45)
そうですか、bIntervalは、トランザクションの間隔でしたか。
これは、つまり、USBの仕様書においても、トランスファとトランザクションとパケットの明確な区別が付いていない、混沌状態と考えたほうが良いのでしょうか。仕様書ぐらいはちゃんと統一して欲しかった。
大量のデータを送受信するのでなければ、UARTの方が扱いやすい。HIDを使っている限りは、大量のデータの送受信は難しい。HIDを使わないとまっとうなデバイス・ドライバを提供しなくちゃいけない。八方ふさかり?
それでも、「USBケーブルだけで全て完結する」という「ハードウェアの手軽さ」は捨てがたいな。
by noritan (2008-03-16 16:02)
>USBの仕様書においても、トランスファとトランザクションとパケットの明確な区別が付いていない、混沌状態と考えたほうが良いのでしょうか。
その通りです。改訂でだいぶ良くなってますが。
>八方ふさかり?
この石のターゲットが、カスタムキーボードとかトラックボールとか入力用のガジェットだからでしょう。
フルスピード、ハイスピードとUSB MCUは色々ありますよ。どんなMCUでも、MCU上のUSB エンジンにはそれほどの差はありません。USBスペックが細かく規定してますから。
この石でしっかりUSBの取っかかりをつかんで、他の石にも手を出して下さい。:-)
この石は、TXとかRXとかレジスタの名称はちょっとあれですが、全体に素直な作りで良いと思います。
また、ホストアプリの方は、MCU が替わってもエンドポイントのアドレスを直す程度でそのまま使えます。
なにせUSBはスタンダードですから、どんな石にもつながって当たり前です。
>HID宣言をしておいて、インタラプトOUTエンド・ポイントを装備することも出来ちゃうのかな?
HID インターフェースは、IN/OUT 各一個ずつのエンドポイントを持つことができます
- INエンドポイント : 1 (必須)
- OUTエンドポイント : 1 (オプション)
まず、デスクリプタの加工から。
コンフィギュレーション・デスクリプタ・セットの宣言に、OUTエンドポイント・デスクリプタを加えます。
const struct {
const configuration_descriptor ConfigDesc1;
const interface_descriptor InterfaceDesc1;
const endpoint_descriptor Endpoint1Desc1;
const endpoint_descriptor Endpoint1Desc2; // <------------
const HID_descriptor HIDDesc1;
} configdescriptors = {
今気がついたんですが、HIDデスクリプタの位置が古い仕様になってますね。まあ、HIDのバージョンが1.01 (現在は1.10)だから、ま、いいか。
コンフィギュレーション・デスクリプタで、セット長のエンドポイント・デスクリプタのサイズを2倍にします。(wTotalLength フィールド)
//----------------------------------------------------------------------------
{ // Size of this Descriptor in Bytes
sizeof(configuration_descriptor),
DT_CONFIGURATION, // Descriptor Type (=2)
{sizeof(configuration_descriptor) + sizeof(interface_descriptor) +
sizeof(endpoint_descriptor) * 2 + sizeof(HID_descriptor), // <------------
0x00}, // Total Length of Data for this Conf
1, // No of Interfaces supported by this Conf
1, // Designator Value for *this* Configuration
0, // Index of String Desc for this Conf
0xc0, // Self-powered, no Remote-Wakeup
1 // Max. Power Consumption in this Conf (*2mA)
}, // end of ConfigDesc
インターフェース・デスクリプタで、エンドポイントの数を増やします(bNumEndpoints フィールド)。
{ // Size of this Descriptor in Bytes
sizeof(interface_descriptor),
DT_INTERFACE, // Descriptor Type (=4)
0, // Number of *this* Interface (0..)
0, // Alternative for this Interface (if any)
2, // No of EPs used by this IF (excl. EP0) // <------------
0x3, // IF Class Code (0x03 - HID)
0x01, // Interface Subclass Code
0x1, // IF Protocol Code (0x01 = Keyboard)
0 // Index of String Desc for this Interface
}, // end of InterfaceDesc
INエンドポイント・デスクリプタの直後にOUTエンドポイント・デスクリプタを加えます。
INエンドポイント・デスクリプタをコピペして、アドレスをOUTの2にします。(bEndpointAddress フィールド)
{ // Size of this Descriptor in Bytes
sizeof(endpoint_descriptor),
DT_ENDPOINT, // Descriptor Type (=5)
0x81, // Endpoint Address (EP1, IN)
0x03, // Interrupt transfer
{0x08, 0x00}, // Max. Endpoint Packet Size
10 // Polling Interval (Interrupt) in ms
}, // end of Endpoint1Desc
//----------------------------------------------------------------------------
{ // Size of this Descriptor in Bytes
sizeof(endpoint_descriptor),
DT_ENDPOINT, // Descriptor Type (=5)
0x02, // Endpoint Address (EP2, OUT) // <------------
0x03, // Interrupt transfer
{0x08, 0x00}, // Max. Endpoint Packet Size
10 // Polling Interval (Interrupt) in ms
}, // end of Endpoint1Desc
デスクリプタの加工はこれでOKでしょう。
あとは、Set_Configuration()での OUTエンドポイントのセットアップなどですね。
ちょっとコード全体を見渡してみます。
ところで、この掲示板はコメント内でHTMLタグとか使えるんでしょうか
Tsuneo
by Tsuneo (2008-03-16 17:28)
今まで、どのサンプル・プログラムを見てもエンドポイント0,1を使った例しかなかったので、HIDは、こういうものだと思っていたのですが、エンドポイント2をINとして使うこともできるのですね。
> INエンドポイント : 1 (必須)
ということですが、GET_REPORTで代用することは出来ないのでしょうか?HID仕様書の 6.2.2.5 Input, Output, and Feature Items には、
* Input items define input reports accessible via the Control pipe with a Get_Report (Input) request.
* Input type reports are also sent at the polling rate via the Interrupt In pipe.
と書いてあるから、両方とも発行されるのが本当なのかな?
このBLOGのコメント欄には、HTMLタグはおろか、pre-format書式も使えないので、プログラムを書くのにさえ難儀します。私が、hamayanさんの隣に活動拠点を構えた理由の一つが、コメント欄には事実上コードがかけないことが判ったからです。
Tsuneoさんも、隣に越していらっしゃいませんか?
by noritan (2008-03-16 17:54)
>GET_REPORTで代用することは出来ないのでしょうか?
HIDのスペックはかなり微妙な書き方をしていますが、以下の引用を総合すると、
- インターラプトINエンドポイント ー 必須
- Get_Report リクエスト ー 必須
- Inputレポートを繰返すのは、インターラプトINエンドポイントからとする。
- Get_Reportからも、ホストが要求すれば同じフォーマットのInputレポートを返す。
ということになりますね。
基本的には、インターラプトINエンドポイントを使えと。
Device Class Definition for HID 1.11
http://www.usb.org/developers/devclass_docs/HID1_11.pdf
4.4 Interfaces (HID1_11.pdf p10)
A HID class device communicates with the HID class driver using either the
Control (default) pipe or an Interrupt pipe.
...
The Interrupt Out pipe is optional.
Pipe: Interrupt In
Required: Y
7.2.1 Get_Report Request (HID1_11.pdf p51)
- This request is mandatory and must be supported by all devices
...
- The Interrupt In pipe should be used for recurring Input reports. The Input report reply has the same format as the reports from Interrupt pipe.
>Tsuneoさんも、隣に越していらっしゃいませんか?
個人的にはブログも持ってないんです。なまけものなので3日と続きそうにない。
大抵このあたりのボードに顔出ししてます。
SiLabs Forum: http://www.cygnal.org/scripts/Ultimate.cgi?action=intro
USB-IF: https://www.usb.org/phpbb/viewforum.php?f=1&topicdays=0&start=0
8052.com: http://www.8052.com/forum/index.phtml?top=
LPC 2000: http://tech.groups.yahoo.com/group/lpc2000/messages
KEIL Forum: http://www.keil.com/forum/threads.asp
こちらで遊んでいたら、SiLabsのUSB Forum が コメント0のオンパレードになってますね。
Help も来てるし。救援にいかなくちゃ。
Tsuneo
by Tsuneo (2008-03-16 19:07)
OUTエンドポイント追加の続きです。
たぶんこれでいけると思いますが、やってみてください。
// SET_CONFIGURATION Standard Device Request Handler
// called by handleSETUP();
//
void setConfiguration() {
if( SetupBuffer.wIndex.hi ||
SetupBuffer.wIndex.lo ||
SetupBuffer.wLength.hi ||
SetupBuffer.wLength.lo ||
SetupBuffer.wValue.hi ||
(SetupBuffer.wValue.lo > 1) ||
(USB_State == US_DEFAULT)) {
forceSTALL();
}
else {
if(SetupBuffer.wValue.lo > 0) {
// no need to remember the Configuration Value
// since we support only one Configuration anyway
USB_State = US_CONFIGURED;
// Activate Interrupt Endpoints, reset STALL and DATA-Toggle
UCR1=0; // EP1 Tx Enable, Data Size is 0
UCR1_TX1E=1;
UCR2=0; // EP2 Rx Enable <--------------
UCR2_RX2E=1; // <--------------
}
else {
// Zero means: go back to Adressed State
USB_State = US_ADDRESSED;
UCR1=0; // deactivate EP1
UCR2=0; // deactivate EP2 <--------------
}
// prepare to send empty DATA1 at next IN Transaction
UCR0=0;
UCR0_T0SEQ=1;
UCR0_TX0E=1;
}
}
/***********************************************************
Endpoint 2 receive data service (readout receive buffer)
***********************************************************/
volatile char outBuffer[ 8 ];
void ep2_rxd() // <--------------
{
byte n;
byte volatile *src;
UCR2_RX2E=0; // Deactivate EP2 Receiver
UIR2_RXD2FR=1; // Reset EP2 Receive Flag
// unload EP2 Rx to buffer
src = &UE2D0;
for(n=0;n<USR1_RP2SIZ;n++)
outBuffer[n] = *src++;
UCR2_RX2E=1;
}
// USB Interrupt Handler
// All Interrupt Sources of the JB16's integrated USB peripheral
// will be treated by this ISR
void interrupt VectorNumber_USB isrUSB() {
if(UIR1_EOPF) { // End of Packet detected?
SuspendCounter = 0; // reset 3ms-Suspend Counter
UIR2_EOPFR=1; // reset EOP Intr Flag
}
if(UIR1_RXD0F) { // any data received via EP0?
if(USR0_SETUP) { // SETUP packet?
do_setup(); // go to setup
} else { // or a normal packet
ep0_rxd(); // receive it
}
}
if(UIR1_TXD0F) { // has EP0 sent Data?
ep0_txd();
}
if(UIR1_TXD1F) { // has EP1 sent Data?
ep1_txd();
int_flg = 1;
}
if(UIR1_RXD2F) { // any data received via EP2? <--------------
ep2_rxd();
// int_flg = 1; // if required, add another flag
}
if(UIR1_RSTF) { // USB Reset Signal State detected?
initUSB(); // Soft Reset of USB Systems
UCR3_ENABLE1=1; // Enable EP1
UCR3_ENABLE2=1; // Enable EP2 <--------------
UIR0 = 0x9B; // INT enabled for: end of packet <--------------
// EP2 RX
// EP0 Rx/Tx Intr Enable and
// EP1 Tx
UCR0_RX0E=1; // EP0 Receive Enable
USB_State = US_DEFAULT; // Device is powered and reset
}
}
Tsuneo
by Tsuneo (2008-03-16 21:13)
OUTエンドポイント追加の訂正です。
このサンプルで、OUTエンドポイント2(RX)を実装していますね。
"USB08 Firmware V1.01 for Metrowerks Codewarrior - Revision A"
http://hc08web.de/usb08/files/usb08_fwv101_metrowerks_reva.zip
これを見ると、上記のコードではエンドポイントHALT周りが足りないのと、レジスタが間違っている部分がありますね。
新たにインターラプトあるいはバルク・エンドポイントを加えるとき(または、はずすとき)には、以下の部分について下位USBサポートルーチンと標準デバイスリクエストハンドラに手を加える必要があります。
1)下位USBサポートルーチン
a) bus-reset ハンドラ
USBエンジンのこのエンドポイント関係のすべての設定をディぜーブルする
- エネーブルビット(TX1EまたはTX2EまたはRX2E)
- STALL(STALL1またはSTALL2)ビット - 0
- データトグル(T1SEQまたはT2SEQビット)- 0
- ハードウェアインターラプト設定
など
b) Set_Configuration ハンドラ
デバイスがコンフィギュレーションされたときに(wValueが1以上)、
- USBエンジンでエンドポイントをエネーブルにする。
- ファームウェア上のUSB通信に関連するフラグ、変数、バッファなどをすべてイニシャライズする。
- ハードウェアインターラプトでINエンドポイントを扱う場合はダミーパケットをロードする。
デバイスがコンフィギュレーションからはずれたときに(wValueが0)
- USBエンジンでエンドポイントをディぜーブルにする。
c) USBハードウェアインターラプトハンドラ( isrUSB() )
ハードウェアインターラプトでこのエンドポイントを扱う場合は、
このエンドポイントのインターラプトフラグを見てハンドラにとぶディスパッチャを実装する
2)エンドポイントHALTのサポート
a) Set_Feature ハンドラ
Set_Feature( ENDPOINT_HALT )で、このエンドポイントが指定されたときに
- ファームウェア上のHALTステータスをTRUEにする
- USBエンジンのこのエンドポイントのSTALL(STALL1またはSTALL2)ビットをTRUEにする
b) Clear_Feature ハンドラ
Clear_Feature( ENDPOINT_HALT )で、このエンドポイントが指定されたときに
ファームウェア上で支障がなければ、
- ファームウェア上のHALTステータスをFALSEにする
- USBエンジンのこのエンドポイントのSTALLビットをFALSEにする
c) Get_Status ハンドラ
Get_Status( ENDPOINT )で、このエンドポイントが指定されたときに
現在のこのエンドポイントのHALTステータスを返す
d) Set_Configuration ハンドラ
デバイスがコンフィギュレーションされたときに、
すべてのインターラプト、バルク・エンドポイントの
- ファームウェア上のHALTステータスをFALSEにする
- USBエンジンのSTALLビットをFALSEにする
e) Set_Interface ハンドラ (もしalternate インターフェースがあれば)
このインターフェース上のインターラプト、バルク・エンドポイントの
- ファームウェア上のHALTステータスをFALSEにする
- USBエンジンのSTALLビットをFALSEにする
3)データトグルのサポート
a) データトグルを自動的にサポートしないUSBエンジンでは(MC68HC908JB16はこのタイプ)
- INエンドポイント・ハンドラ(Tx)の中で、データトグル(T1SEQまたはT2SEQビット)を反転する
- OUTエンドポイント・ハンドラ(Rx)の中で、データトグル(R2SEQビット)をチェックする
b) Set_Configuration、Set_Interface ハンドラ
デバイスもしくはインターフェースがコンフィギュレーションされたときに、
USBエンジンのデータトグル(T1SEQまたはT2SEQビット)を0 (DATA0)にする
c) Clear_Feature ハンドラ
Clear_Feature( ENDPOINT_HALT )で、このエンドポイントが指定されたときに
USBエンジンのデータトグル(T1SEQまたはT2SEQビット)を0 (DATA0)にする
このリストと上記サンプルを参考にOUTエンドポイント2を実装してみてください。
Tsuneo
by Tsuneo (2008-03-17 20:04)
わざわざ、コードまで書いていただいて、お手数をかけます。
まだ、チンプンカンプンなので、「がんばります」としか、言いようがないです。
by noritan (2008-03-18 08:25)
そうですね、いきなりいわば中級編に話がとんじゃいましたからね。
この元記事に書いてらっしゃるコントロール転送と標準デバイスリクエストからエンドポイントの追加までをつなぐ背景について書いてみましょう。
a) バスリセットとSet_Configuration
USBではデバイスの状態をデバイスステート(デフォルト、アドレス、コンフィギュレーションなど)として定義しています。これらの状態は、バスリセット、Set_AddressおよびSet_Configurationリクエストで移り変わります。
すべての状態から
|
|-- バスリセット
|
デフォルト・ステート
|
|-- Set_Address
|
アドレス・ステート
|
|-- Set_Configuration
|
コンフィギュレーション・ステート
デバイスをコンフィギュレーション・ステートにしないとホストはそのデバイスの機能を使うことができません。これを行うのがエニュメレーションの主な目的です。実はホストがバスリセット、Set_Address、Set_Configurationさえ発行すれば、エニュメレーションが終わってしまうんですね。OTG (USB On-The-Go)デバイスなどはこの簡略化したエニュメレーションを行います。
バスリセット
バスリセットは、パケットなどと同じく、USBバス上の信号です(D+、D-ラインともに0)。この信号を受けるとMCUのUSBエンジンは(設定してあれば)ハードウェア・インターラプトを発行し、ファームウェアに通知します。ファームウェアは、デバイスをデフォルト・ステートにします。つまり、
- USBエンジンのパラメータをすべてイニシャライズする。
- デフォルトエンドポイント(エンドポイント0)のみ生かしておく。
- それ以外のすべてのエンドポイントはディぜーブルし、ハードウェア・インターラプトも落とす。
Set_Configurationリクエスト
デバイスをコンフィギュレーション・ステートにします。
USBエンジンに対してファームウェアは、デスクリプタに指定した設定通りに、デフォルトエンドポイント以外のエンドポイントもエネーブルにし、まっさらの状態にイニシャライズします。
- ファームウェア上のUSB通信に関連するフラグ、変数、バッファなどをすべてイニシャライズする。
これをやっておかないと、PCがスタンバイ(スリープ)から戻った時やリブートした時に問題が起こります。
- ハードウェアインターラプトでINエンドポイントを扱う場合はダミーパケットをロードする。
INエンドポイントは、ホストにバッファを転送してバッファが空になったときにインターラプトを発行します。だから、ダミーのデータをこの段階で入れておかないと、一発目のインターラプトがいつまでたっても出ません。
b) HALTとSTALL
まずインターラプトとバルクエンドポイントで、HALTとはエンドポイントの状態を指します。そして、STALLとは、HALT状態のエンドポイントがホストからアクセスされたときに返す信号です。
USBのコンプライアンステスト(USBCV)を通すには全てのインターラプトとバルクエンドポイントでHALTをサポートする必要があります。しかし、実際に使っているのはマスストレージクラスくらいです。
HALTとSTALLは主に次のようなコンテクストで使用されます。
1) デバイスがエラーなどのため、あるエンドポイントをHALTにする。
2) ホストアプリがこのエンドポイントにアクセスして、STALLを受け取る。
3) ホストアプリはクラスリクエストやベンダーリクエストでデバイスと通信し、エラーの原因を解析し、何とかエラーの原因を解除する。
4) ホストアプリはパイプリセットをこのエンドポイントの属するパイプに送る
5) デバイスドライバは、パイプリセットを受けて、
- Get_Status( ENDPOINT ) でHALTを確認し、
- Clear_Feature( ENDPOINT_HALT ) でHALTを解除しようとし、
- Get_Status( ENDPOINT ) でHALTが解除されたことを確認する。
6) 通常の通信が復活する。
どうでしょうか。多少は埋まったでしょうか。
Tsuneo
by Tsuneo (2008-03-19 21:21)