引き続きRaspberry PiのベアメタルプログラミングでUSBデバイスを動かす調査です。
前回、Pi Zero WにUSBキーボードを直結するとcsudデバイスドライバがうまく動作しない原因は、本来送るべきでないSplitパケットが送られているためらしいということが分かりました。
Splitパケットが送られている箇所を修正するため、csudの中身を覗いてみました。
csudのソースツリーは以下のような構成です。
csud/source/
├── configuration.c
├── device
│ ├── hid
│ │ ├── hid.c
│ │ ├── keyboard.c
│ │ └── mouse.c
│ └── hub.c
├── hcd
│ └── dwc
│ ├── designware20.c
│ └── roothub.c
├── platform
│ ├── arm
│ │ ├── armv6.c
│ │ └── broadcom2835.c
│ └── platform.c
└── usbd
└── usbd.c
このうち、コントローラチップの制御を行っているのがdesignware20.cで、USBデバイス一般の処理がusbd.c、プラットホーム依存部分がplatformディレクトリ、それ以外が各種デバイスの処理となっています。
hcd/dwc/roothub.cで実装されるroothubは、コントローラ直下に存在する(仮想的な?)USBハブです。今回問題となっているのは、roothubにUSBキーボードを接続した場合の動作ということになります。
コードの流れを追ってみると、Raspberry Pi Zero Wにキーボードを接続したときの関数コールは概ね以下のようになっているようです。
UsbCheckForChange →HubCheckForChange →HubCheckConnection →HubPortConnectionChanged →HubPortGetStatus UsbAttachDevice →UsbReadDeviceDescriptor →UsbGetDescriptor →UsbControlMessage →HcdSumbitControlMessage →HcdChannelSendWait →HcdPrepareChannel HcdChannelSendWaitOne
SPLITパケットの送受信の有無はhcd/dwc/designware20.cで定義されているHcdPrepareChannelで設定されていました。
if ((pipe->Speed != High) {
Host->Channel[channel].SplitControl.SplitEnable = true;
のようにフラグを設定しており、これがSplitパケットの送信をオンにしています。
pipe->Speedは、デバイスの速度に設定されています。
前回も書きましたが、この処理はRaspberry Pi Model 1では正しいです。
Model 1ではオンボードのLAN9512チップがUSB High Speed Hubになっていますので、root Hubは下図(A)や(B)のように、HS Hubとだけ通信していると想定できるからです。
一方、LAN9512チップを搭載していないZeroシリーズやModel Aシリーズ、Compute Moduleでは、図(C)のパターンが生じます。
速度はハードウェア側で自動調整されますが、上記のロジックだとこのパターンでもSplitEnableがtrueになり、Splitパケットが送信されてしまいます。
従って、図の3つのパターンのうち(B)だけがSplitパケットを送信するようにロジックを変更すれば良いということになります。
これに関して、ネットを調べていたところ、FreeBSD用のデバイスドライバで同様の問題が生じて解決されていることが分かりました。
[base] Diff of /head/sys/dev/usb/controller/dwc_otg.c
このコードでは dwc_otg_uses_splitという関数が定義されています。関数の中身は
459 static uint8_t
460 dwc_otg_uses_split(struct usb_device *udev)
461 {
462 /*
463 * When a LOW or FULL speed device is connected directly to
464 * the USB port we don't use split transactions:
465 */
466 return (udev->speed != USB_SPEED_HIGH &&
467 udev->parent_hs_hub != NULL &&
468 udev->parent_hs_hub->parent_hub != NULL);
469 }
となっています。この()内を翻訳すると、
・デバイスの速度がHigh Speedでない
・デバイスの上位のHubが存在し、かつHigh Speedである
・デバイスの上位のHubのさらに上位にHubが存在する
を全て満たす場合にSplitを有効にする、ということになります。
これなら、(B)のとき有効になり、(C)のとき無効になります(上位の上位にHubが無いため)ので、うまく動作しそうです。
同様のロジックをcsudに適用すると、前述のHcdPrepareChannel内での処理を以下のように変更することになります。
if ((pipe->Speed != High) && (device->Parent != NULL) && (device->Parent->Speed == High) && (device->Parent->Parent != NULL)) {
Host->Channel[channel].SplitControl.SplitEnable = true;
このコードを試してみたところ、思ったとおりUSBキーボードが認識され、キーボードからの入力も行えました。
しかし今度は
・キーボードを抜いたときの処理が異常(切断と接続のループになる)
・時間が経過するとキーボードを見失ってしまう
といった問題が出ました。
前者は図(A)や(B)では生じない状況についての異常動作なので、この状況に対応するための実装が必要と思われます。
後者については、上記のFreeBSDのドライバではデバイスの速度に応じてパケット送信間隔を調整するコードが入っていましたので、同様の処理を行う必要がありそうです。
コメント