原文标题: Writing a basic Linux device driver when you know nothing about Linux drivers or USB 原文链接: https://crescentro.se/posts/writing-drivers/ 作者: Ivan (crescentro.se) 说明: 本文记录了作者从零开始逆向工程 Nanoleaf Pegboard Desk Dock(带 RGB 灯效果的 USB 扩展坞)并使用 Rust +
libusb(rusb库)编写 Linux 用户态驱动程序的完整过程与心得。
背景与前言
作者买了一个 Nanoleaf Pegboard Desk Dock(具有 RGB LED 灯效和挂钩功能的 USB Hub)。该设备官方仅支持 Windows 和 macOS,未提供 Linux 官方驱动。
作者通过 Windows 虚拟机抓包逆向了通信协议,同时也联系了 Nanoleaf 技术支持,并在 4 小时内收到了官方提供的详细协议文档(确认了灯效控制、电源与亮度管理协议)。由于作者过去从未写过 Linux 驱动或直接操作过 USB 协议,本文记录了他从零起步编写 Linux 驱动的全过程。
1. 从零开始:硬件识别与 USB 基础
在 Linux 中插入该 USB 设备后,系统能够自动识别并为其供电。通过 lsusb 命令可以查看到设备节点:
$ lsusb
Bus 001 Device 062: ID 37fa:8201 JW25021301515 Nanoleaf Pegboard Desk Dock
使用详细模式 lsusb -d 37fa:8201 -v 可以展开设备的描述符信息:
- Vendor ID (idVendor):
0x37fa - Product ID (idProduct):
0x8201 - Device Class:
0(Generic / Unknown) - Interface Class:
3(Human Interface Device, HID) - Endpoints:
EP 2 IN(Interrupt,0x82, 64 bytes)EP 2 OUT(Interrupt,0x02, 64 bytes)
USB 核心概念极简科普
根据 USB in a NutShell 的规范提炼:
- Configuration(配置):声明设备的电源需求与工作模式,绝大多数设备仅有 1 个配置。
- Interface(接口):代表设备的功能抽象(例如摄像头可能包含“文件存储”和“Webcam 视频”两个接口)。
- Endpoint(端点):数据传输的管道通道(支持 Control, Bulk, Interrupt, Isochronous 4 种传输类型)。
- IN 端点:数据从设备发送给主机(Host)。
- OUT 端点:数据从主机(Host)发送给设备。
该设备暴露了一个 HID(Human Interface Device) 接口。内核默认带有通用 HID 驱动,因此能自动识别并供电,但因为缺少专门的 RGB 控制子类逻辑,内核只能让其处于默认待机动画状态。
2. 驱动实现路径选择:内核驱动 vs 用户态驱动
针对这种 RGB 设备,有两种实现思路:
- 内核空间驱动(Kernel Space Driver):
- 遵循内核标准,把每个 LED 在
/sys/class/leds目录下暴露为 R/G/B 三个属性文件。 - 缺点:开发门槛高、合入主线内核流程繁琐,且
/sys/class/leds主要用于状态指示灯而非复杂 RGB 灯效。
- 遵循内核标准,把每个 LED 在
- 用户空间驱动(Userspace Driver via
libusb):- 在用户态使用
libusb(Rustrusb绑定)直接向 USB 端点读写字节。 - 优点:无需编译内核模块、无需 root 权限(配置 udev 规则后)、开发调试极其灵活。
- 在用户态使用
作者选择了方案 2(用户态驱动)。
3. udev 规则配置(免 root 权限访问)
在 Linux 中直接操作底层 USB 设备默认需要 root 权限。为了允许普通用户免提权运行驱动,需在 /etc/udev/rules.d/70-pegboard.rules 中添加 udev 规则:
ACTION=="add", SUBSYSTEM=="usb", DRIVERS=="usb", ATTRS{idVendor}=="37fa", ATTRS{idProduct}=="8201", MODE="0770", TAG+="uaccess"
注意:文件名需保证按字典序排在
73之前(如70-pegboard.rules),以便uaccess标签生效。重新插拔设备后即可无需sudo操作该 USB 设备。
4. 使用 Rust + rusb 编写驱动
步骤一:创建项目与获取设备句柄
添加依赖 rusb = "0.9":
use rusb::{Context, UsbContext};
const VENDOR: u16 = 0x37fa;
const DEVICE: u16 = 0x8201;
fn main() {
let context = Context::new().expect("cannot open libusb context");
let device = context
.open_device_with_vid_pid(VENDOR, DEVICE)
.expect("cannot get device");
let descriptor = device
.device()
.device_descriptor()
.expect("cannot describe device");
println!("{descriptor:#?}");
}
步骤二:解绑内核默认驱动与 Claim Interface
尝试 device.claim_interface(0) 时会报错 Busy,因为内核通用的 usbhid 驱动已占用了该 Interface 0。需要在 Claim 前主动调用 detach_kernel_driver:
const INTERFACE: u8 = 0x0;
if device.kernel_driver_active(INTERFACE).unwrap_or(false) {
device.detach_kernel_driver(INTERFACE).expect("cannot detach kernel driver");
}
device.claim_interface(INTERFACE).expect("unable to claim interface");
步骤三:向 Interrupt OUT 端点发送控制数据
根据协议,向端点 0x02 发送 02 00 c0 指令加上 64 组 RGB 数据(如 0f ff 0f 表示纯红)即可设置全板灯效:
use std::time::Duration;
const ENDPOINT_OUT: u8 = 0x02;
const TIMEOUT: Duration = Duration::from_secs(1);
let command: [u8; 3] = [0x02, 0x00, 0xc0];
let color: [u8; 3] = [0x0f, 0xff, 0x0f]; // RGB 控制
let body: Vec<u8> = command
.into_iter()
.chain(color.into_iter().cycle().take(192))
.collect();
device
.write_interrupt(ENDPOINT_OUT, &body, TIMEOUT)
.expect("unable to write to device");
步骤四:处理 Interrupt IN 响应(防止固件崩溃)
在 USB 规范中,主机(Host)必须主动 Polling 轮询读取中断。如果在向设备写入指令后不去读取设备的 Response 中断,设备固件会因缓冲区溢出而挂起并重置为离线默认状态。
读取端点 0x82 的响应数据:
const ENDPOINT_IN: u8 = 0x82;
let mut buf = [0u8; 64];
device
.read_interrupt(ENDPOINT_IN, &mut buf, TIMEOUT)
.expect("unable to read from device");
dbg!(buf); // 收到 [130, 0, 1, 0...] 代表 ACK 成功
5. 多线程并发架构:定时刷新与事件响应
在完整驱动中,需要一边维持灯效帧的流式刷新(避免设备因为超时未收到帧而退回离线模式),一边监听物理按键触发的 Interrupt 事件(单击/双击/长按):
use std::thread;
use std::time::Duration;
const WRITE_TIMEOUT: Duration = Duration::from_secs(1);
const READ_TIMEOUT: Duration = Duration::from_millis(1); // 符合 bInterval 声明
thread::scope(|s| {
// 线程 1:定时发送灯效控制帧
s.spawn(|| {
device
.write_interrupt(ENDPOINT_OUT, &body, WRITE_TIMEOUT)
.expect("unable to write to device");
});
// 线程 2:死循环轮询读取中断事件(如按键)
s.spawn(|| loop {
let mut buf = [0u8; 64];
match device.read_interrupt(ENDPOINT_IN, &mut buf, READ_TIMEOUT) {
Ok(_) => println!("Interrupt event: {}", buf[0]),
Err(rusb::Error::Timeout) => continue, // 正常无事件超时
Err(e) => panic!("{e:?}"),
}
});
});
6. 逆向过程中的避坑总结
- 颜色通道顺序坑:官方协议文档标注为标准 RGB 顺序,但实际硬件抓包与显示发现颜色顺序为 GRB。
- 亮度过高复位:如果将 RGB 设得过亮,硬件功耗超出 70mA 限制,设备会触发过流保护硬重化复位。
- 中断缓冲区清理:每发一条指令必须及时读掉 IN 端点返回的 ACK 帧,否则固件崩溃。
总结
作者指出,编写简单硬件设备的 Linux 用户态驱动其实并不复杂,使用不到 50 行 Rust 代码配合 libusb 与 udev 规则,就能建立起稳定双向通信的驱动程序。这为探索 Linux 硬件驱动与逆向工程提供了一个非常好的实战入门视角。