【USB设备设计】-- CDC 设备开发(虚拟串口设备)
在嵌入式系统中,串行异步通信接口(UART)使用很频繁的接口,跟主机建立通信往往会用到USB转串口的设备,本章将介绍如何将USB虚拟成串口设备。
前期准备
1.带USB 功能的MCU (笔者使用的NXP RT1062)
2.串口调试助手
虚拟串口为cdc类(Communication Device Class),通常CDC类设备由两个子类接口组成:1个通信接口类接口(Communication Interface Class)和 0到多个数据接口类接口(Data Interface Class)。通常来说CDC设备一般需要至少2个接口。废话不多说,先贴上描述符:
Device Descriptor:
------------------------------
0x12 bLength
0x01 bDescriptorType
0x0200 bcdUSB
0xEF bDeviceClass (Miscellaneous device)
0x02 bDeviceSubClass
0x01 bDeviceProtocol
0x40 bMaxPacketSize0 (64 bytes)
0x1FC9 idVendor
0x00A3 idProduct
0x0101 bcdDevice
0x01 iManufacturer "L17"
0x02 iProduct "USB VCP"
0x03 iSerialNumber "0123456789ABCDEF"
0x01 bNumConfigurations
Configuration Descriptor:
------------------------------
0x09 bLength
0x02 bDescriptorType
0x004B wTotalLength (75 bytes)
0x02 bNumInterfaces
0x01 bConfigurationValue
0x00 iConfiguration
0xC0 bmAttributes (Self-powered Device)
0x32 bMaxPower (100 mA)
Interface Association Descriptor:
------------------------------
0x08 bLength
0x0B bDescriptorType
0x00 bFirstInterface
0x02 bInterfaceCount
0x02 bFunctionClass (Communication Device Class)
0x02 bFunctionSubClass (Abstract Control Model - ACM)
0x00 bFunctionProtocol
0x02 iFunction "USB VCP"
Interface Descriptor:
------------------------------
0x09 bLength
0x04 bDescriptorType
0x00 bInterfaceNumber
0x00 bAlternateSetting
0x01 bNumEndPoints
0x02 bInterfaceClass (Communication Device Class)
0x02 bInterfaceSubClass (Abstract Control Model - ACM)
0x01 bInterfaceProtocol (ITU-T V.250)
0x00 iInterface
CDC Header Functional Descriptor:
------------------------------
0x05 bFunctionalLength
0x24 bDescriptorType
0x00 bDescriptorSubtype
0x0110 bcdCDC
CDC Call Management Functional Descriptor:
------------------------------
0x05 bFunctionalLength
0x24 bDescriptorType
0x01 bDescriptorSubtype
0x01 bmCapabilities
0x01 bDataInterface
CDC Abstract Control Management Functional Descriptor:
------------------------------
0x04 bFunctionalLength
0x24 bDescriptorType
0x02 bDescriptorSubtype
0x06 bmCapabilities
CDC Union Functional Descriptor:
------------------------------
0x05 bFunctionalLength
0x24 bDescriptorType
0x06 bDescriptorSubtype
0x00 bControlInterface
0x01 bSubordinateInterface(0)
Endpoint Descriptor:
------------------------------
0x07 bLength
0x05 bDescriptorType
0x81 bEndpointAddress (IN endpoint 1)
0x03 bmAttributes (Transfer: Interrupt / Synch: None / Usage: Data)
0x0010 wMaxPacketSize (1 x 16 bytes)
0x07 bInterval (64 microframes)
Interface Descriptor:
------------------------------
0x09 bLength
0x04 bDescriptorType
0x01 bInterfaceNumber
0x00 bAlternateSetting
0x02 bNumEndPoints
0x0A bInterfaceClass (CDC Data)
0x00 bInterfaceSubClass
0x00 bInterfaceProtocol
0x00 iInterface
Endpoint Descriptor:
------------------------------
0x07 bLength
0x05 bDescriptorType
0x82 bEndpointAddress (IN endpoint 2)
0x02 bmAttributes (Transfer: Bulk / Synch: None / Usage: Data)
0x0200 wMaxPacketSize (512 bytes)
0x00 bInterval
Endpoint Descriptor:
------------------------------
0x07 bLength
0x05 bDescriptorType
0x02 bEndpointAddress (OUT endpoint 2)
0x02 bmAttributes (Transfer: Bulk / Synch: None / Usage: Data)
0x0200 wMaxPacketSize (512 bytes)
0x00 bInterval
通信接口
通信接口下拥有一个输入端点,用于报告主机一些状态。对于实现USB虚拟串口功能,需要将USB通信接口类的类型设置为:Abstract Control Model子类和Common AT Commands传输协议
通信接口描述符后面会跟,功能描述符(Functional Descriptors),用于描述接口功能
描述符子类 | 基本功能信息 |
---|---|
00h | 功能描述符头 |
01h | Call Management 功能描述符 |
02h | 抽象控制管理 功能描述符 |
03h | Direct Line Management 功能描述符 |
04h | Telephone Ringer 功能描述符 |
05h | Telephone Call and Line State Reporting Capability 功能描述符 |
06h | Union 功能描述符 |
07h | Country Selection 功能描述符 |
08h | Telephone Operation Mode 功能描述符 |
09h | USB Terminal 功能描述符 |
0Ah | Network Channel 功能描述符 |
0Bh | 协议单元功能描述符 |
0Ch | 扩展单元功能描述符 |
0Dh | 多通道管理功能描述符 |
0Eh | CAPI控制管理功能描述符 |
0Fh | 以太网网络功能描述符 |
10h | ATM功能描述符 |
11-FFh | 保留 |
数据接口
数据接口,没啥好说的,接口类定义为:0x0A 。端点描述符,注意传输类型为:Bulk 块传输
虚拟串口相关类请求
- GET LINE CODING
主机获取当前串口属性请求,包括波特率、停止位、校验位及数据位的位数。CTL请求编码格式
返回7Byte参数,分别为: [0:3]波特率,[4]停止位,[5]校验位,[6]数据位。可知上图获取波特率为:115200
- SET LINE CODING
类似GET LINE CODING,用于主机设置从机当前属性,可修改波特率、停止位、校验位及数据位
- SET CTRL LINE STATE
该请求没有数据输出阶段,作用是设置设备的DTR和RTS引脚电平,D0位表示DTR,D1位表示RTS
类请求回调函数内容如下:
usb_status_t USB_DeviceCdcVcomCallback(class_handle_t handle, uint32_t event, void *param)
{
uint32_t len;
uint8_t *uartBitmap;
usb_cdc_acm_info_t *acmInfo;
usb_device_cdc_acm_request_param_struct_t *acmReqParam;
usb_device_endpoint_callback_message_struct_t *epCbParam;
volatile usb_cdc_vcom_struct_t *vcomInstance;
usb_status_t error = kStatus_USB_InvalidRequest;
acmReqParam = (usb_device_cdc_acm_request_param_struct_t *)param;
epCbParam = (usb_device_endpoint_callback_message_struct_t *)param;
vcomInstance = &g_deviceComposite->cdcVcom;
acmInfo = vcomInstance->usbCdcAcmInfo;
switch (event)
{
case kUSB_DeviceCdcEventSendResponse:
{
if ((epCbParam->length != 0) && (!(epCbParam->length % vcomInstance->bulkInEndpointMaxPacketSize)))
{
/* If the last packet is the size of endpoint, then send also zero-ended packet,
** meaning that we want to inform the host that we do not have any additional
** data, so it can flush the output.
*/
error = USB_DeviceCdcAcmSend(handle, vcomInstance->bulkInEndpoint, NULL, 0);
}
else if ((1 == vcomInstance->attach) && (1 == vcomInstance->startTransactions))
{
if ((epCbParam->buffer != NULL) || ((epCbParam->buffer == NULL) && (epCbParam->length == 0)))
{
/* User: add your own code for send complete event */
/* Schedule buffer for next receive event */
error = USB_DeviceCdcAcmRecv(handle, vcomInstance->bulkOutEndpoint, vcomInstance->currRecvBuf,
vcomInstance->bulkOutEndpointMaxPacketSize);
}
}
else
{
}
}
break;
case kUSB_DeviceCdcEventRecvResponse:
{
if ((1 == vcomInstance->attach) && (1 == vcomInstance->startTransactions))
{
vcomInstance->recvSize = epCbParam->length;
if (!vcomInstance->recvSize)
{
/* Schedule buffer for next receive event */
error = USB_DeviceCdcAcmRecv(handle, vcomInstance->bulkOutEndpoint, vcomInstance->currRecvBuf,
vcomInstance->bulkOutEndpointMaxPacketSize);
}
}
}
break;
case kUSB_DeviceCdcEventSerialStateNotif:
((usb_device_cdc_acm_struct_t *)handle)->hasSentState = 0;
error = kStatus_USB_Success;
break;
case kUSB_DeviceCdcEventSendEncapsulatedCommand:
break;
case kUSB_DeviceCdcEventGetEncapsulatedResponse:
break;
case kUSB_DeviceCdcEventSetCommFeature:
if (USB_DEVICE_CDC_FEATURE_ABSTRACT_STATE == acmReqParam->setupValue)
{
if (1 == acmReqParam->isSetup)
{
*(acmReqParam->buffer) = vcomInstance->abstractState;
*(acmReqParam->length) = COMM_FEATURE_DATA_SIZE;
}
else
{
/* no action, data phase, s_abstractState has been assigned */
}
error = kStatus_USB_Success;
}
else if (USB_DEVICE_CDC_FEATURE_COUNTRY_SETTING == acmReqParam->setupValue)
{
if (1 == acmReqParam->isSetup)
{
*(acmReqParam->buffer) = vcomInstance->countryCode;
*(acmReqParam->length) = COMM_FEATURE_DATA_SIZE;
}
else
{
/* no action, data phase, s_countryCode has been assigned */
}
error = kStatus_USB_Success;
}
else
{
/* no action, return kStatus_USB_InvalidRequest */
}
break;
case kUSB_DeviceCdcEventGetCommFeature:
if (USB_DEVICE_CDC_FEATURE_ABSTRACT_STATE == acmReqParam->setupValue)
{
*(acmReqParam->buffer) = vcomInstance->abstractState;
*(acmReqParam->length) = COMM_FEATURE_DATA_SIZE;
error = kStatus_USB_Success;
}
else if (USB_DEVICE_CDC_FEATURE_COUNTRY_SETTING == acmReqParam->setupValue)
{
*(acmReqParam->buffer) = vcomInstance->countryCode;
*(acmReqParam->length) = COMM_FEATURE_DATA_SIZE;
error = kStatus_USB_Success;
}
else
{
/* no action, return kStatus_USB_InvalidRequest */
}
break;
case kUSB_DeviceCdcEventClearCommFeature:
break;
case kUSB_DeviceCdcEventGetLineCoding:
*(acmReqParam->buffer) = vcomInstance->lineCoding;
*(acmReqParam->length) = LINE_CODING_SIZE;
error = kStatus_USB_Success;
break;
case kUSB_DeviceCdcEventSetLineCoding:
{
if (1 == acmReqParam->isSetup)
{
*(acmReqParam->buffer) = vcomInstance->lineCoding;
*(acmReqParam->length) = LINE_CODING_SIZE;
}
else
{
/* no action, data phase, s_lineCoding has been assigned */
}
error = kStatus_USB_Success;
}
break;
case kUSB_DeviceCdcEventSetControlLineState:
{
error = kStatus_USB_Success;
vcomInstance->usbCdcAcmInfo->dteStatus = acmReqParam->setupValue;
/* activate/deactivate Tx carrier */
if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_CARRIER_ACTIVATION)
{
acmInfo->uartState |= USB_DEVICE_CDC_UART_STATE_TX_CARRIER;
}
else
{
acmInfo->uartState &= (uint16_t)~USB_DEVICE_CDC_UART_STATE_TX_CARRIER;
}
/* activate carrier and DTE. Com port of terminal tool running on PC is open now */
if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_DTE_PRESENCE)
{
acmInfo->uartState |= USB_DEVICE_CDC_UART_STATE_RX_CARRIER;
}
/* Com port of terminal tool running on PC is closed now */
else
{
acmInfo->uartState &= (uint16_t)~USB_DEVICE_CDC_UART_STATE_RX_CARRIER;
}
/* Indicates to DCE if DTE is present or not */
acmInfo->dtePresent = (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_DTE_PRESENCE) ? true : false;
/* Initialize the serial state buffer */
acmInfo->serialStateBuf[0] = NOTIF_REQUEST_TYPE; /* bmRequestType */
acmInfo->serialStateBuf[1] = USB_DEVICE_CDC_NOTIF_SERIAL_STATE; /* bNotification */
acmInfo->serialStateBuf[2] = 0x00; /* wValue */
acmInfo->serialStateBuf[3] = 0x00;
acmInfo->serialStateBuf[4] = 0x00; /* wIndex */
acmInfo->serialStateBuf[5] = 0x00;
acmInfo->serialStateBuf[6] = UART_BITMAP_SIZE; /* wLength */
acmInfo->serialStateBuf[7] = 0x00;
/* Notify to host the line state */
acmInfo->serialStateBuf[4] = acmReqParam->interfaceIndex;
/* Lower byte of UART BITMAP */
uartBitmap = (uint8_t *)&acmInfo->serialStateBuf[NOTIF_PACKET_SIZE + UART_BITMAP_SIZE - 2];
uartBitmap[0] = acmInfo->uartState & 0xFFu;
uartBitmap[1] = (acmInfo->uartState >> 8) & 0xFFu;
len = (uint32_t)(NOTIF_PACKET_SIZE + UART_BITMAP_SIZE);
if (0 == ((usb_device_cdc_acm_struct_t *)handle)->hasSentState)
{
error = USB_DeviceCdcAcmSend(handle, vcomInstance->interruptEndpoint, acmInfo->serialStateBuf, len);
if (kStatus_USB_Success != error)
{
// usb_echo("kUSB_DeviceCdcEventSetControlLineState error!");
}
((usb_device_cdc_acm_struct_t *)handle)->hasSentState = 1;
}
/* Update status */
if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_CARRIER_ACTIVATION)
{
/* To do: CARRIER_ACTIVATED */
}
else
{
/* To do: CARRIER_DEACTIVATED */
}
if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_DTE_PRESENCE)
{
/* DTE_ACTIVATED */
if (1 == vcomInstance->attach)
{
vcomInstance->startTransactions = 1;
}
}
else
{
/* DTE_DEACTIVATED */
if (1 == vcomInstance->attach)
{
vcomInstance->startTransactions = 0;
}
}
}
break;
case kUSB_DeviceCdcEventSendBreak:
break;
default:
break;
}
return error;
}
测试自发自收功能
串口调试工具使用MobaXterm,底层自发自收,测试结果如下