Search code examples
usbstm32cdcwinusb

How to work with WinUSB?


This is a follow-up to my previous question, Need to write driver for USB peripheral device?

Background

I'm working on designing a USB peripheral using an STM32 microcontroller (bare metal / no OS). The device will occasionally be connected to a Windows PC, and transfer a few KB of data in each direction. There will be a custom PC application that controls the data transfers, using a proprietary protocol (i.e. for the USB payloads).

The PC will always be the master (initiator) - it'll send commands, and the device will issue responses, with up to a few hundred bytes of data going in either direction in a single command or response. I think I'll want to use USB Bulk Transfer mode.

Option 1 - USB CDC

From what I understand, one option is I could use the USB Communications Device Class (CDC). On the device side, I could use sample code from ST for the USB CDC, e.g. from STM32Cube. On the PC side, the device would present as a Virtual COM Port (VCP). Then in software I'd basically have a raw, bidirectional stream, on top of which I'd have to define my messsage formats, commands, etc.

  1. Have I explained this correctly?

Option 2 - WinUSB

I'm having trouble wrapping my mind around exactly what this is, and how to work with it.

  1. What is the relationship of WinUSB to USB device classes? It seems to function as a "generic" USB class, but I can't find any documentation that spells it out as such.

  2. Does WinUSB provide any built-in message delimiters? e.g. Does WinUsb_WritePipe send the contents of the buffer down to the device as an atomic unit? Or do I just get raw streams like a VCP/UART?

  3. How does one implement WinUSB on the device? Is there sample code available? (Preferably for STM32.)

Choosing

  1. What are the relevant considerations in choosing between options 1 and 2 for my application?

Solution

    1. Yes, you described USC CDC ACM correctly.
    2. WinUSB exists to support devices that do not have a particular device class. If your device implements a device class such as Human Interface Device, Mass Storage Device, or Communications Device (CDC), you can just use the drivers that come with your operating system to talk to that device. If you want a more customizable and flexible USB interface that doesn't conform to one of those classes, you can use WinUSB to talk to your device. You could also write your own driver if you want, but that is going to be a lot of work and I wouldn't recommend it. Some people have written drivers that are alternatives to WinUSB: you can look up libusbK, libusb0.sys, and UsbDK for examples. WinUSB has the advantage that it comes with Windows so I wouldn't use one of those other drivers unless it has a specific feature you really need.
    3. I believe that WinUSB_WritePipe sends the data as a single USB transfer. A transfer has a specific definition in the USB specification. You can tell when a transfer ends because you will get a short packet at the end of the transfer. A short packet is a packet that is smaller than the maximum packet size of the endpoint, and it could possibly be zero-length. You should double check whether that is actually true though; try sending a transfer that is a multiple of the maximum packet size and make sure that Windows sends a zero-length packet at the end of that. By the way, you should consider sending your data as a control transfer or series of control transfers on endpoint 0. Control transfers have a built-in concept of a request and a response to the request. Despite the name, control transfers can be used to transfer large amounts of data; they are commonly used in the USB bootloaders (see the DFU class). Another advantage of using control transfers instead of non-zero endpoints is that you don't have to add extra endpoint descriptors and initialize the endpoints in your firmware. Your USB stack should have the machinery for handling custom control transfers and you should be able to make custom control transfers just by writing a few callback functions.
    4. To implement WinUSB on the device side, you would need to write your own USB descriptors and then use low-level USB transfer commands to read and write data from endpoints, or to handle vendor-specific control transfers on endpoint zero. I am not familiar with the STM32 USB libraries, but you should be able to identify the core component of it that implements control transfers and IN and OUT endpoints, without doing anything specific to a device class. That is the component that you would need to learn how to use.
    5. By default, you should use WinUSB instead of USB CDC ACM. The only reason to use USB CDC ACM is if your device is actually a serial port or if you want to make it easier for people to talk to your device in a variety of programming languages and environments. Most programming languages support serial ports, but the user would still have to write the code on top of that that generates your particular command format, so it doesn't actually give you that much. One problem with USB CDC ACM is that if the device gets disconnected while you have a handle open to it, and then it gets reconnected, the various USB CDC ACM drivers can often get into a bad state. In particular, usbser.sys prior to Windows 10 does not handle this well, and you usually have to unplug and replug the device to make the COM port usable again. USB CDC ACM uses bulk endpoints for its data transfer, so there is no latency guarantee. With WinUSB, you have the option to use Interrupt endpoints so that you can be guaranteed to always have one packet transferred for every USB frame.