USB Gateware: Part 2 - WCID Descriptors
This series of tutorial walks through the process of implementing a complete USB device with Cynthion and LUNA:
USB Gateware: Part 2 - WCID Descriptors (This tutorial)
The goal of this tutorial is to define the descriptors that will tell Microsoft Windows to use the built-in generic WinUSB driver to communicate with our device.
This tutorial is optional and only required if you would like to use your device on Windows.
Prerequisites
Complete the USB Gateware: Part 1 - Enumeration tutorial.
WCID Devices
WCID devices or “Windows Compatible ID devices”, are USB devices that provide extra information to Windows in order to facilitate automatic driver installation or, more frequently, allow programs to obtain direct access to the device.
Historically, Windows required manual installation of drivers for non-class devices with custom vendor interfaces. Contrasted with Linux or macOS which will automatically assign a generic USB driver that allows for direct interaction with the device’s endpoints via a cross-platform library such as libusb or operating system API’s.
Microsoft eventually relented and now provide a Windows-specific mechanism for a device to advertise that it requires a generic WinUSB driver.
The full details are documented in the Microsoft OS 1.0 and Microsoft OS 2.0 specifications but the basic mechanism consists of a set of Windows-specific descriptor requests made by the host whenever a new device is plugged in.
For Microsoft OS 1.0, this boils down to three descriptor requests we need to be able to handle:
Microsoft OS String Descriptor
Microsoft Compatible ID Feature Descriptor
Microsoft Extended Properties Feature Descriptor
Microsoft OS String Descriptor
To start with, edit your gateware-usb-device.py
file from the previous tutorial and add/modify the highlighted lines:
1from amaranth import *
2from luna.usb2 import USBDevice
3from usb_protocol.emitters import DeviceDescriptorCollection
4
5from usb_protocol.emitters.descriptors.standard import get_string_descriptor
6
7...
8
9class GatewareUSBDevice(Elaboratable):
10 ...
11
12 def elaborate(self, platform):
13 m = Module()
14
15 # configure cynthion's clocks and reset signals
16 m.submodules.car = platform.clock_domain_generator()
17
18 # request the physical interface for cynthion's TARGET C port
19 ulpi = platform.request("target_phy")
20
21 # create the USB device
22 m.submodules.usb = usb = USBDevice(bus=ulpi)
23
24 # create our standard descriptors and add them to the device's control endpoint
25 descriptors = self.create_standard_descriptors()
26 control_endpoint = usb.add_standard_control_endpoint(
27 descriptors,
28 # the blockram descriptor handler lacks support for
29 # non-contiguous string descriptor indices, which is
30 # required for the Microsoft OS string descriptor at 0xEE.
31 avoid_blockram=True,
32 )
33
34 # add the microsoft os string descriptor
35 descriptors.add_descriptor(get_string_descriptor("MSFT100\xee"), index=0xee)
36
37 # configure the device to connect by default when plugged into a host
38 m.d.comb += usb.connect.eq(1)
39
40 return m
The Microsoft OS String Descriptor responds to a standard String Descriptor request with an index of 0xee
. It encodes two values:
0x12, # Descriptor Length: 18 bytes
0x03, # Descriptor Type: 3 = String
0x4d, 0x00, # M
0x53, 0x00, # S
0x46, 0x00, # F
0x54, 0x00, # T
0x31, 0x00, # 1
0x30, 0x00, # 0
0x30, 0x00, # 0
0xee, 0x00, # Vendor Code: 0xee
The first 14 bytes correspond to the little-endian encoded Unicode string MSFT100
, with the remaining two bytes corresponding to the Vendor Code Windows should use when requesting the other descriptors. This is often set to the same value as the Microsoft OS String Descriptor index of 0xee
, but you can use another value if it conflicts with an existing Vendor Code used by your device.
Microsoft Compatible ID Feature Descriptor
Next, add the Microsoft Compatible ID Feature Descriptor:
1from amaranth import *
2from luna.usb2 import USBDevice
3from usb_protocol.emitters import DeviceDescriptorCollection
4
5from luna.gateware.usb.request.windows import (
6 MicrosoftOS10DescriptorCollection,
7)
8from usb_protocol.emitters.descriptors.standard import get_string_descriptor
9
10
11VENDOR_ID = 0x1209 # https://pid.codes/1209/
12PRODUCT_ID = 0x0001
13
14class GatewareUSBDevice(Elaboratable):
15 ...
16
17 def elaborate(self, platform):
18 ...
19
20 # add the microsoft os string descriptor
21 descriptors.add_descriptor(get_string_descriptor("MSFT100\xee"), index=0xee)
22
23 # add a microsoft descriptor collection for our other two microsoft descriptors
24 msft_descriptors = MicrosoftOS10DescriptorCollection()
25
26 # add the microsoft compatible id feature descriptor
27 with msft_descriptors.ExtendedCompatIDDescriptor() as c:
28 with c.Function() as f:
29 f.bFirstInterfaceNumber = 0
30 f.compatibleID = 'WINUSB'
31
32 # configure the device to connect by default when plugged into a host
33 m.d.comb += usb.connect.eq(1)
34
35 return m
Our remaining descriptors are not returned via Standard Requests, instead they are implemented as Vendor Requests with Microsoft-defined Vendor Indices and the Vendor Code supplied in the Microsoft OS String Descriptor. We will implement the actual vendor request handler in the final step of the tutorial but for now we are just defining the Microsoft OS 1.0 Descriptor Collection that will contain these descriptors.
Our example is defining the simplest possible Compatible ID Feature descriptor, specifying a Function with a device interface number of 0
and a compatible ID of WINUSB
. This is how we tell Windows to use the generic WinUSB driver for the interface.
If our device had multiple interfaces we could simply extended this by adding additional functions for each interface like so:
with msft_descriptors.ExtendedCompatIDDescriptor() as c:
with c.Function() as f:
f.bFirstInterfaceNumber = 0
f.compatibleID = 'WINUSB'
with c.Function() as f:
f.bFirstInterfaceNumber = 1
f.compatibleID = 'WINUSB'
...
Microsoft Extended Properties Feature Descriptor
We now come to our third descriptor, the Microsoft Extended Properties Feature Descriptor:
1from amaranth import *
2from luna.usb2 import USBDevice
3from usb_protocol.emitters import DeviceDescriptorCollection
4
5from luna.gateware.usb.request.windows import (
6 MicrosoftOS10DescriptorCollection,
7)
8from usb_protocol.emitters.descriptors.standard import get_string_descriptor
9from usb_protocol.types.descriptors.microsoft10 import RegistryTypes
10
11..
12
13class GatewareUSBDevice(Elaboratable):
14 ...
15
16 def elaborate(self, platform):
17 ...
18
19 # add the microsoft os string descriptor
20 descriptors.add_descriptor(get_string_descriptor("MSFT100\xee"), index=0xee)
21
22 # add a microsoft descriptor collection for our other two microsoft descriptors
23 msft_descriptors = MicrosoftOS10DescriptorCollection()
24
25 # add the microsoft compatible id feature descriptor
26 with msft_descriptors.ExtendedCompatIDDescriptor() as c:
27 with c.Function() as f:
28 f.bFirstInterfaceNumber = 0
29 f.compatibleID = 'WINUSB'
30
31 # add microsoft extended properties feature descriptor
32 with msft_descriptors.ExtendedPropertiesDescriptor() as d:
33 with d.Property() as p:
34 p.dwPropertyDataType = RegistryTypes.REG_SZ
35 p.PropertyName = "DeviceInterfaceGUID"
36 p.PropertyData = "{88bae032-5a81-49f0-bc3d-a4ff138216d6}"
37
38 # configure the device to connect by default when plugged into a host
39 m.d.comb += usb.connect.eq(1)
40
41 return m
The Extended Properties Feature Descriptor can be used to define additional device registry settings but, in our example, we only define the Device Interface GUID we’d like our device to be accessed with.
In this case it’s the Microsoft-defined GUID of {88bae032-5a81-49f0-bc3d-a4ff138216d6}
which is defined as “all USB devices that don’t belong to another class”. If, for example, our device were a Keyboard or Mouse we’d need to use the appropriate value here.
Microsoft Descriptor Request Handler
Finally, now that all our descriptors are defined we need to add the actual Vendor Request Handler that will be responsible for responding to descriptor requests from a Windows Host:
1from amaranth import *
2from luna.usb2 import USBDevice
3from usb_protocol.emitters import DeviceDescriptorCollection
4
5from luna.gateware.usb.request.windows import (
6 MicrosoftOS10DescriptorCollection,
7 MicrosoftOS10RequestHandler,
8)
9from usb_protocol.emitters.descriptors.standard import get_string_descriptor
10from usb_protocol.types.descriptors.microsoft10 import RegistryTypes
11
12..
13
14class GatewareUSBDevice(Elaboratable):
15 ...
16
17 def elaborate(self, platform):
18 ...
19
20 # add the microsoft os string descriptor
21 descriptors.add_descriptor(get_string_descriptor("MSFT100\xee"), index=0xee)
22
23 # add a microsoft descriptor collection for our other two microsoft descriptors
24 msft_descriptors = MicrosoftOS10DescriptorCollection()
25
26 # add the microsoft compatible id feature descriptor
27 with msft_descriptors.ExtendedCompatIDDescriptor() as c:
28 with c.Function() as f:
29 f.bFirstInterfaceNumber = 0
30 f.compatibleID = 'WINUSB'
31
32 # add microsoft extended properties feature descriptor
33 with msft_descriptors.ExtendedPropertiesDescriptor() as d:
34 with d.Property() as p:
35 p.dwPropertyDataType = RegistryTypes.REG_SZ
36 p.PropertyName = "DeviceInterfaceGUID"
37 p.PropertyData = "{88bae032-5a81-49f0-bc3d-a4ff138216d6}"
38
39 # add the request handler for Microsoft descriptors
40 msft_handler = MicrosoftOS10RequestHandler(msft_descriptors, request_code=0xee)
41 control_endpoint.add_request_handler(msft_handler)
42
43 # configure the device to connect by default when plugged into a host
44 m.d.comb += usb.connect.eq(1)
45
46 return m
LUNA provides a pre-defined implementation for handling Microsoft OS10 Descriptor Requests and only requires the descriptor collection and the vendor request code we defined in the Microsoft OS10 String Descriptor.
Testing the Device
Connect
For this tutorial you will need to connect the Cynthion TARGET C port to a Windows computer for testing.
Plug the CONTROL port into the computer you’ve been using to control Cynthion. If this is the same machine as the Windows computer you’re using to test, plug it in there.
Build
Build the device gateware and upload it to your Cynthion by typing the following into your terminal shell:
python ./gateware-usb-device.py
If everything went well we should now be able to check if Windows can recognize the device.
Test
To test whether the WCID descriptors have been recognized, open the Windows Device Manager and look for the device under the “Universal Serial Bus devices” section:
You should find that the Python test program from USB Gateware: Part 1 - Enumeration now works as expected:
1import usb1
2
3def list_devices(context):
4 for device in context.getDeviceList():
5 try:
6 manufacturer = device.getManufacturer()
7 product = device.getProduct()
8 print(f"{device}: {manufacturer} - {product}")
9 except Exception as e:
10 print(f"{device}: {e}")
11
12if __name__ == "__main__":
13 with usb1.USBContext() as context:
14 list_devices(context)
Run the file with:
python ./test-gateware-usb-device.py
And, if the device is recognized, you should see a line like:
Bus 000 Device 001: ID 1d5c:5010: Fresco Logic, Inc. - USB2.0 Hub
Bus 000 Device 002: ID 1d5c:5000: Fresco Logic, Inc. - USB3.0 Hub
Bus 000 Device 003: ID 1d50:615c: Great Scott Gadgets - Cynthion Apollo Debugger
Bus 000 Device 007: ID 1209:0001: Cynthion Project - Gateware USB Device
Conclusion
Our device can now be enumerated by Microsoft Windows but it can’t actually do anything yet. In the next part we’ll learn how to add Vendor Request Handlers to our device that allow it to receive and respond to control requests from the host: USB Gateware: Part 3 - Control Transfers
Exercises
Modify the example to use a different request code, does it still work?
Could you use the information you learnt in this tutorial modify the LUNA ACM Serial example example to support Windows?
Modify the
PropertyData
field of the extended properties descriptor to one of the Microsoft-provided USB device class drivers. What happens?
More information
Pete Batard’s excellent introduction to WCID Devices.
Microsoft USB device class drivers included in Windows.
Microsoft System-defined device setup classes available to vendors.
Source Code
1#!/usr/bin/env python3
2#
3# This file is part of Cynthion.
4#
5# Copyright (c) 2024 Great Scott Gadgets <info@greatscottgadgets.com>
6# SPDX-License-Identifier: BSD-3-Clause
7
8from amaranth import *
9from luna.usb2 import USBDevice
10from usb_protocol.emitters import DeviceDescriptorCollection
11
12from luna.gateware.usb.request.windows import (
13 MicrosoftOS10DescriptorCollection,
14 MicrosoftOS10RequestHandler,
15)
16from usb_protocol.emitters.descriptors.standard import get_string_descriptor
17from usb_protocol.types.descriptors.microsoft10 import RegistryTypes
18
19VENDOR_ID = 0x1209 # https://pid.codes/1209/
20PRODUCT_ID = 0x0001
21
22class GatewareUSBDevice(Elaboratable):
23 """ A simple USB device that can also enumerate on Windows. """
24
25 def create_standard_descriptors(self):
26 """ Create the USB descriptors for the device. """
27
28 descriptors = DeviceDescriptorCollection()
29
30 # all USB devices have a single device descriptor
31 with descriptors.DeviceDescriptor() as d:
32 d.idVendor = VENDOR_ID
33 d.idProduct = PRODUCT_ID
34 d.iManufacturer = "Cynthion Project"
35 d.iProduct = "Gateware USB Device"
36
37 d.bNumConfigurations = 1
38
39 # and at least one configuration descriptor
40 with descriptors.ConfigurationDescriptor() as c:
41
42 # with at least one interface descriptor
43 with c.InterfaceDescriptor() as i:
44 i.bInterfaceNumber = 0
45
46 # interfaces also need endpoints to do anything useful
47 # but we'll add those later!
48
49 return descriptors
50
51
52 def elaborate(self, platform):
53 m = Module()
54
55 # configure cynthion's clocks and reset signals
56 m.submodules.car = platform.clock_domain_generator()
57
58 # request the physical interface for cynthion's TARGET C port
59 ulpi = platform.request("target_phy")
60
61 # create the USB device
62 m.submodules.usb = usb = USBDevice(bus=ulpi)
63
64 # create our standard descriptors and add them to the device's control endpoint
65 descriptors = self.create_standard_descriptors()
66 control_endpoint = usb.add_standard_control_endpoint(
67 descriptors,
68 # the blockram descriptor handler lacks support for
69 # non-contiguous string descriptor indices, which is
70 # required for the Microsoft OS string descriptor at 0xEE
71 avoid_blockram=True,
72 )
73
74 # add the microsoft os string descriptor
75 descriptors.add_descriptor(get_string_descriptor("MSFT100\xee"), index=0xee)
76
77 # add a microsoft descriptor collection for our other two microsoft descriptors
78 msft_descriptors = MicrosoftOS10DescriptorCollection()
79
80 # add the microsoft compatible id feature descriptor
81 with msft_descriptors.ExtendedCompatIDDescriptor() as c:
82 with c.Function() as f:
83 f.bFirstInterfaceNumber = 0
84 f.compatibleID = 'WINUSB'
85
86 # add microsoft extended properties feature descriptor
87 with msft_descriptors.ExtendedPropertiesDescriptor() as d:
88 with d.Property() as p:
89 p.dwPropertyDataType = RegistryTypes.REG_SZ
90 p.PropertyName = "DeviceInterfaceGUID"
91 p.PropertyData = "{88bae032-5a81-49f0-bc3d-a4ff138216d6}"
92
93 # add the request handler for Microsoft descriptors
94 msft_handler = MicrosoftOS10RequestHandler(msft_descriptors, request_code=0xee)
95 control_endpoint.add_request_handler(msft_handler)
96
97 # configure the device to connect by default when plugged into a host
98 m.d.comb += usb.connect.eq(1)
99
100 return m
101
102
103if __name__ == "__main__":
104 from luna import top_level_cli
105 top_level_cli(GatewareUSBDevice)
1import usb1
2
3# - list available usb devices ------------------------------------------------
4
5def list_available_usb_devices(context):
6 for device in context.getDeviceList():
7 try:
8 manufacturer = device.getManufacturer()
9 product = device.getProduct()
10 print(f"{device}: {manufacturer} - {product}")
11 except Exception as e:
12 print(f"{device}: {e}")
13
14
15# - main ----------------------------------------------------------------------
16
17if __name__ == "__main__":
18 with usb1.USBContext() as context:
19 list_available_usb_devices(context)