文章目录
- 前言
- 一、DMA介绍
- [1.1 背景](#1.1 背景)
- [1.2 为什么要使用DMA](#1.2 为什么要使用DMA)
- [1.3 STM32F1的DMA资源](#1.3 STM32F1的DMA资源)
- 二、DMA的使用
-
- [2.1 DMA使用简述](#2.1 DMA使用简述)
- [2.2 ADC非连续模式+DMA实验](#2.2 ADC非连续模式+DMA实验)
- [2.3 ADC连续转换+DMA实验](#2.3 ADC连续转换+DMA实验)
- 注意事项:
- 总结
前言
在之前学习ADC时,曾提到过使用扫描模式或者连续模式要采用"常规序列 + DMA "。
下面,我们就来学习DMA。
本文内容参考了B站UP"爱上半导体 "和"Keysking"。
一、DMA介绍
1.1 背景

在单片机的物理结构中,主要有两个存放数据的地方:
- Flash(硬盘):掉电不丢失,空间大,速度慢,通常只读(存放代码和常量)。
- SRAM(内存):掉电丢失,空间小,速度快,可读可写(存放变量)。
右图所示,我们定义一个变量a,它存放了数据0xAA,如果我们要使用串口将其发送出去。那么这需要 CPU 将变量 a,从 SRAM 转入到串口发送数据寄存器。
这里只发送一个字符的数据,CPU处理的所花时间很少,如果这里要发送大量的数据,那么CPU就要花大量时间搬运数据,而不能处理其它事情。 那么,有没有办法解放CPU,让它处理重要的事情,把搬运数据这种杂活交给别人干呢?
1.2 为什么要使用DMA
DMA(Direct Memory Access,直接存储器访问)是一种硬件机制,它允许外设(如 ADC、UART、SPI)与内存之间,或者内存与内存之间直接传输数据,而不需要 CPU 的介入。

用老板与搬运工来通俗类比 CPU 和 DMA 的关系
- CPU = 老板:工资高(算力强),擅长做决策、搞运算,时间宝贵。
- DMA = 搬运工:工资低(功能单一),没脑子,但力气大,专门负责搬东西。
- 数据 = 砖头。
没有 DMA 时 :
老板(CPU)想把 1000 块砖头从仓库(内存)搬到卡车(串口)上。老板必须亲自一块一块地搬。搬砖的时候,老板没法做别的事(比如思考公司战略、处理突发情况),效率极低。
有 DMA 时 :
老板(CPU)把搬运工(DMA)叫来,说:"把那 1000 块砖从仓库搬到卡车上,搬完了告诉我一声。"
交代完这几句话(配置 DMA),老板就回办公室算账、喝茶去了(执行其他代码)。DMA 就在后台默默搬砖,互不干扰。等搬完了,DMA 会敲门(产生中断)告诉老板:"干完了"。
从上面的案例,可以看出使用 DMA 的核心目的只有一个------解放 CPU。
使用DMA有以下几个优势:
- 提高 CPU 效率:CPU 可以腾出手来做它该做的事(复杂的逻辑运算、PID 控制、算法处理),而不是浪费在枯燥的数据搬运上。
- 提高数据传输速度:DMA 是专门为搬运数据设计的硬件,传输速度通常比 CPU 通过软件指令(Load/Store)搬运要快,特别是在批量传输时。
- 保证实时性:在高速通信(如 10Mbps 的 SPI 屏幕刷新)或高速采集(如 1Msps 的 ADC 采样)中,如果靠 CPU 一个字节一个字节地去读,很容易因为 CPU 稍微处理慢了一点而丢失数据。DMA 可以严丝合缝地自动接收数据。
- 降低功耗:在一些低功耗应用中,可以让 CPU 进入睡眠模式,只留 DMA 在那里搬运数据,等数据凑够了再唤醒 CPU,从而省电。
1.3 STM32F1的DMA资源

STM32F1(大容量型号)最多拥有 2 个 DMA 控制器 ,总共 12 个通道。
它们挂载在 AHB 总线上,可以直接访问 Flash、SRAM 和外设寄存器。
DMA1 :拥有 7 个通道。
DMA2 :拥有 5 个通道。(只有大容量 High-density 及以上型号才有 DMA2,我们常用的STM32F103C8T6只有 DMA1)。
核心特点一:通道请求是"写死"的(硬连线)
在STM32F1中,外设与 DMA 通道的对应关系是固定的,这是与其它系列单片机最大的不同。
例如:ADC1 只能用 DMA1 的通道 1。你不能随意更改!

注意: 如果你想同时开启两个外设的 DMA,而它俩恰好被映射到了同一个 DMA 通道,那么这两个功能就无法同时使用 DMA。
核心特点二:优先级管理
当多个通道同时请求搬运数据时,DMA 控制器通过两级判断来决定先帮谁搬:
软件优先级 :你可以在代码里配置(最高、高、中、低)。
硬件优先级 :如果软件优先级一样,通道号越小,优先级越高(例如通道 1 > 通道 2)。

核心特点三:数据宽度与对齐
DMA的数据宽度支持8位(Byte)、16位(Half-word)、32位(Word)。
源和目标的宽度可以不同,DMA 会自动打包或拆包。
二、DMA的使用
2.1 DMA使用简述

前面说过DMA是搬运工,负责搬运数据,使用DMA之前就像填快递单。
我们需要告诉它谁是发货方,发货方的地址;谁是接收方,接收方的地址,这里也就确定了数据的传输方向和地址。然后,我们还要确定发多少货,货物的大小在这里是统一的,即数据宽度和数据数量。这个单要不要加急,即DMA的优先级。搬一次,还是要一直搬,即常规模式还是循环模式。

1.关于地址自增,以串口发送+DMA为例
我们定义一个数组,里面存放的数据为'H','E','L','L','O',它存放在存储器地址中,需要依次将数据传输出去。
串口发送呢,需要先从"存储器地址"把数据转入"发送数据寄存器",再由发送数据寄存器把数据转入发送移位寄存器,然后一个比特位一个比特位的往外发。之前是由CPU从"存储器地址"把数据转入"发送数据寄存器",现在这个任务交给了DMA。
那么内存地址,数据'H','E','L','L','O'的地址显然是不同的,发送完一个数据,我们就要让内存地址自增,即移到数组的下一位。
这里外设地址寄存器,由于对象一直是"发送数据寄存器",所以它不变,即地址不自增。
2.我们要发送的数据很多,但怎么知道什么时候搬下一位数据,发的太快会覆盖上一位数据,造成丢包
这需要触发信号,串口发完一字节的数据会给DMA发触发信号,这一步由硬件完成。
这里需要注意外设与 DMA 通道的对应关系是固定的,不能随便乱填,可以看1.3节的表格,查看对应关系。
3.总结一下DMA的配置步骤,以ADC为例
第一步:开启ADC1的时钟,设置ADC分频因子,开启GOIO的时钟,开启DMA的时钟
第二步:配置GPIO,模式设为GPIO_Mode_AIN,选中需要的引脚
第三步:配置DMA (搬运工)
- 源地址:外设地址(ADC的数据寄存器)
- 目标地址:内存地址,即我们定义的数组地址
- 数据方向:这里是接收信号,所以是外设到内存
- 数据量:缓冲区大小,通道数量,例如两个通道就填2
- 数据宽度:外设,HalfWord (16位,因为 ADC 是 12位的);内存,HalfWord (16位,配合定义数组为
uint16_t) - 地址自增:外设地址自增,Disable (始终从 ADC->DR 取数据,不许动);内存地址自增,Enable
(存完数组第0个,自动存第1个...) - 模式:常规模式或循环模式,连续转换要开循环模式,否则传一轮就停下了
- 触发源/握手信号:开启 ADC 触发 DMA 请求,ADC_DMACmd(ADC1, ENABLE);
2.2 ADC非连续模式+DMA实验
下面进行使用ADC同时采集光敏模块、NTC热敏模块的模拟输出,这里要使用扫描模式,先不进行连续转换模式。
由于大量转运数据,我们使用DMA来搬运DMA的数据,由于ADC没有使用连续模式,这里DMA使用常规模式,不循环。
由于使用非连续转换且DMA不循环,那么系统工作一次后就会彻底停下来。想要"一直搬运数据",你必须在DMA 传输完成中断中,手动像给手枪上膛一样,重新装填 DMA 计数器,并再次扣动 ADC 的扳机。
通道0即PA0接热敏电阻,通道2即PA1接光敏电阻。
对ADC和DMA进行配置,要在.h文件中声明extern volatile uint16_t ADC_Value[2];
c
#include "ADC.h"
// 定义用于存放转换结果的数组
// volatile 关键字很重要,防止编译器优化掉这个变量
// 数组大小为2,分别对应两个通道的数据
volatile uint16_t ADC_Value[2] = {0};//定义用于存放AD转换结果的全局数组
/*
简介:ADC1常规序列,多通道转换,扫描模式,要配合DMA进行数据转运
*/
void MyADC1_Init(void){
//#1.开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置分频器的分频系数(6分频)
//#2.初始化PA0和PA1引脚,模拟输入
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;//通道0和通道1
GPIO_Init(GPIOA,&GPIO_InitStruct);
//#3.初始化ADC的基本参数
ADC_InitTypeDef ADC_InitStruct = {0};
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;//连续转换
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//独立模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//软件触发
ADC_InitStruct.ADC_NbrOfChannel = 2;//转换通道数为2
ADC_InitStruct.ADC_ScanConvMode = ENABLE;//扫描模式,两个通道
ADC_Init(ADC1,&ADC_InitStruct);
//#4.配置常规序列通道
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_13Cycles5);//配置常规序列的通道0
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_13Cycles5);//配置常规序列的通道1
ADC_ExternalTrigConvCmd(ADC1,ENABLE);//闭合外部触发开关
//#5.开启DMA1的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//#6.初始化ADC1对应的DMA1通道1的基本参数
DMA_InitTypeDef DMA_InitStruct = {0};
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);//外设基地址:ADC1 的数据寄存器地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)ADC_Value;// 内存基地址:我们要存数据的数组地址
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;// 数据传输方向:外设 -> 内存
DMA_InitStruct.DMA_BufferSize = 2;// 缓冲区大小:2个数据 (对应2个通道)
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;// 外设地址不自增 (始终读 &ADC1->DR)
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;// 内存地址自增 (Buffer[0] -> Buffer[1])
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;// 外设数据宽度:16位 (HalfWord)
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;// 内存数据宽度:16位 (HalfWord)
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;// 模式:常规模式,不循环
DMA_InitStruct.DMA_Priority = DMA_Priority_High;// 优先级:高
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;// 内存到内存:否
DMA_Init(DMA1_Channel1,&DMA_InitStruct);
//开启 DMA1_Channel1 的传输完成中断
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
NVIC_InitTypeDef NVIC_InitStructure={0};
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn; // ADC1对应DMA1通道1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//#7.开启DMA
DMA_Cmd(DMA1_Channel1,ENABLE);
//#8.开启ADC的DMA请求
ADC_DMACmd(ADC1,ENABLE);
//#9.闭合ADC的总开关
ADC_Cmd(ADC1,ENABLE);
//#10.执行ADC的校准
ADC_ResetCalibration(ADC1);//复位标准寄存器
while(ADC_GetResetCalibrationStatus(ADC1) == SET);//等待复位完成,标志位变为RESET
ADC_StartCalibration(ADC1);//触发硬件开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
//#11.转换,由于没使用连续转换,在main函数的while中要调用
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
因为没有开连续模式,需要重新触发ADC和重装DMA,DMA的中断服务函数检测到传输完成,就将全局变量置1,在main函数中进行处理,中断服务函数要快进快出。
全部转换完成,DMA会触发中断。使用串口打印数据。
c
#include "stm32f10x.h" // Device header
#include "ADC.h"
#include "usart.h"
void OnBoardLED_Init(void);
volatile uint8_t My_ADC_Complete_Flag = 0;
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
usart_Init();
OnBoardLED_Init();
MyADC1_Init();
while(1)
{
if(My_ADC_Complete_Flag == 1){
//#1.清除标志位
My_ADC_Complete_Flag = 0;
//#2.读取转换的结果
uint16_t adc_value1 = ADC_Value[0];//对应通道0
uint16_t adc_value2 = ADC_Value[1];//对应通道1
float voltage1 = adc_value1 * 3.3f / 4095.0f;
float voltage2 = adc_value2 * 3.3f / 4095.0f;
if(voltage1>=1.5){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);//光照弱,灭灯
}else{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);//光照强,亮灯
}
My_USART_Printf(USART1, "CH1:%.3fV, CH2:%.3fV\r\n",voltage1,voltage2);
//#3.重装DMA
DMA_Cmd(DMA1_Channel1, DISABLE); // 必须先关闭 DMA 才能改计数器
DMA_SetCurrDataCounter(DMA1_Channel1, 2); // 重新填入搬运数量(2个)
DMA_Cmd(DMA1_Channel1, ENABLE); // 重新开启 DMA 等待数据
//#4.重装ADC
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
Delay_ms(100);
}
}
/*
简介:DMA的中断服务函数
*/
void DMA1_Channel1_IRQHandler(void){
// 检查是否是传输完成中断 (Transfer Complete)
if (DMA_GetITStatus(DMA1_IT_TC1) != RESET){
// 1. 清除中断标志位 (如果不清,会卡死在中断里)
DMA_ClearITPendingBit(DMA1_IT_TC1);
// 2. 告诉 main 函数,数据到了
My_ADC_Complete_Flag = 1;
}
}
/*
简介:初始化板载LED
*/
void OnBoardLED_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct ={0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}


实验现象如上,实验完成。
2.3 ADC连续转换+DMA实验
配置大部分相同,这里将ADC的持续转换模式开启

DMA的模式改为循环模式

由于是连续转换,DMA是循环模式一直转换,就不用中断回调函数,在main函数中,直接读取数据即可
c
#include "stm32f10x.h" // Device header
#include "ADC.h"
#include "usart.h"
void OnBoardLED_Init(void);
int main(void)
{
usart_Init();
OnBoardLED_Init();
MyADC1_Init();
while(1)
{
//#1.读取转换的结果
uint16_t adc_value1 = ADC_Value[0];//对应通道0
uint16_t adc_value2 = ADC_Value[1];//对应通道1
float voltage1 = adc_value1 * 3.3f / 4095.0f;
float voltage2 = adc_value2 * 3.3f / 4095.0f;
//#2.实验现象
if(voltage1>=1.5){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);//光照弱,灭灯
}else{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);//光照强,亮灯
}
My_USART_Printf(USART1, "CH1:%.3fV, CH2:%.3fV\r\n",voltage1,voltage2);
Delay_ms(100);
}
}
/*
简介:初始化板载LED
*/
void OnBoardLED_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct ={0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}
ADC部分的代码,要在.h文件中声明extern volatile uint16_t ADC_Value[2];
c
#include "ADC.h"
// 定义用于存放转换结果的数组
// volatile 关键字很重要,防止编译器优化掉这个变量
// 数组大小为2,分别对应两个通道的数据
volatile uint16_t ADC_Value[2] = {0};//定义用于存放AD转换结果的全局数组
/*
简介:ADC1常规序列,多通道转换,扫描模式,要配合DMA进行数据转运
*/
void MyADC1_Init(void){
//#1.开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置分频器的分频系数(6分频)
//#2.初始化PA0和PA1引脚,模拟输入
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;//通道0和通道1
GPIO_Init(GPIOA,&GPIO_InitStruct);
//#3.初始化ADC的基本参数
ADC_InitTypeDef ADC_InitStruct = {0};
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;//连续转换
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//独立模式
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//软件触发
ADC_InitStruct.ADC_NbrOfChannel = 2;//转换通道数为2
ADC_InitStruct.ADC_ScanConvMode = ENABLE;//扫描模式,两个通道
ADC_Init(ADC1,&ADC_InitStruct);
//#4.配置常规序列通道
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_13Cycles5);//配置常规序列的通道0
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_13Cycles5);//配置常规序列的通道1
ADC_ExternalTrigConvCmd(ADC1,ENABLE);//闭合外部触发开关
//#5.开启DMA1的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//#6.初始化ADC1对应的DMA1通道1的基本参数
DMA_InitTypeDef DMA_InitStruct = {0};
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);//外设基地址:ADC1 的数据寄存器地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)ADC_Value;// 内存基地址:我们要存数据的数组地址
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;// 数据传输方向:外设 -> 内存
DMA_InitStruct.DMA_BufferSize = 2;// 缓冲区大小:2个数据 (对应2个通道)
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;// 外设地址不自增 (始终读 &ADC1->DR)
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;// 内存地址自增 (Buffer[0] -> Buffer[1])
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;// 外设数据宽度:16位 (HalfWord)
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;// 内存数据宽度:16位 (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_Channel1,&DMA_InitStruct);
//#7.开启DMA
DMA_Cmd(DMA1_Channel1,ENABLE);
//#8.开启ADC的DMA请求
ADC_DMACmd(ADC1,ENABLE);
//#9.闭合ADC的总开关
ADC_Cmd(ADC1,ENABLE);
//#10.执行ADC的校准
ADC_ResetCalibration(ADC1);//复位标准寄存器
while(ADC_GetResetCalibrationStatus(ADC1) == SET);//等待复位完成,标志位变为RESET
ADC_StartCalibration(ADC1);//触发硬件开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
//#11.转换,由于没使用连续转换,在main函数的while中调用
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}

手不放在热敏电阻上,不遮挡光敏电阻,串口打印出来的值如上。

手放在热敏电阻上,挡住光敏电阻,串口打印出来的值如上,符合预期,实验完成。
注意事项:
Q1:ADC校准
简单来说,ADC校准就像是给电子秤"去皮"(归零)。如果不进行校准,你的ADC测量结果可能会整体"偏大"或"偏小"一个固定的数值。
STM32F103 的 ADC 内部使用的是 逐次逼近型 (SAR) ADC,其核心结构包含一个电容阵列(用于采样和保持电压)。在芯片生产过程中,硅片上的这些微小电容无法做得完全一模一样,总会有微小的物理偏差。此外,温度变化、供电电压的微小波动,都会导致内部电路产生 "零点偏移" (Offset Error)。
如果没有校准,当你输入 0V 时,ADC 读出来的可能不是 0,而是 10 或者 20;当你输入 3.3V 时,读出来的可能不到 4095。这会导致你的所有测量值都带有一个固定的误差。
校准的过程,实际上是 ADC 内部断开外部 GPIO 输入,改为测量内部的基准电压,算出这个"偏差值",然后保存在一个专门的寄存器里。以后每次转换,硬件会自动把这个偏差值减掉,给你一个修正后的结果。
校准必须要在ADC上电之后进行,ADC_Cmd(ADC1, ENABLE);后面进行校准操作
Q2:在ADC.c文件中的变量怎么在main函数中使用
第一步要在ADC.c文件中定义全局变量 volatile uint16_t ADC_Value[2] = {0} ,第二步在ADC.h文件中用 extern关键字声明这个变量volatile extern uint16_t ADC_Value[2],在main函数中包含ADC.h文件,就可以读取这个变量。
Q3:不能在中断服务函数里调用My_USART_Printf(串口打印函数)

原因:
- 封装的 My_USART_Printf 内部处理非常复杂,需要大量堆栈。中断发生时,CPU 处于一种"急救"状态,堆栈空间有限且上下文敏感。在中断里强行打印,极易破坏堆栈,导致打印出的变量(voltage1)变成随机的内存垃圾值
- 串口打印 20 个字符可能需要几毫秒,而中断要求"快进快出"。打印期间可能会阻塞主循环的逻辑,甚至导致丢数据。
- 如果主循环里也在用串口,中断突然插进来又要用串口,数据就打架了。
解决:
中断 (ISR) :只做最简单的事,置位一个标志变量,告诉主程序"数据好了"。
主循环 (Main):负责干重活------检测标志位、计算电压、逻辑判断、串口打印、重装 DMA、再次开启 ADC。
总结
以上就是本文的全部内容,学完之后对DMA有了一个清晰的认识,并用DMA对ADC的数据进行了搬运。
串口,I2C,SPI等外设使用DMA的逻辑与ADC相同,最重要的是搞懂其中的机制,后面用到再去移植。
此外,并不是所有地方都要用DMA,如果能够满足我们的需求,那就不需要开DMA,如果有大量数据要处理,需要快速响应,那就果断开启DMA!