USB для разработчиков: как написать драйвер из пользовательского пространства

USB-драйвер можно написать целиком из userspace — без кода ядра. Endpoint как порты, 4 типа передачи, libusb API и Fastboot-клиент за 50 строк.

Обложка: USB для разработчиков: как написать драйвер из пользовательского пространства

Вам вручили USB-устройство и попросили написать для него драйвер. Звучит страшно? На самом деле это не сложнее, чем написать приложение с сокетами. Не нужно лезть в ядро — всё можно сделать из пользовательского пространства с помощью библиотеки libusb.

Это перевод статьи WerWolv — введение в USB для разработчиков, которые не работали с железом. Концепции объясняются через аналогию с сетевым программированием, а практический пример — реализация протокола Android Fastboot.

Ключевые выводы
  • USB-драйвер можно написать целиком в userspace через libusb — без кода ядра
  • Endpoint в USB — аналог порта в TCP: устройство сообщает, какие «порты» открыты и какой протокол используют
  • 4 типа передачи: Control (конфигурация), Bulk (большие данные), Interrupt (низкая задержка), Isochronous (аудио/видео)
  • USB — master-slave: хост всегда инициирует обмен, устройство только отвечает
  • Практический пример: полноценный Fastboot-клиент на C++ за ~50 строк кода

USB — это как сеть

Ключевая мысль статьи: USB можно понимать через аналогию с сетевым программированием. Endpoint — это порт, на который устройство принимает данные. Дескриптор устройства — аналог DNS, который говорит, кто это и какие «сервисы» доступны. Вам не нужно быть инженером встраиваемых систем, чтобы работать с USB — как не нужно быть сетевым специалистом, чтобы работать с сокетами.

Шаг 1: узнаём, что за устройство

При подключении устройства хост запрашивает у него информацию — это называется enumeration. На Linux это можно увидеть через lsusb:

			$ lsusb
Bus 008 Device 014: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
		

Здесь 18d1 — Vendor ID (Google), 4ee0 — Product ID (загрузчик Pixel). Это уникальные идентификаторы, по которым хост находит нужный драйвер. VID выдаётся организацией USB-IF компаниям за деньги, PID назначает сам производитель.

Более подробный вывод:

			$ lsusb -t
/:  Bus 008.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd/1p, 480M
    |__ Port 002: Dev 014, If 0, Class=Vendor Specific Class, Driver=[none], 480M
		

Driver=[none] означает, что ОС не загрузила драйвер — именно то, что нужно для написания своего.

Шаг 2: общаемся через libusb

Библиотека libusb позволяет общаться с USB-устройствами из пользовательского пространства. Она предоставляет универсальный драйвер и API для отправки запросов — без написания кода ядра.

Пример: обнаружение устройства по VID:PID и получение дескриптора через Control endpoint:

			// Инициализация libusb
libusb_context *context = nullptr;
libusb_init(&context);

// Регистрируем callback на подключение устройства 18d1:4ee0
libusb_hotplug_register_callback(
    context,
    LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
    LIBUSB_HOTPLUG_ENUMERATE,
    0x18d1, 0x4ee0,           // VID и PID
    LIBUSB_HOTPLUG_MATCH_ANY, // Любой USB-класс
    hotplug_callback, nullptr,
    &handle
);
		

Каждое USB-устройство имеет Control endpoint на адресе 0x00 — это стандартизированная «точка входа», через которую хост получает дескрипторы (кто устройство, какие интерфейсы, какие endpoint'ы). Аналогия: это как DNS-запрос перед подключением.

Endpoint'ы: порты USB-устройства

Endpoint'ы — аналог портов на сетевом устройстве. Устройство описывает в дескрипторе, какие endpoint'ы доступны и какой протокол каждый из них использует. Четыре типа:

  • Control — один на устройство, адрес 0x00. Для конфигурации и запроса информации. Решает проблему курицы и яйца: чтобы узнать endpoint'ы, нужен endpoint — и это он
  • Bulk — большие объёмы данных, низкий приоритет. Используется в Mass Storage (флешки), CDC-ACM (Serial over USB), RNDIS (Ethernet over USB)
  • Interrupt — маленькие данные, минимальная задержка. Клавиатуры и мыши опрашиваются 1000+ раз в секунду через HID. Название обманчивое: реальных прерываний нет, хост просто очень часто опрашивает
  • Isochronous — потоковые данные с гарантией тайминга. Аудио и видео, где задержка означает заикание
IN и OUT: USB — master-slave протокол, устройство никогда не говорит первым. IN = хост просит данные «к себе», OUT = хост отправляет данные «от себя». Направление кодируется в старшем бите адреса endpoint'а.

Практика: Fastboot-клиент за 50 строк

Протокол Fastboot (документация) предельно прост: хост отправляет строковую команду, устройство отвечает 4-символьным статусом + данные:

			Host:    "getvar:version"        // запрос версии
Client:  "OKAY0.4"               // ответ: версия 0.4

Host:    "getvar:nonexistant"    // запрос несуществующей переменной
Client:  "OKAY"                  // пустое значение
		

Реализация через libusb — отправляем команду на OUT Bulk endpoint, читаем ответ с IN Bulk endpoint:

			// Отправляем "getvar:version" на OUT endpoint 0x02
std::vector<uint8_t> bytes(64);
std::ranges::copy("getvar:version", bytes.begin());
libusb_bulk_transfer(handle,
    LIBUSB_ENDPOINT_OUT | 0x02,  // EP 2 OUT
    bytes.data(), bytes.size(),
    &num_bytes, 1000);

// Читаем ответ с IN endpoint 0x01
std::ranges::fill(bytes, 0x00);
libusb_bulk_transfer(handle,
    LIBUSB_ENDPOINT_IN | 0x01,   // EP 1 IN
    bytes.data(), bytes.size(),
    &num_bytes, 1000);

// Результат: "OKAY0.4"
		

Это всё. Два вызова libusb — и мы общаемся с загрузчиком Android без единой строки кода ядра.

Частые вопросы
1
Работает ли libusb на Windows?

Да, но с нюансами. Если устройство имеет Microsoft OS Descriptor, Windows автоматически загрузит WinUSB-драйвер. Если нет — придётся использовать утилиту Zadig для установки совместимого драйвера. На Linux и macOS libusb работает из коробки.

2
Нужно ли разбираться в электронике?

Нет. USB абстрагирует физический уровень. Для написания userspace-драйвера достаточно понимать endpoint'ы и типы передачи — всё это логические концепции, не электрические.

3
Зачем писать свой драйвер, если есть готовые?

Кастомные устройства (IoT-сенсоры, программаторы, специализированное оборудование) часто используют Vendor Specific Class и не имеют стандартных драйверов. Userspace-драйверы через libusb — самый быстрый способ начать с ними работать.

Полный текст с исходным кодом: USB for Software Developers, WerWolv.