I started working on this because I had another project that needed USB communication accross all platforms while being relatively affordable. This ruled out trusty FTDI and most of the other USB-Serial cconverters. As for the software, using the Arduino M0 stack was an option but I wanted to learn the inner workings of USB while keeping the code lean. I found this great USB stack by kevinmehall and wrote the USB-CDC interfacing functions and tried on a board at hand. After ironing out the various little bugs in the descriptors I was able to get my device to enumerate as a COM port on windows. USBTreeView is a great tool for debugging USB enumeration issues. It updates faster than device manager and you can view all the descriptors in one page.

Even though the enumeration was successful, opening the port in a terminal program was a hit or miss. Someitmes the port would open and communicate for a while and hang up the terminal program, sometimes it wouldn’t open at all. Microsoft page for the driver doesn’t mention any hardware requirements other than having both Class and Subclass codes set to 0x02 in the device descriptor. After digging around the internet for a while I found out that as soon as you open a COM port in windows, it tries to set the port properties (Baud rate, parity etc) and immediately reads it back to make sure the values were changed. Obviously I wasn’t interested in baurd rates as I wasn’t implementing a USB->Serial converter, mine was only a device that was pretending to be one. However to satisfy this requirement I had to change one of the CDC desriptors.

.CDC_functional_ACM = {
    .bLength = sizeof(CDC_FunctionalACMDescriptor),
    .bDescriptorType = USB_DTYPE_CSInterface,
    .bDescriptorSubtype = CDC_SUBTYPE_ACM,
    .bmCapabilities = 0x02,
}

The bmCapabilities field indicates that this device supports the requests SET_LINECODING and GET_LINECODING. Then I had to update the EndPoint 0 handler to handle these Class-Specific CDC requests.

bmRequestType Request Code Value (bRequest) Data
00100001B SET_LINE_CODING 0x20 Line Coding
10100001B GET_LINE_CODING 0x21 Line Coding
bitmap CDC_SET_CONTROL_LINE_STATE n meaning
bitmap CDC_SEND_BREAK n meaning

Line Coding is a 7 byte structre that encode the properties of the Serial port we are supposed to be emulating. The structure is described below, however it’s not really that important since I wasn’t working with a real serial port.

Offset Field Size Description
0 dwDTERate 4 Baud rate, in bits/s
4 bCharFormat 1 Stop bits
0 - 1 Stop bit
1 - 1.5 Stop bits
2 - 2 Stop bits
5 bParityType 1 Parity
0 - None
1 - Odd
2 - Even
3 - Mark
4 - Space

The handler functions just load and store the data from/to host in a variable, which is sufficient for the driver to function properly with the device.

// Isolating the class specific requests (Standard requests are handled separately)
if ((bmRequestType & USB_REQTYPE_TYPE_MASK) == USB_REQTYPE_CLASS)
{
	uint8_t interface = wIndex & 0xff;
	uint8_t entity = wIndex >> 8;
	switch (bmRequestType &  USB_REQTYPE_RECIPIENT_MASK )
	{
		case USB_RECIPIENT_INTERFACE: 
			//CDC Requests are sent to the CDC_CONTROL interface
			if (interface == INTERFACE_CDC_CONTROL)
			{
				if (bRequest == CDC_GET_LINE_CODING)
				{
					uint16_t i = 0;
					uint8_t* p = &_usbLineInfo;
					for(i = 0; i < 7; i++)
					{
						ep0_buf_in[i] = *p;
						p++;
					}
					usb_ep0_in(7);
					return usb_ep0_out();
				}
				else if (bRequest == CDC_SET_LINE_CODING)
				{
					if (wLength)
					{
						uint16_t i = 0;
						uint8_t* p = &_usbLineInfo;
						for(i = 0; i < wLength; i++)
						{
							*p = ep0_buf_out[8+i];
							p++;
							if (i >= 7)
								break;
						}

					}
					usb_ep0_in(0);
					return usb_ep0_out();
				}
				else if (bRequest == CDC_SET_CONTROL_LINE_STATE)
				{
					_usbLineInfo.lineState = wValue&0xff;
					usb_ep0_in(0);
					return usb_ep0_out();
				}
				else if (CDC_SEND_BREAK == bRequest)
				{
					uint16_t breakValue = (uint16_t)wValue;
					usb_ep0_in(0);
					return usb_ep0_out();
				}
				else
				{
					return usb_ep0_stall();
				}
			}
			break;
		default:
			//There are other cases that aren't needed right now

	}
}

After this little modification I was able to get the device working quite smoothly on Windows (10), macOS( Mojave 10.14) and Chrome OS.