尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

AT32裸机下USBx Device移植:HID+CDC ACM复合设备实战

AT32裸机下USBx Device移植:HID+CDC ACM复合设备实战 最近接了块带USB功能的板子芯片型号是LAT1486跑的是雅特力AT32平台的USBx Device协议栈。需求很典型一个USB口要同时承担两件事一个是模拟成HID设备做交互控制另一个是模拟成CDC ACM虚拟串口做数据通信也就是组成一个HIDCDC ACM的复合设备。麻烦在于官方例程基本都挂在RT-Thread或者完整的BSP工程里而我这边是裸机环境只能把USBx Device库单独抽出来做standalone移植。这篇文章就把整个移植过程、描述符设计、踩坑点和调试方法完整记录下来给后面要搞复合USB设备的同行一个参考。如果你手头正好要做USB复合设备或者在雅特力AT32这类M4 MCU上做USB虚拟串口自定义HID又不想为了一个USB功能把整个RTOS工程都搬进来这篇内容应该能帮你省不少时间。我尽量把“为什么这样做”讲清楚而不是只扔代码。1. 项目背景与方案选型心得1.1 为什么做 standalone 移植先说说为什么会走到“standalone移植”这一步。官方SDK里其实已经提供了不少USB Device例程但大部分是建立在RT-Thread、FreeRTOS或者完整BSP工程之上的。比如RT-Thread版本的例程usbd的初始化、类驱动的注册、端点的回调都跟内核的IPC、内存管理纠缠在一起。如果你的项目只是需要一个小巧的USB设备功能却又被拖进整个RTOS体系里性价比其实很低。我之前在一个产测工具项目里就遇到过这种情况功能本身很简单固件里跑一个状态机就够不需要任务调度也不需要信号量。但官方例程依赖RTOS强行移植意味着要么把RTOS引进来要么把RTOS相关的调用全部改掉。前者会让代码体积和复杂度暴涨后者则要求把USBx库内部的调度点都摸清楚。最后我选了后者也就是把USBx Device从官方大工程里拆出来加上一薄层standalone适配直接跑在裸机上。USBx Device这层协议栈本身其实对RTOS没什么强依赖。它做的事情就是处理USB标准请求、维护端点状态、调用类驱动的回调。真正需要“锁”的地方在裸机环境里完全可以靠关中断来保证临界区。也就是说USBx在设计上就有standalone运行的潜力只是官方没有单独给一份裸机工程而已。1.2 为什么选 HID CDC ACM 复合方案复合设备说白了就是一个USB物理口上同时挂多个功能。这次选HIDCDC ACM是从实际需求出发的。设备端有一个简单的按键/状态输入需要让主机感知同时设备还要通过串口跟主机交换大量数据。如果只做CDCHID那部分交互就得通过串口协议来模拟主机端写上位机要自己解析协议如果只做HID大数据量的传输带宽又不够灵活。两个功能合并到一个USB口上主机端插一根线既能看到一个虚拟串口又能识别到一个HID设备交互和通信分开干净利落。为什么不选两个独立USB设备很多板子只有一个USB控制器做不到。为什么不做成一个CDC虚拟串口加一个厂商自定义类也可以但Windows和Linux对HID的支持是最底层的免驱、稳定状态上报还能被BIOS/UEFI识别。CDC ACM在操作系统里也有原生驱动所以组合起来就是“双免驱”。另外HIDCDC ACM这个组合还有一个隐藏优势HID的中断传输适合小数据量、低延迟的控制通道CDC的批量传输适合大数据量、高吞吐的数据通道。两者配合就是典型的“控制数据”双通道架构。实际项目里很多设备都适用这种模式。1.3 LAT1486 与 USBx Device 的基本资源LAT1486具体型号对应的内部资源我这边没有把数据手册完整贴出来的习惯但有一点是确定的它使用的是AT32平台上的USB设备控制器配合雅特力官方的USBx Device软件库使用。这个库的分层很清楚从上到下大致是应用层描述符、类回调、类驱动层HID、CDC等、核心层usbd_core、usbd_std、以及最底层的硬件适配端点的寄存器操作、中断处理。在做移植之前建议先把芯片的USB SRAM容量、可用端点数量和每端点FIFO大小查清楚。HIDCDC ACM复合设备至少需要3个IN端点HID中断IN、CDC通知中断IN、CDC批量IN和1个OUT端点CDC批量OUT部分场景还会额外加HID OUT端点。端点数量不够的方案就得做端点复用复杂度会明显上升。LAT1486这类的USBD控制器基本都带4个以上端点做这个复合设备是够用的但如果你的目标芯片是某款低端点数的片子选型阶段就得先把端点规划想清楚。2. 描述符设计复合设备最难啃的部分2.1 从设备描述符说起声明复合设备身份很多人在复合设备上栽跟头第一刀就死在设备描述符上。复合设备不是简单地把两个接口放在同一个配置里就行你需要在设备描述符里声明“我是一个复合设备”否则操作系统在解析配置描述符的时候认不出后面的IAD接口关联描述符会按普通多接口设备去处理导致功能错乱或者直接枚举失败。设备描述符里有三个字节很关键bDeviceClass、bDeviceSubClass、bDeviceProtocol。对于带IAD的复合设备一般建议设置成bDeviceClass 0xEFbDeviceSubClass 0x02bDeviceProtocol 0x01这个组合就是USB规范里定义的“Miscellaneous Device Class”专门用来标识使用IAD的复合设备。如果你的设备描述符里写的是0x00操作系统可能会认为每个接口是独立功能而不是一个复合功能HID和CDC有可能分开识别也可能识别出各种奇怪的问题。顺带说一句bcdUSB我一般写0x0200。如果你的HID报表里用了比较新的特性可以考虑0x0210但全速设备写2.0就好兼容性最好。bMaxPacketSize0全速设备固定写64这个不需要犹豫。2.2 配置描述符里的 IAD 与 CDC 结构配置描述符是整个复合设备最繁琐的部分。你需要把IAD、CDC控制接口、CDC数据接口、HID接口依次组织好所有描述符的长度都要算对一个字节的偏差都会导致枚举失败。以我的实际布局为例IAD接口关联描述符bFirstInterface0bInterfaceCount2bFunctionClass0x02CDCbFunctionSubClass0x02ACMbFunctionProtocol0x01表示接口0和接口1合并成一个CDC ACM功能CDC控制接口接口0接口类0x02、子类0x02、协议0x00里面要带CDC特有的功能描述符包括Header Functional Descriptor、Call Management Functional Descriptor、ACM Functional Descriptor和Union Functional Descriptor。很多CDC无法被识别成串口的问题就是漏了Union描述符或者bControlInterface和bSubordinateInterface的编号对不上CDC数据接口接口1接口类0x0A、子类0x00、协议0x00带两个批量端点Bulk IN和Bulk OUT最大包长全速设备写64HID接口接口2接口类0x03、子类0x00、协议0x00带HID描述符和一个中断IN端点。这里最典型的坑是接口顺序问题。IAD的bFirstInterface和bInterfaceCount必须与实际接口编号完全一致。如果你把HID接口放在CDC前面IAD的bFirstInterface就要跟着改。而且CDC Control和CDC Data必须是相邻接口编号中间不能插入HID接口否则IAD就续不上了。有个实用的排查方法枚举失败时用USBlyzer或者Wireshark的USB抓包功能直接看主机请求配置描述符时设备返回的原始字节流再跟自己定义的描述符数组逐字节比对。绝大部分描述符问题都能这样查出来。2.3 HID 描述符与 Report Descriptor 设计HID接口的描述符相对CDC要简单但Report Descriptor才是真正决定HID设备“长什么样”的部分。你的设备是键盘、鼠标、还是自定义HID都由Report Descriptor决定。这次我做的HID端是一个简单的自定义HID设备用来上报按键和状态。报表描述符里用了一个输入报表包含几个字节的开关量和状态位。对于HID报表描述符需要注意几点第一报表描述符的长度要跟HID描述符里的wDescriptorLength字段完全一致多一个字节少一个字节都不行。我遇到过报表长度算错主机直接报设备描述符请求失败的情况。建议在代码里用sizeof自动计算不要手填。第二中断IN端点的最大包长一定要能容纳你的报表长度。比如报表长度为8字节端点最大包长如果只写4数据会被截断。反过来端点包长改大了轮询间隔也要对应调整。第三如果不确定自己的Report Descriptor有没有问题可以在Windows的设备管理器里看HID设备的“功能说明”或者用HID调试助手读报告描述符比对着规范逐字检查直观得多。HID类协议在系统层面的支持很成熟基本上描述符对了就能免驱工作但正因为描述符出错时的报错五花八门反而更容易让人摸不着头脑。2.4 端点规划与带宽预算复合设备的端点规划直接关系到USB传输的实时性和可靠性。我先把我这次的端点分配列出来端点0控制端点所有枚举流程必备端点1 INCDC通知端点中断传输最大包长8字节或者16字节轮询间隔按手册配置端点2 INCDC数据Bulk IN最大包长64字节端点3 OUTCDC数据Bulk OUT最大包长64字节端点4 INHID中断IN最大包长根据报表长度定轮询间隔1~10ms。全速USB的总带宽是1ms一帧每帧约1500字节左右可用带宽。HID中断IN如果轮询间隔是1ms每帧占用8字节CDC通知中断IN如果轮询间隔设16ms平均每帧占用不到1字节CDC的两个Bulk端点属于大块传输抢占剩余带宽。整体算下来HIDCDC ACM的复合设备在全速模式下带宽压力不大但要注意批量端点不要长时间占满总线否则会影响HID中断传输的实时性。端点FIFO分配也是容易被忽略的点。USB控制器的每个端点拥有独立的FIFO空间但总容量有限。CDC Bulk IN/OUT包长64字节每个端点至少分配64字节HID中断IN按包长分配控制端点64字节。像AT32的USB设备控制器FIFO是按端点静态或动态配置的建议给Bulk端点留双缓冲空间比如128字节这样连续传输的时候效率更高。如果FIFO分配不足会出现端点状态卡死、数据错乱的现象。3. standalone 移植实操从官方例程里“抠代码”3.1 源码筛选与工程搭建从官方SDK开始移植第一步是搞清楚哪些文件是必须的哪些是RTOS相关的垃圾。我的做法是新建一个空工程然后只拷贝USBx Device库的核心文件不碰官方例程的Application目录。需要拷贝的核心文件大致包括USB Device核心层源码usbd_core相关的c/h文件标准请求处理usbd_std相关的请求处理逻辑类驱动源码HID类驱动和CDC类驱动文件硬件适配层针对LAT1486 USB控制器的底层驱动文件包括端点操作、控制器初始化和中断处理配置头文件包含端点号宏定义、最大包长、缓冲区地址等配置。RTOS相关的东西比如rtos_mutex、rtos_semaphore这类文件一个都不要。官方类驱动里可能有条件编译通过宏开关把RTOS相关代码关掉。工程搭好后先编译一次肯定会有不少报错。把报错逐个解决的过程其实就是把库对外部依赖找出来的过程。大部分依赖集中在内存分配和临界区保护上。内存分配可以用简单的静态缓冲区替代临界区保护在裸机上直接__disable_irq()/__enable_irq()就行。3.2 裸机环境适配时钟、中断、延时USB控制器对时钟要求很严格。全速USB要求48MHz的USB时钟AT32系列一般通过PLL分频得到。如果USB时钟不对设备上电后枚举时会频繁复位表现为主机不断重复“设备描述符请求失败”。这个坑我第二次移植时又踩了一次排错排了大半天最后发现是初始化顺序的问题——先把系统时钟切到PLL再配置USB时钟源顺序反了就会出现诡异问题。中断处理也不复杂。USB控制器产生中断后会进入对应的中断服务函数。裸机环境下这个ISR直接调用USBx Device库的中断处理入口即可。注意在ISR里不要做耗时操作把数据搬运和回调都留在库内部处理应用层如果需要处理数据通过标志位或者环形缓冲区延后到主循环执行。延时函数也比较关键。USB复位、端点使能这些操作有时序要求。裸机上如果没有现成的毫秒延时我直接用SysTick实现了一个简单的延时函数。SDK里RTOS版本用的都是OSAL层的延时standalone移植后直接换成自己的实现就行。3.3 初始化流程与主循环调度USB设备初始化的流程我整理成了一套固定顺序每次新板子都是这么跑配置系统时钟确保USB时钟为48MHz使能USB控制器GPIO引脚配置D/D-为复用功能复位USB控制器等待就绪调用USBx Device的初始化函数注册描述符和类驱动回调连接USB有些控制器是软件拉上拉电阻有的控制内部上拉等待主机枚举。初始化完成之后应用逻辑就是裸机主循环。HID的上报、CDC的收发都通过主循环轮询来实现。这里我强烈建议用环形缓冲区来收发数据。USB中断把数据搬进缓冲区主循环再从缓冲区取走或者主循环把待发送数据放进缓冲区USB中断有IN令牌时从缓冲区取数据发送。这样中断和主循环不会互相踩内存处理不过来时最多丢数据不会导致系统崩溃。3.4 数据收发接口封装把USBx库跑起来之后还得给应用层封装一套顺手的接口。我这边封装了几个接口HID发送把当前按键状态和状态字打包通过HID中断IN端点上报CDC发送把需要发往主机的数据通过CDC Bulk IN端点发送CDC接收回调主机下发数据时把数据搬运到接收缓冲区并置一个数据就绪标志CDC线路编码处理主机修改波特率时系统会调用这个回调至少要做个空实现不然Set Line Coding请求失败虚拟串口可能打不开。接口封装有个原则应用层不要直接操作端点号也不要直接调用usbd core的底层函数。中间加一层一是方便后续换芯片二是让逻辑层好维护。比如HID报表内容将来要从上报按键改成上报摇杆数据只需要改封装层不用动主逻辑。4. 踩坑实录与排查技巧4.1 枚举失败 / 设备描述符请求超时这是USB开发最常见的问题没有之一。现象是USB插上后主机提示“无法识别的USB设备”或者枚举设备时设备反复复位。我的排查顺序是固定的先量USB时钟频率不对先解决时钟看D上拉是否正常全速设备是靠D上拉告诉主机“这里有全速设备”的用USBlyzer或者Linux下的dmesg抓枚举过程看主机发出的第一个请求是什么设备有没有响应对比抓包得到的描述符与代码里定义的描述符逐字节检查。其中第三步最有价值。我遇到过一例很隐蔽的问题描述符数组里“配置描述符总长度”只差了1个字节枚举到配置描述符那一步就卡死前面设备描述符一切正常。这种问题不用抓包工具盯着设备管理器看一辈子也看不出原因。4.2 设备管理器报错、HID 感叹号Windows设备管理器里如果HID设备出现感叹号常见原因有两类一是描述符返回的数据跟主机期望的不一致二是Report Descriptor校验失败。描述符里的HID描述符有两个字段很容易写错bcdHID版本号一般写0x0111wDescriptorLength必须和实际Report Descriptor长度一致。之前见过有人在HID描述符里把bNumDescriptors写成2说自己想放两个Report Descriptor结果主机只认第一个后面全乱套。Report Descriptor本身也要注意。如果你声明了输入报表但中断IN端点一直不上报数据部分系统会在设备管理器里给HID设备打个黄色感叹号提示“无法启动”。所以在验证阶段建议先让HID定时上报数据确认主机能读到再做按键触发上报的逻辑。4.3 CDC ACM 识别成未知设备或串口无数据CDC ACM识别异常九成问题出在描述符剩下的一成出在回调没有注册。CDC识别成未知设备优先检查两个地方第一Union Functional Descriptor里bControlInterface和bSubordinateInterface0是否填对了接口编号。很多人把Union描述符里的接口编号写错导致操作系统无法把控制接口和数据接口关联成一个串口功能。第二ACM Functional Descriptor的bmCapabilities字段。如果你希望主机支持Set Line Coding、Get Line Coding这些标准串口控制请求这个字段要按规范设置。有些极简实现把bmCapabilities全写0Windows下也能识别成串口但打开串口时可能会异常。还有个常见问题是识别成串口后打开串口发数据没反应。这时候要看CDC的两个Bulk端点方向有没有搞反。USB的IN方向是设备到主机OUT方向是主机到设备如果你把主机发来的数据放到了IN端点数据自然就发不出去。4.4 数据乱码、丢包与实时性瓶颈运行起来之后最闹心的是数据乱码和丢包。我遇到过的原因有三种第一种端点FIFO配置不足Bulk端点数据量稍大就丢包。解决办法是给Bulk端点分配双缓冲的FIFO空间AT32系列可以在配置里直接指定端点缓冲区大小尽量给Bulk端点留够。第二种中断处理里数据拷贝耗时太长导致下一包数据到来时上一包还没处理完。解决办法是中断里只做指针搬运和标志置位实际数据解析放到主循环。第三种没有处理ZLP零长度包。当你发送的数据长度刚好是端点最大包长的整数倍时USB协议要求再发送一个零长度包表示传输结束。很多批量传输丢最后一个包就是ZLP没处理。在USBx库的CDC类驱动里一般会处理但如果你用的是自己封装的接口记得把这点加上。实时性方面HID的上报间隔主要看中断端点的bInterval。全速设备bInterval单位是ms键盘类通常用10ms自定义HID需要低延迟就设1ms但端点占用带宽也会增加。CDC的数据吞吐主要靠Bulk端点本身没有实时保证所以不要把关键控制指令走CDC通道控制逻辑走HID才是最稳的。4.5 好用的调试工具和方法最后分享几个我调试USB设备时常用的工具组合。Windows下最推荐的是USBlyzer可以抓USB枚举过程和后续的传输数据。Bus Hound也行但界面古老数据解析不如USBlyzer直观。设备管理器里的“查看→显示隐藏的设备”也能看到一些问题设备但信息量太少。Linux下就方便了dmesg直接看内核日志插上设备后什么错误一目了然lsusb -v可以查看设备的完整描述符lsusb -t可以看设备树结构USB口的实际数据抓包可以用Wireshark配合usbmon。HID调试方面Windows下用HID调试助手或者官方自带的“游戏控制器”面板查看HID输入报表。CDC调试就用串口助手先确认枚举出的COM口号再打开测试收发。调试阶段还有个隐藏技巧在HID或者CDC的数据里加一个递增计数器。如果计数器连续说明数据通路没问题如果跳变说明有丢包如果乱序说明缓冲区管理有并发问题。这个小技巧帮我解决了很多看起来莫名其妙的“偶尔丢数据”问题。5. 最后的一点体会与建议整套移植做完之后最大的体会是USB复合设备本身不难难的是把“描述符”这关过了之后还要在一个精简工程里把所有依赖都理顺。USBx Device库拆出来之后代码量增加不多但可控性比之前用官方大工程强太多出问题能直接看到底层逻辑。有几个习惯我建议保持一是描述符相关代码全部用宏和sizeof自动计算长度不要手算手算必出错二是保留一份USB抓包工具枚举失败时候先抓包再改代码三是每次改动只动一个变量测完再动下一个USB问题很多时候是“多个小问题叠加”造成的一次性改太多反而定位不了问题。如果后续还想扩展可以考虑在这套框架上增加第二个HID接口比如把厂商自定义HID和键盘HID同时做进去或者把CDC从ACM改成RNDIS做虚拟网卡。USBx Device库的类驱动设计基本都支持多实例注册核心逻辑不变只是描述符和端点规划要重新排。这次lat1486 usb hid这个项目做到这里告一段落后续有新的坑我再来补充。
返回列表