ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GD32F4xx USB CDC驱动移植实战:从虚拟串口到4G模块通信

GD32F4xx USB CDC驱动移植实战:从虚拟串口到4G模块通信 这块板子在我桌上躺了快两周了。主控是GD32F470要干的事其实不复杂把传感器数据收上来存一份再通过4G模块发到远端服务器同时还要留一个能被电脑随时访问的本地数据口。最开始我图省事打算放一颗USB转UART芯片上去但板子空间实在紧张主控自带的USB外设放着不用又觉得亏于是就走上了GD32F4xx USB CDC驱动移植这条路。最后本地调试口变成了免驱的虚拟串口远程通道走4G模块的AT指令整条链路跑通之后回看整个过程踩坑多、收获也不少。这篇东西就把当时的选型思路、移植细节和排查过程完整记录一遍。1. 项目整体设计与方案选型1.1 为什么用USB CDC而不是外部转接芯片先说结论如果板子面积充足、物料成本不敏感直接放一颗CP2102或CH340确实省心。但这次项目里的采集板面积已经排得很满主控又是GD32F4xx这种自带USB外设的Cortex-M4芯片USB FS控制器闲着不用单独再挂一颗USB转UART芯片怎么看都亏。GD32F4xx的USB模块是片上外设硬件上只需要一组差分引脚加一个3.3V上拉电阻。做USB CDC虚拟串口时电脑端不需要额外安装厂商驱动Windows自带的usbser.sys就能识别成COM口Linux里直接就是ttyACM0这对现场调试和后期维护都很友好。软件成本上GD官方库和STM32的标准外设库结构相近CDC类设备有现成框架移植工作量主要集中在描述符裁剪和底层端点配置上理论上两三个工作日能跑通。不过要认清一个现实GD32F4xx的USB FS是Full-Speed也就是12Mbps。12Mbps看着不小实际批量传输扣掉协议开销和总线仲裁有效吞吐大概在1Mbps到2Mbps之间。用这个通道跑日志、交互指令、脚本配置完全没问题但要拿它当大流量数据搬运通道还是趁早换USB HS外设或者直接走以太网。1.2 4G模块接入方式的取舍4G模块和主控之间的物理接口市面上常见的有三种。第一种是UART串口走AT指令。这是最老派也最稳的方式几乎所有4G模块都保留了这个接口。AT指令包括查询SIM卡、注册网络、配置APN、建立TCP/UDP连接、收发数据整套流程清晰可控适合MCU这种资源受限的场景。缺点是纯串口的波特率上限一般只有921600bps即便模块支持更高的非标波特率在嵌入式侧也很难稳定跑满所以UART方案适合业务数据量不大的物联网场景。第二种是USB接口。很多模块比如移远EC200系列、广和通L610都带USB口内部实现了RNDIS或者ECM拨号网络插到电脑上就是一个虚拟网卡。这种方式的吞吐量上限高模式也贴近通用网络栈但要让MCU通过USB Host去控制模块主控就得跑USB Host协议栈复杂度瞬间上去一个等级内存和CPU开销也不小。第三种是SPI/I2C之类的扩展接口一般用于高速应用或模块做从机直连的场景通用性不强实际项目里用得少。我这块板子的业务数据量不大一次上报几百字节模块和主控之间用UART完全够。USB CDC留给本地调试和数据导出4G模块挂在UART2上两条通路互不干扰后期如果哪条链路出了问题也方便单独排查。2. USB CDC驱动移植的核心细节2.1 时钟配置与端点规划做USB CDC移植首要问题不是描述符而是时钟。GD32F4xx的USB FS外设需要48MHz的时钟输入。我手头的板子主频跑到200MHz从系统时钟到USB时钟要经过专门的预分频链路具体的分频系数不同主频不一样查芯片用户手册的Clock Tree时重点确认USBCK是不是精确到48MHz。频率如果偏了现象非常隐蔽设备能枚举成功但一跑数据就丢包或者主机端频繁报告“设备无法识别”。时钟搞定之后下一步是规划端点。USB CDC虚拟串口的端点建议这样分配端点方向传输类型用途EP0双向控制传输枚举和类请求EP1IN中断传输串口状态通知比如DTR/RTS变化EP2IN批量传输设备向主机上报数据EP2OUT批量传输主机向设备下发数据GD32F4xx的USB模块支持多个端点和双缓冲实际用的时候每个端点都要在软件里分配对应的FIFO空间。FIFO大小不是平均分配的批量传输端点给大一些中断通知端点给16字节就够了。刚开始我按默认配置跑结果EP2 OUT的接收FIFO偏小主机连续下发大数据包时设备端一直丢数据后来重新调整了FIFO分配比例才稳定。2.2 描述符结构里最容易踩的暗坑USB CDC设备在枚举阶段要做的事就是按照协议要求把“自己是什么设备”告诉主机。这里面的核心是描述符。一套能正常识别的CDC设备描述符链路大致是设备描述符声明设备类别为0xEF混合设备、子类0x02、协议0x01这样Windows会认为它是兼容IAD的组合设备。配置描述符集合配置描述符、接口关联描述符IAD、CDC通信接口描述符、CDC功能描述符、端点描述符、CDC数据接口描述符、数据端点描述符。端点描述符紧跟在所属接口之后。顺序尤其重要。我第一次移植时图省事直接拿了别人工程里的配置描述符数组改改就用结果Windows设备管理器里显示“USB Composite Device”下面却没有串口。抓包分析之后发现是IAD放的位置不对导致usbser.sys根本识别不到CDC类接口。如果有条件建议在电脑端先用Wireshark加USBPcap抓一下枚举过程重点看主机请求配置描述符后设备返回的数据结构和USB-IF的CDC ACM规范对照一遍。没有条件抓包就用一套已知能工作的CDC描述符做底板再根据自己需要修改bcdUSB、idVendor、idProduct和字符串描述符。文档上看着很小的字段错了之后排查起来可能一下午起步。2.3 类请求处理虚拟串口为什么“虚拟”CDC设备除了标准USB请求还要处理一句类请求最核心的是SetLineCoding、GetLineCoding和SetControlLineState。SetLineCoding会携带波特率、数据位、校验位等参数GetLineCoding就是把这套参数回传给主机SetControlLineState负责DTR和RTS两个信号。这里要理解一个关键点USB CDC虚拟串口里的波特率不会真正参与数据传输。USB链路相当于一根虚拟管道主机把数据包发给设备设备把数据包收走。上位机串口助手设置波特率时主机通过SetLineCoding发过来的波特率值设备端如果做的是透明串口透传可以直接忽略或者收到后原样保存等将来输出到真实UART时再使用。但类请求不能假装没收到。如果StoreLineCoding的响应处理不完善Windows的串口驱动会认为设备异常表现为打开串口时报“参数错误”或者“设备无法启动”。我在处理GetLineCoding时最初把数据长度搞错了少回了几字节结果一旦上位机打开串口再关闭设备就掉线需要重新插拔——这就是典型的类请求处理不严谨导致的问题。2.4 数据通路的缓冲与跨时钟域问题USB外设和主控总线之间天然存在时钟域差异。USB侧是48MHz工作时钟和总线传输时序MCU侧是AHB或APB总线的时钟两边不在同一个域里数据交接点必须用FIFO或环形缓冲做隔离。我的设计思路是这样的USB设备收到主机发来的数据后先把数据包搬进一个环形缓冲区主循环里的应用代码再从缓冲区里取出处理。向上数据也类似应用要发送的数据先写进发送环形缓冲区再由USB端点中断去取。这样设计的好处是USB中断和应用之间不会因为处理速度不一致互相阻塞。环形缓冲区要注意临界区保护。如果写指针和读指针都只有一个生产者、一个消费者可以在关闭中断的前提下完成更新但如果在中断里写、在主循环里读就不能简单关闭中断了事因为读写指针本身是共享变量。我踩过这个坑中断里写数据主循环读数据两边都没有做保护低负载时没事一压数据就出现随机丢包查了两天才定位到是读写指针竞态。后来改成简单有效的临界区保护把读或写的关键几行塞进关中断的块里问题立刻消失。3. 驱动验证与USB枚举排查3.1 从设备管理器“感叹号”说起驱动移植完成后第一次插上USB线设备管理器里大概率会有一个带着黄色感叹号的未知设备。出现这种现象我们要顺着枚举流程倒查。USB枚举的第一步是主机给设备发复位信号。复位之后设备地址是0主机发一个GET_DESCRIPTOR请求读取设备描述符的前8字节。这时候设备至少要把bMaxPacketSize0字节返回给主机。之后主机给设备分配地址再请求完整的配置描述符。每一步都是一问一答哪一步回答不对枚举就停在那里。最常见的两个坑第一是D引脚的上拉电阻。USB FS设备要求D线上有1.5kΩ上下拉电阻用于告诉主机这设备是Full-Speed。如果主控内部没有集成这个上拉或者软件里没有使能对应的内部上拉主机永远检测不到设备插入设备管理器里连未知设备都不会出现。第二是配置描述符的长度和实际发送长度不一致。描述符数组里配置了一个100字节的配置描述符集合但枚举时主机只收到前80字节USB驱动就会判定描述符非法。这个问题的排查方法很直接数一下配置描述符里各种描述符的长度之和和bLength字段对一下。描述符长度不对是最容易发生又最不容易发现的问题我习惯把配置描述符按块分开注释哪一段是多少字节一眼就能数清楚。3.2 识别成Composite Device但没有串口我第二次移植时遇到的情况更隐蔽主机能识别设备出了问题表现为设备管理器显示“USB Composite Device”但展开之后没有串口号打开设备管理器详细信息页能看到接口描述符但加载不了驱动。后来排查出是IAD和CDC功能描述符的兼容性处理不对。Windows系统的usbser.sys对IAD的解析有严格要求IAD的bFirstInterface要指向通信接口bInterfaceCount要填2如果这里填了1或者填错接口号系统就认为只存在单个接口设备不触发CDC驱动绑定。另外Union Function Functional Descriptor里的bMasterInterface和bSlaveInterface0也要跟实际接口编号一致不一致的话Windows会把通信接口和数据接口当成两个互相独立的设备对待。这类问题用抓包工具看枚举返回值比较直观。主机读取配置描述符后能够看到接口关联描述符和Function描述符对照协议规范检查字段值基本能定位到问题所在。3.3 免驱之后的性能实测驱动装好、设备管理器出现COM口之后并不代表事情就完了。USB CDC虚拟串口的实际吞吐受到很多因素影响我用串口助手连续下发128KB数据测试发现如果PC端发送速度快于设备端处理速度系统会把数据缓存在USB主控制器的缓冲区里短暂看起来一切正常一旦缓冲区满设备端就表现为数据包丢失上位机软件可能还会报“写串口超时”。这就回到缓冲区设计的重要性。我设备端把USB端点OUT的数据放在一个4KB的环形缓冲区里主循环每次轮询取走一批数据去做协议解析或转发到UART。只要缓冲区没有被冲爆数据一个字节都不会丢一旦冲爆再大的缓冲区也无非是延缓了问题出现的时间。所以我的建议是虚拟串口链路一定要让上位机支持流控或者在上位机协议里加入应答确认机制。工业现场的数据链路宁可复杂一点也不要裸奔。4. 4G模块通信实战4.1 模块选型与硬件连接4G模块我用过移远的EC200系列和广和通的L610两者在AT指令风格上有些差异但连接方式大同小异。主控这边要注意的是电平匹配很多4G模块的UART电平是1.8V而GD32F4xx的数字IO是3.3V兼容5V如果模块和MCU间没有做电平转换轻则通信不稳定重则烧坏模块的串口引脚。我这边直接在板上加了电平转换芯片一侧接MCU的3.3V UART另一侧按模块手册要求接1.8V实测非常稳。4G模块的PWRKEY也是一个容易被忽略的点。模块开机不是上电就自动运行的很多型号需要PWRKEY引脚拉低几百毫秒才能启动。主控上电后要先做延时再拉PWRKEY等待一段时间让模块初始化完成然后才能发AT指令。时序不对模块看起来有电但AT指令一条都不回。SIM卡供电同样是坑。模块对SIM卡供电有自己的要求有些模块内部有可调输出有些要求外部供电。我在这块板子上第一版设计时SIM卡供电脚走线过长又没有放足够容量的电容导致模块注册网络时频繁重启后来在SIM卡VCC处加了两个并联的陶瓷电容和一个钽电容问题才消除。4.2 AT指令流程与状态机设计4G模块的启动流程建议不要一开机就噼里啪啦发一堆指令而是按阶段进行。我这边实现的状态机大致分成这几个阶段阶段核心指令预期结果上电自检AT返回OK说明模块已经就绪模块配置ATE0关闭回显减少无效数据SIM卡检测ATCPIN?返回READY代表SIM卡正常读出信号质量ATCSQ返回信号强度值建议大于10网络注册ATCREG?返回0,1或0,5表示注册成功APN配置ATCGDCONT1,IP,cmiot返回OK激活PDPATCGACT1,1返回OK此时已具备IP能力建立连接ATQIOPEN1,0,TCP,服务器地址,端口,0,0返回CONNECT OK数据发送ATQISEND0,要发送的数据返回SEND OK数据接收QIURC: recv,0通知MCU有新数据到达每个阶段都要有超时和重试机制。比如说发AT指令查模块就绪我这边每500ms发一次最多等待10秒查SIM卡和注册网络每2秒查一次最多等60秒。直接写死延时会浪费大量等待时间完全不做超时又可能让主控卡死折中方案是状态机配合定时器轮询。4.3 与USB CDC通道的数据互转板子上两条数据链路最终要打通电脑通过USB CDC向MCU下发数据MCU再把数据通过UART转到4G模块反过来4G模块收到的数据经过MCU进入USB CDC通过虚拟串口在电脑上显示。最直接的实现方式是把USB CDC收到的内容原样转发到4G模块的UART上4G模块UART返回的内容原样从USB CDC发出去这样上位机可以像操作串口模块一样直接操作4G模块调试阶段非常方便。我直接用串口助手打开虚拟COM口然后输入ATQISEND指令发送数据模块返回的结果就在同窗口显示整套链路的状态一目了然。实际产品里这样做不够安全因为上位机可能发来非预期指令。我后来在MCU里加了一层简单的帧协议一帧数据用帧头0xA5 0x5A加长度字段加数据加校验和组成。MCU把完好的帧解析出来写入4G模块模块返回的数据也按同样格式封装后从USB CDC吐出。这样从虚拟串口能看到的数据是干净整洁的业务数据而不是夹杂一堆AT回显的原始串口流。5. 常见问题与排查技巧实录5.1 USB枚举类问题速查现象可能原因排查方向插入USB后设备管理器无任何反应D上拉未使能、VBUS检测未配置用示波器量D引脚是否有上拉波形检查VBUS检测代码显示“设备描述符请求失败”描述符地址不对、DP/DM短路检查配置描述符数组的内存地址确认没有越界和字节对齐问题识别成Composite Device但没有串口IAD或Union描述符配置错误抓包查看配置描述符里接口编号是否一致枚举成功但一传数据就断开端点FIFO配置不足、时钟频率不对检查48MHz时钟增大批量端点FIFO打开串口报参数错误GetLineCoding响应数据长度不对核对类请求处理函数返回的数据长度关于描述符内存地址不对这个问题我想多说一句。GD32F4xx的USB模块访问描述符时如果使用DMA方式搬运对描述符表的内存地址对齐有要求。我遇到过一次设备枚举时好时坏后来发现是描述符数组定义时没有用对齐属性修饰编译器把它放到了一个非对齐地址上。解决办法很简单给描述符数组加上4字节对齐的属性声明。这类问题用逻辑分析仪抓DP/DM波形最直观如果主机发了GET_DESCRIPTOR请求设备没有响应大概率就是描述符表地址或者中断处理的问题。5.2 4G通信类问题实录AT指令无响应。先确认模块是否真的开机了再看TXD和RXD有没有接反。MCU发送引脚要接到模块的接收引脚交叉接法有些人习惯性把发送接发送数据全部走丢。之后检查电平比如1.8V的模块接3.3V的MCU模块能收到但回给MCU的电平信号可能出现误码。能AT但注册不上网络。检查SIM卡方向物理接触是否良好SIM卡VCC电压是否稳定。很多4G模块的天线没接好也能开机但信号强度极低注册网络会反复失败。用ATCSQ查一下信号值如果返回值是99表示没有信号或者天线断路。TCP连接经常掉线。4G模块和服务器之间要做心跳。我这边用MQTT协议保活参数设置的30秒服务器侧正常的TCP连接一般几分钟到几十分钟内没有数据通信就会被运营商或服务器关闭。心跳频率不是越高越好频率太高增加流量消耗频率太低连接容易断开具体数值要结合服务器和运营商策略调整。粘包和半包问题。4G模块UART返回的数据不是按帧组织的MCU收到数据后要自己拼包。我这边在UART接收中断里先把数据攒进缓冲区主循环里按协议帧头找帧。帧头一旦丢失后续数据全部错乱所以我在协议设计里强制要求服务器端每条消息都有唯一的起始标志和长度字段MCU侧发现校验失败会丢弃当前帧重新同步帧头。5.3 系统联调时的整体排查思维USB CDC和4G模块两条链路都调通之后联调阶段出现的问题往往不在单条链路上而是两条链路互相影响。比如USB CDC上位机打开COM口时模块收到了一些奇怪的AT指令或者4G模块数据下发时正好撞上USB CDC正在发送大块数据导致MCU主循环处理不过来。我的排查习惯是先把两条链路从逻辑上彻底隔离。USB CDC链路里做一层数据抓取统计4G链路里也做一层抓取统计两边的数据进出数量一目了然。当发现某一边数据包丢失时优先看这一边的缓冲区是否满了再看主循环处理逻辑里有没有耗时过长的代码段。联调阶段的bug大多不是单点故障而是系统资源的竞争和调度问题不要一上来就怀疑驱动和硬件先从数据流模型上把问题边界画清楚。6. 最后分享几个能直接用的调试技巧6.1 我现在每次做USB CDC调试都会准备的套路第一先用USB分析工具把枚举过程抓下来留存。Windows下我习惯用Wireshark配USBPcapLinux下用usbmon抓包抓到数据直接对照USB规范看比在代码里盲改快得多。第二准备一个USB转TTL小板。很多人把USB CDC调通之后就直接拔掉后面4G模块调试又得接一堆线其实用USB转TTL小板把4G模块单独接到电脑上先在上位机把AT指令全部调通再连接到MCU上联调。这样能把“模块自身问题”和“MCU驱动问题”快速分开。第三给USB CDC设备型号设置一个固定且唯一的PID和VID。有的上位机软件或者串口枚举工具会根据PID识别设备如果你把PID填成0xFFFF这种测试值有些工具会直接不认。量产设备的PID选择一个自定义范围内的值后面驱动签名和识别都省事。6.2 这套方案还能往哪扩展USB CDC通道除了做调试和透传其实还能扩展成一个类似AT命令集的控制通道。主控这边通过虚拟串口接收指令完成参数配置、固件版本查询、历史数据导出等操作。对现场运维的工程师来说插上USB线就能看到完整的状态信息和运行日志比再去拆机插调试器方便太多。4G模块这条链路后期如果带宽需求变大可以考虑把USB Host方案引入主控让GD32F4xx直接控制4G模块的USB口形成RNDIS拨号网络。这样模块不只是发AT指令和数据帧而是能跑完整的TCP/IP协议栈。不过这个方案的工程量和内存占用都会翻倍没有硬需求不要轻易上。最后说一个我踩过最深的坑地址对齐。整个工程编译一次可能要几分钟USB枚举不稳定和数组地址不对齐有关的排查往往要反复刷固件非常消耗耐心。建议各位在工程开始阶段就养成好习惯所有描述符数组、FIFO缓冲区和协议缓冲区一律加对齐属性声明前缀宏直接写进公用头文件里。一个细节的疏忽可能让后面整个联调周期多出好几天。
返回列表