STM32H7 DMA发送失败排查指南
STM32H7 DMA 发送失败问题排查与解决
问题描述
在使用 STM32H7 进行 UART DMA 发送时,发现 HAL_UART_Transmit_DMA() 调用后串口没有任何数据发出,也不进入 DMA 传输完成中断。但使用 HAL_UART_Transmit_IT()(中断模式)可以正常工作。
(过程中一度认为是不是RTT把我的程序给干死了,结果来看担心是多余的)
环境信息
- 芯片:STM32H7 系列
- 外设:USART2 (RF433 模块通信)
- DMA:DMA1_Stream1 (USART2_TX)
- 编译器:ARMCC (Keil MDK)
排查过程
第一步:确认 DMA 配置是否正确
首先检查 DMA 初始化配置:
- DMA 通道配置为
DMA_MEMORY_TO_PERIPH - 数据宽度设置为
BYTE - DMA 中断已使能
检查结果:DMA 配置正确,中断已使能。
第二步:确认 DMA 中断是否使能
检查 DMA 中断服务函数:
void DMA1_Stream1_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_usart2_tx);
}
检查结果:中断函数已实现并正确调用。
第三步:关键发现 —— 简单字符串可以正常发送 🔑
这是整个排查过程的转折点:
// ✅ 这样是可以正常工作的!
HAL_UART_Transmit_DMA(&huart2, (uint8_t *)"Hello, World!\r\n", 15);
这个测试证明了什么?
- DMA 硬件配置是正确的
- UART 外设配置是正确的
- 中断配置是正确的
- 问题出在我的数据缓冲区本身
既然硬件事先配置没有问题,那就只剩一个可能:内存地址问题。
第四步:对比测试
// 直接传入字符串常量(工作正常)
HAL_UART_Transmit_DMA(&huart2, (uint8_t *)"Hello, World!\r\n", 15);
// 传入全局数组(不工作)
HAL_UART_Transmit_DMA(&huart2, send_rf_buff, 96);
为什么字符串常量可以,全局数组不行?
字符串常量存储在 Flash(0x08000000 段),DMA 通过 AHB 总线可以访问 Flash。
而全局数组默认分配在 DTCM(0x20000000 段),DMA1/DMA2 无法访问 DTCM。
第五步:查看内存分配(最终定位!)
通过查看编译生成的 .map 文件,发现 DMA 缓冲区的地址分配:
send_rf_buff 0x20004eb4 Data 96 tally433.o(.bss)
问题确认:缓冲区被分配到了 0x20000000 地址段(DTCM),DMA 无法访问。
根本原因分析
STM32H7 的内存架构特殊性
STM32H7 与 F1/F4 系列不同,它拥有异构内存架构,包含多个独立的 RAM 区域:
| 内存区域 | 起始地址 | 大小 | DMA1/DMA2 可访问 |
|---|---|---|---|
| DTCM (Data TCM) | 0x20000000 |
128KB | ❌ 不可访问 |
| ITCM (Instruction TCM) | 0x00000000 |
64KB | ❌ 不可访问 |
| AXI SRAM | 0x24000000 |
512KB | ✅ 可访问 |
| SRAM1 | 0x30000000 |
128KB | ✅ 可访问 |
| SRAM2 | 0x30020000 |
128KB | ✅ 可访问 |
| SRAM3 | 0x30040000 |
32KB | ✅ 可访问 |
为什么 F1/F4 没这个问题?
| 芯片系列 | 内存架构 | DMA 访问限制 |
|---|---|---|
| STM32F1 | 统一 SRAM | DMA 可访问全部 SRAM |
| STM32F4 | 统一 SRAM | DMA 可访问全部 SRAM(112KB/128KB) |
| STM32H7 | 异构内存 | DMA1/DMA2 无法访问 DTCM |
F1/F4 的 SRAM 是连续统一的,所有 DMA 都能访问。而 H7 为了追求性能,引入了 TCM(Tightly Coupled Memory)作为 CPU 的"专属高速缓存",但它不在 DMA 的总线矩阵上。
为什么中断模式可以工作?
HAL_UART_Transmit_IT() 使用 CPU 直接搬运数据(从内存读到 CPU 寄存器,再写入外设),不涉及 DMA 访问内存,所以不受 DTCM 限制。
为什么字符串常量可以工作?
字符串常量存储在 Flash 中,DMA 可以通过 AHB 总线访问 Flash,所以可以正常工作。
解决方案
方案一:使用 __attribute__((at())) 指定地址(推荐)
参考以太网 DMA 描述符的写法,强制将缓冲区分配到 DMA 可访问的区域:
// AXI SRAM (0x24000000) - 512KB 空间充裕
#define DMA_BUFFER_BASE 0x24000000
__attribute__((at(DMA_BUFFER_BASE))) uint8_t send_rf_buff[96];
__attribute__((at(DMA_BUFFER_BASE + 96))) uint8_t rf_buff[4];
方案二:修改 Scatter 文件
自行百度我没用
注意事项
1. 地址冲突检查
编译后务必查看 .map 文件,确认分配的地址:
# 在 .map 文件中搜索缓冲区地址
send_rf_buff 0x24000000 Data 96 tally433.o(.ARM.__AT_0x24000000)
如果遇到地址重叠错误:
Error: L6982E: AT section xxx overlaps address range with xxx
说明地址已被其他变量占用,需要更换地址。
2. 以太网描述符占用
如果使用 SRAM3 (0x30040000),注意以太网 DMA 描述符可能已经占用该区域:
// 以太网描述符通常占用 0x30040000 - 0x300407FF
__attribute__((at(0x30040180))) ETH_DMADescTypeDef DMARxDscrTab[...];
__attribute__((at(0x30040000))) ETH_DMADescTypeDef DMATxDscrTab[...];
建议优先使用 AXI SRAM (0x24000000),空间充裕,不易冲突。
3. D-Cache 缓存一致性(重要!)
使用 AXI SRAM (0x24000000) 时,该区域默认开启了 D-Cache,可能导致 DMA 与 CPU 数据不一致:
// 发送前:将 CPU Cache 中的数据刷新到内存
SCB_CleanDCache_by_Addr((uint32_t*)send_rf_buff, sizeof(send_rf_buff));
// 启动 DMA 传输
HAL_UART_Transmit_DMA(RFUART, send_rf_buff, 96);
如果数据异常,可以通过 MPU 将该区域配置为 Non-Cacheable:
(我直接屏蔽了SCB_EnableDCache()😉)
4. 快速诊断方法
如果 DMA 不工作,添加以下代码快速定位:
// 打印缓冲区地址
printf("Buffer address: 0x%08X\r\n", (uint32_t)send_rf_buff);
// 如果是字符串常量,打印其地址
printf("String address: 0x%08X\r\n", (uint32_t)"Hello");
// 用 LED 或串口输出确认 DMA 状态
rt_kprintf("DMA State: %d, Error: %d\r\n",
RFUART->gState, RFUART->ErrorCode);
判断标准:
- 地址在
0x20000000段 → 问题就在这里(DTCM) - 地址在
0x24000000或0x30000000段 → DMA 应该可以正常工作
经验总结
排查思路流程图
DMA 不工作
↓
检查 DMA 配置 → 正确
↓
检查中断使能 → 正确
↓
测试字符串常量 → ✅ 工作正常
↓
测试全局数组 → ❌ 不工作
↓
查看 .map 文件
↓
发现地址在 0x20000000 (DTCM)
↓
使用 __attribute__((at())) 分配到 0x24000000 (AXI SRAM)
↓
✅ DMA 正常工作
核心要点
| 芯片系列 | DMA 访问 DTCM | 是否需要特殊处理 |
|---|---|---|
| STM32F1 | ✅ 可以 | 否 |
| STM32F4 | ✅ 可以 | 否 |
| STM32H7 | ❌ 不可以 | 是,必须指定 DMA 缓冲区到 AXI SRAM 或 SRAM1/2/3 |
关键调试经验
- 字符串常量测试法:如果
HAL_UART_Transmit_DMA()发送字符串常量正常,发送自己的缓冲区失败 → 99% 是内存地址问题 - .map 文件是调试利器:遇到内存相关问题时,第一时间查看
.map文件确认变量地址 - H7 系列要特别注意内存分配:不再是"随便定义个数组就能用 DMA"的时代了
- AT 属性是简单有效的解决方案:不需要修改链接脚本,直接指定地址即可
完整示例代码
static uint8_t Rf_buf[4] = {0xF0, 0x86, 0xA1, 0x16}; // 4 bytes buffer for RF commands
static uint8_t RfBuf[96];
static uint8_t *Rf_buf_p = RfBuf;
// ============================================================================
// RF 模块 DMA 发送缓冲区
// ============================================================================
// 问题背景:
// STM32H7 的 DMA1/DMA2 控制器无法访问 DTCM (Data Tightly Coupled Memory)
// 区域 (地址 0x20000000 - 0x2001FFFF)。如果 DMA 缓冲区默认被编译器分配
// 到 DTCM,将导致 DMA 传输失败,并且不会触发中断。
//
// 解决方案:
// 使用 __attribute__((at())) 将 DMA 缓冲区强制分配到 AXI SRAM 区域
// (地址 0x24000000 - 0x2407FFFF),该区域可被 DMA1/DMA2 正常访问。
//
// 注意事项:
// 1. 地址范围 0x30040000 - 0x300407FF 已被以太网 DMA 描述符占用,
// 故此处使用 AXI SRAM (0x24000000) 避免地址冲突。
// 2. 若使用 AXI SRAM,需注意 D-Cache 缓存一致性问题:
// - 在 DMA 传输前调用 SCB_CleanDCache_by_Addr() 确保数据已刷新到内存
// - 在 DMA 接收后调用 SCB_InvalidateDCache_by_Addr() 使 Cache 失效
// * - 或通过 MPU 将该区域配置为 Non-Cacheable
// 3. AXI SRAM 起始地址为 0x24000000,大小为 512KB (0x80000)
// ============================================================================
#define DMA_BUFFER_BASE 0x24000000
__attribute__((at(DMA_BUFFER_BASE))) uint8_t send_rf_buff[96];
static volatile uint8_t rfTxEnd = 1;
#define RF_FULL_REFRESH_INTERVAL 400 // 全刷周期
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == RFUART)
{
rfTxEnd = 1;
HAL_GPIO_WritePin(RUN_LED_GPIO_Port, RUN_LED_Pin, GPIO_PIN_RESET);
}
}
static void rf_tally_one(uint8_t ch, rf_sw sw, rf_led led)
{
HAL_StatusTypeDef status;
Rf_buf[0] = 0xF0 | ch;
Rf_buf[1] = sw;
Rf_buf[2] = led;
Rf_buf[3] = 0x16;
// status = HAL_UART_Transmit(RFUART, Rf_buf, 4, 0xFF);
// if (status != HAL_OK)
// {
// rt_kprintf("status=%d\r\n", status);
// }
for (size_t i = 0; i < 4; i++)
{
*Rf_buf_p = Rf_buf[i];
Rf_buf_p++;
}
// if ((Rf_buf_p - RfBuf) == (MAX_RF_NUM - 1) * 4 * 2)
if ((Rf_buf_p - RfBuf) == 96)
{
if (rfTxEnd)
{
rfTxEnd = 0;
memcpy(send_rf_buff, RfBuf, 96);
// HAL_GPIO_WritePin(RUN_LED_GPIO_Port, RUN_LED_Pin, GPIO_PIN_SET);
// status = HAL_UART_Transmit_DMA(RFUART, RfBuf, (MAX_RF_NUM - 1) * 4 * 2);
status = HAL_UART_Transmit_DMA(RFUART, send_rf_buff, 96);
// status = HAL_UART_Transmit_IT(RFUART, RfBuf, 96);
// status = HAL_UART_Transmit(RFUART, RfBuf, 96, 0xff);
if (status != HAL_OK)
{
rt_kprintf("HAL_UART_Transmit_DMA fail status=%d\r\n", status);
}
else
{
// rt_kprintf("send rf\r\n");
}
}
Rf_buf_p = RfBuf;
}
}
本文档基于 STM32H7 实际项目问题排查经验整理,希望对遇到类似问题的开发者有所帮助。
关键发现:字符串常量测试法是定位 DMA 内存问题的最快捷手段!
更多推荐


所有评论(0)