24 ポイント 投稿者 GN⁺ 21 일 전 | 1件のコメント | WhatsAppで共有
  • 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 の VID4ee0 は Nexus/Pixel ブートローダーの PID
  • lsusb -t コマンドでクラスとドライバ状態を確認できる
    • Class=Vendor Specific ClassDriver=[none] と表示され、OSがドライバをロードしていない
  • Windowsでは Device ManagerUSB 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 リクエストでデバイスディスクリプタを取得できる
    • 返されたデータには idVendoridProductbDeviceClass などのデバイス情報が含まれる
  • lsusb -v コマンドであらゆるディスクリプタ(デバイス、構成、インターフェース、エンドポイントなど)を詳しく確認できる
    • 例: Android Fastboot インターフェースに Bulk IN(0x81)Bulk OUT(0x02) エンドポイントが存在する

エンドポイント

  • エンドポイントはネットワークポートに似た概念で、デバイスがデータを送受信する経路
  • ディスクリプタには各エンドポイントの種類と方向が定義されている
  • Control転送タイプ

    • すべてのデバイスに1つ存在し、アドレスは常に 0x00
    • 初期設定やデバイス情報の要求に使われる
    • インターフェースには属さず、デバイス自体の一部として存在する
  • 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 バージョン

まとめ

  • カーネルコードを書かずにユーザー空間で完全なUSBドライバを実装できる
  • すべてのUSBドライバは同じ基本原理に従い、異なるのはプロトコルだけ
  • 複雑なプロトコル(MTPなど)も基本構造は同じで、ソケット通信に近い概念として扱える

1件のコメント

 
GN⁺ 21 일 전
Hacker Newsのコメント
  • まさに完璧なタイミングだった。もうすぐ MOTU MIDI Express XT を地元の Guitar Center で受け取る予定
    中古機材なので、法律上一定期間保留しなければならず待っているところ。問題は、この機材が 標準の MIDI-over-USB ではなく独自プロトコルを使っているため、Linux や OpenBSD、Haiku といった自分のシステムでは USB でそのまま使えないこと
    当面はシンセモジュールとコントローラー間のルーティングだけできれば十分なので構わないが、PC 側でも動くようにできたらうれしい
    既存の Linux ドライバ はあるが、安定性も不確かだし XT をサポートしているかも曖昧。カーネルパニックの問題は解決したらしいが、まだ issue は残っている
    なので LibUSB ベースのユーザー空間ドライバ を自分で作ってみようと思う。MIDI ポートを公開してルーティング用ツールも追加すれば、かなり便利になりそう

    • Guitar Center の保留期間は、単に盗品かどうか確認するためだけではない。法律上、質屋(pawn shop) のように一定期間は販売禁止で、元の所有者が取り戻せる期間が過ぎてからでないと販売できない
    • 私も同じ機材を使っていて、そのドライバを AUR にパッケージした。バイナリ blob は動かなかったが、単純な MIDI ルーターとして使うには十分だった
  • Go 言語でこういうことをやってみたいなら、cgo なしで USB にアクセスできる go-usb ライブラリを作ってある
    これを使って UVC デバイス を扱う go-uvc も開発した

    • Rust では nusb を勧める
  • 私も最近 Macbook M3 で usbip システムを似たような形で実装している
    ただし最新の macOS には制約がある。システムが認識する USB デバイスについては libusb ベースのユーザー空間ドライバ をビルドできず、セキュリティ機能を手動で無効にしないと不可能

    • ドライバのオーバーライドは 1 層だけ調整すればよいので、緩和は可能
  • このアプローチだと、結局 USB ドライバがアプリケーションコードの役割も果たすことになる。つまり、ドライバというよりライブラリ+プログラム に近い
    たとえば USB-Ethernet デバイスを OS のネットワークアダプタとして接続したい場合、どうすればよいのか気になる

    • 標準化されたデバイスなら通常は USB/CDC/ECMRNDIS を使うので自動認識される。ユーザー空間からのアクセスはむしろ 非標準デバイス に有用。Windows ではドライバ署名なしで libusb により移植性高く実装できる
    • Linux では tun/tap デバイス を作ってユーザー空間からカーネルと通信するか、ほかのサブシステムもユーザー空間で動かす必要がある
  • この文章を 数年前 に読んでいたら、ノートPCの機能をリバースエンジニアリングするときにはるかに楽だったはず。特に キーボード LED 制御プログラム は、今でも自分のお気に入りのプロジェクトのひとつ

  • 本当に役立つ入門書だった。低レベルのハードウェア API を扱うのは難しいがやりがいがある。現代の OS の抽象化レイヤーのおかげで簡単にはなったが、その下を理解することは今でも重要

  • C++ のコードが奇妙に見えた。矢印文字を直接入力できるキーボードなんて見たことがない

    • あれは プログラミングフォントの合字(ligature)。コピーすると実際には -> になる。モダン C++ の trailing return type 構文だ
    • 合字フォントを好む開発者もいる。2 文字を 1 つのグリフにまとめてくれる
    • Compose キー を設定すれば、どんなキーボードでも “→” を入力できる
    • 結局はただの "->"。フォントがそれを矢印としてレンダリングしているだけ
  • USB デバイスが DMA をサポートしているのか気になった。ホスト経由でしかできないのか、それともデバイスが直接メモリにアクセスするのかも知りたい

    • USB デバイスは PCIe や FireWire のようにホストメモリへ直接アクセスしない。代わりに XHCI コントローラ が DMA を実行し、多くのデバイスコントローラは自身の RAM と USB 間の DMA をサポートしている
    • すべての転送は ホスト主導 で行われる。デバイスが先にデータを送っているように見えても、実際にはホストが要求している。直接 DMA はセキュリティ上の大きなリスクになる
  • 以前、簡単な USB デバイスを作ろうとしたが、ディスクリプタ(descriptor) の書き方に関する情報がほとんどなかった。たいていは「似たデバイスを見つけてコピーして直してみろ」という感じだった。USB が本当に優れた標準なのか疑問だった

    • 私もディスクリプタは謎めいていると思っていたが、結局 固定されたバイナリ構造体 だと気づいた。各 USB クラスで規定されたフィールドとエンドポイントさえ合っていれば認識される
    • USB は悪くないが、電気的な面では USB 1/2 は 真の差動信号ではない
    • チュートリアル資料はほとんどないが、大企業の標準としてはかなり妥当。ただし 選択肢が多すぎる ので、関連仕様を大量に読む必要がある
  • 「USB デバイスドライバを自分で書け」と言われたら、そのデバイスを返却して、まず 仮想 COM ポート で処理できないか確認するだろう