USB转多路串口 - USB CDC设备枚举

先上参考资料:
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指令,接口和端点的描述符都包含在配置描述符里。标准的设备描述符如下:

OffsetFieldSizeValueDescription
0bLength1Number以字节为单位的描述符大小
1bDescriptorType1Constant设备描述符类型
2bcdUSB2BCDUSB规范版本号
4bDeviceClass1Class类码
5bDeviceSubClass1SubClass子类码
6bDeviceProtocol1Protocol协议码
7bMaxPacketSize01Number端点0的最大包大小
8idVendor2ID厂商ID
10idProduct2ID产品ID
12bcdDevice2BCD设备版本号
14iManufacturer1Index制造商字符串描述符索引
15iProduct1Index产品的字符串描述符索引
16iSerialNumber1Index设备序列号的字符串描述符索引
17bNumConfigurations1Number可能的配置数目

第1个是描述符长度,以字节为单位,第2个字节是类型,描述符基本是这样的开头,后面是该类型下描述符信息。配置描述符以及所包含的接口描述符,端点描述符定义如下:

配置描述符:

OffsetFieldSizeValueDescription
0bLength10x09本描述符长度
1bDescriptorType10x02CONFIGURATION 类型
2wTotalLength20x003b配置总长度,包括后面的接口和端点配置,2字节
4bNumInterfaces10x02接口个数
5bConfigurationValue10x01Set_Configuration命令所需要的参数值
6iConfiguration10x03描述该配置的字符串的索引值
7bmAttributes10xa0主机供电(0x80), 自己供电(0xC0)
8bMaxPower10x2d主机供电最大电流;0x32(100mA), 0xFA(500mA)

接口描述符:

OffsetFieldSizeValueDescription
0bLength10x09本描述符长度
1bDescriptorType10x04INTERFACE 类型
2bInterfaceNumber10x00当前接口index(编号)
3bAlternateSetting10x00
4bNumEndpoints10x01该接口的端点个数
5bInterfaceClass10x03接口通信类别,0x03代表HID设备
6bInterfaceSubClass10x01子类别
7bInterfaceProtocol10x01这里定义为键盘(Keyboard)
8iInterface10x02本描述符长度

端点描述符:

OffsetFieldSizeValueDescription
0bLength10x07本描述符长度
1bDescriptorType10x05ENDPOINT 类型
2bEndpointAddress10x81端点地址,最高bit=1代表IN方向的端点
3bmAttributes10x03中断传输(0x03), 批量传输(0x02)
4wMaxPacketSize20x0008传输的数据报文最大长度
6bInterval10x0A

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)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值