江科大stm32中的AD单通道原理与多通道原理详细解析
顺序:ADC 采 CH0 → DMA 自动复制到 AD_Value [0]ADC 采 CH1 → DMA 自动复制到 AD_Value [1]ADC 采 CH2 → DMA 自动复制到 AD_Value [2]ADC 采 CH3 → DMA 自动复制到 AD_Value [3]ADC 通道 = 单片机上的模拟输入引脚,每个通道对应一个引脚(比如 ADC1_IN2 对应 PA2),用来读外部的模拟电
STM32 的 ADC 单通道(AD 单通道),核心是:只对 1 路模拟信号进行采样、保持、逐次逼近转换,最终得到 12 位数字量。以最常用的 STM32F103(12 位 SAR ADC) 为例,原理如下:
一、硬件结构(单通道路径)

1.模拟输入通道
◦ 外部信号(如传感器、电位器)→ 对应 GPIO 引脚(如 PA0 → ADC1_IN0)。
◦ GPIO 必须设为 模拟输入 AIN(禁用施密特触发器,避免数字电平干扰)。
2. 通道选择(多路开关 MUX)
◦ 单通道 = 固定选通 1 路(如 IN0),不切换其他通道。
3. 采样保持(S/H)
◦ 内部电容对输入电压快速充电,锁定电压值,保证转换期间电压稳定。
◦ 采样时间由寄存器(SMPR)配置(越长越稳定、越慢)。
4. 逐次逼近核心(SAR ADC)
◦ 12 位 DAC + 比较器 + 逐次逼近寄存器(SAR)。
◦ 用 二分法 从最高位(MSB)到最低位(LSB)逐位试探、比较。
5. 参考电压
◦ 范围:VREF–(0V)~ VREF+(通常 3.3V)
◦ 1 位精度 ≈ 3.3V / 4095 ≈ 0.806mV
二、单通道转换全过程(单次模式)
信号流:模拟电压 → 采样保持 → 12 次逐次比较 → 12 位数字 → DR 寄存器 → 读取
1. 启动(软件触发)
• 置位启动位,ADC 开始工作。
2. 采样阶段
• 开关闭合,内部电容快速跟随输入电压。
• 达到采样时间后,开关断开 → 电压被保持。
3. 逐次逼近(12 个 ADC 时钟)
以 输入 2.0V、量程 0~3.3V 为例:
1. 第 1 次(bit11):试 1.65V(½ 量程)
◦ 2.0V > 1.65V → bit11 = 1
2. 第 2 次(bit10):试 2.475V(¾ 量程)
◦ 2.0V < 2.475V → bit10 = 0
3. 第 3 次(bit9):试 2.0625V(⅝ 量程)
◦ 2.0V < 2.0625V → bit9 = 0
4. …… 重复 12 次……
5. 最终得到 12 位二进制码(如 110011001100)
4. 转换结束
• EOC 标志置 1。
• 结果存入 DR 寄存器(0~4095)。
5. 读取与换算
uint16_t adc_val = ADC_GetConversionValue(ADC1);
float voltage = adc_val * 3.3f / 4095.0f;
三、单通道两种常用模式
1. 单次转换(非扫描)
• 触发 → 转 1 次 → 停止 → 等待下次触发。
• 适合:定时采样、低功耗、按需读取。

2. 连续转换(非扫描)
• 启动后 自动循环转换,不断更新 DR。
• 适合:高速连续采集、实时监测。

单通道 = 非扫描模式(扫描模式用于多通道轮流切换)。
四、单通道要点总结
• 只测一路:始终固定一个通道(如 IN0)。
• 12 位:结果 0~4095,对应 0~3.3V。
• 逐次逼近:12 次二分比较,速度快、精度高。
• 模式:单次(省电)/ 连续(高速)。
• 关键配置:GPIO 设 AIN、通道选择、采样时间、触发方式。
五、一句话记原理
单路模拟信号 → 采样保持 → 12 位逐次逼近 → 数字量 0~4095 → 换算为实际电压。
六.采样保持是怎么做的?
1. 什么是采样保持(S/H)?
简单说:ADC 转换需要时间,而电压可能在变。
采样保持就是:先 “抓住” 这一刻的电压,把它固定住,再慢慢转成数字。
它内部有一个小电容。
2. 内部电容对输入电压快速充电
• 外部电压进来
• 开关闭合 → 电容迅速充电
• 电容电压跟着输入电压变
这一步叫 采样(Sample)。
3. 锁定电压值,保证转换期间电压稳定
充好电后:
• 开关断开
• 电容就像一个小电池,电压保持不变
这一步叫 保持(Hold)。
为什么要这样?因为 ADC 逐次比较需要十几个时钟周期。如果电压在转换过程中变了,结果就错了。所以必须先固定电压,再转换。
4. 采样时间由 SMPR 寄存器配置
SMPR = Sample Time Register用来设置:采样多久再断开开关
• 采样时间短→ 充电可能不充分→ 电压不准→ 速度快
• 采样时间长→ 电容充得更满、更接近真实电压→ 更稳定、更准→ 速度变慢
所以:采样时间越长越稳定、越慢就是这个意思。
5. 一句话总结采样保持
先快速抓住电压(采样),再把电压钉住不动(保持),让 ADC 有时间慢慢转换,保证结果不乱跳。
七.通道与规则序列
一、先搞懂共同的基础概念
1. 什么是「通道」?
ADC 通道 = 单片机上的模拟输入引脚,每个通道对应一个引脚(比如 ADC1_IN2 对应 PA2),用来读外部的模拟电压信号(比如电位器、传感器)。
2. 什么是「序列 1~16」?
这 16 个位置叫规则序列,是用来定义「ADC 转换的顺序和通道」的。你可以把不同通道按顺序填进去,ADC 就会按这个顺序依次转换。
二、模式 1:单次转换 + 非扫描模式(你图里的模式)
核心逻辑:一次触发,只转换 1 个通道
• 你只需要把要转换的通道(比如「通道 2」)填到 序列 1 里,序列 2~16 空着就行。
• 触发信号来了之后:
1. ADC 只执行「序列 1」里的通道(通道 2),进行一次采样和转换。
2. 转换完成后,立刻置位 EOC(转换结束标志),然后 ADC 就停止工作,等下一次触发。
• 序列 2~16 里的内容完全不会被执行,因为非扫描模式只认序列 1。
一句话总结:
单通道、单次采样,用完就停,只看序列 1。

三、模式 2:扫描模式(多通道用的)
核心逻辑:一次触发,按序列顺序转换多个通道
• 你可以把多个通道按顺序填到序列 1~N 里(比如序列 1 = 通道 2、序列 2 = 通道 5、序列 3 = 通道 7……),最多支持 16 个。
• 触发信号来了之后:
1. ADC 从序列 1 开始,依次转换每个序列里的通道(通道 2 → 通道 5 → 通道 7 → ……)。
2. 每转换完一个通道,数据会自动存到对应的寄存器里。
3. 当所有序列里的通道都转换完成,才会置位 EOC(也可以配置成每转换完一个就触发中断)。
• 序列里有几个通道,就会按顺序转换几个,不会漏。
一句话总结:
多通道、批量采样,按序列顺序跑,跑完所有通道才结束。

四、两种模式对比(结合你的图)
表格
五、补充:再加上「单次 / 连续」转换的区别
上面两种模式,还可以搭配「单次」或「连续」转换:
• 单次转换:不管是单通道还是多通道,跑完一次就停,必须等下一次触发才会再跑。
• 连续转换:跑完一次(单通道 / 多通道)之后,自动重新开始,循环采样,直到你手动停止 ADC。
江科大32AD单通道代码解析
一、整体功能总览
1. 初始化 ADC1 + PA0 为模拟输入
2. 配置 ADC 为单次转换、软件触发、独立模式
3. 完成 ADC 校准(必须步骤)
4. 提供读取函数:触发转换 → 等待完成 → 返回结果
5. 输出:12 位 ADC 结果(0~4095)
二、逐行逐函数详细解析
第一部分:AD_Init () 初始化函数
1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
函数:RCC_APB2PeriphClockCmd()
• 功能:使能 APB2 总线上的外设时钟(GPIO、ADC、USART1 都挂在 APB2)
• 参数 1:外设选择
◦ RCC_APB2Periph_ADC1:ADC1 时钟
◦ RCC_APB2Periph_ADC2:ADC2 时钟
◦ RCC_APB2Periph_GPIOA~G:对应 GPIO 口时钟
◦ RCC_APB2Periph_USART1:串口 1
• 参数 2:状态
◦ ENABLE:开启
◦ DISABLE:关闭
重要:STM32 任何外设使用前必须先开时钟,否则外设不工作。
2. 设置 ADC 分频时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
函数:RCC_ADCCLKConfig()
• 功能:配置 ADC 时钟(ADCCLK)
• ADC 时钟不能超过 14MHz,STM32F103 系统时钟 72MHz,APB2=72MHz
• 6 分频后:72/6=12MHz(安全、推荐)
可选参数:
• RCC_PCLK2_Div2 2 分频 → 36MHz(超量程,禁止使用)
• RCC_PCLK2_Div4 4 分频 → 18MHz(超量程)
• RCC_PCLK2_Div6 6 分频 → 12MHz ✔️
• RCC_PCLK2_Div8 8 分频 → 9MHz ✔️
3. GPIO 初始化(模拟输入)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
关键:GPIO_Mode_AIN(模拟输入)
• ADC 采集引脚必须配置为 AIN 模拟输入
• 此时引脚浮空、无上下拉、禁止数字输入,避免干扰 ADC 采集
GPIO 模式可选参数(其他模式):
• GPIO_Mode_AIN:模拟输入(ADC 专用)
• GPIO_Mode_IN_FLOATING:浮空输入
• GPIO_Mode_IPD:下拉输入
• GPIO_Mode_IPU:上拉输入
• GPIO_Mode_Out_OD:开漏输出
• GPIO_Mode_Out_PP:推挽输出
• GPIO_Mode_AF_OD:复用开漏
• GPIO_Mode_AF_PP:复用推挽
GPIO 速度(输出才有用,输入可随便写):
• GPIO_Speed_10MHz
• GPIO_Speed_2MHz
• GPIO_Speed_50MHz
4. 规则组通道配置
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
函数:ADC_RegularChannelConfig()
功能:配置规则组通道(ADC 正常采集通道)单通道、多通道都用这个函数配置序列。
4 个参数详解:
1. ADCx:ADC1 / ADC2
2. ADC_Channel_x:通道选择
◦ ADC_Channel_0 ~ ADC_Channel_17
◦ 通道 0~15 对应 GPIOA0~GPIOG5
◦ 通道 16:内部温度传感器
◦ 通道 17:内部参考电压
3. Rank:序列排名(1~16)
◦ 单通道写 1 即可
4. SampleTime:采样周期
◦ 周期越长,采集越稳定,速度越慢
采样时间可选参数:
• ADC_SampleTime_1Cycles5 1.5 周期
• ADC_SampleTime_7Cycles5 7.5 周期
• ADC_SampleTime_13Cycles5 13.5 周期
• ADC_SampleTime_28Cycles5 28.5 周期
• ADC_SampleTime_41Cycles5 41.5 周期
• ADC_SampleTime_55Cycles5 55.5 周期(最常用、稳定)
• ADC_SampleTime_71Cycles5 71.5 周期
• ADC_SampleTime_239Cycles5239.5 周期
5. ADC 初始化结构体
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
逐字段解析:
1. ADC_Mode 模式
• ADC_Mode_Independent:独立模式(只使用一个 ADC,最常用)
• 其他模式(双 ADC 模式,极少用):
◦ ADC_Mode_RegInjecSimult
◦ ADC_Mode_RegSimult_AlterTrig
◦ 等同步 / 交替触发模式
2. ADC_DataAlign 数据对齐
• ADC_DataAlign_Right:右对齐(推荐,结果直接 0~4095)
• ADC_DataAlign_Left:左对齐(结果会左移 12 位,需要手动处理)
3. ADC_ExternalTrigConv 外部触发
• ADC_ExternalTrigConv_None:不使用外部触发(软件触发)
• 其他可选触发源(定时器 / 外部引脚):
◦ ADC_ExternalTrigConv_T1_CC1
◦ ADC_ExternalTrigConv_T2_CC2
◦ ADC_ExternalTrigConv_T3_TRGO
◦ ADC_ExternalTrigConv_EXTI_11
4. ADC_ContinuousConvMode 连续转换
• DISABLE:单次转换(转换一次就停止)
• ENABLE:连续转换(触发一次后自动不停转换)
5. ADC_ScanConvMode 扫描模式
• DISABLE:单通道模式
• ENABLE:多通道扫描(依次转换多个通道)
6. ADC_NbrOfChannel 通道数目
• 单通道:1
• 多通道:写对应通道数量(2~16)
6. ADC 使能
ADC_Cmd(ADC1, ENABLE);
功能:上电启动 ADC
• 参数:ENABLE / DISABLE
7. ADC 校准(必须执行!)
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
为什么要校准?
ADC 内部有误差,上电必须校准一次,否则采集不准。
函数解释:
1. ADC_ResetCalibration():复位校准
2. ADC_GetResetCalibrationStatus():等待复位完成
3. ADC_StartCalibration():开始校准
4. ADC_GetCalibrationStatus():等待校准完成
第二部分:AD_GetValue () 获取 ADC 值
uint16_t AD_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
return ADC_GetConversionValue(ADC1);
}
1. 软件触发转换
功能:软件触发 ADC 开始转换
• 不使用硬件触发时,必须用这个函数启动转换
2. 等待转换完成
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
函数:ADC_GetFlagStatus()
• 功能:获取 ADC 标志位状态
• ADC_FLAG_EOC:转换结束标志(End Of Conversion)
• 等待直到 EOC=1,代表转换完成
常用 ADC 标志位:
• ADC_FLAG_EOC 转换完成
• ADC_FLAG_AWD 模拟看门狗(电压超限)
• ADC_FLAG_JEOC 注入组转换完成
3. 读取转换结果
ADC_GetConversionValue(ADC1);
功能:读取 ADC 数据寄存器
• 返回值:12 位结果 0~4095
• 右对齐时直接返回真实数字量
三、代码中没用到但非常重要的 ADC 相关函数
下面是标准库中与 ADC 相关、但本代码没用到的高频函数,你做项目一定会用到:
1. 模拟看门狗(电压超限检测)
ADC_AnalogWatchdogCmd(ADC1, ADC_AnalogWatchdog_SingleRegEnable);
ADC_SetAnalogWatchdogThresholds(ADC1, 3000, 1000);
ADC_AnalogWatchdogSingleChannelConfig(ADC1, ADC_Channel_0);
功能:当采集电压超出设定上下限时,触发中断
2. ADC 中断配置
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);
NVIC_Init();
转换完成后不轮询,用中断通知
• 适合低功耗、多任务程序
3. 注入组转换(紧急通道)
ADC_InjectedChannelConfig();
ADC_AutoInjectedConvCmd();
注入组优先级高于规则组
• 用于紧急、实时采集
4. 获取 ADC 状态寄存器标志位
ADC_GetFlagStatus(ADC1, ADC_FLAG_AWD); // 看门狗
ADC_ClearFlag(ADC1, ADC_FLAG_EOC); // 清除标志
5. 连续转换 / 扫描模式(多通道)
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
多通道采集必须开启扫描模式
• 连续转换无需每次都软件触发
6. 内部温度传感器 / 内部参考电压
ADC_TempSensorVrefintCmd(ENABLE);
开启后可采集芯片内部温度和内部参考电压 1.2V
四、核心工作流程总结
1. 开时钟 → GPIO 设为模拟输入
2. 配置 ADC 时钟、通道、采样时间
3. 配置单次 / 软件触发 / 右对齐 / 单通道
4. 使能 ADC → 校准
5. 采集时:软件触发 → 等待 EOC → 读结果
五、关键参数速查表(最实用)
| 参数 | 可选值 | 推荐 |
|---|---|---|
| ADC 分频 | 2/4/6/8 | 6 分频 |
| 采样时间 | 1.5~239.5 | 55.5 周期 |
| 对齐 | 左 / 右 | 右对齐 |
| 模式 | 独立 / 双 ADC | 独立 |
| 转换模式 | 单次 / 连续 | 单次 |
总结
1. 这份代码是STM32 ADC 最基础、最标准的单通道采集模板
2. 所有函数、参数、可选配置我都完整拆解
3. 补充了项目必备的未使用重要函数
4. 你可以直接用这份代码驱动 PA0 采集 0~3.3V 电压
主函数完整逐行解析
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
包含 STM32 核心库 + 延时 + OLED 屏幕 + AD 采集驱动
• 是一个 ADC 采集电压 + OLED 显示 的完整小项目
uint16_t ADValue; //定义AD值变量
float Voltage; //定义电压变量
ADValue:存 ADC 采集的原始数字量,范围 0 ~ 4095
• Voltage:存计算后的真实电压,比如 0.00V ~ 3.30V
ADValue = AD_GetValue(); //获取AD转换的值
调用 AD 驱动里的函数
• 去 PA0 引脚采集一次电压
• 得到 0~4095 的数字,存到 ADValue
Voltage = (float)ADValue / 4095 * 3.3;
这一行是核心公式,必须懂!
• ADC 是 12 位 → 最大值 4095
• 单片机参考电压 3.3V
• 所以:
电压 = AD值 / 4095 × 3.3
(float) 是强制转
OLED_ShowNum(1, 9, ADValue, 4);
换成小数,否则整数除法会直接变成 0
第 1 行第 9 列,显示 AD 值
• 显示 4 位数字(0000 ~ 4095)
OLED_ShowNum(2, 9, Voltage, 1);
显示电压整数部分,比如 3.3V 里的 3
OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);
✔ 这一行是显示小数的技巧
OLED 不能直接显示小数,所以用数学方法拆分:
• 3.30 × 100 = 330
• 330 % 100 = 30
• 显示 30
最终屏幕看到:3.30V
Delay_ms(100);
延时 100ms
• 防止刷新太快屏幕闪
• 也给 ADC 一点间隔
补充:
软件触发 和 硬件触发 的区别,一眼就能懂。
一、什么是 软件触发?
你现在代码里用的就是:
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
一句话解释
由程序代码手动 “喊一声开始”,ADC 才开始转换。
过程
1. 程序跑到这一行
2. 代码告诉 ADC:开始转换!
3. ADC 开始采集
4. 转换完返回结果
特点
• 想什么时候采,代码说了算
• 简单、灵活、好控制
• 缺点:CPU 必须一直等着,占资源
适用场景
• 简单采集
• 偶尔采一次
• 不需要固定频率
• 初学者最常用
二、什么是 硬件触发?
一句话解释
不用代码管,由定时器 / 外部引脚自动触发 ADC 转换。
触发源可以是:
• 定时器(TIM2、TIM3 等)
• 外部引脚(比如 EXTI11)
• 其他硬件事件
过程
1. 配置好硬件触发源(例如定时器 1 秒触发一次)
2. CPU 完全不用管
3. 定时器到时间 → 自动让 ADC 转换
4. 转换完可以用中断通知 CPU
特点
• 自动、定时、精准
• CPU 不用一直等
• 适合固定频率采集(比如每秒采 10 次)
适用场景
• 示波器、连续采集
• 固定周期采样
• 低功耗、不想 CPU 一直轮询
三、最直观的比喻
• 软件触发 = 你手动按一下快门拍照
• 硬件触发 = 相机设置定时,每 1 秒自动拍一张
你现在的代码就是手动按快门。
四、代码层面的区别(非常关键)
软件触发(你现在的)
// 软件触发启动
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
初始化里写:
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
硬件触发(例如定时器 3 触发)
初始化里写:
// 硬件触发源 = 定时器3
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
然后不需要写:
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 硬件触发不需要这句!
定时器一响,ADC 自动转换。
五、总结(超级精简版)
• 软件触发:代码手动启动,想采就采
• 硬件触发:硬件自动启动,定时 / 外部事件触发
• 软件触发简单,硬件触发更高级、更自动
ADC 校准到底是什么?为什么必须做?
1. 什么是 ADC 校准?
ADC 校准 = 给 ADC 内部电路 “调零、修正误差”。
STM32 的 ADC 内部有很多模拟电路、电容、比较器,它们出厂时、上电后,都会有微小的偏移误差(Offset Error)。
如果不校准:
• 你采集 0V,可能读到 5、10、20 这种不是 0 的数
• 采集 3.3V,可能读到 4090、4080,不准
• 整体线性会歪
校准就是:让 ADC 自己测量内部参考,自动修正这些误差,让结果更准。
2. 校准到底在干什么?(内部原理简化版)
STM32 ADC 校准过程(硬件自动完成,你不用管细节):
1. 给 ADC 内部输入 0V
2. ADC 自己测应该输出多少
3. 把误差存在内部校准寄存器里
4. 之后每次转换,自动用这个值修正结果
一句话:校准 = 让 ADC 知道 “什么是真正的 0”,以后读数才准。
3. 你代码里的校准流程(逐句解释)
ADC_ResetCalibration(ADC1); // 第一步:复位校准寄存器
while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位完成
ADC_StartCalibration(ADC1); // 第二步:开始自动校准
while (ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
① ADC_ResetCalibration(ADC1)
• 把之前的校准数据清空
• 让校准电路回到初始状态
② while 等待复位完成
硬件复位需要几个周期,必须等它结束。
③ ADC_StartCalibration(ADC1)
真正开始校准!ADC 内部硬件自动执行:
• 短接输入到 0V
• 测量偏移
• 自动修正
• 存入校准寄存器
④ while 等待校准完成
校准大概需要几个 ADC 时钟周期,必须等它结束。
4. 不校准会怎样?
• 低电压采集明显不准
• 不同芯片之间差异更大
• 温度变化时误差更飘
• 官方手册明确要求:上电必须校准
不是不能用,是不准、不稳定。
5. 什么时候需要校准?
只需要 1 次:
• 系统上电初始化时
• 从停止 / 待机模式唤醒后
不需要每次采集都校准。
6. 超简总结(记住这句就够)
ADC 校准 = 让 ADC 自动修正内部硬件误差,保证采集准确。
不校准也能跑,但数据不准;校准是官方强制要求的标准流程。
在 STM32F1 系列中,ADC1 与 ADC2 是两颗独立的 12 位 ADC 模块,共享 APB2 时钟与 ADCCLK,但在通道、内部连接、双 ADC 模式与中断 / DMA 上有明确区别。你当前代码仅用 ADC1,下面按 “共性→差异→应用” 逐步说明。
一、共性(两者完全一致)
• 精度:均为 12 位,范围 0~4095。
• 时钟:共享 APB2 分频得到的 ADCCLK,最大 ≤14 MHz。
• 基本配置:相同寄存器与库函数(ADC_Init()、ADC_Cmd() 等)。
• 转换方式:支持单次 / 连续、软件 / 硬件触发、规则组 / 注入组。
二、核心差异(F1 系列)
| 对比项 | ADC1 | ADC2 |
|---|---|---|
| 通道与引脚 | 16 个外部通道(IN0~IN15),对应固定 GPIO | 16 个外部通道(IN0~IN15),与 ADC1 共享同一组 GPIO |
| 内部通道 | 通道 16:温度传感器;通道 17:内部参考电压(Vrefint) | 通道 16/17:内部接地(VSS),不可用温度 / 参考电压 |
| 双 ADC 模式 | 主 ADC,可触发 ADC2 同步 / 交替工作 | 从 ADC,由 ADC1 或外部触发同步 |
| 中断 / DMA | 独立中断向量(ADC1_IRQn)、独立 DMA 请求 |
独立中断向量(ADC2_IRQn)、独立 DMA 请求 |
| 校准 | 必须单独校准(ADC_ResetCalibration()/ADC_StartCalibration()) |
必须单独校准,校准数据存于各自寄存器 |
关键说明
1. 通道共享不冲突:同一引脚同一时刻只能给一个 ADC 用(例如 PA0 只能归 ADC1 或 ADC2 其一)。
2. ADC2 无内部传感器:温度、Vrefint 仅 ADC1 可用;ADC2 内部通道接地,只能测外部信号。
3. 双 ADC 模式价值:
◦ 同步采样:两 ADC 同时采同一通道,平均后降噪、提升 SNR。
◦ 交替采样:两 ADC 交替采同一通道,等效采样率翻倍,适合高频信号。
◦ 独立并行:各采不同通道,互不干扰。
4. 配置要点:双模式需在 ADC_Init() 中设置 ADC_Mode 为非独立模式,并按数据手册配置触发源。

三、与你代码的关系
你当前代码仅用 ADC1,所以无需考虑 ADC2:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 仅使能 ADC1 时钟
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_Cmd(ADC1, ENABLE);
// 校准仅针对 ADC1
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
若要使用 ADC2,需额外:
1. 使能 ADC2 时钟:RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2, ENABLE);
2. 初始化 ADC2 并单独校准。
3. 分配不冲突的 GPIO 通道。
四、没用到的相关函数(F1 系列 ADC 通用)
1. ADC_DeInit(ADCx):将 ADCx 寄存器恢复默认值,方便多 ADC 切换。
2. ADC_GetFlagStatus()/ADC_ClearFlag():查询 / 清除转换结束、溢出等标志(非中断方式)。
3. ADC_GetITStatus()/ADC_ClearITPendingBit():中断方式下查询 / 清除中断标志。
4. ADC_DMACmd(ADCx, ENABLE):使能 ADCx 的 DMA 请求(每转换一次触发一次 DMA)。
5. ADC_ExternalTrigConvConfig():配置硬件触发源(定时器 TRGO 等),替代软件触发。
6. ADC_InjectedChannelConfig():配置注入通道(优先级更高,常用于突发采样)。
7. ADC_SoftwareStartInjectedConvCmd():软件触发注入组转换。
五、超简总结
• ADC1 与 ADC2 独立但共享时钟 / 引脚:通道、内部连接、中断 / DMA 各自独立。
• ADC2 无温度 / 参考电压通道:只能测外部信号。
• 双 ADC 模式:同步降噪、交替提速、独立并行,按需使用。
• 校准必须单独做:两 ADC 各有校准寄存器。
规则组和注入组的区别
一句话核心结论
规则组 = 正常排队办事(普通通道)
注入组 = 插队办事(紧急通道)
一、最通俗的比喻
想象 ADC 是银行柜台:
1. 规则组(Regular Group)
◦ 就是普通顾客
◦ 乖乖排队,一个一个来
◦ 人多必须排队等待
◦ 平时 99% 的情况都用它
2. 注入组(Injected Group)
◦ 就是 VIP / 紧急客户
◦ 可以直接插队!
◦ 不管规则组有没有在转换,它都能打断、优先转换
◦ 转换完,再让规则组继续刚才的工作
二、专业版区别(超级清晰)
1. 优先级(最关键)
• 注入组 > 规则组
• 注入组可以随时打断规则组
• 规则组必须等注入组完成才能继续
2. 数量
• 规则组:最多 16 个通道 排队
• 注入组:最多 4 个通道 排队
3. 使用场景
• 规则组:日常采集、普通传感器、电压采集(你现在代码用的)
• 注入组:紧急采集、快速响应、需要立刻采样的信号
4. 数据寄存器
• 规则组:共用 1 个数据寄存器
(多通道必须快速读走,否则会覆盖)
• 注入组:每个通道独立寄存器
(4 个通道 = 4 个寄存器,不会覆盖)
5. 触发方式
• 都可以软件 / 硬件触发
• 但注入组触发立刻执行
三、用你的代码举例
你现在用的是:
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
这就是 规则组通道,也就是普通排队模式。
如果是注入组,函数是:
ADC_InjectedChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5);
四、完整对比表格(最好记)
| 项目 | 规则组 (Regular) | 注入组 (Injected) |
|---|---|---|
| 优先级 | 低 | 高(可插队) |
| 通道数量 | 最多 16 个 | 最多 4 个 |
| 数据寄存器 | 共用 1 个 | 每个通道独立 |
| 使用场景 | 常规采集 | 紧急、快速、实时 |
| 你的代码 | ✅ 正在使用 | ❌ 未使用 |
| 中断标志 | EOC | JEOC |
五、超级直白总结(必背)
1. 规则组 = 普通排队你现在的代码、绝大多数项目,都只用这个。
2. 注入组 = 紧急插队只有特别高要求、需要立刻采样时才用。
3. 注入组可以打断规则组,规则组不能打断注入组。
江科大多通道代码
#include "stm32f10x.h" // Device header
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*不在此处配置规则组序列,而是在每次AD转换前配置,这样可以灵活更改AD转换的通道*/
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //连续转换,失能,每转换一次规则组序列后停止
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //扫描模式,失能,只转换规则组的序列1这一个位置
ADC_InitStructure.ADC_NbrOfChannel = 1; //通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*ADC使能*/
ADC_Cmd(ADC1, ENABLE); //使能ADC1,ADC开始运行
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
}
/**
* 函 数:获取AD转换的值
* 参 数:ADC_Channel 指定AD转换的通道,范围:ADC_Channel_x,其中x可以是0/1/2/3
* 返 回 值:AD转换的值,范围:0~4095
*/
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5); //在每次转换前,根据函数形参灵活更改规则组的通道1
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发AD转换一次
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待EOC标志位,即等待AD转换结束
return ADC_GetConversionValue(ADC1); //读数据寄存器,得到AD转换的结果
}
他这个实际上就是单通道,只不过用了四次,根本就没有用到扫描模式。
扫描模式改良版:
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4]; //定义用于存放AD转换结果的全局数组
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*规则组通道配置*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //规则组序列2的位置,配置为通道1
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //规则组序列3的位置,配置为通道2
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //规则组序列4的位置,配置为通道3
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换,使能,每转换一次规则组序列后立刻开始下一次转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式,使能,扫描规则组的序列,扫描数量由ADC_NbrOfChannel确定
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数,为4,扫描规则组的前4个通道
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据宽度,选择半字,对应16为的ADC数据寄存器
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能,始终以ADC数据寄存器为源
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器基地址,给定存放AD转换结果的全局数组AD_Value
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器数据宽度,选择半字,与源数据宽度对应
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器,ADC数据寄存器转到数组
DMA_InitStructure.DMA_BufferSize = 4; //转运的数据大小(转运次数),与ADC通道数一致
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式,与ADC的连续转换一致
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能,数据由ADC外设触发转运到存储器
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA和ADC使能*/
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA1的通道1使能
ADC_DMACmd(ADC1, ENABLE); //ADC1触发DMA1的信号使能
ADC_Cmd(ADC1, ENABLE); //ADC1使能
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
/*ADC触发*/
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
}
扫描模式必须用到DMA
主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
AD_Init(); //AD初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
OLED_ShowNum(1, 5, AD_Value[0], 4); //显示转换结果第0个数据
OLED_ShowNum(2, 5, AD_Value[1], 4); //显示转换结果第1个数据
OLED_ShowNum(3, 5, AD_Value[2], 4); //显示转换结果第2个数据
OLED_ShowNum(4, 5, AD_Value[3], 4); //显示转换结果第3个数据
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间
}
}
后面一章将会学习到DMA,简单介绍一下。
1. DMA 到底是什么?
一句话:DMA = 直接内存存取
不用 CPU 插手,外设自动把数据传到内存里。
正常人理解版比喻:
• CPU = 老板
• ADC = 快递员
• 内存数组 = 仓库
没有 DMA(你原来单通道模式):
快递员(ADC)送完一个包裹 → 喊老板(CPU)老板亲自跑过去把包裹搬进仓库 → CPU 读数据老板闲不下来,一直在搬运。
有 DMA:
快递员(ADC)送完一个包裹 → 直接扔仓库老板(CPU)完全不用管,该干嘛干嘛!DMA 就是自动搬运工。
2. 官方定义(精简)
DMA:Direct Memory Access直接存储器访问硬件通道,外设 ↔ 内存之间直接传输数据,不经过 CPU,CPU 不参与、不占用 CPU 资源。
3. 重点问题:
为什么【ADC 扫描模式】必须用 DMA???
我结合你 PA0 PA1 PA2 PA3 4 通道扫描 给你讲原理!
扫描模式发生了什么?
你软件触发一次:ADC 自动连续采 4 次CH0 → CH1 → CH2 → CH3
ADC 只有1 个数据寄存器 DR!!!
过程是这样:
1. ADC 采完 CH0 → 存入 DR
2. ADC立刻采 CH1 → 直接覆盖 DR 里 CH0 的数据!!!
3. ADC 立刻采 CH2 → 覆盖 CH1
4. ADC 立刻采 CH3 → 覆盖 CH2
CPU 反应速度远远慢于 ADC!等 CPU 读到 DR 时,只剩下最后一个 CH3 的数据,前面 3 个全丢了!!!
这就是扫描模式最大问题:数据会快速覆盖,CPU 来不及读,数据丢失!
DMA 解决了这个致命问题:
DMA 监控 ADC 数据寄存器 DRADC 每采完一个,DMA 立刻自动复制到数组里!
顺序:ADC 采 CH0 → DMA 自动复制到 AD_Value [0]ADC 采 CH1 → DMA 自动复制到 AD_Value [1]ADC 采 CH2 → DMA 自动复制到 AD_Value [2]ADC 采 CH3 → DMA 自动复制到 AD_Value [3]
CPU 完全不用管!4 个数据完整保存,一个都不会丢!
4. 总结:为什么扫描必须 DMA?
1. 扫描模式:一次触发,连续自动采多个通道
2. ADC 只有一个数据寄存器,数据会快速覆盖
3. CPU 速度太慢,来不及逐个读取,前面数据丢失
4. DMA = 硬件自动搬运,ADC 存一个,DMA 搬一个到数组
5. CPU 零占用,数据完整不丢失
5. 对比你原来的【非扫描单通道】
你之前代码:一次只采 1 个通道 → ADC 停下 → CPU 慢慢读 → 不会覆盖→ 不需要 DMA
现在扫描模式:一次采 4 个,不停连续转换 → 数据疯狂覆盖→ 必须 DMA
6. 超简终极口诀(背这个就够)
• 单通道 ADC:CPU 自己读 → 不用 DMA
• 多通道扫描 ADC:数据自动覆盖 → 必须 DMA 自动搬运
• DMA:外设自动传内存,CPU 不干活、不占用、不丢数据
更多推荐



所有评论(0)