# Sterowniki USB bez bólu: od teorii do kodu na libusb
Pisanie sterowników dla urządzeń USB często przeraża programistów złożonością niskopoziomowego kodu. Jednak z odpowiednim narzędziem, takim jak libusb, proces staje się porównywalny pod względem złożoności z programowaniem sieciowym za pomocą socketów. W tym artykule omówimy, jak stworzyć sterownik dla urządzenia USB, korzystając z przykładu urządzenia Android w trybie bootloadera.
Podstawy USB: co powinien wiedzieć programista
Specyfikacja USB określa standardowy mechanizm identyfikacji urządzeń za pomocą VID (Vendor ID) i PID (Product ID). Te identyfikatory są przypisywane producentowi (VID — USB-IF) i konkretnemu produktowi (PID — samemu producentowi). Po podłączeniu urządzenia host żąda deskryptorów — binarnych struktur w firmware, zawierających informacje o klasie urządzenia, jego możliwościach i wymaganym sterowniku. Kluczowa kwestia: większość zadań da się rozwiązać w przestrzeni użytkownika za pomocą standardowych sterowników, takich jak Winusb.sys (Windows) czy usbfs (Linux), bez ingerencji w kod jądra.
Ważne jest zrozumienie terminologii:
- Punkt końcowy (Endpoint): Logiczny kanał do przesyłania danych (Control, Bulk, Interrupt, Isochronous).
- Deskryptor urządzenia: Główna struktura z VID/PID i parametrami podstawowymi.
- Klasa urządzenia: Standaryzowana funkcjonalność (HID, Mass Storage), ale wiele urządzeń korzysta z klasy Vendor Specific Class.
Przygotowanie urządzenia: wybór i konfiguracja
Jako przykład weźmiemy urządzenie Android w trybie Bootloader (fastboot). Taki wybór jest uzasadniony:
- Dostępność: większość smartfonów obsługuje wejście w tryb fastboot.
- Prostota protokołu: dokumentacja fastboot jest otwarta i zwięzła.
- Brak preinstalowanych sterowników: system operacyjny nie przejmuje interakcji.
Przełączenie w tryb Bootloader zazwyczaj wymaga przytrzymania kombinacji przycisków podczas uruchamiania (np. przycisk głośności w górę + zasilanie). Do identyfikacji urządzenia w Linux używamy lsusb:
$ lsusb
Bus 008 Device 014: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
Tutaj 18d1 to VID Google, 4ee0 to PID dla fastboot. W Windows podobnych informacji dostarcza USB Device Tree Viewer. Kluczowa sprawa: jeśli system załaduje sterownik (np. w Menedżerze urządzeń pokaże się z ikoną ⚠️), trzeba go zastąpić Winusb.sys za pomocą Zadig.
Enumeracja urządzenia: od analizy ręcznej do implementacji programowej
Proces enumeracji uruchamia się automatycznie po podłączeniu. System analizuje VID/PID i klasę urządzenia, by wybrać sterownik. W przestrzeni użytkownika zaimplementujemy to za pomocą libusb. Przyjrzyjmy się kodowi rejestracji obsługi hotplug:
#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);
}
Ten kod inicjalizuje libusb, rejestruje callback dla urządzeń z VID=0x18d1 i PID=0x4ee0 oraz czeka na podłączenie. Po udanym uruchomieniu wyświetli „Device plugged in!”. Na Windows może być potrzebne odłączenie sterownika jądra:
libusb_detach_kernel_driver(handle, 0);
Komunikacja z urządzeniem przez Control Endpoint
Do podstawowej interakcji używamy Control Endpoint (ID 0x00) — standardowego kanału dla żądań serwisowych. Zaimplementujmy wysłanie żądania 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);
Udana odpowiedź (np. 01 00) dekoduje się według specyfikacji: pierwszy bajt — źródło zasilania (1 = z akumulatora), drugi — obsługa zdalnego budzenia (0 = nieobsługiwane). Control Endpoint pozwala na wysyłanie innych standardowych żądań (GET_DESCRIPTOR, SET_CONFIGURATION), co jest kluczowe do pobrania deskryptorów.
Pobranie deskryptorów: klucz do zrozumienia urządzenia
Deskryptory to podstawa interakcji z urządzeniem USB. Pobierzmy deskryptor urządzenia za pomocą 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), // Typ deskryptora
0x00,
data.data(), data.size(),
1000
);
Otrzymane dane zawierają strukturę libusb_device_descriptor z polami:
bcdUSB: Wersja specyfikacji USBbMaxPacketSize0: Maksymalny rozmiar pakietu dla Control EndpointidVendor/idProduct: VID i PIDbNumConfigurations: Liczba konfiguracji
Analiza deskryptorów pozwala określić obsługiwane interfejsy i punkty końcowe. Do pobrania deskryptora konfiguracji zmień typ żądania na LIBUSB_DT_CONFIG i podaj indeks konfiguracji w wIndex.
Co ważne
- Praca w przestrzeni użytkownika: Używaj libusb zamiast kodu jądra — łatwiej debugować i bezpieczniej.
- VID/PID — główne identyfikatory: Na nich opiera się przypisanie sterownika, klasa urządzenia często jest nieinformatywna (Vendor Specific Class).
- Control Endpoint — podstawowe narzędzie: Przez niego pobiera się deskryptory i zarządza urządzeniem przed konfiguracją innych endpointów.
- Deskryptory — źródło prawdy: Ich struktura jest opisana w specyfikacji USB, dekodowanie daje pełne wyobrażenie o możliwościach urządzenia.
— Editorial Team
Brak komentarzy.