Comment gérer les périphériques USB dans une application Windows — Choisir entre COM virtuel, HID, WinUSB et SDK dédié

· · USB, HID, WinUSB, Communication série, Intégration d'équipements, Développement Windows, C#, Pilotes de périphériques

« Cet appareil se connecte en USB, donc on peut le piloter depuis l’application, non ? » — c’est la première question qui revient dans les consultations sur l’intégration d’équipements. La réponse est « cela dépend de la façon dont il se connecte », et cette simple phrase cache un écart de plusieurs dizaines de fois dans la charge de développement. Même pour un « équipement connecté en USB », selon qu’il apparaît comme un port COM, comme un HID, ou qu’il nécessite un pilote dédié, le code à écrire, la méthode de distribution et le type de problèmes rencontrés sur le terrain sont totalement différents.

Ce qui complique les choses, c’est que cette décision doit être prise avant même de commencer à écrire l’application. Si vous avancez avec un « ça a marché en installant le SDK pour voir », cela vous retombe dessus plus tard sous la forme de « impossible de faire une build 64 bits », « impossible d’identifier les appareils quand on en connecte deux », ou « le pilote ne s’installe pas sur le poste du client ».

Cet article passe en revue les quatre méthodes pour gérer un périphérique USB depuis une application Windows — port COM virtuel, HID, WinUSB et SDK fourni par le fabricant — en couvrant les critères de choix, les points clés de mise en œuvre, ainsi que la conception commune requise pour les quatre méthodes.

1. La conclusion, d’abord

  • La première chose à vérifier est « où et sous quelle forme l’appareil apparaît dans le Gestionnaire de périphériques ». Ports (COM et LPT), périphériques d’interface utilisateur, contrôleurs de bus USB universel, catégorie propriétaire — c’est là que la méthode se décide presque entièrement (chapitre 2).
  • Un appareil appartenant à une classe standard ne nécessite pas de pilote. Windows intègre en standard des pilotes de classe pour l’audio, le CDC, le HID, le stockage de masse, l’impression, etc., et les appareils correspondants fonctionnent automatiquement. Il est déconseillé qu’un fabricant écrive son propre pilote pour une classe standard.1
  • L’ordre de sélection officiel est « du plus simple au plus complexe ». (1) Si un pilote de classe standard peut être utilisé, n’écrivez rien ; (2) sinon, si l’accès se fait depuis une seule application, utilisez WinUSB ; (3) si plusieurs applications doivent accéder simultanément à l’appareil, utilisez un pilote UMDF ; (4) si cela ne suffit pas non plus, un pilote KMDF — c’est dans cet ordre qu’il faut raisonner.2
  • Le COM virtuel excelle en portabilité et réutilisation du code, mais est le plus faible pour l’identification. Pour un appareil CDC-ACM, usbser.sys se charge automatiquement à partir de Windows 10, et il suffit d’écrire le code avec SerialPort. Mais le numéro de COM n’est pas l’identifiant de l’appareil. Une implémentation qui retrouve le port au moment de l’exécution à partir du VID/PID et du numéro de série est indispensable (chapitre 3).3
  • HID est l’option cachée qui permet une communication bidirectionnelle sans distribuer le moindre pilote. Mais les collections correspondant à une souris, un clavier, un écran tactile ou un stylet sont ouvertes en exclusivité par l’OS, donc inaccessibles. La vitesse est aussi limitée par la bande passante du transfert par interruption (chapitre 4).4
  • WinUSB convient quand il faut de la vitesse via un transfert en bloc, ou pour un protocole propriétaire. L’installation automatique sans INF n’est possible que si le firmware annonce l’ID compatible WINUSB via un descripteur Microsoft OS, et seulement sous Windows 8 ou ultérieur. Pour un appareil existant ou pour cibler Windows 7 et antérieur, un INF personnalisé est en principe nécessaire (chapitre 5).5
  • Le SDK du fabricant ne se « choisit » pas : il s’« impose ». La bitness (version 32 bits ou 64 bits), le modèle de threads, le cycle de vie et les conditions de redistribution sont tous dictés par un tiers, il faut donc recenser dès le départ les contraintes du SDK comme prémisses de la conception de l’application (chapitre 6).
  • Quelle que soit la méthode choisie, quatre points restent à concevoir soi-même : l’identification unique de l’appareil, le suivi des branchements/débranchements, les délais d’expiration et la gestion de l’alimentation. Une application qui néglige ces points finit toujours par « ne pas fonctionner de temps en temps » (chapitre 8).
  • Si vous développez vous-même un pilote en mode noyau, la signature par Microsoft est obligatoire à partir de Windows 10 1607. Y compris le fait que l’ouverture d’un compte Partner Center nécessite un certificat EV, budgétez ce coût de distribution en amont (chapitre 10).6

2. Le postulat de base — du point de vue de Windows, tout dépend du pilote qui a été chargé

Commençons par présenter en un seul schéma l’arbre de décision des quatre méthodes. Il reprend l’ordre de sélection officiel (du plus simple au plus complexe) cité au chapitre 1, mais réorganisé dans l’ordre réel des décisions à prendre.2 Le détail de chaque embranchement correspond aux chapitres 3 à 6.

apparaît comme un port (COM et LPT)apparaît comme un périphérique d'interface utilisateurun pilote du fabricant est chargéaucun des deux / périphérique inconnuimpossiblepossiblesuffisanteinsuffisante (données volumineuses, protocole propriétaire)nonouiVouloir gérer un périphérique USB depuis une applicationSous quelle forme apparaît-il dans le Gestionnaire de périphériques ?(brancher l'appareil réel et vérifier d'abord)Peut-on modifier le firmware de l'appareil ?(conception maison, ou possibilité de le demander au fabricant)La bande passante nécessaire tient-elle dans un transfert par interruption ?(repère : notifications d'état ou commandes de quelques dizaines de Ko/s ou moins)Plusieurs applications doivent-elles accédersimultanément au même appareil ?Méthode A : port COM virtuelchapitre 3Méthode B : HIDchapitre 4Méthode C : WinUSBchapitre 5Méthode D : SDK du fabricantchapitre 6Envisager le développement d'un pilote UMDFsinon KMDF.Coût de distribution au chapitre 10

Figure 1 : l’arbre de décision des quatre méthodes. Le point de départ est toujours « sous quelle forme l’appareil apparaît-il dans le Gestionnaire de périphériques »

Ce schéma comporte deux points clés. Le point de départ est le Gestionnaire de périphériques de l’appareil réel, et non le catalogue produit ; et plus on descend, plus le coût de distribution augmente. S’il est possible de résoudre le problème en haut de l’arbre, c’est la bonne réponse ; ne descendez que lorsqu’il existe une véritable raison de le faire.

Quel que soit l’appareil branché au bout du câble USB, ce que l’application voit se limite à l’interface exposée par le pilote chargé sur cet appareil. Sans bien saisir ce point, la discussion part dans tous les sens.

Lorsqu’on branche un appareil, Windows lit les descripteurs qu’il annonce et détermine le pilote à charger à partir du code de classe et du VID/PID. S’il correspond à une classe standard, le pilote de classe fourni avec Windows se charge automatiquement.1

Code de classe USB-IF Pilote standard Windows Forme visible depuis l’application
Audio (01h) Usbaudio.sys Périphérique audio
CDC (02h, sous-classe 02h) Usbser.sys Port COM
HID (03h) Hidclass.sys / Hidusb.sys Collection HID
Image (06h) Usbscan.sys Périphérique WIA
Printer (07h) Usbprint.sys Imprimante
Mass Storage (08h) Usbstor.sys Lecteur
Video (0Eh) Usbvideo.sys Caméra (UVC)
Vendor Specific (FFh) (aucun) WinUSB recommandé

La dernière ligne est importante. Les appareils propriétaires d’un fabricant se déclarent souvent FFh (Vendor Specific), et dans ce cas la recommandation de Microsoft est WinUSB.1

Il faut aussi connaître la notion de périphérique composite (composite device). Pour un appareil offrant plusieurs fonctions au bout d’un seul câble USB, Usbccgp.sys déploie chaque fonction comme un périphérique distinct. C’est pourquoi « un seul appareil physique apparaît sous forme de trois entrées dans le Gestionnaire de périphériques », et il n’est pas rare de voir une configuration du type « le contrôle passe par CDC (port COM), les notifications d’état par HID ». La méthode se décide non pas par appareil, mais par fonction (interface).

La première chose à faire : examiner l’appareil réel dans le Gestionnaire de périphériques

Avant toute discussion, branchez l’appareil réel et vérifiez les points suivants. Cela prend cinq minutes et change ensuite toutes les décisions.

  1. Dans quelle catégorie du Gestionnaire de périphériques il apparaît, et sous quel nom
  2. Propriétés → onglet Détails → ID matériel (USB\VID_xxxx&PID_yyyy&...)
  3. Idem → ID compatible (voir si USB\Class_02&SubClass_02 ou USB\MS_COMP_WINUSB apparaît)
  4. Idem → fin du chemin d’instance du périphérique (contient-il un numéro de série, ou une valeur générée incluant &)
  5. Onglet Pilote → Fournisseur et fichiers du pilote (Microsoft ou fabricant)

Si USB\MS_COMP_WINUSB apparaît au point 3, l’appareil est conçu comme un périphérique WinUSB.5 Le point 4 servira au paragraphe 8.1 pour déterminer si l’identification unique de l’appareil est possible.

3. Méthode A : le port COM virtuel — le plus simple, et le plus facile à mal utiliser

3.1 Ce qui se passe réellement

Pour un appareil se déclarant de la classe USB CDC (Communications and CDC Control), sous-classe 02h (ACM), le Usbser.sys standard de Windows se charge automatiquement, sans distribution d’INF. Il suffit de configurer la classe 02 et la sous-classe 02 dans le descripteur de périphérique : l’ID compatible USB\Class_02&SubClass_02 fait alors correspondre le Usbser.inf standard.3

Cependant, ce chargement automatique est un comportement propre à Windows 10 et versions ultérieures.1 Si Windows 8.1 et antérieur font aussi partie des cibles, le descripteur seul ne suffit pas : il faut préparer et distribuer un INF référençant le pilote standard (par exemple un INF personnalisé référençant mdmcpq.inf). C’est la cause du phénomène « ça marchait sans rien faire sous Windows 10, mais sur le poste Windows 7 du client, l’appareil apparaît comme périphérique inconnu ».

Une autre voie est le pilote VCP fourni par les fabricants de puces de conversion USB-série, comme FTDI, Silicon Labs ou Prolific. Celui-ci nécessite l’installation d’un pilote, mais comme le fabricant de la puce publie aussi des pilotes signés sur Windows Update, en pratique cela revient presque à « brancher et ça s’installe tout seul ».

Dans les deux cas, ce que voit l’application est un simple port COM. C’est là le plus grand avantage : les actifs, le savoir-faire et les logiciels de terminal de test hérités de l’époque RS-232 restent utilisables tels quels.

3.2 L’implémentation se limite à SerialPort, mais en hérite aussi les pièges

En .NET, c’est System.IO.Ports.SerialPort (à partir de .NET 5, il faut référencer le package System.IO.Ports). Les points de vigilance à l’implémentation ne sont pas spécifiques à l’USB mais concernent la communication série en général ; nous les avons regroupés, framing, délais d’expiration, reconnexion et conception des journaux inclus, dans « Les pièges des applications de communication série ». En particulier, Read(buffer, 0, 16) ne garantit pas de lire exactement 16 octets, et cela reste vrai en passant par USB. Recevez les données comme un flux d’octets, accumulez-les dans un tampon, puis découpez les trames avec un analyseur (parser).

3.3 Ne jamais écrire le numéro de COM dans un fichier de configuration

C’est la cause numéro un des incidents sur le terrain avec la méthode du COM virtuel.

  • Le numéro de COM n’est qu’un numéro attribué par Windows sur ce PC précis ; ce n’est pas un identifiant de l’appareil
  • Changer le port USB utilisé peut changer ce numéro
  • En connectant deux appareils du même modèle, le numéro seul ne permet pas de savoir lequel est lequel
  • Il est courant que « COM3 est déjà utilisé » fasse sauter des numéros et qu’on se retrouve avec COM13 ou COM27

La bonne implémentation consiste à retrouver le numéro de COM au moment de l’exécution à partir du VID/PID (et si possible du numéro de série). On peut l’obtenir via l’énumération PnP.

# Lister tous les ports COM avec leur ID matériel (regarder sans filtrer d'abord est le geste prudent)
Get-CimInstance Win32_PnPEntity |
  Where-Object { $_.PNPClass -eq 'Ports' } |
  Select-Object Name, PNPDeviceID |
  Format-List

# Exemple de sortie :
# Name        : Périphérique série USB (COM5)          ← CDC-ACM (usbser.sys)
# PNPDeviceID : USB\VID_2341&PID_0043\85436323631351D0E1C1
#                    ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
#                    VID/PID            ID d'instance du périphérique
#
# Name        : USB Serial Port (COM7)               ← pilote VCP de FTDI
# PNPDeviceID : FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000
#               ^^^^^^^ l'énumérateur est FTDIBUS, pas USB

Filtrer ici avec quelque chose comme PNPDeviceID -like 'USB\*' provoque un accident. Comme le montre l’exemple ci-dessus, le port COM créé par le pilote VCP de FTDI commence par FTDIBUS\ et ne correspond à aucune entrée avec un filtre USB\. D’autres fabricants de puces, comme Silicon Labs, peuvent aussi avoir leurs propres énumérateurs.

Une implémentation sûre est l’une des deux suivantes.

  • Récupérer toutes les entrées de la classe Ports, puis extraire VID_xxxx/PID_yyyy (ou VID_xxxx+PID_yyyy) contenus dans PNPDeviceID avec une expression régulière — robuste, car indépendant du nom de l’énumérateur
  • Mettre les noms d’énumérateurs sur une liste blanche explicite (USB\, FTDIBUS\, etc.) — suffisant si l’appareil cible est fixe

Dans tous les cas, branchez votre appareil cible réel, exécutez cette commande et vérifiez visuellement avec quel PNPDeviceID il apparaît, avant d’écrire le filtre. Le nom de l’énumérateur dépend de la combinaison appareil/pilote, on ne peut pas le décider sur le papier.

Depuis C#, on peut lancer la même requête avec ManagementObjectSearcher de System.Management, ou construire un sélecteur AQS avec Windows.Devices.SerialCommunication.SerialDevice.GetDeviceSelectorFromUsbVidPid(vid, pid) puis appeler DeviceInformation.FindAllAsync (attention : GetDeviceSelector ne prend pas de VID/PID — il ne prend aucun argument ou un nom de port, ne les confondez pas). La première option a moins de dépendances et est souvent plus facile à manier dans une application de bureau.

Écrire d’un bout à l’autre le passage de l’énumération à l’ouverture de SerialPort donne ce qui suit. C’est la vérification PowerShell ci-dessus transposée telle quelle en code (.NET 8 ; nécessite de référencer le package System.Management, et est spécifique à Windows).

using System.Globalization;
using System.IO.Ports;
using System.Management;
using System.Text.RegularExpressions;

// Extraire le VID/PID de PNPDeviceID. Attention, le séparateur est soit & soit +
//   USB\VID_2341&PID_0043\...      ← CDC-ACM (usbser.sys)
//   FTDIBUS\VID_0403+PID_6001+...  ← pilote VCP de FTDI
private static readonly Regex VidPidPattern = new(
    @"VID[_+](?<vid>[0-9A-Fa-f]{4})[&+]PID[_+](?<pid>[0-9A-Fa-f]{4})",
    RegexOptions.IgnoreCase | RegexOptions.Compiled);

private static readonly Regex ComNamePattern = new(@"\((?<com>COM\d+)\)", RegexOptions.Compiled);

/// <summary>Énumère les noms de port COM pour un VID/PID donné (indépendant du nom de l'énumérateur).</summary>
static IEnumerable<(string PortName, string PnpDeviceId)> FindComPorts(ushort vid, ushort pid)
{
    // On prend tout avec PNPClass = 'Ports'. Filtrer sur USB\ ferait perdre FTDIBUS\
    using var searcher = new ManagementObjectSearcher(
        "SELECT Name, PNPDeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports'");

    foreach (var device in searcher.Get().Cast<ManagementObject>())
    {
        using (device)
        {
            var name = device["Name"] as string;
            var pnpId = device["PNPDeviceID"] as string;
            if (name is null || pnpId is null) { continue; }

            var ids = VidPidPattern.Match(pnpId);
            if (!ids.Success) { continue; }
            if (ushort.Parse(ids.Groups["vid"].Value, NumberStyles.HexNumber) != vid) { continue; }
            if (ushort.Parse(ids.Groups["pid"].Value, NumberStyles.HexNumber) != pid) { continue; }

            // Extraire le (COM5) de "Périphérique série USB (COM5)"
            var com = ComNamePattern.Match(name);
            if (!com.Success) { continue; }

            yield return (com.Groups["com"].Value, pnpId);
        }
    }
}

// L'endroit où se trouve le numéro de série dans le chemin d'instance du périphérique dépend de l'énumérateur.
//   USB\VID_2341&PID_0043\85436323631351D0E1C1  le dernier élément est le numéro de série
//   FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000    il est intégré dans l'élément du milieu, et
//                                               le dernier élément est \0000. De plus, FTDI
//                                               ajoute un caractère représentant le port à la fin
// Se contenter de tester « correspond-il au dernier élément » ne fait correspondre aucun VCP FTDI.
static bool MatchesSerial(string pnpDeviceId, string serial)
{
    foreach (var part in pnpDeviceId.Split('\\'))
    {
        if (part.Equals(serial, StringComparison.OrdinalIgnoreCase)) { return true; }

        // Dernier champ du format FTDI VID_xxxx+PID_xxxx+<numéro de série><caractère de port>
        var fields = part.Split('+');
        if (fields.Length < 3) { continue; }
        var tail = fields[fields.Length - 1];
        if (tail.Equals(serial, StringComparison.OrdinalIgnoreCase)) { return true; }
        if (tail.Length == serial.Length + 1 &&
            tail.StartsWith(serial, StringComparison.OrdinalIgnoreCase)) { return true; }
    }
    return false;
}

// Côté appelant : ce que l'on ouvre est « le résultat de l'identification », pas le numéro de COM d'un fichier de configuration
var candidates = FindComPorts(0x2341, 0x0043).ToList();
if (candidates.Count == 0) { throw new InvalidOperationException("L'appareil cible est introuvable."); }

// Quel que soit le nombre trouvé, toujours filtrer par numéro de série. Même s'il n'y en a qu'un
// seul, rien ne garantit qu'il s'agit du bon numéro d'unité (le bon numéro peut être absent et
// seul un autre numéro être branché). L'ordre d'énumération WMI ne garantit pas l'identité de
// l'appareil, donc saisir directement candidates[0] provoque l'accident « on pilotait l'unité voisine ».
var wanted = config.DeviceSerial;   // On reçoit « quelle unité » via la configuration ou un argument
candidates = candidates.Where(c => MatchesSerial(c.PnpDeviceId, wanted)).ToList();

if (candidates.Count != 1)
{
    throw new InvalidOperationException(
        $"Impossible d'identifier une unité unique avec le numéro de série '{wanted}' ({candidates.Count} correspondance(s)).");
}

using var port = new SerialPort(candidates[0].PortName, 115200)
{
    ReadTimeout = 1000,
    WriteTimeout = 1000,
};
port.Open();

Il y a quatre points clés. (1) Récupérer toutes les entrées avec PNPClass = 'Ports' puis filtrer par VID/PID (ne pas filtrer sur USB\), (2) retrouver à chaque fois le nom de port depuis cette énumération (ne pas écrire COM3 dans un fichier de configuration), (3) toujours appliquer le filtrage par numéro de série, pas seulement « quand plusieurs sont trouvés », (4) ne pas rendre la recherche du numéro de série dépendante de l’énumérateur (la façon de construire la clé est au paragraphe 8.1).

Se tromper sur (3) ou (4) produit le même symptôme : « on pilotait l’unité voisine ». (3) est un piège car, quand un seul appareil est trouvé, on a l’impression qu’aucune vérification n’est nécessaire. Si l’unité voulue est absente et que seule une autre unité est branchée, il y a bien un seul candidat, mais ce n’est pas le bon appareil. (4) : en se basant uniquement sur USB\ et en décidant a priori que « le numéro de série est le dernier élément », plus aucune correspondance ne se produit pour un format comme FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000, où il est intégré dans l’élément du milieu. Dans le code ci-dessus, ces deux points sont confinés dans MatchesSerial.

Notez que si l’on peut en faire une clé, conserver l’intégralité du chemin d’instance du périphérique dans la configuration est plus fiable encore (paragraphe 8.1) : cela évite même d’avoir à extraire le numéro de série.

Extraire le numéro du (COM5) en fin de Name peut sembler peu élégant, mais c’est en pratique le moyen qui fonctionne le plus sûrement. Pour le faire rigoureusement, lisez la valeur PortName sous la clé de registre du périphérique.

3.4 Les handles se corrompent au débranchement

Avec l’USB-série, débrancher le câble fait disparaître le port lui-même. Débrancher pendant que SerialPort est ouvert, avec une exception levée depuis le thread de réception interne qui fait tomber toute l’application, est un accident classique bien connu. Il est plus sûr d’établir l’ordre suivant : à réception de la notification PnP de retrait, fermer d’abord le port avec Close (paragraphe 8.2).

La reconnexion ne se résume pas à « relancer Open() ». Concevez-la comme une recréation complète de la session, couvrant l’invalidation de l’ancienne session, l’échec définitif des requêtes en cours, l’arrêt du lecteur/écrivain, la réouverture après un délai de recul (backoff), et la ré-exécution de la séquence d’initialisation de l’appareil.

3.5 Cas adaptés et inadaptés

Adapté Inadapté
Actifs existants en protocole série Débit élevé via une puce de conversion USB-UART
Équipements et instruments à commandes texte avec réponse Contrôle exigeant en faible latence
Vouloir diagnostiquer sur le terrain avec un logiciel de terminal Connexion simultanée de nombreux appareils identiques (l’identification devient lourde)
Développeurs sans connaissance des pilotes Nouveau développement où l’on peut définir soi-même le protocole

Concernant le débit, ne généralisez pas en disant « c’est du COM virtuel, donc c’est lent ». Dans une configuration passant par une puce de conversion USB-UART comme celles de FTDI, le débit en bauds de l’UART en aval constitue le plafond (921,6 kbps donne environ 92 Ko/s). En revanche, sur un appareil USB natif où un microcontrôleur implémente directement CDC-ACM, l’interface de données utilise le transfert en bloc USB, sans contrainte liée à l’UART, et une connexion en haute vitesse peut atteindre plusieurs Mo/s. Cependant, dans cette plage, le surcoût des couches Usbser.sys et SerialPort commence à se faire sentir, donc si la bande passante nécessaire dépasse quelques centaines de Ko/s, mesurez sur l’appareil réel avant de choisir la méthode. Décider « il faut de la vitesse, donc WinUSB » sans avoir mesuré revient à s’encombrer d’une distribution de pilote inutile.

4. Méthode B : HID — une communication bidirectionnelle sans distribuer le moindre pilote

4.1 HID n’est pas réservé aux périphériques de saisie

HID évoque spontanément la souris et le clavier, mais sur le plan de la spécification, c’est un protocole générique permettant d’échanger dans les deux sens des suites d’octets arbitraires (rapports). Lecteurs de codes-barres, lecteurs de cartes, serrures électroniques, unités de mesure, onduleurs, boîtiers d’E/S propriétaires — si des appareils qui « ne veulent pas distribuer de pilote mais veulent échanger des données propriétaires » se déclarent HID, c’est parce que Hidclass.sys et Hidusb.sys sont intégrés en standard à Windows et que cela fonctionne sans distribuer le moindre INF ni le moindre pilote.1

L’unité que Windows manipule pour le HID est la collection de niveau supérieur (top-level collection, TLC). Un même périphérique physique peut posséder plusieurs TLC, auquel cas chacune apparaît comme une interface de périphérique distincte.4

4.2 Les HID accessibles et les HID inaccessibles

C’est la contrainte la plus importante. Windows ouvre certaines TLC en mode exclusif, afin d’empêcher d’autres applications d’intercepter l’état global des entrées ; c’est le Raw Input Manager (RIM) qui ouvre ces périphériques en exclusivité.4

Usage Page / Usage Usage Mode d’accès
0x0001 / 0x0001-0x0002 Souris Exclusif
0x0001 / 0x0004-0x0005 Manette de jeu Partagé
0x0001 / 0x0006-0x0007 Clavier, pavé numérique Exclusif
0x000C / 0x0001 Contrôle grand public Partagé
0x000D / 0x0001-0x0002 Stylet Exclusif
0x000D / 0x0004-0x0005 Écran tactile, pavé tactile de précision Exclusif
0x0020 / divers Capteur Partagé
0x008C / 0x0002 Scanner de codes-barres Partagé (l’obtention des données décodées est exclusive)

Autrement dit, on ne peut pas récupérer directement les données d’un lecteur de codes-barres en mode émulation clavier via les API HID. Il est ouvert en exclusivité en tant que clavier. Pour ce type d’appareil, si l’on souhaite recevoir « des données plutôt que des frappes clavier », la bonne approche consiste à basculer, via la configuration de l’appareil lui-même, vers un mode TLC défini par le fournisseur ou vers un mode CDC.

Notez que même pour un périphérique ouvert en exclusivité, si l’on ouvre le handle sans demander de droits de lecture/écriture, on peut toujours obtenir les attributs et les chaînes via la famille HidD_GetXxx.4 Cela suffit pour l’usage « je veux seulement vérifier si l’appareil est connecté ».

4.3 Implémentation — une erreur sur la longueur du rapport entraîne toujours un échec

La procédure côté application en mode utilisateur est fixe. Trouver la collection HID avec SetupDi*, l’ouvrir avec CreateFile, obtenir les informations avec HidD_*, lire et écrire les rapports avec ReadFile/WriteFile, et interpréter les rapports avec HidP_* — c’est tout.7

// Énumération des périphériques HID et récupération de la longueur des rapports (déclarations P/Invoke, extrait)
[DllImport("hid.dll")]
static extern void HidD_GetHidGuid(out Guid hidGuid);

[DllImport("hid.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_GetAttributes(SafeFileHandle device, ref HIDD_ATTRIBUTES attributes);

[DllImport("hid.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_GetPreparsedData(SafeFileHandle device, out IntPtr preparsedData);

[DllImport("hid.dll")]
static extern int HidP_GetCaps(IntPtr preparsedData, out HIDP_CAPS capabilities);

// Le tampon renvoyé par GetPreparsedData doit toujours être libéré. L'oublier provoque une fuite côté natif
[DllImport("hid.dll")]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_FreePreparsedData(IntPtr preparsedData);

[StructLayout(LayoutKind.Sequential)]
struct HIDD_ATTRIBUTES
{
    public int Size;              // Toujours définir sizeof(HIDD_ATTRIBUTES)
    public ushort VendorID;
    public ushort ProductID;
    public ushort VersionNumber;
}

// HIDP_CAPS ne doit pas être déclaré avec « seulement les champs utilisés ».
// HidP_GetCaps écrit toute la longueur de la définition native (USHORT×32 = 64 octets),
// donc passer une structure tronquée corrompt la pile au-delà de sa taille
[StructLayout(LayoutKind.Sequential)]
struct HIDP_CAPS
{
    public ushort Usage;
    public ushort UsagePage;
    public ushort InputReportByteLength;    // Longueur de tampon à passer à ReadFile
    public ushort OutputReportByteLength;   // Longueur de tampon à passer à WriteFile
    public ushort FeatureReportByteLength;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
    public ushort[] Reserved;               // Zone réservée. Ne peut pas être omise

    public ushort NumberLinkCollectionNodes;
    public ushort NumberInputButtonCaps;
    public ushort NumberInputValueCaps;
    public ushort NumberInputDataIndices;
    public ushort NumberOutputButtonCaps;
    public ushort NumberOutputValueCaps;
    public ushort NumberOutputDataIndices;
    public ushort NumberFeatureButtonCaps;
    public ushort NumberFeatureValueCaps;
    public ushort NumberFeatureDataIndices;
}

L’accident classique dans la déclaration de structures consiste à « n’écrire que les champs utilisés et se contenter d’un commentaire pour le reste ». HidP_GetCaps écrit toute la longueur définie nativement (USHORT×32 = 64 octets), donc si l’on passe une structure ne contenant que les cinq premiers champs (10 octets), l’écriture déborde de 54 octets au-delà de la zone allouée par le marshaller. Avec de la chance, cela produit une AccessViolationException ; sans chance, cela corrompt silencieusement d’autres variables. Déclarez les structures P/Invoke avec la même taille et le même agencement que la définition native, y compris les champs inutilisés.8

Le tampon renvoyé par HidD_GetPreparsedData étant alloué côté natif, il faut toujours le libérer avec HidD_FreePreparsedData une fois l’utilisation terminée. Dans une application qui réénumère tous les périphériques HID à chaque branchement/débranchement, oublier cela consomme silencieusement de la mémoire en continu. Entourez l’appel d’un try/finally, ou encapsulez-le dans une classe dérivée de SafeHandle pour empêcher structurellement toute fuite.

const int HIDP_STATUS_SUCCESS = 0x00110000;

// Toujours vérifier la valeur de retour. Si l'appareil a été débranché juste après l'énumération, FALSE est renvoyé
if (!HidD_GetPreparsedData(handle, out IntPtr preparsed))
{
    return null;   // On saute cet appareil. preparsed est invalide, ne pas y toucher
}

try
{
    if (HidP_GetCaps(preparsed, out HIDP_CAPS caps) != HIDP_STATUS_SUCCESS)
    {
        return null;
    }
    // Ce n'est qu'ici que caps.InputReportByteLength et les autres champs deviennent valides
}
finally
{
    HidD_FreePreparsedData(preparsed);   // Ne libérer que si l'acquisition a réussi
}

N’ignorez pas la valeur de retour de HidD_GetPreparsedData. Si l’appareil est débranché entre l’énumération et l’ouverture du handle, FALSE est renvoyé et preparsed n’est pas un pointeur valide. Le passer ensuite à HidP_GetCaps puis à HidD_FreePreparsedData revient à déterminer la longueur du rapport à partir d’un caps au contenu inconnu. Plus le site branche et débranche souvent, plus cette situation de concurrence se produit, donc n’entrez dans le try que si l’acquisition a réussi, et vérifiez aussi la valeur de retour de HidP_GetCaps (est-elle HIDP_STATUS_SUCCESS).

L’échec d’implémentation le plus fréquent concerne la gestion de la longueur des rapports.

  • Le tampon passé à ReadFile doit faire exactement InputReportByteLength. Trop court, cela échoue ; trop long, cela n’est pas non plus traité correctement
  • Le premier octet du tampon est l’ID de rapport. Si la conception de l’appareil n’utilise pas d’ID de rapport, il contient 0. Les données réelles commencent à l’octet 1
  • De même, le tampon de WriteFile fait exactement OutputReportByteLength, avec l’ID de rapport en tête

Dans neuf cas sur dix, « j’ai envoyé mais l’appareil ne réagit pas » vient soit d’un décalage d’un octet dû à l’ID de rapport, soit d’une longueur de tampon incorrecte. Quand la documentation de l’appareil indique « la commande fait 8 octets » et que OutputReportByteLength vaut 9, cela signifie 9 octets en incluant l’ID de rapport.

Outre WriteFile, il existe aussi HidD_SetOutputReport comme voie d’envoi des rapports de sortie, et l’usage de chacun est fixé officiellement.9

Usage À utiliser
Envoyer des rapports de sortie en continu WriteFile (c’est la base)
Définir l’état courant de la collection HidD_SetOutputReport
Envoyer un rapport de fonctionnalité (Feature) HidD_SetFeature

Il faut prêter attention au fait que la documentation officielle avertit que « certains appareils ne prennent pas en charge HidD_SetOutputReport et peuvent cesser de répondre si on l’utilise ».9 Autrement dit, basculer vers HidD_SetOutputReport parce que « WriteFile ne fonctionne pas » n’est pas une alternative sûre à coup sûr. Choisissez après avoir vérifié lequel des deux la fiche technique de l’appareil et les exemples du fabricant utilisent, et si vous basculez, vérifiez sur l’appareil réel que les réponses ne s’arrêtent pas.

Si le seul objectif est d’énumérer les appareils, ouvrez-les avec dwDesiredAccess à 0 dans CreateFile. Cela permet d’énumérer même les appareils ouverts en exclusivité, et d’obtenir le VID/PID avec HidD_GetAttributes et le numéro de série avec HidD_GetSerialNumberString.

Si vous préférez ne pas écrire de P/Invoke brut en C#, utiliser une bibliothèque comme HidSharp est aussi une option. Cependant, il faudra de toute façon comprendre la gestion de la longueur des rapports et des ID de rapport, donc passer une première fois par la forme ci-dessus accélère les investigations ultérieures. Dans une application packagée, Windows.Devices.HumanInterfaceDevice.HidDevice est également utilisable, mais nécessite de déclarer DeviceCapability dans le manifeste.10

4.4 Le plafond de vitesse

HID utilise le transfert par interruption. Pour un appareil USB 2.0 en pleine vitesse (full-speed, 12 Mbps), la longueur maximale de paquet d’un point de terminaison d’interruption est de 64 octets, et l’intervalle de scrutation est la valeur déclarée par le firmware dans la plage 1 à 255 ms. En haute vitesse (high-speed, 480 Mbps), c’est au maximum 1024 octets, avec un intervalle exprimé en unités de 125 µs.11

Autrement dit, pour un appareil HID en pleine vitesse scrutant à 1 ms avec 64 octets, on obtient environ 64 Ko/s même en théorie. Choisir HID pour un usage qui ne rentre pas dans ce cadre — images, formes d’onde, extraction massive de journaux — est une décision sans retour possible ensuite. À l’inverse, pour des réponses à des commandes ou des notifications d’état de quelques dizaines d’octets, la bande passante est largement suffisante.

5. Méthode C : WinUSB — piloter directement un protocole propriétaire

5.1 Positionnement

Winusb.sys est un pilote USB générique fourni par Microsoft ; en le chargeant comme pilote de fonction, on peut lire et écrire directement sur les points de terminaison via les fonctions exposées par Winusb.dll en mode utilisateur. C’est un mécanisme permettant de gérer un protocole propriétaire sans écrire de pilote.2

Les conditions officielles pour adopter WinUSB sont claires.2

  • Une seule application accède à l’appareil
  • L’appareil possède des points de terminaison en bloc, par interruption ou isochrones (l’isochrone nécessite Windows 8.1 ou ultérieur)
  • La cible est Windows XP SP2 ou ultérieur

À l’inverse, WinUSB ne peut pas être utilisé pour un appareil nécessitant un accès simultané depuis plusieurs applications. C’est le domaine du pilote UMDF.

Fonctionnalité WinUSB UMDF KMDF
Accès simultané de plusieurs applications Non Oui Oui
Transferts en bloc, par interruption et de contrôle Oui Oui Oui
Transfert isochrone Oui (8.1 et ultérieur) Non Oui
Empilement de pilotes filtres Non Non Oui
Suspension sélective Oui Oui Oui

5.2 Les conditions dans lesquelles « pas besoin d’INF » est vrai

C’est le point le plus souvent mal compris dans les explications sur WinUSB. Winusb.sys ne se charge automatiquement sans INF que si le firmware de l’appareil porte des descripteurs Microsoft OS et annonce WINUSB comme ID compatible.5

De plus, cette correspondance automatique ne fonctionne que sous Windows 8 et ultérieur. Le Winusb.inf standard n’a pris en charge l’ID compatible USB\MS_COMP_WINUSB qu’à partir de Windows 8 ; avant cela, un INF personnalisé spécifiant l’ID matériel était obligatoire.5 Comme indiqué au paragraphe 5.1, WinUSB lui-même fonctionne dès Windows XP SP2, mais « fonctionner depuis XP » et « s’installer sans INF » sont deux choses différentes. Si Windows 7 ou antérieur fait aussi partie des cibles, planifiez la distribution d’un INF même si les descripteurs OS sont implémentés (sous Windows 7 et antérieur, la correspondance peut malgré tout se faire si une version mise à jour de Winusb.inf est arrivée via Windows Update, mais on ne peut pas baser un plan de distribution là-dessus).

Concrètement, l’appareil doit implémenter ce qui suit. Notez qu’il existe deux lignées, la version 1.0 (WCID) et la version 2.0.

  • Descripteurs Microsoft OS 1.0 (toutes versions confondues)
    1. Posséder un descripteur de chaîne OS à l’index de chaîne 0xEE, qui renvoie un code fournisseur
    2. Définir compatibleID à WINUSB dans le descripteur de fonctionnalité OS d’ID compatible étendu (par fonction, pour un périphérique composite)
  • Descripteurs Microsoft OS 2.0 (Windows 8.1 et ultérieur)
    1. Annoncer l’emplacement du jeu de descripteurs via le descripteur de fonctionnalité de plateforme du descripteur BOS. Le descripteur de chaîne 0xEE n’est pas utilisé
    2. Dans ce jeu de descripteurs, placer un descripteur de fonctionnalité d’ID compatible annonçant WINUSB. Le simple fait d’indiquer via BOS « un jeu existe » ne suffit pas à faire sélectionner Winusb.sys. Ce qui décide de la liaison est, comme en 1.0, l’ID compatible

    Cette version a été conçue pour résoudre les limites et les problèmes de fiabilité de la 1.0 ; pour un firmware de conception nouvelle, c’est le premier choix12

L’enregistrement du GUID d’interface de périphérique joue un rôle différent de ce qui précède. C’est le GUID par lequel l’application trouve l’appareil, alors que ce qui décide si Winusb.sys peut se lier est l’ID compatible. Le GUID est du côté de la découverte (discovery). Cela dit, sans GUID personnalisé enregistré, il est difficile de construire la recherche d’appareil côté application, donc en pratique on les implémente ensemble.

Il faut ici faire attention au fait que le nom de propriété dans le registre existe sous deux formes, singulier et pluriel.13

Nom Type Cas d’usage
DeviceInterfaceGUID Chaîne (REG_SZ) Le descripteur de propriétés étendues Microsoft OS 1.0 spécifie ce nom avec un wPropertyNameLength de 40 octets5
DeviceInterfaceGUIDs Chaîne multiple (REG_MULTI_SZ) Forme standard des INF personnalisés. Les exemples de Microsoft utilisent aussi le pluriel : HKR,,DeviceInterfaceGUIDs,0x10000,"{...}"13

Quand la documentation officielle décrit le comportement de Winusb.sys, elle utilise le pluriel : « il lit la clé de registre DeviceInterfaceGUIDs et enregistre l’interface de périphérique avec le GUID qui y est spécifié ».13 La procédure d’ajout manuel dans le registre indique également les deux options : placer sous Device Parameters soit la chaîne DeviceInterfaceGUID, soit la chaîne multiple DeviceInterfaceGUIDs.13

Si vous utilisez le descripteur de fonctionnalité de propriétés de registre OS 2.0, vérifiez le nom de propriété et le type de données dans les spécifications (en pratique, le pluriel accompagné de REG_MULTI_SZ est le plus courant). Reporter tel quel le singulier de la 1.0 sur la voie de la 2.0 peut aboutir à écrire à un endroit que Windows ne consulte pas, produisant l’état « la liaison fonctionne mais l’application ne trouve pas l’appareil ».

La méthode de vérification fiable consiste à brancher l’appareil, puis à consulter le registre. En vérifiant que HKLM\SYSTEM\CurrentControlSet\Enum\USB\<ID matériel>\<ID d'instance>\Device Parameters contient le nom, le type et la valeur voulus, on connaît les faits quelle que soit l’interprétation des descripteurs.

De plus, lors de l’écriture d’un INF, utilisez la classe d’installation USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}). La classe USB est explicitement réservée aux contrôleurs hôtes, aux concentrateurs (hubs) et aux périphériques composites ; l’utiliser pour un appareil propriétaire est documenté comme entraînant des problèmes de fiabilité et de performance.5

Si l’on ne peut pas toucher le firmware d’un appareil existant, il faudra préparer et distribuer soi-même un INF personnalisé spécifiant l’ID matériel. À ce stade, on obtient une configuration où « l’installeur installe un pilote », et la question de la distribution (chapitre 10) se pose. Même un INF qui ne contient pas un seul octet de .sys propre et se contente de référencer le winusb.sys de Microsoft ne s’installera pas sur un Windows en production sans catalogue signé. Ce n’est donc pas « il suffit d’écrire un INF », alors lisez le chapitre 10 avant d’estimer la charge de travail.

Remplacer le pilote par WinUSB avec un outil comme Zadig pendant le développement est une technique de vérification valable, mais c’est une opération qui retire le pilote du fabricant. N’en faites pas votre méthode de distribution en production : cela empêcherait d’autres applications d’utiliser le même appareil.

5.3 Points clés de l’implémentation

// Initialisation de WinUSB (gestion des erreurs omise)
HANDLE h = CreateFile(devicePath,
                      GENERIC_READ | GENERIC_WRITE,
                      FILE_SHARE_READ | FILE_SHARE_WRITE,
                      NULL, OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // l'asynchrone est quasi obligatoire
                      NULL);

WINUSB_INTERFACE_HANDLE usb;
WinUsb_Initialize(h, &usb);

// Toujours définir un délai d'expiration sur les lectures (par défaut, attente indéfinie)
ULONG timeoutMs = 1000;
WinUsb_SetPipePolicy(usb, bulkInPipeId, PIPE_TRANSFER_TIMEOUT,
                     sizeof(timeoutMs), &timeoutMs);

// En asynchrone, passer NULL pour LengthTransferred et récupérer la longueur transférée après la fin.
// Toujours vérifier la valeur de retour. FALSE avec autre chose que ERROR_IO_PENDING signifie que
// l'échec est déjà acquis à ce stade et qu'aucune opération n'est en attente
BOOL started = WinUsb_ReadPipe(usb, bulkInPipeId, buffer, bufferLength, NULL, &overlapped);
if (!started && GetLastError() == ERROR_IO_PENDING) {
    started = TRUE;   // En cours. On attend la fin plus bas
}

ULONG transferred = 0;
BOOL  collected   = FALSE;   // Le résultat de l'OVERLAPPED a-t-il été récupéré (succès ou non) ?
BOOL  ok          = FALSE;   // La lecture a-t-elle réussi ?

if (started) {
    // Que l'opération se soit terminée de façon synchrone ou via ERROR_IO_PENDING, le résultat est récupéré ici.
    // Vérifier ici aussi la valeur de retour. Timeout, annulation ou débranchement renvoient FALSE,
    // et dans ce cas transferred n'est pas « la longueur effectivement transférée »
    ok = WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE);
    collected = TRUE;
    if (!ok) {
        ReportError(GetLastError());   // À récupérer immédiatement : un seul appel API entre les deux l'écraserait
    }
} else {
    ReportError(GetLastError());   // Débranchement, mauvais ID de pipe, handle déjà invalide, etc.
}

if (ok) {
    Consume(buffer, transferred);   // transferred n'est valide que si l'on arrive ici
}

// --- Nettoyage. Ce code est traversé à chaque branchement/débranchement ; toute fuite s'accumule à chaque reconnexion ---
// Dans une application réelle, la lecture ci-dessus tourne en boucle, donc on arrive ici dans un état où
// « une requête pas encore récupérée peut subsister »
CancelIoEx(h, NULL);                                  // On arrête l'E/S en cours,
if (started && !collected) {                          // seulement si elle n'a pas encore été récupérée
    WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE); // on récupère d'abord la fin de l'opération,
}
WinUsb_Free(usb);                                     // on libère le handle d'interface,
CloseHandle(h);                                       // puis on ferme le handle de fichier

Sept points comptent sur le terrain.

  • Ouvrir avec FILE_FLAG_OVERLAPPED et fonctionner en asynchrone. Avec une E/S synchrone, dès que l’appareil devient silencieux, tout le thread se fige
  • Toujours définir PIPE_TRANSFER_TIMEOUT. Par défaut, les lectures ne reviennent jamais
  • Vérifier la valeur de retour de WinUsb_ReadPipe avant d’attendre. Quand FALSE est renvoyé et que GetLastError() retourne autre chose que ERROR_IO_PENDING, la requête elle-même n’a pas été acceptée. Cela arrive couramment juste après un débranchement, avec un mauvais ID de pipe, ou un handle déjà invalide. Dans ce cas, il n’existe aucune opération en attente, donc appeler quand même WinUsb_GetOverlappedResult revient à attendre l’achèvement d’un transfert qui n’a jamais commencé. Le code d’erreur d’origine est alors écrasé et disparaît ; il ne reste plus que « terminé avec 0 octet » ou une tout autre erreur. La cause disparaît avant même que l’investigation ne commence. C’est pour cela que le code ci-dessus conserve started : le nettoyage applique la même condition pour la récupération14
  • Vérifier aussi la valeur de retour de WinUsb_GetOverlappedResult. Même après que la requête a été acceptée, cette fonction renvoie FALSE en cas de timeout (le PIPE_TRANSFER_TIMEOUT défini plus haut), de CancelIoEx, ou de débranchement en cours de transfert. Dans ce cas, transferred n’est pas « la longueur effectivement transférée ». L’utiliser sans vérifier la valeur de retour fait que le 0 d’initialisation redescend tel quel en aval comme « un paquet vide a été reçu », et une erreur qui aurait dû provoquer la recréation de la session est étouffée. Le symptôme qui en résulte est en plus « un appareil qui, de temps en temps, ne renvoie rien », ce qui allonge considérablement le temps avant d’en trouver la cause. Si FALSE est renvoyé, récupérez GetLastError() sur-le-champ (un seul appel d’une autre API entre les deux suffit à l’écraser)
  • En asynchrone, ne pas passer de pointeur pour LengthTransferred. La documentation officielle précise que « si Overlapped est non NULL, LengthTransferred peut être NULL », et que si l’on passe une valeur non NULL, la valeur au moment où WinUsb_ReadPipe revient n’a aucun sens tant que l’opération n’est pas terminée. Récupérez la longueur transférée avec WinUsb_GetOverlappedResult.14 Passer l’adresse d’une variable locale n’est pas seulement dénué de sens : si cette variable sort de portée par conception, cela devient un pointeur pendouillant (dangling pointer)
  • Toujours associer WinUsb_Free et CloseHandle. Chaque appel réussi à WinUsb_Initialize alloue un handle d’interface. Si, comme au paragraphe 8.2, la session est recréée à chaque branchement/débranchement, toute libération oubliée s’accumule à chaque reconnexion. Regroupez le nettoyage en un seul endroit, de façon à ce qu’il soit atteint non seulement sur le chemin de succès, mais aussi sur les chemins qui échouent en cours d’initialisation (par exemple WinUsb_Initialize réussit mais la configuration des pipes échoue). L’ordre est : « arrêter l’E/S en cours avec CancelIoEx → récupérer la fin avec WinUsb_GetOverlappedResultWinUsb_FreeCloseHandle ». Fermer le handle avant de récupérer la fin de l’opération libère un tampon que le noyau touche encore
  • Empêcher le lancement multiple de l’application. WinUSB ne prend pas en charge l’accès simultané par plusieurs applications ; incluez donc dans les spécifications une protection contre le double lancement (un mutex nommé, par exemple)

Pour l’utiliser depuis C#, le backend Windows de libusb étant implémenté au-dessus de WinUSB, un wrapper comme LibUsbDotNet est une option. Dans une application packagée, Windows.Devices.Usb.UsbDevice est également utilisable, mais avec une restriction explicite : il est impossible d’accéder aux classes de périphériques Audio, HID, Image, Printer, Mass Storage, Smart Card, Audio/Video et Wireless Controller.15

6. Méthode D : SDK fourni par le fabricant et pilotes dédiés — on ne choisit pas, on l’accepte

Caméras industrielles, instruments de mesure, périphériques POS, authentification par empreinte digitale ou par veine, cartes d’E/S dédiées — pour ces appareils, le fabricant fournit le pilote et le SDK ensemble, et il n’existe en pratique aucune autre façon de les utiliser. La méthode D est moins un choix qu’une condition déjà fixée au moment où l’appareil a été sélectionné.

C’est précisément pour cela que recenser les contraintes du SDK dès la phase de sélection de l’appareil constitue en soi la conception. Voici les points à vérifier.

Point à vérifier Ce qui arrive si on le néglige
Prise en charge du 32 bits et du 64 bits Une application 64 bits ne peut pas appeler une DLL réservée au 32 bits ; il faut alors séparer les processus
Forme de l’API (DLL C / COM / .NET) Cela change la manière d’appeler et la conception du marshalling. Avec COM s’ajoutent des contraintes de modèle de threads
Contraintes de threads (STA obligatoire, thread des rappels) Blocage du thread d’interface, voire interblocage
Éléments redistribuables et conditions de distribution Impossible de les inclure dans l’installeur ; une installation manuelle chez le client devient nécessaire
État de signature du pilote fourni Impossible à installer sur les nouvelles builds de Windows 11 ou sur les PC d’équipement
OS pris en charge et durée de maintenance Une mise à jour de l’OS oblige à refaire toute l’application
Possibilité de connexions simultanées multiples et méthode d’identification Tout s’effondre dès qu’on connecte un deuxième appareil
Disponibilité du code source de l’application de démonstration Le coût d’investigation d’un comportement non documenté explose

La bitness (largeur de bits) en première ligne du tableau désigne le fait que la DLL du SDK est compilée en version 32 bits ou 64 bits. Ce point a un impact direct en pratique parce qu’un processus Windows ne peut pas mélanger du code 32 bits et 64 bits au sein d’un même processus. Un SDK qui ne fournit qu’une DLL réservée au 32 bits ne peut pas être appelé directement depuis une application compilée en x64 (cela produit une BadImageFormatException ou un échec de LoadLibrary). Pour contourner cela, il faut soit compiler toute l’application en x86, soit repousser la partie qui appelle le SDK dans un processus séparé en 32 bits, relié par communication interprocessus. Ces deux options sont des décisions qui touchent à la structure de l’application, et doivent donc être connues dès la phase de sélection de l’appareil.

Côté implémentation, la meilleure défense consiste à ne pas disperser le SDK directement dans toute l’application. Confinez les appels au SDK derrière une couche d’abstraction fine unique (une interface), et écrivez le corps de l’application contre cette abstraction. Ainsi, l’impact d’un changement de référence d’appareil, d’une montée de version majeure du SDK ou d’un changement de fabricant se limite à un seul endroit, et il devient possible d’écrire des tests unitaires sans matériel.

Si l’on doit utiliser un SDK réservé au 32 bits depuis une application 64 bits, la solution classique consiste à le repousser dans un processus séparé relié par communication interprocessus. Via COM, « Exemple pratique d’un pont COM pour appeler une DLL 64 bits depuis une application 32 bits » applique la même idée dans le sens inverse, et la façon d’appeler une DLL native est traitée dans « Appeler une DLL native depuis C# : wrapper C++/CLI contre P/Invoke ». Pour la gestion du cycle de vie des processus enfants, voir « Gestion sûre des processus enfants ».

7. Tableau de décision des quatre méthodes

Critère COM virtuel HID WinUSB SDK du fabricant
Distribution de pilote Non nécessaire (CDC) / standard (VCP) Non nécessaire Non nécessaire sous condition, INF souvent requis Nécessaire
Difficulté d’implémentation Faible Moyenne Moyenne à élevée Selon le SDK (très variable)
Débit Faible à moyen (dépend de l’appareil, à mesurer) Faible Élevé Élevé
Latence Moyenne Moyenne (dépend de l’intervalle de scrutation) Faible Faible
Utilisation simultanée par plusieurs applications Non (port exclusif) Oui (pour les TLC partagées) Non Selon le SDK
Identification unique de l’appareil Implémentation nécessaire (le numéro de COM ne convient pas) Possible via VID/PID/numéro de série Possible via le chemin du périphérique Selon le SDK
Facilité de diagnostic sur le terrain Élevée (logiciel de terminal) Moyenne Faible Faible
Dépendance au firmware de l’appareil Faible Faible Forte (descripteurs OS) Totale
Usage adapté Équipements et instruments de type commande-réponse Notifications d’état, petites commandes Données volumineuses, protocole propriétaire Caméras, instruments, appareils dédiés

L’ordre des décisions est le suivant.

  1. L’appareil apparaît-il déjà comme un port COM / HID / classe standard ? → Si oui, utilisez cela
  2. Non, mais le firmware peut être modifié → décidez entre HID (petites données) et WinUSB (données volumineuses) selon la bande passante
  3. Le firmware ne peut pas être modifié, mais un SDK du fabricant existe → utilisez le SDK, en effectuant d’abord les vérifications du chapitre 6
  4. Aucun de ces cas ne s’applique et un accès simultané par plusieurs applications est requis → envisagez le développement d’un pilote UMDF2

8. Quatre choses à concevoir soi-même, quelle que soit la méthode

Même une fois la méthode choisie, il reste quatre choses à construire soi-même. Une application d’intégration d’équipement qui « ne fonctionne pas de temps en temps » a presque toujours l’un de ces quatre points manquant.

8.1 Identification unique de l’appareil — s’accrocher à un identifiant, pas à un numéro

Ce que l’on peut écrire dans la configuration est le VID/PID + le numéro de série, ou bien le chemin d’interface de périphérique. Le numéro de COM ou l’ordre d’affichage dans le Gestionnaire de périphériques ne sont pas des identifiants.

La présence ou l’absence d’un numéro de série se voit dans le dernier élément du chemin d’instance du périphérique.

USB\VID_2341&PID_0043\85436323631351D0E1C1   ← l'appareil annonce un numéro de série (invariant même en cas de déplacement)
USB\VID_0403&PID_6001\5&1a2b3c4d&0&2         ← il n'en annonce pas (valeur générée par Windows à partir de l'emplacement de connexion)

Pour un périphérique composite, le VID/PID + le numéro de série ne suffisent pas. Comme indiqué au chapitre 2, un même appareil peut avoir plusieurs fonctions, auquel cas chaque fonction partage le même VID, le même PID et le même numéro de série. Pour un appareil avec « deux CDC, un pour le contrôle et un pour la maintenance » ou « deux collections de niveau supérieur HID », on trouve plusieurs candidats partageant les mêmes valeurs pour ce triplet, et lequel on ouvre relève du hasard. Incluez dans la clé un élément qui distingue la fonction.

USB\VID_1234&PID_5678&MI_00\7&2a3b4c5d&0&0000   ← fonction 0 (par exemple le CDC de contrôle)
USB\VID_1234&PID_5678&MI_02\7&2a3b4c5d&0&0002   ← fonction 2 (par exemple le CDC de maintenance)
       même VID/PID, même périphérique parent ↑ seul MI_xx (numéro d'interface USB) diffère

En pratique, la clé se construit de l’une des façons suivantes.

  • Inclure le numéro d’interface USB (MI_xx) — la méthode standard pour distinguer les fonctions CDC/WinUSB d’un périphérique composite
  • Pour le HID, combiner avec Usage Page + Usage — c’est ainsi que l’on distingue plusieurs collections de niveau supérieur sur un même appareil (UsagePage/Usage de HidP_GetCaps)
  • Conserver tel quel le chemin d’interface de périphérique — la chaîne obtenue par énumération est unique par fonction, en faire la clé est donc l’option la plus fiable

Si vous êtes en mesure de définir vous-même les spécifications de l’appareil, une conception de firmware qui attribue un numéro d’interface distinct à chaque fonction et annonce toujours un numéro de série simplifie radicalement la logique d’identification côté logiciel.

Le second contient &, et sa valeur change si l’on branche sur un autre port.

Cependant, n’appliquez pas ce test aux nœuds enfants d’un périphérique composite. Sur un périphérique composite, la fin de l’ID d’instance des PDO enfants énumérés en tant que fonctions CDC ou collections HID est une valeur générée par Usbccgp.sys, et elle contient & même quand le périphérique physique annonce bien un numéro de série. Se fier uniquement au chemin de l’enfant pour conclure « pas de numéro de série » serait une erreur. Le numéro de série se trouve du côté du nœud USB parent ; remontez donc jusqu’au parent avec CM_Get_Parent (ou DEVPKEY_Device_Parent) avant de regarder son ID d’instance.

USB\VID_1234&PID_5678\SN0001234           ← parent (périphérique physique). Le numéro de série est ici
 └ USB\VID_1234&PID_5678&MI_00\7&2a3b…    ← enfant. Valeur générée par Usbccgp, contient donc & (ne pas l'utiliser pour le test)

Si, dans une exploitation où plusieurs unités identiques sont connectées, on choisit un appareil sans numéro de série, le seul moyen d’identification restant est « à quel port USB il est branché ». Dans ce cas, il faut concevoir jusqu’à fixer les ports du concentrateur et apposer des étiquettes comme procédure d’exploitation — c’est une partie que la technique ne peut pas résoudre, il faut donc s’en rendre compte dès la phase de sélection de l’appareil.

8.2 Suivre les branchements/débranchements — ne pas faire de scrutation (polling)

On voit souvent des implémentations qui réénumèrent toutes les secondes via un Timer, mais Windows dispose d’un mécanisme de notification.

  • Windows 8 et ultérieur : enregistrez CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE (détection d’arrivée/de suppression) et CM_NOTIFY_FILTER_TYPE_DEVICEHANDLE (détection de la disparition d’un appareil dont on tient un handle ouvert) via CM_Register_Notification16
  • Si Windows 7 et antérieur font aussi partie des cibles : enregistrez DBT_DEVTYP_DEVICEINTERFACE via RegisterDeviceNotification et traitez WM_DEVICECHANGE17

Il y a deux précautions incontournables dans l’implémentation.16

  • CM_Register_Notification ne notifie pas les « interfaces déjà existantes au moment de l’enregistrement ». Enregistrez-vous d’abord, puis énumérez l’existant avec CM_Get_Device_Interface_List. Inverser l’ordre fait passer à travers les mailles les appareils branchés dans l’intervalle
  • En contrepartie, concevez en partant du principe que des doublons apparaîtront. Une interface activée après l’enregistrement mais avant l’énumération apparaît à la fois dans la notification d’arrivée et dans la liste. Faire transiter les deux directement dans le traitement d’arrivée crée deux sessions pour le même appareil, ce qui fait échouer la deuxième ouverture exclusive ou écrase un état déjà établi. Intégrez toujours une déduplication en conservant un ensemble indexé par le chemin d’interface de périphérique et en ignorant les chemins déjà connus (cet ensemble est vidé lors du retrait)
  • N’exécutez rien de bloquant dans le rappel (callback). Renvoyez tout traitement impliquant des E/S vers un autre thread. Attendre ici bloque tout le traitement des événements PnP

Dans une application packagée ou une configuration où les API WinRT sont disponibles, DeviceWatcher permet d’écrire la même chose de façon concise.

// Surveiller les périphériques série pour un VID/PID donné (WinRT)
string selector = SerialDevice.GetDeviceSelectorFromUsbVidPid(0x2341, 0x0043);
DeviceWatcher watcher = DeviceInformation.CreateWatcher(selector);

watcher.Added   += (s, info) => OnDeviceArrived(info.Id);
watcher.Removed += (s, info) => OnDeviceRemoved(info.Id);
watcher.Start();

Dans une application de bureau classique (WinForms / WPF) où WinRT n’est pas disponible, on utilise l’un des deux schémas suivants.

Schéma 1 : RegisterDeviceNotification + WM_DEVICECHANGE (recommandé)

On enregistre une notification d’interface de périphérique pour le handle de fenêtre, et on la reçoit dans WndProc. Le point d’entrée est le remplacement (override) de WndProc pour WinForms, ou HwndSource.AddHook pour WPF.

// Exemple WinForms. En WPF, écrire le même traitement dans HwndSource.AddHook
const int WM_DEVICECHANGE          = 0x0219;
const int DBT_DEVICEARRIVAL        = 0x8000;
const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
const int DBT_DEVTYP_DEVICEINTERFACE = 0x00000005;
const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;

// GUID de classe d'interface pour les périphériques USB
static readonly Guid GUID_DEVINTERFACE_USB_DEVICE =
    new("A5DCBF10-6530-11D2-901F-00C04FB951ED");

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct DEV_BROADCAST_DEVICEINTERFACE
{
    public int dbcc_size;
    public int dbcc_devicetype;
    public int dbcc_reserved;
    public Guid dbcc_classguid;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
    public char[] dbcc_name;
}

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, IntPtr filter, int flags);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool UnregisterDeviceNotification(IntPtr handle);

private IntPtr _notification = IntPtr.Zero;

protected override void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);

    var filter = new DEV_BROADCAST_DEVICEINTERFACE
    {
        dbcc_size       = Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>(),
        dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
        dbcc_classguid  = GUID_DEVINTERFACE_USB_DEVICE,
        dbcc_name       = new char[1],
    };

    IntPtr buffer = Marshal.AllocHGlobal(filter.dbcc_size);
    int error;
    try
    {
        Marshal.StructureToPtr(filter, buffer, fDeleteOld: false);
        _notification = RegisterDeviceNotification(Handle, buffer, DEVICE_NOTIFY_WINDOW_HANDLE);
        error = Marshal.GetLastWin32Error();   // À récupérer avant FreeHGlobal
    }
    finally
    {
        Marshal.FreeHGlobal(buffer);   // Copié au moment de l'enregistrement, donc on peut libérer ici
    }

    // En cas d'échec, aucune exception n'est levée : la valeur de retour est simplement NULL. Sans cette
    // vérification, on obtient une application où « le démarrage réussit mais WM_DEVICECHANGE n'arrive
    // jamais », et il faut du temps pour comprendre que la cause de l'absence de suivi des branchements
    // est l'enregistrement de la notification
    if (_notification == IntPtr.Zero)
    {
        // Win32Exception vient de System.ComponentModel
        throw new Win32Exception(error, "L'enregistrement de la notification de périphérique a échoué.");
    }

    // Énumérer l'existant après l'enregistrement. Inverser l'ordre fait passer à travers les mailles
    // les appareils branchés dans l'intervalle (la déduplication est indispensable)
    ScanExistingDevices();
}

protected override void WndProc(ref Message m)
{
    if (m.Msg == WM_DEVICECHANGE)
    {
        switch ((int)m.WParam)
        {
            case DBT_DEVICEARRIVAL:
                // On se contente ici de poser un indicateur. L'E/S est renvoyée vers un autre thread
                QueueRescan();
                break;
            case DBT_DEVICEREMOVECOMPLETE:
                QueueRescan();
                break;
        }
    }
    base.WndProc(ref m);
}

protected override void OnHandleDestroyed(EventArgs e)
{
    if (_notification != IntPtr.Zero)
    {
        UnregisterDeviceNotification(_notification);
        _notification = IntPtr.Zero;
    }
    base.OnHandleDestroyed(e);
}

On rencontre aussi des implémentations qui se contentent de surveiller WM_DEVICECHANGE sans enregistrement, mais dans ce cas ce qui arrive est essentiellement DBT_DEVNODES_CHANGED (un événement informant seulement que l’arbre des périphériques a changé), et on ne sait pas quel appareil est concerné. Cela finit par imposer une réénumération complète à chaque fois ; si vous voulez cibler précisément, enregistrez-vous comme ci-dessus.

Schéma 2 : événements de création/suppression d’instance WMI

Pour un service sans fenêtre ou une application console, l’abonnement aux événements WMI est une solution pratique.

using System.Management;

// WITHIN 2 signifie « scruter avec un intervalle de 2 secondes ». Plus la valeur est petite, plus la charge augmente
var arrival = new ManagementEventWatcher(new WqlEventQuery(
    "SELECT * FROM __InstanceCreationEvent WITHIN 2 " +
    "WHERE TargetInstance ISA 'Win32_PnPEntity'"));

var removal = new ManagementEventWatcher(new WqlEventQuery(
    "SELECT * FROM __InstanceDeletionEvent WITHIN 2 " +
    "WHERE TargetInstance ISA 'Win32_PnPEntity'"));

arrival.EventArrived += (s, e) =>
{
    var target = (ManagementBaseObject)e.NewEvent["TargetInstance"];
    var pnpId = target["PNPDeviceID"] as string;   // On détermine ici le VID/PID
    QueueRescan();
};
removal.EventArrived += (s, e) => QueueRescan();

arrival.Start();
removal.Start();

Cette requête WMI reste toutefois une scrutation. Écrire WITHIN 2 introduit un retard de détection pouvant aller jusqu’à 2 secondes, et resserrer l’intervalle augmente la charge WMI. Pour une application devant réagir immédiatement aux branchements, choisissez le schéma 1 ; considérez WMI comme une option pour le cas d’un « service résident où un retard de quelques secondes est acceptable ».

Notez que quel que soit le schéma utilisé, le contenu de QueueRescan est le même. La notification n’est qu’un signal indiquant que « quelque chose a changé » ; ce qui est réellement connecté se vérifie en réénumérant. Écrire un traitement distinct pour chaque type de notification ne fait qu’ajouter des trous de duplication et d’oubli, comme décrit plus loin.

Et l’important est de ne pas faire de la notification PnP le « seul point d’entrée ». Quand le câble est débranché, une E/S en cours peut se terminer avec une erreur de suppression ou d’annulation avant, ou en même temps que, la notification. L’ordre « fermer le handle dès réception de la notification » n’est valable que si la notification arrive en premier, donc une implémentation qui s’appuie uniquement là-dessus conserve le problème du paragraphe 3.4, « débrancher fait planter ».

La bonne approche consiste à avoir deux points d’entrée pour la fin de session.

  • Sur chaque chemin d’achèvement de lecture et d’écriture, traiter les erreurs de suppression (ERROR_DEVICE_NOT_CONNECTED / ERROR_DEVICE_REMOVED / ERROR_GEN_FAILURE, ou l’IOException correspondante en .NET) comme « l’appareil a disparu » et enclencher à partir de là la destruction de la session
  • Traiter la notification PnP comme un signal auxiliaire. Elle est nécessaire pour capter le cas où l’appareil est débranché pendant une attente sans E/S en cours, mais elle ne suffit pas à elle seule

Ce qu’il faut absolument distinguer ici, c’est le cas où « on a annulé soi-même ». L’abandon sur délai d’expiration de réponse, le nettoyage à la fermeture de l’application, une interruption due à une action de l’utilisateur — utiliser CancelIoEx, un CancellationToken ou Dispose dans ces cas produit ERROR_OPERATION_ABORTED, OperationCanceledException ou ObjectDisposedException alors même que le fonctionnement est normal. Juger cela inconditionnellement comme « l’appareil a disparu » construit une boucle particulièrement vicieuse où l’appareil reste connecté, mais on jette la session et on se reconnecte à chaque timeout.

// Déterminer par l'état si « c'est moi qui ai arrêté » ou si « l'appareil a disparu ».
// operationCts est la source de jeton propre à cette lecture/écriture en cours.
// Le timeout de réponse et l'interruption par l'utilisateur annulent via ce jeton
catch (OperationCanceledException ex) when (IsSelfCancelled(ex, operationCts.Token))
{
    // Auto-annulation. La session n'est pas cassée, on ne la détruit donc pas
}
catch (ObjectDisposedException) when (_shutdown.IsCancellationRequested)
{
    // Cas où une E/S en vol est revenue après la fermeture du handle pendant l'arrêt
}
catch (Exception ex) when (IsDeviceGone(ex))
{
    TearDownSession();   // Idempotent. Ne s'exécute pas deux fois même appelé aussi depuis la notification PnP
}

// Déterminer si « on est en train de demander soi-même une annulation » en comparant
// avec le jeton effectivement utilisé pour l'annulation
private bool IsSelfCancelled(OperationCanceledException ex, CancellationToken operation) =>
    ex.CancellationToken == operation ||
    ex.CancellationToken == _shutdown.Token ||
    operation.IsCancellationRequested ||
    _shutdown.IsCancellationRequested;

Se contenter de surveiller l’arrêt de toute l’application ne suffit pas. Le timeout de réponse et l’interruption par action de l’utilisateur annulent via un jeton propre à cette seule opération. Dans ce cas, _shutdown n’est pas déclenché, donc un catch conditionné uniquement sur _shutdown.IsCancellationRequested laisse passer l’exception. L’OperationCanceledException ainsi laissée passer ne correspond pas non plus à IsDeviceGone (il ne faut pas juger une auto-annulation comme « l’appareil a disparu »), et elle traverse donc tout jusqu’en bas, faisant tomber toute la boucle d’E/S avec elle. C’est le genre de panne où le thread de réception meurt à chaque timeout.

Le type d’exception ou le code d’erreur seuls ne suffisent pas comme critère de décision. Ce n’est qu’en le confrontant au jeton côté annulation que l’on peut vraiment distinguer les cas. Autrement dit, si votre code possède un chemin d’auto-annulation, ce fait doit rester visible depuis la couche d’E/S. Écrivez également IsDeviceGone de façon à ne pas juger inconditionnellement OperationCanceledException et ObjectDisposedException comme « l’appareil a disparu ».

Pour que le même nettoyage s’exécute quel que soit le point d’entrée, regroupez la destruction de session en un seul traitement idempotent, de façon à ne pas se casser en cas de double appel (par exemple en posant un indicateur via Interlocked.Exchange pour n’exécuter qu’une seule fois, au premier arrivé). Les exceptions levées par une E/S sur un handle corrompu surviennent souvent depuis un endroit difficile à intercepter — c’est précisément pour cela qu’il faut une conception qui les capture à la source et les relie à une transition d’état.

8.3 Délais d’expiration et reconnexion — « un seul timeout » ne suffit pas

Une E/S sur un appareil USB produit le même symptôme de « ça ne revient pas » qu’il s’agisse d’un débranchement, d’une coupure d’alimentation ou d’un blocage du firmware. Il faut prévoir des délais d’expiration distincts selon leur signification.

Délai d’expiration Portée Ordre de grandeur
Délai d’ouverture Jusqu’à l’ouverture de l’appareil De l’ordre de la seconde
Délai de réponse De l’émission de la commande à la fin de la réponse Pire cas des spécifications de l’appareil × facteur de sécurité
Délai entre octets La suite d’une trame ne vient pas Calculé à partir de la vitesse de communication
Backoff de reconnexion Intervalle d’attente avant réouverture Backoff exponentiel + plafond

Et traitez les délais d’expiration non pas comme une « assurance en cas de lenteur » mais comme une « règle qui fait avancer une transition d’état ». Ce n’est qu’en décidant vers quel état on bascule au moment du timeout, comment faire échouer la requête en cours, et ce qu’on affiche à l’écran, que l’on obtient une vraie conception. La façon de l’afficher à l’écran est traitée dans « Bonnes pratiques pour vérifier et afficher l’état d’un équipement externe » — ne vous contentez pas d’un simple « connexion en cours ».

8.4 Gestion de l’alimentation — la vraie raison derrière « ça réagit lentement alors que rien n’a été débranché »

La suspension sélective (selective suspend) de l’USB est un mécanisme qui fait passer un appareil inactif dans un état de faible consommation. Le réveil prenant du temps, elle est responsable des symptômes du type « seule la première réponse est lente », « après un moment sans activité, la première commande est perdue ».

  • Pour Usbser.sys (COM virtuel), elle est désactivée par défaut ; on l’active et la configure via IdleUsbSelectiveSuspendPolicy dans le registre3
  • Pour WinUSB, on la contrôle via DeviceIdleEnabled, DefaultIdleTimeout, UserSetDeviceIdleEnabled etc. du descripteur de fonctionnalité OS de propriétés étendues (ou de l’INF)5

Sur le terrain, la première chose à vérifier est la case « Autoriser l’ordinateur à éteindre ce périphérique pour économiser de l’énergie » dans les propriétés de l’appareil concerné (et du concentrateur racine USB) dans le Gestionnaire de périphériques. Sur les PC d’équipement, il existe réellement des dysfonctionnements que le simple fait de décocher cette case corrige. En incluant les paramètres d’économie d’énergie des portables, effectuez la vérification avec le même plan d’alimentation qu’en production.

9. Estimation des performances et de la latence

Chiffrer la bande passante et la latence nécessaires dès la phase de sélection de la méthode évite tout retour en arrière.

Type de transfert Méthodes qui l’utilisent Caractéristiques
Transfert de contrôle Toutes les méthodes (usage interne) Adapté à la configuration et aux petites commandes. Pas de garantie de bande passante
Transfert par interruption HID, WinUSB Scrutation périodique. Faible latence mais faible capacité
Transfert en bloc WinUSB, stockage de masse Adapté aux gros volumes. Pas de garantie de bande passante, utilise ce qui est disponible
Transfert isochrone UVC (caméra), UAC (audio), WinUSB (8.1 et ultérieur) Bande passante garantie, sans retransmission

Un point de terminaison d’interruption USB 2.0 offre au maximum 64 octets par paquet en pleine vitesse, avec un intervalle de scrutation de 1 à 255 ms, et au maximum 1024 octets en haute vitesse, avec un intervalle exprimé en unités de 125 µs.11 Si vous choisissez HID, vérifiez que la bande passante nécessaire dispose d’au moins un ordre de grandeur de marge par rapport à ce plafond.

Par ailleurs, Windows étant un OS généraliste, il n’offre aucune garantie de latence. Satisfaire au niveau applicatif une exigence du type « répondre systématiquement avec une périodicité de 10 ms » est irréaliste. Si un contrôle périodique est fondamentalement nécessaire, confinez-le dans le microcontrôleur de l’appareil et concevez le PC pour se limiter aux commandes et à la supervision. Cette frontière est traitée en détail dans « Guide pratique pour approcher le temps réel souple sur un Windows ordinaire ».

10. Distribution et exploitation — le coût change dès l’instant où l’on distribue un pilote

Entre les méthodes « sans pilote » (classe standard, HID, périphérique WinUSB) et les méthodes qui « distribuent un pilote », il existe une rupture non pas en coût de développement, mais en coût de distribution et de maintenance.

  • « Comme je n’écris pas moi-même de .sys, la signature n’est pas nécessaire » est faux. Pour une installation PnP de périphérique, si le fichier catalogue du paquet de pilote n’est pas signé, il n’est pas mis en scène (staged) dans le Driver Store.18 Cette exigence est indépendante du contenu du paquet, donc même un INF qui se contente de référencer le winusb.sys de Microsoft, comme au paragraphe 5.2, nécessite l’étape de génération et de signature d’un catalogue (.cat). Estimer que « écrire un INF et le distribuer suffit à faire fonctionner » mène à ne s’en rendre compte qu’après un refus chez le client, avec le message « le pilote de ce périphérique n’est pas signé ». La signature du catalogue passe soit par une signature de version WHQL, soit par une signature via un certificat de version tiers (SPC).18
  • Signature d’un pilote en mode noyau. À partir de Windows 10 version 1607, un nouveau pilote en mode noyau ne se charge que s’il a été signé par Microsoft via le Dev Portal (Partner Center). L’ouverture d’un compte Partner Center nécessite un certificat EV de signature de code.6 C’est une exigence d’une couche distincte de la signature de catalogue ci-dessus : si un binaire en mode noyau est inclus, il faut satisfaire les deux.
  • Il existe deux voies de signature, dont le champ d’application diffère. La signature HLK tested / dashboard signed, obtenue après un test HLK, est valable de Windows Vista jusqu’à Windows Server inclus, et Microsoft en fait la voie recommandée. L’autre voie, l’attestation signing, dispense du test HLK mais n’est valable qu’à partir de Windows 10 bureau (elle n’est pas acceptée sous Windows 7/8.1 ni sous Windows Server 2016 et ultérieur). De plus, elle ne permet pas de diffuser vers le grand public via Windows Update, et n’accorde pas la certification Windows (Windows Certified). La documentation Microsoft elle-même la positionne comme « à des fins de test uniquement ».19 Utiliser l’attestation signing pour un pilote maison distribué par son propre installeur est en pratique une pratique répandue, mais adoptez-la seulement après avoir consigné dans votre tableau des OS pris en charge le fait que les OS cibles se limitent à Windows 10/11 bureau. Si les PC d’équipement tournent sous Windows Server ou une ancienne LTSC, cette voie n’est d’emblée pas envisageable.
  • Ne comptez pas sur les conditions d’exception. Un pilote à signature croisée fonctionne encore si le démarrage sécurisé est désactivé, ou s’il est signé avec un certificat émis avant le 29 juillet 2015, mais un plan de distribution basé là-dessus s’effondre en quelques années.6
  • Renseignez-vous sur le coût et les délais avant de commencer à écrire du code. Pour distribuer un pilote en mode noyau, l’obtention du certificat EV de signature de code et l’ouverture d’un compte Partner Center sont des étapes préalables incontournables.6 Le montant et le délai variant selon l’autorité de certification, la période et l’état de préparation des informations d’immatriculation de votre société, ne vous fiez pas aux chiffres d’autres entreprises et vérifiez concrètement, sous votre propre raison sociale, ces trois points : (1) le tarif annuel du certificat EV (demandez des devis à plusieurs autorités de certification), (2) le délai de la vérification d’existence de l’organisation exigée pour le certificat EV, (3) le délai d’ouverture du compte Partner Center. Il s’agit là d’un temps de procédure et non de technique ; si vous ne le menez pas en parallèle du calendrier de développement, vous risquez de vous retrouver bloqué avec du code terminé mais impossible à distribuer.
  • Conception de l’installeur. Un installeur incluant un pilote nécessite des droits d’administrateur, et la vérification d’une installation silencieuse devient aussi nécessaire. Le choix de la méthode de distribution elle-même est traité dans « Comment choisir la méthode de distribution d’une application Windows », et la manière de reconnaître les conditions qui exigent des droits d’administrateur dans « Quand les privilèges d’administrateur Windows deviennent-ils nécessaires ».
  • Sur les PC d’équipement, le pilote et les mises à jour de l’OS entrent en concurrence. Si vous installez un pilote de fabricant sur un PC d’équipement en configuration LTSC, décidez conjointement la politique de fixation de la build de l’OS et celle de mise à jour du pilote. « Quelle version de Windows installer sur un PC industriel » peut servir de référence.

En matière de décision de conception, « s’il existe une méthode qui évite de distribuer un pilote, la choisir même si l’implémentation est un peu plus pénible » est presque toujours la bonne réponse. Si HID est discrètement si fort, c’est précisément pour cette seule raison.

11. Pannes courantes et parades

Symptôme Cause fréquente Parade
Fonctionne sur le poste de développement mais pas chez le client Le numéro de COM est codé en dur dans la configuration Le résoudre à l’exécution à partir du VID/PID et du numéro de série (8.1)
Dysfonctionnement en connectant un deuxième appareil L’appareil n’annonce pas de numéro de série Revoir la sélection de l’appareil. À défaut, fixer le port et instaurer un étiquetage opérationnel
Un seul appareil, mais on saisit à chaque fois un interlocuteur différent Périphérique composite dont la clé ne distingue pas les fonctions Inclure dans la clé MI_xx, l’Usage HID, ou le chemin d’interface de périphérique (8.1)
Débrancher le câble fait planter l’application E/S sur un handle corrompu, nettoyage reposant uniquement sur la notification PnP Traiter aussi les erreurs de suppression comme fin de session sur le chemin d’achèvement des E/S (8.2)
Seule la première réponse est lente / des données sont perdues Réveil après suspension sélective Vérifier et désactiver les paramètres de gestion de l’alimentation (8.4)
L’envoi en HID ne provoque aucune réaction de l’appareil Les données sont décalées de la longueur de l’ID de rapport La longueur du tampon doit être exactement OutputReportByteLength, avec l’ID de rapport en tête (4.3)
Aucune donnée ne peut être lue en HID La cible est une TLC ouverte en exclusivité par l’OS (clavier, etc.) Changer le mode de l’appareil. Pour une simple énumération, ouvrir avec des droits d’accès à 0 (4.2)
La lecture WinUSB ne revient jamais PIPE_TRANSFER_TIMEOUT non défini Définir un délai d’expiration via la politique de pipe (5.3)
Reconnexion à chaque timeout Auto-annulation mal jugée comme déconnexion de l’appareil Distinguer en confrontant à l’état du jeton d’annulation, etc. (8.2)
Ralentissement progressif au fil des branchements/débranchements Fuite de WinUsb_Free/CloseHandle Regrouper le nettoyage en un seul endroit, atteint même sur les chemins d’échec (5.3)
Une variable sans rapport est corrompue après un appel P/Invoke Structure déclarée tronquée Déclarer tous les champs avec la même taille et le même agencement que côté natif (4.3)
Le pilote ne s’installe pas chez le client Catalogue non signé (peu importe l’existence d’un .sys maison) Inclure la génération et la signature du .cat dans le plan de distribution (chapitre 10)
Lancer deux instances de l’application fait échouer l’une des deux WinUSB ne permet pas l’accès simultané Empêcher le double lancement, ou centraliser via un service résident
Le SDK ne peut pas être chargé en build 64 bits DLL réservée au 32 bits Séparer dans un processus distinct relié par IPC (chapitre 6)
Impossible de savoir si le contenu de la communication arrive vraiment Aucun moyen d’observation Analyseur de protocole USB, trace équivalente à usbmon, implémentation d’un journal de communication

La dernière ligne est souvent négligée, mais elle est importante. Disposer dès le départ d’un moyen de déterminer « qui est en tort, l’application ou l’appareil » réduit radicalement le temps passé à chercher une cause inconnue. Si la méthode du COM virtuel est si solide sur le terrain, c’est parce qu’elle dispose d’un outil de diagnostic accessible à tous : le logiciel de terminal. Si vous choisissez HID ou WinUSB, construisez vous-même le journal et l’outil en ligne de commande de test qui en tiendront lieu.

12. Résumé

  • La façon de gérer un périphérique USB ne dépend pas de l’appareil lui-même, mais du pilote qui a été chargé dessus. Commencez par examiner l’ID matériel, l’ID compatible et le chemin d’instance du périphérique dans le Gestionnaire de périphériques.
  • L’ordre de sélection officiel va du plus simple au plus complexe : pilote de classe standard → WinUSB (application unique) → UMDF (applications multiples) → KMDF. Écrire son propre pilote est le dernier recours.
  • Le COM virtuel est facile à implémenter et à diagnostiquer sur le terrain, mais le numéro de COM n’est pas un identifiant. Résolvez-le à l’exécution à partir du VID/PID et du numéro de série. Le débit change d’ordre de grandeur selon qu’on passe par une conversion USB-UART ou par un CDC natif ; mesurez plutôt que de trancher a priori.
  • HID est une option solide permettant une communication bidirectionnelle sans distribuer le moindre pilote, mais les collections correspondant à une souris, un clavier, un écran tactile ou un stylet sont ouvertes en exclusivité par l’OS et donc inaccessibles ; la bande passante du transfert par interruption en constitue le plafond.
  • WinUSB convient aux données volumineuses et aux protocoles propriétaires. Mais « pas besoin d’INF » n’est vrai que pour un appareil doté de descripteurs OS utilisé sous Windows 8 ou ultérieur, et l’accès simultané par plusieurs applications n’est pas possible. Pour un nouveau firmware, les descripteurs OS 2.0 sont le premier choix.
  • Le SDK du fabricant n’est pas un choix mais une condition préalable. Recensez la bitness, les contraintes de threads, les conditions de redistribution et la durée de maintenance dès la sélection de l’appareil, et enveloppez le SDK côté application dans une couche d’abstraction fine.
  • Quelle que soit la méthode, concevez vous-même ces quatre points : identification unique, suivi des branchements/débranchements, délais d’expiration à plusieurs niveaux, gestion de l’alimentation. C’est de là que naît le « ça ne marche pas de temps en temps ». Pour un périphérique composite, descendez l’identification jusqu’au niveau de la fonction, et captez la déconnexion à la fois via la notification PnP et via les erreurs d’E/S.
  • Si vous distribuez un paquet de pilote, la signature du catalogue est nécessaire même sans .sys maison. Si un binaire en mode noyau est inclus, une signature Microsoft supplémentaire (et un certificat EV) est requise à partir de la version 1607. L’attestation signing dispense du HLK mais se limite à Windows 10 bureau et ultérieur ; choisissez-la après l’avoir confrontée à votre tableau des OS pris en charge. S’il existe une méthode qui évite de distribuer un pilote, la choisir est presque toujours la bonne réponse en pratique.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC (合同会社小村ソフト) prend en charge la conception de l’intégration entre applications Windows et équipements/instruments connectés en USB, l’encapsulation de SDK existants et leur passage en 64 bits, ainsi que l’investigation des causes et la remise en état des applications d’intégration d’équipements devenues instables lors de branchements, débranchements ou reconnexions.

Références

  1. Microsoft Learn, USB device class drivers included in Windows. Sur la liste des pilotes de classe USB intégrés en standard à Windows (Usbaudio.sys / Usbser.sys / Hidclass.sys et Hidusb.sys / Usbscan.sys / Usbprint.sys / Usbstor.sys / Usbvideo.sys, etc.), sur le fait qu’un fabricant ne devrait pas écrire de pilote pour les classes de périphériques prises en charge, sur la recommandation de WinUSB (Winusb.sys) pour les classes non classifiées incluant Vendor Specific (FFh), sur le fait qu’Usbccgp.sys génère un PDO par fonction pour les périphériques composites, et sur la distinction d’usage entre la classe d’installation USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}) et la classe USB. La même page mentionne aussi, à la ligne CDC (02h), que « sous Windows 10, Usbser.inf charge automatiquement Usbser.sys comme pilote de fonction », ainsi que la voie consistant à traiter la sous-classe 02h (ACM) via un INF personnalisé référençant mdmcpq.inf.  2 3 4 5

  2. Microsoft Learn, Choose a driver model for developing a USB client driver. Sur l’ordre de sélection « commencer par la méthode la plus simple » (pilote de classe standard → WinUSB → UMDF → KMDF) ; sur le fait que WinUSB convient à un accès depuis une seule application, à des points de terminaison en bloc/par interruption/isochrones, et pour cibler Windows XP SP2 et ultérieur ; sur le fait que WinUSB ne peut pas être utilisé pour un accès simultané par plusieurs applications ; et sur le tableau comparatif des fonctionnalités WinUSB / UMDF / KMDF (le transfert isochrone est pris en charge par WinUSB à partir de Windows 8.1, mais pas par UMDF).  2 3 4 5

  3. Microsoft Learn, USB serial driver (Usbser.sys). Sur le fait que définir la classe 02 et la sous-classe 02 dans le descripteur de périphérique fait correspondre le Usbser.inf standard via l’ID compatible (USB\Class_02&SubClass_02), permettant à Usbser.sys de se charger automatiquement sans distribuer d’INF personnalisé ; sur le fait qu’il ne se charge pas automatiquement pour une sous-classe autre que 02 ; sur la possibilité de communiquer avec un périphérique CDC via l’espace de noms Windows.Devices.SerialCommunication ; et sur le fait que la suspension sélective est désactivée par défaut et se configure via IdleUsbSelectiveSuspendPolicy dans le registre.  2 3

  4. Microsoft Learn, HID Architecture. Sur le fait que le pilote de classe HID (hidclass.sys) fait l’abstraction entre le client HID et le transport ; sur la liste des collections de niveau supérieur prises en charge par Windows et leur mode d’accès (souris, clavier, stylet, écran tactile et pavé tactile de précision en exclusif ; manette de jeu, capteur, scanner de codes-barres, etc. en partagé) ; sur le fait que le Raw Input Manager (RIM) ouvre ces périphériques en exclusivité pour des raisons de sécurité ; et sur le fait que même pour un périphérique ouvert en exclusivité, ouvrir le handle sans demander de droits de lecture/écriture permet d’obtenir des informations via HidD_GetXxx 2 3 4

  5. Microsoft Learn, WinUSB Device. Sur le fait qu’un périphérique WinUSB est un périphérique USB dont le firmware annonce WINUSB comme ID compatible via un descripteur de fonctionnalité Microsoft OS, permettant à Winusb.sys de se charger sans INF personnalisé ; sur le fait qu’avant Windows 8, la correspondance automatique par ID compatible n’existait pas et qu’un INF personnalisé était obligatoire (le Winusb.inf standard a pris en charge USB\MS_COMP_WINUSB à partir de Windows 8, un INF mis à jour étant fourni via Windows Update pour les versions antérieures) ; sur le mécanisme du descripteur de chaîne OS à l’index de chaîne 0xEE et du code fournisseur ; sur la définition de compatibleID à WINUSB dans le descripteur d’ID compatible étendu ; sur le fait qu’enregistrer DeviceInterfaceGUID via le descripteur de propriétés étendues permet à l’application de découvrir et manipuler l’appareil ; sur l’usage de la classe d’installation USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}) plutôt que la classe USB ; et sur la configuration de la gestion de l’alimentation via DeviceIdleEnabled / DefaultIdleTimeout / UserSetDeviceIdleEnabled / SystemWakeEnabled 2 3 4 5 6 7

  6. Microsoft Learn, Driver Signing Policy. Sur le fait qu’à partir de Windows 10 version 1607, un nouveau pilote en mode noyau non signé via le Dev Portal ne se charge pas ; sur la nécessité d’un certificat EV de signature de code pour s’inscrire au programme Windows Hardware Dev Center ; et sur les conditions d’exception dans lesquelles un pilote à signature croisée est toléré (mise à niveau vers la 1607, démarrage sécurisé désactivé, certificat final émis avant le 29 juillet 2015).  2 3 4

  7. Microsoft Learn, Opening HID collections. Sur la séquence par laquelle une application en mode utilisateur identifie une collection HID avec les fonctions SetupDi*, l’ouvre avec CreateFile, obtient les données pré-analysées et les informations avec HidD_Xxx, lit les rapports d’entrée avec ReadFile et envoie les rapports de sortie avec WriteFile, puis interprète les rapports avec HidP_Xxx

  8. Microsoft Learn, HIDP_CAPS structure (hidpi.h). Sur la définition complète de la structure (Usage / UsagePage / InputReportByteLength / OutputReportByteLength / FeatureReportByteLength / Reserved[17] / NumberLinkCollectionNodes et les dix membres Number suivants, soit un total de USHORT×32), et sur le fait que chaque longueur de rapport inclut l’octet de l’ID de rapport. 

  9. Microsoft Learn, Sending HID Reports. Sur le fait qu’une application en mode utilisateur doit utiliser WriteFile pour envoyer des rapports de sortie en continu ; que les routines de la famille HidD_SetXxx (HidD_SetOutputReport / HidD_SetFeature) peuvent aussi envoyer des rapports de sortie ou de fonctionnalité, mais que HidD_SetXxx ne devrait être utilisé que pour définir l’état courant de la collection ; et sur l’avertissement selon lequel « certains appareils ne prennent pas en charge HidD_SetOutputReport et l’utilisation de cette routine peut les faire cesser de répondre ». Ainsi que, d’après HidD_SetOutputReport function, sur le fait que ReportBufferLength est déterminé par OutputReportByteLength de HIDP_CAPS, et que le premier octet doit être à 0 si les ID de rapport ne sont pas utilisés.  2

  10. Microsoft Learn, HidDevice Class (Windows.Devices.HumanInterfaceDevice). Sur le fait que HidDevice représente le périphérique correspondant à une collection de niveau supérieur ; sur le déroulement consistant à construire un sélecteur AQS avec GetDeviceSelector à partir de usagePage / usageId / vendorId / productId, puis à l’ouvrir avec FromIdAsync ; et sur le fait qu’une application accédant à un périphérique HID via cette classe doit inclure des données DeviceCapability spécifiques dans le nœud Capabilities du manifeste. 

  11. USB Implementers Forum, Universal Serial Bus Specification Revision 2.0. Sur le fait que la longueur maximale de paquet d’un point de terminaison d’interruption est de 64 octets en pleine vitesse et de 1024 octets en haute vitesse ; que l’intervalle de scrutation (bInterval) s’exprime en 1 à 255 millisecondes en pleine vitesse, et en unités de 125 microsecondes sous la forme 2^(bInterval-1) en haute vitesse (section 9.6.6 Endpoint) ; et sur les caractéristiques de bande passante de chaque type de transfert : contrôle, bloc, interruption et isochrone.  2

  12. Microsoft Learn, Microsoft OS 2.0 Descriptors Specification. Sur le fait que la version 2.0 des descripteurs Microsoft OS a été conçue pour résoudre les limites et les problèmes de fiabilité de la version 1.0, et sur les OS ciblés (Windows 10 et Windows 8.1 Preview). 

  13. Microsoft Learn, WinUSB (Winusb.sys) Installation for Developers. Sur l’ajout, sous la clé Device Parameters du périphérique, d’une entrée chaîne DeviceInterfaceGUID ou d’une entrée chaîne multiple DeviceInterfaceGUIDs pour définir le GUID ; sur le fait que l’AddReg d’un INF personnalisé s’écrit HKR,,DeviceInterfaceGUIDs,0x10000,"{...}" (0x10000 = REG_MULTI_SZ) ; sur les formulations « lorsque Winusb.sys est chargé comme pilote de fonction, il lit la clé de registre DeviceInterfaceGUIDs et représente l’interface de périphérique avec le GUID spécifié » et « à chaque chargement, Winusb.sys enregistre une interface de périphérique avec la classe d’interface de périphérique spécifiée sous la clé DeviceInterfaceGUIDs » ; sur le fait que, côté mode utilisateur, on énumère les interfaces enregistrées avec SetupDiGetClassDevs avant de les passer à WinUsb_Initialize ; et sur la nécessité d’un fichier catalogue signé pour le paquet de pilote.  2 3 4

  14. Microsoft Learn, WinUsb_ReadPipe function (winusb.h). Sur le fait que spécifier Overlapped fait revenir la fonction immédiatement et exécute l’opération de façon asynchrone ; que dans ce cas GetLastError renvoie ERROR_IO_PENDING et que l’on vérifie le succès avec WinUsb_GetOverlappedResult ; qu’en asynchrone (Overlapped non NULL), LengthTransferred peut être défini à NULL ; que même si l’on passe une valeur non NULL pour LengthTransferred, la valeur au moment où la fonction revient est dénuée de sens (meaningless) tant que l’opération overlapped n’est pas terminée, et que le nombre d’octets réellement lus doit être obtenu via WinUsb_GetOverlappedResult ; et que pour un appel synchrone (Overlapped NULL), LengthTransferred doit être non NULL.  2

  15. Microsoft Learn, Windows.Devices.Usb Namespace. Sur le fait que cet espace de noms cible les périphériques WinUSB (ID compatible USB\MS_COMP_WINUSB) gérés par le winusb.sys standard ; sur l’impossibilité d’accéder aux classes de périphériques Audio (0x01) / HID (0x03) / Image (0x06) / Printer (0x07) / Mass Storage (0x08) / Smart Card (0x0B) / Audio/Video (0x10) / Wireless Controller (0xE0) ; sur la nécessité de déclarer la fonctionnalité de périphérique usb dans le manifeste, la spécification de VendorId/ProductId n’étant plus requise à partir de Windows 10 version 1809 ; et sur l’impossibilité générale d’accéder à une pile de périphériques incluant des pilotes filtres supérieurs/inférieurs. 

  16. Microsoft Learn, CM_Register_Notification function (cfgmgr32.h). Sur le fait que cette fonction est disponible à partir de Windows 8, et qu’il faut utiliser RegisterDeviceNotification pour cibler Windows 7 et antérieur ; sur les types de filtre CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE / DEVICEHANDLE / DEVICEINSTANCE ; sur le fait que les événements PnP doivent être traités le plus vite possible et que tout traitement potentiellement bloquant comme une E/S doit s’exécuter de façon asynchrone sur un autre thread ; et sur le fait que cette fonction ne notifie pas les interfaces de périphérique existantes, ce qui impose d’appeler CM_Get_Device_Interface_List après l’enregistrement, les interfaces activées durant cet intervalle apparaissant à la fois dans la notification et dans la liste.  2

  17. Microsoft Learn, RegisterDeviceNotificationW function (winuser.h). Sur le fait qu’il s’agit de la fonction d’enregistrement permettant à une application de recevoir des notifications de périphérique, renvoyant un handle de notification de périphérique en cas de succès et NULL en cas d’échec. Ainsi que, d’après WM_DEVICECHANGE message et DBT_DEVICEARRIVAL event, sur le fait que WM_DEVICECHANGE est diffusé avec wParam égal à DBT_DEVICEARRIVAL quand un périphérique ou un média est inséré et devient disponible. 

  18. Microsoft Learn, PnP Device Installation Signing Requirements. Sur le fait que la mise en scène (staging) d’un paquet de pilote dans le Driver Store nécessite de satisfaire une exigence de signature ; que pour être considéré comme « signé » lors d’une installation PnP, le fichier catalogue du paquet de pilote doit être signé WHQL ou par un certificat de version tiers (SPC, certificat de version commerciale) ; que l’exigence de signature pour charger le binaire d’un pilote en mode noyau est imposée séparément ; que sur les versions 64 bits de Windows, la politique de signature de code en mode noyau exige une signature WHQL ou SPC ; et que certaines éditions, comme Windows 10 en mode S, n’acceptent que les catalogues signés WHQL.  2

  19. Microsoft Learn, Driver Signing Options. Sur le fait qu’un pilote signé dashboard ayant réussi le test HLK fonctionne sur Windows Vista et les OS ultérieurs y compris les éditions Windows Server, et qu’il s’agit de la méthode recommandée puisqu’elle peut signer pour toutes les versions d’OS ; sur le fait que l’attestation signing est positionnée comme étant « à des fins de test uniquement » (for testing purposes only) et ne nécessite pas de test HLK ; sur le fait qu’un pilote signé par attestation ne peut pas être publié vers le grand public via Windows Update ; qu’il n’est valable qu’à partir de Windows 10 bureau ; que cibler une version de Windows antérieure exige la soumission des journaux de test HLK/HCK ; que Windows Server 2016 et ultérieur n’accepte pas les soumissions d’attestation signing et ne charge que les pilotes ayant réussi le HLK ; et que l’attestation signing nécessite un certificat EV, sans que la signature obtenue confère la certification Windows (Windows Certified). 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Je veux utiliser un périphérique USB depuis mon application. Dois-je écrire moi-même un pilote ?
Dans la plupart des cas, non. Les directives officielles de Microsoft précisent explicitement de « commencer par la méthode la plus simple et ne passer à une méthode plus complexe que lorsque c'est nécessaire ». Si l'appareil appartient à une classe USB standard (CDC, HID, stockage de masse, etc.), le pilote de classe standard de Windows se charge automatiquement et aucun pilote n'est nécessaire. S'il n'appartient pas à une classe standard et qu'une seule application y accède, WinUSB (winusb.sys) peut être utilisé tel quel comme pilote de fonction. Ce n'est que lorsque plusieurs applications doivent y accéder simultanément que l'on passe à un pilote UMDF, et si cela ne suffit pas non plus, à un pilote KMDF. Écrire son propre pilote est le dernier recours.
Quel est le plus grand point faible de la méthode du port COM virtuel (USB série) ?
Le fait que le numéro de port COM ne soit pas l'identifiant de l'appareil. Pour un même appareil, changer de port USB peut modifier le numéro de COM, et en connecter plusieurs rend impossible de distinguer lequel est lequel à partir du seul numéro. Une exploitation qui écrit « COM3 » dans un fichier de configuration finira toujours par se casser sur le terrain. En pratique, la bonne approche consiste à retrouver, à l'exécution, la correspondance entre le VID/PID plus le numéro de série et le numéro de COM, par exemple via Win32_PnPEntity, puis à l'ouvrir. De plus, la communication série étant un flux d'octets, les limites de message ne sont pas garanties, ce qui nécessite un analyseur (parser) séparé qui accumule dans un tampon de réception avant d'en extraire les trames.
On dit que la méthode HID est pratique car elle ne nécessite pas de pilote. À quoi faut-il faire attention ?
Il y a trois points. Le premier est la vitesse : HID utilise le transfert par interruption, ce qui ne convient pas au transfert continu de grands volumes de données. Le deuxième est la longueur du rapport : le tampon passé à ReadFile doit faire exactement InputReportByteLength, tel que renvoyé par HidP_GetCaps, et le premier octet est l'ID de rapport. Se tromper sur ce point entraîne le bug classique où l'on ne peut rien lire, ou l'application plante. Le troisième est le contrôle d'exclusivité : les collections de niveau supérieur correspondant à la souris, au clavier, à l'écran tactile et au stylet sont ouvertes en exclusivité par le Raw Input Manager de Windows, donc inaccessibles en lecture/écriture depuis une application. Cependant, en ouvrant le handle sans demander de droits de lecture/écriture, il reste possible d'obtenir des informations via la famille HidD_GetXxx.
Je veux utiliser WinUSB. Puis-je l'appliquer tel quel à un appareil existant ?
Souvent non. winusb.sys ne se charge automatiquement sans fichier INF que si le firmware de l'appareil porte des descripteurs Microsoft OS et annonce WINUSB comme ID compatible — un « périphérique WinUSB » —, et seulement sous Windows 8 ou ultérieur. Comme le Winusb.inf standard n'a pris en charge l'ID compatible qu'à partir de Windows 8, cibler aussi Windows 7 ou antérieur suppose de distribuer un INF. Si l'appareil existant ne correspond pas à un périphérique WinUSB, il faudra aussi préparer et distribuer soi-même un INF personnalisé spécifiant l'ID matériel, puis l'installer. Remplacer le pilote avec un outil comme Zadig pendant le développement est valable comme technique de vérification, mais il s'agit d'une opération qui retire le pilote du fabricant : n'en faites pas votre méthode de distribution en production. Si le firmware peut être modifié, faire ajouter des descripteurs OS est la solution la plus propre, et pour une conception nouvelle, les descripteurs OS 2.0 annoncés via BOS (Windows 8.1 et ultérieur) sont le premier choix.
Comment faire pour que l'application suive les branchements et débranchements d'un périphérique USB ?
Abonnez-vous aux notifications PnP plutôt que de faire de la scrutation (polling). Pour une application de bureau sous Windows 8 ou ultérieur, l'approche standard consiste à spécifier un filtre d'interface de périphérique avec CM_Register_Notification ; si Windows 7 ou antérieur fait aussi partie des cibles, utilisez RegisterDeviceNotification et WM_DEVICECHANGE. Point de vigilance : CM_Register_Notification ne notifie pas les interfaces déjà existantes au moment de l'enregistrement, il faut donc s'enregistrer d'abord, puis énumérer l'existant avec CM_Get_Device_Interface_List (l'ordre inverse fait passer des appareils à travers les mailles). De plus, exécuter dans le rappel (callback) un traitement potentiellement bloquant comme une E/S est dangereux : renvoyez-le vers un autre thread. Au-delà de cela, il est important de ne pas faire de la notification PnP le seul point d'entrée. Une E/S en cours de traitement peut se terminer par une erreur de suppression ou d'annulation avant, ou en même temps que, la notification. Traitez les erreurs de suppression comme une fin de session sur tous les chemins d'achèvement de lecture et d'écriture, et combinez-y la notification PnP comme signal auxiliaire captant les déconnexions survenues pendant l'attente.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog