USBドライバの開発
参考
仕様書
xHCI
PCI
参考実装
memo
- デバイスのMSIレジスタに割り込み発生時に書き込むデータと書き込み先のアドレスを設定する。
- 現代のx86では割り込みはAPIC(Adcanced Programmable Interrupt Controller)が処理する。
- 流れとしては、割り込みが発生したときに実行する処理に関する情報をIDT(Interrupt Descriptor Table)に保存しておき、Messageデータレジスタのベクタ番号でそれを指定する。
- xHCIでは3つのリングバッファでやり取りする。interfaceの定義なのか?いずれもTRBと呼ばれる16バイト固定のデータ構造になっている。
- xHCはリングバッファの先頭アドレスの情報しか持っていない。そのためソフトウェア側でリングバッファの終端を表すLink TRBを配置する必要がある。xHCはこれを読んだら先頭から読むという実装になっている。
- 4章からはバスドライバの話。
疑問点
- PCIコンフィグレーション空間のレジスタというのは本当にレジスタなのか。それともそうやって読んでいるだけなのか。
- USBDIはバスドライバが勝手に決めてよいのか?
- HCを初期化するのはバスドライバなのに、その下がHC driverなん?
PCIバスを探索してxHCIを見つける
- PCIデバイスはそれぞれPCIコンフィギュレーション空間を持っている。
疑問点
- PCI-to-PCI bridgeの判断をscanFuncの中でやっていたのが気になった。PCI-to-PCI bridgeって、デバイスじゃなくて機能なのか?scanDevの中でデバイスのfunciton0のclassIdを調べて判断するのじゃだめなのか?
- makefileを書くのが面倒。相互参照が置きすぎて無駄に冗長に書いている気がする。もっといい書き方はないものか...
MSIの設定
-
割り込みを取得するためにはMSI capability registerを設定する必要がある。
→ how? -
Local APIC IDはコアごとに存在する。これを取得するには0xFEE00020にあるLocal APIC レジスタの値を読めばよい。そうすると、参照するコードが実行されたコアのLocal APIC IDが取得できる。
-
これで割り込みが取得できるようになる。割り込みが起きた際に実行される処理は割り込み要因番号を用いてIDT(Interrupt Descriptor Table)から選択される。
-
MSI割り込みはPCIデバイスからCPUに通知する仕組みの事か?
-
MikanOSの実装読む必要がありそう...
-
とりあえず流れとしては、MSIを設定することでPCIデバイスからCPUにイベントの発生を通知できるようにする。そして割り込みハンドラをIDTにセットすることで割り込みが発生した際に特定の処理が実行されるようにする。
-
これ、動作確認がムズイな。MSIとIDT設定して、マウス動かしたときに文字列が表示されるようにするか?
-
とりあえず4章の流れに沿ってやればxHCを初期化することはできそう。
→ここで出てきているレジスタ群にどうやってアクセスするんだろう? -
capability listはMSIの設定に関する内容か?
疑問点
- MSI capability registerはどこにあるん?
- 割り込みって種類があるはずだけど、いくつか設定する必要があるってこと?
- これはxHCの初期化処理とは違うん?
- 4章までの内容がxHCIドライバなのか?バスドライバで初期化するというのはいったいどこへ...
step1: コントローラのリセット☑
- まずBAR(Base Address Register)からMMIO_BASEを求める。ここからxHCI Capability Registersが配置される。
- MMIO_BASE + CAPLENGTHから、Operational Registersが配置される。この詳細は仕様書の5.4にある
- 次にUSBSTS(USB Status Regiser)のHCH(HC Halted)bitが1であることを確認する。次にUSBCMD(USB Command Regiser)のHCRST(Host Controller Reset)bitに1を書き込む。するとxHCがリセット動作を開始する。xHCはリセット動作が終了時にUSBCMDのHCRSTbitを0にするのでこれが0になるまで待つ。USBSTSのCNR(Controller Not Ready)bitが0になるまで待つ。このbitが0でないと、DoorbellレジスタやOperationalレジスタには書き込むことができない。
step2: デバイスコンテキストの設定☑
- デバイスコンテキストとはxHCがデバイスの管理に使う情報。デバイス1つにつき1つ必要。
- xHC Capability RegistersのHCSPARAMS1(HC Structure Params1 Register)のMaxSlots(0:7)にxHCが扱える最大のデバイスコンテキストの数がセットされている。これが同時に扱えるUSBデバイスの最大値
- メモリ使用量を抑えたい場合はxHCが扱える最大のデバイスコンテキストの数をMaxSlotsよりも小さくする。そのためにはMMIO_BASE + 0x38にあるCONFIG(Configure Register)のMaxSlotsEn(Max Device Slots Enabled)に数を設定する。
- 次にデバイスコンテキストの先頭アドレスを並べた配列DCBAA(Device Context Base Address Array)を用意する。要素数は上で設定した最大数+1であり、いかに書いてあるように物理的に連続している必要がある。配列の要素全てを0に初期化する。それができたらDCBAAP(Device Context Base Addres Array Pointer)レジスタに先頭アドレスを63:6にセットする。(Reserved zeroのことRsvdZって書くのやめろ)
step3: Commnad Ringの生成と設定☑
memo
- Command RingはxHCに指示を出すためのリングバッファ。xHC一つにつき一つ用意する。ソフトウェアがproducerでxHCがconsumer。
- xHCはバッファのサイズを知らないので、サイクルビットが用いられる。producerが保持しているサイクルビットをPCS(Producer Cycle State)、consumerが保持しているサイクルビットをCCS(Consumer Cycle State)という。例えば初期状態でPCS=1, CCS = 1の場合、ソフトウェアはCommand RingにTRBをpushする際に最下位bitを1にする。xHCは最下位bitが1の間TRBを読み続け、0になると読み込みをストップする。バッファの終端に来た場合は先頭に戻り、サイクルbitを反転してもちいる。つまり次からは最下位bitが0である間TRBを読み込むようになる。このような実装になっているのはRing Buffer内の全てのTRBの最下位bitを書き変えるよりも内部のcycle bitを反転するほうが効率的であるためだろう。
手順
- まずCommand Ringをメモリ上に生成する。先頭アドレスは64byte alignされている必要がある。
- 次にCRCR(Command Ring Control Register)を設定する。最下位bitはCCSの初期値を指定するもの。PCSと同じになるようにセットする。
疑問点
- CRCRのその他のbitはどのように設定するべきなのか。
→とりあえずpointerとRCSのみ設定した。
step4: Event Ringの生成と設定☑
memo
- Event RingはxHCがproducerで、ソフトウェアがconsumer。xHCから通知する際に用いる。
- Event Ringには必ず1つ設定する必要があるPrimary Event Ringと制限値の範囲で任意の数だけ設定できるEvent Ringがある。Event RingはバッファサイズをxHCに設定するので終端を表すLinkTRBは用いない。サイクルbitはxHCがもつPCSになる。
手順
- Runtime Registers内にある、IR(Interrupter Register Set)を設定していく。IRは0-1023までの計1024個ある。0がPrimary Interrupter。今回はPrimary Interruperのみ使用する。
- まずEvnent Ringセグメントテーブルをメモリ上に作成する。
- 次にEvent Ring セグメントをメモリ上に生成し、Event Ring セグメントテーブルにアドレスを登録する
- ここからEvent Ring Registersを設定していく。
- ERSTSZ(Event Ring Segment Table Size Regiseter)の15:0にEvent Ringセグメントテーブルのエントリ数を書き込む
- ERDP(Event Ring Dequeue Pointer Register)にEvent Ringセグメントの先頭アドレスを設定する
- ERSTBA(Event Ring Segment Table Base Address Register)の63:6にEvent Ringセグメントテーブルの先頭アドレスをセットする。
疑問点
- Primary Interruper以外を使用するとどういうことが出来るんだろう。普通に考えれば各割り込み番号に対して別々のEvent Ringを割り当てることができるんだろうけど、IRが全部で1024個あって、
x64で使える割り込み要因番号の合計である256個よりも大分多いのが気になる。 - メモリが足りなくなったときにxHCがソフトウェアに対してメモリ割り当てを要求するってこと?
- ERDPの設定について、xHCはproducerなのにdequeueなのが気になった。ソフトウェアが今読み込んでる場所ってこと?だとしたらソフトウェアはEvent Ringから読み込んだら何らかの方法でERDPを更新してるってこと?
step5: 割り込みの設定
memo
- xHCではイベントを通知する方法割り込みがサポートされている。xHCはPCIデバイスなのでPCI規格に従って割り込みを行う。ここではMSI割り込みを用いる。
手順
- IMOD(Interrupter Moderation Register)のIMODI(Interrupt Moderation Interval)(15:0)に4000を書き込む。これは割り込み間隔の最低時間を20nsの倍数で指定する。4000だと1msになる。
- IMAN(Interrupter Management Register)のIP(Interrupter Pending)(0)とIE(Interrupter Enable)(1)に1を書き込む。以下に書いてある通り、IPとIEをどちらも1にするとこのinterrupterは割り込み間隔のカウンタが0になったとき割り込みを発生させるという意味になる。
- USBCMDのINTE(Interrupter Enable)に1を書き込む。(本ではIEになってるけど間違いかな?)これによりxHCが割り込みを生成できるようになる。
- Message Address, Message Dataの値を決める。
- MSIレジスタにMessageAddresとMesasge Dataを設定する。
step6: コントローラの開始☑
手順
- USBCMDのR/S(Run/Stop)(0)に1を書き込む
- USBSTSのHCH(HC Halted)が0になるまで待つ
まとめ
- xHCからソフトウェアに対しては割り込みで、ソフトウェアからxHCに対してはドアベルレジスタへの書き込みでイベントを通知する。
疑問点
- Transfer Ringは設定しなくてよいのか
- Transfer Ringを用いたデータ通信の応答はPrimary Event RingかEvent Ringに書き込まれるとあるが、Event RingはTransfer Ringを用いた通信の応答にしか用いられないのか。(Command Ringを用いた通信の結果をEvent Ringに書き込むことはできないのか。)
- 上が真ならPrimary Event Ringのみ用いる場合はTransfer Ringを用いることはできないってこと?
- Transfer Ringは個々のUSBデバイスに特有の情報をやり取りするのに用いる感じなのかな
- MilkanOSのソースを見た感じ、MSIの設定は普通にCapabilities PointerからCapability Listをたどって、MSI, MSIX用のCapability Listを見つけたら設定している。これらはそれぞれ一つずつしかないのか。だとしたら、割り込みが発生したとしてソフトウェアから分かるのは「どのデバイスから割り込みが発生したか」だけってことか?Primary Event Ringに要素が追加されたときの割り込み、○○のときの割り込み...のように機能ごとに割り込みを設定できて、割り込み要因番号を設定できるって話ではないのか?
- Primary InterruperがPrimary Event Ringに要素が追加されたときに実行される割り込みなのかな?
追記
- 以下の資料によればInterrupterごとにEvent Ringがあるらしい。Primary Event Ringは必ず設定する必要があることを考えると、Primary Interrupterはデフォルトで有効ってことになる。でもこれは誰がいつ呼ぶものなのか。
- TRBでInterrupterを指定できるっぽいけど...(xHCI企画書の4.17.4)
- MSIの設定とInterrupterの関係性が良く分からん
- MSI割り込みはあくまでイベントの発生をソフトウェアに通知するもので、TRBを読むことでどんなイベントなのかを判断するってことなのかな?でもEvent Ringってinterrupterごとに存在するんでしょ?
じゃあどのEvent Ringから読むべきなのかを判断する必要があるよね? - MME(Multiple Message Enabled)は、Message Data内のvecrtor fieldの下位何ビットをその機能が変更できるかを表す。
- MMEに設定した値をnとするとき、その機能が持てるinterrupterの最大数は2^nまで。
→その機能が発生させ得る割り込みの数だけinterrupterがあるという理解は正しそう。
→一つの機能は複数の割り込み要因を発生させることができる。
→この判断はxHCがやるってこと?
現時点での理解
- Command RingはxHC一つにつき一つ。Primary Event RingにはComand Ringに書き込んだ命令の結果が入る。
- Event RingはInterrupterごとに用意されている。Primary Evnet RingはPrimary Interrupter用
割り込みの設定
step1: MSI Capabilityを見つける
- PCI コンフィギュレーション空間はin,out命令で読み書きしているので4byte単位でしか読めない。
- まずコンフィギュレーション空間のCapabilities Pointerを読む。これを先頭からのoffsetとしてCapalilityを読んでいく。header(先頭32bit)を読んでCapIdが0x5であるものがMSI Capablity。見つかるまでCapabilityのNextPtrをたどる。
step2: 設定する
- Message Address,Message Data、その他のフィールドに適切な値をセットする。
- x86_64では割り込み要因番号として0-255までが使え、32からがユーザー定義になっている。
これでPCIデバイスが指定したCPUに対して指定した割り込み要因番号で割り込みを発生させることができるようになる。
これでPCIデバイスが割り込みを発生させることができるようになった。
step3: 割り込みハンドラを実装する
- 割り込み発生時に実行するハンドラを定義して、IDTの適切な場所に登録する。
- 割り込みハンドラはCPUに割り込みの終了をCPUに伝えるために0xfee00b0番地(End of Interrupt Register)に値を書き込み必要がある。
step4: IDTを作成してCPUに登録する
- IDTをメモリ上に確保して適切な場所に先ほど作った割り込みハンドラを登録する。
- lidt命令でIDTの場所をCPUに教えてあげる
これで割り込みが発生した際、割り込み要因番号に応じてCPUが適切な処理を呼び出せるようになった。
step5: 割り込みハンドラを高速化する
- 割り込みハンドラの処理を実行している間は他の割り込みを受け付けることができない。割り込みを取りこぼすのはいいことではないので、割り込みハンドラを高速化することが必要。割り込みハンドラには割り込みが発生したことを伝える処理を書くだけにして、その後の処理は割り込みハンドラから分離するとよい。これにはQueueを用いる。
- ただし、処理を割り込みハンドラから分離してユーザー空間で実行するように変更すると、データの整合性に気をつける必要が出てくる。割り込みが発生した際は(処理の途中であっても)問答無用で割り込みハンドラに処理が移るので今回のQueueのようにデータを共有している場合は注意が必要になる。(並列処理の際にとロックが必要なのと同じ)そこでQueueを操作する際はcli命令でrflagsのIF(Interrupt Flag)を0にするIFが0の時はCPUは割り込みを受け付けなくなるので処理が中断する心配がなくなる。共有データをいじる処理が終わったらsli命令でIFを1に戻しておく。
疑問点
- End Of Interrupt Registerへの書き込みをしないと、同じ割り込み要因番号の割り込みが処理されなくなり、rflagsレジスタのIF(Interrupt Flag)を1にすると割り込みを受け付けないという認識でOK?
- 割り込みハンドラの処理中にほかの割り込みが発生した場合はどうなるのか?
- IDTにハンドラが存在しない割り込みが発生した場合はどうなるのか。
- interrupt attributeを関数に付ける際に引数としてInterrupt Frame構造他へのポインタがセットされていた。どうしてこれが必要なのかが分からない。
- IDTにsegment selectorがあるのにどうしてattrでDPLを指定する必要があるのか。
TODO
- リファクタリング
→event処理をdriverに関数として実装する。 - popEventでERDPを更新する所を修正する
- Queueの処理がバグってないか確かめる
- ブログ書く
TODO
- mikanosではPortStatusChangedTRBが来たらまずEnableSlotを読んでスロットIDを取得している。その後AddressDeviceを呼んでいる。DeviceContextの生成は以下の処理で行われている(と思う)
xhc.DeviceManager()->AllocDevice(slot_id, xhc.DoorbellRegisterAt(slot_id));
この関数は以下のように定義されている。
Error DeviceManager::AllocDevice(uint8_t slot_id, DoorbellRegister* dbreg) {
if (slot_id > max_slots_) {
return MAKE_ERROR(Error::kInvalidSlotID);
}
if (devices_[slot_id] != nullptr) {
return MAKE_ERROR(Error::kAlreadyAllocated);
}
devices_[slot_id] = AllocArray<Device>(1, 64, 4096);
new(devices_[slot_id]) Device(slot_id, dbreg);
return MAKE_ERROR(Error::kSuccess);
}
メモリ確保しているのはAllocArray。これは以下のように定義されている。
template <class T>
T* AllocArray(size_t num_obj, unsigned int alignment, unsigned int boundary) {
return reinterpret_cast<T*>(
AllocMem(sizeof(T) * num_obj, alignment, boundary));
}
つまり先ほどの呼び出しはAllocArray(sizeof(Device) * 1, 64, 4096)になる。
この関数と関連する定義は以下。
Ceilはalignementに合う一番近いアドレスを返す。alignment - 1のnotとandを取るとその部分が0クリアされるのでアラインメントされたアドレスが手に入る。単純にandを取るだけだと、元の値よりも小さくなってしまう可能性があるのでalignment - 1を足している。例えばアドレスが48h (0b1001000)のとき、64byte alignすることを考える。
48h (0b1001000)
40h (0b1000000)
単純にandを取ると、確かに64byte alignされたアドレス(40h)になるが元のアドレスよりも小さくなってしまう。これは40hとandを取ることで0:6が無視されてしまうため。だから元の値に0b111111(3fh)を足す。すると必ず元の値よりも大きな値になる。(最低でも等しくなる。等しくなるのはもともと64byte alignされていた時)以上をまとめると以下のようにすればよいことが分かる。
return (ptr + align - 1) & align
→これだと上手くいかない。同じサイズで計算されるので、上位bitが0になってしまうから。MikanOSの
が正解。
return (ptr + align - 1) & ~(align - 1)
mikan osのように64byte alignされたメモリプールを切り出すように実装するとよい。
- USB driverが使用するデータ構造は固定長だからそれぞれのデータ構造に対してメモリプールを用意しとくのもありだな。プールが同じサイズでしか切り分けられないことが分かっていれば先頭アドレスを調整するだけで使えるし、フラグメンテーションを最小限に抑えることができる。汎用性はなくなるけど。
疑問点
- 突然Transfer Ringが出てきたけどこれはなに?
- USB3.0ではxhcがポートを自動でEnable状態に持って行ってくれる。(PortStatusChangedEventTRBが発行される)
- 対応するPORTSCのCSCとCCSが1であることを確認する。そしてCSCに1を書き込んでこれをクリアする。他のビットに影響を与えないように注意が必要。(ref: p.406, p.322)
- EnableSlotCommandを発行する(ref: p.107)SlotTypeには関連するxHCI Supported Protocol Capability 構造体のProtocol Slot Type fieldを指定するとあるが、p.531(以下)に書いてある通り、これは0になる。(じゃあなんでこのメンバが用意されているかは謎)
- CommandCompletionEventがどのコマンドに対する結果なのかはCommandTRBPointerで指定されているエントリのTRBTypeで判断できる。
- EnableSlotCommandが成功した場合はSlotIDフィールドに割り当てられたIDが渡される。
- デバイスのリセットからアドレスの割り当てまでは続けて行う必要がある
- ここで必要なデータ構造はDevice Context(1KiB)とInput Context(1053byte)。最初はそれぞれに対してメモリプールを用意しようとも思ったが、普通に汎用的なメモリアロケータを書く。処理を簡単にするために、要求サイズを1KiBの倍数に丸めたブロックを返すようにする。こうすれば自動的に先頭は64byte alignされるし、PAGE境界(2048byte境界)をまたぐこともない。
- リセットが終わったらEnableSlotCommandを発行してSlotIDを得る。DeviceContextを生成して対応するDCBAAにアドレスを書き込む。
- それが終わったらInputContextを生成してA0(SlotContext)とA1(EP0)を有効化して初期化する。その際一番最初のPortIDとEnableSlotCommandで取得したSlotIDが必要になる。
→リセットからアドレス割り当てまで一気に行う必要があるので、今扱っているPortIDをグローバル変数にするようにした。 - A0は素直に初期化すればよい。A1のMaxPacketSizeはPORTSCのPortSpeedフィールドに依存する。
- また、この時にTransferRingを作成し、設定する必要がある。(TransferRingはEndpointに対して一つ必要。)
→ 現状CommandRingは固定長配列になっているが、せっかくメモリアロケータを作ったのだからこれを使うようにしたい。TransferRing.cを新たに作ることも考えたが、CommandRingの処理と重複が多いのでtxRing.cにまとめることにした。これに合わせて(現状はPrimary Event Ringしか使っていないが)EventRing.cをrxRing.cに修正した。 - MikanOSのコードではこの後addressingPortを更新して、アドレス割り当てを待っていたデバイスたちの更新処理を行っている。その後InitializeDeviceを呼んでいる。
→逐次処理でやらなきゃいけないならQueueにしちゃった方がよくね?
bugs
- 原因として、ProcessEventのなかで複数回の割り込み分を処理してしまっている説
→これが濃厚。ProcessEventを呼ぶ前に、EventRingにエントリがあるかを確認する必要がある。
→やっぱりそうだった。
todo
- 型名がbitmapになっている奴を修正する?
- エラーを全てerror.hにまとめる。
- CommandRingを固定長配列にしているのをやめてメモリアロケータで動的にメモリを確保する。ファイル名をtxRing.cに変えてTransferRingにも使えるようにする。
- アドレス割り当てを待っている子たちのためのQueueを作る?
→Process Event内でEventRingを逐次処理しているから現状のままでいいのではという説もある。 - デバイスマネージャを作る。必要な情報は何か考える。
→DevContextはSlotIdが分かっていればDCBAAからたどれる。
→別の構造体を作ってEnque pointerのコピーを作ると書き込んだ際に更新するべき値が増えるのであまりよろしくない。 - エラー処理をちゃんとやる
- pushCommandでSlotIDを指定できるようにする。
→将来的にはEndpointを指定できるようにするかもだが、今は簡単のためにDefault Controll Pipeを使用するようにする。 - NoOpCommandを発行する
疑問点
- 以下に書いてある通り、HCCPARAMS1のCSZが1の場合はSlotContext, EndPointContextともに64byteになる。これはパディングが入るということか?
→このbitはソフトウェアがセットするものだから好きにできるってことか。64byteにしたいときってどんなときなん?
- また、上に関連してDeviceContextとInputContextはページ境界をまたいではいけなかったけどこれが例えば1024byteとかだった場合、DeviceContextはエントリを31個も持てないはず。(じゃなけばどうやってもページ境界をまたぐ)。エントリの数は31固定ではなく、自分で設定する必要があるということか?
→手元の環境ではPAGESIZEが2048になったのでこれでも問題はない。
現時点での理解
- 一つのデバイスはEndpointと呼ばれる論理的な通信路を最大で16個持てる。DeviceContextには31個のエントリがあるがこれは送信方向に加えて受信方向の情報があるから。
- TransferRingはEndpointに対して一つ用意する。
- SlotはxHCがデバイスに対して割り当てるもの。割り当てられたSlotIDをDCBAAのインデックスとして用い、これが対応するデバイスのDeviceContext情報になる。EndPoint構造体の中にTransferRingを設定するフィールドがある。
- DoorBell RegisterはEvnet RingにTRBを追加した際にそれをxHCに通知するために存在する。DoorBell Register0はCommand Ringに追加したことを知らせるために使う。DoorBellRegister nはDeviceContext nに対応する(つまりSlotIDに対応する)ここにDCI値(1-31)の値を書き込むことで対応するEndPointのTransferRingに書き込んだことが通知される。
- DeviceContext内のSlotContexにInterrupterTargetフィールドがあり、これを設定することでレスポンスをPrimary Event Ring以外のところに書き込むようにできるのだろう。
→現状は0になっているのでInterrupter0が対応する。
4/7
- エラーを全てusbError.hにまとめた
- CommandRingとEventRingが今まで固定長配列だったが、それを動的に確保するように変更。
- DCBAAも動的に確保するようにした。
- TransferRingを生成してDefault Controll Pipeに設定するようにした。
- AddressCommandに関する説明はp.110にある。SlotContextの初期化についてはp.97に説明がある。本で言及されているフィールドに加えて、RouteStringとContextEntriesを設定する必要がある。
- EP0の初期化は本で言及されているものに加えてTransferRingの生成とTRDequeuePointerフィールドへの設定が必要。
- 上の初期化処理はMikanOSの処理をそのままパクった
- AddreeDeviceCommandに対する返答は場合分けが激しくめんどくさい。p.113
- 結果は非同期で来る。Slot割り当てからアドレスの割り当てまでを連続して行う必要があるため、処理中は他のPortStatusChangedEventを無視する必要がある。
→どこかのタイミングで失敗した場合、それをどうするべきか分からない。
→これって無視しても同じやつが来たりするのかな? - DCIが使われている。
DeviceContextIndex(int ep_num, bool dir_in)
: value{2 * ep_num + (ep_num == 0 ? 1 : dir_in)} {}
疑問点
- 複合リテラルを代入式や初期化式に用いた場合、他のメンバはどうなるのか。初期化式は0になりそうだけど、代入式に自信がない。これが保証されない場合はメモリアロケータ側で0クリアする処理を入れる予定。
→とりあえず場合分けが面倒なのでアロケータ側で0クリアするようにした。これによってコードが簡単になる。
fucking bugs
- まず以下のところで複合リテラルを使うと実行が止まる。(System Errorが出てるのか?)
inputctx->SlotContext = (SlotContext) {
.RouteString = 0,
.Speed = pr[addressingPortID - 1].PORTSC.bits.PortSpeed,
.ContextEntries = 1,
.RootHubPortNumber = addressingPortID,
};
普通に代入すると動く。(は?)
→これは本当に分からない。0-31に割り込みハンドラを登録して実行しても何も起こらなかったのでシステムエラーでもない。
→わんちゃん普通にこの先の処理は実行されていて、newTXRingが呼ばれているとも思い、newTXRingの先頭で文字列を表示して見たのだがこれも表示されない。
→本当に意味が分からない。普通に代入式が問題なのかなと思って、使わない変数をつくって複合リテラルを代入して見たけど、これでも実行が止まった。
→代入式で使われている命令群で実行を止めるようなものはないと思うのだが...
→現実的なデバッグ手段としてはアセンブリを埋め込んで二分探索すること。
- AddressDeviceCommand発行しても返答が返ってこない。NoOpCommandは続けて遅れるのでリングバッファ関連のバグではなさそう。
→データ構造の制約ミス?なんかAddressDeviceCommand発行するとxHCが凄いいろんなことやるっぽいのでその処理の中でうまくいってない可能性はある。
→PCSの書き込みを忘れてただけっだった(馬鹿)
TODO
- usbError → 文字列の関数を作る。
- 状態遷移をちゃんと扱えるようにする
- とりあえずTransferRingでNoOpCommandを実行できるようにしたい。
memo
- PAGESIZEがもし1024byteよりも小さかったとしたらDeviceContextやInputContextはエントリを31個も持てなくなる。これを考慮する必要はあるか?
気になったこと
- レジスタの読み書き制約
- DCBAAから指されるDeviceContextの所有権はxHCにある。(これがOutputContextと呼ばれていたやつか?)
deviceの状態遷移について
- p.83~にdeviceの初期化に関する説明が書いてある。
- xHCを初期化した段階だと全てのポートはDisconnected状態になる。その状態でデバイスが接続されるとUSB3.0の場合はPolling Stateに移行する。これが成功した場合は対応するポートのCCSとCSCが1となり。Enable Stateに移行する。Polllingが失敗した場合はDisconnected状態に移行する。
- Root Hub Portの状態遷移(USB3.0)についてはp.305に記述がある。
- でもUSB3.0だとデバイスが接続されると自動でEnable Stateになるのでここを気にすることは少ないと思う。
- Enable StateになったらPORTSCのPortSpeedがデバイスの速度を表すようになる。
- とりあえずPortStatusChanged Eventが来たら、CCSとCSCを確認してそれが1であればEnable State状態にするで良いと思う。
- CSC(Connect Status Changed)とCCS(Current Conntect State)は単に接続状態が接続→切断(CSC=1かつCCS=0)もしくは切断→接続(CSC=1かつCCS=1)に変化したことを知らせるためのものっぽい。
- Polling State→Enable Stateへの移行が成功したかどうかはPEDが1、PRとPLSが共に0であることを確認する必要がある。こうなっていればUSB DeviceはDefault Stateになる。もし失敗した場合はPEDとPRが共に0になり、PLSが5(RxDetected)になり、Disconnected状態に移行する。
- Enable StateになったらEnableSlotコマンドを発行する。
- Enable SlotコマンドはSlot StateをDisable→Default Stateに遷移させる。(p.72)
→USB3.0ではDevice StateがPollingからEnableへ移行するとDeviceがDefault Stateになるという記述があるが、この"Default State"とは別物なのか? - EnableSlotコマンドが成功するとDevice Slot StateがEnable Stateに移行する。Device Slot Stateに関する説明はp.98にある。
- EnableSlotコマンドが成功したら次はDevice Slotの初期化処理(p.88-)
- これが終わったらAddress Device Commandを発行する。このコマンドはdeviceをDefault StateからAddress Stateにし、(これもSlot StateとPort Stateとは別物なのか?)さらにSlot StateをEnable StateからAddressed Stateにする。
→Device StateとDevice Slot StateとDevice Port Stateの三つがあるということか
→Device StateはUSB側が定義している状態で、xHC Specificationには記述がない。
step1: PortStatusChangedTRBの受信とポートのリセット
座学
- USB2のRoot Hub Portの状態遷移はp.300に、USB3の状態遷移はp.305にある。
- PSCEG(Port Status Changed Event Generation)が0から1に切り替わったときにPort Status Changed Event TRBが発行される。(p.319)PSCEGはp.321に書かれている通りxHCの内部変数で、ソフトウェア側からは見えない。
- ○○の変更によってPSCEGが0から1になったらPortStatusChangedEvnetが発行されるという記述が複数あるのだが、どういう条件でこれが切り替わるのかは良く分からない。PORTSCレジスタの各フラグはソフトウェが明示的にクリアするまで残り続けるという記載がある(p.321)ので、各フラグが前回のものと切り替わったタイミングでPOCEGが0から1になると考えるのが自然。
- p.83に書いてある通り、xHCの初期化後は全てのPortの状態はDisconnectedになっている。デバイスの接続が検知された場合、USB2とUSB3では異なる処理が行われる。USB3ではPolling stateに移行し、pollingが成功した場合はCCS(Current Connext State)とCSC(Connect State Changed)が1になり、Enabled stateに遷移する。失敗した場合はDisconnected stateになる。USB2ではDisabled stateに遷移し、CCSとCSCが1になる。
→USB3のデバイスで、pollingが失敗した場合はEventが発生することはないのか?
- もしCSCのassertionによってPSCEGが0から1に切り替わった場合はPortStatusChanged Eventが発行されると書いてある。つまりdeviceの接続が検知され、Port Status Changed Eventが来た場合、可能性としてはUSB3のデバイスで、既にEnabled stateになっている場合と、USB2のデバイスで、Disabled stateになっている場合の二つが考えられる。
- これを判断する方法はp.84に書いてある。
- リセット処理の詳細はp.327にある。ソフトウェアはPortStatusChanged TRBからPortIDを取得する。そして対応するPORTSCレジスタのPR(Port Reset)に1を書き込む。これを行うと以下の処理が走る。
- PRCの変更によりPSCEGが0から1に切り替わったら再びPort Status Changed Eventが発行される。
- 以下に書いてある通り、PRが0から1に切り替わったとき、Device stateがDefaultになる。ソフトウェアは即座にアドレスを割りあててDefault stateからAddress stateにする必要がある。(アドレス割り当てまでを連続して行う必要がある。)
実装
- やることを以下にまとめておく。
-
PortStatusChangedTRBを受信する。
-
TRBからPortIDを取得して対応するPORSTを見に行く
-
もしPort stateがEnabledだったら(PED=1, PR=0,PLS=0)そのままアドレス割り当てに進む(Port stateはEnabled, Device stateはDefault)
→これが起こるのはUSB3でpollingが成功した場合と、USB2でリセット処理が成功した場合の二択がある。
→CCSとCSCをチェックする必要はあるのか? -
もしUSB2だった場合はリセット処理に進む。USB2ではリセット処理が失敗することはあり得ない。PRに1を書き込み、PortStatusChangedEventを待つ。
以上によりPort stateはEnabled, Device stateはDefaultになる。
TODO
- Root Hub Portの個数はHCSPARAMS1レジスタのMaxPortsに設定されている。アドレスの割り当てまでを連続して行う必要があるので、MaxPorts分の情報を管理するテーブルでも作るか?
→MikanOSと同じように、このテーブルをループで見て処理していく。 - PortStatusChangedイベントが発行されたときに、今はデバイスのattachしか処理していないのでdetachも処理できるようにする
疑問点
- USB3でかつpollingが失敗した場合はEventは発行されないのか?
- p.321にはソフトウェアはPORTSCの対応するbitをクリアする必要があると書かれているが、MikanOSにはCSCbitをクリアする処理はなかった。CSCはCCSに変化があったことを通知するために用いられるもの。
→しかしクリアしないとリセット処理に対するPortStatusChangedTRBが発行されなかった。
→どれをクリアするべきなのかがいまいち良く分からない。
→この場合、USB2のportで初期化処理をした後は別にCCSが変化するわけではないのでCSCが1になることはない。 - 処理を実行している間にデバイスが抜かれたりした場合はどうするのか。エラー処理がメンドそう。
step2: Slotの割り当て要求
- EnableSlotCommandを発行してスロット割り当てを行う。これに関する詳しい説明はp.107にある。
- 成功した場合はslotIDが返ってくる。
疑問点
- p.72にはEnableSlotCommandがSlot StateをDisableからDefaultにするとあるが、これを発行している時点でSlot StateはEnabledではないのか。(まずDefaultってなんだよ)
step3: Addressの割り当て要求
- Slot割り当ての次にやることはp.85~に詳しく書いてある。
- まずInputContextの生成と初期化を行う。(初期化についてはp.96に詳しい記述がある)
- 次にDefault Controll Pipe用のTransfer Ringを生成して、Default Control Pipe(EP0)を設定する。(p.89に詳しい説明がある)
- OutputDeviceContextを生成して0に初期化し、DCBAAの対応する場所にアドレスを書き込む
step4: デバイスディスクリプタの取得
座学
- p.429を参照。Door Bell RegisterはDevice Slotごとに存在する。Slot IDは1始まりなので、Door Bell Register 0だけはCommand RingにTRBを書き込んだことを通知するのに用いられる。
- デバイスは最大で16個のEndPoint(論理的な通信路)を持てる。EndpointごとにTransferRingを設定する。この設定はInput Contextに書き込む。Input Context内にはEndoint Contextの配列(要素数31)があり、配列のIndexとして、以下の計算式で計算されるDCI(Device Context Index)が用いられる。全てのデバイスはEndpoint0(Default Controll Pipeと呼ばれる)を持ち、これは両方向の通信ができる。
DCI = 2 * Endpoint番号 + 通信方向
通信方向 = 出力? 1 : 0 (ただし両方向のEndpointの場合は1)
- Door Bell Register nにDCI値dciを書き込むと、Device Slot nに割り当てられているデバイスのEndppoint dciにTRBを書き込んだことがxHCに通知される。
→ つまりpushCommandにはSlot番号とDCI値を指定して、TransferRingに書き込むことができるようにする。Slot番号が0の場合はCommand Ringに書き込みを行うようにする。
→DoorBellを鳴らす関数も同様にSlot番号とDCI値を受け取れるようにする。 - デバイスディスクリプタを取得するにはコントロール転送を行う必要がある。コントロール転送はSetup,Data,Stausという三つのステージからなる。これを行うために同名のTRBが用意されている。
- Data Stage TRBのIOCを1にするとコントロール転送完了時に割り込みが発生し、Transfer Event TRBがEvent Ringに書き込まれる。
- Device Descriptorの定義はUSb3.0の仕様書のTable9-8(p.329)に定義されている。GET Descriptorリクエストについては9.4.3(p.317)に説明がある。
実装
- まずpushCommand関数がSlot番号とDCI値を受け取るように変更した。DoorBell Registerに書き込みを行う処理を関数として分離して通知するタイミングを選択できるようにした。これはDefault Control Pipeを用いたコントロール転送時に3つのTRBを書き込んだ後にDoorBell Registerを鳴らす必要があるっぽいため。(p.85-)
- FS(Full Speed Device)の場合はMax Packet Sizeの値を取得するためにp.85に書いてある手順を行う必要があるらしい。(とりあえず飛ばす)
- まずSetup Stage TRB,Data Stage TRB, Status Stage TRB, Transfer Event TRBを定義する。
- 今まではCommand TRBはCommandRingにしか書き込んでいなかった。しかしここからはTransfer Ringに書き込むことが必要になってくる上に、書き込んだ後にそれを通知するのには対応するDoorBellレジスタに書き込む必要がある。というわけでpushCommand関数の引数にRingへのポインタを渡せるようにして、CommandRingをグローバル変数にする。さらにDoorBellレジスタに書き込みを行う関数を作り、DCIを指定できるようにする。DCIが0の時はCommand Ringに書き込むようにする。
- また、今はとりあえず一つのポートしかハンドルしておらず、ポートの処理中にほかのポートからのTRBが来ても無視しているがそれはよろしくないので全てのポートの状態を保持するようにしておく。ポートの最大数はMaxPortsEnに設定したものになるので、その分を配列として動的に生成してそこに全てのポートの状態を保持するようにする。さらにデバイスごとに必要な情報をまとめた構造体を作成する。とりあえず必要なのはデバイスの状態、Input/outPut Cotntextへのポインタ、TransferRingへのポインタ、DoorBellRegisterへのポインタ。
- それぞれのTRBのフィールドを本に書かれている通りに設定してDefault Controll PipeのTransfer Ringに書き込んで送信する。
→これは一気に3つ送る必要があるのか?
→MikanOSではControlIn関数でこの処理を行っており、三つ書き込んだ後にDoorBellを鳴らしている。
step5: コンフィギュレーションディスクリプタの取得
座学
- コンフィギュレーションとはデバイスの構成を定義するもの。
- デバイスは基本1つのコンフィギュレーションを持つが、中には複数のコンフィギュレーションを持つものもある。
- 一つのコンフィギュレーションは1つ以上のインタフェースを持つ。インターフェースは特定の機能を表すもので,インターフェースごとにデバイスクラスを持つ。機能ごとにインタフェースとして分かれているおかげでクラスドライバを使いまわすことができ柔軟な開発が行える。
- Configuration Descriptorの定義はUSBの仕様書の9.6.3(p.334)にある。
4/10
- 今まではCommandRingにしか書き込んでいなかったが、ここからはTransferRingにも書き込みを行う必要がある。そこでpushTRBという関数で共通の処理(CフィールドにPCSを書き込んでドアベルを鳴らし、もし最後のエントリだったらLinkTRBのCにもPCSを書き込むという処理)を行うようにし、ドアベルレジスタに書き込みを行う処理を関数として分離した。
- p.511に書いてある通り、それぞれのRingで使えるTRBが決まっているため、CommanRing用、TransferRing用の関数を用意し、それぞれでTRBTypeが許されているものかをチェックするようにした。
- ポートの処理をport/manager.cに移してこれを通じてport操作をすることにした。範囲外のportIDが指定されたときのエラー処理を書いているが、そもそもこれは外部から呼ばれるものではないので別にイラン気もする。(俺が気を付ければいい話)
memo
- デバイスは最大で16個のEndpointを持ち、各EndpointはEndpointはEndpointNumberで指定される。これは0始まり。
- リセット処理が正常に完了した場合(USB2)とUSB3の場合、Slot StateはEnabled, Device StateはDefaultになる。
- EnableSlotCommandによる状態遷移はない。(たぶん)
- 以下に書いてあるとおりリセット処理を行った後はPortStatusChangedTRBが来るまで、EnableSlotCommandとAddressDeviceCommandを発行した後はCommandCompletionEventが来るまでは他のCommandを発行してはいけない。
→これがリセット処理からアドレス割り当てまでは一つのポートのみ処理しなきゃいけない理由
→こういったCommandを発行した場合、そのCommandに対する返答が必ず次に来ることは保障されているのか?つまり他のEventが処理してる間に発生することはないのか?
todo
- エラー処理をちゃんとやる。
- エラーをラップする構造体を作る。
- MikanOSをまねてProcessEventはエラーを返すようにし、ProcessEventsでEventRingを検査する。
- デバッグ
4/11
- デバッグを行った。ポート関連の処理をport/manager.cに分離した。
- ProcessEventsを作った。
- error処理をusbError.hに切り出すようにした。
→今まではerrorはenumだったが、他にも情報を持てるようにした。
TODO
- Loggerを作る
<ファイル名>:<行番号>: error: <code> <info>
エラー処理についての考察
- 現状はErrorCodeとして失敗か成功かしかない。今回のケースではエラーが起きたと分かっても回復できる見込みはない。
- また、エラーコードに過度に説明的な内容を書くべきではないと個人的には思う。(enumのメンバが増えるし)
例えばメモリが足りずにメモリ割り当てに失敗する場合、エラーコードはErrLowMemoryとかにするだろう。このエラーコードを持つエラーは複数の箇所で発生する可能性がある。メモリアロケータのような汎用的な処理は複数の場所で使われるのが普通だからだ。しかし、エラーを表示する際、これをそのまま"ErrLowMemory"と表示してもあまり意味がない。"○○ initialization failed"のようなエラーメッセージの方がよっぽど分かりやすい。
つまりエラーには階層がある。ここではそれぞれの箇所で発生し得るエラーをErrLowMemoryというエラーコードで同一視している。逆に複数のエラーを一つの
4/12
- log関数を作った。
4/13
座学
- slotの状態遷移について、デフォルトはDisabled。EnableSlotCommandが成功するとEnabled(p.88参照)になる。p.89に書いてある通り、Legacy Deviceを扱っており、Slotの状態がDefaultの時になにか操作が必要な場合以外はBSR=0でAddressDeviceCommandを実行する。BSR=0のAddressDeviceCommandが成功すると、Slotの状態はAddressedになる。
実装
- 全てのslotの状態を管理するmanagerを作った。
4/14
座学
deviceの状態遷移について、USB2のデバイスでリセット処理が成功した場合とUSB3のデバイスでpollingが成功した場合はDefaultになっている。AddressDeviceCommandでアドレスを割り当てるとAddressになる。
実装
- USBデバイスごとに必要になるInputContextとOutputContextへのポインタをUSBDevice構造体として定義した。
- スロットに関連付られているデバイスを管理するためにusb/driver.cを作成した。AddressDeviceCommandが成功した際に、管理テーブルにデバイスを登録するようにした。
TODO
- エラーメッセージを改良する
- デバイスディスクリプタとコンフィギュレーションディスクリプタを取得する
5/4
- デバイスディスクリプタの取得処理を実装して、Completion Code=1で、Trasnfer Evnet TRBが返ってくることを確認した。
- ドライバ自作本に書いてあるものに加えて、SetupStageTRBのIDTを1にする必要がある。
- 自分の環境ではSetup, Data, Status全て、終了時にTrasnferEventTRBが生成された。
5/7
知識
- USBデバイスは(通常)一つのコンフィグレーションを持つ。コンフィギュレーションはデバイスの構成を定義するもの。
- 一つのコンフィグレーションは一つ以上のインタフェースを持つ。
- インタフェースごとにデバイスクラスを持つ。
→デバイスクラスはインタフェースの種類を表すものってイメージか? - コンフィギュレーションディスクリプタを取得するとそのコンフィギュレーションに含まれるインタフェースとエンドポイントのディスクリプタも同時に取得される。
- Transfer Event TRBのTrasnfer Lengthは残りのバイト数。
実装
- Device DescriptorやらInterface Descipotrやら沢山のDescriptorがあるので定義を別ファイルに切り出した。
疑問点
- Standard Device Descriptorのバイト数だけ要求したらTransfer Lengthが0x20になっていた。GET Descritorで取得されるのはこれだけじゃないのか?
→bLengthは0x12だった。
→試しにuint8 buf[256]に受信するようにしたけど、これをするとCompletionCodeが0xd(ShortPacket)になった。Transfer Lengthは変わらず0x20だった。
→やり取りするサイズが可変長の場合に使用されるのかな? - Get Descriptorが成功したことはどうやって判断すればいいのか?
→ Transfer Eventで判断できるかと思ったけど、Trasnfer EventはTransfer Ringで生成されたTRBに対するレスポンスなのでこれがGet Desciptorに対する応答か分からない。
→Setup Stage TRBが完了した際のTransfer TRBのTRBPointerHiandLoから調べることにした。
→Setup Stage TRBに対する応答が返ってきた時点でDevice Descriptorは取得できていた。 - 本ではConfiguration Descriptorのサイズを計算しているけど、普通にCDのwTotalLengthじゃだめなん?
memo
- Control転送に関してはドキュメントのp.8-48に乗っている。9-13の方が詳しい。
- short packetは要求サイズよりも少ないバイト数しか受け取らなかったときに発生するものらしい。short packetは完了が通知されないらしい。
→今はstatus stageとかsetup stageとかでもIOCを1にしているのでdata stageのcompletion codeがshort packetになってもこれを処理できるので問題ないが、short packetにしないためには取得するデータピッタリのサイズを指定する必要があるってことだろうか?
5/27
知識
- サブクラスが0のものはOSから使われることを意図したもの。1はBIOSなど機能が制約された環境で使用されることを意図したもの。
- サブクラスが0のものではHIDで入力されるデータをHID自身が決めることができるのでマウスやキーボードの区別がない。
- サブクラスが1のものだとデータ形式が仕様として定まっている。
- クラスコードはデバイスディスクリプタとインタフェースディスクリプタに記載がある。HIDクラスだとインタフェースディスクリプタに書いてあるクラスコードで判断する。
- HIDの仕様ではデフォルトコントロールパイプで初期状態を取得して後はInterrupt Inエンドポイントでやり取りすることが意図されている。
→デフォルトコントロールパイプは同期的なので、ソフトウェアが要求しない限り押されたキーの情報は取得できない。Interrupt Inは非同期的なので状態の変化があった場合にそれが通知される。 - 送られているキーコードはaciiではない。以下の10章で定義されている。
-
キー配列の違いにより同じキーコードに別の文字が当てられている場合がある。HIDにはキー配列を判別する仕組みがないので、キー配列を判別するのもOSの仕事(どうして...)例えばMACではキーボード接続時にいくつかのキーを入力させてキー配列を判断するようになっている。
-
ブートインタフェースを使うのが楽。
やること
-
まずHIDデバイスを探す
-
ポートのリセットした後, Device StateはDefautlになる。この後すぐにAddress Deviceコマンドを発行してDevice StateをAddress Stateに移行させる必要がある。つまりデフォルトコントロールパイプの有効化までを一気に行う必要がある。
→Queueを使うのが良いと思うけど、Port Status Changed TRBが発行されるのっていろんな場合があるんだよな... -
デバイスディスクリプタとコンフィギュレーションディスクリプタを取得する。
-
コンフィギュレーションディスクリプタからデバイスが持っているインタフェースとエンドポイントの情報を取得する。
-
ブートインタフェースのエンドポイントを有効化する。
-
SET_PROTOCOLでキーボードがしゃべるプロトコルを設定する。
-
(Interrupt Inを使う場合は)Normal TRBを送信する。受信するたびにこれを送信する必要がある。
momo
- インタフェースは機能、エンドポイントは(論理的な)通信路と考えると分かりやすい。
- 各EndpointごとにTransfer RingとEvnet Ringがあるんじゃなかったけ?Trasnfer Ringの結果ってPrimary Event Ringに返ってくるん?
→設定することもできる。 - USBキーボードで同時に押せるのは6つまでだけど、6つ以上押した場合は押したキーのみが送られているのかな?
- デバイスディスクリプタのデバイスクラスと、インタフェースディスクリプタのインタフェースクラスは別物?デバイスディスクリプタのクラスコードでそのデバイスが何かを判断して、さらにインタフェースディスクリプタのクラスコードからそのデバイスがどのような機能を持つかを判断するってイメージか?
→と思ったが、デバイスディスクリプタのbase classは0になっていた。ってことはどんなデバイスでもConfiguration Descripotorを取得する必要があるってことだな。(めんどい...) - class codeの一覧は以下で定義されている。
流れ
- デバイスがportに接続されるとPort Status Changed TRBが発行される。
- ソフトウェアはその後(USB2.0の場合は)ポートのリセットを行う。
- ポートのリセットが終わるとデバイスはDefault Stateになる。その後すぐにAddress Device Commandを発行してアドレスの割り当てを行い、デバイスをAddress Stateにする必要がある。(つまりポートのリセットからデフォルト・コントロール・パイプの有効化までを一気に行う必要がある。)
- Deviceの種類(クラスコード)が分かるのはこの後、デバイスディスクリプタやコンフィギュレーションディスクリプタを取得した時。
方針
- 処理中のポートがあったために処理が遅延されたポート用にQueueを実装して、Address Device Commandが完了した時点で(Queueに要素がある場合は)これを取り出して処理するようにする。
→いい加減Queueをライブラリ化したいな。扱うデータが違うだけでほとんど同じ処理になるから。
- デバイスのリセットからスロット割り当て、アドレス割り当てを行う必要があるとあるけど、この順序を常に成功させられる保証はなくない?スロット割り当てに失敗した場合はスロットの空きがでるまで処理が進まないのだから、Address Deviceコマンドの前にスロットを開放するコマンドを実行しないといけないだろうし。