先上参考资料:
ST社区的: <<USB CDC类入门培训.pdf>>
STM32 USB如何配置多个CDC设备
状态与枚举过程
CDC串口之从认识到认知
USB CDC 类基础
CDC(Communication Device Class)类是 USB2.0 标准下的一个子类,定义了通信相关设备的抽象集合。它与USB2.0标准以
及其下的子类的相互关系如下图所示:
CDC-PSTN之间的关系
USB2.0标准下定义了很多子类,有音频类,CDC类,HID类等,通信类CDC下面,又有一些子类,这里我们主要使用PSTN(Public Switched Telephone Network)。从 PSTN官方标准文档来看,PSTN子类是一个与电信相关的子类,而这里,我们只是将它作为一个普通的通信设备使用,并没有使用到它的一些电话特性。PSTN定义了三种模型:DLM(Direct Line Mode),ACM(Abstract Control Model)和 TCM(Telephone Control Model). 一般选择ACM模型,个人估计是该模型控制接口传输的高层指令支持比如设置、获取波特率,设置获取与通信相关的参数,与串口控制,数据传输比较接近。
USB转串口,根据设备类型主要分为USB VCP串口、USB转CDC串口、HID转串口。USB HID从Win2000版本起内置驱动,是真正意义上的免驱,CDC串口驱动从Win10系统版本才开始内置,因CDC协议的用途定位,串口功能较其他方式并不完整。VCP串口驱动只需安装一次也可以联网自动安装,且有部分操作系统会内置厂商VCP驱动。根据实际使用情况,做了下对比汇总:
CDC_HID_VCP类对比
基于CDC-ACM协议开发纯USB传输应用还是十分方便的,工程师只需要关注USB设备本身的开发工作,驱动软件甚至是应用软件均不用开发。
VCP串口主要是指使用厂商专用USB转串口驱动和通信协议实现的串口,该方式也最接近16C450/16C550等原生串口。HID转串口USB传输速度没有CDC和VCP快,不适合较高波特率通讯,且不兼容串口应用软件。
USB设备、接口、端点描述符
USB 协议已规定好支持的设备,主机系统中也准备好了许多对应设备的驱动程序。USB 设备第一次连接到主机时, 主机要知道是哪一类的USB设备才能指派对应的驱动。要知道占用多少资源、使用了哪些传输方式以及传输的数据量等,才能真正开始工作。这些信息是通过存储在设备中的USB描述符来体现的。
一个USB设备只有一个设备描述符,设备描述符里面定义了该设备有多少种配置,每种配置有对应的配置描述符;而在配置描述符中又定义了该配置里面有多少个接口,每个接口有对应的接口描述符;在接口描述符里面又定义了该接口有多少个端点,每个端点对应一个端点描述符;端点描述符定义了端点的大小,类型等等。由此我们可以看出,USB的描述符之间的关系是一层一层的,最上一层是设备描述符,下面是配置描述符,再下面是接口描述符,再下面是端点描述符。在获取描述符时,先获取设备描述符,然后再获取配置描述符,根据配置描述符中的配置集合长度,一次将配置描述符、接口描述符、端点描述符一起一次读回。其中可能还会有获取设备序列号,厂商字符串,产品字符串等。详细关系如下图所示:
描述符层级关系
这些描述符是以字节流的形式传输的,所以软件代码里一般是用数组或者结构体封装。根据协议,主机有询问设备描述和配置描述符的SETUP指令,接口和端点的描述符都包含在配置描述符里。标准的设备描述符如下:
Offset | Field | Size | Value | Description |
---|---|---|---|---|
0 | bLength | 1 | Number | 以字节为单位的描述符大小 |
1 | bDescriptorType | 1 | Constant | 设备描述符类型 |
2 | bcdUSB | 2 | BCD | USB规范版本号 |
4 | bDeviceClass | 1 | Class | 类码 |
5 | bDeviceSubClass | 1 | SubClass | 子类码 |
6 | bDeviceProtocol | 1 | Protocol | 协议码 |
7 | bMaxPacketSize0 | 1 | Number | 端点0的最大包大小 |
8 | idVendor | 2 | ID | 厂商ID |
10 | idProduct | 2 | ID | 产品ID |
12 | bcdDevice | 2 | BCD | 设备版本号 |
14 | iManufacturer | 1 | Index | 制造商字符串描述符索引 |
15 | iProduct | 1 | Index | 产品的字符串描述符索引 |
16 | iSerialNumber | 1 | Index | 设备序列号的字符串描述符索引 |
17 | bNumConfigurations | 1 | Number | 可能的配置数目 |
第1个是描述符长度,以字节为单位,第2个字节是类型,描述符基本是这样的开头,后面是该类型下描述符信息。配置描述符以及所包含的接口描述符,端点描述符定义如下:
配置描述符:
Offset | Field | Size | Value | Description |
---|---|---|---|---|
0 | bLength | 1 | 0x09 | 本描述符长度 |
1 | bDescriptorType | 1 | 0x02 | CONFIGURATION 类型 |
2 | wTotalLength | 2 | 0x003b | 配置总长度,包括后面的接口和端点配置,2字节 |
4 | bNumInterfaces | 1 | 0x02 | 接口个数 |
5 | bConfigurationValue | 1 | 0x01 | Set_Configuration命令所需要的参数值 |
6 | iConfiguration | 1 | 0x03 | 描述该配置的字符串的索引值 |
7 | bmAttributes | 1 | 0xa0 | 主机供电(0x80), 自己供电(0xC0) |
8 | bMaxPower | 1 | 0x2d | 主机供电最大电流;0x32(100mA), 0xFA(500mA) |
接口描述符:
Offset | Field | Size | Value | Description |
---|---|---|---|---|
0 | bLength | 1 | 0x09 | 本描述符长度 |
1 | bDescriptorType | 1 | 0x04 | INTERFACE 类型 |
2 | bInterfaceNumber | 1 | 0x00 | 当前接口index(编号) |
3 | bAlternateSetting | 1 | 0x00 | |
4 | bNumEndpoints | 1 | 0x01 | 该接口的端点个数 |
5 | bInterfaceClass | 1 | 0x03 | 接口通信类别,0x03代表HID设备 |
6 | bInterfaceSubClass | 1 | 0x01 | 子类别 |
7 | bInterfaceProtocol | 1 | 0x01 | 这里定义为键盘(Keyboard) |
8 | iInterface | 1 | 0x02 | 本描述符长度 |
端点描述符:
Offset | Field | Size | Value | Description |
---|---|---|---|---|
0 | bLength | 1 | 0x07 | 本描述符长度 |
1 | bDescriptorType | 1 | 0x05 | ENDPOINT 类型 |
2 | bEndpointAddress | 1 | 0x81 | 端点地址,最高bit=1代表IN方向的端点 |
3 | bmAttributes | 1 | 0x03 | 中断传输(0x03), 批量传输(0x02) |
4 | wMaxPacketSize | 2 | 0x0008 | 传输的数据报文最大长度 |
6 | bInterval | 1 | 0x0A |
CDC类设备
CDC类设备与其他标准USB设备枚举过程的并没有什么特殊的地方。在设备描述符内可以使用DeviceClass=0x00,
SubClass=0x00, Protocol=0x00 表示此类信息在接口描述符内给出;或者也可以使用0x02,0x00,0x00;来表明该设备为
CDC类设备。或者使用0xef, 0x02,0x01表示当前为复合设备。 USB CDC类配置描述符的结构:
CDC类描述符结构
如上图所示,CDC类的配置描述符(1个串口)一般包含两个接口(Interface 0),一个控制接口,另外一个是数据接口(Interface 1), 除此之外,还有一个虚线指向的IAD(Interface Association Description),这个表示这个是不是可选的,得根据实际情况来确定其是否真实存在。
USB设备状态与枚举过程
USB协议规定USB可见设备状态分为连接(Attached), 上电(Powered),默认(Default),地址(Address),配置(Configured)和挂起(Suspended)6个状态。所谓可见,即USB系统和主机可见的状态,其它状态属于USB设备内部状态。
- 接入态(Attached):设备接入主机后,主机通过检测信号线上的电平变化来发现设备的接入;
- 供电态(Powered):就是给设备供电,分为设备接入时的默认供电值,配置阶段后的供电值(按数据中要求的最大值,可通过编程设置)
- 缺省态(Default):USB在被配置之前,通过缺省地址0与主机进行通信;
- 地址态(Address):经过了配置,USB设备被复位后,就可以按主机分配给它的唯一地址来与主机通信,这种状态就是地址态;
- 配置态(Configured):通过各种标准的USB请求命令来获取设备的各种信息,并对设备的某此信息进行改变或设置。
-
挂起态(Suspended):USB总线处于空闲状态的话,该设备就要自动进入挂起状态,在进入挂起状态后,总的电流功耗不超过280UA。
USB主机在检测到USB设备插入后,就要对设备进行枚举了。Host根据Device所报告上来的参数,获得一些信息,知道设备是什么样的设备,如何进行通信,这样主机就可以根据这些信息来加载合适的驱动程序。枚举过程
如上图,USB CDC类的通信部分主要包含三部分:枚举过程、虚拟串口操作和数据通信。其中虚拟串口操作部分并不一定强制需要,因为若跳过这些虚拟串口的操作,实际上USB依然是可以通信的,这也就是为什么上图中,在操作虚拟串口之前会有两条数据通信的数据。之所以会有虚拟串口操作,主要是我们通常使用PC作为Host端,在PC端使用一个串口工具来与其进行通信,PC端的对应驱动将其虚拟成一个普通串口,这样一来,可以方便PC端软件通过操作串口的方式来与其进行通信,但实际上,Host端与Device端物理上是通过USB总线来进行通信的,与串口没有关系,这一虚拟化过程,起决定性作用的是对应驱动,包含如何将每一条具体的虚拟串口操作对应到实际上的USB操作。这里需要注意地是,Host端与Device端的USB通信速率并不受所谓的串口波特率影响,它就是标准的USB2.0全速(12Mbps)速度,实际速率取决于总线的实际使用率、驱动访问USB外设有效速率(两边)以及外部环境对通信本身造成的干扰率等等因素组成。
USB2.0协议CDC类设备枚举过程详解
检测电压变化,报告主机
首先,USB设备上电后,一直监测USB设备接口电平变化HUB检测到有电压变化,将利用自己的中断端点将信息反馈给主控制器有设备连接。
Host了解连接的设备
每个hub利用它自己的中断端点向主机报告它的各个端口的状态(对于这个过程,设备是看不到的,也不必关心),报告的内容只是hub端口的设备连接/断开的事件。如果有连接/断开事件发生,那么host会发送一个 Get_Port_Status请求(request)给hub以了解此次状态改变的确切含义。Get_Port_Status等请求属于所有hub都要求支持的hub类标准请求(standard hub-class requests)。
Hub检测所插入的设备是高速还是低速设备
hub通过检测USB总线空闲(Idle)时差分线的高低电压来判断所连接设备的速度类型,当host发来Get_Port_Status请求时,hub就可以将此设备的速度类型信息回复给host。USB 2.0规范要求速度检测要先于复位(Reset)操作。
hub复位设备
主机一旦得知新设备已连上以后,它至少等待100ms以使得插入操作的完成以及设备电源稳定工作。然后主机控制器就向hub发出一个 Set_Port_Feature请求让hub复位其管理的端口(刚才设备插上的端口)。hub通过驱动数据线到复位状态(D+和D-全为低电平 ),并持续至少10ms。当然,hub不会把这样的复位信号发送给其他已有设备连接的端口,所以其他连在该hub上的设备自然看不到复位信号,不受影响。
Host检测所连接的全速设备是否是支持高速模式
因为根据USB 2.0协议,高速(High Speed)设备在初始时是默认全速(Full Speed )状态运行,所以对于一个支持USB 2.0的高速hub,当它发现它的端口连接的是一个全速设备时,会进行高速检测,看看目前这个设备是否还支持高速传输,如果是,那就切到高速信号模式,否则就一直在全速状态下工作。
同样的,从设备的角度来看,如果是一个高速设备,在刚连接bub或上电时只能用全速信号模式运行(根据USB 2.0协议,高速设备必须向下兼容USB 1.1的全速模式)。随后hub会进行高速检测,之后这个设备才会切换到高速模式下工作。假如所连接的hub不支持USB 2.0,即不是高速hub,不能进行高速检测,设备将一直以全速工作。
Hub建立设备和主机之间的信息通道
主机不停地向hub发送Get_Port_Status请求,以查询设备是否复位成功。Hub返回的报告信息中有专门的一位用来标志设备的复位状态。
当hub撤销了复位信号,设备就处于默认/空闲状态(Default state),准备接收主机发来的请求。设备和主机之间的通信通过控制传输,默认地址0,端点号0进行。此时,设备能从总线上得到的最大电流是100mA。(所有的USB设备在总线复位后其地址都为0,这样主机就可以跟那些刚刚插入的设备通过地址0通信。)
主机发送Get_Descriptor请求获取默认管道的最大包长度
默认管道(Default Pipe)在设备一端来看就是端点0。主机此时发送的请求是默认地址0,端点0,虽然所有未分配地址的设备都是通过地址0来获取主机发来的请求,但由于枚举过程不是多个设备并行处理,而是一次枚举一个设备的方式进行,所以不会发生多个设备同时响应主机发来的请求。
设备描述符的第8字节代表设备端点0的最大包大小。虽然说设备所返回的设备描述符(Device Descriptor)长度只有18字节,但系统也不在乎,此时,描述符的长度信息对它来说是最重要的,其他的瞄一眼就过了。当完成第一次的控制传输后,也就是完成控制传输的状态阶段,系统会要求hub对设备进行再一次的复位操作(USB规范里面可没这要求)。再次复位的目的是使设备进入一个确定的状态。
主机给设备分配一个地址
主机控制器通过Set_Address请求向设备分配一个唯一的地址。在完成这次传输之后,设备进入地址状态(Address state),之后就启用新地址继续与主机通信。设备在,地址在;设备消失(被拔出,复位,系统重启),地址被收回。同一个设备当再次被枚举后得到的地址不一定是上次那个了。
主机获取设备的信息
主机发送 Get_Descriptor请求到新地址读取设备描述符,这次主机发送Get_Descriptor请求可算是诚心,它会认真解析设备描述符的内容。设备描述符内信息包括端点0的最大包长度,设备所支持的配置(Configuration)个数,设备类型,VID(Vendor ID,由USB-IF分配), PID(Product ID,由厂商自己定制)等信息。
接着说到描述符。总的来讲描述符就是USB设备之间通信的规范。当一个新的USB设备接入时,他的默认地址为0,此时主设备通过描述符识别从设备,并与其通信,来获得更多关于从设备的信息。并为从设备在1到127找到一个没有分配的地址。并将该地址赋给这个从设备,这样,这个从设备就可以使用新获得的地址和主设备通信了。
GET_INTERFACE(取获取接口)
这个请求向指定接口返回选中的备用设备。
一些USB设备有接口设置互斥的配置。这个请求允许主机确定当前选定的备用设置。如果wValue或者wLength的值与上面指定的不一致,那么设备的行为没有定义;如果指定的接口不存在,那么设备将用请求错误响应。
USB CDC 描述符实例
设备描述符:
const uint8_t MyDevDescr[] =
{
0x12, // bLength
0x01, // bDescriptorType (Device)
0x00, 0x02, // bcdUSB 2.0 // 0x10, 0x01, bcdUSB 1.10
0x00, // bDeviceClass 以为是: 0xEF 0x02 0x01 实际是:0x00 0x00 0x00
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
DEF_USBD_UEP0_SIZE, // bMaxPacketSize, 64
(uint8_t)DEF_USB_VID, (uint8_t)(DEF_USB_VID >> 8),
(uint8_t)DEF_USB_PID, (uint8_t)(DEF_USB_PID >> 8),
0x00, 0x02, //0x00, DEF_IC_PRG_VER, // bcdDevice 2.0
0x01, // iManufacturer (String Index)
0x02, // iProduct (String Index)
0x03, // iSerialNumber (String Index)
0x01, // bNumConfigurations 1
};
设备描述符定义了USB 2.0设备;指明类别在接口描述符中给出;端点0的最大报文长度;USB 设备的PID, VID;
本项目是模拟了7个串口,配置描述符比较大,还是用数组直接写会非常长。在github上参考了别人的代码,用宏定义了串口描述,数组定义就简洁了许多。宏定义如下:
/**
* @brief macro to help generate CDC ACM USB descriptors
* @param desc CdcDeviceDesc 对象
*
*/
#define CDC_DESCRIPTOR(desc) \
/*Interface Association Descriptor */ \
0x08, /* bLength: Interface Association Descriptor size */ \
0x0B, /* bDescriptorType: Interface Association */ \
desc.CmdInterface, /* bFirstInterface: First Interface of Association, 第一个接口的序号 */ \
0x02, /* bInterfaceCount: quantity of interfaces in association, 本IDA的接口数量 */ \
0x02, /* bFunctionClass: Communication Interface Class, CDC设备 */ \
0x02, /* bFunctionSubClass: Abstract Control Model */ \
0x01, /* bFunctionProtocol: Common AT commands */ \
0x00, /* iInterface */ \
\
/*Interface Descriptor */ \
0x09, /* bLength: Interface Descriptor size */ \
0x04, /* bDescriptorType: Interface */ \
desc.CmdInterface, /* bInterfaceNumber: Number of Interface, 接口编号 */ \
0x00, /* bAlternateSetting: Alternate setting */ \
0x01, /* bNumEndpoints: One endpoints used */ \
0x02, /* bInterfaceClass: Communication Interface Class */ \
0x02, /* bInterfaceSubClass: Abstract Control Model */ \
0x01, /* bInterfaceProtocol: Common AT commands */ \
0x00, /* iInterface */ \
\
/*Header Functional Descriptor*/ \
0x05, /* bLength: Endpoint Descriptor size */ \
0x24, /* bDescriptorType: CS_INTERFACE */ \
0x00, /* bDescriptorSubtype: Header Func Desc */ \
0x10, /* bcdCDC: spec release number */ \
0x01, \
\
/*Call Management Functional Descriptor*/ \
0x05, /* bFunctionLength */ \
0x24, /* bDescriptorType: CS_INTERFACE */ \
0x01, /* bDescriptorSubtype: Call Management Func Desc */ \
0x00, /* bmCapabilities: D0+D1 */ \
desc.DataInterface, /* bDataInterface */ \
\
/*ACM Functional Descriptor*/ \
0x04, /* bFunctionLength */ \
0x24, /* bDescriptorType: CS_INTERFACE */ \
0x02, /* bDescriptorSubtype: Abstract Control Management desc */ \
0x02, /* bmCapabilities */ \
\
/*Union Functional Descriptor*/ \
0x05, /* bFunctionLength */ \
0x24, /* bDescriptorType: CS_INTERFACE */ \
0x06, /* bDescriptorSubtype: Union func desc */ \
desc.CmdInterface, /* bMasterInterface: Communication class interface */ \
desc.DataInterface, /* bSlaveInterface0: Data Class Interface */ \
\
/* Command Endpoint Descriptor, 控制端点描述符(IN) */ \
0x07, /* bLength: Endpoint Descriptor size */ \
0x05, /* bDescriptorType: Endpoint */ \
desc.CmdEndPoint, /* bEndpointAddress */ \
0x03, /* bmAttributes: Interrupt */ \
0x40, /* wMaxPacketSize: 报文最大字节数(低位)*/ \
0x00, \
0x10, /* bInterval: */ \
\
/* Data class interface descriptor */ \
0x09, /* bLength: Endpoint Descriptor size */ \
0x04, /* bDescriptorType: */ \
desc.DataInterface, /* bInterfaceNumber: Number of Interface */ \
0x00, /* bAlternateSetting: Alternate setting */ \
0x02, /* bNumEndpoints: Two endpoints used */ \
0x0A, /* bInterfaceClass: CDC */ \
0x00, /* bInterfaceSubClass: */ \
0x00, /* bInterfaceProtocol: */ \
0x00, /* iInterface: */ \
\
/* Data Endpoint OUT Descriptor, 数据端点描述符(OUT) */ \
0x07, /* bLength: Endpoint Descriptor size */ \
0x05, /* bDescriptorType: Endpoint */ \
desc.DataOutEndPoint, /* bEndpointAddress */ \
0x02, /* bmAttributes: Bulk */ \
0x40, /* wMaxPacketSize: */ \
0x00, \
0x00, /* bInterval: ignore for Bulk transfer */ \
\
/* Data Endpoint IN Descriptor, 数据端点描述符(IN) */ \
0x07, /* bLength: Endpoint Descriptor size */ \
0x05, /* bDescriptorType: Endpoint */ \
desc.DataInEndPoint, /* bEndpointAddress */ \
0x02, /* bmAttributes: Bulk */ \
0x40, /* wMaxPacketSize: */ \
0x00, \
0x00 /* bInterval: ignore for Bulk transfer */
// 每个控制接口号都是数据接口号 + 固定偏移
#define CMD_EP_OFFSET (7) // 从8改为7就没报 USBFS_UIS_TOKEN_IN 的 TOG error了,奇怪
/**
* @brief 定义CDC 设备描述符关键接口属性
*
*/
struct CdcDeviceDesc
{
uint8_t CmdInterface; // 控制(命令)接口号
uint8_t DataInterface; // 数据接口号
uint8_t CmdEndPoint; // 控制端点
uint8_t DataOutEndPoint; // 数据端点(OUT 方向)
uint8_t DataInEndPoint; // 数据端点(IN 方向)
};
const static struct CdcDeviceDesc CDC_DEV_DESCs[USB_CDC_NUM] =
{
{0, 1, 0x81+CMD_EP_OFFSET, 0x01, 0x81}, // 通道0, 接口使用0和1, 端点:1
{2, 3, 0x82+CMD_EP_OFFSET, 0x02, 0x82},
{4, 5, 0x83+CMD_EP_OFFSET, 0x03, 0x83},
{6, 7, 0x84+CMD_EP_OFFSET, 0x04, 0x84},
{8, 9, 0x85+CMD_EP_OFFSET, 0x05, 0x85},
{10, 11, 0x86+CMD_EP_OFFSET, 0x06, 0x86},
{12, 13, 0x87+CMD_EP_OFFSET, 0x07, 0x87}
};
/* Configure descriptor 配置描述 */
const uint8_t MyCfgDescr[] =
{
0x09, // 长度,包括自已,下同
0x02, // configuration
0xD7, // 配置描述数组(MyCfgDescr)大小, 66 * 7 + 9 = 471 = 0x01D7
0x01,
(uint8_t)(USB_CDC_NUM*2), // 接口大小,每个CDC有2个interface
0x01, // configuration value
0x00, // index of string descriptor describing the configuration
0x80, // bmAttributes: 主机供电(0x80)。如果是自己供电这里配置为:0xC0
0xFA, // max power: 0x32->100mA, 0xFA->500mA
CDC_DESCRIPTOR(CDC_DEV_DESCs[0]),
CDC_DESCRIPTOR(CDC_DEV_DESCs[1]),
CDC_DESCRIPTOR(CDC_DEV_DESCs[2]),
CDC_DESCRIPTOR(CDC_DEV_DESCs[3]),
CDC_DESCRIPTOR(CDC_DEV_DESCs[4]),
CDC_DESCRIPTOR(CDC_DEV_DESCs[5]),
CDC_DESCRIPTOR(CDC_DEV_DESCs[6]),
};
看代码的注释即可,不再说明了。
(end)