STM32 进阶封神之路(二十二):DMA 实战全攻略 ——ADC 采集 + 串口收发 + 内存复制(库函数 + 代码落地)

0 阅读18分钟

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 引脚连接说明
光敏电阻模块VCC3.3V传感器供电
光敏电阻模块GNDGND共地
光敏电阻模块AOPA5(ADC1_CH5)模拟信号输入,配置为模拟输入模式
USB-TTL 模块TXPA10(USART1_RX)串口接收引脚
USB-TTL 模块RXPA9(USART1_TX)串口发送引脚
USB-TTL 模块GNDGND共地
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 实战核心:通道映射正确 + 传输方向 + 地址增量 + 数据宽度 + 模式选择,缺一不可;

  • 三大核心场景:

    1. ADC+DMA:循环模式 + 连续转换,实现无 CPU 干预采集;
    2. 串口 DMA:正常模式 + 中断,实现大数据收发;
    3. 内存 DMA:内存到内存模式,实现高速数据复制;
  • 避坑核心:通道映射、方向配置、地址增量、中断标志位清除、数据宽度匹配。

2. 进阶学习方向

  • DMA+DAC 波形生成:DMA 循环传输波形数据到 DAC,实现正弦波、方波输出;
  • 多通道 DMA 并发:多个 DMA 通道同时传输(如 ADC + 串口),通过优先级仲裁实现有序传输;
  • DMA+SPI 闪存读写:W25Q64 等 SPI 闪存的批量数据读写,DMA 提升传输效率;
  • 低功耗 DMA 传输:CPU 在 DMA 传输期间进入睡眠模式,降低系统功耗;
  • DMA 传输错误处理:添加传输错误中断,处理数据传输异常(如总线错误)。

掌握 DMA 实战后,你已具备嵌入式系统 “高速数据传输” 的核心能力,可应对 ADC 高频采集、串口文件传输、闪存批量读写等复杂场景。下一篇我们将学习 I2C 通信,实现 STM32 与 OLED 屏幕、EEPROM 的交互,让数据可视化!