Tim H / Mbed OS UsbKnob

Dependencies:   USBDevice

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.
+