DMA(直接内存访问)¶
DMA(Direct Memory Access)允许外设与内存之间直接传输数据,不需要 CPU 参与。这就像给 CPU 请了一个"搬运工",CPU 可以去做更重要的事情,数据搬运由 DMA 自动完成。
一、为什么需要 DMA?¶
没有 DMA 时(CPU 搬运数据)¶
假设我们要用 ADC 连续采集 1000 个数据:
for (int i = 0; i < 1000; i++)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // CPU 在这里等!
buffer[i] = HAL_ADC_GetValue(&hadc1); // CPU 搬运数据
}
问题:CPU 大部分时间在等待和搬运数据,无法做其他事情。
有 DMA 时¶
// 配置一次,然后启动
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buffer, 1000);
// ADC 每转换完成一次,DMA 自动把数据从 ADC->DR 搬到 buffer[i]
// CPU 完全不参与!可以去处理其他任务
graph LR
subgraph 无 DMA
CPU1[CPU] --> |"1.启动ADC"| ADC1[ADC]
ADC1 --> |"2.等待完成"| CPU1
CPU1 --> |"3.读数据"| ADC1
CPU1 --> |"4.写入内存"| MEM1[内存]
end
subgraph 有 DMA
CPU2[CPU 做其他事] -.-> |"只需初始配置"| DMA2[DMA]
ADC2[ADC] --> |"自动搬运"| DMA2
DMA2 --> |"自动写入"| MEM2[内存]
end
二、DMA 基本概念¶
DMA 控制器概览¶
STM32F103 有 2 个 DMA 控制器:
| DMA 控制器 | 通道数 | 总线 | 服务的外设 |
|---|---|---|---|
| DMA1 | 7 个通道 | AHB | ADC1, USART1/2/3, SPI1/2, I2C1/2, TIM1/2/3/4 |
| DMA2 | 5 个通道 | AHB | ADC3, USART4, SPI3, TIM5/6/7/8(仅大容量型号) |
通道 ≠ 可以随意选择
每个外设对应固定的 DMA 通道。例如 ADC1 只能用 DMA1 通道 1,USART1 TX 只能用 DMA1 通道 4。需要查阅数据手册确认。
DMA1 通道对应表(常用)¶
| 通道 | 外设请求源 |
|---|---|
| CH1 | ADC1, TIM2_CH3, TIM4_CH1 |
| CH2 | USART3_TX, TIM1_CH1, TIM2_UP, SPI1_RX |
| CH3 | USART3_RX, TIM1_CH2, TIM3_CH4, SPI1_TX |
| CH4 | USART1_TX, TIM1_CH4, TIM4_CH2, SPI2_RX, I2C2_TX |
| CH5 | USART1_RX, TIM1_UP, TIM2_CH1, SPI2_TX, I2C2_RX |
| CH6 | USART2_RX, TIM1_CH3, TIM3_CH1, I2C1_TX |
| CH7 | USART2_TX, TIM2_CH2, TIM4_CH3, I2C1_RX |
三种传输方向¶
| 方向 | 说明 | 典型应用 |
|---|---|---|
| 外设 → 内存 | 从外设数据寄存器读取,写入内存数组 | ADC 采集、USART 接收 |
| 内存 → 外设 | 从内存数组读取,写入外设数据寄存器 | USART 发送、DAC 输出 |
| 内存 → 内存 | 在两个内存地址之间拷贝数据 | 数据缓冲区复制 |
三、CubeMX 中配置 DMA¶
在 HAL + CubeMX 工作流中,DMA 的配置非常简单——在外设配置页面的 DMA Settings 标签页中添加 DMA 通道即可。CubeMX 会自动选择正确的通道并生成所有初始化代码。
关键配置参数¶
在 CubeMX 的 DMA Settings 中,需要设置以下参数:
| 参数 | 含义 | 常用设置 |
|---|---|---|
| Direction | 传输方向 | Peripheral To Memory / Memory To Peripheral |
| Mode | 工作模式 | Normal(单次)/ Circular(循环) |
| Increment Address - Peripheral | 外设地址是否递增 | 通常不勾选(外设寄存器地址固定) |
| Increment Address - Memory | 内存地址是否递增 | 通常勾选(依次写入数组) |
| Data Width - Peripheral | 外设数据宽度 | Byte / Half Word / Word |
| Data Width - Memory | 内存数据宽度 | 与外设数据宽度匹配 |
| Priority | 优先级 | Low / Medium / High / Very High |
HAL 中的 DMA 使用特点
使用 HAL 库时,你通常不需要直接操作 DMA 寄存器或函数。外设的 HAL 函数(如 HAL_ADC_Start_DMA()、HAL_UART_Transmit_DMA())会自动管理 DMA 的启动、传输和回调。
工作模式¶
四、DMA 编程实战(CubeMX + HAL)¶
示例 1:DMA 搬运 ADC 数据¶
连续采集 ADC 通道 0 的数据,存入缓冲区(不占用 CPU):
CubeMX 配置:
- Analog → ADC1 使能 IN0(PA0),Continuous Conversion = Enabled
- DMA Settings → Add → ADC1
- Direction: Peripheral To Memory
- Mode: Circular
- Data Width: Half Word(ADC 是 12 位,16 位存储)
- 生成代码
/* main.c */
#define ADC_BUFFER_SIZE 100
uint16_t adc_buffer[ADC_BUFFER_SIZE];
/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1); // 校准
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE); // 启动 ADC + DMA
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
// adc_buffer 会被 DMA 自动持续更新
// 直接读取即可,CPU 完全不需要参与数据搬运
uint32_t sum = 0;
for (int i = 0; i < ADC_BUFFER_SIZE; i++)
sum += adc_buffer[i];
uint16_t avg = sum / ADC_BUFFER_SIZE;
printf("ADC avg = %d\r\n", avg);
HAL_Delay(500);
/* USER CODE END WHILE */
}
示例 2:DMA 发送串口数据¶
用 DMA 发送一串字符,CPU 启动后就可以做别的事情:
CubeMX 配置:
- Connectivity → USART1 配置好波特率
- DMA Settings → Add → USART1_TX
- Direction: Memory To Peripheral
- Mode: Normal
- Data Width: Byte
- 生成代码
/* main.c */
char tx_buffer[] = "Hello DMA!\r\n";
/* USER CODE BEGIN WHILE */
while (1)
{
// DMA 方式发送,函数立即返回,CPU 不阻塞
HAL_UART_Transmit_DMA(&huart1, (uint8_t*)tx_buffer, strlen(tx_buffer));
HAL_Delay(1000);
/* USER CODE END WHILE */
}
/* USER CODE BEGIN 4 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
// DMA 发送完成回调(可选)
// 可以在这里设置标志位或启动下一次发送
}
/* USER CODE END 4 */
DMA 发送注意事项
DMA 发送期间,不能修改发送缓冲区的数据!因为 DMA 还在读取这块内存。等发送完成回调触发后才能安全修改。
示例 3:内存到内存拷贝¶
HAL 库也支持纯内存拷贝,但使用场景较少(通常用 memcpy 更简单)。如果需要,可以直接使用 HAL DMA 函数:
uint32_t src_buffer[100] = { /* 源数据 */ };
uint32_t dst_buffer[100];
/* 在 CubeMX 中配置一个 Memory To Memory 的 DMA 通道 */
/* 或者直接使用 HAL_DMA_Start() */
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src_buffer, (uint32_t)dst_buffer, 100);
HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);
五、DMA 回调函数¶
HAL 库为 DMA 提供了回调机制,通过外设的回调函数通知 CPU:
| 回调函数 | 触发时机 | 使用场景 |
|---|---|---|
HAL_ADC_ConvCpltCallback() |
ADC DMA 全部传输完成 | 处理采集完成的数据 |
HAL_ADC_ConvHalfCpltCallback() |
ADC DMA 传输到一半 | 双缓冲处理 |
HAL_UART_TxCpltCallback() |
UART DMA 发送完成 | 发送下一包数据 |
HAL_UART_RxCpltCallback() |
UART DMA 接收完成 | 处理接收数据 |
/* 以 ADC DMA 为例 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
// DMA 传输完成,数据已在 adc_buffer 中
// 在这里处理数据或设置标志位
data_ready = 1;
}
}
双缓冲技巧
利用 HalfCpltCallback 和 CpltCallback,可以实现双缓冲:
- 前半段数据传输完成 → CPU 处理前半段,DMA 继续传输后半段
- 后半段数据传输完成 → CPU 处理后半段,DMA 重新传输前半段
这样 CPU 和 DMA 交替工作,不会互相等待。
六、常见问题¶
DMA 传输完成后数据不对?
- 检查数据宽度:外设和内存的 DataSize 必须匹配(如 ADC 是 16 位,要用 HalfWord)
- 检查地址递增:外设地址通常不递增,内存地址通常递增
- 检查 BufferSize:是传输的数据个数,不是字节数
- 检查传输方向:
PeripheralSRC和PeripheralDST不要搞反
DMA 只能传输一次?
检查是否设置为 Normal 模式。Normal 模式下传输完成后停止,如果要持续传输需使用 Circular 模式,或在传输完成中断中重新使能 DMA。
何时使用 DMA?
- 大块数据传输:ADC 多通道连续采集、大量串口数据收发
- CPU 资源紧张:需要 CPU 做复杂计算时,把数据搬运交给 DMA
- 高速传输:DMA 的传输速度通常比 CPU 轮询更快
- 不适合 DMA 的场景:偶尔传输一两个字节(配置 DMA 的开销大于直接搬运)