11. DMA 传输
📝本节您将了解 DMA(直接内存访问)的工作原理,学习如何配置 DMA 实现 ADC 采集数据的自动搬运,彻底释放 CPU 资源。
🏆本章目标
1️⃣ 理解 DMA 的工作原理和传输模式
2️⃣ 了解 MSPM0G3519 的 DMA 通道资源
3️⃣ 配置 ADC + DMA 实现自动数据采集
4️⃣ 理解 DMA 与中断配合的使用场景
11.1 DMA 基本概念
DMA(Direct Memory Access,直接内存访问)是一种硬件机制,允许外设与内存之间直接传输数据,无需 CPU 逐字节参与。
传统方式 vs DMA
传统中断方式(CPU 参与):
ADC完成 → 中断 → CPU读取ADC结果 → CPU写入内存 → 返回每次转换都需要 CPU 介入,高频采样时 CPU 负担沉重。
DMA 方式(CPU 不参与):
ADC完成 → DMA硬件自动搬运数据到内存 → 全部完成后通知CPUCPU 只需在最开始配置 DMA,之后数据传输全程自动完成。
DMA 传输要素
| 要素 | 说明 |
|---|---|
| 源地址 | 数据从哪里读(如 ADC 数据寄存器) |
| 目标地址 | 数据写到哪里(如内存数组) |
| 传输长度 | 总共搬运多少个数据 |
| 触发源 | 什么事件触发一次搬运(如 ADC 转换完成) |
| 传输宽度 | 每次搬运的数据大小(8/16/32 bit) |
| 地址递增 | 每次搬运后地址是否自动递增 |
11.2 DMA 传输模式
| 模式 | 说明 | 典型用途 |
|---|---|---|
| 单次传输 | 传输完设定长度后停止 | 一次性数据块搬运 |
| 循环传输 | 完成后自动重新开始 | 连续采集到环形缓冲区 |
传输方向
| 方向 | 说明 | 示例 |
|---|---|---|
| 外设 → 内存 | 从外设数据寄存器读到 RAM | ADC 结果自动存入数组 |
| 内存 → 外设 | 从 RAM 写到外设数据寄存器 | 预存数据自动发送到 UART/DAC |
| 内存 → 内存 | RAM 之间的块搬运 | 数据拷贝、缓冲区搬运 |
11.3 MSPM0G3519 DMA 资源
MSPM0G3519 的 DMA 控制器支持多个通道,每个通道可独立配置不同的触发源和传输参数。
常用 DMA 触发源:
| 触发源 | 说明 |
|---|---|
| ADC0 转换完成 | ADC 每完成一次转换触发一次 DMA |
| ADC1 转换完成 | 同上 |
| UART RX | UART 每收到一个字节触发一次 DMA |
| UART TX | UART 发送缓冲区空时触发 |
| SPI RX/TX | SPI 收发触发 |
| Timer 事件 | 定时器周期事件触发 |
11.4 ADC + DMA 实验
本实验实现:定时器触发 ADC 连续采样,DMA 自动将结果搬运到内存数组,全部完成后通知 CPU 处理。
11.4.1 SysConfig 配置
ADC 配置:
| 参数 | 设置值 | 说明 |
|---|---|---|
| Name | ADC12_0 | |
| ADC Module | ADC0 | |
| Trigger Source | Timer Event | 定时器触发 |
| Conversion Mode | Repeat Single | 重复单通道转换 |
| DMA Trigger | Enable | ✅ 使能 DMA 触发 |
DMA 配置:
| 参数 | 设置值 | 说明 |
|---|---|---|
| Name | DMA_CH0 | |
| DMA Channel | Channel 0 | |
| Trigger Source | ADC0 | ADC 转换完成触发 |
| Source Increment | No Increment | 源地址固定(始终读同一个寄存器) |
| Destination Increment | Increment | 目标地址递增(依次填充数组) |
| Transfer Size | 16 bit | ADC 结果为 16-bit |
| Transfer Count | 64 | 采集 64 个样本 |
| Transfer Mode | Single | 完成后停止 |
| Interrupt | Enable | 传输完成中断 |
定时器配置(采样触发源):
配置一个定时器以固定频率产生事件触发 ADC 采样,例如 1kHz 采样率:
| 参数 | 设置值 |
|---|---|
| Timer Module | TIMG12 |
| Timer Mode | Periodic |
| Clock Prescaler | 32 |
| Period | 1000 |
| Event Publisher | Enable |
保存并编译。
11.4.2 实验代码
#include "ti_msp_dl_config.h"
#include <stdio.h>
#define ADC_SAMPLE_COUNT 64
// DMA 目标缓冲区
volatile uint16_t adc_buffer[ADC_SAMPLE_COUNT];
volatile uint8_t dma_done = 0;
void uart_send_string(const char *str)
{
while (*str) {
while (DL_UART_Main_isBusy(UART_0_INST));
DL_UART_Main_transmitData(UART_0_INST, *str++);
}
}
int main(void)
{
SYSCFG_DL_init();
// 配置 DMA 源地址(ADC 数据寄存器)和目标地址(缓冲区)
DL_DMA_setSrcAddr(DMA, DMA_CH0_CHAN_ID,
(uint32_t)DL_ADC12_getMemResultAddress(ADC12_0_INST, DL_ADC12_MEM_IDX_0));
DL_DMA_setDestAddr(DMA, DMA_CH0_CHAN_ID, (uint32_t)&adc_buffer[0]);
DL_DMA_setTransferSize(DMA, DMA_CH0_CHAN_ID, ADC_SAMPLE_COUNT);
// 使能 DMA 通道和中断
DL_DMA_enableChannel(DMA, DMA_CH0_CHAN_ID);
NVIC_EnableIRQ(DMA_INT_IRQN);
// 使能 ADC 和 DMA 触发
DL_ADC12_enableConversions(ADC12_0_INST);
DL_ADC12_enableDMA(ADC12_0_INST);
// 启动定时器(开始触发 ADC 采样)
DL_TimerG_startCounter(TIMER_TRIGGER_INST);
char buf[64];
while (1) {
if (dma_done) {
dma_done = 0;
// 停止采集
DL_TimerG_stopCounter(TIMER_TRIGGER_INST);
// 处理 64 个采样数据
uint32_t sum = 0;
uint16_t max_val = 0, min_val = 4095;
for (int i = 0; i < ADC_SAMPLE_COUNT; i++) {
sum += adc_buffer[i];
if (adc_buffer[i] > max_val) max_val = adc_buffer[i];
if (adc_buffer[i] < min_val) min_val = adc_buffer[i];
}
uint16_t avg = sum / ADC_SAMPLE_COUNT;
uint32_t avg_mv = (uint32_t)avg * 3300 / 4095;
sprintf(buf, "Avg: %lu mV, Min: %u, Max: %u\r\n",
avg_mv, min_val, max_val);
uart_send_string(buf);
// 重新配置 DMA 并启动下一轮采集
DL_DMA_setDestAddr(DMA, DMA_CH0_CHAN_ID, (uint32_t)&adc_buffer[0]);
DL_DMA_setTransferSize(DMA, DMA_CH0_CHAN_ID, ADC_SAMPLE_COUNT);
DL_DMA_enableChannel(DMA, DMA_CH0_CHAN_ID);
DL_TimerG_startCounter(TIMER_TRIGGER_INST);
delay_cycles(32000000); // 1 秒后处理下一批
}
__WFI();
}
}
// DMA 传输完成中断
void DMA_IRQHandler(void)
{
switch (DL_DMA_getPendingInterrupt(DMA)) {
case DL_DMA_EVENT_IIDX_DMACH0:
dma_done = 1;
break;
default:
break;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
代码工作流程
- 定时器以 1kHz 频率产生事件,每次触发 ADC 进行一次转换
- ADC 转换完成后自动触发 DMA 搬运一个 16-bit 数据到
adc_buffer - DMA 搬运 64 次后触发完成中断
- 中断中设置标志位,主循环计算平均值/最大值/最小值并串口输出
- 整个采集过程中 CPU 完全空闲
64 个采样点 × 1kHz = 64ms 采集时间,期间零 CPU 介入。
11.5 DMA 适用场景
| 场景 | 不用 DMA | 用 DMA |
|---|---|---|
| ADC 高速采样(10kHz+) | CPU 每秒处理上万次中断 | CPU 空闲,DMA 自动搬运 |
| UART 大量数据发送 | CPU 逐字节轮询等待 | DMA 自动发送整个缓冲区 |
| SPI Flash 读取大块数据 | CPU 每字节参与 | DMA 批量传输 |
| 音频播放(DAC 输出) | CPU 每采样点更新 DAC | DMA 自动从缓冲区馈送 DAC |
| WS2812 驱动 | CPU 精确计时每个 bit | PWM+DMA 自动输出完整波形 |
何时使用 DMA
- 数据量大且频率高的外设传输
- 需要 CPU 同时处理其他任务
- 对实时性有要求(DMA 响应比中断更快)
- 连续数据流场景(音频、波形采集)
对于少量低频数据(如每秒读一次温度),中断方式足够,无需 DMA。
11.6 使用注意事项
注意
volatile 修饰:DMA 写入的缓冲区必须声明为
volatile,防止编译器优化掉对缓冲区的读取操作。缓冲区大小:DMA 传输长度不能超过目标数组的容量,否则会越界写入覆盖其他变量。
数据一致性:DMA 传输进行中不要访问目标缓冲区。通过完成中断或标志位确保传输结束后再读取数据。
重新启动:单次模式下 DMA 完成后通道自动禁用。重新采集前需重新设置目标地址和传输长度,并重新使能通道。
地址对齐:16-bit 传输的缓冲区建议 2 字节对齐,32-bit 传输建议 4 字节对齐。
中断优先级:DMA 完成中断优先级不宜过低,否则可能被其他中断长时间阻塞,导致下一轮 DMA 启动不及时。
11.7 知识总结
| 内容 | 说明 |
|---|---|
| DMA 功能 | 外设与内存之间自动数据搬运,CPU 不参与 |
| 核心优势 | 释放 CPU 算力,提高系统并发能力 |
| 触发方式 | 外设事件触发(ADC完成、UART收发等) |
| 传输模式 | 单次(完成后停止)/ 循环(自动重复) |
| 配置要素 | 源/目标地址、传输长度、触发源、地址递增 |
| 典型搭配 | ADC+DMA、UART+DMA、SPI+DMA、DAC+DMA |
DMA 是构建高效嵌入式系统的关键。下一章将学习 I2C 通信协议,驱动天巧星板载的 OLED 显示屏和姿态传感器。