STM32H7 SAI+HPDMA传输无法访问DTCM的排查与替代方案

STM32H7 SAI+HPDMA传输无法访问DTCM的排查与替代方案 做 STM32H7 音频采集的时候我碰到过一个很典型的问题SAI 接收音频数据想用 HPDMA 直接从外设搬到 DTCM结果 DMA 传输死活不生效寄存器配置看着全对中断也开了但数据就是不动。后来查了很久才发现问题根本不是出在 SAI 或 HPDMA 的配置上而是我对 H7 的内存架构有一个默认假设是错的DTCM 根本没有挂到 DMA 能看到的总线上。这篇把整个排查过程和能落地的替代方案整理出来给正准备在 H7 上做音频采集、语音处理或者其他低延迟数据搬运的朋友做个参考。这里涉及的三个关键词SAI、DTCM、HPDMA其实是三套完全不同的东西。SAI 是串行音频接口负责把 I2S/TDM 等格式的音频数据收进来DTCM 是 Cortex-M7 内核紧耦合的快速数据内存HPDMA 则是 H7 新一代的高性能 DMA 控制器。把它们串在一起之前必须先搞清楚它们在芯片内部到底各自挂在什么总线上否则就会出现这个“看起来配置没问题实际跑不通”的经典故障。1. 问题现象与背后的真实需求1.1 先看报错现场先说现象。我当时的工程是基于 STM32H723音频接口是 SAI1 Block A配置成 Master RX从外部 Codec 以 48kHz、16bit 立体声采样。需求比较直接SAI 收到数据后由 HPDMA 把数据从 SAI 的数据寄存器搬到内存里内存我直接选在了 DTCM 区域也就是 0x20000000 附近想着这样后续做音频处理时 CPU 访问延迟最低。代码用的是 STM32CubeH7 的 HAL 库流程大致是这样的// SAI 句柄配置省略主要看 DMA 部分 hpdma_channel_conf.Request HPDMA_REQUEST_SAI1_A; hpdma_channel_conf.Direction HPDMA_PERIPH_TO_MEMORY; hpdma_channel_conf.PeriphInc HPDMA_PINC_DISABLE; hpdma_channel_conf.MemInc HPDMA_MINC_ENABLE; hpdma_channel_conf.DataAlignment HPDMA_DATAALIGN_PACK_ENABLE; HAL_DMA_Init(hdma_sai1_a); __HAL_LINKDMA(hsai1, hdmarx, hdma_sai1_a); HAL_SAI_Receive_DMA(hsai1, (uint8_t *)dtcm_audio_buf, AUDIO_BUF_SIZE);结果就是HAL_SAI_Receive_DMA调用之后函数返回HAL_OK但是之后 DMA 通道状态一直停在 busy没有传输完成中断也没有错误中断DTCM 目标地址里的数据永远是 0。用调试器看 SAI 的接收 FIFO能发现 FIFO 里其实是有数据的说明 SAI 工作正常问题就卡在 DMA 这一环。1.2 为什么音频数据偏偏想进 DTCM有人可能会问为什么不直接把 DMA 缓冲区放在普通的 AXI SRAM 或 D2 SRAM 里非要往 DTCM 塞原因其实很现实。DTCM全称 Data Tightly Coupled Memory是 Cortex-M7 内核的紧耦合数据内存。它挂在 CPU 自己的 TCM 接口上CPU 访问它只需要一个周期延迟极低而且它没有 Cache不涉及到 Cache 一致性问题。对音频处理这种数据量大、延迟敏感、还要做大量计算滤波、FFT、编解码的场景把数据放在 DTCM 里CPU 处理起来会舒服很多。我当时的主观想法是反正音频数据最终是要给 CPU 算的不如直接从 SAI 用 DMA 搬进 DTCM省掉中间一次拷贝。这个想法本身没错但忽略了一个最关键的前提DMA 到底能不能碰 DTCM。很多人第一次用 H7 的时候都会把它当成一个加强版的 F4 来用这是个非常大的误区。M4 内核没有 TCM 接口所有 SRAM 都挂在系统总线上DMA 随便都能访问。但到了 Cortex-M7 的 H7内存架构彻底变了TCM 区域根本不在 DMA 的视野里。这个问题一旦没搞清楚掉坑是必然的。2. 要搞清楚 SAI、HPDMA、DTCM 这三者谁才是“邻居”2.1 SAI 与 HPDMA 的搭配逻辑先看 SAI 与 HPDMA 这一侧。SAI 外设有独立的发送和接收 FIFO数据寄存器宽度是 32 位。当 FIFO 里的数据量超过某个阈值时SAI 会产生一个 DMA 请求DMA 控制器收到请求后把数据从 SAI 的数据寄存器读走写入内存。这是典型的外设到内存搬运本身没什么特别的。HPDMA 是 STM32H7 引入的新一代 DMA相比传统 DMA1/DMA2 最大的优势是支持 linked-list 链表模式可以挂多块传输描述符也支持突发传输、可配置的数据对齐pack/unpack、事件触发等高级功能。在音频应用里如果使用链表模式可以连续配置多个音频缓冲块传输完一块后自动跳到下一块不需要 CPU 反复重新启动 DMA这是它非常实用的地方。但注意HPDMA 能力再强它也是一个总线主设备它发出的读写请求必须通过芯片内部的互联矩阵才能到达目标内存和外设。它的主接口挂载在 AXI/AHB 互联矩阵上所以它只能访问互联矩阵上的外围资源。这个“总线主设备”的身份限制决定了它根本看不到 TCM 接口上的东西。2.2 DTCM 在总线里的真实位置再看 DTCM 在系统架构中的位置。Cortex-M7 内核本身有多个总线接口其中两个专门用于访问 TCMITCM 接口连接指令紧耦合内存地址范围通常在 0x00000000 附近DTCM 接口连接数据紧耦合内存地址范围通常在 0x20000000 附近。这两个接口不是 AXI/AHB 总线接口它们是 CPU 核心私有的内存接口不对芯片上的其他总线主设备开放。换句话说DTCM 更像是 CPU 核心的“私有仓库”只有 CPU 自己能直接读写系统里的其他主设备包括 HPDMA、MDMA、Ethernet、USB、LTDC 等全部看不到它。从系统框图的角度理解大概是这样CPU 核心通过 TCM 接口访问 DTCM同时 CPU 也通过 AXI 主接口访问外部互联矩阵而 HPDMA 则是互联矩阵上的一个主设备。CPU 与 DTCM 是“直连邻居”HPDMA 与 DTCM 之间隔着整个互联矩阵还找不到入口。这里还需要澄清一个常见误区调试器能看到 DTCM 的数据比如在 Keil/IAR/ST-LINK 的 Memory 窗口能读出 DTCM 内容会让人误以为 DMA 也能访问它。实际上调试器是通过调试接口DAP直接访问内核空间的它的路径和 DMA 完全不一样不能作为 DMA 可访问性的判断依据。2.3 为什么 DMA 就是够不到 DTCM为了彻底说清楚我列了一张访问能力对照表。这表里的结论基于 H7 系列的内核架构非常关键访问方能否访问 DTCM说明CPU 核心可以通过私有 TCM 接口单周期访问调试器DAP/ST-LINK可以通过调试接口访问内核空间HPDMA / MDMA / DMA1/DMA2不可以都是互联矩阵上的主设备TCM 不在其地址空间内以太网 MAC、USB、LTDC 等不可以同为总线主设备只看到互联矩阵上的地址当 HPDMA 发起对 0x20000000 地址的写请求时互联矩阵在地址译码阶段就发现这个地址不属于自己管辖的内存或外设范围于是事务无法完成。在很多系统里这种情况会直接挂起总线DMA 一致处于等待状态不产生完成事件也不产生错误事件。所以在调试时DMA 的状态寄存器会显示通道处于 busy但实际上它一直在等一个永远不会来的响应。这种“总线无响应”的问题比明显的配置错误更难排查因为它不会报任何寄存器错误位。我当时就是在 HAL_DMA_IRQHandler 里加了断点发现中断根本进不去才意识到问题可能出在地址可达性上。3. 三种可落地的替代方案既然 HPDMA 不能直接访问 DTCM那是不是就没办法了当然不是。实际项目里有三种比较成熟的绕行方案关键看你的应用场景对实时性和 CPU 负载的要求。3.1 方案A把DMA缓冲放在普通SRAM最推荐最直接的做法就是放弃把 DMA 直接打到 DTCM把 DMA 缓冲区放到 HPDMA 正常可访问的内存区域里。我优先推荐 D2 域的 SRAM1/2/30x30000000 附近。原因有两个第一它挂在 AHB 总线上HPDMA 访问完全没有问题第二这段区域默认不经过 D-Cache不存在数据一致性的坑。相比之下D1 域的 AXI SRAM0x24000000 附近虽然 DMA 也能访问但如果你开启了 D-Cache 且 MPU 把它配成了 Write-Back 模式DMA 写入的数据不会直接更新缓存CPU 读到的可能是旧数据。处理 Cache 一致性问题虽然不难但没必要给自己找麻烦。做法就是定义一个普通 SRAM 段里的缓冲区。在 CubeMX 工程里我通常是把 DMA 缓冲区放在 D2 SRAM 域里用 GCC 时通过 section 属性指定配合链接脚本里的对应段__attribute__((section(.sram1))) int32_t sai_audio_buf[AUDIO_BUF_SIZE / 4];然后在 CubeMX 里正常把 HPDMA 配置成 SAI1_A_RX 的请求源启动传输HAL_SAI_Receive_DMA(hsai1, (uint8_t *)sai_audio_buf, AUDIO_BUF_SIZE);这样 DMA 就能正常工作了数据会稳定落在 SRAM 里。后续音频处理时如果不想多一次拷贝直接在 SRAM 里处理就可以如果确实需要 DTCM 的高速计算环境就配合方案C做一次搬运。3.2 方案B关掉DMACPU直接读SAI FIFO如果你的音频采样率不高、通道数不多或者数据量可控其实可以完全绕过 DMA让 CPU 直接在中断里读 SAI 的接收 FIFO然后写到 DTCM。因为 CPU 是唯一能直接访问 DTCM 的设备这个方法天然不存在总线可达性问题。具体做法是SAI 配置接收中断当接收 FIFO 非空时触发中断在中断服务函数里读取 SAI 数据寄存器写入 DTCM 缓冲区void SAI1_IRQHandler(void) { /* 检查接收 FIFO 非空标志 */ if (__HAL_SAI_GET_FLAG(hsai1, SAI_FLAG_FIFO_NOT_EMPTY)) { dtcm_audio_buf[wr_idx] (int32_t)SAI1_Block_A_DR; wr_idx (wr_idx 1) % AUDIO_BUF_SIZE; } __HAL_SAI_CLEAR_FLAG(hsai1, SAI_FLAG_FIFO_NOT_EMPTY); }这种方式的优点是逻辑简单CPU 直接从外设把数据搬进 DTCM省掉了中间层。缺点也非常明显每收到一个字都会触发一次中断对 CPU 占用率影响很大。比如 48kHz 双声道 16bit每秒就有约 192 次中断每次中断还要处理保存/恢复上下文CPU 负载会明显变高。所以这个方案只适合低采样率、低通道数的简单应用或者是验证阶段快速跑通逻辑。3.3 方案CDMA打到普通SRAM再搬运到DTCM这算是我个人觉得最均衡的方案也是实际音频项目里用得最多的组合。思路是HPDMA 先把 SAI 数据搬到普通 SRAM 的缓冲区DMA 传输完成中断触发后CPU 再把这段数据批量拷贝到 DTCM。从表面上看这个方案比方案A多了一次内存拷贝比方案B多了一层 DMA 搬运。但它解决的痛点非常实际音频输出这种高频、大批量、持续不断的数据流不能靠中断一个一个读会让 CPU 忙死而 DTCM 对后续音频计算滤波、卷积、FFT带来的速度提升又值得多这一次块拷贝块拷贝可以用高效的 LDM/STM 指令或者 memcpy 的优化实现完成成本远低于中断逐字搬运。最关键的是DMA 完成中断的频率远低于 SAI FIFO 逐字中断的频率。比如 DMA 每次传输 256 字48kHz 下大概不到 3ms 才触发一次中断CPU 完全有时间在两次中断之间处理其他任务。void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef *hsai) { if (hsai-Instance SAI1_Block_A) { /* 先从 SRAM 缓冲区拷贝到 DTCM再做音频处理 */ memcpy(dtcm_audio_buf, sai_audio_buf, AUDIO_BUF_SIZE * sizeof(int32_t)); process_audio_data(dtcm_audio_buf, AUDIO_BUF_SIZE); } }这里要注意的是如果 DMA 缓冲区放在 D1 域的 AXI SRAM 且开启了 D-Cache在 memcpy 之前必须先做 Cache Invalidate 操作否则 CPU 从缓存里读到的可能是旧数据。如果放在 D2 域 SRAM就没有这个烦恼。4. 实操中的代码与配置细节4.1 CubeMX侧面的改动在 CubeMX 里SAI 和 HPDMA 的配置其实不需要太多特殊处理。主要注意以下几点SAI 配置为 Master RX 模式选择正确的时钟源比如外部 Codec 提供 MCLK 或使用 SAI 内部时钟分频。一般 CubeMX 会自动计算分频系数需要知道自己用的外部主时钟频率数据长度选择 16bit 或 32bit对应 SAI 的帧格式在 DMA Settings 标签页里添加一个 DMA request选择 SAI1_A_RXDirection 设为 Peripheral To Memory模式设为 Circular 或 Normal。音频流一般是 Continuous 的所以用 Circular 模式比较合适数据满了自动从缓冲区头部重新开始写DMA 的数据宽度建议设为 Word32位因为 SAI 数据寄存器本身是 32 位的虽然 16bit 音频数据只用了低 16 位但按字搬运效率更高生成代码后把 DMA 缓冲区的变量定义到普通 SRAM 段。CubeMX 默认把缓冲定义在uint8_t aAudioBuffer[]这种地方链接器会把它放到默认的 RAM 区域如果默认 RAM 是 DTCM那问题就还是存在。所以这一步一定要留意。我在 CubeMX 工程里最容易忽略的一个地方是时钟配置。H7 的 SAI 有时钟源选择不同的型号和封装SAI 外设时钟可能来自 PLL1Q、PLL2P 等。如果 SAI 时钟没配好SAI 根本不会产生数据DMA 就算正常也没数据可用。建议先不要纠结 DMA直接把 SAI 配成中断模式跑一个单次读取确认 FIFO 里有数据后再加 DMA。4.2 代码层面的关键实现在代码层面有几个关键实现细节值得单独拿出来说。第一个是内存段定义。GCC 环境下要在链接脚本里为 D2 SRAM 增加一个输出段。CubeMX 生成的.ld文件里内存区域定义大概长这样MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K DTCMRAM (xrw) : ORIGIN 0x20000000, LENGTH 128K SRAM (xrw) : ORIGIN 0x24000000, LENGTH 512K RAM_D2 (xrw) : ORIGIN 0x30000000, LENGTH 288K RAM_D3 (xrw) : ORIGIN 0x38000000, LENGTH 64K }如果你的工程里已有RAM_D2这种区域那可以直接用 section 定义缓冲区。如果没有需要手动加段并在 SECTIONS 里增加.sram1 : { *(.sram1) *(.sram1.*) } RAM_D2然后 C 代码里就能用了__attribute__((section(.sram1))) int32_t sai_audio_buf[AUDIO_BUF_SIZE / 4]; __attribute__((section(.dtcmram))) int32_t dtcm_audio_buf[AUDIO_BUF_SIZE / 4];.dtcmram段在链接脚本里通常已经存在因为 CubeMX 默认把部分数据放到 DTCM。但注意不要把所有变量都塞进 DTCM它的容量有限而且会被硬件栈等占用。第二个是启动传输的时机。在音频 Codec 上电、时钟稳定、SAI 初始化完成之后再启动 DMA。如果 SAI 已经收到数据但 DMA 还没启动FIFO 会溢出数据会丢而且首次启动可能出现错位。稳妥的方式是在 SAI 初始化完成后先让 Codec 处于静音状态或低增益状态然后启动 DMA稳定后再打开音频通路。第三个是关于 HPDMA 链表模式的使用建议。如果音频数据量特别大、需要多块缓冲交替使用HPDMA 的 linked-list 能力非常有用。但链表描述符本身也必须放在 HPDMA 可访问的内存中不能放 DTCM。这个是我踩过的另一个坑一开始把链表描述符数组放在了 DTCM结果 DMA 连描述符都读不到直接卡死。__attribute__((section(.sram1))) HPDMA_NodeTypeDef hpdma_node[2];4.3 关于Cache一致性与MPU的注意事项如果你选择把 DMA 缓冲区放在 D1 域的 AXI SRAM并且工程里启用了 D-Cache那必须处理一致性问题。否则 DMA 已经写入的数据CPU 在读的时候可能会命中缓存里未更新的旧数据表现出来就是“DMA 明明收到了数据但 CPU 看到的缓冲区一直是零”。正确做法是在 CPU 读取缓冲区之前先把对应地址范围的 D-Cache 无效化SCB_InvalidateDCache_by_Addr((uint32_t *)sai_audio_buf, AUDIO_BUF_SIZE * sizeof(int32_t));要说明的是这个操作是有成本的尤其音频数据频繁需要 Invalidate 时会消耗不少 CPU 周期。这正是我优先推荐 D2 域 SRAM 的原因D2 域在默认配置下不缓存不需要每次做一致性处理代码更简单也不容易出错。MPU 方面一般不用为了这个功能单独配置。不过如果你的工程里专门设置了 MPU 区域要特别注意不要误把 DMA 缓冲区所在的 SRAM 区域配成禁止访问或错误的缓存策略。比如把 D2 SRAM 配置成不可缓存、非共享是最省心的方式。另外注意不要尝试配置一个 MPU Region 去“允许 DMA 访问 DTCM”这是做不到的TCM 接口的访问权限不是由 MPU 控制的它连互联矩阵都上不去。5. 常见问题速查与排查思路5.1 排查表从卡死到数据错乱整理了一份常见现象速查表基本覆盖了 H7 上 SAI HPDMA DTCM 组合常见的各种问题现象可能原因解决办法DMA 启动后一直 busy无完成中断目标地址在 DTCMDMA 不可达把缓冲区改到 D2 SRAM 或 AXI SRAMDMA 完成中断偶尔触发但数据错乱Cache 一致性问题读缓冲前 Invalidate D-Cache或改用非缓存 SRAMDMA 完全无传输FIFO 有数据SAI 的 DMA 请求未映射或时钟没配好检查 DMA 请求源和 SAI 时钟配置第一包数据正常后面全错位Circular 模式启动时序问题先启动 DMA 再打开音频通路或调整 Codec 静音时序链表模式下 DMA 直接卡死链表描述符被放到了 DTCM描述符改放到普通 SRAMSAI FIFO overrun 中断频繁DMA 优先级太低或传输带宽不足提高 HPDMA 优先级、使用突发传输、减小中断负载5.2 几个让我花过冤枉时间的坑第一个坑是 HPDMA 的请求复用。H7 的 HPDMA 不像老系列那样把外设请求固定写在通道里它需要把外设的 DMA 请求信号映射到 HPDMA 通道上。CubeMX 一般会自动配置但如果你手动改过代码很容易漏掉这一步。结果是 SAI 的 FIFO 一直有数据但 HPDMA 根本收不到请求。第二个坑是调试时只看变量值不看内存地址。我刚开始查问题时在 IDE 的 Watch 窗口看目标缓冲区数组的值数组一直显示 0我以为是传输没开始。后来用 Memory 窗口直接看 DMA 缓冲区的实际地址才发现数据其实已经到了 SRAM只是我之前看的是另一个数组地址根本不对。第三个坑是 memcpy 从 AXI SRAM 搬到 DTCM 的时机。在 DMA 传输完成回调里如果 DMA 用的是 Circular 模式那完成回调触发时可能已经有新数据写到了缓冲区后半段如果直接整片拷贝会把新旧数据交错拷贝进去。这个场景要用双缓冲或者队列机制确保 CPU 拷贝的数据是稳定的一帧完整数据。5.3 快速验证内存可达性的小工具最后分享一个我在项目早期用来快速验证“某块内存能不能被 DMA 访问”的小方法成本极低效果很好。先找一个能确认工作的 DMA 传输来源比如 TIM 触发 DMA 从一个普通 SRAM 数组复制到目标地址。如果目标地址是 DTCM配置好后启动传输观察 DMA 是否完成。用这个方式可以迅速判断问题到底出在 DMA 配置、外设请求还是目标内存不可达。比如我当时的验证流程是这样的/* 源D2 SRAM 里的数据 */ __attribute__((section(.sram1))) volatile uint32_t src_buf[8] {1,2,3,4,5,6,7,8}; /* 目标先试 DTCM */ volatile uint32_t dst_buf[8] __attribute__((section(.dtcmram))); /* 用 HPDMA 做 mem2mem 一次传输src - dst */ HAL_DMA_Start(hdma_mem2mem, (uint32_t)src_buf, (uint32_t)dst_buf, 8); /* 等传输完成超时若超时则说明目标地址不可达 */ if (HAL_DMA_PollForTransfer(hdma_mem2mem, HAL_DMA_FULL_TRANSFER, 100) ! HAL_OK) { /* 目标地址可能不可 DMA 访问 */ }如果源地址是普通 SRAM目标地址是 DTCM传输会超时如果把目标地址改成 D2 SRAM传输立刻完成。这个测试能在一分钟内确认问题方向比对着参考手册翻半天总线矩阵图高效得多。我在 H7 相关项目里凡是遇到 DMA 不工作的诡异问题都会先跑一遍这个小工具。我个人在实际操作中最深的感受是H7 这类多总线架构的芯片跑不通一个问题时第一反应不要总想着去调中断优先级或翻转 GPIO而是先从系统架构层面问一句“这个数据通路上的主设备和目标从设备到底在不在同一个总线域里”。一旦把这个思维模型建立起来很多看似玄学的问题其实都有明确的解释。只要搞清楚了 DTCM 只有 CPU 能直接访问这一点SAI 加 HPDMA 的数据搬运反而变得很简单DMA 负责把数据送进普通 SRAMCPU 再按需把数据挪到 DTCM各司其职稳定可靠。