USB 驱动开发无痛指南:libusb 从理论到代码
为 USB 设备编写驱动程序常常让开发者望而却步,因为底层代码过于复杂。然而,使用像 libusb 这样的合适工具包,这个过程就变得像用 socket 进行网络编程一样简单直接。本文将逐步讲解如何为 USB 设备创建一个驱动,以 Android 设备处于 Bootloader(fastboot)模式为例。
USB 基础知识:开发者需要了解的内容
USB 规范定义了通过 VID(厂商 ID)和 PID(产品 ID)识别设备标准机制。这些标识符分配给制造商(VID 由 USB-IF 分配)和特定产品(PID 由制造商自身分配)。设备连接时,主机会请求描述符——固件中的二进制结构,包含设备类、功能以及所需驱动的信息。关键点:大多数任务可以在用户空间通过标准驱动如 Winusb.sys(Windows)或 usbfs(Linux)处理,无需内核代码。
重要术语:
- 端点:数据传输的逻辑通道(控制、批量、中断、等时)。
- 设备描述符:包含 VID/PID 和基本参数的根结构。
- 设备类:标准化功能(HID、大容量存储),但许多设备使用供应商特定类。
准备设备:选择与设置
我们以 Android 设备处于 Bootloader(fastboot)模式为例。选择此例子的原因:
- 易获取性:大多数智能手机支持切换到 fastboot。
- 简单协议:fastboot 文档公开且简洁。
- 无预装驱动:操作系统不会接管交互。
切换到 Bootloader 模式通常需要在开机时按住按键组合(例如,音量上键 + 电源键)。在 Linux 上识别设备,使用 lsusb:
$ lsusb
Bus 008 Device 014: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
此处,18d1 是 Google 的 VID,4ee0 是 fastboot 的 PID。在 Windows 上,USB Device Tree Viewer 提供类似信息。关键点:如果操作系统加载了驱动(例如,在设备管理器中显示 ⚠️),你需要使用 Zadig 将其替换为 Winusb.sys。
枚举设备:从手动分析到程序化实现
枚举过程在连接时自动启动。操作系统分析 VID/PID 和设备类来选择驱动。对于用户空间,我们使用 libusb 实现。下面是注册连接处理程序的代码:
#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!");
return 0;
}
auto main() -> int {
libusb_context *context = nullptr;
libusb_init(&context);
libusb_hotplug_callback_handle handle;
libusb_hotplug_register_callback(
context,
LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
LIBUSB_HOTPLUG_ENUMERATE,
0x18d1, 0x4ee0,
LIBUSB_HOTPLUG_MATCH_ANY,
hotplug_callback, nullptr,
&handle
);
while (true) {
if (libusb_handle_events(context) < 0) break;
}
libusb_hotplug_deregister_callback(context, handle);
libusb_exit(context);
}
此代码初始化 libusb,为 VID=0x18d1 和 PID=0x4ee0 的设备注册回调,并等待连接。成功时打印 "Device plugged in!"。在 Windows 上,可能需要分离内核驱动:
libusb_detach_kernel_driver(handle, 0);
通过控制端点与设备通信
对于基本交互,使用控制端点(ID 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,
LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD,
LIBUSB_REQUEST_GET_STATUS,
0x00, 0x00,
data.data(), data.size(),
1000
);
if (result >= 0) print_bytes(std::span(data).subspan(0, result));
libusb_close(handle);
成功响应(例如 01 00)根据规范解码:第一个字节——电源来源(1 = 电池供电),第二个——远程唤醒支持(0 = 不支持)。控制端点还支持其他标准请求(GET_DESCRIPTOR、SET_CONFIGURATION),对于获取描述符至关重要。
请求描述符:理解设备的关键
描述符是 USB 设备交互的基础。通过 GET_DESCRIPTOR 请求设备描述符:
const auto result = libusb_control_transfer(
handle,
LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD,
LIBUSB_REQUEST_GET_DESCRIPTOR,
(LIBUSB_DT_DEVICE << 8), // Tip deskriptora
0x00,
data.data(), data.size(),
1000
);
接收到的数据匹配 libusb_device_descriptor 结构,其字段包括:
bcdUSB:USB 规范版本bMaxPacketSize0:控制端点的最大数据包大小idVendor/idProduct:VID 和 PIDbNumConfigurations:配置数量
分析描述符可揭示支持的接口和端点。对于配置描述符,将请求类型改为 LIBUSB_DT_CONFIG,并在 wIndex 中指定配置索引。
关键要点
- 用户空间操作:使用 libusb 而非内核代码——更容易调试且更安全。
- VID/PID 作为主要标识符:驱动绑定依赖于它们;设备类往往缺乏信息(供应商特定类)。
- 控制端点作为基本工具:在使用其他端点前,用它获取描述符并控制设备。
- 描述符作为真相来源:其结构定义于 USB 规范;解码提供设备能力的完整视图。
— Editorial Team
暂无评论。