ARTICLE DETAIL

资讯详情

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

STM32U585 USB枚举失败:RxFIFO连改两次导致EP0_IN失效的排查与修复

STM32U585 USB枚举失败:RxFIFO连改两次导致EP0_IN失效的排查与修复 最近在STM32U585上调USB Device从F4上移植了一套CDC虚拟串口代码本以为换个芯片编译就能跑结果烧进去之后设备死活不能被电脑识别。用逻辑分析仪抓D/D-发现主机发完SETUP请求之后设备端根本没把数据放上总线Get Device Descriptor这步就卡死了。排查到最后问题居然出在一个特别容易忽视的细节上在调用HAL_PCD_Start之前我连续改了两次RxFIFO的大小。这个标题我在ST社区和GitHub的STM32CubeU5仓库里也看到过类似的帖子现象描述基本一致。这篇文章就把这个问题的来龙去脉讲透。包含FIFO在U585上的分配原理、HAL_PCD_Start到底在启动时做了什么、为什么“改两次RxFIFO”会单独把EP0_IN搞坏以及正确的配置姿势和排查手段。内容偏向有基础但没深究过USB协议栈的嵌入式开发者如果你正在用STM32的USB Device库或者准备把项目从F1/F4往U5上搬这篇应该能帮你省下好几个下午。1. 问题现场这个Bug到底是什么样的1.1 我遇到的典型表现先说现象。代码逻辑大概是这样的使用STM32CubeMX生成了U585的USB Device工程选了CDC类然后在用户代码里加了自定义的FIFO配置大概是为了保证大批量接收不丢包特意把RxFIFO调大了。具体调用顺序类似下面这样// main.c 中HAL_PCD_Start 之前 HAL_PCDEx_SetRxFiFo(hpcd, 0x180); // 第一次调整 RxFIFO // ... 中间隔了几行其他初始化代码 ... HAL_PCDEx_SetRxFiFo(hpcd, 0x200); // 第二次调整 RxFIFO HAL_PCD_Start(hpcd);烧进去之后Windows那边要么提示“无法识别的USB设备”要么在总线分析工具里看到设备枚举反复失败。用Bus Hound或者Wireshark的USB抓包看主机发SETUP请求设备能ACK但后续数据阶段没有响应。换句话说SETUP包能收到设备也能识别出这是个控制请求但真正要把描述符数据发出去的时候就卡住了。这其实就是EP0_IN的问题。USB控制传输分三个阶段SETUP阶段、数据阶段可选、状态阶段。当主机请求设备描述符时数据阶段是IN方向设备需要把描述符字节从EP0的TX FIFO里发出去。如果EP0_IN的FIFO出了问题最直接的表现就是SETUP阶段正常、设备地址能被设置、但读描述符超时。1.2 为什么会只坏EP0_IN很多人在遇到这类问题时第一反应是检查端点配置、检查描述符、甚至怀疑是时钟配置有问题。但如果你把USB换成HID或者MSC类问题依然存在而且坏的都是枚举早期阶段那基本可以排除描述符内容本身的问题。真正的原因是EP0_IN依赖的那块FIFO内存区域被后来被改大的RxFIFO给“吞”了。STM32的USB OTG控制器里所有FIFO都在一块USB专用SRAM中RxFIFO和各个IN端点的TxFIFO是从低地址到高地址顺序排布的。RxFIFO如果无脑调大后面所有TxFIFO的起始地址都得跟着挪。你以为改了RxFIFO大小只是个独立参数实际上它会影响整个FIFO内存池的地图。EP0_IN之所以先遭殃是因为它的TX FIFO通常紧挨着RxFIFO排布而且EP0是所有USB设备通信的必经之路。其他BULK端点的FIFO排得靠后可能暂时还没冲突但EP0这个位置是首当其冲的。这也是为什么只看到EP0_IN挂掉而其他端点看起来“好像还没问题”的原因。2. U585的USB FIFO分配机制2.1 RxFIFO和TxFIFO是一块共享SRAM很多从标准外设库或者低端单片机转过来的开发者对USB里的FIFO理解往往是“每个端点一块独立的缓冲区”但实际上STM32的USB OTG控制器不是这么设计的。它把整块USB专用SRAM当作一个动态分配的内存池通过几个寄存器来划分各个FIFO的起始地址和大小。U585的USB OTG_FS主要涉及这几个寄存器寄存器位域功能GRXFSIZ[15:0] RXFDRxFIFO深度单位是32位字4字节DIEPTXF0[15:0] TX0FDEP0的IN方向TX FIFO深度DIEPTXF0[31:16] TX0FSAEP0的IN方向TX FIFO起始地址DIEPTXF1[15:0] TX1FDIN EP1的TX FIFO深度DIEPTXF1[31:16] TX1FSAIN EP1的TX FIFO起始地址DIEPTXF2/3/...同上其他IN端点的TX FIFO配置注意这里的“深度”和“起始地址”都是以32位字为单位的。也就是说如果GRXFSIZ设置成0x100表示给RxFIFO分配了256个字也就是1024字节。这个单位概念在手动算FIFO大小的时候非常容易搞混我见过有人把字节数直接当作字数填进去结果RxFIFO被配置成远大于实际SRAM容量后面所有FIFO全部错乱。整块FIFO内存池的排布是线性的大致这样地址低 - 高 --------------------- | RxFIFO | 起始地址0大小GRXFSIZ --------------------- | EP0 IN TX FIFO | 起始地址GRXFSIZ大小DIEPTXF0.TX0FD --------------------- | IN EP1 TX FIFO | 起始地址上一个结束地址大小DIEPTXF1.TX1FD --------------------- | ... | ---------------------所以每一个FIFO的“起始地址”都等于之前所有FIFO大小之和。这个依赖链路决定了一件事改任何一个FIFO的大小后面所有FIFO的起始地址都必须同步调整。2.2 为什么改RxFIFO会连坐TxFIFO从上面那张排布图就能看出RxFIFO是整块内存池里最靠前的那一块。它的大小一旦变化后面所有FIFO的起始地址都会受到影响尤其是紧挨着它的EP0 IN TX FIFO。举个例子假设初始配置是这样的RxFIFO: 起始0x000大小0x100 EP0 TX: 起始0x100大小0x040 EP1 TX: 起始0x140大小0x080如果你把RxFIFO从0x100改成0x200但EP0 TX的起始地址还停留在0x100那么新的RxFIFO区域就覆盖到了0x1FFEP0的TX FIFO整块区域都被RxFIFO占用了。硬件层面RxFIFO和EP0 TX FIFO指向了同一块SRAM这是一场灾难。设备在收到SETUP之后想从EP0 TX FIFO读数据发出去结果读到的是RxFIFO里的残留内容或者根本没有有效数据标记USB核心直接不响应IN令牌主机侧表现为超时。这里有一个很容易让人误解的地方你只是“改了一个参数”为什么硬件不自动帮你把后面的FIFO都安排好因为USB控制器只是个外设它没有操作系统也没有一个“内存管理器”来统一协调这些FIFO。所有排布规则都由软件来保证。HAL库只是提供了一组寄存器读写接口它不会帮你检查当前RxFIFO大小会不会和后面的TxFIFO重叠。注意无论是CubeMX生成的代码还是手写的HAL初始化FIFO的划分从来都不是“按比例自动分配”而是由软件明确指定每个FIFO的起始地址和大小。一旦你手动调用HAL_PCDEx_SetRxFiFo修改了RxFIFO却没有同步修改对应的TxFIFO寄存器FIFO重叠的问题就会悄然而至。3. HAL_PCD_Start前后的FIFO初始化时序3.1 HAL_PCD_Init与HAL_PCD_Start各自做了什么要理解“改两次RxFIFO”为什么会出问题得先搞清楚HAL库在初始化USB设备时不同阶段做的工作分别是什么。HAL_PCD_Init做的事情很多其中和FIFO相关的核心操作是调用内部的USB_DevInit不同HAL版本函数名可能略有差异。USB_DevInit会从hpcd-Init结构体里读取FIFO配置然后把这些配置写入GRXFSIZ、DIEPTXF0、DIEPTXF1等寄存器。也就是说HAL_PCD_Init是“把Init结构体里的规划落进寄存器”的动作。而HAL_PCD_Start呢它在库里的职责更偏向“使能设备、打开EP0、触发连接”。它并不会再次执行USB_DevInit也不会根据当前寄存器值重新计算每一块FIFO的布局。所以如果你在HAL_PCD_Start之前通过HAL_PCDEx_SetRxFiFo改了RxFIFO却没有触发一次完整的“FIFO重新规划”那寄存器里RxFIFO和TxFIFO之间就可能不一致。有个更隐蔽的点HAL_PCDEx_SetRxFiFo这个函数在不同版本的HAL库里实现逻辑不完全一样。在部分版本里它会同时更新hpcd-Init.RxFIFOSize和寄存器GRXFSIZ在另一部分版本里它可能只更新了Init结构体真正写寄存器要靠后续的USB_DevInit来完成。如果你的HAL版本是前者那改完寄存器后如果没有同步改TxFIFO问题立刻暴露如果是后者改完Init结构体后必须重新走一遍HAL_PCD_Init或者USB_DevInit否则寄存器根本没变。这就能解释为什么很多人“改了RxFIFO没效果”或者“改了RxFIFO系统挂了”——都是因为函数行为、调用顺序和FIFO依赖链这三者之间没有对齐。3.2 HAL_PCDEx_SetRxFiFo到底改了什么看这个名字很多人以为它只是改RxFIFO大小其实它写的寄存器就是GRXFSIZ。这个寄存器里的RXFD位域决定了RxFIFO能接收多少数据但它没有能力去修改DIEPTXF0/DIEPTXF1这些TxFIFO寄存器的起始地址。你可以这样理解GRXFSIZ是“给RxFIFO画了一块地”DIEPTXF0的TX0FSA是“给EP0的IN FIFO画了一块地”。你只把RxFIFO这块地扩大了一圈却没有移动EP0的界碑那EP0的地就被吞了。我手头一个项目里就踩过这个坑。CubeMX默认生成的CDC工程里RxFIFO是0x80EP0的TX FIFO是0x40BULK IN的TX FIFO是0x80。我为了加大接收缓冲把RxFIFO调到了0x180结果EP0的TX FIFO起始地址还留在0x80的位置上。最终现象就是设备能收到SETUP包也能正确返回ACK但一旦要发送描述符总线上就一片死寂。这种问题查描述符、查端点配置是查不出来的因为USB协议栈的上层逻辑完全正常问题出在更底层的FIFO分配上。3.3 “两次修改”为什么会破坏一致性现在回到标题里的关键词Modifying RxFIFO size twice。为什么改一次可能没事改两次反而更容易翻车这里说的“两次修改”我总结下来通常有两种典型情况。第一种情况第一次修改发生在HAL_PCD_MspInit里。HAL_PCD_Init会先调用HAL_PCD_MspInit然后在MspInit返回之后才执行USB_DevInit去把FIFO配置写入寄存器。如果你在MspInit里调用了HAL_PCDEx_SetRxFiFo那这个改动会被后续的USB_DevInit采纳最终寄存器配置和你第一次修改的值是一致的看起来没问题。但如果你回到main函数后又在HAL_PCD_Start之前改了第二次RxFIFO那么这第二次修改只改了GRXFSIZ却没有重算TxFIFO的起始地址。此时第一次修改已经按旧布局初始化好了TxFIFO第二次修改又把RxFIFO的地盘扩大重叠就发生了。第二种情况两次修改都在HAL_PCD_Start之前中间隔了一些代码。第一次修改后你可能基于这个RxFIFO大小调用了HAL_PCDEx_SetTxFiFo设置了某个IN端点的TX FIFO起始地址。第二次修改RxFIFO后新值覆盖了原来的规划但那个IN端点的TX FIFO起始地址不会自动跟着变。只要第二次修改的RxFIFO比第一次大就会把后面某个FIFO的头部覆盖掉。如果最先被覆盖的就是EP0 IN的TX FIFO那EP0_IN就保不住了。如果你遇到的问题是“只改一次没事改两次就挂”大概率就是上面这两种情况之一。本质上不是“两次”这个次数本身有问题而是“第二次修改时没有把整张FIFO地图重新画一遍”。注意HAL_PCDEx_SetRxFiFo和HAL_PCDEx_SetTxFiFo是两个独立的函数各自只改各自的寄存器。它们不会自动联动。修改RxFIFO后你必须重新设置每一个IN端点对应的DIEPTXFx寄存器确保起始地址落在新的RxFIFO边界之后。4. 正确配置RxFIFO的三种姿势4.1 推荐方案一次性在Init结构体里定好最省心的做法是不要在执行阶段反复去改FIFO而是在初始化之前把整套FIFO规划写进hpcd.Init结构体让HAL_PCD_Init一次搞定。CubeMX生成的USB工程里通常有这样一个结构体初始化hpcd.Instance USB_OTG_FS; hpcd.Init.dev_endpoints 6; hpcd.Init.speed PCD_SPEED_FULL; hpcd.Init.ep0_mps EP_MPS_64; hpcd.Init.phy_itface PCD_PHY_EMBEDDED; hpcd.Init.low_power_enable DISABLE; hpcd.Init.lpm_enable DISABLE; hpcd.Init.battery_charging_enable DISABLE; hpcd.Init.RxFIFOSize 0x180; hpcd.Init.TxEndPointFIFOSize[0] 0x40; hpcd.Init.TxEndPointFIFOSize[1] 0x80; // ... 其他端点 ... HAL_PCD_Init(hpcd);这样做的关键点是所有FIFO的深度都在同一个地方定义HAL_PCD_Init内部会用这些值一次性设置好GRXFSIZ、DIEPTXF0、DIEPTXF1等寄存器。软件层面不存在“先改了一个后又改另一个”的窗口FIFO布局从启动那一刻就是一致的。如果你是在CubeMX里配置的请在USB的Device Parameters里直接填好各个FIFO大小不要生成完代码再在main函数里重复调用HAL_PCDEx_SetRxFiFo。这种做法最容易出现“一次在MspInit里改、一次在main里改”的撕裂式配置。4.2 用HAL_PCDEx_SetRxFiFo的正确调用时机如果你确实需要在代码里动态修改RxFIFO一定要把“改RxFIFO”和“改后续所有TxFIFO”放在同一个逻辑块里并且最好在HAL_PCD_Init完成之后、HAL_PCD_Start之前一次性完成之后不要再有第二次修改。推荐的调用模式是这样// 一次性调整整个FIFO布局 HAL_PCDEx_SetRxFiFo(hpcd, 0x180); HAL_PCDEx_SetTxFiFo(hpcd, 0, 0x40); // EP0 IN HAL_PCDEx_SetTxFiFo(hpcd, 1, 0x80); // IN EP1 // 其他IN端点同理注意HAL_PCDEx_SetTxFiFo的第二个参数是端点号。0号端点就是EP0的IN FIFO它对应的寄存器是DIEPTXF0起始地址由HAL库根据当前RxFIFO大小自动计算还是需要手动指定取决于HAL库实现。在大多数STM32 HAL版本里这个函数会读取当前的GRXFSIZ作为起点然后依次排布后面的FIFO所以只要你连续调用并且没有中途插入其他写FIFO寄存器的操作布局是一致的。但这里有个大坑如果你在第一次调用HAL_PCDEx_SetRxFiFo之后、调用HAL_PCDEx_SetTxFiFo之前有任何代码重新触发了USB_DevInit或者HAL_PCD_ReInit那RxFIFO又会被Init结构体里的旧值覆盖。结果就是你以为改成了0x180实际上最终还是老配置。这种“改了等于没改”的问题同样麻烦因为它不会报错只会让性能达不到预期。4.3 为你的端点算一套FIFO参数手动规划FIFO之前先明确一个单位问题寄存器里的深度以“32位字”为单位。1个字等于4字节。一个全速USB包最大64字节等于16个字。以一个典型的全速CDC设备为例包含EP0控制端点、一个BULK IN端点、一个BULK OUT端点MPS都是64字节。RxFIFO要容纳的东西包括SETUP包、OUT方向的数据包、控制传输的状态阶段包。正常规划时RxFIFO至少需要能装下SETUP包8字节 2字 OUT最大包64字节 16字 状态阶段包64字节 16字这是最小需求但实际使用中建议留些余量。因为BULK OUT传输可能连续到达多包如果RxFIFO太小控制器来不及往内存搬运就会被新包冲掉。我在项目里一般会给每个OUT端点至少留两个MPS的空间再加一个MPS的余量。按这个思路CDC场景可以这样分配RxFIFO: 0x100 (256字 1024字节) EP0 TX: 0x040 (64字 256字节) BULK IN: 0x100 (256字 1024字节)总大小0x240字。如果你的U585 USB SRAM够放这个配置是稳的。如果SRAM紧张可以压缩BULK IN的TX FIFO到0x80但要考虑到BULK IN端点在host侧发起连续IN令牌时TX FIFO太小会导致NAK频繁拉低传输速率。U585的USB是全速设备全速BULK理论带宽也就1MB/s左右实际打个八折TX FIFO给到256字已经完全够用。不用盲目追求大FIFO够用就好。注意算完总和之后一定要确认总字数不超过芯片USB控制器FIFO SRAM的总容量。具体值以对应型号的参考手册为准。不同封装的STM32U585可能有所差异别想当然按照别的型号来。5. 排查这种问题的手段5.1 调试器里直接读寄存器当怀疑是FIFO重叠问题时最快的确认方式是在调试器里读出几个关键寄存器的值自己手算一遍。需要读的寄存器USB_OTG_FS-GRXFSIZ USB_OTG_FS-DIEPTXF0 USB_OTG_FS-DIEPTXF1用调试器或者串口打印把值拉出来看。假设你读到GRXFSIZ 0x00000200 DIEPTXF0 0x01800040其中DIEPTXF0的高16位是0x0180代表EP0 IN的起始地址是0x180低16位是0x0040代表EP0 IN的深度是0x40。计算一下0x180 0x40 0x1C0还没有超出RxFIFO的0x200说明EP0 IN的FIFO已经完全落在RxFIFO里面了。这个配置必然是坏的。正常的配置应该是GRXFSIZ 0x00000180 DIEPTXF0 0x01800040这样EP0 IN的起始地址0x180正好等于RxFIFO的深度0x180两个FIFO首尾相接没有重叠。我排查这类问题时会在代码里加一段自检逻辑uint32_t rx_size USB_OTG_FS-GRXFSIZ 0xFFFFU; uint32_t ep0_tx_start (USB_OTG_FS-DIEPTXF0 16) 0xFFFFU; uint32_t ep0_tx_size USB_OTG_FS-DIEPTXF0 0xFFFFU; if (ep0_tx_start rx_size) { /* EP0 IN FIFO 与 RxFIFO 重叠必须修正 */ }这段逻辑放在HAL_PCD_Start之后执行可以帮你快速在固件里抓到配置错误不用每次都用调试器看寄存器。5.2 USB分析仪/逻辑分析仪抓控制传输寄存器自检确认了FIFO配置有问题但如果你想看看USB总线上到底发生了什么可以用逻辑分析仪抓D/D-信号或者用支持USB协议解析的工具。抓包时的关注点主机发SETUP令牌设备是否回复ACK第一阶段之后的IN令牌设备是回复DATA0还是NAK/STALL如果设备对IN令牌无任何响应大概率是FIFO层面前提不满足如果设备回NAK说明FIFO里没有准备好数据可能是描述符没有正确写入EP0 TX FIFO如果设备回STALL说明端点状态异常可能是固件主动STALL了请求在EP0_IN故障场景下最常见的是SETUP阶段正常IN阶段无响应。因为设备收到SETUP后固件处理完请求准备往EP0 TX FIFO写数据。但如果EP0 TX FIFO和RxFIFO重叠写入操作根本不会产生一个有效的“FIFO非空”状态硬件层面不会把数据送出去。主机那边就一直等等到超时。这种故障用Wireshark配合Linux的usbmon或者Windows上的Bus Hound都能看得很清楚。如果身边没有专门的USB分析仪逻辑分析仪采样率够高也能凑合抓出来的数据报文基本能定位到是哪一阶段出了问题。5.3 查HAL返回值和状态机另外一个容易被忽略的排查入口是HAL函数的返回值。HAL_PCDEx_SetRxFiFo和HAL_PCDEx_SetTxFiFo在部分实现里会检查当前USB状态。如果状态不满足条件函数可能返回HAL_ERROR但很多人不看返回值以为配置成功了。建议在调用后加上断言if (HAL_PCDEx_SetRxFiFo(hpcd, 0x180) ! HAL_OK) { Error_Handler(); }如果这里真的返回了HAL_ERROR那就说明调用时机的确不对。HAL库内部可能要求设备处于RESET状态或者至少不能在RUNNING状态改FIFO。了解你手上HAL版本函数实现的具体逻辑比盲目猜测更高效。另外可以看看hpcd-State的值。在调试器里观察HAL_PCD_Start前后State的变化如果调用HAL_PCDEx_SetRxFiFo时State已经不是预期的RESET状态函数可能只是改了内存变量甚至什么都没改。这个“改了等于没改”的假象比直接报错更坑。6. 常见问题速查表与避坑心得6.1 速查表现象可能原因解决思路设备完全无法枚举主机报“无法识别的USB设备”FIFO布局整体错误RxFIFO溢出或与TxFIFO重叠重新规划全部FIFO在Init结构体里统一配置枚举到一半失败Get Device Descriptor超时EP0 IN TX FIFO被RxFIFO覆盖检查DIEPTXF0的起始地址是否等于GRXFSIZ设备能枚举但反馈“USB device descriptor request failed”控制传输数据阶段异常可能FIFO重叠或EP0 TX FIFO大小不足核对EP0 TX FIFO深度至少等于EP0 MPSBULK IN传输速度极慢频繁NAKIN端点TX FIFO太小增大对应端点的TX FIFO并同步调整后续FIFO起始地址修改RxFIFO后完全没有效果HAL_PCDEx_SetRxFiFo未真正写寄存器或被后续Init覆盖检查函数返回值确认调用顺序和HAL版本行为连续调用两次SetRxFiFo后主机枚举失败第二次修改未联动TxFIFO或状态机不允许只在初始化阶段一次规划好避免多次修改6.2 我个人反复踩过的坑第一个坑太相信CubeMX生成的代码。CubeMX确实会按你在图形界面里填的参数生成FIFO配置但如果你生成后又手动在main函数里调用HAL_PCDEx_SetRxFiFo就会产生“双写”问题。CubeMX生成的初始化里已经设置过一次你在main里又设置一次相当于改动两次。每次设置都必须保证后续FIFO链的一致性连续两次很容易出错。第二个坑忽略了Init结构体里的TxEndPointFIFOSize数组。很多人只改了RxFIFOSize却没有同步修改TxEndPointFIFOSize。这个数组是给各个IN端点分配TX FIFO用的它和RxFIFO的起始地址计算是强关联的。只改一个必然导致整个FIFO排布错乱。建议在HAL_PCD_Init之前打印或调试查看整个Init结构体的内容确保所有FIFO相关字段都符合预期。第三个坑调试时只看了GRXFSIZ没看DIEPTXF0。GR
返回列表