Retour à l'accueil

Écriture de pilotes USB avec libusb : un guide pour les développeurs

L'article explique la création de pilotes USB via libusb sans travailler avec du code noyau. Considère un exemple avec Android en mode fastboot, analyse VID/PID, requêtes via Control Endpoint et décodage des descripteurs. Matériel orienté vers les développeurs middle/senior.

Pilotes USB en 5 minutes : de la théorie au code fonctionnel
Advertisement 728x90

## Pilotes USB sans douleur : De la théorie au code avec libusb

Rédiger des pilotes pour les périphériques USB effraie souvent les développeurs par la complexité du code de bas niveau. Cependant, avec l'outil adapté comme libusb, le processus devient aussi simple que la programmation réseau avec des sockets. Dans cet article, nous allons parcourir la création d'un pilote pour un périphérique USB, en utilisant un appareil Android en mode bootloader comme exemple.

Bases USB : Ce qu'un développeur doit savoir

La spécification USB définit un mécanisme standard pour identifier les périphériques via VID (identifiant du fournisseur) et PID (identifiant du produit). Ces identifiants sont attribués au fabricant (VID par USB-IF) et au produit spécifique (PID par le fabricant lui-même). Quand un périphérique se connecte, l'hôte demande des descripteurs — structures binaires dans le firmware contenant des infos sur la classe du périphérique, les capacités et le pilote requis. Point clé : la plupart des tâches peuvent être gérées en espace utilisateur via des pilotes standards comme Winusb.sys (Windows) ou usbfs (Linux), sans code noyau.

Terminologie importante :

Google AdInline article slot
  • Point de terminaison : Canal logique pour le transfert de données (Control, Bulk, Interrupt, Isochronous).
  • Descripteur de périphérique : Structure racine avec VID/PID et paramètres de base.
  • Classe de périphérique : Fonctionnalité standardisée (HID, Mass Storage), mais de nombreux périphériques utilisent la classe Spécifique au fournisseur.

Préparation du périphérique : Sélection et configuration

Nous utiliserons un appareil Android en mode Bootloader (fastboot) comme exemple. Raisons de ce choix :

  • Accessibilité : La plupart des smartphones supportent le passage en fastboot.
  • Protocole simple : La documentation fastboot est ouverte et minimaliste.
  • Pas de pilotes préinstallés : L'OS ne s'approprie pas l'interaction.

Passer en mode Bootloader nécessite généralement de maintenir une combinaison de boutons lors de l'allumage (par ex., volume haut + alimentation). Pour identifier le périphérique sous Linux, utilisez lsusb :

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

Ici, 18d1 est le VID de Google, 4ee0 est le PID pour fastboot. Sous Windows, USB Device Tree Viewer fournit des infos similaires. Point clé : Si l'OS charge un pilote (par ex., affiché avec un ⚠️ dans le Gestionnaire de périphériques), vous devrez le remplacer par Winusb.sys en utilisant Zadig.

Google AdInline article slot

Énumération du périphérique : De l'analyse manuelle à l'implémentation programmatique

Le processus d'énumération démarre automatiquement à la connexion. L'OS analyse VID/PID et la classe de périphérique pour sélectionner un pilote. En espace utilisateur, nous l'implémentons avec libusb. Voici du code pour enregistrer un gestionnaire de connexion :

#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);
}

Ce code initialise libusb, enregistre un rappel pour les périphériques avec VID=0x18d1 et PID=0x4ee0, et attend la connexion. En cas de succès, il affiche « Device plugged in ! ». Sous Windows, vous devrez peut-être détacher le pilote noyau :

libusb_detach_kernel_driver(handle, 0);

Communication avec le périphérique via le point de terminaison de contrôle

Pour une interaction de base, utilisez le point de terminaison de contrôle (ID 0x00) — le canal standard pour les requêtes de service. Voici l'implémentation d'une requête 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);

Une réponse réussie (par ex., 01 00) se décode selon la spécification : premier octet — source d'alimentation (1 = alimenté par batterie), second — support du réveil à distance (0 = non supporté). Le point de terminaison de contrôle permet aussi d'autres requêtes standard (GET_DESCRIPTOR, SET_CONFIGURATION), cruciales pour récupérer les descripteurs.

Demande de descripteurs : La clé pour comprendre le périphérique

Les descripteurs sont la base de l'interaction avec un périphérique USB. Demandez le descripteur de périphérique 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
);

Les données reçues correspondent à la structure libusb_device_descriptor avec des champs comme :

  • bcdUSB : version de la spécification USB
  • bMaxPacketSize0 : taille max de paquet pour le point de terminaison de contrôle
  • idVendor/idProduct : VID et PID
  • bNumConfigurations : nombre de configurations

L'analyse des descripteurs révèle les interfaces et points de terminaison supportés. Pour un descripteur de configuration, changez le type de requête en LIBUSB_DT_CONFIG et spécifiez l'index de config dans wIndex.

Points clés

  • Opération en espace utilisateur : Utilisez libusb au lieu du code noyau — plus facile à déboguer et plus sûr.
  • VID/PID comme identifiants principaux : Le liage du pilote repose sur eux ; la classe de périphérique est souvent peu informative (classe Spécifique au fournisseur).
  • Point de terminaison de contrôle comme outil de base : Utilisez-le pour récupérer les descripteurs et contrôler le périphérique avant de configurer d'autres points de terminaison.
  • Descripteurs comme source de vérité : Leur structure est définie dans la spécification USB ; leur décodage donne une vue complète des capacités du périphérique.

— Editorial Team

Advertisement 728x90

Lire ensuite