1. STM32 DMA实战全攻略:从原理到工程落地
作为一名在嵌入式领域摸爬滚打多年的工程师,我深知DMA(直接内存访问)对于提升系统性能的重要性。今天我将分享基于STM32F103的DMA实战经验,涵盖ADC采集、串口收发和内存复制三大核心场景。这些代码都是我在实际项目中反复验证过的,可以直接移植到你的项目中。
2. 硬件环境搭建
2.1 硬件清单与连接
在开始之前,我们需要准备以下硬件组件:
- STM32F103C8T6最小系统板(蓝色药丸板)
- 光敏电阻模块(用于ADC采集)
- USB-TTL转换模块(CH340G)
- LED指示灯(用于状态显示)
- 10KΩ下拉电阻(光敏电阻分压用)
硬件连接关系如下表所示:
| 模块 | 引脚 | STM32引脚 | 功能说明 |
|---|---|---|---|
| 光敏电阻 | VCC | 3.3V | 传感器供电 |
| 光敏电阻 | GND | GND | 共地 |
| 光敏电阻 | AO | PA5 | ADC1通道5输入 |
| USB-TTL | TX | PA10 | USART1_RX |
| USB-TTL | RX | PA9 | USART1_TX |
| USB-TTL | GND | GND | 共地 |
| LED | 正极 | PB0 | 通过1KΩ限流电阻 |
| LED | 负极 | GND | 接地 |
2.2 开发环境配置
我推荐使用以下开发环境:
- Keil MDK-ARM 5.xx
- STM32标准外设库V3.5.0
- ST-Link V2调试器
- 串口调试助手(如SecureCRT或Putty)
在Keil中新建工程时,需要添加以下关键文件:
- startup_stm32f10x_md.s(启动文件)
- stm32f10x.h(核心头文件)
- system_stm32f10x.c(系统时钟配置)
- stm32f10x_rcc.c(时钟控制)
- stm32f10x_gpio.c(GPIO控制)
- stm32f10x_usart.c(串口驱动)
- stm32f10x_adc.c(ADC驱动)
- stm32f10x_dma.c(DMA驱动)
3. ADC+DMA循环采集实现
3.1 初始化配置详解
ADC与DMA的协同工作需要精心配置多个寄存器。以下是关键配置步骤:
c复制void ADC_DMA_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
ADC_InitTypeDef ADC_InitStruct;
DMA_InitTypeDef DMA_InitStruct;
// 1. 使能相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2. 配置PA5为模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. ADC时钟配置(72MHz/6=12MHz)
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 4. ADC基本参数配置
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStruct.ADC_NbrOfChannel = 1; // 1个转换通道
ADC_InitStruct.ADC_ScanConvMode = DISABLE; // 非扫描模式
ADC_Init(ADC1, &ADC_InitStruct);
// 5. 配置ADC通道5(PA5),采样时间28.5周期
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_28Cycles5);
// 6. DMA配置(关键部分)
DMA_InitStruct.DMA_Channel = DMA_Channel_1; // ADC1对应通道1
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设到内存
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_dma_buf; // 目标地址
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 源地址
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址自增
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 16位数据
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; // 中等优先级
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; // 非内存到内存模式
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
// 7. 使能DMA传输完成中断
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
// 8. NVIC中断配置
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 9. 使能ADC的DMA请求
ADC_DMACmd(ADC1, ENABLE);
// 10. 使能DMA通道
DMA_Cmd(DMA1_Channel1, ENABLE);
// 11. ADC校准与启动
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
3.2 数据处理与优化技巧
在循环模式下,DMA会不断将ADC采样值写入缓冲区。我们需要在中断中处理数据:
c复制void DMA1_Channel1_IRQHandler(void) {
if(DMA_GetITStatus(DMA1_IT_TC1) != RESET) {
adc_dma_flag = 1; // 设置标志位
DMA_ClearITPendingBit(DMA1_IT_TC1); // 清除中断标志
}
}
void ADC_DMA_DataProcess(void) {
if(adc_dma_flag) {
adc_dma_flag = 0;
// 计算平均值
uint32_t sum = 0;
for(uint16_t i=0; i<ADC_DMA_BUF_LEN; i++) {
sum += adc_dma_buf[i];
}
uint16_t avg = sum / ADC_DMA_BUF_LEN;
// 转换为电压值(0-3.3V)
float voltage = (avg / 4096.0f) * 3.3f;
// 串口输出结果
printf("ADC平均值: %d (%.2fV)\n", avg, voltage);
}
}
优化技巧:
- 使用移动平均滤波:维护一个滑动窗口,减少数据波动
- 动态调整采样率:根据环境变化调整采样频率
- 低功耗设计:在两次采样间让CPU进入睡眠模式
4. 串口DMA收发实现
4.1 双通道DMA配置
串口DMA需要同时配置发送和接收通道:
c复制void USART_DMA_Init(void) {
// ...(串口初始化部分省略)
// DMA接收配置(USART1_RX -> DMA1_Channel5)
DMA_InitStruct.DMA_Channel = DMA_Channel_5;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)usart_dma_rx_buf;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStruct);
// DMA发送配置(USART1_TX <- DMA1_Channel4)
DMA_InitStruct.DMA_Channel = DMA_Channel_4;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)usart_dma_tx_buf;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel4, &DMA_InitStruct);
// 使能串口DMA请求
USART_DMACmd(USART1, USART_DMAReq_Rx | USART_DMAReq_Tx, ENABLE);
// 配置接收完成中断
DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE);
// 启动接收DMA
DMA_Cmd(DMA1_Channel5, ENABLE);
USART_Cmd(USART1, ENABLE);
}
4.2 大数据传输优化
对于大数据传输,可以采用以下策略:
- 双缓冲技术:维护两个接收缓冲区,当一个缓冲区满时切换到另一个,同时处理已满的数据
- 流控制:当缓冲区快满时,通过硬件流控(RTS/CTS)通知发送方暂停
- 数据分包:将大数据分成固定大小的包,每个包添加校验信息
c复制// 双缓冲实现示例
#define BUF_SIZE 512
uint8_t rx_buf1[BUF_SIZE], rx_buf2[BUF_SIZE];
uint8_t *current_buf = rx_buf1;
void DMA1_Channel5_IRQHandler(void) {
if(DMA_GetITStatus(DMA1_IT_TC5)) {
// 切换缓冲区
if(current_buf == rx_buf1) {
current_buf = rx_buf2;
} else {
current_buf = rx_buf1;
}
// 重新配置DMA
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5, BUF_SIZE);
DMA_SetMemoryBaseAddr(DMA1_Channel5, (uint32_t)current_buf);
DMA_Cmd(DMA1_Channel5, ENABLE);
// 处理已满的缓冲区
process_rx_data(current_buf == rx_buf1 ? rx_buf2 : rx_buf1);
DMA_ClearITPendingBit(DMA1_IT_TC5);
}
}
5. 内存到内存DMA复制
5.1 性能对比测试
内存复制是验证DMA性能的最佳场景。我们设计了一个对比测试:
c复制void MEM_DMA_CopyCompare(void) {
uint32_t start, end;
// CPU复制测试
start = Timer_Start();
memcpy(mem_dma_dst, mem_dma_src, MEM_DMA_BUF_LEN);
uint32_t cpu_time = Timer_Stop(start);
// DMA复制测试
start = Timer_Start();
DMA_SetCurrDataCounter(DMA1_Channel1, MEM_DMA_BUF_LEN);
DMA_Cmd(DMA1_Channel1, ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
uint32_t dma_time = Timer_Stop(start);
// 验证结果
bool cpu_ok = memcmp(mem_dma_dst, mem_dma_src, MEM_DMA_BUF_LEN) == 0;
bool dma_ok = memcmp(mem_dma_dst, mem_dma_src, MEM_DMA_BUF_LEN) == 0;
printf("测试结果(%d字节):\n", MEM_DMA_BUF_LEN);
printf("CPU复制: %s, 耗时: %dus\n", cpu_ok?"成功":"失败", cpu_time);
printf("DMA复制: %s, 耗时: %dus\n", dma_ok?"成功":"失败", dma_time);
printf("性能提升: %.1f倍\n", (float)cpu_time/dma_time);
}
5.2 内存DMA配置要点
内存到内存DMA需要特别注意以下配置:
c复制void MEM_DMA_Init(void) {
DMA_InitTypeDef DMA_InitStruct;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStruct.DMA_Channel = DMA_Channel_1;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST; // 内存到内存
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)mem_dma_dst;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)mem_dma_src;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_Highest;
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable; // 关键配置
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
}
性能优化建议:
- 对齐内存地址:确保源和目标地址都是4字节对齐的
- 使用最大数据宽度:如果数据允许,使用32位传输
- 合理设置突发传输:通过DMA_SetBurstMode配置
6. 系统整合与主循环设计
6.1 主函数实现
将三个功能模块整合到主函数中:
c复制int main(void) {
// 系统时钟初始化
SystemInit();
// 初始化SysTick定时器(用于计时)
SysTick_Config(SystemCoreClock / 1000000); // 1us分辨率
// 初始化串口(用于调试输出)
USART1_Init();
printf("系统启动...\n");
// 初始化内存DMA(用于性能测试)
MEM_DMA_Init();
MEM_DMA_CopyCompare();
// 初始化ADC+DMA
ADC_DMA_Init();
// 初始化串口DMA
USART_DMA_Init();
// 主循环
while(1) {
static uint32_t tick = 0;
// 每500ms处理一次ADC数据
if(tick % 500 == 0) {
ADC_DMA_DataProcess();
}
// 处理串口接收数据
USART_DMA_DataProcess();
// 简单的延时
Delay_Us(1000); // 1ms
tick++;
}
}
6.2 任务调度策略
对于更复杂的系统,可以采用以下调度策略:
- 基于时间片的轮询:为每个任务分配固定的处理时间
- 事件驱动:通过中断标志触发任务执行
- 优先级调度:重要任务优先处理
c复制typedef struct {
uint32_t interval;
uint32_t last_run;
void (*task_func)(void);
} Task;
Task tasks[] = {
{500, 0, ADC_DMA_DataProcess}, // 每500ms执行
{100, 0, USART_DMA_DataProcess}, // 每100ms执行
{1000, 0, SystemMonitor} // 每1s执行
};
void Scheduler_Run(void) {
uint32_t now = Get_System_Tick();
for(int i=0; i<sizeof(tasks)/sizeof(Task); i++) {
if(now - tasks[i].last_run >= tasks[i].interval) {
tasks[i].task_func();
tasks[i].last_run = now;
}
}
}
7. 常见问题与解决方案
7.1 DMA传输失败排查步骤
当DMA不工作时,可以按照以下步骤排查:
- 检查时钟:确认相关外设时钟已使能(DMA、ADC/USART等)
- 验证通道映射:确保DMA通道与外设匹配(参考参考手册)
- 检查地址设置:源和目标地址必须有效且可访问
- 确认传输方向:DMA_DIR配置是否正确
- 检查中断配置:NVIC和DMA中断是否使能
- 验证缓冲区大小:DMA_SetCurrDataCounter设置的值是否正确
7.2 性能优化技巧
- 使用循环缓冲:减少DMA重新配置的开销
- 合理设置优先级:高带宽外设使用高优先级
- 内存对齐:4字节对齐可提高传输效率
- 数据打包:将多个小数据打包传输
- 双缓冲技术:避免处理数据时的传输停顿
8. 进阶应用方向
掌握了基础DMA应用后,可以尝试以下进阶方向:
- DMA+DAC波形生成:实现高速波形输出
- 多通道ADC扫描:同时采集多个传感器数据
- SPI Flash读写:加速外部存储器访问
- 图像处理:用于摄像头数据搬运
- 音频处理:实现实时音频采集与播放
c复制// DAC波形生成示例
void DAC_DMA_Init(void) {
// 配置DAC
DAC_InitTypeDef DAC_InitStruct;
DAC_InitStruct.DAC_Trigger = DAC_Trigger_T6_TRGO; // 定时器触发
DAC_InitStruct.DAC_WaveGeneration = DAC_WaveGeneration_None;
DAC_InitStruct.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
DAC_Init(DAC_Channel_1, &DAC_InitStruct);
DAC_Cmd(DAC_Channel_1, ENABLE);
// 配置DMA
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_Channel = DMA_Channel_3;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)waveform_data;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel3, &DMA_InitStruct);
// 配置触发定时器
TIM_TimeBaseInitTypeDef TIM_InitStruct;
TIM_InitStruct.TIM_Period = 100-1; // 100个采样点
TIM_InitStruct.TIM_Prescaler = 72-1; // 1MHz计数频率
TIM_InitStruct.TIM_ClockDivision = 0;
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_Init(TIM6, &TIM_InitStruct);
TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update);
TIM_Cmd(TIM6, ENABLE);
// 启动DMA
DMA_Cmd(DMA1_Channel3, ENABLE);
}
9. 工程实践建议
在实际项目中应用DMA时,建议:
- 文档记录:详细记录每个DMA通道的用途和配置
- 资源规划:提前规划DMA通道和外设的使用
- 错误处理:添加DMA错误中断处理
- 性能监控:定期检查DMA传输效率
- 代码模块化:将DMA相关代码封装成独立模块
通过本教程,你应该已经掌握了STM32 DMA的核心应用技术。在实际项目中,DMA可以显著提升系统性能,特别是在需要高速数据传输的场景。建议从简单应用开始,逐步尝试更复杂的DMA应用场景。
