
2026/04/09 4:23
**ソフトウェア開発者のためのUSB:ユーザースペース USB ドライバー作成入門**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
USB デバイスの操作は、libusb を使用してユーザー空間だけで完全に処理できるため、カーネルレベルのドライバ開発は不要です。 例として、Fastboot モード(VID 18d1 / PID 4ee0)にある Android フォンを挙げます。接続すると `lsusb` は「Google Inc. Nexus/Pixel Device (fastboot)」と表示し、カーネルドライバは付いていません。また、ベンダー固有クラスインターフェースが 2 つのバルクエンドポイントを公開します:コマンド送信用 OUT 0x02 とレスポンス受信用 IN 0x81。 libusb のホットプラグコールバックはこのデバイスの到着を検出し、Fastboot コマンドを自動的に発行できます。典型的な手順は次のとおりです: 1. `libusb_control_transfer` を使用して GET_STATUS リクエストを送信します。2 バイトの応答はデバイスがセルフパワーであり、リモートウェイクアップをサポートしないことを示します。 2. GET_DESCRIPTOR リクエストを送信して完全なデバイスディスクリプタ(ベンダー/プロダクト ID、USB バージョン等)を取得します。 3. バルク OUT 0x02 を介して Fastboot コマンドを発行します(例:「getvar:version」を 64 バイトにパディング)。 デバイスは IN 0x81 で 4 文字のステータス(「OKAY」または「FAIL」)と任意のペイロードを返します。 同じユーザー空間アプローチは、バルク転送に依存する他の USB プロトコルにも適用できます。主な作業はカーネルコードを書く代わりにプロトコルロジックを実装することです。これにより OEM 向けドライバ開発が簡素化され、ブートローダーのテストが迅速化し、カーネルモジュールなしでカスタム USB デバイスの高速プロトタイピングやデバッグが可能になり、組込み開発者と広範な USB エコシステムに恩恵をもたらします。
本文
はじめに
USBデバイスのドライバーを書きなさいという指示を受けたとします。
最初は大変そうに思えるかもしれません――ドライバーはカーネルコードで、低レベル、デバッグが難しい…しかし実際にはそうではありません。USBデバイス用のドライバーを書くことは、ソケットを使うアプリケーションを書くほど難しくありません。
この記事は、ハードウェアに触れた経験がなくても USB を利用したい人向けに、USB の概要をわかりやすく紹介します。
「USB in a Nutshell」など詳細なリソースもありますが、ハードウェアの背景知識がないと取り付きづらいものです。USB やソケットを使うために組み込みシステムエンジニアでもネットワークスペシャリストでもなくても構いません。
USB デバイス
ここでは Android スマートフォンのブートローダー(fastboot)モードを使います。理由は次の通りです。
- 取得が簡単
- プロトコルがよく文書化されていてシンプル
- ドライバーが事前にインストールされていることがほとんどないため、OS が実験を妨げません
デバイスをブートローダーにする方法は機種ごとに異なります。一般的には電源投入時にボタンの組み合わせを押し続けます。私の場合は 音量ダウン を押したまま起動します。
手動でデバイスを列挙(Enumeration)
列挙とは、ホストがデバイス自身について情報を問い合わせることです。
USB デバイスをコンピュータに差し込むと、OS は以下の情報から適切なドライバーを決定します。
- USB Device Class → 標準ドライバー
- Vendor ID (VID) と Product ID (PID) → ベンダー固有ドライバー
基本情報
ドライバーがなくても、スマートフォンを PC に差し込むと USB デバイスとして認識されます。
Linux では
lsusb コマンドで確認できます。
$ lsusb Bus 008 Device 014: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
- Bus / Device – 実際の USB ポートを示す識別子
- ID – VID(
)と PID(18d1
)4ee0
VID は USB‑IF が割り当て、PID はメーカーが決めます。
クラスとドライバー情報
lsusb -t でデバイスツリーを表示すると次のようになります。
$ lsusb -t /: Bus 008.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd/1p, 480M |__ Port 001: Dev 002, If 0, Class=Hub, Driver=hub/4p, 480M |__ Port 003: Dev 003, If 0, Class=Hub, Driver=hub/4p, 480M |__ Port 002: Dev 014, If 0, Class=Vendor Specific Class, Driver=[none], 480M
最下のデバイスが私たちのスマートフォン(Bus 008、Device 014)です。
Class=Vendor Specific Class はカスタムプロトコルを使用していることを示し、Driver=[none] はドライバーがロードされていない状態です。これこそが私たちにとって良い兆候です。
Windows の場合
が無いので、同じ情報はデバイスマネージャーや USB Device Tree Viewer などで確認できます。lsusb
libusb を使った列挙
カーネルコードを書く代わりに、libusb を利用したユーザースペースアプリケーションを書きます。libusb は汎用ドライバーとデバイスをクレームするための API を提供します。
#include <print> #include <libusb-1.0/libusb.h> auto hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *user_data) -> int { std::println("Device plugged in!\n"); return 0; } auto main() -> int { libusb_context *context = nullptr; libusb_init(&context); libusb_hotplug_callback_handle hotplug_callback_handle; libusb_hotplug_register_callback( context, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED, // デバイス投入イベント LIBUSB_HOTPLUG_ENUMERATE, // 既に接続されているデバイスも対象 0x18d1, 0x4ee0, // VID / PID LIBUSB_HOTPLUG_MATCH_ANY, hotplug_callback, nullptr, &hotplug_callback_handle); while (true) { if (libusb_handle_events(context) < 0) break; } libusb_hotplug_deregister_callback(context, hotplug_callback_handle); libusb_exit(context); }
プログラムを実行し、スマートフォンを差し込むと次のように表示されます。
$ ./libusb_enumerate Device plugged in!
カーネルコードは触れていません。
Windows の場合
既にドライバーがロードされている場合はを使用します。そうでない場合は Zadig などで WinUSB.sys をインストールしてください。libusb_detach_kernel_driver()
デバイスとの通信
最も簡単な通信方法は Control エンドポイント(アドレス 0x00) を使うことです。このエンドポイントは標準プロトコルを使用します。ここでは
GET_STATUS 要求を送ります。
libusb_device_handle *handle = nullptr; libusb_open(device, &handle); std::vector<std::uint8_t> data(0xFF); const auto result = libusb_control_transfer( handle, uint8_t(LIBUSB_ENDPOINT_IN) | // デバイスからデータを取得 LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD, // 標準要求 LIBUSB_REQUEST_GET_STATUS, // GET_STATUS 0x00, 0x00, // wValue / wIndex data.data(), data.size(), 1000); // タイムアウト if (result >= 0) print_bytes(std::span(data).subspan(0, result)); libusb_close(handle);
出力例:
$ ./libusb_enumerate Addr: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0000: 01 00
最初のバイトはデバイスがセルフパワーであること(
1)を示し、2 番目はリモートウェイクアップをサポートしていないことを表します。
ディスクリプタの要求
ディスクリプタとはデバイスを記述する二進構造です。
GET_DESCRIPTOR で Device Descriptor を取得できます。
const auto result = libusb_control_transfer( handle, uint8_t(LIBUSB_ENDPOINT_IN) | // IN エンドポイント LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD, LIBUSB_REQUEST_GET_DESCRIPTOR, // GET_DESCRIPTOR 要求 (LIBUSB_DT_DEVICE << 8) | 0, // 0 番目の Device Descriptor 0x00, // 言語 ID data.data(), data.size(), 1000);
出力例:
$ ./libusb_enumerate Addr: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0000: 12 01 00 02 00 00 00 40 D1 18 E0 4E 99 99 01 02 0010: 00 01
USB 規格(第 9.6.1章)に従うと、以下の構造体になります。
struct DeviceDescriptor { u8 bLength; u8 bDescriptorType; u16 bcdUSB; u8 bDeviceClass; u8 bDeviceSubClass; u8 bDeviceProtocol; u8 bMaxPacketSize0; u16 idVendor; u16 idProduct; u8 iManufacturer; u8 iProduct; u8 iSerialNumber; u8 bNumConfigurations; };
idVendor と idProduct は lsusb の値と一致します。
他のディスクリプタ(Configuration、Interface、Endpoint、String)も同様に取得できます。
lsusb -v を使うとすべてを確認できる便利な方法です。
$ lsusb -d 18d1:4ee0 -v Bus 001 Device 012: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot) ... Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 [unknown] ...
このデバイスは、単一の Configuration を持ち、その中に Interface と 2 つの Bulk エンドポイントがあります。
エンドポイント
| 種類 | 用途 |
|---|---|
| Control | デバイスごとに 1 本。常にアドレス 。設定や問い合わせ用。ディスクリプタには表示されない。 |
| Bulk | 高帯域幅だが低優先度。大容量でタイムセンシティブでないデータ(例:Mass Storage)。 |
| Interrupt | 小さなデータを低レイテンシで送受信。ホストが頻繁にポーリング。 |
| Isochronous | 音声/映像などの時間的制約が厳しいストリーミング。libusb では非同期で扱う。 |
エンドポイントには方向もあります。
- IN – ホストがデバイスからデータを要求(ビット7=1)
- OUT – ホストがデバイスへデータ送信(ビット7=0)
USB バス上に存在できるカスタムエンドポイントは、制御エンドポイントを除き 127 本までです。
Fastboot プロトコル
Fastboot は Android のブートローダーで使われるシンプルなプロトコルです。
Host → Client
getvar:version
Client → Host
OKAY0.4 // 4 桁のステータスコード + データ
以下は、コマンドを送信し応答を受け取るサンプルです。
libusb_device_handle *handle = nullptr; libusb_open(device, &handle); libusb_claim_interface(handle, 0); std::vector<uint8_t> bytes(64); // Full‑speed 用に 64 バイト std::ranges::copy("getvar:version", bytes.begin()); int num_bytes_transferred = 0; libusb_bulk_transfer(handle, LIBUSB_ENDPOINT_OUT | 0x02, // OUT エンドポイント 0x02 bytes.data(), bytes.size(), &num_bytes_transferred, 1000); std::println("Request: {}", std::string_view(reinterpret_cast<const char*>(bytes.data()), num_bytes_transferred)); std::ranges::fill(bytes, 0x00); num_bytes_transferred = 0; libusb_bulk_transfer(handle, LIBUSB_ENDPOINT_IN | 0x01, // IN エンドポイント 0x81 bytes.data(), bytes.size(), &num_bytes_transferred, 1000); std::println("Response: {}", std::string_view(reinterpret_cast<const char*>(bytes.data()), num_bytes_transferred)); libusb_release_interface(handle, 0); libusb_close(handle);
実行すると次のように表示されます。
$ ./libusb_enumerate Request: getvar:version Response: OKAY0.4
最初の 4 バイト(
OKAY)は成功を示し、残りが値 0.4 です。
おわりに
カーネルコードを触ることなく、ユーザースペースで完全な USB “ドライバー” を実装しました。
USB ドライバーの共通原則は「列挙 → ディスクリプタ取得 → エンドポイント設定 → プロトコル通信」です。
MTP などより複雑なプロトコルでも、基本的な仕組みは同じです。TCP 上でソケットを扱うのと比べて決して難しいものではありません。