# USB Drivers Without the Pain: From Theory to Code with libusb
Writing drivers for USB devices often intimidates developers with the complexity of low-level code. However, with the right toolkit like libusb, the process becomes as straightforward as network programming with sockets. In this article, we'll walk through creating a driver for a USB device, using an Android device in bootloader mode as an example.
USB Basics: What a Developer Needs to Know
The USB specification defines a standard mechanism for identifying devices via VID (Vendor ID) and PID (Product ID). These identifiers are assigned to the manufacturer (VID by USB-IF) and the specific product (PID by the manufacturer itself). When a device connects, the host requests descriptors—binary structures in the firmware containing info on the device class, capabilities, and required driver. Key point: most tasks can be handled in user space via standard drivers like Winusb.sys (Windows) or usbfs (Linux), without kernel code.
Important terminology:
- Endpoint: Logical channel for data transfer (Control, Bulk, Interrupt, Isochronous).
- Device Descriptor: Root structure with VID/PID and basic parameters.
- Device Class: Standardized functionality (HID, Mass Storage), but many devices use Vendor Specific Class.
Preparing the Device: Selection and Setup
We'll use an Android device in Bootloader (fastboot) mode as an example. Reasons for this choice:
- Accessibility: Most smartphones support switching to fastboot.
- Simple protocol: Fastboot documentation is open and minimalistic.
- No preinstalled drivers: The OS doesn't hijack the interaction.
Switching to Bootloader mode usually requires holding a button combo during power-on (e.g., volume up + power). To identify the device on Linux, use lsusb:
$ lsusb
Bus 008 Device 014: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
Here, 18d1 is Google's VID, 4ee0 is the PID for fastboot. On Windows, USB Device Tree Viewer provides similar info. Key point: If the OS loads a driver (e.g., shows with a ⚠️ in Device Manager), you'll need to replace it with Winusb.sys using Zadig.
Enumerating the Device: From Manual Analysis to Programmatic Implementation
The enumeration process starts automatically on connection. The OS analyzes VID/PID and device class to select a driver. For user space, we implement this with libusb. Here's code for registering a connection handler:
#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);
}
This code initializes libusb, registers a callback for devices with VID=0x18d1 and PID=0x4ee0, and waits for connection. On success, it prints "Device plugged in!". On Windows, you may need to detach the kernel driver:
libusb_detach_kernel_driver(handle, 0);
Communicating with the Device via Control Endpoint
For basic interaction, use the Control Endpoint (ID 0x00)—the standard channel for service requests. Here's implementing a GET_STATUS request:
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);
A successful response (e.g., 01 00) decodes per the spec: first byte—power source (1 = battery-powered), second—remote wakeup support (0 = not supported). The Control Endpoint enables other standard requests too (GET_DESCRIPTOR, SET_CONFIGURATION), crucial for fetching descriptors.
Requesting Descriptors: The Key to Understanding the Device
Descriptors are the foundation of USB device interaction. Request the device descriptor via 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
);
The received data matches the libusb_device_descriptor structure with fields like:
bcdUSB: USB specification versionbMaxPacketSize0: Max packet size for Control EndpointidVendor/idProduct: VID and PIDbNumConfigurations: Number of configurations
Analyzing descriptors reveals supported interfaces and endpoints. For a configuration descriptor, change the request type to LIBUSB_DT_CONFIG and specify the config index in wIndex.
Key Points
- User Space Operation: Use libusb instead of kernel code—easier to debug and safer.
- VID/PID as Primary Identifiers: Driver binding relies on them; device class is often uninformative (Vendor Specific Class).
- Control Endpoint as Basic Tool: Use it to fetch descriptors and control the device before setting up other endpoints.
- Descriptors as Source of Truth: Their structure is defined in the USB spec; decoding provides a full picture of device capabilities.
— Editorial Team
No comments yet.