STM32H743 的 USB 内部 DMA 能否正常工作,至少取决于控制器与硅片版本、DMA 对内存的可达性、缓冲区对齐、缓存一致性和 USB 中间件配置。启用 DMA 后无法枚举,是排查入口,不能仅凭这个现象判定一定是 D-Cache。
本文复核原笔记中的解释和代码,并按 ST 文档整理排查方法。没有提供原工程、链接映射、抓包和目标板测试结果,因此下面是核验指南,不是对某块板卡已经完成的故障归因。
1. 分开理解控制器、PHY、速度与 DMA
跳转到“1. 分开理解控制器、PHY、速度与 DMA”STM32H743 参考手册 RM0433 将两个 USB 实例描述为 OTG_HS1 和 OTG_HS2;HAL/Cube 工程中的实例名字可能使用 USB_OTG_HS、USB_OTG_FS。其中 HS1 可通过外部 ULPI PHY 实现高速,HS2 的引脚接口只能实现全速。运行速度为 FS 不等于没有内部 DMA。
同时,普通 DMA 传输与 scatter/gather 功能不是同一个能力。RM0433 的实现表还按实例和硅片修订区分 scatter/gather,不能把某个修订版的功能套到全部 H7 型号。设计前应核对具体料号、修订号、RM0433 及勘误。参见 ST RM0433。
2. 第一个检查:DMA 能否访问这块内存
跳转到“2. 第一个检查:DMA 能否访问这块内存”CPU 能读取某个地址,不代表 USB 内部 DMA 能读取它。ST 的 USB DMA 指南指出,H743 所属架构中的 USB DMA 无法访问 DTCM;如果缓冲区被链接到 0x20000000 附近的 DTCM,只关闭缓存也不会增加 DMA 到该内存的通路。
先在 .map 文件和调试器中检查所有实际传给 USB DMA 的地址,包括数据缓冲、EP0 控制传输相关缓冲以及中间件临时对象。不要只移动 UserRxBufferFS、UserTxBufferFS,就假定整条枚举路径都已迁移。见 ST USB DMA 配置指南。
原文的 MPU 代码从 0x24000000 起设置 512 KB 非缓存区,只会改变该地址范围的 CPU 访问属性,不会把位于 DTCM 的变量搬过去。对象放在哪里由链接脚本、段属性、分配器或栈位置决定;MPU 和链接布局必须一起检查。
3. 第二个检查:CPU 与 DMA 是否看到同一份数据
跳转到“3. 第二个检查:CPU 与 DMA 是否看到同一份数据”H743 的 L1 D-Cache 是 16 KB,I-Cache 也是 16 KB;原文把两者相加后的 32 KB 当成了 D-Cache 大小。参见 STM32H743 产品规格。
缓存行大小是另一个概念:Cortex-M7 缓存行是 32 字节。原代码 aligned(32) 表示 32 字节对齐,不能称为“32 位对齐”;32 位仅为 4 字节。见 ST AN4839 缓存说明。
两种典型的不一致如下:
发送:CPU 写入缓存 → SRAM 仍是旧值 → USB DMA 读出旧数据接收:USB DMA 写 SRAM → CPU 缓存仍是旧值 → CPU 读出旧数据clean 把脏缓存行写回内存;invalidate 使缓存行失效,下次读取时重新取内存。接收完成后如果错误地 clean 一份过期的脏缓存,反而可能覆盖 DMA 刚写入的数据。
4. 三种处理路线及适用边界
跳转到“4. 三种处理路线及适用边界”| 路线 | 要解决的问题 | 仍然需要检查 |
|---|---|---|
| 关闭 USB 内部 DMA | 用 CPU/FIFO 路径作为调试基线或最终实现 | 吞吐、CPU 占用、最坏延迟是否满足实际需求 |
| 为 DMA 保留非缓存区 | 避免该区 CPU 数据缓存一致性维护 | 内存可达、链接布局、MPU 覆盖范围、所有 DMA 对象 |
| 缓存区内显式维护 | 保留其他数据的缓存收益 | 缓存行独占、清理时序、所有权交接、长度与对齐 |
原文直接断言“CDC 无 DMA 性能完全够用”,没有采样率、数据量或测试依据。CDC 是设备类,不是吞吐量需求;应测量目标负载后选择。
路线 A:非缓存区
跳转到“路线 A:非缓存区”在可被 USB DMA 访问的 SRAM 中划出一块专用区,通过链接段放入 DMA 对象,再用 MPU 将同一区域设为合适的非缓存普通内存。MPU 区域大小和基地址必须满足硬件对齐要求,且不能与其他区域、堆、栈重叠。
原草案将整个 AXI SRAM 512 KB 配为非缓存,可用于有明确布局的对照实验,但会影响该区全部对象的性能,不宜未经检查直接作为通用推荐配置。设置 DisableExec 时也应依据用途;单纯数据缓冲区不需要允许执行。
MPU 应在规划好的启动阶段配置;若修改已有缓存区属性,需要先按平台要求停止访问并处理缓存。只有调用 HAL_MPU_ConfigRegion() 的片段,没有启用顺序、链接段和区域冲突说明,不是完整方案。
路线 B:缓存维护与所有权
跳转到“路线 B:缓存维护与所有权”把发送和接收看作 CPU 与 DMA 之间的所有权转移:
发送:CPU 填数据 → clean → 屏障 → DMA 读取 → 完成 → CPU 才能改写接收:CPU 准备独占缓冲 → 清理/失效 → 屏障 → DMA 写入 → 完成 → invalidate → CPU 读取 → 再次交给 DMA缓存维护必须覆盖完整缓存行,但不能误伤邻近对象。因此接收缓冲区的起点和分配大小都按 32 字节规划,并让其独占所涉及的缓存行;不能只把任意地址向下取整,再对包含无关脏数据的区域执行 invalidate。
下面只是缓存维护层的示意,需在实际 USB 中间件的交接位置接入。.usb_dma 必须由链接脚本放到 USB DMA 可达且允许缓存的 SRAM;如果放在非缓存区,则不应再把这组操作当成必要条件。
#include <stdint.h>#include "stm32h7xx.h"
enum { USB_BUFFER_BYTES = 128 }; /* 分配大小为 32 的整数倍 */__attribute__((section(".usb_dma"), aligned(32)))static uint8_t usb_tx[USB_BUFFER_BYTES];__attribute__((section(".usb_dma"), aligned(32)))static uint8_t usb_rx[USB_BUFFER_BYTES];
/* 调用前由 CPU 独占;之后交给 DMA,直到完成前不能修改。 */static void prepare_tx(void) { SCB_CleanDCache_by_Addr((uint32_t *)usb_tx, sizeof usb_tx); __DSB();}
/* 调用前确保前一次接收已完成,CPU 不再使用该缓冲。 */static void prepare_rx(void) { SCB_CleanInvalidateDCache_by_Addr((uint32_t *)usb_rx, sizeof usb_rx); __DSB();}
/* 仅在 DMA 已完成且停止写入后调用,随后 CPU 才能解析有效字节。 */static void finish_rx(void) { SCB_InvalidateDCache_by_Addr((uint32_t *)usb_rx, sizeof usb_rx); __DSB();}此片段没有启动 USB 传输,也没有证明这些两个数组覆盖了全部枚举缓冲。CMSIS 函数签名、USB 接收长度和端点最大包长应按工程版本检查;有的接收路径会按包长准备接收空间,实际分配不能只按应用想要的有效字节数估算。
5. 按证据定位枚举失败
跳转到“5. 按证据定位枚举失败”- 记录完整料号、芯片修订、Cube/HAL/CMSIS/USB 中间件版本及链接脚本。
- 建立不启用 DMA 的对照,记录主机枚举错误、USB 中断和返回状态。
- 启用 DMA 后先核对缓冲实际地址、硬件所需对齐和内存可达性。
- 再比较非缓存区或正确缓存维护的行为,避免同时修改多个无关设置。
- 检查 EP0 的 SETUP、描述符及短包路径,不只验证后续 CDC 数据传输。
- 验证持续传输、取消、复位、拔插、边界长度与高负载,测量吞吐和 CPU 负载。
原文的“关闭 DMA 正常/开启 DMA 必失败/非缓存必正常”表已改为实验变量。是否正常必须来自对应工程的测试记录,不能从开关组合直接推导。