Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
Diff: Readme.md
- Revision:
- 0:1a3aa2e25db9
- Child:
- 1:0c3b6cc480b6
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Readme.md Fri Nov 04 22:07:07 2016 +0000 @@ -0,0 +1,252 @@ +# WinUSB Device Example + +If you've ever got frustrated with hardcoded COM ports and parsing text-based serial +protocols, this is for you. + +## Introduction + +When we want our microcontroller to communicate with a control program running on a computer +via USB, the usual solution is to use serial-over-USB. It's easy, universally supported, and +signed drivers are often automatically installed - this is even called out as a selling point +by FTDI who make most of the serial-to-USB interface chips used by hobbiests. + +There are other advantages of serial control interfaces: + +* It doesn't require a specific program to control - you can use an terminal program. My favourite is Termie. +* It's easy to debug and you can easily add printf()-style debugging to your program. +* All microcontrollers support UART. + +Eventually however you will run up against the limitations. The highest 'standard' speed is 115200 kb/s. Some +microcontrollers support higher speeds, even up to 3 Mb/s but that is rare. Since the protocol +is stream-based you must create your own framing system or resort to text-based protocol. + +Another big problem is that there isn't a good way to detect which COM port your device is plugged +into. Even if your serial API allows enumerating ports (many don't) you might have several different +serial devices plugged in. Distinguishing them becomes a nightmare, especially as the COM port +can change across reboots, and if you move the device between physical USB ports. + +A solution - if your microcontroller supports it - is to use USB as nature intended. Turn your +device into a custom USB device and have your program communicate with it via libusb. This +is more complicated, but the end result is better. + +Fortunately mBed comes with USB device support for several boards. We'll look at implementing +a simple on/off switch with the FRDM-K22F. + +## WinUSB Devices / WICD + +One of the first problems you'll discover - at least on Windows - is the need for signed device drivers. +In the past the only way to support a custom USB device seamlessly on Windows was to write a USB +driver (difficult!) and submit it to Microsoft to be tested and signed (expensive!). Fortunately now +there is a better way. + +By configuring our USB device in a certain way we can identify it to Windows as a "WinUSB Device". This +means that Windows 7 and later will automatically install Microsoft's WinUSB driver for it. This +is a generic driver that allows access to USB functions from userspace - i.e. your program. +libusb has support for the WinUSB driver so you can still write cross-platform code. + +Before I explain how to implement WinUSB I'll give brief overview of how USB works. + +## A very brief introduction to USB + +USB comes in a few flavours: + +* USB 1.1 - Supports "Low Speed" (1.5 Mb/s) and "Full Speed" (12 Mb/s) +* USB 2 - Adds support for "High Speed" (480 Mb/s) +* USB 3 - Adds support for "Super Speed" (lots of Mb/s) + +To be clear: Low Speed < Full Speed < High Speed < Super Speed. Woo branding. Virtually all microcontrollers +with USB support support USB 2, though almost all of them only support Full Speed, and not High Speed. An exception +is the Atmel SAM3X8E and similar which is found on the Arduino Due. + +USB is a packet based protocol. Each transmission and reception is initiated with an 8-byte 'setup packet'. It has a very simple +structure: + + struct SetupPacket + { + uint8_t bmRequestType; + uint8_t bRequest; + uint16_t wValue; + uint16_t wIndex; + uint16_t wLength; + }; + +All transfers are initiated by the host - the peripheral only ever responds to requests. If it wants to send data +it must wait to be polled. + +The `bmRequestType` lets the host do various standard and vendor requests. Aside from initating data transfers +it allows the host to request configuration descriptors which describe what the device is (keyboard, mouse, etc.), +its name, manufacturer, data endpoints, and loads more. + +The main configuration descriptor describes the most important detail - the endpoints. Endpoints are really just +addresses within the device that you can send data to or from. If you like you can just have one transmit and +one receive endpoint and do your own addressing. However there are some different types of endpoints so you may +wish to have more than two. The simplest type that we'll be looking at is the interrupt endpoint, designed for +small fast bits of data, e.g. keystrokes or mouse movements, or control and status. + +You can simply send and receive packets to/from the endpoints, in a very similar maner to UDP. + +## A USBSwitch class + +We'll implement a generic USBControl class that allows sending and receiving data packets via interrupt endpoints +and then subclass it to implement a light bulb device with a switch. We'll mirror the device on a desktop +application using Qt, so you can switch the light on and off from the device or the computer, and its state +will update on both. + +Finally we'll make it into a WinUSB device so the driver is installed automatically. + +## A note on Vendor and Product IDs + +There is one annoying thing about using native USB for communication. Your device must be given a +Vendor ID and Product ID. These 16-bit identifiers are used to distinguish products and are the usual +method a program uses to find plugged-in devices it cares about. + +Unfortunately since they are only 16-bit, there are only 65536 possible vendors, and each can only +have 65536 products. This was a deliberate ploy by the USB Implementors' Forum (USB-IF) to allow them +to justify charging exhorbitant fees to claim a Vendor ID. + +It is beyond the means of most hobbiests to buy a Vendor ID, and the USB-IF has shut down vendors who +offered their Product IDs for sale for a reasonable price. Therefore the only real solution is to +fuck the USB-IF and squat on an unused or abandoned Vendor ID. Perhaps if enough people do this the +USB-IF will provide a real solution, since there's little they can do to stop it. + +I use the Vendor ID 0x0004 which is for Nebraska Furniture Mart (seriously). I doubt they will be using +it any time soon! Others have suggested using 0xF055 (FOSS). + +## How Windows probes WinUSB devices + +The first thing Windows does to probe WinUSB devices is to send a GET_DESCRIPTOR request +for the STRING_DESCRIPTOR at offset 0xEE. The value 0xEE is defined by Microsoft to be the index +of the MSOS (MicroSoft Operating System) string. + +A WinUSB device should return a valid MSOS string descriptor which looks like this: + + struct PACKED WindowsOsString + { + uint8_t bLength; // Length of this structure, 0x12. + uint8_t bDescriptorType; // String descriptor, 0x03 (STRING_DESCRIPTOR) + uint8_t qwSignature[14]; // 'MSFT100' but in UTF-16 ( "M\0S\0F\0T\01\00\00\0" ) + uint8_t bMS_VendorCode; // Vendor code to retrieve OS feature desciptors. Can be changed. Example 0x01. + uint8_t bPad; // Padding, 0x00. + }; + +The only variable information in this sturcture is the bMS_VendorCode value. This can be set to anything. +It is used in the following USB requests that Windows makes. + +After obtaining the WindowsOsString structure, Windows makes several vendor requests as using the +bMS_VendorCode value as the request type. The value of wIndex indicates which of several things it wants. +The main ones that concern us are: + + #define COMPAT_ID_INDEX (4) + #define EXTENDED_PROPERTIES_INDEX (5) + +The first is for the "Compatible IDs" - the device must return a compatible ID of "WINUSB". + +The second is for the extended properties. Extended properties allow you to set the device icon, label, and +an interface GUID which can be used by programs instead of the Vendor/Product ID to find the device. This +is actually a great move by Microsoft as the Vendor/Product ID system sucks (see below). + +## Implementation in mBed + +To implement this in mBed we first make a WinUSBDevice class that derives from USBDevice: + + class WinUSBDevice : public USBDevice + { + public: + WinUSBDevice(uint16_t vendor_id, uint16_t product_id, uint16_t product_release); + +We need to intercept setup requests, and possibly handle them before the default handling code runs. +mBed provides a virtual function we can override for that: + + // Handle vendor setup requests. Return true if we handle the request, otherwise false to let mBed do its + // default action, which is to call requestSetup(). + bool USBCallback_request(); // override; // override commented out because mBed'd online compiler still doesn't use C++11 + +Finally we need somewhere to store the data that we will return: + + private: + WindowsOsString osStringDesc; + CompatIdData compatIdData; + ExtendedPropertyData extendedPropertyData; + }; + +In the WinUSBDevice constructor we initialise the data to return: + +... + +And now we get to the meat - filtering out the WinUSB-related requests to handle, and returning the data +we have crafted: + + bool WinUSBDevice::USBCallback_request() + { + +First we need a reference to the transfer structure which contains both the request information +and provides a place to store our response. The USBDevice::transfer member is actually private +but there is a protected function to access it. (Thank you mBed people - I always knew your APIs were the best! +Except the BLE API; that sucks medium time.) + + // This can never be null. + CONTROL_TRANSFER& transfer = *getTransferPtr(); + +Check if the setup is addressed to the device. It can also be addressed to an interface or an endpoint +but we do not care about these. More advanced devices may do, and in that case you may wish to use +MSOS Descriptors 2.0 which support treating interfaces separately. + + if (transfer.setup.bmRequestType.Recipient != DEVICE_RECIPIENT) + return false; + +Note that returning false means 'I didn't handle this; carry on as you were'. Returning true means +'I've handled this and stored a response in transfer'. + +Next we see if this is a request for the 0xEE string descriptor. + + // Intercept the request for string descriptor 0xEE + if (transfer.setup.bmRequestType.Type == STANDARD_TYPE && + transfer.setup.bRequest == GET_DESCRIPTOR && + DESCRIPTOR_TYPE(transfer.setup.wValue) == STRING_DESCRIPTOR && + DESCRIPTOR_INDEX(transfer.setup.wValue) == STRING_OFFSET_MSOS) + { + +If it is, return our MSOS string descriptor. + + transfer.ptr = reinterpret_cast<uint8_t*>(&osStringDesc); + transfer.remaining = osStringDesc.bLength; + transfer.direction = DEVICE_TO_HOST; + return true; + } + +After Windows has made that request it will send vendor requests using our custom request type +(GET_MS_DESCRIPTORS). + + // Our custom descriptor. + if (transfer.setup.bmRequestType.Type == VENDOR_TYPE && + transfer.setup.bRequest == GET_MS_DESCRIPTORS) + { + +Then we need to see which descriptor Windows wants and return the appropriate one. + + switch (transfer.setup.wIndex) + { + case COMPAT_ID_INDEX: // Extended Compat ID + transfer.ptr = reinterpret_cast<uint8_t*>(&compatIdData); + transfer.remaining = sizeof(compatIdData); + transfer.direction = DEVICE_TO_HOST; + return true; + case EXTENDED_PROPERTIES_INDEX: // Extended Properties. + transfer.ptr = reinterpret_cast<uint8_t*>(&extendedPropertyData); + transfer.remaining = sizeof(extendedPropertyData); + transfer.direction = DEVICE_TO_HOST; + return true; + case GENRE_INDEX: // Genre + default: + return false; + } + } + +If it wasn't a relevant request, let mBed handle it as it would have. + + return false; + } + +And we're done! Next we need a USB class that takes advantage of this WinUSBDevice class and +responds to our control transfers. +