USB без магии: устройство протокола

USB кажется простой магией: воткнул флешку — и она заработала. Но за этим стоит строгий многоуровневый протокол, где каждый бит имеет значение. Если вы разработчик встраиваемых систем, пишете драйверы или просто хотите понять, как на самом деле общаются ваш компьютер и клавиатура — эта статья для вас. Мы разберём архитектуру USB снизу вверх: от электрических сигналов до логических передач, рассмотрим дескрипторы, типы эндпоинтов и механизм enumeration. Никакой магии — только пакеты, состояния и строгая логика.
Основа основ: кто здесь главный?
USB — строгая host-centric шина. Хост (контроллер) единолично инициирует все транзакции. Устройства только отвечают. Это как допрос следователя: задаёт вопросы только хост, устройство лишь даёт показания. Топология — звёздообразная с хабами. Каждое устройство имеет адрес (от 1 до 127), назначаемый в процессе enumeration. Физически данные передаются дифференциальными сигналами (D+ и D-).
Различают четыре типа передачи данных:
- Control transfers — для конфигурирования и управления, обязательны для всех устройств.
- Bulk transfers — для больших объёмов данных без гарантии задержки (флешки, диски).
- Interrupt transfers — для периодических данных с гарантированной задержкой (клавиатуры, мыши).
- Isochronous transfers — для потоковых данных, где важна скорость, а не целостность (аудио, видео).
Важно: USB — host-centric, все транзакции инициируются хостом. Устройство не может само начать передачу, оно ждёт токена от хоста. Это железное правило. Нарушишь — получишь STALL.
Дескрипторы: кто ты, устройство?
При подключении хост запрашивает дескрипторы — структурированные данные, описывающие устройство. Иерархия: дескриптор устройства, дескриптор конфигурации, дескрипторы интерфейсов и эндпоинтов. Пример структуры дескриптора устройства в C (USB 2.0):
struct usb_device_descriptor {
uint8_t bLength; // 18
uint8_t bDescriptorType; // 1 (DEVICE)
uint16_t bcdUSB; // 0x0200 (USB 2.0)
uint8_t bDeviceClass; // 0 (if per interface)
uint8_t bDeviceSubClass; // 0
uint8_t bDeviceProtocol; // 0
uint8_t bMaxPacketSize0; // 64
uint16_t idVendor; // Ваш Vendor ID
uint16_t idProduct; // Ваш Product ID
uint16_t bcdDevice; // (версия)
uint8_t iManufacturer; // 1
uint8_t iProduct; // 2
uint8_t iSerialNumber; // 3
uint8_t bNumConfigurations; // 1
} __attribute__((packed));
Именно благодаря этим квитанциям хост понимает, что за устройство перед ним и какие драйверы загружать. Без правильного idVendor/idProduct вы — аноним. Никакой драйвер вас не подхватит.
Эндпоинты: каналы связи
Каждое устройство может иметь до 16 входных и 16 выходных эндпоинтов (плюс нулевой управляющий). Эндпоинт — логический канал с заданным типом передачи. Для HID-устройств (клавиатуры, мыши) обычно используют эндпоинт типа Interrupt с максимальным размером пакета 8 байт. Дескриптор эндпоинта:
struct usb_endpoint_descriptor {
uint8_t bLength; // 7
uint8_t bDescriptorType; // 5 (ENDPOINT)
uint8_t bEndpointAddress; // 0x81 (IN, endp1)
uint8_t bmAttributes; // 3 (Interrupt)
uint16_t wMaxPacketSize; // 8
uint8_t bInterval; // 10 ms
} __attribute__((packed));
Транзакция USB состоит из трёх фаз: токен (маркер) → данные (опционально) → подтверждение (HS/NAK/STALL). Например, bulk-чтение: хост отправляет IN-токен с адресом устройства, устройство выкладывает данные, хост отвечает ACK. Если устройство занято — отвечает NAK, и хост повторит позже. У Interrupt-передач хост опрашивает эндпоинт с периодичностью, заданной bInterval. Isochronous не имеет фазы подтверждения — данные летят без гарантии.
Enumeration: как устройство получает паспорт
Полный enumeration включает следующие шаги:
- Хост сбрасывает устройство (RESET).
- Устройство отвечает на адрес 0, хост отправляет GET_DESCRIPTOR (DEVICE).
- Хост назначает адрес (SET_ADDRESS).
- Хост запрашивает конфигурацию (GET_DESCRIPTOR CONFIGURATION).
- Хост выбирает конфигурацию (SET_CONFIGURATION).
- Начинаются нормальные передачи.
Личное наблюдение автора. В своей практике я часто видел, как разработчики избегают низкоуровневой работы с USB, полагаясь на готовые библиотеки. Но когда возникает нестандартное устройство или баг в драйвере, понимание протокола — единственный путь к решению. Например, однажды мы потратили неделю, пока не выяснили, что проблема была в неправильном дескрипторе интерфейса, из-за чего хост не выбирал нужную конфигурацию. Без чтения спецификаций USB мы бы искали ошибку в другом месте. Так что осваивайте протокол — это не магия, а инженерия.
Сравнение версий USB: от мыши до монитора
| Версия | Пропускная способность | Макс. ток порта | Длина кабеля | Типичное применение |
|---|---|---|---|---|
| USB 2.0 | 480 Мбит/с | 500 мА | 5 м | Клавиатуры, мыши, флешки |
| USB 3.2 Gen 1 | 5 Гбит/с | 900 мА | 3 м | Внешние диски, высокоскоростные устройства |
| USB 3.2 Gen 2 | 10 Гбит/с | 1.5 А (с PD) | 1 м | SSD, видеокарты внешние |
| USB4 | 40 Гбит/с | 3 А (с PD) | 0.8 м | Мониторы, док-станции |
Резюме от автора. USB — это не магия. Это строгий протокол, где каждый бит на своём месте. Освоив дескрипторы, эндпоинты и фазы транзакций, вы перестанете быть пассажиром и станете механиком. А когда ваше устройство откажется enumerаться, вы не будете паниковать — вы откроете дамп и найдёте ошибку в дескрипторе. Инженерия, а не колдовство.
















