1. Introduction
We are talking about how to write a USB CDC driver in Silabs Arm Cortex M3 MCU. CDC class will simulate a COM port in PC OS which can be accessed by common serial tool. The chip we are using is SiM3U1xx. We can go through USB basic knowledge and then into detail design method to make it work on SiM3U167. USB device driver used be hard part due to limited PC OS driver support and firmware need to take care many electric layer operations. But now, PC support more and more USB classes by default. We don't need to write PC driver. And SOC with USB component make it easier to manage USB transfer behavior. We can focus on protocol layer, that is really benefit for us to write a USB driver. In this case, USB CDC device is supported by WinXP, Win7,etc. and only need one INF file. So let us start it to learn how to make it look simple.
2. USB background knowledge
Universal Serial Bus (USB) is an industry standard developed in the mid-1990s that defines the cables, connectors and communications protocols used in a bus for connection, communication and power supply between computers and electronic devices. USB interface include VBUS, GND, D+,D-.
2.1. History
There are three USB specifications. USB1.1, USB2.0, USB3.0.
· USB1.1 released in January 1998, USB 1 specified data rates of 1.5 Mbit/s (Low-Bandwidth) and 12 Mbit/s (Full-Bandwidth)
· USB2.0 Released in April 2000. Added higher maximum signaling rate of 480 Mbit/s (effective throughput up to 35 MB/s or 280 MBit/s) (now called "Hi-Speed").
· USB3.0 was released in November 2008. The standard claims a theoretical "maximum" transmission speed of up to 5 Gbit/s (625 MB/s). USB 3.0 reduces the time required for data transmission, reduces power consumption, and is backward compatible with USB 2.0.
2.2 System design.
The design architecture of USB is asymmetrical in its topology, consisting of a host, a multitude of downstream USB ports, and multiple peripheral devices connected in a tiered-star topology. Up to 127 devices, including hub devices if present, may be connected to a single host controller.
USB device communication is based on pipes (logical channels). A pipe is a connection from the host controller to a logical entity, found on a device, and named an endpoint. Because pipes correspond 1-to-1 to endpoints, the terms are sometimes used interchangeably. A USB device can have up to 32 endpoints.
There are two types of pipes: stream and message pipes. A message pipe is bi-directional and is used for control transfers. Message pipes are typically used for short, simple commands to the device, and a status response, used, for example, by the bus control pipe number 0. A stream pipe is a uni-directional pipe connected to a uni-directional endpoint that transfers data using an isochronous, interrupt, or bulk transfer:
· isochronous transfers: at some guaranteed data rate (often, but not necessarily, as fast as possible) but with possible data loss (e.g., realtime audio or video).
· interrupt transfers: devices that need guaranteed quick responses (bounded latency) (e.g., pointing devices and keyboards).
· bulk transfers: large sporadic transfers using all remaining available bandwidth, but with no guarantees on bandwidth or latency (e.g., file transfers).
An endpoint of a pipe is addressable with a tuple (device_address, endpoint_number) as specified in a TOKEN packet that the host sends when it wants to start a data transfer session. If the direction of the data transfer is from the host to the endpoint, an OUT packet (a specialization of a TOKEN packet) having the desired device address and endpoint number is sent by the host. If the direction of the data transfer is from the device to the host, the host sends an IN packet instead.
Endpoints are grouped into interfaces and each interface is associated with a single device function. An exception to this is endpoint zero, which is used for device configuration and which is not associated with any interface. A single device function composed of independently controlled interfaces is called a composite device. A composite device only has a single device address because the host only assigns a device address to a function.
When a USB device is first connected to a USB host, the USB device enumeration process is started. The enumeration starts by sending a reset signal to the USB device. The data rate of the USB device is determined during the reset signaling. After reset, the USB device's information is read by the host and the device is assigned a unique 7-bit address. If the device is supported by the host, the device drivers needed for communicating with the device are loaded and the device is set to a configured state. If the USB host is restarted, the enumeration process is repeated for all connected devices.
2.3 Device classes
The functionality of USB devices is defined by class codes, communicated to the USB host to affect the loading of suitable software driver modules for each connected device. This provides for adaptability and device independence of the host to support new devices from different manufacturers.
Device classes include:
3. USB protocol
For USB 2.0 high speed device. There is a pull up resistor on D+. On USB host side, D+/D- has pull down resistors. Once USB device plug in, USB host find D+ voltage rise up, and then know USB device attached. USB host will execute a bus reset, and assign an address for this USB device. And then USB host want to get detail information of USB device. Know device class type, assign correspond class driver for this device for further communication. We can separate it into two stages: enumeration and class stage communication.
Every packet transfer need ACK/NACK indicate status. Once host send a packet, it will show it is IN or OUT packet type. Here IN means host in, OUT means host out. For IN type, device should prepare data in IN endpoint and response NACK if data not ready, once data ready, sent it out and get ACK from host. For OUT type, device should response with ACK when data received. This is easy understand, just like handshake in any reliable communication.
3.1 Enumeration.
In this stage, USB host try to get essential information of USB device by a set of descriptors. Detail description can be found in USB 2.0 specification chapter 9. Let's see a capture snapshot of USB enumeration status by USB protocol analyzer.
From above snapshot. Let's start with row 5 "Reset(15.0ms)".We can see PC execute a Bus Reset at first. And execute High speed Detection Handshake, this one to get know is high speed or full speed device. In our case, it is TIMEOUT which means it is full speed device.
3.1.1 After that, USB host assign an address to device with SetAddress command. The command package is 8 bytes size, we call it Setup data, the structure illustrate in USB2.0 spec chapter 9.
Let's have a look on SetAddress command, the data is 00, 05,05,00,00,00,00,00.
· So we got bmRequestType = 00, Host-to-device direction; standard type; device recipient.
· bRequest = 05, it is Set_Address refer to Table 9-4
· wValue = 0005(LSB), this is assgigned address = 5.
· wIndex = 0000(LSB), it is zero refer Table 9-3.
· wLenght = 0000(LSB), it s zeron refer Table 9-3.
Once device set address correctly, it ACK to USB host to indicate address assignment was done.
3.1.2. And USB host send a GetDescriptor setup data to know detail description of device. Let's have a look on the setup data. 80,06,00,01,00,00,12,00
· bmRequestType = 0x80, device to host direction, that mean device need to send back data to host.
· bRequest = 06, GetDescriptor command refer Table 9-4.
· wValue = 0001(LSB), Descriptor type is device refer Table 9-5.
· wIndex = 0000(LSB), it is zero refer Table 9-3.
· wLenght = 0x0012(LSB), descriptor length is 18 bytes.
And then we can device send back 18 bytes descriptors (12,01,10,01,02,00,00,40,c4,10,02,a0,01,00,01,02,00,01), the data structure also can be found in chapter 9 Table 9-8. Standard Device Descriptor. Let's have good looking formation of these data.
The idVendor is fix, 0x10c4 for Silabs, idProduct 0xa002 which need customer to ask Silabs assign an unique PID for them. bDeviceClass = 2 means CDC class. And others items are easy to understand, not to talk in detail.
3.1.3. And then USB host execute GetDescriptor(Configuration),GetDescriptor(Sting LangIDs),GetDescriptor(String iProduct),SetConfiguration(1). Those can be found in USB2.0 spec chapter 9 also. Then enumeration was done.
3.2 Class stage communication.
We can see last two setup data was CDC class special command. Class special IN(0x21) and Class Special Out(0x22). We need to get detail information from class CDC spec usbcdc11.pdf. 0x21 is GET_LINE_CODING, 0x22 is SET_CONTROL_LINE_STATE.
All above are endpoint 0 transfer. For CDC ACM USB class, we need on interrupt in endpoint, one bulk in endpoint, one bulk out endpoint. So let's capture some data when open USB CDC device, and data transfer.
We can see at first USB host send GET_LINE_CODING(0x21) command to learn device status, device report 0x1c200(LSB), 115200 bps, 8 data bits, 1 stop bit, no parity. But we set BPS as 57600 in UART terminal tool, so we see SET_LINE_CODING(0X20) send 0xE100 to device and read back for comparison. Also, we capture data in bulk endpoint in/out, there is 'a' (0x61) in OUT endpoint(3), 'b'(0x62) in IN endpoint(2).
4· Write USB CDC device driver
Now we are going to start programming. As we discuss above, the first thing is handle Setup Data in code; Second thing is handle Class special request; Last thing is realize function requirement. Of course, hardware initialization has to be done before USB run.
4.1 Hardware initialization.
· Enable APB clock to PB0, UART0, USB0, FLASH module
SI32_CLKCTRL_A_enable_apb_to_modules_0(SI32_CLKCTRL_0,
SI32_CLKCTRL_A_APBCLKG0_PB0 |
SI32_CLKCTRL_A_APBCLKG0_UART0 |
SI32_CLKCTRL_A_APBCLKG0_USB0 |
SI32_CLKCTRL_A_APBCLKG0_FLASHCTRL0);
· Set flash access speed to 2 for 48Mhz system clock.
SI32_FLASHCTRL_A_select_flash_speed_mode(SI32_FLASHCTRL_0, 2);
· GPIO setup, SMV enable, PB2 for LED as push-pull,Crossbar0,1 enable, UART pin module enabled.
SI32_PBSTD_A_set_pins_push_pull_output(SI32_PBSTD_1, 0x0008);
SI32_PBSTD_A_write_pbskipen(SI32_PBSTD_1, 0x0008);
SI32_PBCFG_A_enable_crossbar_0(SI32_PBCFG_0);
SI32_PBCFG_A_enable_crossbar_1(SI32_PBCFG_0);
SI32_PBSTD_A_set_pins_push_pull_output(SI32_PBSTD_2, 0x00000C00);
// UART PINS TO PROPER CONFIG (TX = PB1.12, RX = PB1.13)
SI32_PBSTD_A_set_pins_push_pull_output(SI32_PBSTD_1, 0x0001000);
SI32_PBSTD_A_set_pins_digital_input(SI32_PBSTD_1, 0x00002000);
SI32_PBSTD_A_write_pbskipen(SI32_PBSTD_0, 0x0000FFFF);
SI32_PBSTD_A_write_pbskipen(SI32_PBSTD_1, 0x00000FFF);
SI32_PBCFG_A_enable_xbar0h_peripherals(SI32_PBCFG_0, SI32_PBCFG_A_XBAR0H_UART0EN);
· Enable USB oscillator, set AHB source as USB oscillator, and system clock as 48Mhz.
SI32_USB_A_enable_usb_oscillator(SI32_USB_0);
SI32_CLKCTRL_A_select_ahb_source_usb_oscillator(SI32_CLKCTRL_0);
SystemCoreClock = 48000000;
· UART hardware initialize and interrupt enable
SI32_UART_A_set_rx_baudrate(SI32_UART_0, (SystemCoreClock / (2 * 115200)) - 1);
SI32_UART_A_set_tx_baudrate(SI32_UART_0, (SystemCoreClock / (2 * 115200)) - 1);
// SETUP TX (8-bit, 1stop, no-parity)
SI32_UART_A_select_tx_data_length(SI32_UART_0, 8);
SI32_UART_A_enable_tx_start_bit(SI32_UART_0);
SI32_UART_A_enable_tx_stop_bit(SI32_UART_0);
SI32_UART_A_disable_tx_parity_bit(SI32_UART_0);
SI32_UART_A_select_tx_stop_bits(SI32_UART_0, SI32_UART_A_STOP_BITS_1_BIT);
SI32_UART_A_enable_tx(SI32_UART_0);
// SETUP RX
SI32_UART_A_select_rx_data_length(SI32_UART_0, 8);
SI32_UART_A_enable_rx_start_bit(SI32_UART_0);
SI32_UART_A_enable_rx_stop_bit(SI32_UART_0);
SI32_UART_A_disable_rx_parity_bit(SI32_UART_0);
SI32_UART_A_select_rx_stop_bits(SI32_UART_0, SI32_UART_A_STOP_BITS_1_BIT);
SI32_UART_A_select_rx_fifo_threshold_1(SI32_UART_0);
SI32_UART_A_enable_rx(SI32_UART_0);
SI32_UART_A_enable_rx_data_request_interrupt(SI32_UART_0);
NVIC_ClearPendingIRQ(UART0_IRQn);
NVIC_EnableIRQ(UART0_IRQn);
· USB hardware initialize and interrupt enable
SI32_USB_A_reset_module (SI32_USB_0);
// Enable Endpoint 0 interrupts
SI32_USB_A_write_cmint (SI32_USB_0, 0x00000000);
SI32_USB_A_write_ioint (SI32_USB_0, 0x00000000);
SI32_USB_A_enable_ep0_interrupt (SI32_USB_0);
USB_DeviceState = DEVICE_STATE_Unattached;
USB_Device_ConfigurationNumber = 0;
USB_Device_SetFullSpeed();
SI32_USB_A_enable_suspend_interrupt (SI32_USB_0);
SI32_USB_A_enable_reset_interrupt (SI32_USB_0);
NVIC_EnableIRQ(USB0_IRQn);
USB_DeviceState = DEVICE_STATE_Powered;
// Uninhibit the module once all initialization is complete
SI32_USB_A_enable_module(SI32_USB_0);
SI32_USB_A_enable_internal_pull_up(SI32_USB_0);
4.2 USB Setup data handler.
There is a default USB ISR entry "void USB0_IRQHandler(void)", we put ep0 handler in this entry.
· Interrupt handle
Once enter ISR, we clear interrupt flag, and check whether it is EP0 interrupt, and jump to ep0 handler if it is.
uint32_t usbCommonInterruptMask = SI32_USB_A_read_cmint(SI32_USB_0);
uint32_t usbEpInterruptMask = SI32_USB_A_read_ioint(SI32_USB_0);
SI32_USB_A_write_cmint(SI32_USB_0, usbCommonInterruptMask);
SI32_USB_A_write_ioint(SI32_USB_0, usbEpInterruptMask);
if (usbEpInterruptMask & SI32_USB_A_IOINT_EP0I_MASK)
{
USB0_ep0_handler();
return;
}
· EP0 Setup Data handle.
o First, we need read 8 bytes from EP0 fifo
for (uint8_t RequestHeaderByte = 0; RequestHeaderByte < sizeof(USB_Request_Header_t); RequestHeaderByte++)
*(RequestHeader++) = Endpoint_Read_8();
}
o Second, we handler difference request type. The form look likes below
switch (USB_ControlRequest.bRequest)
{
case REQ_GetStatus:
USB_Device_GetStatus();
break;
case REQ_ClearFeature:
case REQ_SetFeature:
USB_Device_ClearSetFeature();
break;
case REQ_SetAddress:
USB_Device_SetAddress();
break;
case REQ_GetDescriptor:
USB_Device_GetDescriptor();
break;
case REQ_GetConfiguration:
USB_Device_GetConfiguration();
break;
case REQ_SetConfiguration:
USB_Device_SetConfiguration();
break;
}
4.3 USB Class special request handler.
For CDC class special command handle, we have four commands as we talked before. Here are sample code for reference.
switch (USB_ControlRequest.bRequest)
{
case CDC_REQ_GetLineEncoding:
Endpoint_Write_32_LE(CDCInterfaceInfo->State.LineEncoding.BaudRateBPS);
Endpoint_Write_8(CDCInterfaceInfo->State.LineEncoding.CharFormat);
Endpoint_Write_8(CDCInterfaceInfo->State.LineEncoding.ParityType);
Endpoint_Write_8(CDCInterfaceInfo->State.LineEncoding.DataBits);
break;
case CDC_REQ_SetLineEncoding:
CDCInterfaceInfo->State.LineEncoding.BaudRateBPS = Endpoint_Read_32_LE();
CDCInterfaceInfo->State.LineEncoding.CharFormat = Endpoint_Read_8();
CDCInterfaceInfo->State.LineEncoding.ParityType = Endpoint_Read_8();
CDCInterfaceInfo->State.LineEncoding.DataBits = Endpoint_Read_8();
EVENT_CDC_Device_LineEncodingChanged(CDCInterfaceInfo);
break;
case CDC_REQ_SetControlLineState:
CDCInterfaceInfo->State.ControlLineStates.HostToDevice = USB_ControlRequest.wValue;
EVENT_CDC_Device_ControLineStateChanged(CDCInterfaceInfo);
CDC_Device_SendControlLineStateChange(CDCInterfaceInfo);
break;
case CDC_REQ_SendBreak:
EVENT_CDC_Device_BreakSent(CDCInterfaceInfo, (uint8_t)USB_ControlRequest.wValue);
break;
}
4.4 Realize virtual COM port function.
For virtual COM port function, we need to received data from USB OUT and send it to UART TX, or receive data from UART RX and send out through USB IN request. We need define two ring buffer to store data for USB2UART and UART2USB.
RingBuffer_InitBuffer(&USBtoUSART_Buffer, USBtoUSART_Buffer_Data, sizeof(USBtoUSART_Buffer_Data));
RingBuffer_InitBuffer(&USARTtoUSB_Buffer, USARTtoUSB_Buffer_Data, sizeof(USARTtoUSB_Buffer_Data));
o UART receive data interrupt
void uart_rev_handler(void)
{
uint8_t ReceivedByte = uart_get_byte();
if (USB_DeviceState == DEVICE_STATE_Configured)
RingBuffer_Insert(&USARTtoUSB_Buffer, ReceivedByte);
}
o USB receive data interrupt
void EVENT_USB_recv_data(void)
{
if (!(RingBuffer_IsFull(&USBtoUSART_Buffer)))
{
int16_t ReceivedByte = CDC_Device_ReceiveByte(&VirtualSerial_CDC_Interface);
if (!(ReceivedByte < 0))
RingBuffer_Insert(&USBtoUSART_Buffer, ReceivedByte);
}
}
o Foreground logical control
while(1)
{
uint16_t BufferCount = RingBuffer_GetCount(&USARTtoUSB_Buffer);
if(BufferCount > 0)
{
while (BufferCount--)
{
if (CDC_Device_SendByte(&VirtualSerial_CDC_Interface,
RingBuffer_Peek(&USARTtoUSB_Buffer)) != ENDPOINT_READYWAIT_NoError)
{
break;
}
RingBuffer_Remove(&USARTtoUSB_Buffer);
}
Endpoint_ClearIN();
}
if (!(RingBuffer_IsEmpty(&USBtoUSART_Buffer)))
uart_send_byte(RingBuffer_Remove(&USBtoUSART_Buffer));
}
4.5 Prepare INF file for CDC device.
5. Validate USB CDC function.
We are talking about Win7 installation. Build project and download firmware into SiM3U1xx MCU card, plug in USB cable, it will show "SiM3U1xx CDC Class" under "Other devices" in Device Manger,
Right click and choose "Update Driver Software"
select "Browse my computer for driver software", enter directory path of your CDC_ACM.inf
Windows will show below message, just choose install this driver software anyway.
After done, it shows below message.
Check in Device Manager, you can find new COM port appear in Ports(COM &LPT). And now you can access this COM port with any serial tool.
That is to say, We have done with USB CDC driver development. It is quite easy, isn't?
6. Source code
We don’t' want to reinvent the wheel, so this USB CDC base on open source project LUFA, in terms of MIT License.