多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32F407 USB Host直连4G模块:扔掉USB转串口芯片,纯AT通信方案

STM32F407 USB Host直连4G模块:扔掉USB转串口芯片,纯AT通信方案 做嵌入式这几年AT通信基本都是老套路单片机串口接USB转串口芯片再通过UART去跟4G模块打交道。直到最近接手一个项目要用STM32F407直连广和通MC665模块做数据上报我才下定决心把那条“USB转串口芯片”的路给断了。原因很简单板子上明明有完整的USB OTG控制器F407的Host模式完全可以把MC665当成一个USB虚拟串口设备来枚举收发AT指令省掉一颗CP2102或CH340不说链路还更干净。这篇文章就完整记录一下这套方案的思路、硬件连接、CubeMX配置、底层驱动适配、AT指令交互以及我实际调试中踩过的坑。如果你手里正好有F407和4G模块想做AT通信却不想在USB转串口芯片上多花一毛钱这篇可以直接当参考路线图用。1. 项目概述为什么把USB转串口芯片“扔掉”1.1 传统方案的三个痛点常见的STM324G模块方案基本分两条路一是MCU的UART直接接到模块的UART口靠模块内部把UART数据打包成USB二是MCU的UART先接一颗USB转串口芯片CH340G、CP2102之类再由这颗芯片USB口接到电脑或者4G模块。无论哪条只要涉及“UART直连”你就得面对波特率配置、硬件流控、电平转换、线材干扰这一堆麻烦。实际项目里最容易恼火的三个问题波特率不匹配导致乱码。模块默认波特率可能跟你代码里配的不一样一旦两端的奇偶校验或者停止位设置没对上出现的就是各种“天书”。电平转换问题。F407是3.3V IO很多4G模块的UART电平虽然是1.8V但兼容性并不统一靠跳线、电平转换芯片强行对接生产时多一个料就多一份焊错的风险。干扰和长线问题。UART信号本身抗干扰能力一般线拉长一点附近有电机或者继电器动作AT回复就丢字节。所以在很多量产项目里USB转串口芯片一直是“不可省”的物料。但这颗芯片真的有必要吗F407自带的USB OTG_FS控制器就能做Host直接跟4G模块的USB口通信一条USB线搞定电源和虚拟串口把中间层全部砍掉。1.2 USB Host直连方案的核心价值这套方案的核心逻辑是把4G模块的USB口当作一个“即插即用”的CDC类虚拟串口设备。F407作为USB Host在模块上电后完成枚举、识别出设备类、加载CDC驱动之后你在代码里调用USBH_CDC_Transmit和USBH_CDC_Receive跟操作一个串口没区别但它绕开了物理UART的一切麻烦。它的价值体现在几个地方省物料、省面积、省调试时间。板上少一颗USB转串口芯片BOM成本降低layout面积也省出来。尤其是做小体积产品这个优势很明显。通信链路更稳定。USB协议本身有握手、重传和带宽分配不会像UART那样丢字节就丢字节AT指令交互的成功率明显更高。为后续升级做铺垫。F407的USB Host不只认CDC类之后如果要接U盘升级固件、接USB摄像头、接HID键盘代码框架是同一个。你只是先用4G模块把这个通路打通而已。当然代价是软件复杂度上来了需要配置USB Host中间件、处理枚举状态机、适配CDC类驱动、自己管理AT超时重试。这些在传统UART方案里根本没人在意但把它们理顺之后项目后期的可维护性会高出不少。2. 硬件方案设计与连接细节2.1 STM32F407的USB资源该选谁F407有两个USB控制器OTG_FS和OTG_HS。用一个表格先看清楚区别控制器内置PHY最大速率典型引脚要不要外部PHYOTG_FS有12MbpsFull SpeedPA11(DM)、PA12(DP)、PA9(VBUS)、PA10(ID)不需要OTG_HS无内置可做FS模式480MbpsHigh SpeedPB14/PB15等接到外部PHY需要外接USB3300这类ULPI PHYAT通信的命令本身并不长一次发几十个字节全速12Mbps已经完全够用。所以绝大多数情况选OTG_FS就够了省掉外部PHYPCB上不用多一颗芯片。只有当你后续想在模块上跑RNDIS虚拟网卡、大流量下载固件时才需要认真考虑OTG_HS。我用的是STM32F407VET6开发板USB_OTG_FS模式内置PHY直接引一组USB座出来插模块硬件成本几乎为零。2.2 MC665的USB接口特性广和通MC665是LTE Cat 4的模块支持国内主要频段USB接口符合USB 2.0规范。模块上电后USB口会枚举出多个接口通常一个CDC ACM口用来跑AT指令还有其他口负责MODEM、DIAG之类。对我们AT通信来说只需要认准CDC ACM的AT信道。这里有个关键点模块枚举成什么设备类型可能是可通过AT指令或者硬件配置切换的。也就是说它可能默认枚举成CDC、也可能枚举成RNDIS、ECM甚至MBIM。如果模块被配成了RNDIS方式F407加载的CDC类驱动就匹配不上那代码层面就要换思路。我们的做法是先用AT指令确认模块当前USB配置或者查询模块手册里的USB模式切换方式锁定在“AT虚拟串口”模式。另外MC665的USB口正常工作需要模块VBAT先供电USB接口本身一般不会给模块供电它是靠VBAT供电然后USB做数据通路的。所以上电时序必须是VBAT先稳定模块启动再出现USB枚举F407这边才能识别到。2.3 供电、信号与关键接线这一节非常重要尤其是供电大电流抖动会直接让USB枚举失败。MC665在LTE发射时峰值电流可以到2A左右所以VBAT不能从F407开发板的5V引脚直接拉那样电压会瞬间跌到3V以下模块直接掉电重启USB枚举永远成功不了。我实际用的是DC-DC降压模块输入12V输出调到3.8V峰值电流至少3A输出侧并联470uF电解电容和10uF陶瓷电容压瞬态。USB信号线方面PA11DM和PA12DP走差分对尽量等长建议控制在10cm以内线宽8~12mil铺地包裹。开发板上如果已经布好了USB座那就省事了如果是自己画板最好在USB座子和MCU之间加共模电感或至少贴片磁珠避免射频干扰从USB线耦合进去。典型接线整理如下STM32F407的PA11 → 4G模块USB_DMSTM32F407的PA12 → 4G模块USB_DPSTM32F407的PA9 → 用于检测USB VBUS电压Host供电必须由MCU的USB口输出5V给模块USB接口的VBUS脚VBUS_EN控制引脚 → 控制5V输出给模块USB接口我用的是PC5低电平关断高电平使能模块VBAT独立3.8V供电GND与STM32开发板共地还要注意USB的ID引脚PA10不接在Host模式下内部会识别为A口如果开发板把ID拉低了也没关系。3. 软件栈搭建CubeMX配置与USB Host驱动适配3.1 CubeMX图形化配置流程软件部分我全部基于STM32CubeMX生成工程再手动补驱动和AT逻辑。打开CubeMX后按下面步骤来芯片选STM32F407VET6RCC打开HSE外部晶振SYS里Debug设为Serial Wire方便上J-Link调试。左侧 Connectivity → USB_OTG_FSMode选Host_OnlyInternal Phy保持默认。这一步是把整个控制器配置成Host模式。左侧 Middleware → USB_HOST → Class for FS IP选Communications Device Class (CDC)。时钟配置里要重点确认USB的时钟是48MHz。我用的HSE是8MHzPLL_N336、PLL_P2、PLL_Q7这样SYSCLK跑到168MHz同时USB OTG FS拿到48MHz。如果USB时钟不是48MHz枚举阶段会卡在复位端口或者完全认不到设备。配置PC5引脚为GPIO_Output用作VBUS_EN。这里不要把CubeMX的“Use VBUS Power Selection”勾选状态搞错通常软件控VBUS的方式是自己操作Vbus GPIO。NVIC打开USB_OTG_FS全局中断优先级给中间值就行。生成代码到MDK-ARM工程。CubeMX生成完之后工程里会多出一堆USB_HOST的文件包括usbh_core.c、usbh_cdc.c、usbh_pipes.c等这些不用改我们要做的是把它接入主循环并实现用户回调。3.2 代码生成后的初始化逻辑自动生成的MX_USB_HOST_Init()里已经注册了CDC类句柄并初始化了USB主机栈。你还要在main()里保证打开VBUS电源然后让USBH_Process()持续运行。直接放一段我用的初始化片段重点是VBUS使能和状态检查int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USB_HOST_Init(); // USB Host VBUS使能拉高PC5给模块USB口送5V HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_SET); while (1) { // 必须周期性调用USB主机栈内部状态机全靠它推进 USBH_Process(hUsbHostFS); APP_USB_Process(); // 用户层AT状态机后面细说 } }细心的朋友会发现我把用户逻辑放在了while(1)里和USBH_Process并行跑。AT交互设计成状态机避免阻塞式等待把USBH_Process卡死。如果你用了FreeRTOS更合理的姿势是开一个1ms周期任务专门执行USBH_Process再开一个任务跑AT指令逻辑两者互不阻塞。3.3 底层驱动的关键回调USB Host库在设备枚举成功后会调用用户回调通常是USBH_User_Process()。这个函数长的样子类似void USBH_User_Process(USBH_HandleTypeDef *phost, uint8_t id) { switch(id) { case HOST_USER_CONNECTION: break; case HOST_USER_DISCONNECTION: break; case HOST_USER_ENUMERATION: break; case HOST_USER_CLASS_ACTIVE: usb_cdc_active 1; // 模块枚举成功CDC类激活 break; } }在这个回调里我只置标志位不做耗时操作。等usb_cdc_active变成1AT任务才开始发送AT\r\n测试。如果模块没插入或者枚举失败这个标志永远为0AT任务就不会盲目发指令。CDC类这边还有一个接收回调要处理。不同版本的STM32Cube包函数名可能不一样常见的有USBH_CDC_ReceiveCallback()也有的版本是通过HAL的HAL_PCD_DataOutStageCallback()把URB数据交付给上层。不管名字叫什么你的核心工作就两件事把收到的数据拷进自己的环形缓冲区然后立刻再次调用USBH_CDC_Receive()启动下一轮接收。我用的固件包中接收回调的处理方式大概是这样的static uint8_t usb_rx_buf[512]; static ring_buffer_t at_rb; void User_CDC_ReceiveCallback(USBH_HandleTypeDef *phost) { // 把USB收到的数据写入AT查询用环形缓冲区 ring_buffer_write(at_rb, usb_rx_buf, USBH_CDC_GetLastReceivedDataSize(phost)); // 立刻重新挂接收否则模块后续数据没地方放 USBH_CDC_Receive(phost, usb_rx_buf, sizeof(usb_rx_buf)); }有个细节USB CDC接收是按URB包为单位送回来的每次回调拿到的可能是一整包或多包数据所以不要期望它像UART中断那样按字节来。AT回复的OK、ERROR、CSQ:之类都是文本行需要我们自己按\r\n切包配合环形缓冲做跨包拼接。4. AT通信实现与核心代码解析4.1 枚举成功后的AT初始化状态机模块枚举完成不代表能直接发业务指令。4G模块上电后有个启动过程需要先AT握手再关回显、查SIM卡、查信号、设APN最后开数据通道。我也是踩过几次后才总结出这个顺序的。完整的AT初始化状态机可以这样设计AT\r\nATE0\r\n关回显避免后面解析干扰ATCGMM\r\n确认模块型号拿到MC665即链路通ATCPIN?\r\n等待返回READY没有卡或者卡未识别后面不用继续ATCSQ\r\n看信号强度返回值第一位0~31一般超过10就能正常工作ATCGDCONT1,IP,你的APN\r\nATCGACT1,1\r\n激活PDP上下文之后按业务需要进入TCP或者MQTT相关指令。这么做的原因很简单如果第1步AT都不回说明USB链路上层还没通后面全白搭。而SIM卡状态没READY就去设APN大概率返回ERROR。每一步都等确认宁可慢一点也别跳步。4.2 USB CDC收发API与消息解析状态机里每个指令我都通过同一个函数发送并等待预期回复伪代码如下int at_send_and_wait(const char *cmd, const char *expect, uint32_t timeout_ms) { uint32_t start HAL_GetTick(); ring_buffer_clear(at_rb); USBH_CDC_Transmit(hUsbHostFS, (uint8_t *)cmd, strlen(cmd)); do { // 宿主循环必须持续调用否则USB Host驱动无法运转 USBH_Process(hUsbHostFS); if (ring_buffer_find(at_rb, expect)) { return 0; // 找到预期字符串 } HAL_Delay(1); } while (HAL_GetTick() - start timeout_ms); return -1; // 超时 }这里有个容易被忽略的坑USBH_CDC_Transmit()只是把数据扔进USB的发送队列真正发出去要等主机栈调度。如果紧接着用HAL_Delay(2000)干等USBH_Process没有跑数据一直发不出去AT回复也进不来。所以等待期间必须穿插调用USBH_Process()。解析AT回复时建议用子串匹配而不是全包匹配。因为USB CDC数据是分段的你可能先收到CSQ: 19下一包才收到OK甚至CSQ: 1和9, 99 OK被拆成两半。我用的是环形缓冲区里做滚动查找匹配到目标字符串就返回。对于CSQ这种需要提取数字的场景拿到整行之后再用sscanf解析。4.3 从UART调试口交互的落地写法AT通信毕竟是给业务逻辑用的开发阶段最好能从调试串口实时看到收发内容。我在USB Host链路跑通后专门加了一个调试UART把AT指令和模块回复都打出来便于观察。实现很简单在AT发送和接收回调里各加一个打印函数即可。void APP_UsbCdc_Trace(const char *dir, const uint8_t *data, uint32_t len) { char temp[64]; snprintf(temp, sizeof(temp), [%s] %.*s\r\n, dir, (int)len, data); HAL_UART_Transmit(huart1, (uint8_t *)temp, strlen(temp), 100); }平时测试时用USB转串口工具连调试口能看到完整的指令交互记录。正式量产时把这些打印用宏关掉就行。5. 常见问题与排查技巧实录5.1 一张表搞定大部分坑整个调通过程里我记录了不少典型故障先整理成速查表现象可能原因解决办法插上模块后USBH一直停在CONNECTED进不了ENUMERATIONVBUS电源没打开或者5V不足检查VBUS_EN引脚电平外接5V供电串联电流表看电流枚举到一半设备掉线供电瞬间跌落或者USB信号线太长独立给模块供电USB差分线尽量短地线共好枚举成功但CDC类不激活模块被配置成了RNDIS/MBIM而不是CDC查阅MC665手册用AT指令或硬件引脚把USB模式切回CDC虚拟串口发送AT没任何回复接收回调没有重新挂载AT数据卡在底层在接收回调末尾立刻重新调用USBH_CDC_Receive回复乱码或半个命令USB数据分片解析只做了单包处理用环形缓冲区做跨包拼接再按行匹配模块一直不开机PWRKEY上没有足够长的脉冲开机信号拉低至少500ms部分模块要求1s以上检查VBAT电压板子一插模块就复位模块发射电流太大拉到MCU电源模块VBAT用独立DCDC不能共用3.3V/VDD其中供电问题出现的频率最高尤其是模块在天线附近处于发射状态时瞬间电流能把开发板电源拉垮。后来我直接在模块电源入口加了47uF0.1uF的退耦并把MCU电源和模块电源在物理上隔离问题才彻底消失。5.2 我实际踩过的几个硬坑挑几个印象最深的展开讲讲。第一个是时钟配置。最开始我用CubeMX默认的HSE 25MHz评估板结果USB怎么都枚举不成功。排查了半天发现USB_OTG_FS并不是直接吃系统时钟而是必须从PLL的Q引脚分频拿到48MHz。25MHz晶振配出来的组合Q输出不是48MHzUSB物理层复位就过不去。后来老老实实按8MHz晶振配168MHz主频QPLL输出48MHz一次点亮。第二个是VBUS使能脚。F407的Host模式下VBUS不是自动输出的需要软件去控制一个GPIO把5V送到USB座子上。不少开发板的原理图上VBUS_EN不是PC5而是别的引脚。我一开始照着板子默认例程没改导致主机完全不对外供电。解决办法很简单看原理图确认VBUS_EN接到哪个IO初始化里显式拉高。第三个是CDC接收回调的“一锤子买卖”。USB Host的CDC Receive是一次性的你调用一次它只接收一次数据。我在第一次调通时只收到模块返回的AT\r\nOK的“AT”部分就再也没下文了。排查发现是回调里处理完数据后没有再次调用USBH_CDC_Receive()于是后续所有数据都停在了底层。这个细节必须刻在脑门上CDC收数据是反复挂载、反复回调的机制不是中断一次就完了。6. 实战心得与后续扩展思路6.1 几点掏心窝的体会这套方案做下来我对“MCU侧USB Host连接4G模块”这件事的看法变了很多。过去大家总觉得USB Host是Linux或者Android的活单片机搞不定。实际上F407这颗芯片的OTG控制器相当成熟HAL库也把枚举过程封装得很好真正难的不是USB协议本身而是你愿不愿意花时间把整个链路捋顺。捋顺之后你会发现AT通信这件事前所未有的干净——没有电平问题、没有波特率问题、没有丢字节焦虑USB协议栈把物理层全部包办了。如果问我最建议后来者注意什么我会说三条电源要给足这是排第一优先级的问题。USS信号线能短尽量短开发板跳线连接时最好直接焊杜邦线或飞线不要用那种一米长的USB延长线。软件上不要用阻塞式等待AT状态机加环形缓冲区再加超时重试才是稳定结构。我后来甚至把这个方案直接搬到了量产板子上删掉了原本预留的CP2102焊盘。BOM少了一颗芯片生产少了一次贴片测试少了一类“USB转串口芯片焊反”的返修一举三得。6.2 还能怎么玩OTA升级与虚拟网卡USB Host这条路走通之后能做的远不止AT指令。比如MC665支持通过USB口进行FOTA升级。传统UART方案升级固件要下载一整包通过串口慢慢灌时间很长而且容易中断。USB虚拟串口的高带宽天然适合做OTA数据通道代码复用同一套收发机制只是把业务数据换成固件分包就好。再进一步如果后续带宽要求上来了模块还能切换成RNDIS模式让F407通过USB虚拟出一个以太网网卡再配合lwIP协议栈MCU就直接变成一台“能上4G网的Linux小主机”跑TCP、MQTT、HTTP都不再是问题。虽然AT方式够用但RNDIS的大流量能力是UART方案完全无法想象的。这套USB Host框架已经是搭好的后面只是在类驱动上做切换而已。嵌入式开发很多时候就是这样硬件资源明明足够只是没人尝试把链路往前推一步。少焊一颗USB转串口芯片不是目的真正有价值的是把STM32F407的USB Host能力用起来让单片机在通信链路中的地位更进一步。
返回列表