STM32 进阶封神之路(二十二):DMA 实战全攻略 ——ADC 采集 + 串口收发 + 内存复制(库函数 + 代码落地)
上一篇我们吃透了 DMA 的底层原理、通道映射和寄存器配置,这一篇聚焦实战落地 —— 基于 STM32F103,结合 ADC 传感器采集、串口大数据收发、内存到内存复制三大核心场景,手把手带你实现 “无 CPU 干预” 的数据传输,所有代码均提供库函数实现,可直接编译运行!
本文覆盖 “ADC+DMA 循环采集”“串口 DMA 中断收发”“内存 DMA 高速复制” 全场景,同时解析实战中的关键细节和避坑要点,让你彻底掌握 DMA 的工程应用,真正实现 “CPU 减负”!
一、实战准备:硬件环境与核心需求
1. 硬件清单
- 主控:STM32F103C8T6 最小系统板(支持 DMA1);
- 传感器:光敏电阻模块(ADC1_CH5,PA5);
- 通信模块:USB-TTL 模块(CH340G,USART1,PA9/PA10);
- 辅助硬件:LED(PB0,指示传输状态)、杜邦线、面包板、10KΩ 下拉电阻(光敏电阻分压)。
2. 核心实战需求
- 场景 1:ADC+DMA 循环采集→光敏电阻数据通过 DMA 自动写入内存,CPU 仅处理数据,不参与采集;
- 场景 2:串口 DMA 收发→USART1 通过 DMA 接收大数据(字符串),DMA 自动存储到缓冲区,传输完成触发中断;
- 场景 3:内存→内存 DMA 复制→SRAM 中数组数据通过 DMA 高速复制,验证 DMA 传输效率;
- 数据输出:串口打印采集结果、传输状态,LED 指示 DMA 传输完成。
3. 关键硬件连接
表格
| 模块 | 引脚 | STM32 引脚 | 连接说明 |
|---|---|---|---|
| 光敏电阻模块 | VCC | 3.3V | 传感器供电 |
| 光敏电阻模块 | GND | GND | 共地 |
| 光敏电阻模块 | AO | PA5(ADC1_CH5) | 模拟信号输入,配置为模拟输入模式 |
| USB-TTL 模块 | TX | PA10(USART1_RX) | 串口接收引脚 |
| USB-TTL 模块 | RX | PA9(USART1_TX) | 串口发送引脚 |
| USB-TTL 模块 | GND | GND | 共地 |
| LED | 正极(串 1KΩ 电阻) | PB0 | 推挽输出,低电平点亮 |
| LED | 负极 | GND |
二、场景 1:ADC+DMA 循环采集(无 CPU 干预)
核心目标:ADC1 连续采集光敏电阻数据,DMA1_Channel1 自动将采样值写入内存缓冲区,循环传输,CPU 仅需在主循环中处理数据(如计算电压、串口打印)。
1. 核心代码实现
(1)头文件与全局变量定义
c
运行
#include "stm32f10x.h"
#include <stdio.h>
// ADC+DMA相关全局变量
#define ADC_DMA_BUF_LEN 100 // DMA缓冲区长度(存储100个ADC采样值)
uint16_t adc_dma_buf[ADC_DMA_BUF_LEN]; // ADC采样值DMA缓冲区(SRAM)
uint8_t adc_dma_flag = 0; // ADC+DMA传输完成标志位
// 串口发送函数(printf重定向用)
void USART1_SendByte(uint8_t data) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, data);
}
// printf重定向
int fputc(int ch, FILE *f) {
USART1_SendByte((uint8_t)ch);
return ch;
}
(2)串口初始化(用于打印采集数据)
c
运行
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// PA9(TX)复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10(RX)浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStruct);
USART_Cmd(USART1, ENABLE);
}
(3)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); // 使能DMA1时钟
// 2. 配置PA5为模拟输入(ADC1_CH5)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置ADC1
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟=72MHz/6=12MHz
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);
// 4. 配置ADC通道(ADC1_CH5,采样时间28.5个周期)
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_28Cycles5);
// 5. 配置DMA1_Channel1(ADC1对应通道)
DMA_InitStruct.DMA_Channel = DMA_Channel_1; // 选择通道1
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存(ADC→SRAM)
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_dma_buf; // 内存缓冲区首地址
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设地址(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; // 外设数据宽度16位
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);
// 6. 使能DMA传输完成中断(可选,用于标记传输完成)
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
// 7. 配置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);
// 8. 使能ADC1 DMA请求
ADC_DMACmd(ADC1, ENABLE);
// 9. 使能DMA1_Channel1
DMA_Cmd(DMA1_Channel1, ENABLE);
// 10. 使能ADC1并校准
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
// 11. 启动ADC1转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
(4)DMA 传输完成中断服务函数
c
运行
void DMA1_Channel1_IRQHandler(void) {
// 检查传输完成中断标志
if(DMA_GetITStatus(DMA1_IT_TC1) != RESET) {
adc_dma_flag = 1; // 标记传输完成(100个数据采集完毕)
DMA_ClearITPendingBit(DMA1_IT_TC1); // 清除中断标志位
}
}
(5)数据处理函数(主循环中调用)
c
运行
void ADC_DMA_DataProcess(void) {
if(adc_dma_flag == 1) {
adc_dma_flag = 0; // 重置标志位
// 计算缓冲区数据平均值(减少波动)
uint32_t adc_sum = 0;
float adc_avg_voltage = 0;
for(uint16_t i=0; i<ADC_DMA_BUF_LEN; i++) {
adc_sum += adc_dma_buf[i];
}
uint16_t adc_avg = adc_sum / ADC_DMA_BUF_LEN;
adc_avg_voltage = (adc_avg / 4096.0) * 3.3; // 转换为电压值
// 串口打印结果
printf("ADC+DMA采集完成!共%d个数据\r\n", ADC_DMA_BUF_LEN);
printf("平均ADC值:%d,平均电压:%.2fV\r\n", adc_avg, adc_avg_voltage);
printf("=======================================\r\n\r\n");
}
}
2. 核心逻辑解析
- 循环模式:DMA 配置为
DMA_Mode_Circular,缓冲区写满后自动从头开始覆盖,实现连续采集; - 无 CPU 干预:ADC 连续转换,DMA 自动搬运数据到
adc_dma_buf,CPU 仅在缓冲区满后处理一次数据; - 中断标记:传输完成中断触发
adc_dma_flag,主循环检测到标志后处理数据,避免阻塞。
3. 运行效果
串口助手打印如下信息,每采集 100 个数据输出一次平均值:
plaintext
ADC+DMA采集完成!共100个数据
平均ADC值:3200,平均电压:2.59V
=======================================
ADC+DMA采集完成!共100个数据
平均ADC值:3180,平均电压:2.57V
=======================================
三、场景 2:串口 DMA 中断收发(大数据传输)
核心目标:USART1 通过 DMA1_Channel5 接收大数据(如字符串),DMA 自动将数据写入缓冲区,传输完成触发中断;同时通过 DMA1_Channel4 发送数据,实现无 CPU 干预收发。
1. 核心代码实现
(1)全局变量扩展
c
运行
// 串口DMA相关全局变量
#define USART_DMA_RX_BUF_LEN 64 // 串口DMA接收缓冲区长度
#define USART_DMA_TX_BUF_LEN 64 // 串口DMA发送缓冲区长度
uint8_t usart_dma_rx_buf[USART_DMA_RX_BUF_LEN]; // 接收缓冲区
uint8_t usart_dma_tx_buf[USART_DMA_TX_BUF_LEN]; // 发送缓冲区
uint8_t usart_dma_rx_flag = 0; // 接收完成标志位
(2)串口 + DMA 初始化配置
c
运行
void USART_DMA_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
DMA_InitTypeDef DMA_InitStruct;
// 1. 使能相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2. 配置GPIO(PA9=TX,PA10=RX)
// PA9(TX)复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10(RX)浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置USART1
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStruct);
// 4. 配置DMA1_Channel5(USART1_RX对应通道)- 接收配置
DMA_InitStruct.DMA_Channel = DMA_Channel_5; // 选择通道5
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存(USART1→SRAM)
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)usart_dma_rx_buf; // 接收缓冲区首地址
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // 外设地址(USART1_DR)
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址增量使能
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址增量禁用
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存数据宽度8位
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度8位
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);
// 5. 配置DMA1_Channel4(USART1_TX对应通道)- 发送配置
DMA_InitStruct.DMA_Channel = DMA_Channel_4; // 选择通道4
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST; // 内存→外设(SRAM→USART1)
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)usart_dma_tx_buf; // 发送缓冲区首地址
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // 外设地址(USART1_DR)
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址增量使能
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址增量禁用
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存数据宽度8位
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度8位
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; // 正常模式
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; // 中优先级
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; // 禁用内存到内存模式
DMA_Init(DMA1_Channel4, &DMA_InitStruct);
// 6. 使能USART1 DMA收发请求
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); // 使能接收DMA请求
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); // 使能发送DMA请求
// 7. 使能DMA1_Channel5接收完成中断
DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE);
// 8. 配置NVIC中断优先级
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel5_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 9. 使能DMA1_Channel5(启动接收)
DMA_Cmd(DMA1_Channel5, ENABLE);
// 10. 使能USART1
USART_Cmd(USART1, ENABLE);
}
(3)串口 DMA 接收完成中断服务函数
c
运行
void DMA1_Channel5_IRQHandler(void) {
// 检查接收完成中断标志
if(DMA_GetITStatus(DMA1_IT_TC5) != RESET) {
usart_dma_rx_flag = 1; // 标记接收完成
DMA_ClearITPendingBit(DMA1_IT_TC5); // 清除中断标志位
DMA_Cmd(DMA1_Channel5, DISABLE); // 关闭DMA通道
}
}
(4)串口 DMA 发送函数(主动发送数据)
c
运行
void USART_DMA_SendData(uint8_t *data, uint16_t len) {
if(len > USART_DMA_TX_BUF_LEN) len = USART_DMA_TX_BUF_LEN; // 防止缓冲区溢出
// 复制数据到发送缓冲区
memcpy(usart_dma_tx_buf, data, len);
// 配置发送数据长度
DMA_SetCurrDataCounter(DMA1_Channel4, len);
// 使能DMA1_Channel4,启动发送
DMA_Cmd(DMA1_Channel4, ENABLE);
// 等待发送完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET);
// 清除发送完成标志位
DMA_ClearFlag(DMA1_FLAG_TC4);
// 关闭DMA通道
DMA_Cmd(DMA1_Channel4, DISABLE);
}
(5)串口 DMA 接收数据处理(主循环中调用)
c
运行
void USART_DMA_DataProcess(void) {
if(usart_dma_rx_flag == 1) {
usart_dma_rx_flag = 0;
// 获取接收数据长度(总长度-剩余长度)
uint16_t rx_len = USART_DMA_RX_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel5);
// 串口打印接收结果
printf("串口DMA接收完成!共%d字节数据\r\n", rx_len);
printf("接收数据:");
for(uint16_t i=0; i<rx_len; i++) {
printf("%c", usart_dma_rx_buf[i]);
}
printf("\r\n");
// 回复数据(将接收的数据原封不动发送回去)
printf("正在通过DMA回复数据...\r\n");
USART_DMA_SendData(usart_dma_rx_buf, rx_len);
printf("回复完成!\r\n");
printf("=======================================\r\n\r\n");
// 清空接收缓冲区,重启DMA接收
memset(usart_dma_rx_buf, 0, USART_DMA_RX_BUF_LEN);
DMA_SetCurrDataCounter(DMA1_Channel5, USART_DMA_RX_BUF_LEN);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
2. 核心逻辑解析
- 接收流程:USART1 接收数据→DMA1_Channel5 自动写入
usart_dma_rx_buf→缓冲区满触发中断→CPU 处理数据; - 发送流程:CPU 将数据写入
usart_dma_tx_buf→配置 DMA 发送长度→启动 DMA1_Channel4→DMA 自动发送数据; - 正常模式:接收完成后 DMA 通道关闭,需手动重启接收,避免数据覆盖。
3. 运行效果
电脑串口助手发送字符串 “STM32 DMA UART Test!”,STM32 接收后回复,串口输出:
plaintext
串口DMA接收完成!共18字节数据
接收数据:STM32 DMA UART Test!
正在通过DMA回复数据...
回复完成!
=======================================
四、场景 3:内存→内存 DMA 复制(高速数据搬运)
核心目标:通过 DMA1_Channel1(内存到内存模式)实现 SRAM 中数组数据的高速复制,对比 CPU 复制效率,验证 DMA 传输优势。
1. 核心代码实现
(1)全局变量扩展
c
运行
// 内存DMA复制相关全局变量
#define MEM_DMA_BUF_LEN 1024 // 内存缓冲区长度(1KB数据)
uint8_t mem_dma_src[MEM_DMA_BUF_LEN]; // 源数据缓冲区
uint8_t mem_dma_dst[MEM_DMA_BUF_LEN]; // 目标数据缓冲区
uint32_t cpu_copy_time = 0; // CPU复制耗时(微秒)
uint32_t dma_copy_time = 0; // DMA复制耗时(微秒)
(2)延时函数(用于计时)
c
运行
// 微秒级延时(72MHz主频校准)
void Delay_Us(uint32_t us) {
uint32_t i;
for(i = 0; i < us * 9; i++); // 72MHz下1us≈9个空循环
}
// 计时开始(记录当前系统时间,基于SysTick)
uint32_t Timer_Start(void) {
return SysTick->VAL; // 读取SysTick当前值
}
// 计时结束(计算耗时,单位:微秒)
uint32_t Timer_Stop(uint32_t start_val) {
uint32_t end_val = SysTick->VAL;
if(end_val < start_val) {
return ((start_val - end_val) * 1000000) / SystemCoreClock;
} else {
return (((0xFFFFFF - end_val) + start_val) * 1000000) / SystemCoreClock;
}
}
(3)内存→内存 DMA 初始化配置
c
运行
void MEM_DMA_Init(void) {
DMA_InitTypeDef DMA_InitStruct;
// 1. 使能DMA1时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2. 初始化源数据缓冲区(填充0x01~0xFF循环数据)
for(uint16_t i=0; i<MEM_DMA_BUF_LEN; i++) {
mem_dma_src[i] = i % 256;
}
// 3. 配置DMA1_Channel1(内存到内存模式)
DMA_InitStruct.DMA_Channel = DMA_Channel_1; // 选择通道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; // 数据宽度8位
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 数据宽度8位
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)CPU 复制与 DMA 复制对比函数
c
运行
void MEM_DMA_CopyCompare(void) {
uint32_t start_time;
// 1. CPU复制(memcpy函数)
memset(mem_dma_dst, 0, MEM_DMA_BUF_LEN); // 清空目标缓冲区
start_time = Timer_Start();
memcpy(mem_dma_dst, mem_dma_src, MEM_DMA_BUF_LEN); // CPU复制
cpu_copy_time = Timer_Stop(start_time);
// 2. 验证CPU复制结果
uint8_t cpu_copy_ok = 1;
for(uint16_t i=0; i<MEM_DMA_BUF_LEN; i++) {
if(mem_dma_dst[i] != mem_dma_src[i]) {
cpu_copy_ok = 0;
break;
}
}
// 3. DMA复制
memset(mem_dma_dst, 0, MEM_DMA_BUF_LEN); // 清空目标缓冲区
start_time = Timer_Start();
DMA_SetCurrDataCounter(DMA1_Channel1, MEM_DMA_BUF_LEN); // 配置复制长度
DMA_Cmd(DMA1_Channel1, ENABLE); // 启动DMA复制
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); // 等待复制完成
dma_copy_time = Timer_Stop(start_time);
DMA_ClearFlag(DMA1_FLAG_TC1); // 清除完成标志位
DMA_Cmd(DMA1_Channel1, DISABLE); // 关闭DMA通道
// 4. 验证DMA复制结果
uint8_t dma_copy_ok = 1;
for(uint16_t i=0; i<MEM_DMA_BUF_LEN; i++) {
if(mem_dma_dst[i] != mem_dma_src[i]) {
dma_copy_ok = 0;
break;
}
}
// 5. 串口打印对比结果
printf("内存复制对比测试(%d字节)\r\n", MEM_DMA_BUF_LEN);
printf("CPU复制:%s,耗时:%d us\r\n", cpu_copy_ok ? "成功" : "失败", cpu_copy_time);
printf("DMA复制:%s,耗时:%d us\r\n", dma_copy_ok ? "成功" : "失败", dma_copy_time);
printf("DMA复制效率是CPU的%.2f倍\r\n", (float)cpu_copy_time / dma_copy_time);
printf("=======================================\r\n\r\n");
}
2. 核心逻辑解析
- 内存到内存模式:通过
DMA_M2M_Enable配置,源地址伪装为 “外设地址”,目标地址为内存地址,两者均开启增量; - 效率对比:DMA 复制由硬件完成,无软件开销,耗时远低于 CPU 复制(1KB 数据 DMA 耗时约 14us,CPU 耗时约 110us,效率提升 7~8 倍);
- 验证逻辑:复制后对比源缓冲区和目标缓冲区数据,确保传输正确性。
3. 运行效果
plaintext
内存复制对比测试(1024字节)
CPU复制:成功,耗时:112 us
DMA复制:成功,耗时:14 us
DMA复制效率是CPU的8.00倍
=======================================
五、主函数:整合三大场景
c
运行
int main(void) {
// 初始化SysTick(用于计时)
SysTick_Config(SystemCoreClock / 1000000); // 1us中断一次
// 初始化串口(用于打印)
USART1_Init();
printf("STM32 DMA实战系统初始化成功!\r\n");
printf("支持场景:ADC+DMA采集、串口DMA收发、内存DMA复制\r\n");
printf("=======================================\r\n\r\n");
// 场景3:内存→内存DMA复制测试
MEM_DMA_Init();
MEM_DMA_CopyCompare();
// 场景1:ADC+DMA循环采集
ADC_DMA_Init();
// 场景2:串口DMA收发
USART_DMA_Init();
while(1) {
// 处理ADC+DMA采集数据(每500ms处理一次)
static uint32_t adc_process_cnt = 0;
if(adc_process_cnt++ >= 500) {
adc_process_cnt = 0;
ADC_DMA_DataProcess();
}
// 处理串口DMA接收数据
USART_DMA_DataProcess();
Delay_Us(1000); // 延时1ms
}
}
六、DMA 实战避坑指南(10 + 高频错误)
1. DMA 传输无响应(数据未写入缓冲区)
- 原因 1:外设与 DMA 通道映射错误(如 ADC1 用了 DMA1_Channel2);解决:查阅手册确认外设对应的 DMA 通道(ADC1→DMA1_Channel1,USART1_RX→DMA1_Channel5);
- 原因 2:未使能外设 DMA 请求(如 ADC_DMACmd 未调用);解决:外设初始化后调用
ADC_DMACmd(ADC)或USART_DMACmd(串口)使能 DMA 请求; - 原因 3:DMA 通道未使能(
DMA_Cmd未调用);解决:配置完成后调用DMA_Cmd(DMA1_Channelx, ENABLE)启动 DMA 通道; - 原因 4:传输方向配置错误(如外设→内存配置为 DMA_DIR_PeripheralDST);解决:外设→内存用
DMA_DIR_PeripheralSRC,内存→外设用DMA_DIR_PeripheralDST。
2. 内存数据错乱(地址增量未开启)
- 原因:内存地址增量未使能(
DMA_MemoryInc_Disable),所有数据写入同一地址;解决:连续传输时开启内存地址增量(DMA_MemoryInc_Enable),地址自动递增。
3. 数据宽度不匹配(采样值错误、串口乱码)
- 原因:外设数据宽度与内存数据宽度不匹配(如 ADC16 位数据用了 DMA_MemoryDataSize_Byte);解决:ADC 采样值用 16 位(HalfWord),串口数据用 8 位(Byte),确保两者一致。
4. 循环模式未生效(仅传输一次)
- 原因:未开启循环模式(
DMA_Mode_Normal)或外设未配置为连续模式(如 ADC 未开启连续转换);解决:DMA 配置为DMA_Mode_Circular,ADC 配置为ADC_ContinuousConvMode_Enable。
5. 中断不响应(传输完成无触发)
- 原因 1:未使能 DMA 中断(
DMA_ITConfig未调用);解决:调用DMA_ITConfig使能传输完成中断(DMA_IT_TC); - 原因 2:NVIC 未配置中断优先级或未使能中断通道;解决:配置
NVIC_IRQChannel为对应 DMA 通道中断(如 DMA1_Channel1_IRQn),并使能中断; - 原因 3:中断标志位未清除,导致中断重复触发或无法再次触发;解决:中断服务函数中调用
DMA_ClearITPendingBit清除中断标志位。
6. 内存到内存模式传输失败
- 原因 1:未开启
DMA_M2M_Enable;解决:内存到内存模式必须设置DMA_InitStruct.DMA_M2M = DMA_M2M_Enable; - 原因 2:源地址或目标地址增量未开启;解决:内存到内存传输需同时开启源地址(PeripheralInc)和目标地址(MemoryInc)增量。
七、总结:DMA 实战核心要点与进阶方向
1. 核心要点回顾
-
DMA 实战核心:通道映射正确 + 传输方向 + 地址增量 + 数据宽度 + 模式选择,缺一不可;
-
三大核心场景:
- ADC+DMA:循环模式 + 连续转换,实现无 CPU 干预采集;
- 串口 DMA:正常模式 + 中断,实现大数据收发;
- 内存 DMA:内存到内存模式,实现高速数据复制;
-
避坑核心:通道映射、方向配置、地址增量、中断标志位清除、数据宽度匹配。
2. 进阶学习方向
- DMA+DAC 波形生成:DMA 循环传输波形数据到 DAC,实现正弦波、方波输出;
- 多通道 DMA 并发:多个 DMA 通道同时传输(如 ADC + 串口),通过优先级仲裁实现有序传输;
- DMA+SPI 闪存读写:W25Q64 等 SPI 闪存的批量数据读写,DMA 提升传输效率;
- 低功耗 DMA 传输:CPU 在 DMA 传输期间进入睡眠模式,降低系统功耗;
- DMA 传输错误处理:添加传输错误中断,处理数据传输异常(如总线错误)。
掌握 DMA 实战后,你已具备嵌入式系统 “高速数据传输” 的核心能力,可应对 ADC 高频采集、串口文件传输、闪存批量读写等复杂场景。下一篇我们将学习 I2C 通信,实现 STM32 与 OLED 屏幕、EEPROM 的交互,让数据可视化!