ソフトウェア開発者のためのUSB: ユーザー空間USBドライバ作成入門
(werwolv.net)- USBドライバ開発はカーネルレベルの作業と見なされがちだが、実際にはソケットプログラミングに近い難易度でユーザー空間でも実装できる
- libusbを使えばカーネルコードを書かずに、デバイス列挙、制御転送、データ送受信をすべて実行できる
- USB通信は Control、Bulk、Interrupt、Isochronous の4種類の転送タイプと IN/OUT 方向で構成され、各エンドポイントは単方向の経路として動作する
- Android端末の Fastbootプロトコル を例に、Bulkエンドポイントを通じてコマンドと応答をやり取りする過程をコードで実演する
- ユーザー空間でも完全なUSBドライバを実装でき、すべてのUSBプロトコルは同じ基本構造を共有している
紹介
- USBデバイス用ドライバはカーネルコードを扱う必要があるという認識のため難しく感じられがちだが、実際にはソケットを使うアプリケーションレベルの複雑さである
- ハードウェア経験があまりない開発者でもユーザー空間でUSBを扱う方法を身につけられる
- USBの詳細な動作を扱う資料は存在するが、初心者にはとっつきにくい
- USBの利用に組み込みシステムレベルの知識は必要なく、ネットワークソケットのように扱える
USBデバイス
- 例としてブートローダーモードのAndroidスマートフォンを使用
- 入手しやすく、プロトコルが単純で、OSに標準ドライバがないため実験に向いている
- ブートローダーモードへの入り方は機種ごとに異なるが、一般には電源ボタンと音量ボタンの組み合わせで可能
デバイスの手動列挙
- 列挙(Enumeration) はホストがデバイス情報を要求して自身を識別する過程であり、デバイス接続時に自動で実行される
- 標準デバイスは USBクラス に基づいてドライバを自動ロードし、ベンダー専用デバイス は
VID(Vendor ID)とPID(Product ID)を使用する - Linuxでは
lsusbコマンドでデバイス情報を確認できる- 例:
ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot) 18d1は Google の VID、4ee0は Nexus/Pixel ブートローダーの PID
- 例:
lsusb -tコマンドでクラスとドライバ状態を確認できるClass=Vendor Specific Class、Driver=[none]と表示され、OSがドライバをロードしていない
- Windowsでは Device Manager や USB Device Tree Viewer で同じ情報を確認できる
libusbによるデバイス列挙
- libusb ライブラリを使うと、カーネルコードを書かずにユーザー空間でUSBデバイスと通信できる
libusb_hotplug_register_callback()で、特定のVID:PID組み合わせのデバイスが接続されたときにコールバックを実行するよう設定する- プログラム実行後にデバイスを接続すると
"Device plugged in!"メッセージが出力される - Linuxでは基本的にそのまま動作し、必要なら
libusb_detach_kernel_driver()でカーネルドライバを切り離せる - Windowsでは
Winusb.sysドライバが必要で、ない場合は Zadig ツールで手動置き換えが可能
デバイスとの通信
- USBデバイスとの最初の通信は Controlエンドポイント(アドレス 0x00) を通じて行われる
libusb_control_transfer()で**標準リクエスト(GET_STATUS)**を送信してデバイス状態を読み取る- 応答例:
01 00→ 先頭バイトは Self-Powered、2番目は Remote Wakeup非対応
- 応答例:
- その後 GET_DESCRIPTOR リクエストでデバイスディスクリプタを取得できる
- 返されたデータには
idVendor、idProduct、bDeviceClassなどのデバイス情報が含まれる
- 返されたデータには
lsusb -vコマンドであらゆるディスクリプタ(デバイス、構成、インターフェース、エンドポイントなど)を詳しく確認できる- 例:
Android Fastbootインターフェースに Bulk IN(0x81)、Bulk OUT(0x02) エンドポイントが存在する
- 例:
エンドポイント
- エンドポイントはネットワークポートに似た概念で、デバイスがデータを送受信する経路
- ディスクリプタには各エンドポイントの種類と方向が定義されている
-
Control転送タイプ
- すべてのデバイスに1つ存在し、アドレスは常に
0x00 - 初期設定やデバイス情報の要求に使われる
- インターフェースには属さず、デバイス自体の一部として存在する
- すべてのデバイスに1つ存在し、アドレスは常に
-
Bulk転送タイプ
- 大容量の非リアルタイムデータ転送に使われる
- 例: Mass Storage、CDC-ACM(シリアル)、RNDIS(イーサネット)
- 帯域は大きいが優先度は低い
-
Interrupt転送タイプ
- 少量の低遅延データ転送に使われる
- キーボードやマウスなどでボタン入力を高速にポーリングする
- 実際のハードウェア割り込みではなく、ホストが周期的に要求する
-
Isochronous転送タイプ
- 時間に敏感な大容量データ(音声、映像ストリーミング)に使われる
- 遅延が発生すると品質低下がすぐに表れる
- libusbでは非同期方式で処理する
-
IN / OUT 方向
- USBはホスト中心構造であり、デバイスは要求を受ける前にデータを送信できない
IN: ホストがデータを受け取る方向OUT: ホストがデータを送る方向- エンドポイントアドレスの最上位ビット(MSB)が
1なら IN、0なら OUT - 最大127個のユーザー定義エンドポイントを使用可能(
0x00は Control専用) - エンドポイントは単方向であり、Fastbootインターフェースのように IN/OUT の対で構成される
Fastbootプロトコル
- Fastboot はAndroidブートローダー通信用プロトコルで、コマンド文字列を送り、4バイトのステータスコードとデータを受け取る構造
- 例:
Host: "getvar:version"→Client: "OKAY0.4"Host: "getvar:nonexistant"→Client: "OKAY"
- 例:
- libusbを使ってFastbootコマンドを送信するコード例
- インターフェース 0 を
libusb_claim_interface()で確保 "getvar:version"コマンドを Bulk OUT(0x02) エンドポイントへ送信- Bulk IN(0x81) エンドポイントで応答を受信
- 出力例:
Request: getvar:version Response: OKAY0.4 OKAYは成功ステータス、0.4は Fastboot バージョン
- インターフェース 0 を
まとめ
- カーネルコードを書かずにユーザー空間で完全なUSBドライバを実装できる
- すべてのUSBドライバは同じ基本原理に従い、異なるのはプロトコルだけ
- 複雑なプロトコル(MTPなど)も基本構造は同じで、ソケット通信に近い概念として扱える
1件のコメント
Hacker Newsのコメント
まさに完璧なタイミングだった。もうすぐ MOTU MIDI Express XT を地元の Guitar Center で受け取る予定
中古機材なので、法律上一定期間保留しなければならず待っているところ。問題は、この機材が 標準の MIDI-over-USB ではなく独自プロトコルを使っているため、Linux や OpenBSD、Haiku といった自分のシステムでは USB でそのまま使えないこと
当面はシンセモジュールとコントローラー間のルーティングだけできれば十分なので構わないが、PC 側でも動くようにできたらうれしい
既存の Linux ドライバ はあるが、安定性も不確かだし XT をサポートしているかも曖昧。カーネルパニックの問題は解決したらしいが、まだ issue は残っている
なので LibUSB ベースのユーザー空間ドライバ を自分で作ってみようと思う。MIDI ポートを公開してルーティング用ツールも追加すれば、かなり便利になりそう
Go 言語でこういうことをやってみたいなら、cgo なしで USB にアクセスできる go-usb ライブラリを作ってある
これを使って UVC デバイス を扱う go-uvc も開発した
私も最近 Macbook M3 で usbip システムを似たような形で実装している
ただし最新の macOS には制約がある。システムが認識する USB デバイスについては libusb ベースのユーザー空間ドライバ をビルドできず、セキュリティ機能を手動で無効にしないと不可能
このアプローチだと、結局 USB ドライバがアプリケーションコードの役割も果たすことになる。つまり、ドライバというよりライブラリ+プログラム に近い
たとえば USB-Ethernet デバイスを OS のネットワークアダプタとして接続したい場合、どうすればよいのか気になる
この文章を 数年前 に読んでいたら、ノートPCの機能をリバースエンジニアリングするときにはるかに楽だったはず。特に キーボード LED 制御プログラム は、今でも自分のお気に入りのプロジェクトのひとつ
本当に役立つ入門書だった。低レベルのハードウェア API を扱うのは難しいがやりがいがある。現代の OS の抽象化レイヤーのおかげで簡単にはなったが、その下を理解することは今でも重要
C++ のコードが奇妙に見えた。矢印文字を直接入力できるキーボードなんて見たことがない
->になる。モダン C++ の trailing return type 構文だ"->"。フォントがそれを矢印としてレンダリングしているだけUSB デバイスが DMA をサポートしているのか気になった。ホスト経由でしかできないのか、それともデバイスが直接メモリにアクセスするのかも知りたい
以前、簡単な USB デバイスを作ろうとしたが、ディスクリプタ(descriptor) の書き方に関する情報がほとんどなかった。たいていは「似たデバイスを見つけてコピーして直してみろ」という感じだった。USB が本当に優れた標準なのか疑問だった
「USB デバイスドライバを自分で書け」と言われたら、そのデバイスを返却して、まず 仮想 COM ポート で処理できないか確認するだろう