1. 项目概述与问题场景
最近在调一块用 STM32H7 系列做音频采集的板子,遇到一个挺典型的问题:SAI 通过 HPDMA 往 DTCM 里搬数据,怎么配都不工作。现象很统一——HPDMA 的传输完成中断永远不触发,状态寄存器里挂着超时或者总线错误,SAI 侧倒是正常出帧同步,可数据就是过不去。
这个组合(SAI + HPDMA + DTCM)在 H7 系列上其实很常见,尤其是想做低延迟音频处理的场景。SAI 负责收音频数据,HPDMA 负责把数据从外设搬到内存,DTCM 作为最终存储区域,为的是让 CPU 能以零等待状态直接访问数据。理论上这套链路很流畅,实际配起来却有不少暗坑。
先说结论:问题大概率出在 HPDMA 对 DTCM 的访问路径、MPU 配置、以及 SAI 与 DMA 之间的触发方式这三处。这篇博文会把 HPDMA 和 DTCM 的配合逻辑、常见配置错误、排查方法完整梳理一遍,还把最关键的“为什么不工作”几个原因逐一拆开。
这篇文章适合正在用 STM32H7 系列做音频、高速 ADC 采集、或者任何需要外设 DMA 直通 DTCM 的朋友参考。即使你用的是 G4 或者 F4 系列,里面关于 DMA 与内存区域匹配的原理也是通用的。
2. 理解 HPDMA、DTCM 和 SAI 的角色定位
2.1 HPDMA 到底是什么,和普通 DMA 有什么区别
STM32H7 系列里的 HPDMA 是新一代 DMA 控制器,和老的 DMA1/DMA2 对比,主要体现在几个方面:通道数更多、支持 8 字节突发传输、可以处理 32-bit 甚至 64-bit 位宽的数据搬运,还支持 scatter-gather 模式(也就是链表传输)。
HPDMA 的定位是“高性能”,它挂在 AXI 总线上,理论带宽比传统 DMA 高很多。在音频场景里,采样率 48kHz、32bit 双通道,数据量大概是 384KB/s,这个量级老 DMA 也能扛住,但 HPDMA 的触发延迟更低、中断开销更小,更适合长时间不间断的音频流。
不过 HPDMA 的灵活性也带来了配置复杂度。它的每个通道有独立的控制寄存器、状态寄存器和中断标志,配置出错后的表现也和老 DMA 不太一样——老 DMA 配错了直接不进中断或者数据错位,HPDMA 配错了常常表现为总线错误(Bus error)或者传输超时(Timeout),排查难度反而更高。
2.2 DTCM 的特性:为什么选择它作为音频数据存储区
DTCM(Data Tightly Coupled Memory)是 Cortex-M7 内核特有的内存区域,和内核通过专用总线连接,访问延迟极低,而且是零等待状态。这一点在音频处理中非常关键——如果 SAI 数据到达后 CPU 要频繁读取处理,放在 DTCM 里可以显著降低 CPU 等待周期。
但 DTCM 有个比较尴尬的特性:它不是所有总线主设备都能访问。Cortex-M7 内核本身可以直接读写 DTCM,但 DMA 控制器呢?H7 系列上 DMA 是否能访问 DTCM,取决于总线互联矩阵的具体设计。实测下来,HPDMA 是能够通过 AXI 总线访问 DTCM 的,但必须满足一个前提——DTCM 的基地址在系统地址映射中处于正确位置,同时相关的 MPU 区域配置必须允许 DMA 访问。
这里补充一个非常容易忽略的细节:DTCM 和 ITCM 一样,在 H7 系列中有“别名映射”机制。在默认的地址映射里,DTCM 出现在 0x20000000 区域,但如果你在链接脚本或者系统配置里改了别名映射,DMA 侧的地址和 CPU 侧的地址可能就不一致了。这个是“DMA 搬运不到 DTCM”的一个隐藏原因。
2.3 SAI 到 DMA 的数据路径:整个链路怎么走
SAI 外设(Serial Audio Interface)在 H7 上支持同步和异步收发,数据通过内部 FIFO 接收后,会产生 DMA 请求信号。这个请求信号要经过外设互联矩阵(DMAMUX)后,映射到 HPDMA 的某个通道上。
完整链路是:SAI 接收 FIFO 半满/非空 → 触发 DMA 请求 → DMAMUX 映射到 HPDMA 通道 → HPDMA 从 SAI 数据寄存器读取数据 → 写入目标内存地址(DTCM)。
这条链路里任何一环断了都会导致传输不工作。比较常见的断点是:
- DMAMUX 映射没配对,SAI 的请求没有连到目标 HPDMA 通道
- SAI 的 DMA 请求条件没开启(比如没有使能 FIFO 的 DMA 请求输出)
- HPDMA 通道的源地址配错了,没有指向 SAI 的数据寄存器
- 目标地址(DTCM)访问权限不足
所以排查的时候不要一上来就怀疑 HPDMA 寄存器配置,先理清楚整个链路里最容易被忽略的那几环。
3. 核心问题拆解:为什么 HPDMA 到 DTCM 会失败
3.1 原因一:DTCM 区域被 MPU 配置为不可访问或权限受限
Cortex-M7 内置的 MPU(Memory Protection Unit)可以配置内存区域的访问权限、缓存策略和共享属性。H7 系列上电默认的 MPU 配置里,DTCM 区域是正常可读写的,CPU 能访问不等于 DMA 能访问。
但如果你的代码里自行配置了 MPU(很多 RTOS 或安全相关代码会这么做),把 DTCM 区域配置为“只允许特权模式访问”,或者把共享属性设为“不可缓存”但同时又开了某种缓存一致性策略,DMA 访问就可能被拒绝。
我在实际调试中遇到过一次:MPU 把 0x20000000 区域配置为 32KB 粒度、只读属性,CPU 侧写数据没问题(因为内核配置的是可写),但 HPDMA 要向这个地址写数据时,总线层面直接返回错误。HPDMA 的状态寄存器会显示BUSY=1, BUSERR=1,中断标志里也有TE(Transfer Error)。
排查方式:在调试器里读出 MPU 相关寄存器(MPU_RBAR、MPU_RLAR),确认 0x20000000 附近的区域配置是否正确。如果发现权限位有问题,要么修改 MPU 配置,要么把目标地址换到 AXI SRAM 区域(0x24000000 起)。
3.2 原因二:DTCM 的地址映射和链接脚本不匹配
这里的坑比 MPU 更隐蔽。H7 系列的内存映射里,DTCM 默认在 0x20000000,AXI SRAM 在 0x24000000,SRAM1/2/3 在 0x30000000 区域。但很多工程模板会把 DTCM 的地址改到别的区域(比如为了给 ITCM 让路,或者为了配合 bootloader 的加载地址)。
如果链接脚本把音频缓冲区所在的段定义在了 DTCM 的“逻辑地址”区域,但实际运行时总线互联矩阵对这个地址的映射和你的预期不一致,DMA 写入就会失败。
典型的错误是在链接脚本里写了类似.audio_buffer (NOLOAD) : { *(.audio_buffer) } > DTCMRAM,但芯片实际的 DTCM 物理基地址和你链接脚本里定义的内存区域名称不对应。
排查方式:编译后查看 map 文件,确认音频缓冲区的实际链接地址是多少,然后和参考手册里的 DTCM 基地址对照。如果地址明显不在 0x20000000 附近,查一下 startup 文件和链接脚本里对内存区域的划分是否正确。
3.3 原因三:HPDMA 的 FIFO 和突发配置与 SAI 数据宽度不匹配
SAI 的数据寄存器宽度可以配置为 8-bit、16-bit、32-bit。HPDMA 外设端的数据宽度也要和 SAI 保持一致。如果两者不匹配,会出现数据错位、传输卡死、或者只搬了部分数据就停住。
举个例子:SAI 配置为 32-bit 数据宽度,HPDMA 的 PSIZE 也配置为 32-bit,这两个是匹配的。但如果 HPDMA 的 MSIZE(内存端数据宽度)配置为 8-bit,并且没有开启 FIFO 模式,内存端逐字节写入会严重拉低传输效率,极端情况下 DMA 控制器会进入忙等状态,导致看起来像卡住了。
更隐蔽的是 FIFO 阈值设置。HPDMA 在直连模式(Direct mode)下,要求外设端和内存端的数据宽度严格一致。只有开启 FIFO 模式后,才允许外设端和内存端宽度不同。我的建议是:在 SAI + HPDMA 场景里,把 HPDMA 配成 FIFO 模式,外设端宽度跟随 SAI,内存端宽度设为 32-bit(如果目标是 DTCM 或 AXI SRAM 的话),FIFO 阈值设为 1/2 或 1/4。
3.4 原因四:SAI 的 DMA 请求条件和 HPDMA 的触发方式配置不一致
SAI 的 DMA 请求有两种模式,一种是在 FIFO 达到编程阈值时产生请求,一种是在 FIFO 为空时产生请求。HPDMA 的触发可以是电平触发(Level-sensitive)或边沿触发(Edge-sensitive)。
如果 SAI 设置为“FIFO 半满时产生 DMA 请求”,而 HPDMA 的触发方式配置成了边沿触发,有可能出现:FIFO 半满的状态保持时间不足以让 HPDMA 采样到边沿,导致 HPDMA 一直不启动传输。
这个问题的诡异之处在于:你单独看 SAI 寄存器,FIFO 里确实有数据;单独看 HPDMA 寄存器,通道也已经 enable 了;但两者就是没“握手”上。
设置建议:HPDMA 外设端请求设置为电平触发(Level-sensitive),SAI 的 FIFO 阈值设置为 1/2。这是音频场景里实测最稳的组合。如果你用了类似LL_DMA_SetTriggerMode的 API,确认传入的触发模式参数是电平触发而不是边沿触发。
4. 实操过程:从零配置一套可用的 SAI 到 DTCM 链路
4.1 工程准备与 CubeMX 基础配置
我用的是 STM32H743 + CubeMX 6.x 生成的工程,LL 库开发。SAI 接了一个外部音频 Codec,主时钟由 SAI 提供,接收通路数据需要通过 HPDMA 搬运到 DTCM。
CubeMX 里需要配这几样:
- SAI1 Block A 配为接收模式,主时钟输出,数据格式 32-bit/通道,帧长 64-bit
- DMAMUX1 把 SAI1_A 的 DMA 请求映射到 HPDMA1 Channel 0
- HPDMA1 Channel 0 配为外设到内存传输,外设地址固定(SAI1_A 数据寄存器),内存地址递增
- 内存区域选 DTCM(0x20000000 区域),缓冲区定义在 DTCM 段里
CubeMX 生成的初始化代码里,HPDMA 的配置函数已经写好了,但有个地方需要手动改一下:生成的代码默认把 DMA 目标地址设在一个内部数组上,而这个数组可能被链接脚本放在了 AXI SRAM 而非 DTCM。所以要么在代码里手动定义 DTCM 段的缓冲区,要么改链接脚本。
4.2 链接脚本里划分 DTCM 缓冲区
在 STM32H743 的链接脚本中,默认有一个DTCMRAM区域,起始地址 0x20000000,大小 128KB。如果把音频缓冲区分到这个区域里,用起来最直接:
__attribute__((section(".audio_buf"))) uint32_t audio_data[2048];然后在链接脚本里加上:
.audio_buf (NOLOAD) : { *(.audio_buf) } > DTCMRAM编译后从 map 文件确认audio_data确实在 0x20000000 区域,然后 MPU 检查它的区域配置是允许读写、允许 DMA 访问(共享属性建议配置为 Write-Back,Read-Write Allocate)。
4.3 HPDMA 通道寄存器配置要点
如果不想依赖 CubeMX 生成的 HAL 代码,自己用 LL 库配置 HPDMA 也可以。核心代码如下:
LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_0, LL_DMAMUX1_REQ_SAI1_A); LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_0, LL_DMA_DIRECTION_PERIPH_TO_MEMORY); LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_0, LL_DMA_MODE_CIRCULAR); LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_0, LL_DMA_PERIPH_NOINCREMENT); LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_0, LL_DMA_MEMORY_INCREMENT); LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_0, LL_DMA_PDATAALIGN_WORD); LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_0, LL_DMA_MDATAALIGN_WORD); LL_DMA_SetFIFOMode(DMA1, LL_DMA_CHANNEL_0, LL_DMA_FIFO_ENABLE_1_2); LL_DMA_SetTriggerMode(DMA1, LL_DMA_CHANNEL_0, LL_DMA_TRIGGER_LEVEL); LL_DMA_SetStreamPriority(DMA1, LL_DMA_CHANNEL_0, LL_DMA_PRIORITY_HIGH);有几个关键点必须说清楚:
LL_DMA_SetPeriphRequest里的第二个参数,如果是 H7 系列要区分 DMA1 和 DMA2(其实是两个 HPDMA 实例),映射号来自 DMAMUX1 的请求映射表LL_DMA_SetFIFOMode设置的是 FIFO 阈值 1/2,这个在 SAI 32-bit 数据宽度下理论可以保证稳定传输LL_DMA_TRIGGER_LEVEL就是前面说的电平触发,千万不要配成LL_DMA_TRIGGER_EDGE
配置完通道后,使能传输完成中断:
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_0); LL_DMA_EnableStream(DMA1, LL_DMA_CHANNEL_0);注意LL_DMA_EnableStream之后,HPDMA 不是立刻开始搬运的,它在等外设请求。这个外设请求由 SAI 在 FIFO 达到阈值时产生,所以时序上 SAI 要先启动。
4.4 启动顺序:先开 DMA 还是先开 SAI
这其实是一个很容易踩的坑。我个人的实践结论是:先在 DMA 侧使能通道并等待请求,再启动 SAI 接收。原因是如果 SAI 先启动,FIFO 可能已经积压了几帧数据,DMA 通道还没建立好,此时 SA 侧会产生 overrun 错误(ROVR 标志),并且会清掉 FIFO 里的数据。
正确的启动顺序:
- 配置好 SAI 的采样率、帧格式、数据宽度,但不要使能 SAI
- 配置好 HPDMA 的通道参数,使能中断,使能通道,让它挂在那里等
- 最后使能 SAI 的接收使能位
- 在 HPDMA 的传输完成中断里做数据消费
这个顺序能最大程度避免时序竞争。我遇到过几次启动顺序不对导致的偶发丢数据,调整顺序后稳定了很多。
4.5 中断处理:传输完成中断里应该做什么
HPDMA 的传输完成中断触发时,代表指定长度的数据已经从 SAI FIFO 搬到了 DTCM 缓冲区。实际代码里建议做这几件事:
void DMA1_Channel0_IRQHandler(void) { if (LL_DMA_IsActiveFlag_TC0(DMA1)) { LL_DMA_ClearFlag_TC0(DMA1); /* 此时 audio_data 里的数据是有效的 */ process_audio_data(audio_data); } }如果你用的是循环模式(Circular mode),传输完成中断会周期性地触发。每段数据的长度由LL_DMA_SetDataLength指定。在音频流里,这个值建议设置为一次 DMA 中断搬运的采样块大小,比如 256 个采样(32-bit),这样中断频率在 48kHz 采样率下约 187Hz,CPU 负载很轻。
有一个点需要特别提一下:HPDMA 传输完成中断标志需要在读数据之前清掉,这是防止中断标志被重复触发的最简单方式。如果你不清标志就出中断,下次中断永远不会来。
5. 常见问题与排查技巧实录
5.1 问题一:HPDMA 一直不启动,状态寄存器显示 IDLE
排查思路:
- 先确认 SAI 有没有产生 DMA 请求——读 SAI 的
STAT寄存器,看有没有 FIFO 阈值触发标志。如果没有,说明 SAI 侧没产生请求,问题不在 DMA - 确认 DMAMUX 映射是否正确——读 DMAMUX1_C0CR 寄存器,它的
DMAREQ_ID字段值要对应 SAI1_A 的请求号 - 确认 HPDMA 通道是否使能——看
CCR寄存器的 EN 位是否置 1,还要确认没有在使能后又立刻被别的代码禁用
实测中,相当一部分“DMA 不工作”的现象,最终都定位到 DMAMUX 映射错了,或者是 CubeMX 生成的初始化代码里用了默认的映射而不是 SAI 的请求号。
5.2 问题二:HPDMA 传输到一半卡死,状态寄存器显示 BUSY
这种情况通常不是 DMA 本身的问题,而是总线访问失败。优先查这三处:
- 目标地址是否在系统总线上可以访问的内存区域。DTCM 在部分 DMA 配置下可能访问受限,如果确认是这个问题,换到 AXI SRAM(0x24000000)再试
- MPU 缓存策略。如果 MPU 把目标区域配置成了 Write-Through 或者不可缓存,但 DMA 又需要某种缓存一致性保证,可能会出现 DMA 写完后从 CPU 读到的还是旧数据,看起来像“没搬”
- 内存端地址对齐。HPDMA 要求内存端地址按数据宽度对齐,如果目标地址奇数对齐且数据宽度是 32-bit,会触发对齐错误
5.3 问题三:数据偶尔错位,或者首尾有杂音
这种情况和 SAI 的帧长度、槽位配置有关,DMA 本身是正常的。SAI 接收数据时,每帧的第一个槽位如果是有效数据,就应该从帧头开始搬运。如果你的 SAI 配置的槽位数量和实际音频 Codec 输出的不一致,DMA 搬到的数据会出现移位。
排查方式:用调试器看一次 DMA 搬回的数据前几个字节,和 SAI 输入端的预期数据做对比。如果前面多了一两个 0,说明帧长配置有问题。也可以在 SAI 寄存器里打开FRCR的 Frame Length 设置,确保它和 Codec 输出的帧长完全一致。
5.4 问题四:HPDMA 中断触发频率异常高或异常低
先算一下理论中断频率。比如音频采样率 48kHz、通道数 2、位深 32bit,SAI 每个采样帧产生 2 个 32-bit 数据。HPDMA 一次搬运 N 个 32-bit 数据,然后中断一次。那么中断频率就是 48000 * 2 / N Hz。
如果实际中断频率和预期差距很大,多半是LL_DMA_SetDataLength配置的长度不是按数据宽度换算的。HPDMA 的数据长度单位是“次传输”,不是字节。如果配置长度时没有除以 4(32-bit 宽度),实际中断会提前或延后。
5.5 问题五:用 memset 或 memcpy 操作 DTCM 缓冲区时程序跑飞
这不是 DMA 的锅,是 DTCM 的访问权限问题。部分 H7 芯片上,DTCM 区域只有在特权模式下才能由 CPU 用普通 load/store 指令访问。如果你跑的是非特权线程,直接用memcpy访问 DTCM 缓冲区,会触发 HardFault。
解决办法是:要么把缓冲区挪到 AXI SRAM,要么在 MPU 配置里把 DTCM 区域设为“可被非特权访问”。不过更推荐前者——不要把关键音频缓冲区放在 DTCM 里,放在 AXI SRAM 反而更省心,DTCM 留给 CPU 的栈和局部变量。
6. 正确配置 MPU:让 DMA 和 CPU 都能正常访问
6.1 为 DTCM 缓冲区配置 MPU 区域
如果你确实想用 DTCM 作为 DMA 的目标内存,MPU 配置不能省。参考配置如下:
MPU_Region_InitTypeDef MPU_InitStruct = {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20000000; MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);特别说明:TypeExtField和IsCacheable这几个字段组合起来,决定了内存的缓存策略。MPU_TEX_LEVEL1 + CACHEABLE + BUFFERABLE组合对应 Write-Back 缓存策略,这是数据缓冲区最常用的配置。如果 DMA 和 CPU 之间需要强一致性,可以改成IsBufferable = MPU_ACCESS_NOT_BUFFERABLE,但会牺牲一点性能。
6.2 一个更稳的替代方案:把目标内存放到 AXI SRAM
很多时候音频缓冲区真的没必要放在 DTCM。AXI SRAM(0x24000000 起始)在 H7 上同样接在 AXI 总线上,HPDMA 访问它没有任何权限问题,CPU 访问它虽然比 DTCM 慢几个周期,但对于音频处理这种中等实时性需求来说完全够用。
实测对比:同样的一路 48kHz 音频,放在 DTCM 和放在 AXI SRAM 的 CPU 占有率差异不到 2%,但配置复杂度差了一个量级。所以我的建议很直接——如果不是要做极低延迟的高保真音频处理,就别纠结 DTCM,直接 AXI SRAM。
7. 一步步复现一篇可运行的 Demo 流程
7.1 Demo 功能描述
做一个最小验证:SAI1_A 接收外部输入的方波信号,通过 HPDMA 把 64 个 32-bit 数据搬到 DTCM 缓冲区(如果你决定用 AXI SRAM,改个地址就行),每 64 个采样触发一次 DMA 传输完成中断,在中断里翻转一次 GPIO,用逻辑分析仪观察中断频率。
7.2 代码结构拆解
整个 demo 分三层:
- 初始化层:配置时钟、GPIO、SAI、HPDMA、NVIC、MPU
- 业务层:定义音频缓冲区、设置 DMA 目标地址
- 中断层:处理传输完成中断
初始化层的核心代码上文已经给出。业务层的代码更简单:
#define AUDIO_BUF_LEN 64 __attribute__((section(".audio_buf"))) uint32_t audio_buf[AUDIO_BUF_LEN]; void audio_dma_init(void) { LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_0, (uint32_t)audio_buf); LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_0, AUDIO_BUF_LEN); LL_DMA_EnableStream(DMA1, LL_DMA_CHANNEL_0); }中断层的核心代码在 4.5 里已经给出,只需要在中断里加一个 GPIO 翻转:
void DMA1_Channel0_IRQHandler(void) { if (LL_DMA_IsActiveFlag_TC0(DMA1)) { LL_DMA_ClearFlag_TC0(DMA1); LL_GPIO_TogglePin(GPIOB, LL_GPIO_PIN_0); } }7.3 验证步骤和预期结果
正常情况下,逻辑分析仪应该能看到 GPIOB PIN 0 以固定的频率翻转。48kHz 采样率、每次搬 64 个 32-bit 采样,理论中断频率是 750Hz。如果测到的频率不对,回头检查 SAI 的采样率配置和 DMA 的数据长度。
7.4 验证失败时如何缩小问题范围
加一个“手动触发”测试:在代码里把 HPDMA 的LL_DMA_EnableStream之后,临时用软件写一次LL_DMA_SetSWRequest或者触发一次软件请求,如果 HPDMA 能搬到数据,说明 DMA 到内存的通路是好的,问题出在 SAI 的 DMA 请求侧。如果连软件触发都搬不了,就要查 DMA 配置、地址、MPU。
这个方法能把“SAI 不产生请求”和“HPDMA 根本搬不了”两个问题快速分离开。实测排查效率非常高。
8. 常见配置速查表与避坑清单
为了节省大家反复翻参考手册的时间,我把这套配置里最容易出错的参数整理成了一张速查表,建议截图保存。
| 配置项 | 推荐值 | 错误示例 | 后果 |
|---|---|---|---|
| HPDMA 触发模式 | 电平触发 | 边沿触发 | SAI 请求可能不被识别,DMA 不启动 |
| HPDMA 数据宽度 | 32-bit(对齐 SAI 数据宽度) | 8-bit | 数据错位,效率骤降 |
| HPDMA FIFO 模式 | 使能,阈值 1/2 | 直连模式 | 宽度不匹配时导致总线错误 |
| DMAMUX 请求号 | SAI1_A 对应请求号 | 默认请求号 0 | DMA 收不到外设请求 |
| 目标内存区域 | AXI SRAM 或正确配置的 DTCM | 任意未配置区域 | 总线错误或 HardFault |
| MPU 缓存策略 | Write-Back,Read/Write Allocate | 不可缓存且无一致性保障 | 数据读到旧值 |
| 启动顺序 | 先 DMA 后 SAI | 先 SAI 后 DMA | 偶发 overrun,丢数据 |
速查表里最核心的三条是:HPDMA 用电平触发、DMAMUX 映射务必核对、MPU 缓存策略要落实。这三条做到位,绝大多数 “SAI to DTCM not working” 的场景都能解决。
9. 经验总结与性能实测补充
从这次问题排查到现在稳定运行,几个直观感受和建议如下。
第一,H7 系列的 DMA 链路虽然功能强大,但配置入口比 F4 系列多了不少。遇到问题不要只盯着 HPDMA 的寄存器,DMAMUX、MPU、外设请求条件一个都不能漏。我这次真正找到问题的关键,是先把工程里所有涉及内存访问的配置(链接脚本、MPU、DMA 地址)全部拉出来核对了一遍。
第二,性能方面,实测用 HPDMA 把一路 48kHz/32bit 音频流从 SAI 搬到 AXI SRAM,CPU 负载几乎可以忽略,中断频率大约 750Hz 的时候,每次中断处理消耗约 20 个 CPU 周期(只是简单的置标志位)。如果把缓冲区放在 DTCM,中断处理时间会再低一点点,但对整体系统影响很小。所以普通音频场景直接把缓冲区放 AXI SRAM 就好。
第三,关于 DTCM 和 DMA 配合,我个人的结论是:DTCM 适合 CPU 高频访问的小块数据,但不太适合作为外设 DMA 的长缓冲区。因为 DMA 访问 DTCM 虽然能工作,但需要额外确认 MPU、地址映射、总线仲裁这些细节,收益与投入不成正比。如果你的应用对延迟极其敏感,可以预留 DTCM 的一小段(比如 8KB)专门给实时信号处理用,同时用 HPDMA 先把数据搬到 AXI SRAM,再在中断里把需要的那一小段拷贝到 DTCM。这样既利用了 DTCM 的低延迟,又避开了直接 DMA 到 DTCM 的配置复杂度。
最后分享一个小技巧:调试这个类型的问题时,可以在初始化阶段把 HPDMA 的传输完成中断打开,然后在中断里放一个static计数变量,用调试器挂在中断里观察。只要中断能进,DMA 通路就通了,剩下的就是业务逻辑的调整。这套方法帮我节省了很多时间。遇到问题别急着改代码,先把链路每一段的“信号”确认到位,问题自然水落石出。