3、DMA传输类型详解:内存到内存、外设到内存、内存到外设、外设到外设传输场景分析
好,咱们今天来聊聊DMA的四种基本传输类型。说实话,这四种类型是DMA操作的核心,搞懂了它们,你基本就掌握了DMA的精髓。我在项目里见过不少工程师,配置寄存器时头头是道,但一遇到实际场景就不知道选哪种传输模式了。嗯,今天咱们就把这事儿彻底说清楚。
3.1 内存到内存传输(Memory-to-Memory)
这种模式说白了就是数据从一个内存地址搬到另一个内存地址。CPU只需要告诉DMA:「嘿,帮我把这1000个字节从A地址搬到B地址」,然后就可以去干别的事了。
典型应用场景:
- 数据缓冲区拷贝(比如从接收缓冲区搬到应用缓冲区)
- 图像帧缓冲区的快速复制
- 数据结构的初始化或重置
核心要点:内存到内存传输通常需要DMA控制器内部产生源地址和目标地址,不需要外设触发信号。传输一旦启动,就会一直进行直到完成。
我个人习惯在初始化大块数据结构时用这个功能。比如你有一个512字节的查找表,每次系统启动都要初始化,用CPU循环赋值太慢了,DMA一次搞定。
// 伪代码示例:配置内存到内存传输
DMA_ChannelConfig chConfig;
chConfig.srcAddr = (uint32_t)&sourceBuffer;
chConfig.dstAddr = (uint32_t)&destBuffer;
chConfig.transferSize = 512; // 传输512个字节
chConfig.srcInc = DMA_ADDR_INC; // 源地址递增
chConfig.dstInc = DMA_ADDR_INC; // 目标地址递增
chConfig.transferMode = DMA_MEM_TO_MEM;
chConfig.triggerSource = DMA_SOFTWARE_TRIGGER; // 软件触发
DMA_InitChannel(DMA_CH0, &chConfig);
DMA_StartTransfer(DMA_CH0); // 启动传输
我的经验:做内存到内存传输时,一定要注意源地址和目标地址不能重叠,否则数据会被覆盖。我曾经在一个项目中没注意这个,结果调试了整整一天才发现是地址重叠的问题。
3.2 外设到内存传输(Peripheral-to-Memory)
这是嵌入式系统中最常用的DMA传输模式。外设(比如ADC、UART、SPI)产生数据,DMA自动把数据搬到内存里。CPU完全不用管,等DMA传输完成后再来处理数据就行。
典型应用场景:
- ADC连续采样数据存储
- UART接收不定长数据
- SPI从设备接收数据
- 定时器捕获数据记录
你想想看,如果没有DMA,CPU得不停地去检查外设的数据寄存器有没有新数据。这不仅浪费CPU时间,还容易丢数据。用DMA的话,外设每产生一个数据,DMA就自动搬走一个,CPU可以安心处理其他任务。
关键配置:外设到内存传输需要配置外设触发信号。源地址通常是外设的数据寄存器地址(固定不变),目标地址是内存缓冲区地址(递增)。
// 伪代码示例:配置ADC到内存传输
DMA_ChannelConfig chConfig;
chConfig.srcAddr = (uint32_t)&ADC->DR; // ADC数据寄存器,地址固定
chConfig.dstAddr = (uint32_t)&adcBuffer; // 内存缓冲区
chConfig.transferSize = 100; // 采集100个样本
chConfig.srcInc = DMA_ADDR_FIXED; // 源地址固定(外设寄存器)
chConfig.dstInc = DMA_ADDR_INC; // 目标地址递增
chConfig.transferMode = DMA_PERIPH_TO_MEM;
chConfig.triggerSource = DMA_TRIGGER_ADC; // ADC转换完成触发
DMA_InitChannel(DMA_CH1, &chConfig);
DMA_EnableChannel(DMA_CH1); // 等待外设触发
避坑指南:我曾经在配置UART接收DMA时,忘了设置源地址为固定模式,结果DMA每次传输后源地址自动递增,读到的全是垃圾数据。嗯,这个坑我替你们踩过了。
3.3 内存到外设传输(Memory-to-Peripheral)
这个模式正好和上面相反。DMA从内存中读取数据,然后写入外设的数据寄存器。典型的就是发送数据,比如通过UART发送一串数据,或者通过DAC输出波形数据。
典型应用场景:
- UART发送大量数据(比如日志输出)
- DAC波形数据输出
- SPI发送数据到外部设备
- PWM占空比表更新
我记得有个项目需要输出一个正弦波,采样点有1024个。如果用CPU一个一个往外送,CPU基本就废了。用DMA的话,设置好循环模式,DMA会自动循环发送这1024个数据,CPU完全解放出来。
// 伪代码示例:配置内存到DAC传输
DMA_ChannelConfig chConfig;
chConfig.srcAddr = (uint32_t)&sineWaveTable; // 正弦波数据表
chConfig.dstAddr = (uint32_t)&DAC->DR; // DAC数据寄存器
chConfig.transferSize = 1024; // 1024个采样点
chConfig.srcInc = DMA_ADDR_INC; // 源地址递增
chConfig.dstInc = DMA_ADDR_FIXED; // 目标地址固定(外设寄存器)
chConfig.transferMode = DMA_MEM_TO_PERIPH;
chConfig.circularMode = ENABLE; // 循环模式,持续输出波形
chConfig.triggerSource = DMA_TRIGGER_TIMER; // 定时器触发
DMA_InitChannel(DMA_CH2, &chConfig);
DMA_EnableChannel(DMA_CH2);
我的建议:做内存到外设传输时,如果数据量不大,可以考虑用双缓冲(Double Buffering)技术。一个缓冲区在传输,另一个缓冲区在准备数据,这样能实现无缝的数据更新。
3.4 外设到外设传输(Peripheral-to-Peripheral)
这个模式用得相对少一些,但在某些场景下非常有用。数据从一个外设直接传输到另一个外设,完全不经过内存。CPU只需要配置好传输参数,剩下的DMA全包了。
典型应用场景:
- SPI接收的数据直接通过UART发送出去(协议转换)
- 定时器捕获值直接写入比较寄存器
- ADC采样值直接送到DAC输出(信号中继)
- 数字滤波器输入到输出
为什么会用到这种模式?说白了就是减少延迟。数据不经过内存,省去了内存读写的开销,延迟可以做到非常低。我在一个高速数据采集项目中用过这个功能,ADC采集的数据直接通过DMA送到DAC输出,延迟控制在几个时钟周期内。
注意事项:外设到外设传输对时序要求比较高。两个外设的数据宽度、传输速率必须匹配,否则很容易出现数据错位的问题。
// 伪代码示例:配置SPI到UART的直通传输
DMA_ChannelConfig chConfig;
chConfig.srcAddr = (uint32_t)&SPI->DR; // SPI接收数据寄存器
chConfig.dstAddr = (uint32_t)&UART->DR; // UART发送数据寄存器
chConfig.transferSize = 256;
chConfig.srcInc = DMA_ADDR_FIXED; // 源地址固定
chConfig.dstInc = DMA_ADDR_FIXED; // 目标地址固定
chConfig.transferMode = DMA_PERIPH_TO_PERIPH;
chConfig.triggerSource = DMA_TRIGGER_SPI_RX; // SPI接收完成触发
DMA_InitChannel(DMA_CH3, &chConfig);
DMA_EnableChannel(DMA_CH3);
避坑指南:我曾经在一个项目中用外设到外设传输做SPI到UART的协议转换,结果发现UART的波特率跟不上SPI的速率,导致数据丢失。后来加了FIFO缓冲才解决。所以,两个外设的速率匹配一定要提前算好。
3.5 四种传输类型对比总结
| 传输类型 | 源地址 | 目标地址 | 触发方式 | 典型应用 |
|---|---|---|---|---|
| 内存到内存 | 递增 | 递增 | 软件触发 | 数据拷贝、初始化 |
| 外设到内存 | 固定 | 递增 | 外设触发 | ADC采样、UART接收 |
| 内存到外设 | 递增 | 固定 | 外设触发 | DAC输出、UART发送 |
| 外设到外设 | 固定 | 固定 | 外设触发 | 协议转换、信号中继 |
嗯,这四种传输类型基本覆盖了嵌入式系统中所有的数据搬运场景。我个人建议,在实际项目中先想清楚数据流向,再选择对应的传输模式。别一上来就配寄存器,那样容易出错。
最后说一句,TC3xx的DMA控制器还支持链表模式(Linked List),可以把多个不同的传输任务串起来。这个咱们后面章节再细聊。今天先把这四种基础类型吃透,后面的内容就好理解了。