刚好,我做了个USB转7路串口的模块,还在博客写了篇文章,应该能回答你的问题。 先上参考资料: ST社区的: <<USB CDC类入门培训.pdf>> STM32 USB如何配置多个CDC设备 状态与枚举过程 CDC串口之从认识到认知 USB CDC 类基础 CDC(Communication Device Class)类是 USB2.0 标准下的一个子类,定义了通信相关设备的抽象集合。它与USB2.0标准以 及其下的子类的相互关系如下图所示: USB2.0标准下定义了很多子类,有音频类,CDC类,HID类等,通信类CDC下面,又有一些子类,这里我们主要使用PSTN(Public Switched Telephone Network)。从 PSTN官方标准文档来看,PSTN子类是一个与电信相关的子类,而这里,我们只是将它作为一个普通的通信设备使用,并没有使用到它的一些电话特性。PSTN定义了三种模型LM(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-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个字节是类型,描述符基本是这样的开头,后面是该类型下描述符信息。配置描述符以及所包含的接口描述符,端点描述符定义如下: 配置描述符: 接口描述符: 端点描述符: CDC类设备 CDC类设备与其他标准USB设备枚举过程的并没有什么特殊的地方。在设备描述符内可以使用DeviceClass=0x00, SubClass=0x00, Protocol=0x00 表示此类信息在接口描述符内给出;或者也可以使用0x02,0x00,0x00;来表明该设备为 CDC类设备。或者使用0xef, 0x02,0x01表示当前为复合设备。 USB 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)
所在话题
参与活动
-
滴滴押注社区团购,明确“投入不设上限,要做市场第一”
线下 2020-10-16 -
滴滴押注社区团购,明确“投入不设上限,要做市场第一”
网络 2020-10-16 -
滴滴押注社区团购,明确“投入不设上限,要做市场第一”
网络 2020-10-16 -
滴滴押注社区团购,明确“投入不设上限,要做市场第一”
网络 2020-10-16 -
滴滴押注社区团购,明确“投入不设上限,要做市场第一”
线下 2020-10-16 -
滴滴押注社区团购,明确“投入不设上限,要做市场第一”
线下 2020-10-16