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)
  • 地址在 0x240000000x30000000 段 → DMA 应该可以正常工作

经验总结

排查思路流程图

DMA 不工作
    ↓
检查 DMA 配置 → 正确
    ↓
检查中断使能 → 正确
    ↓
测试字符串常量 → ✅ 工作正常
    ↓
测试全局数组 → ❌ 不工作
    ↓
查看 .map 文件
    ↓
发现地址在 0x20000000 (DTCM)
    ↓
使用 __attribute__((at())) 分配到 0x24000000 (AXI SRAM)
    ↓
✅ DMA 正常工作

核心要点

芯片系列 DMA 访问 DTCM 是否需要特殊处理
STM32F1 ✅ 可以
STM32F4 ✅ 可以
STM32H7 不可以 是,必须指定 DMA 缓冲区到 AXI SRAM 或 SRAM1/2/3

关键调试经验

  1. 字符串常量测试法:如果 HAL_UART_Transmit_DMA() 发送字符串常量正常,发送自己的缓冲区失败 → 99% 是内存地址问题
  2. .map 文件是调试利器:遇到内存相关问题时,第一时间查看 .map 文件确认变量地址
  3. H7 系列要特别注意内存分配:不再是"随便定义个数组就能用 DMA"的时代了
  4. 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 内存问题的最快捷手段!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐