news 2026/4/23 16:28:25

DMA存储器到外设传输时序与触发条件图解说明

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DMA存储器到外设传输时序与触发条件图解说明

DMA存储器到外设传输:不是“开个通道就完事”,而是与外设共舞的硬件节拍器

你有没有遇到过这样的场景?
调试一个I²S音频输出,波形看起来没问题,但用频谱仪一测,底噪里总混着几根尖锐的杂音;
或者在做三相电机FOC控制时,明明PWM周期设的是10 µs,实测却总在9.8~10.3 µs之间跳变;
又或者UART连续发包时,偶尔第3帧数据莫名其妙变成0x00——而你的中断服务程序里连个printf都没加。

这些都不是“玄学”,它们全指向同一个被低估的底层机制:DMA存储器到外设(M2P)传输的时序确定性
它不像GPIO翻转那样直白,也不像ADC采样那样有明确的启动-转换-读取三步曲。它的行为,是DMA控制器、系统总线、外设状态机三方实时博弈的结果——稍有不慎,就会在毫秒级的宏观表现下,埋下纳秒级的微观隐患。

这不是配置几个寄存器就能跑通的功能模块,而是一套需要你亲手“校准”的硬件协同协议。


为什么“DMA搬运”从来不是单向快递?

先抛开手册里那些“Memory-to-Peripheral Transfer”的标准定义。我们换个角度理解:
DMA M2P,本质上是一场外设发起的“点餐式”服务——外设说“我饿了”,DMA才去内存取菜,再亲手喂到它嘴里。

关键在于:“我饿了”这个信号,是怎么被识别的?
外设不会主动喊话,它只是默默改变某个寄存器的某一位(比如USART_TDR空了,TXE=1;DAC转换结束,DAC_SR.BUSY=0)。DMA控制器通过专用硬件连线(DMA_REQx)持续采样这个位,一旦满足触发条件(上升沿?高电平?),立刻进入搬运流程。

但这里藏着第一个陷阱:“饿了”和“能吃”是两回事。
TXE=1只代表TDR空了,不代表移位寄存器已经把上一字节发出去了;DAC_SR.BUSY=0只代表上次转换结束,不代表DAC_DHR寄存器此刻能安全写入新值。如果DMA不管不顾地冲进去写,轻则数据覆盖,重则触发外设总线错误(如STM32的DMA_ISR[TEIF])。

所以,真正的M2P时序,必须拆解为两个耦合环路:

  • 请求环路:外设状态 → DMA_REQ信号有效 → DMA响应并启动读内存
  • 就绪环路:外设是否真正准备好接收数据?DMA是否要等?等多久?

这两个环路的时间差,就是所有“首帧丢失”、“数据撕裂”、“时序抖动”的物理根源。


看懂时序图,比背熟寄存器更重要

别急着翻《Reference Manual》查DMA_CCR每一位的含义。先看一张真实示波器捕获的波形(以STM32H743 + I²S驱动TAS5805M为例):

I²S_BCLK ────┬─────────────────────────────────────────────── │ I²S_WS ────┼─────────────────────────────────────────────── │ DMA_TCIF ────┼─────────────────────────────────────────────── │ SDA (Data) ──┼──[0x1234][0x5678][0x9ABC][0xDEF0]──────────── ↑ ↑ ↑ ↑ ↑ t0 t1 t2 t3 t4

你会发现:
-DMA_TCIF(传输完成中断标志)并不是在最后一字节写入后立刻拉高,而是滞后于SDA最后一个bit的下降沿约1.8 ns
- 相邻两帧SDA数据之间,BCLK严格保持20.83 µs(48 kHz),但DMA_TCIF边沿的抖动被压缩在±0.3 ns内;
-I²S_WS(左右声道同步信号)与第一字节SDA上升沿之间,存在固定1.2 µs的建立时间——这正是DMA从捕获TXE到真正把数据推到I²S_TDR所需的时间。

这些数字,手册里不会直接告诉你。它们来自三个地方:
1.外设数据手册:I²S发送器要求Tsu(SDA)≥ 100 ns(SDA相对于BCLK的建立时间);
2.MCU参考手册:DMA对APB外设的请求响应延迟为3~5个PCLK(H743 APB3时钟为120 MHz → 单周期8.3 ns);
3.实测校准:用逻辑分析仪抓1000次TCIF,统计其相对BCLK的相位分布,找到最差情况下的裕量。

✅ 实战口诀:“手册给范围,实测定边界,裕量留20%”
比如理论最大响应延迟是5个PCLK(41.5 ns),实测最差是4.2个,那你在定时器触发点就预留5个——宁可慢一点,不能错一次。


外设触发源,不是“选一个就行”,而是“选对才不翻车”

HAL库里一行hdma_i2s_tx.Init.Request = DMA_REQUEST_I2S1_TX;看似简单,但背后是三种截然不同的硬件语义:

USART TXE:最“娇气”的触发源

TXE不是“我准备好了”,而是“我刚刚清空了”。它的脉宽极短——在1 Mbps波特率下,TXE有效时间仅约1 µs。
致命坑点:如果DMA还没来得及响应,TXE就因TDR被自动清零而消失,这次请求就彻底丢失。
→ 解决方案:必须启用DMA_CFCR[DBM]双缓冲模式,让DMA在搬运Buffer A时,提前预取Buffer B的地址;同时确保DMA_CCR[MINC]使能,内存地址严格递增,避免指针错乱。

定时器UEV:最“可控”的触发源

UEV(Update Event)由ARR重载瞬间产生,是一个干净、稳定、可预测的边沿信号。
但它有个隐藏特性:UEV产生时刻 ≠ DAC实际开始转换时刻。中间隔着APB总线仲裁、DAC寄存器写入、模拟电路建立时间三层延迟。
→ 实测数据:STM32H743 + DAC1,从UEV上升沿到DAC输出电压变化,典型延迟为2.3 µs(±0.2 µs)。
→ 工程做法:若你要生成100 kHz正弦波(周期10 µs),不要把定时器ARR设为10 µs,而应设为10 µs - 2.3 µs = 7.7 µs,把硬件延迟“预补偿”进去。

ADC EOC反向驱动DAC:最“巧妙”的链路

这属于M2P的高级玩法:ADC采样一帧数据(P2M),立刻用这帧数据去更新DAC(M2P),构成闭环反馈。
但ADC和DAC不能共享同一块内存——否则CPU读ADC结果时,DMA可能正在往同一地址写DAC数据。
→ 正解:启用双缓冲+半传输中断(HTIF)。ADC填Buffer A时,DMA从Buffer B往DAC写;当ADC填满一半(HTIF),DMA切换到Buffer A;此时CPU可安全读取Buffer B的旧数据,而DMA已无缝续上。

🔧 寄存器精讲:DMA_CFCR[DBM](Double Buffer Mode)
这个位一开,DMA内部就多出一套地址/计数器寄存器(CMAR1/CMAR2,CNDTR1/CNDTR2)。它不帮你管理数据内容,只负责在两个缓冲区间自动切换。
切换时机由DMA_CFCR[CT]位控制:CT=0时,当前使用Buffer 0,下一个用Buffer 1;CT=1则反之。
千万别以为开了DBM就万事大吉——你得自己保证两个缓冲区的数据是“时空隔离”的。


音频系统的实战心法:从“能响”到“CD级精准”

以I²S音频流为例,很多工程师卡在“能播放”,却跨不过“无杂音”的门槛。真相往往藏在初始化顺序里:

// ❌ 错误顺序:先开DMA,再启I²S HAL_DMA_Start(&hdma_i2s1_tx, (uint32_t)audio_buf, (uint32_t)&hi2s1.Instance->TXDR, AUDIO_BUF_SIZE); HAL_I2SEx_TransmitReceive_DMA(&hi2s1, ...); // 此时I²S尚未使能,TXE永远为0 // ✅ 正确顺序:先启I²S,再开DMA,并插入同步屏障 __HAL_I2S_ENABLE(&hi2s1); // 启动I²S,TXE开始翻转 __DSB(); // 数据同步屏障:确保I²S使能指令已提交 HAL_DMA_Start(&hdma_i2s1_tx, ...); // 此刻DMA才能捕获首个TXE

为什么需要__DSB()?因为ARM Cortex-M的写缓冲(Write Buffer)可能把__HAL_I2S_ENABLE的写操作暂存,导致DMA在I²S物理上电前就开始监听TXE——结果当然是“守株待兔”,首帧永远丢失。

另一个常被忽视的细节:内存地址对齐
I²S发送的是16-bit立体声样本(4字节/帧),若audio_buf起始地址是0x20000001(非4字节对齐),AHB总线在突发传输(Burst Transfer)时会降级为单次传输,带宽直接腰斩。
→ 编译期强制对齐:uint32_t __attribute__((aligned(4))) audio_buf[AUDIO_BUF_SIZE];

最后,关于中断:
别在DMA_TCIF_IRQHandler里直接调用memcpy()填充新缓冲区。那会引入不可控的Cache刷新、分支预测失败等抖动。
→ 正确姿势:ISR里只做两件事:
1.HAL_DMA_IRQHandler()清除TCIF标志;
2.osSignalSet(audio_task_handle, SIGNAL_BUF_READY);(FreeRTOS)或置位一个原子变量。
真正的数据填充,交给一个低优先级任务,在系统空闲时从容完成。


写在最后:DMA不是黑箱,而是你最值得信赖的硬件搭档

当你终于搞懂为什么DMA_CCR[MINC]必须和PeriphInc=DISABLE配对使用,
当你亲手用示波器测出TXE脉宽只有830 ns并据此调整缓冲区大小,
当你在双缓冲切换瞬间捕捉到0.1 ns的毛刺并定位到未关闭的Cache预取——

你就不再是在“调用DMA”,而是在和硬件对话。
DMA不再是数据手册里一段冰冷的描述,而成了你系统里呼吸同步、心跳同频的有机部分。

下次再看到“DMA传输完成中断抖动大”,别急着换芯片。
先问自己三个问题:
- 外设的“就绪窗口”够宽吗?我有没有实测过?
- DMA的“请求响应”路径上,有没有未被注意到的总线竞争?
- 我的缓冲区管理,真的做到了时空隔离,还是只是逻辑上“应该没问题”?

真正的实时性,不在主频有多高,而在你对每一个时钟周期的敬畏与掌控。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 3:51:41

Proteus仿真工业压力检测电路:通俗解释

Proteus仿真工业压力检测电路:从原理到落地的硬核实践手记你有没有遇到过这样的场景?调试一块刚打回来的压力采集板,ADC读数跳变20 LSB,示波器上看到运放输出纹波肉眼可见;或者在客户现场,数码管明明代码写…

作者头像 李华
网站建设 2026/4/18 0:14:19

CH340芯片驱动下载与安装核心要点解析

CH340驱动安装:不是点下一步,而是打通USB与UART之间的信任链你有没有遇到过这样的场景?刚焊好一块STM32最小系统板,插上USB线,打开Arduino IDE,串口监视器下拉菜单里空空如也;在Windows设备管理…

作者头像 李华
网站建设 2026/4/23 15:58:42

emuelec核心模拟器设置:手把手教程优化启动项

EmuELEC启动项优化实战手记:一个嵌入式模拟器工程师的逐层拆解你有没有试过——在Odroid-N2上按下电源键,3秒后手柄震动、4.7秒画面跳出《超级马里奥兄弟》标题,帧率稳如钟摆?这不是魔法,是把Linux启动流程从“尽力而为…

作者头像 李华
网站建设 2026/4/23 8:17:20

高速PCB SerDes通道建模仿真解析

高速PCB SerDes通道建模仿真:不是“验货”,而是“造路” 你有没有遇到过这样的场景? 一块AI加速卡投板回来,28 Gbps NVLink链路在室温下勉强通过,但一上电满载、温度升到75℃,误码率就飙升到1e-5&#xff…

作者头像 李华
网站建设 2026/4/23 9:53:27

新手必看:深度学习项目训练环境使用全攻略

新手必看:深度学习项目训练环境使用全攻略 你是不是也经历过这些时刻? 刚下载完PyTorch,发现CUDA版本不匹配; 配好环境跑通第一个train.py,换台机器又报错“ModuleNotFoundError”; 想试试模型剪枝或微调&…

作者头像 李华
网站建设 2026/4/23 9:53:25

Elasticsearch教程:快速理解日志分析架构集成

日志分析不是“配完就能跑”,而是设计出来的可观测性基础设施 你有没有遇到过这样的场景: - Kibana里查不到刚写入的日志,刷新三遍才出现; - 用 level: ERROR 做筛选,结果返回一堆 WARN 甚至 INFO ; - Dashboard加载要等8秒,点开一个折线图就卡住; - 大促期…

作者头像 李华