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.
Readme.md
- Committer:
- Timmmm
- Date:
- 2017-02-13
- Revision:
- 1:0c3b6cc480b6
- Parent:
- 0:1a3aa2e25db9
File content as of revision 1:0c3b6cc480b6:
# 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() { 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'. 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(); Requests can be addressed to the device, an interface or an endpoint. Most WinUSB requests are addressed to the device, but the Extended Properties request is sent to the interface, so we'll just ignore what the recipient is. We only have on interface anyway. 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. ...WIP...