Powrót do strony głównej

Piszemy sterowniki USB z libusb: przewodnik dla deweloperów

Artykuł wyjaśnia tworzenie sterowników USB za pomocą libusb bez pracy z kodem jądra. Omówiono przykład z Androidem w trybie fastboot, analiza VID/PID, zapytania przez Control Endpoint i dekodowanie deskryptorów. Materiał skierowany do deweloperów middle/senior.

Sterowniki USB w 5 minut: od teorii do działającego kodu
Advertisement 728x90

# 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:

Google AdInline article slot
  • 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.

Google AdInline article slot

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:

Google AdInline article slot
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 USB
  • bMaxPacketSize0: Maksymalny rozmiar pakietu dla Control Endpoint
  • idVendor/idProduct: VID i PID
  • bNumConfigurations: 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

Advertisement 728x90

Czytaj dalej