公司动态

STM32 USB设备枚举失败:FIFO RAM布局与配置顺序分析

📅 2026/8/30 15:35:30
STM32 USB设备枚举失败:FIFO RAM布局与配置顺序分析
1. 现象复盘一次稳定的USB枚举失败前阵子在 STM32U585 上调 USB 设备遇到一个很让人摸不着头脑的问题功能逻辑没有任何改动只是把 FIFO 配置里的HAL_PCDEx_SetRxFiFo调用重复了一次而且是在HAL_PCD_Start之前完成的枚举就挂了。主机侧表现很典型——设备管理器里永远是“unknown device”用协议分析仪抓包能看到主机反复发送 GET_DESCRIPTOR 请求设备却始终不回复。单步进去HAL 的 USB 中断状态看起来也是正常的IN 端点却没有触发完成回调。后来把那次重复调用去掉设备立刻恢复正常。这不是运气问题而是 FIFO RAM 布局在 HAL 库实现里存在一个隐式依赖所有 TX FIFO 的起始地址都是基于当时的 RxFIFO 大小来计算的。只要配置顺序不对EP0_IN 传输就会在枚举阶段暴雷。这个问题的复现场景很好还原。假设你有一段从旧工程迁移过来的初始化代码HAL_PCDEx_SetRxFiFo(hpcd, 0x180); HAL_PCDEx_SetTxFiFo(hpcd, 0, 0x40); HAL_PCDEx_SetTxFiFo(hpcd, 1, 0x80);然后因为某些调试需要比如想让接收缓冲区更大你又在后面加了一行HAL_PCDEx_SetRxFiFo(hpcd, 0x200);紧接着调用HAL_PCD_Start(hpcd);表面上看FIFO 配置在设备启动前全部完成没有任何异步操作介入理论上不该出问题。但实际表现为设备无法完成枚举原因就是第二个HAL_PCDEx_SetRxFiFo把之前已经分配好的 TX FIFO 偏移全部变成了“过期配置”。我必须强调一个容易误判的点问题表象是 USB 设备没有枚举成功但根因并不是时钟、不是中断优先级、不是上拉电阻而是端点 FIFO RAM 地址产生了冲突。很多人在这种问题面前会去查 DSTATUS、查 USB 中断标志、甚至换 PHY结果绕了一大圈才回来。本文把这个坑从头到尾拆开包含原理、排查过程和工程上的防呆办法供同样在 U5 或其它 STM32 OTG 系列上做 USB 设备开发的同学参考。1.1 代码里的“重复调用”具体长什么样先说这次调试的原始工程结构。CubeMX 生成的MX_USB_PCD_Init里已经有一份标准的 FIFO 初始化序列然后我在新加的一个独立 USB 配置模块里又写了一遍类似这样void MX_USB_PCD_Init(void) { hpcd.Instance USB_OTG_FS; hpcd.Init.dev_endpoints 6; hpcd.Init.speed PCD_SPEED_FULL; hpcd.Init.low_power_enable DISABLE; hpcd.Init.lpm_enable DISABLE; hpcd.Init.battery_charging_enable DISABLE; if (HAL_PCD_Init(hpcd) ! HAL_OK) { Error_Handler(); } HAL_PCDEx_SetRxFiFo(hpcd, 0x180); HAL_PCDEx_SetTxFiFo(hpcd, 0, 0x40); HAL_PCDEx_SetTxFiFo(hpcd, 1, 0x80); } void my_usb_extra_config(void) { // 某些情况下动态调整 FIFO HAL_PCDEx_SetRxFiFo(hpcd, 0x200); HAL_PCD_Start(hpcd); }问题就在这里MX_USB_PCD_Init首先设置了 RX FIFO 为 0x180 个字并配置了 EP0 的 TX FIFO 和 EP1 的 TX FIFO。随后my_usb_extra_config又把 RX FIFO 改成 0x200 个字然后立即启动设备。HAL_PCDEx_SetRxFiFo这个函数本身只修改 GRXFSIZ 寄存器并不会主动去重新计算和更新已经分配好的 TX FIFO 偏移。所以第二次修改后设备处于一种“RX FIFO 是新的、TX FIFO 偏移还是旧的”不一致状态。1.2 表面症状和实际故障点的差距当设备不枚举时第一反应通常是查硬件连接。我也一样花了半天时间检查 D 上拉、USB 线缆、供电确定没问题后才回到代码。通过调试器观察发现设备的中断已经触发了主机发来的 Setup 包也被接收了但是当软件调用HAL_PCD_EP_Transmit准备把设备描述符通过 EP0 IN 发给主机时传输一直没有完成。这里有一个很关键的细节EP0 IN 传输在 HAL 里的完成标志不是发完就立刻置位的而是要等主机发送 IN 令牌硬件把 FIFO 中的数据搬上总线然后触发 XFRC 中断。如果 FIFO 地址配置有问题硬件可能无法正确读取数据或者把数据写到了错误的位置XFRC 永远不会触发软件就会一直卡在等待状态。USB 协议里主机对 SET_ADDRESS 请求后的第一个状态包非常敏感。设备没有在地址 0 上正确回复 GET_DESCRIPTOR主机就会认为设备不存在于是反复发送请求直到超时放弃。所以表面现象是“设备不枚举”实际故障点则是“EP0 IN 端点没有完成数据搬运”。2. FIFO RAM 布局为什么 RX FIFO 是控制传输的“地基”要彻底理解这个坑需要先搞清楚 STM32 U5 的 USB OTG FS 内部那一片 FIFO RAM 是怎么划分的。硬件上USB OTG 外设内部有一块独立的 RAM专门用于端点的 FIFO不占用主存。在设备模式下这块 RAM 被划分为一个 RX FIFO 和若干个 TX FIFO。RX FIFO 是所有 OUT 方向事务的公共缓冲区。无论是 EP0 OUT、EP1 OUT 还是 SETUP 包硬件都会先把数据放到 RX FIFO 里然后触发对应的中断由软件把数据读走。TX FIFO 则用于 IN 方向传输一般情况下每个 IN 端点独立占用一段区域比如 EP0 的 IN 用 TX0 FIFOEP1 的 IN 用 TX1 FIFO。和很多人直觉不同的是这些 FIFO 的地址并不是硬件自动分配的而是由软件在初始化时显式写到寄存器里。寄存器命名如下FIFO 角色控制寄存器大小字段起始地址字段RX FIFOGRXFSIZRXFD固定为 0TX FIFO 0EP0 INDIEPTXF0TX0FD由软件配置TX FIFO 1EP1 INDIEPTXF1TXFD由软件配置TX FIFO nDIEPTXFnTXFD由软件配置这里最容易被忽略的是RX FIFO 永远从 RAM 地址 0 开始而每一个 TX FIFO 的起始地址需要软件计算后写进对应的高 16 位字段。计算的基本规则是某个 TX FIFO 的起始地址 之前的 RX FIFO 大小 之前所有 TX FIFO 的大小之和。举个具体例子。假设你设置的 RX FIFO 大小为 0x180 个字TX0 大小为 0x40 个字TX1 大小为 0x80 个字那么布局就是RX FIFO 占 [0x000, 0x180)TX0 起始地址 0x180占 [0x180, 0x1C0)TX1 起始地址 0x1C0占 [0x1C0, 0x240)寄存器里的配置应该是GRXFSIZ 0x180; DIEPTXF0 (0x180 16) | 0x40; // 起始地址 0x180大小 0x40 DIEPTXF1 (0x1C0 16) | 0x80; // 起始地址 0x1C0大小 0x80注意这里的大小单位是 32 位字不是字节。如果某段时间你习惯用字节数直接填数据会偏移得很离谱。2.1 HAL 是如何计算 TX FIFO 起始地址的这正是整个问题的核心。很多人以为HAL_PCDEx_SetTxFiFo只是设置了一个“端点 FIFO 大小”然后硬件会自动分配地址其实 HAL 内部并没有这么智能。看 STM32U5 的 HAL 源码HAL_PCDEx_SetTxFiFo在配置非 0 号 TX FIFO 时会先读取当前的 GRXFSIZ 寄存器再加上前面已经配置好的 TX FIFO 大小最后把算出的偏移写到目标寄存器的起始地址字段。流程示意如下offset GRXFSIZ USB_OTG_GRXFSIZ_RXFD; // 获取当前 RX FIFO 大小 for (i 0; i fifo; i) { offset DIEPTXF[i] 的大小字段; // 累加前面各 TX FIFO } DIEPTXF[fifo] (offset 16) | size;所以每次配置 TX FIFO 时它使用的 RX FIFO 大小是“那一刻 GRXFSIZ 寄存器里的值”。如果后面你再修改 GRXFSIZHAL 不会回头去更新之前已经算好的 TX FIFO 起始地址。这就造成了我在第一节里看到的场景第一次配置时RX 0x180TX0、TX1 都按这个值计算。第二次把 RX 改成 0x200 后GRXFSIZ 变成 0x200但 DIEPTXF0 里的起始地址还是 0x180DIEPTXF1 里的起始地址还是 0x1C0。硬件在传输时按照寄存器里的地址去读写结果可想而知。2.2 为什么单独设置 RX FIFO 不像“改个参数”那么简单FIFO 配置属于互联型寄存器修改一个寄存器就可能牵动整条地址链。这就像你在一个连续的内存池里用指针分配了几块缓冲区假如你把第一块缓冲区的大小改大了后面所有缓冲区的起始地址都必须跟着推后否则就会出现重叠或空洞。在 OTG 硬件里EP0 IN 使用的 TX0 FIFO 和后续 IN 端点使用的 TXn FIFO都要遵循这条规则。软件如果在配置完 TX FIFO 之后再改 RX FIFO 大小相当于把地基挖深了却没有把上面的楼整体平移整个结构就乱了。有一种特殊情况值得说明如果第一次调用HAL_PCDEx_SetRxFiFo后还没有配置任何 TX FIFO此时你连续修改 RX FIFO 两次最终结果不会有问题。因为当前没有 TX FIFO 依赖旧的 RX 值只要在配置 TX FIFO 之前把 RX 定下来后续链条是对的。从这一点看“调用两次”这句话只是一个表象真正的危险是“在已经配置 TX FIFO 之后再次修改 RX FIFO”。3. 根因排查从症状定位到重复调用的完整链路这一节我想完整还原我当时是怎么一步一步定位到根因的不是为了重复结论而是因为类似的 FIFO 问题以后可能会以别的形式出现排查思路比结论更有价值。3.1 第一步确认是不是 EP0 IN 的问题设备不枚举先不要急着怀疑整条 USB 链路。我做的第一件事是打开 STM32CubeIDE 的调试器在USBD_LL_DataInStage这个回调里下了断点这个函数是 USB 设备库收到 IN 传输完成事件后进入的。如果这个断点从来没有被触发说明 EP0 IN 传输压根没完成。实际上我在第一次运行时就发现Setup 包进来过HAL_PCD_SetupStageCallback触发了但是 IN 完成回调始终进不来。这说明设备已经收到了“读取设备描述符”的请求也在尝试回复但回复数据没有成功发出去。到这里整个问题基本被锁定在 EP0 IN 的硬件数据通路上。3.2 第二步检查 FIFO 寄存器手动复原地址分配思路明确之后下一个动作就是看 FIFO 寄存器。在代码停在启动后某个断点时我读取了几个关键寄存器GRXFSIZ 0x00000200; DIEPTXF0 0x01800040; DIEPTXF1 0x01C00080;从数值看GRXFSIZ 是 0x200 个字DIEPTXF0 的起始地址是 0x180、大小是 0x40DIEPTXF1 的起始地址是 0x1C0、大小是 0x80。把地址展开就会发现矛盾按 GRXFSIZRX FIFO 应该占 [0x000, 0x200)按 DIEPTXF0 的起始地址TX0 应该从 0x180 开始这已经落入了 RX FIFO 的范围按 DIEPTXF1 的起始地址TX1 从 0x1C0 开始同样落在 RX FIFO 范围里也就是说TX0 和 TX1 的 FIFO 区域和 RX FIFO 完全重叠了。在这样的布局下硬件收到 IN 令牌后可能把要发送的数据写进与 RX FIFO 重叠的区域而软件接收 Setup 的 RX 路径又可能被这些 IN 数据覆盖。结果是双方都在互相污染EP0 IN 自然无法正常完成。其实如果不改大 RX FIFO而是把 RX FIFO 改小问题一样存在。因为 TX FIFO 的起始地址虽然不会立即重叠但地址链已经断开了后面如果再放新的 TX FIFO就可能和已有地址冲突或者白白浪费大量 RAM导致总 FIFO 空间不够用。3.3 第三步找到第二次调用断点定位调用栈为了让故障“现形”我在HAL_PCDEx_SetRxFiFo函数入口设置了一个条件断点条件就是当前 GRXFSIZ 的值和上一次设置的值不同。这样第一次调用不会停下第二次修改会立刻触发。实际运行时断点停在了我新加的my_usb_extra_config里。调用栈非常清楚先进入MX_USB_PCD_Init完成第一次 RX FIFO 设置和 TX FIFO 设置然后进入另一个文件的新增函数又一次调用了HAL_PCDEx_SetRxFiFo。到这里其实已经可以直接下结论了但我还是做了一次对照实验把第二次调用删掉重新编译烧录设备马上枚举成功。为了进一步证明是“TX FIFO 偏移没有同步更新”导致的问题我还在第二次调用后又强制重新配置了一遍 TX0 和 TX1设备也能正常工作。这个实验说明问题确实出在 FIFO 地址链的联动上而不是某个别的偶然原因。3.4 EP0_IN 为什么是第一个受害者很多 USB 应用场景里EP0 后面还有 EP1、EP2 等批量或中断端点如果 FIFO 布局出错为什么最先暴露的是 EP0 IN因为控制传输是 USB 枚举的起点。设备上电后主机做的第一件事就是通过端点 0 发送 SETUP 包获取设备描述符。也就是说SP 包的目标地址是端点 0设备回复也走端点 0 IN。如果 EP0 IN 的 FIFO 区域已经和 RX FIFO 重叠那么在枚举阶段就会立刻失败后面的 CDC、HID、MSC 这些应用端点甚至还没有机会被涉及。反过来假如你在某个应用里根本不依赖 EP0 的正常枚举而是用 DFU 模式或者特殊 bootloader 跳过枚举那么这个 FIFO 配置错误可能会潜伏很久直到某个 IN 端点开始传输数据才暴露。无论如何EP0 IN 是系统最早、也是最能暴露 FIFO 布局问题的“哨兵”。4. 分析验证最小复现实验和边界情况定位到根因后我又做了一组小实验把“什么时候会出错、什么时候不会出错”彻底摸清楚。这样以后遇到类似问题可以直接根据配置顺序判断风险不需要每次都从头抓寄存器。4.1 最小复现代码我建了一个最小工程代码控制在 30 行以内专门复现这个问题void test_fifo_reproduce(void) { // 场景 A先设 RX再设 TX再改 RX —— 出问题 HAL_PCDEx_SetRxFiFo(hpcd, 0x100); HAL_PCDEx_SetTxFiFo(hpcd, 0, 0x40); HAL_PCDEx_SetTxFiFo(hpcd, 1, 0x80); HAL_PCDEx_SetRxFiFo(hpcd, 0x200); // 改了大 RX // 场景 B先设 RX再设 RX再设 TX —— 没问题 // HAL_PCDEx_SetRxFiFo(hpcd, 0x100); // HAL_PCDEx_SetRxFiFo(hpcd, 0x200); // HAL_PCDEx_SetTxFiFo(hpcd, 0, 0x40); // HAL_PCDEx_SetTxFiFo(hpcd, 1, 0x80); }在实际板子上场景 A 表现稳定异常场景 B 表现正常。这个对比实验的结果和 HAL 内部的地址计算逻辑完全吻合TX FIFO 的偏移只取决于其配置那一刻的 RX FIFO 大小不取决于最终值。4.2 验证重叠