STM32F103 ADC从入门到精通:原理、配置与精度优化实战
2026/8/28 23:10:25 网站建设 项目流程

1. 项目概述:从“芯”开始,理解STM32的模拟世界入口

拿到一块STM32F103的开发板,看着上面密密麻麻的引脚,你可能会想,我能用它做什么?点亮LED、驱动屏幕、控制电机,这些都是数字世界的游戏。但真实世界是连续的、模拟的——温度在细微变化,光线有明暗强弱,声音是连续的波形。如何让这颗数字“大脑”感知这个模拟世界?答案就是ADC,模数转换器。这不仅仅是STM32F103众多外设中的一个,更是连接数字处理器与真实物理世界的桥梁。对于任何涉及传感器采集、信号监控、电池管理或音频处理的项目,ADC都是你必须打通的第一个关键环节。无论你是刚接触嵌入式开发的新手,还是想深入理解MCU外设的老鸟,搞懂STM32F103的ADC,就意味着你拿到了开启物联网、智能硬件、工业控制等众多领域大门的钥匙。它远不止是配置几个寄存器那么简单,其性能的优劣、稳定性的高低,直接决定了你整个系统感知世界的“灵敏度”和“可信度”。接下来,我将以一个资深嵌入式工程师的视角,带你从原理到实践,从配置到优化,彻底吃透这颗经典Cortex-M3芯片的ADC模块。

2. ADC核心原理与STM32F103特性深度解析

2.1 模数转换的本质:从连续到离散的艺术

要玩转ADC,首先得明白它在干什么。想象一下你要用尺子测量一段弯曲的绳子长度。尺子有最小刻度(比如1毫米),你只能读到最接近的刻度值。ADC干的就是类似的事:它把连续变化的模拟电压(比如0到3.3V),按照一个固定的“刻度”(我们称之为分辨率),转换成一个个离散的数字值。

STM32F103系列芯片内置的是逐次逼近型ADC。它的工作原理很像用天平称重:你有一组已知重量的砝码(对应不同的数字位)。转换开始时,ADC先猜测一个中间值(比如最高位为1,其余为0),将这个数字值通过内部的数模转换器变回模拟电压,与输入的实际电压进行比较。如果猜大了,就把这一位清零;如果猜小了,就保留。然后依次猜测下一位,直到最低位。经过N次比较(N就是ADC的位数),就得到了最终的数字结果。这个过程非常快,STM32F103的ADC在最高时钟下完成一次12位转换仅需1微秒左右。

2.2 STM32F103 ADC的硬件家底与关键参数

STM32F103具体型号众多,但ADC模块的基本架构一致。以常见的STM32F103C8T6(俗称“蓝桥杯最小系统板”芯片)为例,它拥有2个独立的ADC单元(ADC1和ADC2),每个ADC有最多18个复用通道。这18个通道包括:

  • 16个外部模拟输入通道:对应芯片的GPIO引脚,如PA0~PA7, PB0~PB1等,用于连接外部传感器信号。
  • 2个内部信号通道:这是非常实用且容易被忽略的功能。
    • 通道16:连接至内部温度传感器。可以用来监测芯片结温,虽然精度不高(±1.5°C),但对于过热保护、性能补偿等应用足够了。
    • 通道17:连接至内部参考电压VREFINT。这是一个出厂时校准过的、非常稳定的电压基准(约1.2V),用于实现“测电池电压”等高级功能,我们后面会详细讲。

ADC的核心性能指标包括:

  • 分辨率:12位。这意味着它可以将参考电压范围划分为 2^12 = 4096 个等级。如果参考电压是3.3V,那么理论上它能分辨的最小电压变化是 3.3V / 4096 ≈ 0.8mV。这个值也叫作1个LSB
  • 转换时间:总转换时间 = 采样时间 + 12.5个ADC时钟周期。采样时间是可编程的,从1.5个周期到239.5个周期,时钟越慢、采样时间越长,对高阻抗信号源的采集就越准确,但速度也越慢。你需要根据信号源特性来权衡。
  • 参考电压:STM32F103的ADC参考电压默认来自VDDAVSSA引脚。这是一个极其重要的硬件设计要点VDDA必须接一个干净、稳定的电源,通常与主电源VDD(3.3V)相连,但强烈建议通过磁珠或电感隔离,并紧挨芯片放置一个10uF钽电容+一个100nF陶瓷电容进行去耦。如果VDDA有噪声,你的所有ADC读数都会跳动。

注意:很多新手采集的数值跳得厉害,第一个要怀疑的不是代码,而是硬件电源和地线是否干净。确保模拟地VSSA和数字地VSS在芯片下方单点连接,且VDDA的滤波电容必不可少。

3. 从零构建ADC采集工程:配置、采样与数据处理全流程

3.1 开发环境搭建与工程初始化

我们以最通用的Keil MDK + 标准外设库为例。首先,创建一个新的工程,选择正确的芯片型号(STM32F103C8)。在项目管理中,你需要添加关键的外设库文件:stm32f10x_adc.cstm32f10x_gpio.cstm32f10x_rcc.c(时钟配置),以及对应的头文件。

工程的第一步永远是配置系统时钟。STM32F103的ADC时钟来自APB2总线,最高不能超过14MHz(对于大多数型号)。通常我们会把系统时钟设置为72MHz,然后对APB2预分频,让ADC时钟为12MHz(72/6=12),这是一个常用且安全的频率。

// 系统时钟初始化 (以72MHz为例) RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET); RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() != 0x08); // 设置APB2时钟为72MHz(不分频),ADC时钟需要后续分频 RCC_HCLKConfig(RCC_SYSCLK_Div1); RCC_PCLK2Config(RCC_HCLK_Div1); RCC_PCLK1Config(RCC_HCLK_Div2);

3.2 GPIO与ADC通道的硬件映射配置

假设我们使用PA1引脚(对应ADC1的通道1)来采集一个电位器的电压。首先需要将PA1配置为模拟输入模式,这是功耗最低、阻抗最高的模式,专门用于ADC。

// 1. 开启GPIOA和ADC1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); // 2. 配置PA1为模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure);

3.3 ADC模块的详细初始化与参数设定

这是核心配置部分,每一个参数都直接影响最终结果。

ADC_InitTypeDef ADC_InitStructure; // 1. ADC基本结构初始化 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式,ADC1单独工作 ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 非扫描模式(单通道) ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐(推荐) ADC_InitStructure.ADC_NbrOfChannel = 1; // 转换通道数为1 ADC_Init(ADC1, &ADC_InitStructure); // 2. 配置ADC时钟(ADCCLK)。系统时钟72MHz,6分频得到12MHz RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 3. 配置指定通道的转换顺序和采样时间 // 规则组通道1,采样时间为239.5周期(在12MHz下约20us,适合较高阻抗信号源) ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); // 4. 使能ADC ADC_Cmd(ADC1, ENABLE); // 5. ADC上电并校准(必须步骤!) ADC_ResetCalibration(ADC1); // 复位校准寄存器 while(ADC_GetResetCalibrationStatus(ADC1)); // 等待复位完成 ADC_StartCalibration(ADC1); // 开始校准 while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成

关键参数解读

  • ADC_SampleTime_239Cycles5:采样时间。对于电位器(输出阻抗可能在10kΩ量级),较长的采样时间可以确保内部采样电容充放电充分,得到稳定值。如果信号源阻抗很低(如运放输出),可以缩短采样时间以提高转换速率。
  • 校准:这一步至关重要!ADC内部存在偏移和增益误差,上电后必须执行一次校准,将校准值存入内部寄存器,后续转换会自动补偿。漏掉这一步,精度会大打折扣。

3.4 单次转换与数据读取的实现

配置完成后,就可以启动转换并读取结果了。

uint16_t Get_ADC_Value(uint8_t ch) { // 重新配置通道(如果通道变化) ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发转换 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换结束 return ADC_GetConversionValue(ADC1); // 读取12位转换结果 } // 在主循环中调用 adc_value = Get_ADC_Value(ADC_Channel_1);

读取到的adc_value是一个0-4095之间的数字。要转换成电压,需要公式:Voltage = (adc_value / 4095.0) * Vref。这里的Vref就是你的VDDA电压,通常是3.3V。

3.5 进阶应用:连续转换、DMA与多通道扫描

单次转换模式简单,但效率低。在实际项目中,如需要实时波形采集,必须使用连续转换模式配合DMA

连续转换模式:设置ADC_ContinuousConvMode = ENABLE,一旦启动,ADC就会马不停蹄地一个接一个进行转换,结果寄存器会被最新结果覆盖。

DMA(直接存储器访问):让DMA硬件自动把ADC数据寄存器(ADC_DR)里的值搬运到我们定义在内存中的数组里,完全不需要CPU干预。这实现了高速、不间断的数据流。

多通道扫描:设置ADC_ScanConvMode = ENABLE,并配置多个通道和顺序(ADC_RegularChannelConfig),ADC会自动按顺序转换多个通道。

一个典型的“连续转换+DMA+多通道扫描”配置步骤如下:

  1. 初始化DMA(设置源地址为ADC1->DR,目标地址为数组,数据宽度为半字,循环模式)。
  2. 配置ADC为扫描模式、连续转换模式。
  3. 配置所有需要用到的通道及其采样时间、顺序。
  4. 使能ADC的DMA请求(ADC_DMACmd(ADC1, ENABLE))。
  5. 使能DMA,然后启动ADC转换。
  6. 此后,目标数组里就会自动被填满循环采集的数据,CPU可以随时去处理这个数组。
// 简化的DMA配置部分代码 uint16_t ADC_ConvertedValue[2]; // 用于存放两个通道的数据 DMA_InitTypeDef DMA_InitStructure; // ... 配置DMA1的通道1(与ADC1固定对应) DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_ConvertedValue; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 2; // 两个数据 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); // 在ADC初始化中使能扫描和连续模式,并关联DMA ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // ... 其他初始化 ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // ... 校准 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动后便永不停止

这种模式下,ADC_ConvertedValue[0]ADC_ConvertedValue[1]会自动更新为通道1和通道2的最新值,效率极高。

4. 精度提升实战:滤波、校准与参考电压技巧

4.1 软件滤波算法:让数据变得“沉稳”

ADC采集到的原始数据总是带有毛刺和噪声。硬件上要做好滤波(比如在输入引脚加一个100pF~10nF的对地电容),软件上则必须进行滤波处理。

  • 算术平均滤波:最简单有效。连续采样N次,求和后取平均。N越大,平滑效果越好,但实时性越差。适用于变化缓慢的信号(如温度、电池电压)。

    #define N 10 uint32_t sum = 0; for(int i=0; i<N; i++) { sum += Get_ADC_Value(ADC_Channel_1); // 可以适当加微小延时,避免采样点过于集中 } uint16_t result = sum / N;
  • 滑动平均滤波:维护一个长度为N的队列,每次新数据入队,最老数据出队,然后计算队列平均值。这种方式既能滤波,又能保持较好的实时性。

    #define FILTER_LEN 10 uint16_t filter_buf[FILTER_LEN] = {0}; uint8_t filter_index = 0; uint32_t filter_sum = 0; // 每次采集后调用 filter_sum -= filter_buf[filter_index]; // 减去最旧的值 filter_buf[filter_index] = Get_ADC_Value(ADC_Channel_1); filter_sum += filter_buf[filter_index]; // 加上最新的值 filter_index = (filter_index + 1) % FILTER_LEN; uint16_t filtered_value = filter_sum / FILTER_LEN;
  • 中位值平均滤波:结合了中值滤波和平均滤波的优点。连续采样N个数据,去掉一个最大值和一个最小值,然后对剩下的N-2个数据求平均。能有效抑制脉冲干扰。

实操心得:对于大多数嵌入式应用,一个简单的滑动平均滤波(N取8~16)就足以应对90%的场景。关键在于根据信号的实际频率选择合适的窗口大小N。一个快速判断方法是:观察原始数据跳动的范围,应用滤波后,跳动范围应显著缩小,同时信号的真实变化趋势不应有肉眼可见的延迟。

4.2 利用内部参考电压VREFINT测量实际VDDA电压

这是STM32 ADC应用中的一个高级技巧,也是提高测量精度的关键。我们之前计算电压的公式依赖于VDDA精确等于3.3V。但现实中,LDO有误差,电池会放电,VDDA可能只有3.2V或3.25V。用一个不准确的参考电压去计算,结果自然不准。

STM32在芯片内部提供了一个高精度、出厂时已校准的基准电压VREFINT(约1.2V)。它的特点是不随外部VDDA变化(只要在允许范围内)。我们可以通过以下步骤反推出真实的VDDA

  1. 在ADC初始化时,除了配置外部通道,也配置内部通道17(ADC_Channel_17)。
  2. 采集VREFINT通道的ADC原始值,记为ADrefint
  3. 查阅芯片数据手册的“电气特性”章节,找到VREFINT在30°C下的典型校准值,记为VREFINT_CAL(例如,对于F103,这个值存储在芯片系统存储区的特定地址,通常通过*((uint16_t *)0x1FFFF7BA)读取)。
  4. 真实VDDA的计算公式为:Vdda_actual = VREFINT_CAL * 4095 / ADrefint

得到真实的VDDA后,再用它去计算其他外部通道的电压,精度将大幅提升。这个方法常用于精确测量电池电压(通过电阻分压)。

// 获取内部参考电压的ADC值 uint16_t Get_Vrefint_ADC(void) { ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 1, ADC_SampleTime_239Cycles5); // ... 启动转换并读取,返回原始ADC值 } // 计算实际VDDA float Get_Actual_Vdda(void) { uint16_t vrefint_cal = *((uint16_t *)0x1FFFF7BA); // 读取校准值 uint16_t vrefint_adc = Get_Vrefint_ADC(); // 公式:Vrefint_cal (mV) / Vrefint_adc = Vdda (mV) / 4095 float vdda = (vrefint_cal * 4095.0) / vrefint_adc; return vdda; // 单位: mV }

4.3 温度传感器采集与实用化处理

STM32F103的内部温度传感器输出与结温成比例的电压,其线性度并不好,需要根据数据手册提供的公式进行计算。通常公式如下:Temperature (°C) = {(V25 - Vsense) / Avg_Slope} + 25其中:

  • V25:芯片在25°C时温度传感器的输出电压(典型值,查数据手册,如1.43V)。
  • Vsense:当前采集到的温度传感器电压(通过ADC值和实际VDDA计算得出)。
  • Avg_Slope:温度传感器的平均斜率(典型值,查数据手册,如4.3 mV/°C)。

重要提示:内部温度传感器的绝对精度很差(可能差±10°C),但相对变化是准的。所以它适合用于监测芯片温度的变化趋势,比如判断是否过热,而不是用于测量环境温度。使用前,必须开启ADC_Channel_16,并保证VDDA稳定。为了减少噪声,建议对Vsense进行大量采样平均(比如64次或更多)。

5. 典型问题排查与工程优化实录

5.1 ADC读数不稳定、跳动大的原因与对策

这是新手最常遇到的问题。请按以下清单逐一排查:

问题现象可能原因排查方法与解决方案
数值在小范围(如±5LSB)随机跳动正常量化噪声与电路热噪声这是ADC的本底噪声,无法完全消除。通过软件滤波(如滑动平均)可有效平滑。确保采样时间设置合理(对高阻抗信号源增加采样时间)。
数值跳动范围大(几十到上百LSB)电源噪声用示波器测量VDDAVSSA引脚,看是否有明显的纹波。确保VDDA滤波电容(10uF+100nF)紧贴芯片引脚焊接,且VDDA与数字VDD通过磁珠隔离。
模拟地与数字地处理不当检查PCB布局,确保模拟部分和数字部分地线分开走线,最后在芯片下方或电源入口处单点连接
信号源阻抗过高ADC输入引脚等效为一个采样电容通过一个开关连接到外部。如果信号源阻抗太高(如>10kΩ),在采样时间内电容无法充放电到稳定电压。解决方案:1) 增加ADC采样周期;2) 在信号源后接一个电压跟随器(运放)进行缓冲;3) 在ADC输入引脚对地加一个100pF~1nF的小电容(注意:这会形成一个RC低通滤波,可能影响信号带宽)。
电磁干扰模拟信号线过长且未屏蔽,靠近高频数字信号线(如时钟、PWM)。让模拟信号线尽量短,远离干扰源,必要时使用屏蔽线或双绞线。
读数固定偏差,或随温度漂移参考电压不准未使用内部VREFINT校准VDDA。按照4.2节的方法测量实际VDDA进行计算。
未执行校准ADC上电后忘记调用ADC_Calibration函数。必须在每次ADC上电后校准一次。
采集直流电压,读数却缓慢周期性变化工频干扰(50/60Hz)设备处于强交流电场环境。尝试将采样间隔设置为工频周期的整数倍(如20ms的倍数),然后对多个周期采样取平均,可以抵消这种干扰。或者改善设备屏蔽和接地。

5.2 多通道采样时的通道间串扰

当使用扫描模式快速切换多个通道时,可能会发现通道A的读数受到之前通道B信号的影响。这是因为ADC内部的采样保持电容在切换通道后没有充分放电。

解决方案

  1. 增加通道切换延时:在扫描序列中,在两个通道转换之间插入少量的额外采样时间。STM32的ADC允许为每个通道独立设置采样时间,对于容易受串扰的通道,可以适当延长其采样时间。
  2. 插入“哑元”通道:在扫描序列中,在两个需要高隔离度的通道之间,插入一个连接到固定电压(如地)的通道进行一次转换,让采样电容充分复位。
  3. 降低转换速率:如果对速度要求不高,可以降低ADC时钟频率,这本质上增加了所有通道的采样和保持时间。

5.3 低功耗应用下的ADC配置要点

在电池供电设备中,ADC的功耗也需要考虑。STM32F103的ADC在运行时有几个mA的电流。优化策略包括:

  • 按需启用:不采样时,调用ADC_Cmd(ADCx, DISABLE)关闭ADC模块。
  • 降低时钟:在满足采样速率的前提下,使用最低可行的ADC时钟(如6MHz甚至更慢)。
  • 关闭不用的外设时钟:采样完成后,关闭ADC和对应GPIO的时钟(RCC_APB2PeriphClockCmd(DISABLE))。
  • 使用间断模式:对于需要间歇性采样的应用,可以配置ADC的间断模式,使其在转换完指定序列后自动停止,等待下次触发。

5.4 DMA传输中的数据对齐与缓冲区管理

使用DMA进行连续传输时,如果配置不当,可能会遇到数据错位的问题。

  • 数据对齐:确保DMA的外设和存储器数据宽度设置一致(通常都是半字HalfWord,16位)。ADC数据寄存器是16位右对齐的,低16位有效。
  • 缓冲区溢出:在循环DMA模式下,DMA会不停地覆盖缓冲区。如果CPU处理数据的速度跟不上DMA填充的速度,就会丢失数据。解决方案有:
    • 使用“双缓冲区”机制:准备两个一样大的数组A和B。DMA先填满A,然后产生半传输完成中断传输完成中断,在中断里切换DMA的目标地址到B,并处理A中的数据。如此循环。
    • 使用DMA的“循环缓冲区”配合读/写指针:CPU维护一个读指针,DMA维护一个写指针(可通过DMA_GetCurrDataCounter函数间接推算)。CPU只处理读指针和写指针之间的数据,避免处理冲突。

我个人在多个量产项目中总结的经验是,STM32F103的ADC性能在12位分辨率下是足够可靠的,但其性能上限极度依赖良好的硬件设计和细致的软件配置。最容易出问题的环节往往在硬件:电源、地线和信号路径。在软件层面,上电校准、合理的采样时间配置以及有效的软件滤波是保证数据质量的三大支柱。把这三个环节做到位,你的ADC采集系统就成功了一大半。剩下的,就是根据具体应用场景,在速度、精度和功耗之间做出最适合的权衡了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询