USB-Geräte aus einer Windows-App ansprechen — Die Wahl zwischen virtual COM, HID, WinUSB und Hersteller-SDKs
· Go Komura · USB, HID, WinUSB, Serielle Kommunikation, Geräteintegration, Windows-Entwicklung, C#, Gerätetreiber
„Dieses Gerät verbindet sich über USB, wir können es also aus der Anwendung heraus ansteuern, oder?“ — das ist die erste Frage, die in Beratungen zur Geräteintegration aufkommt. Die Antwort lautet „das hängt davon ab, wie es sich verbindet“, und in diesem einen Satz verbirgt sich ein Unterschied von einem Vielfachen im Entwicklungsaufwand. Selbst bei demselben „USB-verbundenen Gerät“ ändert sich völlig, ob es als COM-Port erscheint, als HID erscheint oder einen dedizierten Treiber benötigt — und das ändert Ihren Code, Ihren Verteilungsweg und die Art der Probleme, auf die Sie vor Ort stoßen werden.
Das Unangenehme daran ist, dass diese Entscheidung getroffen werden muss, bevor Sie mit dem Schreiben der Anwendung beginnen. Gehen Sie nach dem Motto vor „wir haben das SDK installiert und es hat funktioniert“, holt Sie das später in Form von „wir können keinen 64-Bit-Build erstellen“, „wir können die Geräte nicht mehr unterscheiden, sobald zwei angeschlossen sind“ oder „der Treiber lässt sich auf dem PC des Kunden nicht installieren“ ein.
Dieser Artikel ordnet die vier Wege, USB-Geräte aus einer Windows-Anwendung heraus anzusprechen — virtueller COM-Port, HID, WinUSB und ein vom Hersteller bereitgestelltes SDK — und behandelt die Auswahlkriterien, die entscheidenden Implementierungspunkte sowie die Designarbeit, die allen vier Ansätzen gemeinsam ist.
1. Das Wichtigste zuerst
- Prüfen Sie zuerst, „wo und als was es im Geräte-Manager erscheint“. Anschlüsse (COM & LPT), Human Interface Devices, Universal Serial Bus-Geräte, eine benutzerdefinierte Kategorie — der Ansatz ist damit im Wesentlichen bereits entschieden (Abschnitt 2).
- Geräte, die zu einer Standardklasse gehören, benötigen keinen Treiber. Windows liefert Klassentreiber für Audio, CDC, HID, Massenspeicher, Drucken und mehr, und passende Geräte funktionieren automatisch. Es wird nicht empfohlen, dass Hersteller Treiber für Standardklassen schreiben.1
- Die offizielle Auswahlreihenfolge lautet „das Einfachste zuerst“. (1) Funktioniert ein Standard-Klassentreiber, schreiben Sie keinen eigenen; (2) funktioniert er nicht und nur eine Anwendung greift auf das Gerät zu, WinUSB; (3) greifen mehrere Anwendungen gleichzeitig zu, ein UMDF-Treiber; (4) reicht selbst das nicht, ein KMDF-Treiber — erwägen Sie sie in dieser Reihenfolge.2
- Virtual COM ist am stärksten bei Portabilität und Wiederverwendung und am schwächsten bei der Identifikation. CDC-ACM-Geräte laden unter Windows 10 und höher automatisch usbser.sys, und Sie können alles allein mit
SerialPortschreiben. Aber die COM-Nummer ist nicht die Identität des Geräts. Eine Implementierung, die sie zur Laufzeit aus VID/PID und Seriennummer auflöst, ist zwingend (Abschnitt 3).3 - HID ist der Geheimtipp: bidirektionale Kommunikation ohne jede Treiberverteilung. Aber Collections, die Mäusen, Tastaturen, Touch und Stiften entsprechen, werden exklusiv vom Betriebssystem geöffnet und sind tabu. Die Geschwindigkeit ist außerdem durch die Bandbreite von Interrupt-Transfers begrenzt (Abschnitt 4).4
- WinUSB ist für „Bulk-Transfer, bei dem Geschwindigkeit zählt“ und „proprietäre Protokolle“. Die automatische Installation ohne INF gilt nur, wenn die Firmware über Microsoft-OS-Deskriptoren die kompatible ID
WINUSBmeldet, und nur unter Windows 8 und höher. Bei bestehenden Geräten oder wenn Windows 7 oder älter einbezogen wird, benötigen Sie grundsätzlich eine benutzerdefinierte INF (Abschnitt 5).5 - Ein Hersteller-SDK ist nichts, das Sie „wählen“ — es ist etwas, das Sie „übernehmen“. Bitness, Threading-Modell, Lebensdauer und Weiterverteilungsbedingungen werden alle von den Umständen eines anderen bestimmt. Beginnen Sie damit, die Einschränkungen des SDK als Voraussetzungen für Ihr Anwendungsdesign zu inventarisieren (Abschnitt 6).
- Unabhängig vom Ansatz gestalten Sie diese vier Dinge selbst: eindeutige Geräteidentifikation, Hot-Plug-Handhabung, Timeouts und Energieverwaltung. Eine Anwendung, die diese auslässt, wird zwangsläufig eine, die „manchmal nicht funktioniert“ (Abschnitt 8).
- Schreiben Sie Ihren eigenen Kernelmodus-Treiber, ist die Microsoft-Signierung seit Windows 10 1607 verpflichtend. Kalkulieren Sie sie im Voraus als Verteilungskosten ein, einschließlich der Tatsache, dass die Eröffnung eines Partner-Center-Kontos ein EV-Zertifikat erfordert (Abschnitt 10).6
2. Die grundlegende Voraussetzung — Für Windows dreht sich ein USB-Gerät vollständig um „welcher Treiber geladen hat“
Was auch immer am anderen Ende des USB-Kabels hängt, alles, was die Anwendung sieht, ist die vom auf diesem Gerät geladenen Treiber freigelegte Schnittstelle. Ohne das festzulegen, konvergiert die Diskussion nie.
Wenn Sie ein Gerät anschließen, liest Windows die vom Gerät deklarierten Deskriptoren und entscheidet anhand des Klassencodes und der VID/PID, welcher Treiber geladen wird. Passt es zu einer Standardklasse, lädt der mit Windows ausgelieferte Klassentreiber automatisch.1
| USB-IF-Klassencode | Integrierter Windows-Treiber | Wie es der Anwendung erscheint |
|---|---|---|
| Audio (01h) | Usbaudio.sys | Audiogerät |
| CDC (02h, Unterklasse 02h) | Usbser.sys | COM-Port |
| HID (03h) | Hidclass.sys / Hidusb.sys | HID-Collection |
| Bild (06h) | Usbscan.sys | WIA-Gerät |
| Drucker (07h) | Usbprint.sys | Drucker |
| Massenspeicher (08h) | Usbstor.sys | Laufwerk |
| Video (0Eh) | Usbvideo.sys | Kamera (UVC) |
| Herstellerspezifisch (FFh) | (keiner) | WinUSB empfohlen |
Die letzte Zeile ist entscheidend. Herstellerspezifische Geräte deklarieren häufig FFh (Vendor Specific), und in diesem Fall lautet Microsofts Empfehlung WinUSB.1
Das andere, was zu wissen wichtig ist, ist das zusammengesetzte Gerät (Composite Device). Ein Gerät mit mehreren Funktionen am Ende eines einzelnen USB-Kabels wird von Usbccgp.sys in ein separates Gerät pro Funktion expandiert. Deshalb erscheinen „drei Einträge im Geräte-Manager, obwohl es ein einziges Stück Hardware ist“, und es ist nicht ungewöhnlich, ein Gerät zu finden, das etwa als „Steuerung über CDC (COM-Port), Statusmeldung über HID“ konfiguriert ist. Der Ansatz wird nicht pro Gerät entschieden, sondern pro Funktion (Schnittstelle).
Das Erste, was zu tun ist: Das echte Gerät im Geräte-Manager betrachten
Bevor jede Diskussion beginnt, schließen Sie das tatsächliche Gerät an und prüfen Sie Folgendes. Es dauert fünf Minuten und verändert jede nachfolgende Beurteilung.
- In welcher Geräte-Manager-Kategorie es erscheint und unter welchem Namen
- Eigenschaften → Registerkarte Details → Hardware-IDs (
USB\VID_xxxx&PID_yyyy&...) - Ebenso → Kompatible IDs (ob
USB\Class_02&SubClass_02oderUSB\MS_COMP_WINUSBsichtbar ist) - Ebenso → das Ende des Geräteinstanzpfads (ob eine Seriennummer enthalten ist oder ein generierter Wert mit
&) - Registerkarte Treiber → Treiberanbieter und Treiberdateien (von Microsoft oder vom Hersteller)
Erscheint USB\MS_COMP_WINUSB unter Punkt 3, wurde das Gerät als WinUSB-Gerät entworfen.5 Punkt 4 ist das Material, das in Abschnitt 8.1 verwendet wird, um zu beurteilen, ob eine eindeutige Geräteidentifikation möglich ist.
3. Ansatz A: Virtueller COM-Port — Am einfachsten und am leichtesten falsch zu machen
3.1 Was tatsächlich passiert
Geräte, die die USB-CDC-Klasse (Communications and CDC Control) mit Unterklasse 02h (ACM) deklarieren, laden das integrierte Usbser.sys automatisch, ohne dass eine INF verteilt werden muss. Allein das Setzen von Klasse 02 und Unterklasse 02 im Gerätedeskriptor lässt die Standard-Usbser.inf über die kompatible ID USB\Class_02&SubClass_02 zuordnen.3
Allerdings ist dieses automatische Laden Verhalten von Windows 10 und höher.1 Wird auch Windows 8.1 oder älter einbezogen, reichen Deskriptoren allein nicht: Sie müssen eine INF vorbereiten und verteilen, die auf den integrierten Treiber verweist (etwa eine benutzerdefinierte INF, die auf mdmcpq.inf verweist). „Es hat unter Windows 10 ohne jeden Aufwand funktioniert, aber es erscheint auf dem Windows-7-Rechner des Kunden als unbekanntes Gerät“ kommt genau daher.
Der andere Weg sind die VCP-Treiber, die von Herstellern von USB-zu-Seriell-Brückenchips wie FTDI, Silicon Labs und Prolific bereitgestellt werden. Diese erfordern zwar eine Treiberinstallation, aber da die Chiphersteller signierte Treiber auch über Windows Update veröffentlichen, kommt es in der Praxis nahe an „anschließen und es installiert sich“.
So oder so sieht die Anwendung nur einen COM-Port. Das ist der größte Vorteil: Bestände, Know-how und Terminal-Software aus der RS-232-Ära übertragen sich unverändert.
3.2 Die Implementierung ist nur SerialPort — aber Sie erben auch die Fallstricke
In .NET ist das System.IO.Ports.SerialPort (ab .NET 5 benötigen Sie eine Referenz auf das Paket System.IO.Ports). Die Implementierungsvorbehalte sind nicht USB-spezifisch, sondern allgemeine der seriellen Kommunikation, und sie sind zusammengestellt — einschließlich Framing, Timeouts, Wiederverbindung und Protokollierungsdesign — in „Fallstricke von Apps mit serieller Kommunikation“. Insbesondere gilt der Punkt, dass Read(buffer, 0, 16) nicht notwendigerweise exakt 16 Bytes liest, über USB genauso. Empfangen Sie sie als Byte-Strom, akkumulieren Sie sie in einem Puffer, und schneiden Sie Rahmen mit einem Parser heraus.
3.3 Schreiben Sie die COM-Nummer nicht in eine Konfigurationsdatei
Das ist die häufigste Ursache dafür, dass virtuelle COM-Einrichtungen vor Ort brechen.
- Eine COM-Nummer ist nichts weiter als eine von Windows auf diesem PC vergebene Nummer; sie ist nicht die Kennung des Geräts
- Ändert sich, an welchen USB-Port Sie anschließen, kann sich die Nummer ändern
- Schließen Sie zwei identische Geräte an, verrät die Nummer allein nicht, welches welches ist
- „COM3 wird verwendet“ treibt die Nummerierung weiter, und bei COM13 oder COM27 zu landen, ist völlig normal
Die korrekte Implementierung besteht darin, die COM-Nummer zur Laufzeit aus VID/PID (und, sofern verfügbar, der Seriennummer) nachzuschlagen. Sie können sie über die PnP-Aufzählung erhalten.
# Jeden COM-Port zusammen mit seiner Hardware-ID auflisten (zunächst ungefiltert betrachten ist die sichere Vorgehensweise)
Get-CimInstance Win32_PnPEntity |
Where-Object { $_.PNPClass -eq 'Ports' } |
Select-Object Name, PNPDeviceID |
Format-List
# Beispielausgabe:
# Name : USB Serial Device (COM5) <- CDC-ACM (usbser.sys)
# PNPDeviceID : USB\VID_2341&PID_0043\85436323631351D0E1C1
# ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
# VID/PID Geräteinstanz-ID
#
# Name : USB Serial Port (COM7) <- FTDIs VCP-Treiber
# PNPDeviceID : FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000
# ^^^^^^^ der Enumerator ist FTDIBUS, nicht USB
Eine Filterung hier mit etwas wie PNPDeviceID -like 'USB\*' führt zu einem Unfall. Wie das obige Beispiel zeigt, beginnt der von FTDIs VCP-Treiber erzeugte COM-Port mit FTDIBUS\ und passt auf keinen einzigen Eintrag unter einem USB\-Filter. Auch andere Chiphersteller wie Silicon Labs können ihre eigenen Enumeratoren haben.
Eine sichere Implementierung ist eine der folgenden.
- Alle Einträge der Klasse
Portsabrufen und dann die inPNPDeviceIDenthalteneVID_xxxx/PID_yyyy(oderVID_xxxx+PID_yyyy) mit einem regulären Ausdruck herausgreifen — robust, weil es nicht vom Enumeratornamen abhängt - Die Enumeratornamen auf eine explizite Positivliste setzen (
USB\,FTDIBUS\und Ähnliches) — ausreichend, wenn das Zielgerät feststeht
So oder so: Schließen Sie Ihr tatsächliches Zielgerät an, führen Sie diesen Befehl aus und bestätigen Sie visuell, mit welcher PNPDeviceID es erscheint, bevor Sie den Filter schreiben. Der Enumeratorname wird durch die Kombination aus Gerät und Treiber bestimmt, Sie können ihn also nicht am Schreibtisch festlegen.
Von C# aus können Sie dieselbe Abfrage mit ManagementObjectSearcher aus System.Management stellen, oder einen AQS-Selektor mit Windows.Devices.SerialCommunication.SerialDevice.GetDeviceSelectorFromUsbVidPid(vid, pid) erstellen und DeviceInformation.FindAllAsync aufrufen (beachten Sie, dass GetDeviceSelector keine VID/PID entgegennimmt — es nimmt keine Argumente oder einen Portnamen, verwechseln Sie das also nicht). Ersteres hat weniger Abhängigkeiten und ist in einer Desktop-Anwendung oft leichter zu handhaben.
Die Nummer aus dem abschließenden (COM5) in Name herauszuschneiden, wirkt unelegant, ist aber in der Praxis der Ansatz, der am zuverlässigsten funktioniert. Um es strikt zu tun, lesen Sie den Wert PortName unter dem Registrierungsschlüssel des Geräts.
3.4 Handles verfaulen bei Hot-Plug
Bei USB-Seriell lässt das Abziehen des Kabels den Port selbst verschwinden. Das Kabel abzuziehen, während SerialPort geöffnet ist, und eine aus dem internen Empfangs-Thread geworfene Ausnahme, die die gesamte Anwendung mit sich reißt, ist ein klassischer Unfall. Das sichere Muster besteht darin, folgende Reihenfolge zu etablieren: Sobald Sie die PnP-Entfernungsbenachrichtigung erhalten, den Port zuerst schließen (Abschnitt 8.2).
Wiederverbindung ist nicht einfach nur, Open() erneut aufzurufen. Gestalten Sie es als Neuerstellung der Sitzung, einschließlich der Ungültigmachung der alten Sitzung, des definitiven Fehlschlagenlassens laufender Anfragen, des Stoppens von Reader/Writer, eines Backoffs vor dem erneuten Öffnen und der erneuten Ausführung der Geräteinitialisierungssequenz.
3.5 Eignung
| Geeignet für | Ungeeignet für |
|---|---|
| Bestehende serielle Protokollbestände | Hoher Durchsatz über einen USB-UART-Brückenchip |
| Text-Befehl/Antwort-Geräte und Instrumente | Enge, latenzarme Steuerung |
| Vor-Ort-Triage mit Terminal-Software gewünscht | Viele identische Geräte gleichzeitig angeschlossen (Identifikation wird teuer) |
| Entwickler ohne Treiberkenntnisse | Neuentwicklung, bei der Sie das Protokoll selbst definieren |
Beim Durchsatz werfen Sie bitte nicht alles unter „virtueller COM, also langsam“ zusammen. In Konfigurationen, die über einen USB-UART-Brückenchip wie ein FTDI-Bauteil laufen, ist die Baudrate der UART auf der anderen Seite die Obergrenze (921,6 kbps ergeben etwa 92 KB/s). Dagegen verwendet auf einem nativen USB-Gerät, bei dem ein Mikrocontroller CDC-ACM direkt implementiert, die Datenschnittstelle USB-Bulk-Transfers, es gibt also keine UART-Beschränkung, und eine High-Speed-Verbindung kann mehrere MB/s erreichen. In diesem Bereich beginnt jedoch der Overhead der Schichten Usbser.sys und SerialPort zu beißen, sodass Sie, wenn Sie mehr als einige Hundert KB/s benötigen, auf echter Hardware messen sollten, bevor Sie sich für einen Ansatz entscheiden. „Wir brauchen Geschwindigkeit, also WinUSB“ zu entscheiden, bevor Sie gemessen haben, bürdet Ihnen unnötige Treiberverteilung auf.
4. Ansatz B: HID — Bidirektionale Kommunikation ohne jede Treiberverteilung
4.1 HID ist nicht nur für Eingabegeräte
HID lässt Menschen an Mäuse und Tastaturen denken, ist als Spezifikation aber ein Allzweckprotokoll zum bidirektionalen Austausch beliebiger Bytefolgen (Reports). Barcode-Leser, Kartenleser, elektronische Schlösser, Messgeräte, USVs, benutzerdefinierte I/O-Boxen — Geräte, die „keinen Treiber verteilen, aber proprietäre Daten austauschen möchten“, deklarieren sich selbst als HID, weil Hidclass.sys und Hidusb.sys mit Windows ausgeliefert werden und es ohne jede INF- oder Treiberverteilung funktioniert.1
Die Einheit, in der Windows für HID arbeitet, ist die Top-Level Collection (TLC). Ein einzelnes physisches Gerät kann mehrere TLCs haben, in diesem Fall erscheint jede als separate Geräteschnittstelle.4
4.2 Die HIDs, die Sie berühren können, und die, die Sie nicht können
Das ist die wichtigste Einschränkung. Windows öffnet manche TLCs im exklusiven Modus. Dies soll verhindern, dass andere Anwendungen den Gesamteingabezustand abfangen, und der Raw Input Manager (RIM) ist es, der diese Geräte exklusiv öffnet.4
| Usage Page / Usage | Zweck | Zugriffsmodus |
|---|---|---|
| 0x0001 / 0x0001-0x0002 | Maus | Exklusiv |
| 0x0001 / 0x0004-0x0005 | Spielcontroller | Geteilt |
| 0x0001 / 0x0006-0x0007 | Tastatur, Tastenfeld | Exklusiv |
| 0x000C / 0x0001 | Verbrauchersteuerung | Geteilt |
| 0x000D / 0x0001-0x0002 | Stift | Exklusiv |
| 0x000D / 0x0004-0x0005 | Touchscreen, Präzisions-Touchpad | Exklusiv |
| 0x0020 / verschieden | Sensor | Geteilt |
| 0x008C / 0x0002 | Barcode-Scanner | Geteilt (das Abrufen dekodierter Daten ist exklusiv) |
Mit anderen Worten: Sie können Daten nicht direkt mit den HID-APIs aus einem tastaturemulierenden Barcode-Leser ziehen. Er wurde exklusiv als Tastatur geöffnet. Möchten Sie Daten von dieser Art Gerät „als Daten statt als Tastenanschläge“ empfangen, besteht der korrekte Ansatz darin, das Gerät selbst über seine eigene Konfiguration in einen HID-herstellerdefinierten TLC-Modus oder einen CDC-Modus umzuschalten.
Beachten Sie, dass Sie selbst bei exklusiv geöffneten Geräten beim Öffnen eines Handles, ohne Lese-/Schreibzugriff anzufordern, weiterhin Attribute und Zeichenketten über die HidD_GetXxx-Familie erhalten können.4 Das genügt für den Anwendungsfall „ich möchte nur prüfen, ob das Gerät verbunden ist“.
4.3 Implementierung — Die Berichtslänge falsch zu machen, führt immer zu einem Fehlschlag
Die Schritte für eine Anwendung im Benutzermodus sind fest vorgegeben. Die HID-Collection mit SetupDi* finden, mit CreateFile öffnen, mit HidD_* Informationen abrufen, mit ReadFile/WriteFile Reports lesen und schreiben, und mit HidP_* Reports interpretieren — das ist alles.7
// HID-Geräte aufzählen und Berichtslängen abrufen (P/Invoke-Deklarationen auszugsweise)
[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);
// Den von GetPreparsedData zurückgegebenen Puffer stets freigeben. Vergessen Sie das, leckt er nativseitig
[DllImport("hid.dll")]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_FreePreparsedData(IntPtr preparsedData);
[StructLayout(LayoutKind.Sequential)]
struct HIDD_ATTRIBUTES
{
public int Size; // Stets auf sizeof(HIDD_ATTRIBUTES) setzen
public ushort VendorID;
public ushort ProductID;
public ushort VersionNumber;
}
// HIDP_CAPS darf nicht mit "nur den verwendeten Feldern" deklariert werden.
// HidP_GetCaps schreibt die volle Länge der nativen Definition (USHORT x 32 = 64 Bytes),
// die Übergabe einer verkürzten Struktur beschädigt also den Stack darüber hinaus
[StructLayout(LayoutKind.Sequential)]
struct HIDP_CAPS
{
public ushort Usage;
public ushort UsagePage;
public ushort InputReportByteLength; // An ReadFile zu übergebende Pufferlänge
public ushort OutputReportByteLength; // An WriteFile zu übergebende Pufferlänge
public ushort FeatureReportByteLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
public ushort[] Reserved; // Reservierter Bereich. Kann nicht weggelassen werden
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;
}
Der häufige Unfall bei der Deklaration von Strukturen ist, „nur die verwendeten Felder zu schreiben und den Rest als Kommentar zu belassen“. HidP_GetCaps schreibt die volle native Länge (USHORT × 32 = 64 Bytes), übergeben Sie also eine Struktur, die nur die ersten fünf Felder (10 Bytes) enthält, wird der vom Marshaller zugewiesene Bereich um 54 Bytes überschrieben. Mit Glück erhalten Sie eine AccessViolationException; ohne Glück werden stillschweigend andere Variablen beschädigt. Deklarieren Sie P/Invoke-Strukturen mit derselben Größe und demselben Layout wie die native Definition, einschließlich der nicht verwendeten Felder.8
Der von HidD_GetPreparsedData zurückgegebene Puffer ist nativseitig zugewiesen, geben Sie ihn also stets mit HidD_FreePreparsedData frei, wenn Sie fertig sind. In einer Anwendung, die bei jedem Hot-Plug-Ereignis jedes HID-Gerät erneut aufzählt, frisst das Vergessen dieses Schritts stillschweigend für immer Speicher. Verpacken Sie es in try/finally, oder verpacken Sie es in eine von SafeHandle abgeleitete Klasse, sodass das Leck strukturell unmöglich wird.
const int HIDP_STATUS_SUCCESS = 0x00110000;
// Immer den Rückgabewert prüfen. Wurde das Gerät unmittelbar nach der Aufzählung abgezogen, kommt FALSE zurück
if (!HidD_GetPreparsedData(handle, out IntPtr preparsed))
{
return null; // Dieses Gerät überspringen. preparsed ist ungültig, also nicht anfassen
}
try
{
if (HidP_GetCaps(preparsed, out HIDP_CAPS caps) != HIDP_STATUS_SUCCESS)
{
return null;
}
// Erst jetzt sind caps.InputReportByteLength und Ähnliches gültig
}
finally
{
HidD_FreePreparsedData(preparsed); // Nur freigeben, wenn der Erwerb erfolgreich war
}
Ignorieren Sie den Rückgabewert von HidD_GetPreparsedData nicht. Wird das Gerät zwischen dem Aufzählen und dem Öffnen des Handles abgezogen, kommt FALSE zurück, und preparsed ist kein gültiger Zeiger. Sie würden ihn dann an HidP_GetCaps und HidD_FreePreparsedData übergeben und anschließend Berichtslängen aus einem caps mit unbekanntem Inhalt bestimmen. Je mehr Hot-Plugging an einem Standort stattfindet, desto wahrscheinlicher treffen Sie diese Race-Condition, betreten Sie das try also nur, wenn der Erwerb erfolgreich war, und prüfen Sie zusätzlich den Rückgabewert von HidP_GetCaps (ob er HIDP_STATUS_SUCCESS ist).
Der häufigste Implementierungsfehler ist die Handhabung von Berichtslängen.
- Der an
ReadFileübergebene Puffer muss exaktInputReportByteLengthentsprechen. Zu kurz schlägt fehl; zu lang wird ebenfalls nicht korrekt behandelt - Das erste Byte des Puffers ist die Berichts-ID. Verwendet das Gerätedesign keine Berichts-IDs, enthält es 0. Die tatsächlichen Daten beginnen bei Byte 1
- Ebenso ist der
WriteFile-Puffer exaktOutputReportByteLength, mit der Berichts-ID vorn
In neun von zehn Fällen ist „ich habe es gesendet, aber das Gerät antwortet nicht“ entweder um das eine Byte der Berichts-ID verschoben oder eine nicht passende Pufferlänge. Sagt die Dokumentation des Geräts „der Befehl ist 8 Bytes“ und OutputReportByteLength ist 9, bedeutet das 9 Bytes einschließlich der Berichts-ID.
Neben WriteFile gibt es auch HidD_SetOutputReport als Weg zum Senden von Ausgabeberichten, und die offizielle Anleitung dazu, wann was zu verwenden ist, ist fest vorgegeben.9
| Zweck | Was zu verwenden ist |
|---|---|
| Ausgabeberichte fortlaufend senden | WriteFile (das ist der Standardfall) |
| Den aktuellen Zustand der Collection setzen | HidD_SetOutputReport |
| Einen Feature-Report senden | HidD_SetFeature |
Vorsicht ist geboten, weil die offizielle Dokumentation davor warnt, dass „manche Geräte HidD_SetOutputReport nicht unterstützen und möglicherweise nicht mehr antworten, wenn es verwendet wird“.9 Mit anderen Worten: Zu HidD_SetOutputReport zu wechseln, „weil WriteFile nicht funktioniert“, ist keine bedingungslos sichere Alternative. Wählen Sie erst, nachdem Sie geprüft haben, welches die Spezifikation des Geräts und die Beispiele des Herstellers verwenden, und verifizieren Sie beim Wechsel auf echter Hardware, dass Antworten nicht ausbleiben.
Möchten Sie nur Geräte aufzählen, öffnen Sie sie mit CreateFiles dwDesiredAccess auf 0 gesetzt. Exklusiv geöffnete Geräte lassen sich weiterhin aufzählen, und Sie können die VID/PID mit HidD_GetAttributes und die Seriennummer mit HidD_GetSerialNumberString erhalten.
Möchten Sie kein rohes P/Invoke aus C# schreiben, ist die Verwendung einer Bibliothek wie HidSharp eine Option. Sie werden jedoch weiterhin Berichtslängen und Berichts-IDs verstehen müssen, sodass es sich einmal in der obigen Form durchzuarbeiten spätere Untersuchungen erheblich beschleunigt. In einer paketierten Anwendung können Sie auch Windows.Devices.HumanInterfaceDevice.HidDevice verwenden, das erfordert jedoch die Deklaration von DeviceCapability im Manifest.10
4.4 Die Geschwindigkeitsobergrenze
HID verwendet Interrupt-Transfers. Bei USB-2.0-Full-Speed-Geräten (12 Mbps) beträgt die maximale Paketgröße eines Interrupt-Endpoints 64 Bytes, und das Polling-Intervall ist der von der Firmware deklarierte Wert im Bereich 1–255 ms. Bei High-Speed (480 Mbps) sind es bis zu 1024 Bytes mit dem Intervall in Einheiten von 125 µs.11
Ein Full-Speed-HID-Gerät, das mit 1 ms und 64 Bytes pollt, liefert also selbst theoretisch nur etwa 64 KB/s. Wählen Sie HID für Verwendungszwecke, die nicht in diesen Rahmen passen — Bilder, Wellenformen, Massenabruf von Protokollen —, gibt es später kein Zurück mehr. Umgekehrt ist die Bandbreite für Befehl-Antwort-Austausche von einigen Dutzend Bytes oder Statusmeldungen mehr als ausreichend.
5. Ansatz C: WinUSB — Ein proprietäres Protokoll direkt ansteuern
5.1 Wo es passt
Winusb.sys ist ein von Microsoft bereitgestellter Allzweck-USB-Treiber. Laden Sie ihn als Funktionstreiber, können Sie direkt über die vom benutzermodalen Winusb.dll freigelegten Funktionen von Endpunkten lesen und in diese schreiben. Es ist der Mechanismus, um ein proprietäres Protokoll ohne das Schreiben eines Treibers zu handhaben.2
Die von Microsoft aufgeführten Bedingungen für die Einführung von WinUSB sind klar.2
- Eine einzige Anwendung greift auf das Gerät zu
- Das Gerät verfügt über Bulk-, Interrupt- oder isochrone Endpunkte (isochron erfordert Windows 8.1 oder höher)
- Windows XP SP2 oder höher ist das Ziel
Umgekehrt kann WinUSB nicht für Geräte verwendet werden, die gleichzeitigen Zugriff von mehreren Anwendungen benötigen. Das ist das Terrain von UMDF-Treibern.
| Fähigkeit | WinUSB | UMDF | KMDF |
|---|---|---|---|
| Gleichzeitiger Zugriff von mehreren Anwendungen | Nein | Ja | Ja |
| Bulk-, Interrupt- und Kontrolltransfers | Ja | Ja | Ja |
| Isochrone Transfers | Ja (8.1 und höher) | Nein | Ja |
| Stapelung von Filtertreibern | Nein | Nein | Ja |
| Selective Suspend | Ja | Ja | Ja |
5.2 Die Bedingungen, unter denen „keine INF erforderlich“ gilt
Das ist der am häufigsten missverstandene Teil jeder WinUSB-Erklärung. Winusb.sys lädt nur dann automatisch ohne INF, wenn die Firmware des Geräts Microsoft-OS-Deskriptoren trägt und WINUSB als kompatible ID meldet.5
Darüber hinaus funktioniert diese automatische Zuordnung nur unter Windows 8 und höher. Die integrierte Winusb.inf erhielt die Unterstützung für die kompatible ID USB\MS_COMP_WINUSB erst unter Windows 8; davor war eine benutzerdefinierte INF, die die Hardware-ID angibt, verpflichtend.5 Wie in Abschnitt 5.1 vermerkt, läuft WinUSB selbst unter Windows XP SP2 und höher, aber „läuft ab XP“ und „installiert ohne INF“ sind unterschiedliche Aussagen. Wird Windows 7 oder älter einbezogen, planen Sie die Verteilung einer INF, selbst wenn Sie OS-Deskriptoren implementiert haben. (Unter Windows 7 und älter kann es dennoch zuordnen, wenn eine aktualisierte Winusb.inf über Windows Update eintraf, aber darauf lässt sich kein Verteilungsplan aufbauen.)
Konkret benötigt die Geräteseite folgende Implementierung. Beachten Sie, dass es zwei Linien gibt: Version 1.0 (WCID) und 2.0.
- Microsoft-OS-1.0-Deskriptoren (alle Versionen)
- Einen OS-Zeichenkettendeskriptor beim Zeichenkettenindex
0xEEtragen, der einen Herstellercode zurückgibt compatibleIDim Extended-Compat-ID-OS-Feature-Deskriptor aufWINUSBsetzen (pro Funktion, bei einem zusammengesetzten Gerät)
- Einen OS-Zeichenkettendeskriptor beim Zeichenkettenindex
- Microsoft-OS-2.0-Deskriptoren (Windows 8.1 und höher)
- Den Ort des Deskriptorsatzes über den Plattformfähigkeitsdeskriptor im BOS-Deskriptor ankündigen. Der
0xEE-Zeichenkettendeskriptor wird nicht verwendet - Innerhalb dieses Deskriptorsatzes einen Feature-Deskriptor für die kompatible ID platzieren, der
WINUSBmeldet. Allein über BOS anzukündigen, dass „ein Satz existiert“, führt nicht dazu, dassWinusb.sysausgewählt wird. Was die Bindung entscheidet, ist wie bei 1.0 die kompatible ID
Es wurde definiert, um die Einschränkungen und Zuverlässigkeitsprobleme von 1.0 zu lösen, für neue Firmware-Designs ist dies also die erste Wahl12
- Den Ort des Deskriptorsatzes über den Plattformfähigkeitsdeskriptor im BOS-Deskriptor ankündigen. Der
Die Registrierung einer Geräteschnittstellen-GUID spielt eine andere Rolle als das Obige. Das ist die GUID, mit der eine Anwendung das Gerät findet, während die kompatible ID entscheidet, ob Winusb.sys binden kann. Die GUID gehört zur Discovery-Seite. Dennoch ist es ohne registrierte benutzerdefinierte GUID schwer, die Geräteerkennung anwendungsseitig aufzubauen, sodass sie in der Praxis gemeinsam implementiert werden.
Vorsicht ist hier geboten, weil der Registrierungseigenschaftsname sowohl in Singular- als auch in Pluralform existiert.13
| Name | Typ | Wo er verwendet wird |
|---|---|---|
DeviceInterfaceGUID |
Zeichenkette (REG_SZ) | Der Microsoft-OS-1.0-Extended-Properties-Deskriptor gibt diesen Namen mit einer wPropertyNameLength von 40 Bytes an5 |
DeviceInterfaceGUIDs |
Multi-Zeichenkette (REG_MULTI_SZ) | Die Standardform für benutzerdefinierte INFs. Auch Microsofts eigene Beispiele verwenden den Plural: HKR,,DeviceInterfaceGUIDs,0x10000,"{...}"13 |
Bei der Beschreibung des Verhaltens von Winusb.sys verwendet die offizielle Dokumentation den Plural: „es liest den Registrierungsschlüssel DeviceInterfaceGUIDs und registriert die Geräteschnittstelle mit der dort angegebenen GUID.“13 Das Verfahren zum manuellen Hinzufügen des Eintrags in die Registrierung bietet ebenfalls beide an: Legen Sie entweder die Zeichenkette DeviceInterfaceGUID oder die Multi-Zeichenkette DeviceInterfaceGUIDs unter Device Parameters ab.13
Verwenden Sie den OS-2.0-Registrierungseigenschafts-Feature-Deskriptor, prüfen Sie den Eigenschaftsnamen und den Datentyp gegen die Spezifikation (in der Praxis ist Plural plus REG_MULTI_SZ die übliche Wahl). Übertragen Sie die 1.0-Singularform unverändert auf den 2.0-Weg, landen Sie womöglich bei einem Ort, den Windows nicht konsultiert, was den Zustand erzeugt „die Bindung funktioniert, aber die Anwendung findet es nicht“.
Der zuverlässige Weg, dies zu verifizieren, besteht darin, das Gerät anzuschließen und dann die Registrierung zu betrachten. Prüfen Sie HKLM\SYSTEM\CurrentControlSet\Enum\USB\<Hardware-ID>\<Instanz-ID>\Device Parameters auf den beabsichtigten Namen, Typ und Wert, und Sie kennen die Fakten unabhängig davon, wie die Deskriptoren interpretiert wurden.
Verwenden Sie beim Schreiben einer INF außerdem die Setup-Klasse USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}). Die Klasse USB ist dokumentiert als ausschließlich für Hostcontroller, Hubs und zusammengesetzte Geräte gedacht, und ihre Verwendung für ein benutzerdefiniertes Gerät wird ausdrücklich als Einladung zu Zuverlässigkeits- und Leistungsproblemen bezeichnet.5
Können Sie die Firmware eines bestehenden Geräts nicht anfassen, bereiten Sie eine eigene benutzerdefinierte INF vor und verteilen sie, die die Hardware-ID angibt. An diesem Punkt haben Sie eine Konfiguration, bei der „der Installer einen Treiber installiert“, und die Verteilungsdiskussion (Abschnitt 10) beginnt. Selbst eine INF, die nicht ein einziges Byte Ihres eigenen .sys enthält und lediglich auf Microsofts winusb.sys verweist, lässt sich auf realem Windows nicht ohne signierten Katalog installieren. Es ist kein Fall von „schreiben Sie einfach eine INF und Sie sind fertig“, lesen Sie also Abschnitt 10, bevor Sie den Aufwand schätzen.
Das Umschalten des Treibers auf WinUSB mit einem Werkzeug wie Zadig während der Entwicklung ist eine legitime Verifikationstechnik, aber es ist ein Vorgang, der den Treiber des Herstellers entfernt. Machen Sie daraus nicht Ihren Produktivverteilungsmechanismus — es hindert andere Anwendungen daran, dasselbe Gerät zu verwenden.
5.3 Implementierungspunkte
// WinUSB-Initialisierung (Fehlerbehandlung ausgelassen)
HANDLE h = CreateFile(devicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // asynchron ist im Wesentlichen zwingend
NULL);
WINUSB_INTERFACE_HANDLE usb;
WinUsb_Initialize(h, &usb);
// Bei Lesevorgängen stets ein Timeout setzen (der Standard ist unendliches Warten)
ULONG timeoutMs = 1000;
WinUsb_SetPipePolicy(usb, bulkInPipeId, PIPE_TRANSFER_TIMEOUT,
sizeof(timeoutMs), &timeoutMs);
// Bei asynchronem Betrieb NULL für LengthTransferred übergeben und die Transferlänge nach Abschluss abrufen
WinUsb_ReadPipe(usb, bulkInPipeId, buffer, bufferLength, NULL, &overlapped);
// -> Rückgabewert FALSE / GetLastError() == ERROR_IO_PENDING bedeutet, es läuft noch
ULONG transferred = 0;
WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE); // erst jetzt ist der Wert gültig
// --- Aufräumen. Dies läuft bei jedem Hot-Plug, alles Geleakte häuft sich bei jeder Wiederverbindung an ---
CancelIoEx(h, NULL); // laufende E/A stoppen,
WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE); // Abschluss zuerst einsammeln,
WinUsb_Free(usb); // das Schnittstellen-Handle freigeben,
CloseHandle(h); // dann das Datei-Handle schließen
Vier Punkte sind in der Praxis entscheidend.
- Mit
FILE_FLAG_OVERLAPPEDöffnen und asynchron betreiben. Bei synchroner E/A friert der gesamte Thread ein, sobald das Gerät still wird - Stets
PIPE_TRANSFER_TIMEOUTsetzen. Standardmäßig kehren Lesevorgänge nie zurück - Im asynchronen Modus keinen Zeiger für
LengthTransferredübergeben. Die offizielle Dokumentation stellt ausdrücklich fest, dass „wennOverlappedungleich NULL ist,LengthTransferredNULL sein darf“, und dass, wenn Sie dennoch einen von NULL verschiedenen Wert übergeben, der Wert im Moment, in demWinUsb_ReadPipezurückkehrt, bis zum Abschluss der Operation bedeutungslos ist. Ermitteln Sie die Transferlänge mitWinUsb_GetOverlappedResult.14 Die Adresse einer lokalen Variablen zu übergeben, ist nicht nur bedeutungslos — lässt das Design dieser Variablen sie außerhalb des Gültigkeitsbereichs geraten, wird sie zu einem baumelnden Zeiger WinUsb_FreeundCloseHandlestets im Paar verwenden. Jedes erfolgreicheWinUsb_Initializeweist ein Schnittstellen-Handle zu. Gestalten Sie, wie in Abschnitt 8.2, die Sitzung so, dass sie bei jedem Hot-Plug-Ereignis neu erstellt wird, häuft sich alles, was Sie zu befreien vergessen, bei jeder Wiederverbindung an. Stellen Sie sicher, dass das Aufräumen nicht nur auf dem Erfolgspfad erreicht wird, sondern auch auf Pfaden, die mitten in der Initialisierung fehlschlagen (zum BeispielWinUsb_Initializeerfolgreich, aber die Pipe-Konfiguration schlägt fehl), indem Sie das Aufräumen an einer einzigen Stelle bündeln. Die Reihenfolge lautet „laufende E/A mitCancelIoExstoppen → Abschluss mitWinUsb_GetOverlappedResulteinsammeln →WinUsb_Free→CloseHandle.“ Schließen Sie das Handle vor dem Einsammeln des Abschlusses, geben Sie einen Puffer frei, den der Kernel noch anfasst- Mehrfachinstanzen der Anwendung verhindern. WinUSB unterstützt keinen gleichzeitigen Zugriff von mehreren Anwendungen, nehmen Sie also eine Ein-Instanz-Durchsetzung (etwa einen benannten Mutex) in die Spezifikation auf
Um es von C# aus zu verwenden: libusbs Windows-Backend ist auf WinUSB aufgebaut, ein Wrapper wie LibUsbDotNet ist also eine Option. In einer paketierten Anwendung können Sie auch Windows.Devices.Usb.UsbDevice verwenden, das trägt jedoch eine ausdrückliche Einschränkung: Es kann nicht auf die Geräteklassen Audio, HID, Bild, Drucker, Massenspeicher, Smartcard, Audio/Video oder Wireless-Controller zugreifen.15
6. Ansatz D: Hersteller-SDKs und dedizierte Treiber — Nicht gewählt, sondern übernommen
Industriekameras, Messinstrumente, POS-Peripheriegeräte, Fingerabdruck- und Venenauthentifizierung, dedizierte I/O-Karten — dafür liefert der Hersteller Treiber und SDK als Paket, und in der Praxis ist keine andere Nutzung möglich. Ansatz D ist weniger eine Wahl als eine Voraussetzung, die in dem Moment feststand, in dem das Gerät gewählt wurde.
Gerade deshalb ist das Inventarisieren der Einschränkungen des SDK in der Gerätewahlphase die eigentliche Designarbeit. Hier die zu prüfenden Punkte.
| Zu prüfender Punkt | Was passiert, wenn Sie es übersehen |
|---|---|
| Ob sowohl 32-Bit als auch 64-Bit unterstützt werden | Eine 64-Bit-Anwendung kann eine nur-32-Bit-DLL nicht aufrufen, Sie benötigen Prozesstrennung |
| API-Form (C-DLL / COM / .NET) | Es ändert die Aufrufkonvention und das Marshalling-Design. COM bringt Einschränkungen des Threading-Modells |
| Threading-Einschränkungen (STA erforderlich, auf welchem Thread Callbacks ankommen) | Es blockiert den UI-Thread oder verursacht Deadlocks |
| Weiterverteilbare Komponenten und Verteilungsbedingungen | Sie können es nicht in den Installer bündeln, und manuelle Installation beim Kunden wird nötig |
| Signierungsstatus des mitgelieferten Treibers | Es lässt sich nicht auf neueren Windows-11-Builds oder auf Gerätecomputern installieren |
| Unterstützte Betriebssysteme und Support-Lebensdauer | Ein Betriebssystem-Upgrade bedeutet, die gesamte Anwendung neu zu erstellen |
| Ob mehrere gleichzeitige Verbindungen unterstützt werden und wie sie identifiziert werden | Es bricht in dem Moment zusammen, in dem Sie eine zweite Einheit anschließen |
| Ob der Quellcode der Demo-Anwendung verfügbar ist | Untersuchungskosten für nicht dokumentiertes Verhalten explodieren |
Auf der Implementierungsseite ist der größte Schutz, das SDK nicht über die gesamte Anwendung zu verstreuen. Beschränken Sie SDK-Aufrufe auf eine einzige dünne Abstraktionsschicht (eine Schnittstelle) und schreiben Sie die eigentliche Anwendung gegen diese Abstraktion. Tun Sie das, bleibt die Auswirkung eines Gerätemodellwechsels, eines SDK-Hauptversions-Upgrades oder eines Herstellerwechsels auf einen einzigen Ort beschränkt, und Sie können auch Unit-Tests ohne die Hardware schreiben.
Müssen Sie ein nur-32-Bit-SDK aus einer 64-Bit-Anwendung heraus verwenden, besteht die Standardlösung darin, es in einen separaten Prozess auszulagern und über prozessübergreifende Kommunikation zu verbinden. Über COM ist „Ein durchgearbeitetes Beispiel einer COM-Brücke zum Aufrufen einer 64-Bit-DLL aus einer 32-Bit-App“ derselbe Gedanke in umgekehrter Richtung, und wie man native DLLs überhaupt aufruft, ist zusammengestellt in „Native DLLs aus C# aufrufen: C++/CLI-Wrapper gegenüber P/Invoke“. Zur Verwaltung der Lebensdauer von Kindprozessen siehe „Eine Checkliste für den sicheren Umgang mit Kindprozessen in Windows-Apps“.
7. Entscheidungstabelle für die vier Ansätze
| Aspekt | Virtual COM | HID | WinUSB | Hersteller-SDK |
|---|---|---|---|---|
| Treiberverteilung | Nicht nötig (CDC) / Standard (VCP) | Nicht nötig | Bedingt nicht nötig; oft ist eine INF erforderlich | Erforderlich |
| Implementierungsschwierigkeit | Niedrig | Mittel | Mittel bis hoch | Abhängig vom SDK (stark variabel) |
| Durchsatz | Niedrig bis mittel (gerätabhängig; messen) | Niedrig | Hoch | Hoch |
| Latenz | Mittel | Mittel (abhängig vom Polling-Intervall) | Niedrig | Niedrig |
| Gleichzeitige Nutzung durch mehrere Anwendungen | Nein (der Port ist exklusiv) | Ja (bei geteilten TLCs) | Nein | Abhängig vom SDK |
| Eindeutige Geräteidentifikation | Muss implementiert werden (COM-Nummer genügt nicht) | Möglich über VID/PID/Seriennummer | Möglich über Gerätepfad | Abhängig vom SDK |
| Einfachheit der Vor-Ort-Triage | Hoch (Terminal-Software) | Mittel | Niedrig | Niedrig |
| Abhängigkeit von der Gerätefirmware | Gering | Gering | Groß (OS-Deskriptoren) | Vollständig |
| Geeignet für | Befehl-Antwort-Geräte und Instrumente | Statusmeldung, kleine Befehle | Große Datenmengen, proprietäre Protokolle | Kameras, Instrumente, Spezialgeräte |
Die Reihenfolge der Entscheidungen verläuft so.
- Erscheint das Gerät bereits als COM-Port / HID / Standardklasse? → Falls ja, das verwenden
- Tut es das nicht, aber Sie können die Firmware ändern → zwischen HID (kleine Daten) und WinUSB (große Daten) anhand der Bandbreite entscheiden
- Sie können die Firmware nicht ändern, und ein Hersteller-SDK existiert → das SDK verwenden, aber zuerst die Prüfungen aus Abschnitt 6 durchführen
- Nichts davon trifft zu, und gleichzeitiger Zugriff von mehreren Anwendungen ist erforderlich → die Entwicklung eines UMDF-Treibers in Betracht ziehen2
8. Vier Dinge, die Sie selbst gestalten, unabhängig vom Ansatz
Ist der Ansatz festgelegt, müssen Sie dennoch die folgenden vier Dinge selbst aufbauen. Geräteintegrationsanwendungen, die „manchmal nicht funktionieren“, vermissen fast sicher eines davon.
8.1 Eindeutige Geräteidentifikation — Nach ID greifen, nicht nach Nummer
Was in die Konfiguration einfließen darf, sind VID/PID + Seriennummer oder der Geräteschnittstellenpfad. COM-Nummern und die Reihenfolge im Geräte-Manager sind keine Kennungen.
Ob eine Seriennummer existiert, ist am letzten Element des Geräteinstanzpfads sichtbar.
USB\VID_2341&PID_0043\85436323631351D0E1C1 <- das Gerät meldet eine Seriennummer (invariant bei Standortwechsel)
USB\VID_0403&PID_6001\5&1a2b3c4d&0&2 <- es tut das nicht (ein von Windows aus dem Anschlussort generierter Wert)
Bei zusammengesetzten Geräten genügt VID/PID + Seriennummer nicht. Wie in Abschnitt 2 erklärt, kann ein Gerät mehrere Funktionen haben, und in diesem Fall teilen sich alle Funktionen dieselbe VID, PID und Seriennummer. Bei einem Gerät mit „zwei CDCs, eines für Steuerung und eines für Wartung“ oder „zwei HID-Top-Level-Collections“ finden Sie mehrere Kandidaten mit identischen Werten für dieses Trio, und welches Sie öffnen, ist reine Glückssache. Nehmen Sie etwas, das die Funktion unterscheidet, in Ihren Schlüssel auf.
USB\VID_1234&PID_5678&MI_00\7&2a3b4c5d&0&0000 <- Funktion 0 (etwa das Steuerungs-CDC)
USB\VID_1234&PID_5678&MI_02\7&2a3b4c5d&0&0002 <- Funktion 2 (etwa das Wartungs-CDC)
gleiche VID/PID, gleiches übergeordnetes Gerät ^ nur MI_xx (die USB-Schnittstellennummer) unterscheidet sich
Bauen Sie den Schlüssel in der Praxis auf einer der folgenden Weisen auf.
- Die USB-Schnittstellennummer (
MI_xx) einbeziehen — der Standardweg, um CDC-/WinUSB-Funktionen auf einem zusammengesetzten Gerät zu unterscheiden - Bei HID zusätzlich Usage Page + Usage verwenden — so unterscheiden Sie mehrere Top-Level-Collections auf demselben Gerät (
UsagePage/UsageausHidP_GetCaps) - Den Geräteschnittstellenpfad selbst speichern — die von der Aufzählung zurückgegebene Zeichenkette ist pro Funktion eindeutig, sich darauf als Schlüssel zu stützen, ist also die zuverlässigste Option
Sind Sie in der Position, die Gerätespezifikation selbst festzulegen, macht eine Firmware-Spezifikation, die jeder Funktion eine eigene Schnittstellennummer zuweist und stets eine Seriennummer meldet, die Identifikationslogik auf der Softwareseite drastisch einfacher.
Letzteres enthält &, und sein Wert ändert sich, wenn Sie an einen anderen Port anschließen.
Wenden Sie diesen Test jedoch nicht auf die Kindknoten eines zusammengesetzten Geräts an. Bei einem zusammengesetzten Gerät ist das Ende der Instanz-ID der als CDC-Funktionen oder HID-Collections aufgezählten Kind-PDOs ein von Usbccgp.sys generierter Wert, und er enthält &, selbst wenn das physische Gerät tatsächlich eine Seriennummer meldet. Nur den Pfad des Kindes zu betrachten und „keine Seriennummer“ zu schließen, ist falsch. Die Seriennummer liegt am übergeordneten USB-Geräteknoten, gehen Sie also mit CM_Get_Parent (oder DEVPKEY_Device_Parent) zum Elternteil hinauf und betrachten Sie dessen Instanz-ID.
USB\VID_1234&PID_5678\SN0001234 <- Elternteil (das physische Gerät). Die Seriennummer liegt hier
└ USB\VID_1234&PID_5678&MI_00\7&2a3b... <- Kind. Von Usbccgp generiert, enthält daher & (nicht für den Test verwenden)
Landen Sie am Ende bei einem Gerät ohne Seriennummer für einen Betrieb, der mehrere identische Einheiten anschließt, ist das einzig verbleibende Identifikationsmittel „an welchem USB-Port es angeschlossen ist“. Gestalten Sie in diesem Fall bis hin zur Festlegung der Hub-Ports und fügen Sie als betriebliches Verfahren Beschriftungen hinzu — das ist ein Teil, den Technik nicht lösen kann, Sie müssen es also bereits in der Gerätewahlphase bemerken.
8.2 Hot-Plug verfolgen — Nicht pollen
Man sieht häufig Implementierungen, die per Timer jede Sekunde neu aufzählen, aber Windows besitzt einen Benachrichtigungsmechanismus.
- Windows 8 und höher:
CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE(Erkennung von Ankunft und Entfernung) undCM_NOTIFY_FILTER_TYPE_DEVICEHANDLE(Erkennung, dass ein Gerät, dessen Handle Sie geöffnet halten, verschwunden ist) mitCM_Register_Notificationregistrieren16 - Wird auch Windows 7 oder älter einbezogen:
DBT_DEVTYP_DEVICEINTERFACEmitRegisterDeviceNotificationregistrieren undWM_DEVICECHANGEbehandeln17
Es gibt zwei Vorbehalte, die Sie in der Implementierung nicht überspringen können.16
CM_Register_Notificationbenachrichtigt Sie nicht über „Schnittstellen, die zum Registrierungszeitpunkt bereits existieren“. Registrieren Sie zuerst, zählen Sie dann die bestehenden mitCM_Get_Device_Interface_Listauf. Tun Sie es in umgekehrter Reihenfolge, verpassen Sie Geräte, die während der Lücke angeschlossen wurden- Bauen Sie im Gegenzug auf der Annahme auf, dass Duplikate erscheinen werden. Eine nach der Registrierung, aber vor der Aufzählung aktivierte Schnittstelle erscheint sowohl in der Ankunftsbenachrichtigung als auch in der Liste. Speisen Sie beide direkt in die Ankunftsbehandlung ein, erzeugen Sie zwei Sitzungen für dasselbe Gerät, sodass das zweite exklusive Öffnen fehlschlägt oder ein bereits etablierter Zustand überschrieben wird. Deduplizieren Sie stets, indem Sie eine nach dem Geräteschnittstellenpfad geschlüsselte Menge führen und bereits bekannte Pfade ignorieren (Einträge aus dieser Menge bei Entfernung löschen)
- Führen Sie innerhalb des Callbacks nichts aus, das blockieren kann. Schieben Sie Arbeit, die E/A beinhaltet, auf einen anderen Thread. Warten Sie hier, staut sich die gesamte Verarbeitung von PnP-Ereignissen
In einer paketierten Anwendung, oder in jeder Konfiguration, in der Sie die WinRT-APIs verwenden können, drückt DeviceWatcher dasselbe knapp aus.
// Nach seriellen Geräten mit einer bestimmten VID/PID Ausschau halten (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();
Und das Wichtige ist, PnP-Benachrichtigungen nicht zum „einzigen Einstiegspunkt“ zu machen. Wird das Kabel gezogen, kann laufende E/A vor oder gleichzeitig mit der Benachrichtigung mit einem Entfernungs- oder Abbruchfehler abschließen. Die Reihenfolge „das Handle in dem Moment schließen, in dem die Benachrichtigung eintrifft“ gilt nur, wenn die Benachrichtigung zuerst eintrifft, sodass eine Implementierung, die sich allein darauf verlässt, weiterhin das Verhalten „abziehen und es stürzt ab“ aus Abschnitt 3.4 trägt.
Die korrekte Form besteht darin, zwei Einstiegspunkte zum Beenden der Sitzung zu haben.
- Behandeln Sie auf jedem Lese- und Schreibabschlusspfad entfernungsartige Fehler (
ERROR_DEVICE_NOT_CONNECTED/ERROR_DEVICE_REMOVED/ERROR_GEN_FAILURE, oder die entsprechendeIOExceptionin .NET) als „das Gerät ist weg“ und leiten Sie von dort den Sitzungsabbau ein - Behandeln Sie PnP-Benachrichtigung als ergänzendes Signal. Sie wird benötigt, um den Fall zu erfassen, dass das Gerät im Leerlauf ohne laufende E/A abgezogen wird, allein reicht sie jedoch nicht aus
Der Fall, den Sie hier unbedingt unterscheiden müssen, ist „ich habe selbst abgebrochen“. Das Abbrechen bei einem Antwort-Timeout, das Aufräumen beim Herunterfahren der Anwendung, eine Unterbrechung durch eine Benutzeraktion — verwenden Sie dafür CancelIoEx, ein CancellationToken oder Dispose, und Sie erhalten ERROR_OPERATION_ABORTED, OperationCanceledException oder ObjectDisposedException, obwohl alles korrekt funktioniert hat. Beurteilen Sie diese bedingungslos als „das Gerät ist weg“, bauen Sie eine besonders bösartige Schleife, in der das Gerät verbunden bleibt, Sie aber die Sitzung verwerfen und bei jedem Timeout neu verbinden.
// "Ich habe es selbst gestoppt" von "das Gerät ist weg" anhand des Zustands unterscheiden
catch (OperationCanceledException) when (_shutdown.IsCancellationRequested)
{
// Selbstabbruch. Die Sitzung ist nicht kaputt, also nicht abbauen
}
catch (Exception ex) when (IsDeviceGone(ex))
{
TearDownSession(); // Idempotent. Läuft nicht zweimal, wenn auch von der PnP-Benachrichtigung aufgerufen
}
Der Ausnahmetyp oder Fehlercode allein genügt nicht zur Entscheidung. Sie können nur triagieren, indem Sie es mit dem Zustand „fordere ich gerade einen Abbruch an?“ kreuzreferenzieren (ein Cancellation-Token, ein Shutdown-Flag). Anders gesagt: Haben Sie einen Selbstabbruchpfad, müssen Sie diese Tatsache irgendwo für die E/A-Schicht sichtbar halten.
Damit der Eintritt über beide Pfade zu demselben Aufräumen führt, bündeln Sie den Sitzungsabbau in einer einzigen idempotenten Operation und stellen Sie sicher, dass ein doppelter Aufruf sie nicht brechen kann (setzen Sie zum Beispiel ein Flag mit Interlocked.Exchange und lassen Sie nur den ersten Aufrufer durch). Von E/A gegen ein verfaultes Handle geworfene Ausnahmen kommen oft von Stellen geflogen, die schwer abzufangen sind — genau deshalb brauchen Sie ein Design, das sie an der Quelle abfängt und in einen Zustandsübergang verwandelt.
8.3 Timeouts und Wiederverbindung — „Ein Timeout“ genügt nicht
E/A auf einem USB-Gerät erzeugt dasselbe Symptom — „es kommt nichts zurück“ —, egal ob es abgezogen wurde, den Strom verlor oder die Firmware hängengeblieben ist. Halten Sie separate Timeouts für separate Bedeutungen.
| Timeout | Gilt für | Grober Wert |
|---|---|---|
| Öffnungs-Timeout | Bis das Gerät geöffnet ist | Größenordnung Sekunden |
| Antwort-Timeout | Vom Ausgeben eines Befehls bis zum Abschluss der Antwort | Der Worst Case der Gerätespezifikation × ein Sicherheitsfaktor |
| Inter-Byte-Timeout | Der Rest eines Rahmens kommt nicht an | Aus der Leitungsgeschwindigkeit berechnet |
| Wiederverbindungs-Backoff | Die Wartezeit zwischen erneuten Öffnungsversuchen | Exponentieller Backoff + eine Obergrenze |
Und behandeln Sie ein Timeout nicht als „Versicherung für langsame Fälle“, sondern als „eine Regel, die einen Zustandsübergang vorantreibt“. Es wird erst zu einem Design, sobald Sie entschieden haben, in welchen Zustand Sie bei Timeout wechseln, wie laufende Anfragen fehlschlagen gelassen werden und was Sie in der Oberfläche anzeigen. Wie Sie es in der Oberfläche darstellen, ist behandelt in „Best Practices für die Prüfung und Anzeige des Zustands externer Geräte“ — geben Sie sich bitte nicht mit dem einzelnen Wort „Verbunden“ zufrieden.
8.4 Energieverwaltung — Die wahre Ursache von „es reagiert langsam, obwohl nichts abgezogen wurde“
Selective Suspend von USB ist der Mechanismus, der ein untätiges Gerät in einen Energiesparzustand versetzt. Weil das Aufwachen Zeit braucht, ist es der Übeltäter hinter Symptomen wie „nur die allererste Antwort ist langsam“ und „lässt man es eine Weile in Ruhe, verliert es den ersten Befehl“.
- Bei
Usbser.sys(virtueller COM) ist es standardmäßig deaktiviert, und Sie aktivieren und konfigurieren es über den RegistrierungswertIdleUsbSelectiveSuspendPolicy3 - Bei WinUSB steuern Sie es über den Extended-Properties-OS-Feature-Deskriptor (oder die INF) mit
DeviceIdleEnabled,DefaultIdleTimeout,UserSetDeviceIdleEnabledund Ähnlichem5
Das Erste, was Sie vor Ort prüfen sollten, ist das Kontrollkästchen „Computer kann das Gerät ausschalten, um Energie zu sparen“ in den Eigenschaften des betreffenden Geräts (und des USB-Root-Hubs) im Geräte-Manager. Auf Gerätecomputern gibt es tatsächlich Fehler, die allein das Deaktivieren dieses Kästchens behebt. Einschließlich Notebook-Energiesparoptionen: Führen Sie Ihre Verifikation im selben Energiesparplan wie in der Produktion durch.
9. Leistung und Latenz abschätzen
Den benötigten Durchsatz und die Latenz bereits in der Auswahlphase des Ansatzes in Zahlen zu fassen, beseitigt späteres Zurückrudern.
| Transfertyp | Ansätze, die ihn verwenden | Eigenschaften |
|---|---|---|
| Kontrolltransfer | Alle Ansätze (intern verwendet) | Für Konfiguration und kleine Befehle. Keine Bandbreitengarantie |
| Interrupt-Transfer | HID, WinUSB | Periodisches Polling. Niedrige Latenz, aber geringe Kapazität |
| Bulk-Transfer | WinUSB, Massenspeicher | Für große Datenmengen. Keine Bandbreitengarantie; nutzt, was frei ist |
| Isochroner Transfer | UVC (Kameras), UAC (Audio), WinUSB (8.1 und höher) | Garantierte Bandbreite, keine erneute Übertragung |
Ein USB-2.0-Interrupt-Endpoint umfasst bei Full-Speed bis zu 64 Bytes pro Paket mit einem Polling-Intervall von 1–255 ms, und bei High-Speed bis zu 1024 Bytes mit Intervallen in Einheiten von 125 µs.11 Wählen Sie HID, prüfen Sie, ob Ihre benötigte Bandbreite mindestens eine Größenordnung Spielraum gegenüber diesen Grenzen hat.
Noch etwas: Windows ist ein Allzweckbetriebssystem und bietet daher keine Latenzgarantie. Eine Anforderung wie „muss immer in einem 10-ms-Zyklus antworten“ auf Anwendungsebene zu erfüllen, ist ein weiter Weg. Ist ein zyklisches Steuern tatsächlich notwendig, gestalten Sie so, dass der Zyklus auf den Mikrocontroller im Gerät beschränkt bleibt und der PC bei Befehlsausgabe und Überwachung bleibt. Diese Trennlinie ist im Detail behandelt in „Ein praktischer Leitfaden, um auf gewöhnlichem Windows so nah wie möglich an Soft-Echtzeit heranzukommen“.
10. Verteilung und Betrieb — In dem Moment, in dem Sie einen Treiber ausliefern, ändern sich die Kosten
Zwischen den Ansätzen „kein Treiber nötig“ (Standardklasse, HID, WinUSB-Geräte) und den Ansätzen „einen Treiber ausliefern“ liegt ein Bruch nicht bei den Entwicklungskosten, sondern bei den Verteilungs- und Wartungskosten.
- „Ich schreibe kein eigenes
.sys, also ist keine Signierung erforderlich“ ist falsch. Bei der PnP-Geräteinstallation wird ein Treiberpaket nicht in den Driver Store aufgenommen, sofern seine Katalogdatei nicht signiert ist.18 Diese Anforderung ist unabhängig davon, was im Paket enthalten ist, sodass selbst eine INF, die wie in Abschnitt 5.2 lediglich auf Microsoftswinusb.sysverweist, einen Schritt erfordert, der einen Katalog (.cat) erzeugt und signiert. Schätzen Sie es als „eine INF schreiben und ausliefern und es funktioniert“, erfahren Sie es erst, wenn der Rechner des Kunden mit „der Treiber für dieses Gerät ist nicht signiert“ ablehnt. Katalogsignierung bedeutet entweder WHQL-Release-Signierung oder Signierung mit einem Drittanbieter-Release-Zertifikat (SPC).18 - Signierung von Kernelmodus-Treibern. Seit Windows 10 Version 1607 lädt ein neuer Kernelmodus-Treiber nicht, sofern Microsoft ihn nicht über das Dev Portal (Partner Center) signiert hat. Die Eröffnung eines Partner-Center-Kontos erfordert ein EV-Code-Signing-Zertifikat.6 Das ist eine Anforderung auf einer anderen Ebene als die obige Katalogsignierung, und enthalten Sie eine Kernelmodus-Binärdatei, müssen Sie beide erfüllen.
- Es gibt zwei Signierungswege, und ihre Reichweite unterscheidet sich. HLK-getestet / Dashboard-signiert, das das Bestehen der HLK-Tests erfordert, gilt von Windows Vista bis zu den Windows-Server-Editionen, und Microsoft empfiehlt diesen Weg. Der andere, die Attestierungssignierung, erfordert keine HLK-Tests, gilt aber nur unter Windows 10 Desktop und höher (sie wird unter Windows 7/8.1 oder Windows Server 2016 und höher nicht akzeptiert). Darüber hinaus lässt sie sich nicht über Windows Update an die Allgemeinheit verteilen, und sie macht den Treiber nicht Windows-zertifiziert. Microsofts eigene Dokumentation positioniert sie als „für Testzwecke“.19 Attestierungssignierung für einen selbstgebauten, im eigenen Installer ausgelieferten Treiber zu verwenden, ist in der Praxis weit verbreitet, übernehmen Sie es aber erst, nachdem Sie die Einschränkung auf Windows 10/11 Desktop in Ihre Tabelle der unterstützten Betriebssysteme eingetragen haben. Läuft der Gerätecomputer unter Windows Server oder einem älteren LTSC, steht Ihnen dieser Weg von vornherein nicht zur Verfügung.
- Verlassen Sie sich nicht auf die Ausnahmebedingungen. Cross-signierte Treiber funktionieren weiterhin, wenn Secure Boot deaktiviert ist oder das Zertifikat vor dem 29. Juli 2015 ausgestellt wurde und Ähnliches — aber ein darauf aufgebauter Verteilungsplan zerfällt innerhalb weniger Jahre.6
- Installer-Design. Ein Installer, der einen Treiber enthält, erfordert Administratorrechte, und Sie müssen auch die stille Installation verifizieren. Wie Sie die Verteilungsmethode selbst wählen, ist behandelt in „Eine Windows-App-Verteilungsmethode wählen“, und wie Sie erkennen, wann Administratorrechte tatsächlich erforderlich sind, ist zusammengefasst in „Wann Sie unter Windows tatsächlich Administratorrechte benötigen“.
- Auf Gerätecomputern geraten Treiber und Betriebssystem-Updates in Konflikt. Bei der Installation eines Herstellertreibers auf einem LTSC-konfigurierten Gerätecomputer entscheiden Sie die Richtlinie zur Fixierung des Betriebssystem-Builds und die Treiber-Update-Richtlinie als Paar. „Welches Windows sollten Sie auf einen Industrie-PC setzen?“ ist eine nützliche Referenz.
Als Designentscheidung gilt fast immer: „Gibt es einen Ansatz, der das Ausliefern eines Treibers vermeidet, wählen Sie ihn, auch wenn die Implementierung etwas umständlicher ist“ ist die richtige Antwort. Genau dieser eine Punkt ist der ganze Grund, warum HID still und leise so stark ist.
11. Häufige Fehlschläge und wie man mit ihnen umgeht
| Symptom | Wahrscheinliche Ursache | Was zu tun ist |
|---|---|---|
| Funktioniert auf dem Entwicklungsrechner, aber nicht beim Kunden vor Ort | Die COM-Nummer ist in der Konfiguration fest codiert | Zur Laufzeit aus VID/PID und Seriennummer auflösen (8.1) |
| Fehlverhalten, sobald ein zweites Gerät angeschlossen wird | Das Gerät meldet keine Seriennummer | Die Gerätewahl überdenken. Andernfalls den Port fixieren und Beschriftungen verwenden |
| Ein Gerät, aber es greift jedes Mal einen anderen Partner | Ein zusammengesetztes Gerät wurde ohne Unterscheidung der Funktion geschlüsselt | MI_xx, HID-Usage oder den Geräteschnittstellenpfad in den Schlüssel aufnehmen (8.1) |
| Die App stürzt ab, wenn das Kabel gezogen wird | E/A gegen ein verfaultes Handle; Aufräumen, das sich allein auf PnP-Benachrichtigung verlässt | Entfernungsartige Fehler auch auf E/A-Abschlusspfaden als Sitzungsende behandeln (8.2) |
| Nur die erste Antwort ist langsam / geht verloren | Aufwachen aus Selective Suspend | Die Energieverwaltungseinstellung prüfen und deaktivieren (8.4) |
| Das Gerät antwortet nicht auf HID-Ausgabe | Die Daten sind um die Berichts-ID verschoben | Pufferlänge exakt OutputReportByteLength, Berichts-ID zuerst (4.3) |
| Es lassen sich überhaupt keine HID-Daten lesen | Das Ziel ist eine vom Betriebssystem exklusiv geöffnete TLC (Tastatur usw.) | Den Modus des Geräts umschalten. Nur zum Aufzählen mit Zugriff 0 öffnen (4.2) |
| WinUSB-Lesevorgänge kehren nie zurück | PIPE_TRANSFER_TIMEOUT nicht gesetzt |
Das Timeout in der Pipe-Richtlinie setzen (5.3) |
| Es verbindet sich bei jedem Timeout neu | Selbstabbruch wird fälschlich als Gerätetrennung gedeutet | Anhand eines Zustands wie eines Cancellation-Tokens triagieren (8.2) |
| Es wird bei wiederholtem Hot-Plugging allmählich schwerer | Fehlendes WinUsb_Free/CloseHandle |
Aufräumen an einer Stelle bündeln und auch auf Fehlerpfaden erreichen (5.3) |
| Unabhängige Variablen werden nach einem P/Invoke-Aufruf beschädigt | Eine nur teilweise deklarierte Struktur | Jedes Feld mit derselben Größe und demselben Layout wie die native Definition deklarieren (4.3) |
| Der Treiber lässt sich beim Kunden nicht installieren | Unsignierter Katalog (unabhängig davon, ob Sie ein .sys geschrieben haben) |
Die Erzeugung und Signierung der .cat in den Verteilungsplan aufnehmen (Abschnitt 10) |
| Der Start von zwei Instanzen lässt eine davon fehlschlagen | WinUSB erlaubt keinen gleichzeitigen Zugriff | Mehrfachinstanzen verhindern, oder alles durch einen residenten Dienst leiten |
| Das SDK lässt sich in einem 64-Bit-Build nicht laden | Eine nur-32-Bit-DLL | In einen anderen Prozess auslagern und über IPC verbinden (Abschnitt 6) |
| Keine Möglichkeit zu erkennen, ob der Datenverkehr tatsächlich durchkommt | Kein Beobachtungsmittel | Ein USB-Protokollanalysator, eine usbmon-äquivalente Aufzeichnung, ein implementiertes Kommunikationsprotokoll |
Die letzte Zeile lässt sich leicht abtun, ist aber wichtig. Von Anfang an ein Mittel zu haben, um zu triagieren, „welche Seite schuld ist (die Anwendung oder das Gerät)“, verkürzt die Zeit, in der die Ursache unbekannt ist, drastisch. Was den virtuellen COM-Ansatz vor Ort stark macht, ist, dass Terminal-Software jedem ein Triage-Werkzeug an die Hand gibt. Wählen Sie HID oder WinUSB, bauen Sie sich die Protokolle und die Test-CLI, die an dessen Stelle treten, selbst.
12. Zusammenfassung
- Wie Sie ein USB-Gerät ansprechen, wird nicht vom Gerät selbst bestimmt, sondern davon, welcher Treiber darauf geladen hat. Beginnen Sie damit, Hardware-ID, kompatible ID und Geräteinstanzpfad im Geräte-Manager zu betrachten.
- Die offizielle Auswahlreihenfolge lautet „das Einfachste zuerst“: Standard-Klassentreiber → WinUSB (eine Anwendung) → UMDF (mehrere Anwendungen) → KMDF. Einen eigenen Treiber zu schreiben, ist das letzte Mittel.
- Virtual COM ist leicht zu implementieren und leicht vor Ort zu triagieren, aber die COM-Nummer ist keine Kennung. Lösen Sie sie zur Laufzeit aus VID/PID und Seriennummer auf. Der Durchsatz unterscheidet sich um eine Größenordnung, je nachdem, ob es sich um eine USB-UART-Brücke oder natives CDC handelt, nehmen Sie es also nicht an — messen Sie.
- HID ist eine starke Option, die bidirektionale Kommunikation ohne jede Treiberverteilung bietet, aber Collections, die Mäusen, Tastaturen, Touch und Stiften entsprechen, werden vom Betriebssystem exklusiv geöffnet und sind tabu, und die Bandbreite von Interrupt-Transfers ist Ihre Obergrenze.
- WinUSB ist für große Datenmengen und proprietäre Protokolle gedacht. Allerdings gilt „keine INF erforderlich“ nur für Geräte mit OS-Deskriptoren unter Windows 8 und höher, und gleichzeitiger Zugriff von mehreren Anwendungen ist unmöglich. Für neue Firmware sind OS-2.0-Deskriptoren die erste Wahl.
- Ein Hersteller-SDK ist eine Voraussetzung, keine Option. Inventarisieren Sie Bitness, Threading-Einschränkungen, Weiterverteilungsbedingungen und Support-Lebensdauer bereits in der Gerätewahlphase, und verpacken Sie das SDK anwendungsseitig in eine dünne Abstraktionsschicht.
- Unabhängig vom Ansatz gestalten Sie diese vier selbst: eindeutige Identifikation, Hot-Plug-Handhabung, geschichtete Timeouts und Energieverwaltung. Daher kommt „es funktioniert manchmal nicht“. Treiben Sie bei zusammengesetzten Geräten die Identifikation bis auf die Funktionsebene hinunter, und erfassen Sie Trennungen sowohl über PnP-Benachrichtigung als auch über E/A-Fehler.
- Liefern Sie ein Treiberpaket aus, ist Katalogsignierung selbst ohne eigenes
.syserforderlich. Enthält es eine Kernelmodus-Binärdatei, benötigen Sie zusätzlich die Microsoft-Signierung unter 1607 und höher (und ein EV-Zertifikat). Attestierungssignierung überspringt HLK, ist aber auf Windows 10 Desktop und höher beschränkt, prüfen Sie sie also gegen Ihre Tabelle unterstützter Betriebssysteme, bevor Sie sich dafür entscheiden. Gibt es einen Ansatz, der das Ausliefern eines Treibers vermeidet, wählen Sie ihn — in der Praxis ist das fast immer die richtige Antwort.
Verwandte Artikel
- Fallstricke von Apps mit serieller Kommunikation - Über Wiederverbindung und Protokollierungsdesign
- Best Practices für die Prüfung und Anzeige des Zustands externer Geräte - Gestaltung jenseits eines einzelnen „Verbunden“
- Win32-APIs sicher aus C# aufrufen — Ein praktischer P/Invoke-Leitfaden
- Native DLLs aus C# aufrufen: C++/CLI-Wrapper gegenüber P/Invoke
- Ein praktischer Leitfaden, um auf gewöhnlichem Windows so nah wie möglich an Soft-Echtzeit heranzukommen
- Eine Windows-App-Verteilungsmethode wählen - MSI/MSIX/ClickOnce/xcopy/Custom Updater
- Welches Windows sollten Sie auf einen Industrie-PC setzen? — Ein praktischer Leitfaden zu Windows IoT Enterprise / LTSC
- Ein praktischer Leitfaden zu Process Monitor (ProcMon) — „Einstellung nicht übernommen“ und „ACCESS DENIED“ in 10 Minuten eingrenzen
Verwandte Beratungsleistungen
KomuraSoft LLC übernimmt das Integrationsdesign zwischen USB-verbundenen Geräten oder Instrumenten und Windows-Anwendungen, das Verpacken und die 64-Bit-Migration bestehender SDKs sowie die Ursachenuntersuchung und -behebung für Geräteintegrationsanwendungen, die bei Hot-Plug oder Wiederverbindung instabil werden.
- Windows-Anwendungsentwicklung
- Technische Beratung und Design-Review
- Fehleruntersuchung und Ursachenanalyse
- Kontakt
Referenzlinks
-
Microsoft Learn, USB device class drivers included in Windows. Die Liste der mit Windows ausgelieferten USB-Klassentreiber (Usbaudio.sys / Usbser.sys / Hidclass.sys und Hidusb.sys / Usbscan.sys / Usbprint.sys / Usbstor.sys / Usbvideo.sys und Ähnliches); dass Hersteller keine Treiber für unterstützte Geräteklassen schreiben sollten; dass WinUSB (Winusb.sys) für nicht klassifizierte Klassen einschließlich Vendor Specific (FFh) empfohlen wird; dass Usbccgp.sys für zusammengesetzte Geräte pro Funktion ein PDO erzeugt; und die Unterscheidung zwischen der Setup-Klasse USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}) und der USB-Klasse. Die Aussage in der CDC-Zeile (02h), dass „unter Windows 10 Usbser.inf Usbser.sys automatisch als Funktionstreiber lädt“, sowie der Weg, Unterklasse 02h (ACM) mit einer benutzerdefinierten, auf mdmcpq.inf verweisenden INF zu behandeln, stammen ebenfalls von derselben Seite. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Choose a driver model for developing a USB client driver. Die Auswahlreihenfolge „mit dem einfachsten Ansatz beginnen“ (Standard-Klassentreiber → WinUSB → UMDF → KMDF); dass WinUSB für den Zugriff von einer einzigen Anwendung, Bulk-/Interrupt-/isochrone Endpunkte und die Ausrichtung auf Windows XP SP2 und höher geeignet ist; dass WinUSB nicht für gleichzeitigen Zugriff von mehreren Anwendungen verwendet werden kann; und die Fähigkeitsvergleichstabelle WinUSB / UMDF / KMDF (isochrone Transfers von WinUSB ab Windows 8.1 unterstützt, von UMDF nicht unterstützt). ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, USB serial driver (Usbser.sys). Dass das Setzen von Klasse 02 und Unterklasse 02 im Gerätedeskriptor die Standard-Usbser.inf über die kompatible ID (USB\Class_02&SubClass_02) zuordnen lässt, sodass Usbser.sys automatisch lädt, ohne eine benutzerdefinierte INF zu verteilen; dass eine andere Unterklasse als 02 nicht automatisch geladen wird; dass mit CDC-Geräten aus dem Namespace Windows.Devices.SerialCommunication kommuniziert werden kann; und dass Selective Suspend standardmäßig deaktiviert ist und über den Registrierungswert IdleUsbSelectiveSuspendPolicy konfiguriert werden kann. ↩ ↩2 ↩3
-
Microsoft Learn, HID Architecture. Dass der HID-Klassentreiber (hidclass.sys) zwischen HID-Clients und Transporten abstrahiert; die Liste der von Windows unterstützten Top-Level-Collections und ihrer Zugriffsmodi (Maus, Tastatur, Stift, Touchscreen und Präzisions-Touchpad exklusiv; Spielcontroller, Sensor, Barcode-Scanner und andere geteilt); dass der Raw Input Manager (RIM) aus Sicherheitsgründen diese Geräte exklusiv öffnet; und dass selbst bei exklusiv geöffneten Geräten das Öffnen eines Handles ohne Anforderung von Lese-/Schreibzugriff das Abrufen von Informationen über HidD_GetXxx erlaubt. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, WinUSB Device. Dass ein WinUSB-Gerät ein USB-Gerät ist, dessen Firmware über Microsoft-OS-Feature-Deskriptoren WINUSB als kompatible ID meldet, sodass Winusb.sys ohne benutzerdefinierte INF lädt; dass es vor Windows 8 keine automatische Zuordnung anhand der kompatiblen ID gab und eine benutzerdefinierte INF verpflichtend war (die integrierte Winusb.inf von Windows 8 erhielt Unterstützung für USB\MS_COMP_WINUSB, und eine aktualisierte INF wird für frühere Versionen über Windows Update bereitgestellt); den Mechanismus des OS-Zeichenkettendeskriptors beim Zeichenkettenindex 0xEE und den Herstellercode; dass compatibleID im Extended-Compat-ID-Deskriptor auf WINUSB gesetzt wird; dass die Registrierung einer DeviceInterfaceGUID über den Extended-Properties-Deskriptor Anwendungen erlaubt, das Gerät zu entdecken und zu bedienen; dass die Setup-Klasse USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}) statt der USB-Klasse verwendet werden sollte; und die Energieverwaltungseinstellungen DeviceIdleEnabled / DefaultIdleTimeout / UserSetDeviceIdleEnabled / SystemWakeEnabled. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Driver Signing Policy. Dass ab Windows 10 Version 1607 neue, nicht über das Dev Portal signierte Kernelmodus-Treiber nicht laden; dass ein EV-Code-Signing-Zertifikat erforderlich ist, um sich beim Windows-Hardware-Dev-Center-Programm zu registrieren; und die Ausnahmebedingungen, unter denen Cross-signierte Treiber weiterhin erlaubt sind (Upgrade auf 1607, Secure Boot deaktiviert, ein vor dem 29. Juli 2015 ausgestelltes Endentitätszertifikat). ↩ ↩2 ↩3
-
Microsoft Learn, Opening HID collections. Die Abfolge, in der eine benutzermodale Anwendung HID-Collections mit den SetupDi*-Funktionen identifiziert, sie mit CreateFile öffnet, vorabgeparste Daten und Informationen mit HidD_Xxx erhält, Eingabeberichte mit ReadFile liest und Ausgabeberichte mit WriteFile sendet, sowie Berichte mit HidP_Xxx interpretiert. ↩
-
Microsoft Learn, HIDP_CAPS structure (hidpi.h). Die vollständige Definition der Struktur (Usage / UsagePage / InputReportByteLength / OutputReportByteLength / FeatureReportByteLength / Reserved[17] / NumberLinkCollectionNodes und die zehn folgenden Number-Member, insgesamt USHORT × 32), und dass jede Berichtslänge ein Wert ist, der das eine Byte der Berichts-ID einschließt. ↩
-
Microsoft Learn, Sending HID Reports. Dass eine benutzermodale Anwendung WriteFile verwenden sollte, um Ausgabeberichte fortlaufend zu senden; dass die HidD_SetXxx-Familie (HidD_SetOutputReport / HidD_SetFeature) ebenfalls Ausgabe- und Feature-Berichte senden kann, HidD_SetXxx aber nur zum Setzen des aktuellen Zustands der Collection verwendet werden sollte; und die Warnung, dass „manche Geräte HidD_SetOutputReport nicht unterstützen und möglicherweise nicht mehr antworten, wenn diese Routine verwendet wird“. Außerdem aus HidD_SetOutputReport function, dass ReportBufferLength durch HIDP_CAPS’ OutputReportByteLength bestimmt wird und dass das erste Byte 0 sein sollte, wenn keine Berichts-IDs verwendet werden. ↩ ↩2
-
Microsoft Learn, HidDevice Class (Windows.Devices.HumanInterfaceDevice). Dass HidDevice ein Gerät darstellt, das einer Top-Level-Collection entspricht; den Ablauf, einen AQS-Selektor aus usagePage / usageId / vendorId / productId mit GetDeviceSelector zu erstellen und ihn mit FromIdAsync zu öffnen; und dass Anwendungen, die über diese Klasse auf HID-Geräte zugreifen, die entsprechenden DeviceCapability-Daten im Capabilities-Knoten des Manifests einschließen müssen. ↩
-
USB Implementers Forum, Universal Serial Bus Specification Revision 2.0. Dass die maximale Paketgröße eines Interrupt-Endpoints bei Full-Speed 64 Bytes und bei High-Speed 1024 Bytes beträgt; dass das Polling-Intervall (bInterval) bei Full-Speed 1–255 Millisekunden beträgt und bei High-Speed als 2^(bInterval-1) in Einheiten von 125 Mikrosekunden ausgedrückt wird (Abschnitt 9.6.6 Endpoint); und die Bandbreiteneigenschaften der Transfertypen Kontrolle, Bulk, Interrupt und isochron. ↩ ↩2
-
Microsoft Learn, Microsoft OS 2.0 Descriptors Specification. Dass Version 2.0 der Microsoft-OS-Deskriptoren definiert wurde, um die Einschränkungen und Zuverlässigkeitsprobleme von Version 1.0 zu lösen, und dass die Zielbetriebssysteme Windows 10 und Windows 8.1 Preview sind. ↩
-
Microsoft Learn, WinUSB (Winusb.sys) Installation for Developers. Dass Sie die GUID setzen, indem Sie entweder den Zeichenketteneintrag
DeviceInterfaceGUIDoder den Multi-ZeichenketteneintragDeviceInterfaceGUIDsunter demDevice Parameters-Schlüssel des Geräts hinzufügen; dass dasAddRegeiner benutzerdefinierten INF alsHKR,,DeviceInterfaceGUIDs,0x10000,"{...}"geschrieben wird (0x10000 = REG_MULTI_SZ); dass „wenn Winusb.sys als Funktionstreiber geladen wird, es den RegistrierungswertDeviceInterfaceGUIDsliest und die Geräteschnittstelle mit der angegebenen GUID darstellt“ und dass „bei jedem Laden Winusb.sys eine Geräteschnittstelle mit der unter demDeviceInterfaceGUIDs-Schlüssel angegebenen Geräteschnittstellenklasse registriert“; dass die benutzermodale Seite registrierte Schnittstellen mitSetupDiGetClassDevsaufzählt, bevor eine anWinUsb_Initializeübergeben wird; und dass ein Treiberpaket eine signierte Katalogdatei erfordert. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, WinUsb_ReadPipe function (winusb.h). Dass die Angabe von Overlapped die Funktion sofort zurückkehren und die Operation asynchron laufen lässt; dass in diesem Fall GetLastError ERROR_IO_PENDING zurückgibt und Erfolg oder Fehlschlag mit WinUsb_GetOverlappedResult geprüft wird; dass bei asynchronem Betrieb (Overlapped ungleich NULL) LengthTransferred auf NULL gesetzt werden darf; dass selbst bei Übergabe eines von NULL verschiedenen LengthTransferred der Wert im Moment der Rückkehr der Funktion bis zum Abschluss der Overlapped-Operation bedeutungslos ist und die tatsächliche Anzahl gelesener Bytes mit WinUsb_GetOverlappedResult ermittelt werden muss; und dass bei synchronen Aufrufen (Overlapped NULL) LengthTransferred nicht NULL sein darf. ↩
-
Microsoft Learn, Windows.Devices.Usb Namespace. Dass sich dieser Namespace an WinUSB-Geräte richtet, die vom integrierten winusb.sys behandelt werden (kompatible ID USB\MS_COMP_WINUSB); dass er nicht auf die Geräteklassen Audio (0x01) / HID (0x03) / Bild (0x06) / Drucker (0x07) / Massenspeicher (0x08) / Smartcard (0x0B) / Audio-Video (0x10) / Wireless-Controller (0xE0) zugreifen kann; dass die usb-Gerätefähigkeit im Manifest deklariert werden muss und dass ab Windows 10 Version 1809 die Angabe von VendorId/ProductId nicht mehr erforderlich ist; und dass Gerätestapel mit oberen oder unteren Filtertreibern generell unzugänglich sind. ↩
-
Microsoft Learn, CM_Register_Notification function (cfgmgr32.h). Dass sie unter Windows 8 und höher verfügbar ist und dass RegisterDeviceNotification verwendet werden sollte, wenn Windows 7 oder älter als Ziel dient; die Filtertypen CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE / DEVICEHANDLE / DEVICEINSTANCE; dass PnP-Ereignisse so schnell wie möglich verarbeitet werden sollten und dass alles, was blockieren kann, wie E/A, asynchron auf einem anderen Thread erfolgen sollte; und dass diese Funktion nicht über bestehende Geräteschnittstellen benachrichtigt, sodass CM_Get_Device_Interface_List nach der Registrierung aufgerufen werden muss, wobei zwischenzeitlich aktivierte Schnittstellen sowohl in der Benachrichtigung als auch in der Liste erscheinen. ↩ ↩2
-
Microsoft Learn, RegisterDeviceNotificationW function (winuser.h). Dass es die Registrierungsfunktion ist, mit der eine Anwendung Gerätebenachrichtigungen empfängt und bei Erfolg ein Gerätebenachrichtigungs-Handle, bei Fehlschlag NULL zurückgibt. Außerdem aus WM_DEVICECHANGE message und DBT_DEVICEARRIVAL event, dass WM_DEVICECHANGE mit wParam auf DBT_DEVICEARRIVAL gesetzt gesendet wird, wenn ein Gerät oder Medium eingesteckt wurde und verfügbar wird. ↩
-
Microsoft Learn, PnP Device Installation Signing Requirements. Dass ein Treiberpaket die Signierungsanforderungen erfüllen muss, um in den Driver Store aufgenommen zu werden; dass, um für die PnP-Geräteinstallation als „signiert“ zu gelten, die Katalogdatei des Treiberpakets mit WHQL oder einem Drittanbieter-Release-Zertifikat (SPC, ein kommerzielles Release-Zertifikat) signiert sein muss; dass die Signierungsanforderungen zum Laden einer Kernelmodus-Treiber-Binärdatei separat davon auferlegt werden; dass auf 64-Bit-Windows die Kernelmodus-Code-Signing-Richtlinie WHQL- oder SPC-Signierung erfordert; und dass manche Editionen, wie Windows 10 im S-Modus, nur WHQL-signierte Kataloge akzeptieren. ↩ ↩2
-
Microsoft Learn, Driver Signing Options. Dass Dashboard-signierte Treiber, die die HLK-Tests bestanden haben, unter Windows Vista und höher einschließlich der Windows-Server-Editionen funktionieren und der empfohlene Ansatz sind, weil sie für alle Betriebssystemversionen signiert werden können; dass Attestierungssignierung als „nur für Testzwecke“ positioniert ist und keine HLK-Tests erfordert; dass Attestierungssignierte Treiber nicht für die Allgemeinheit an Windows Update veröffentlicht werden können; dass sie nur unter Windows 10 Desktop und höher gültig sind; dass die Ausrichtung auf frühere Windows-Versionen die Einreichung von HLK-/HCK-Testprotokollen erfordert; dass Windows Server 2016 und höher keine Attestierungssignierungs-Einreichungen akzeptieren und nur HLK-bestandene Treiber laden; und dass Attestierungssignierung ein EV-Zertifikat erfordert, ohne den Treiber Windows-zertifiziert zu machen. ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Code-Design für Geschäftssysteme — Produkt- und Kundencodes festlegen, und Prüfziffern
Ein praxisnaher Leitfaden zur Festlegung des Codesystems eines Geschäftssystems, einschließlich Produkt- und Kundencodes. Behandelt eine ...
Das Datenbankschema Ihrer Business-Anwendung versionieren ── Migrationspraxis gegen „jeder Kunde hat eine andere Datenbank“
Ein praktischer Leitfaden zur Versionierung des Datenbankschemas einer Business-Anwendung, deren Datenbanken über zahlreiche Kundenstando...
CI/CD für WinForms-/WPF-Anwendungen in der Praxis ── Vom Build über die Signierung bis zur Distribution mit GitHub Actions automatisieren
Ein praktischer Leitfaden zur Einrichtung von CI/CD für WinForms-/WPF-Anwendungen mit GitHub Actions. Behandelt eine minimale YAML für Bu...
Wenn die eigene Windows-App als Virus gemeldet wird — Umgang mit Fehlalarmen von Microsoft Defender und den Auswirkungen auf die Leistung
Wir legen dar, wie richtig reagiert wird, wenn Microsoft Defender die eigene Windows-App als Malware meldet: wie moderne Antivirus-Erkenn...
Energiesparmodus, Ruhezustand, Modern Standby und lang laufende Anwendungen ── Design gegen „über Nacht stehengeblieben“
Warum eine lang laufende Windows-Anwendung morgens beim Nachsehen „stehengeblieben“ sein kann, aufgearbeitet anhand der Unterschiede zwis...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Ich möchte ein USB-Gerät aus meiner Anwendung heraus nutzen. Muss ich selbst einen Treiber schreiben?
- In den meisten Fällen nicht. Microsofts offizielle Anleitung sagt ausdrücklich, mit dem einfachsten Ansatz zu beginnen und erst dann zu einem komplexeren zu wechseln, wenn es unumgänglich ist. Gehört das Gerät zu einer Standard-USB-Klasse (CDC, HID, Massenspeicher und Ähnliches), lädt ein in Windows integrierter Klassentreiber automatisch, und es ist kein Treiber nötig. Gehört es zu keiner Standardklasse und greift nur eine Anwendung darauf zu, können Sie WinUSB (winusb.sys) unverändert als Funktionstreiber verwenden. Erst wenn mehrere Anwendungen gleichzeitig zugreifen müssen, wechseln Sie zu einem UMDF-Treiber, und nur wenn das nicht funktioniert, zu einem KMDF-Treiber. Einen eigenen Treiber zu schreiben, ist das letzte Mittel.
- Was ist die größte Schwäche des virtuellen COM-Port-Ansatzes (USB-Seriell)?
- Dass die COM-Portnummer nicht die Identität des Geräts ist. Selbst bei demselben Gerät kann sich die COM-Nummer ändern, wenn es an einen anderen USB-Port angeschlossen wird, und schließen Sie mehrere an, lässt sich anhand der Nummer allein nicht unterscheiden, welches welches ist. „COM3“ in eine Konfigurationsdatei zu schreiben, ist eine betriebliche Praxis, die vor Ort garantiert bricht. In der Praxis besteht die richtige Lösung darin, die Zuordnung zwischen VID/PID plus Seriennummer und der COM-Nummer zur Laufzeit nachzuschlagen, etwa über Win32_PnPEntity, und diese zu öffnen. Zusätzlich ist die serielle Kommunikation ein Byte-Strom, sodass Nachrichtengrenzen nicht garantiert sind und Sie einen eigenen Parser benötigen, der in einem Empfangspuffer akkumuliert und dann Rahmen herausschneidet.
- Ich habe gehört, der HID-Ansatz sei einfach, weil er keinen Treiber benötigt. Worauf sollte ich achten?
- Auf drei Dinge. Erstens die Geschwindigkeit: HID verwendet Interrupt-Transfers und eignet sich daher nicht für die anhaltende Übertragung großer Datenmengen. Zweitens die Berichtslänge: Der an ReadFile übergebene Puffer muss exakt der von HidP_GetCaps zurückgegebenen InputReportByteLength entsprechen, und das erste Byte ist die Berichts-ID. Dies falsch zu machen, ist der klassische Fehler, bei dem nichts gelesen wird oder die App abstürzt. Drittens die Exklusivität: Top-Level-Collections, die Mäusen, Tastaturen, Touchscreens und Stiften entsprechen, werden vom Windows Raw Input Manager exklusiv geöffnet, sodass eine Anwendung nicht daraus lesen oder hineinschreiben kann. Öffnen Sie jedoch ein Handle, ohne Lese-/Schreibzugriff anzufordern, können Sie dennoch über die HidD_GetXxx-Familie Informationen erhalten.
- Ich möchte WinUSB verwenden. Kann ich es unverändert auf ein bestehendes Gerät anwenden?
- Oft nicht. winusb.sys lädt nur dann automatisch ohne INF-Datei, wenn die Firmware des Geräts Microsoft-OS-Deskriptoren trägt und WINUSB als kompatible ID meldet — ein „WinUSB-Gerät“ — und nur unter Windows 8 und höher. Die integrierte Winusb.inf begann erst unter Windows 8 mit der Übereinstimmung anhand der kompatiblen ID, sodass bei Einbeziehung von Windows 7 oder älter die Verteilung einer INF gesetzt ist. Qualifiziert sich Ihr bestehendes Gerät nicht als WinUSB-Gerät, müssen Sie zusätzlich eine eigene benutzerdefinierte INF vorbereiten und verteilen, die die Hardware-ID angibt. Das Umschalten des Treibers mit einem Werkzeug wie Zadig während der Entwicklung ist zur Verifikation nützlich, entfernt aber den Treiber des Herstellers — machen Sie daraus nicht Ihren Produktivverteilungsmechanismus. Lässt sich die Firmware ändern, ist das Hinzufügen von OS-Deskriptoren die bei Weitem sauberste Lösung, und für ein neues Design sind die über BOS angekündigten OS-2.0-Deskriptoren (Windows 8.1 und höher) die erste Wahl.
- Wie sollte eine Anwendung das Einstecken und Abziehen von USB-Geräten verfolgen?
- Abonnieren Sie PnP-Benachrichtigungen, statt zu pollen. Für eine Desktop-Anwendung unter Windows 8 oder höher ist CM_Register_Notification mit einem Geräteschnittstellenfilter der Standardansatz; wird auch Windows 7 oder älter einbezogen, verwenden Sie RegisterDeviceNotification und WM_DEVICECHANGE. Ein Vorbehalt: CM_Register_Notification benachrichtigt Sie nicht über Schnittstellen, die zum Registrierungszeitpunkt bereits existieren, registrieren Sie also zuerst und zählen Sie dann die bestehenden mit CM_Get_Device_Interface_List auf (in umgekehrter Reihenfolge verpassen Sie einige). Führen Sie außerdem innerhalb des Callbacks nichts aus, das blockieren kann — etwa E/A —, das ist gefährlich; reichen Sie es an einen anderen Thread weiter. Darüber hinaus ist es wichtig, PnP-Benachrichtigungen nicht zu Ihrem einzigen Einstiegspunkt zu machen. Eine laufende E/A-Operation kann vor oder gleichzeitig mit der Benachrichtigung mit einem Entfernungs- oder Abbruchfehler abschließen. Behandeln Sie entfernungsartige Fehler auf jedem Lese- und Schreibabschlusspfad als Sitzungsende, und kombinieren Sie PnP-Benachrichtigungen als ergänzendes Signal, das Trennungen im Leerlauf erfasst.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.