STM32 ADC 模数转换 学习笔记

基于示例工程:D:\下载\STM32入门教程资料\程序源码\STM32Project-有注释版\7-1 AD单通道
硬件:STM32F103C8 + 标准外设库 V3.5.0(Keil 工程)
前置:已学完 GPIO、中断、定时器、PWM。ADC 是单片机第一次真正"接触模拟世界"的地方——它让只会说 0 和 1 的单片机,能读出连续变化的电压


一、什么是 ADC?为什么需要它?

1.1 回到《前置基础》的一句话

你的单片机引脚只会输出高/低电平(0 / 1),它是数字世界的产物。但现实世界是模拟的:电位器旋钮的角度、温度传感器的电压、声音的波形……都是连续变化的。

ADC(Analog-to-Digital Converter,模数转换器)就是把"连续变化的电压"翻译成"数字 0~4095"的硬件。

电位器分压电压 0~3.3V(模拟、连续)
        │ ADC 转换
        ▼
数字值 0~4095(数字、离散)
        │ 公式换算
        ▼
0.00V ~ 3.30V(又变回人能读懂的电压)

一句话:ADC 是数字单片机看模拟世界的"眼睛"。

1.2 生活类比

想象一把只有 4096 个刻度的尺子,量程 0~3.3V。ADC 就是拿这把尺子去量引脚上的电压,量出来是"第几格",就输出几。

  • 电压 0V → 第 0 格 → 输出 0
  • 电压 1.65V → 第 2048 格 → 输出 2048
  • 电压 3.3V → 第 4095 格 → 输出 4095

刻度之间那些"细到量不出来"的部分,就是 量化误差(见 2.2)。刻度的总数,就是 分辨率


二、ADC 的核心参数(先建立概念)

2.1 分辨率(Resolution)

STM32F103 的 ADC 是 12 位的,所以结果范围是 0 ~ 4095(共 4096 个值)。

能分辨的最小电压 = 满量程 ÷ 2^位数 = 3.3V ÷ 4096 ≈ 0.0008V = 0.8mV
  • 位数越多 → 分辨率越高 → 量得越精细(12位、16位、24位…)
  • 12 位在单片机里是很常见的折中:精度够用,转换快,寄存器正好 16 位能装下

2.2 量化与量化误差

把连续电压"分段"成 4096 格,就是量化。落在两格之间的真实电压,会被"就近取整"到某一格,这个抹掉的误差叫量化误差,最大不超过半个格(约 0.4mV)。量化误差是 ADC 天生的,无法消除,只能通过提高位数减小。

2.3 参考电压(VREF)

ADC 的"满量程"由参考电压决定:输入电压到达 VREF 时,ADC 输出最大(4095)。

  • STM32F103 的 VREF+ 通常直接接 3.3V(即 VDDA),所以满量程是 3.3V。
  • 换算公式里那个 * 3.3 就是参考电压。
  • 输入电压不能超过参考电压! 超过会烧引脚。所以外部信号进 PA0 前,必须确保在 0~3.3V 范围内。

2.4 转换时间与采样保持

ADC 转换不是瞬时的,需要时间。转换前还要采样——把引脚电压"抓住"并保持住,让内部电路稳定测量。

  • 采样时间越长,结果越准,但转换越慢。
  • STM32F103 ADC 时钟上限 14MHz,本项目配成 12MHz(见 4.2)。
  • 本项目采样时间 55.5 个周期,总转换时间 ≈ (55.5 + 12.5) ÷ 12MHz ≈ 5.7μs,约每秒 17 万次——对"读电位器"来说绰绰有余。

三、STM32F103 的 ADC 资源

资源 数量 说明
ADC 外设 3 个(ADC1 / ADC2 / ADC3) 本项目用 ADC1
通道 每个 ADC 有 16 个外部通道(0~15) 通道0 = PA0,通道1 = PA1 …
分辨率 12 位 结果 0~4095
规则组 / 注入组 规则组可排最多 16 路;注入组是"插队"的紧急转换 本项目只用规则组单通道
触发方式 软件触发 / 定时器触发 / 外部引脚触发 本项目软件触发

通道和引脚的对应关系(STM32F103,记住常用的前几个):

通道 引脚 通道 引脚
通道0 PA0 通道4 PA4
通道1 PA1 通道5 PA5
通道2 PA2 通道6 PA6
通道3 PA3 通道7 PA7

另外还有两个内部通道:通道16 = 内部温度传感器,通道17 = 内部参考电压(1.2V 左右)。测芯片温度用通道16。


四、逐行精讲 AD_Init(AD.c)

4.1 开启时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);   // 开 ADC1 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // 开 GPIOA 时钟
  • ADC1 在 APB2 总线上(所以用 RCC_APB2PeriphClockCmd),GPIOA 也在 APB2。
  • 老规矩:用外设先开时钟。

4.2 设置 ADC 时钟

RCC_ADCCLKConfig(RCC_PCLK2_Div6);   // 72MHz / 6 = 12MHz
  • ADC 有自己独立的时钟,由 APB2(72MHz)分频得到。
  • 上限是 14MHz。所以 72MHz 只能分频成:72/2=36(超限❌)、72/4=18(超限❌)、72/6=12MHz(✅)、72/8=9MHz(✅)。
  • 本项目用 6 分频 = 12MHz,在允许范围内,速度也够快。

4.3 配置 GPIO:PA0 为模拟输入

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(Analog Input,模拟输入) 是 GPIO 的又一个新模式。对照你学过的模式:

模式 用途
Out_PP 普通推挽输出(LED)
AF_PP 复用推挽输出(PWM 等外设控制输出)
IPU / IPD / IN_FLOATING 数字输入(按键)
AIN 模拟输入(ADC 专用)

为什么用 AIN 读模拟电压时,引脚内部的数字输入缓冲器必须断开(否则数字电路会干扰模拟信号),让电压直接进入 ADC 的采样电路。这个模式把引脚内部配置成"纯模拟通路"。

4.4 规则组通道配置

ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

这句的含义:把"通道0(PA0)"放进 ADC1 规则组序列的第 1 位,采样时间 55.5 个周期。

  • ADC_Channel_0:要转换的通道(PA0)。
  • 1:在规则组序列里的位置。本项目只转换一个通道,放第 1 位即可。
  • ADC_SampleTime_55Cycles5:采样时间。可选 1.5 / 7.5 / 13.5 / 28.5 / 41.5 / 55.5 / 71.5 / 239.5 个周期。采样时间越长越准但越慢,新手用 55.5 或 71.5 更稳。

4.5 ADC 初始化(核心,参数最多)

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;                 // 非扫描(只转一路)
ADC_InitStructure.ADC_NbrOfChannel = 1;                       // 通道数 = 1
ADC_Init(ADC1, &ADC_InitStructure);

逐项解释(每个参数都对照"单通道读一次电位器"这个需求来理解):

参数 本项目值 含义 别的可能
ADC_Mode 独立 只用 ADC1 双 ADC 同步/交叉模式
ADC_DataAlign 右对齐 12 位结果靠右存,高 4 位补 0 左对齐(读高 8 位用)
ADC_ExternalTrigConv None 软件触发 定时器触发(自动连续采样)
ADC_ContinuousConvMode DISABLE 单次转换:转完一次就停 连续转换:自己不停转
ADC_ScanConvMode DISABLE 非扫描:只转序列第 1 位 扫描:把序列里多个通道依次转完
ADC_NbrOfChannel 1 序列里通道个数 多通道时才 >1

两个容易混的概念(重点):

  • 单次 vs 连续(ContinuousConvMode):单次 = 你喊一声转一次;连续 = 转换完自动又开始下一次,像流水一样不停。
  • 非扫描 vs 扫描(ScanConvMode):非扫描 = 只转序列第 1 位;扫描 = 把序列里排好的多个通道依次全转一遍(转完一轮产生 EOC)。

本项目:单次 + 非扫描 = "手动喊一次,转一路"。最简单。

4.6 使能 ADC

ADC_Cmd(ADC1, ENABLE);

启动 ADC。老规矩,别漏。

4.7 ADC 校准(固定流程,照抄)

ADC_ResetCalibration(ADC1);                       // 复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);  // 等复位完成
ADC_StartCalibration(ADC1);                       // 开始校准
while (ADC_GetCalibrationStatus(ADC1) == SET);    // 等校准完成
  • ADC 内部有电容等模拟电路,上电后会有微小偏差。校准就是让 ADC 自己测出偏差并修正
  • 这是固定流程:每次上电使能后都要执行一次,照着写即可,不用深究。
  • 校准完成后,ADC 才能放心使用。

五、AD_GetValue 逐行(怎么真正读一个值)

uint16_t AD_GetValue(void)
{
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);                   // ① 软件触发,开始转换
    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);   // ② 等转换结束
    return ADC_GetConversionValue(ADC1);                      // ③ 读结果
}

三个动作对应"喊一声 → 等它量完 → 拿结果":

  1. 触发:因为配置的是软件触发,这里喊一声"开始转换"。转换是硬件自动进行的,CPU 不等它。
  2. 等待 EOC 标志:EOC = End Of Conversion(转换结束)。硬件转完会把这个标志位置 1。这里用轮询——不断问"好了吗?",没好就一直等。(这是最简单的方式;以后可换成中断/DMA,CPU 就不用干等。)
  3. 读结果:从 ADC 的数据寄存器读走 12 位结果(0~4095)。

注意:读数据寄存器后,EOC 标志会自动清除,下次可以继续触发。


六、电压换算公式(main.c 的精华)

ADValue = AD_GetValue();                    // 0~4095
Voltage = (float)ADValue / 4095 * 3.3;      // 换算成 0~3.3V

6.1 公式推导

12 位 ADC,输入 0V 对应 0,输入 3.3V(参考电压)对应 4095,是线性关系:

电压 = AD值 ÷ 最大AD值 × 参考电压 = ADValue / 4095 × 3.3
ADValue 计算 电压
0 0/4095×3.3 0.00V
2048 2048/4095×3.3 ≈ 1.65V
4095 4095/4095×3.3 3.30V

6.2 为什么要有 (float)ADValue / 4095

ADValue 是整数(uint16_t)。整数除法 ADValue / 4095 会把结果截断成 0 或 1,再乘以 3.3 就只有 0 或 3.3 两个值,完全错误。

(float)ADValue 把 ADValue 强制转成浮点数,之后整个除法就是浮点除法,小数就保住了。这是单片机编程里最常见的坑之一:整数除法丢小数。

错误:ADValue / 4095 * 3.3      // 先整数除法,结果≈0或1
正确:(float)ADValue / 4095 * 3.3  // 先转浮点,保留小数

6.3 OLED 显示小数的一个技巧

OLED_ShowNum(2, 9, Voltage, 1);                            // 整数部分
OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);   // 小数部分

OLED 显示不了浮点,于是拆开:

  • 整数部分直接显示 Voltage(隐式转成整数)。
  • 小数部分:Voltage * 100 把小数移到整数位(如 1.657→165.7),取整后 % 100 取出后两位(→65),显示成 .65

小提示:OLED_ShowNum(2, 9, Voltage, 1) 里直接把 float 传给形参(uint32_t),C 会隐式截断成整数,能跑但对编译器不够友好,写成 (uint16_t)Voltage 更规范。


七、main.c 主程序逻辑

uint16_t ADValue;   // AD 原始值
float    Voltage;   // 换算后的电压

int main(void)
{
    OLED_Init();
    AD_Init();

    OLED_ShowString(1, 1, "ADValue:");
    OLED_ShowString(2, 1, "Voltage:0.00V");

    while (1)
    {
        ADValue = AD_GetValue();                    // 读一次 AD 值
        Voltage = (float)ADValue / 4095 * 3.3;      // 换成电压
        OLED_ShowNum(1, 9, ADValue, 4);             // 显示 AD 值
        OLED_ShowNum(2, 9, Voltage, 1);             // 显示电压整数
        OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);  // 显示电压小数
        Delay_ms(100);                              // 100ms 读一次
    }
}

流程:初始化 → 主循环里"读一次 → 换算 → 显示 → 延时 100ms → 再读"。旋动电位器,OLED 上的电压实时变化。

Delay_ms(100) 控制采样节奏:每 100ms 刷新一次显示。想刷更快就减小延时。


八、硬件接线与注意事项

电位器(或任何 0~3.3V 可调电压)──► PA0
                                    GND 共地
  • 输入电压必须 0~3.3V。超过 3.3V 可能烧 ADC/引脚;超过后结果会"卡"在 4095 不再变化(也叫"饱和")。
  • 如果信号本身可能是 0~5V(比如某些传感器输出),要先分压(两个电阻分压)再进 PA0,不能直接接。
  • 信号源要稳定;如果读到的值上下乱跳,可以在 AD 值上加滤波(多读几次取平均,见练习 3)。

九、常见问题与调试

现象 可能原因 排查方法
读数恒为 0 PA0 没接信号 / 悬空 给 PA0 接一个明确电压(如接 3.3V)看是否变 4095
读数恒为 4095 输入电压超过参考电压 确认输入在 0~3.3V;检查是否错接了 5V
读数跳变厉害 信号本身不稳 / 没滤波 软件多采几次取平均(见练习3)
读数没变化 忘了触发 / 没等 EOC / 引脚配错通道 确认 AD_GetValue 三步齐全;确认通道0=PA0
换成其他脚不工作 通道和引脚没对应 查通道表:PA1=通道1,PA2=通道2…
精度差 ADC 时钟超 14MHz / 采样时间太短 用 6 或 8 分频;采样时间用大档

万能排查顺序:先确认输入电压在范围内 → 再看引脚是不是 AIN 模式 → 再查通道号对不对 → 最后查转换流程(触发/等待/读)


十、动手扩展练习

  1. 多通道扫描:把 PA0、PA1、PA2 三个通道都接上,配扫描模式,一次转完三路,分别显示。
  2. ADC + 中断:把"等待 EOC"从轮询改成中断(ADC_ITConfig + ADC_IRQHandler),转完自动进中断读值,CPU 不再干等。
  3. 软件滤波(均值):连续读 10 次取平均,观察读数是不是更稳定。
  4. 定时器触发 + 连续转换 + DMA:让 ADC 以固定频率自动采样,用 DMA 把结果搬进数组——这是做示波器/音频采集的雏形,也是 ADC 的最高效用法。
  5. 内部温度传感器:用通道16 读芯片温度(需要参考手册里的计算公式)。
  6. AD 值控制 PWM:读电位器 → 映射到占空比 → 控制 LED 亮度或舵机角度。把 ADC 和前面的 PWM 串起来!

附录:本工程用到的标准库函数

函数 作用
RCC_ADCCLKConfig(RCC_PCLK2_Div6) 设置 ADC 时钟 = APB2 / 6 = 12MHz
ADC_RegularChannelConfig(ADC1, ch, rank, time) 把某个通道放进规则组某位置,设采样时间
ADC_Init(ADC1, &init) 配置 ADC 模式/对齐/触发/单次连续/扫描
ADC_Cmd(ADC1, ENABLE) 使能 ADC
ADC_ResetCalibration / ADC_StartCalibration 校准(固定流程)
ADC_SoftwareStartConvCmd(ADC1, ENABLE) 软件触发一次转换
ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) 查转换结束标志
ADC_GetConversionValue(ADC1) 读转换结果(0~4095)

本篇一句话总结:ADC 把 0~~3.3V 的模拟电压"量化"成 12 位的数字 0~~4095。工程里做对了三件事——PA0 配成模拟输入(AIN)、ADC 时钟分频到 12MHz、单通道单次软件触发;读值时"软件触发 → 等 EOC → 读数据寄存器"三步走;最后用 Voltage = ADValue/4095*3.3 换回电压,别忘了 (float) 防止整数除法丢小数。从此单片机就能"看见"模拟世界了。