STM32F103 ADC电压采集:从原理到高精度实现的实战指南
1. 从零开始:为什么STM32F103的ADC电压采集值得深究
如果你刚开始接触STM32,或者已经用它点过灯、调过串口,那么ADC(模数转换器)功能大概率是你遇到的第一个“模拟世界”接口。而STM32F103,作为经典的“蓝桥杯”神器和无数电子爱好者的入门首选,其内置的12位ADC更是项目中的常客。表面上看,配置ADC、读取一个电压值似乎很简单,网上例程一抓一大把。但当你真正把它用在一个要求稍高的项目里,比如做一个简易的万用表、电池管理系统(BMS)的电压监控,或者需要稳定读取传感器信号时,各种问题就会接踵而至:读数跳得厉害、采集的值和万用表对不上、多通道切换时数据错乱……这时候你才会发现,ADC这潭水,远比想象中深。
我最初用STM32F103做一个小型无人机的电池电压监控时,就踩过不少坑。明明电路分压算得好好的,代码也是从官方例程改的,但采集到的电压值总是不稳定,偶尔还会出现一个离谱的跳变,导致系统误判电池电量,差点酿成事故。后来经过一番折腾,从基准电压、采样时间、PCB布局到软件滤波,层层排查,才让采集结果稳定可靠。这个过程让我意识到,ADC采集不是一个配置完寄存器就一劳永逸的功能,它涉及硬件设计、时钟配置、采样时序、数据处理等一系列环环相扣的细节。
今天,我就以STM32F103C8T6这款最普遍的芯片为例,抛开那些简单的“三步读取电压”教程,深入聊聊如何实现一个高精度、高稳定性的ADC电压采集系统。我们会从最核心的ADC原理与STM32的硬件特性出发,一步步拆解CubeMX配置背后的门道,然后深入到多通道、DMA等高级应用,最后重点攻克软件滤波与校准这些决定最终精度的关键环节。无论你是想做一个精密的测量设备,还是仅仅想让你的产品读数更准一些,这里面的经验都值得你仔细琢磨。
2. 核心原理与硬件设计:别让第一步就埋下隐患
在动手写代码之前,我们必须先搞清楚两件事:STM32F103的ADC到底是怎么工作的?以及我们的硬件电路应该如何设计才能给它提供最好的“食材”?很多采集不准的问题,根子都出在这里。
2.1 STM32F103 ADC的“家底”与工作模式
STM32F103系列大多数型号内置了1到3个12位逐次逼近型(SAR)ADC。以常见的STM32F103C8T6为例,它拥有2个ADC(ADC1和ADC2),每个ADC最多有16个外部模拟输入通道(ADC1_IN0~ADC1_IN15,其中一些与ADC2复用)。此外,还有两个内部通道:一个连接温度传感器,一个连接内部参考电压VREFINT。这个12位的分辨率,意味着它可以将模拟输入电压(例如0-3.3V)量化为0到4095之间的一个数字值(ADC_Value)。理想情况下,电压 = (ADC_Value / 4095) * Vref,其中Vref是ADC的参考电压。
这里就引出了第一个关键点:参考电压Vref。对于STM32F103,Vref默认连接到VDDA(模拟电源)。VDDA必须和VSSA(模拟地)一起,从一个干净、稳定的电源引入,通常与数字电源VDD(3.3V)来自同一LDO,但需要通过磁珠或电感进行隔离,并配合去耦电容滤波。如果VDDA本身纹波很大,或者因为数字电路噪声而波动,那么你采集到的所有电压值都会跟着“跳舞”。所以,一个稳定的VDDA是ADC精度的生命线。
另一个核心概念是采样时间。ADC不是瞬间完成测量的。它内部有一个采样保持电容,需要时间(采样时间)来充电到与被测电压相同的电平。STM32F103允许你为每个通道单独配置采样时间,单位是ADC时钟周期。采样时间太短,电容充电不足,测量值就会偏低且不稳定;采样时间太长,虽然更稳定,但会降低整体采样速率,在多通道扫描时尤其明显。对于高阻抗的信号源(比如经过一个大电阻分压后的信号),必须设置足够长的采样时间。官方数据手册会给出不同源阻抗下的建议采样时间,这是一个必须查阅的黄金参数。
ADC的触发方式也决定了它的工作模式。你可以选择软件触发(调用HAL_ADC_Start()),或者用定时器、外部引脚等硬件触发。对于连续采集的场景,比如波形显示,硬件触发配合DMA是唯一的选择。
2.2 分压电路与输入保护:给ADC穿上“防护服”
绝大多数时候,我们测量的电压都会超过ADC的输入范围(0-Vref,通常0-3.3V)。这时就需要分压电路。计算分压电阻看起来是小学生数学,但选型不当就是灾难。
假设要测量0-12V的电池电压,采用经典的R1(上拉)、R2(下拉)分压,目标是将12V分压到3.0V左右(留一点余量)。你可能会随手选两个10kΩ电阻。但这里有个陷阱:ADC的输入引脚不是完全开路的,它有一个等效的输入阻抗和采样开关的漏电流。如果下拉电阻R2的阻值太大(比如100kΩ以上),ADC采样时微小的漏电流就会在R2上产生一个不可忽视的误差电压。一个实用的经验法则是:分压电路的下拉电阻R2建议在10kΩ以下,常见取2kΩ~10kΩ。这样,ADC输入阻抗的影响可以忽略不计。确定了R2,再根据分压比计算R1。
注意:分压电阻的精度直接影响测量精度。如果要求1%的测量精度,那么电阻至少要用1%精度的。温漂系数也需要根据应用环境考虑。
接下来是输入保护。ADC引脚非常脆弱,过压、静电、负电压都可能损坏它。即使你测量的是电池这种看似安全的电源,也可能会因为插拔、负载突变产生电压尖峰。一个简单有效的保护电路是在ADC输入引脚前串联一个100-500Ω的电阻(Rs),并并联一个钳位二极管到VDDA和VSSA,比如BAT54S这样的肖特基二极管。这个串联电阻Rs和ADC输入电容会形成一个低通滤波器,有助于抑制高频噪声。但Rs不能太大,否则会和ADC的采样电容影响充电时间,可能需要进一步增加采样时间。
关于热词中提到的“ADC采样保护电路能不能用BAV99”,BAV99是一个双二极管,常用于信号钳位。它可以用作简单的钳位保护,但其正向压降(约0.7V)比肖特基二极管(约0.3V)大,这意味着你的输入信号在超过VDDA+0.7V或低于VSSA-0.7V时才会被钳位,保护“门槛”较高。对于3.3V系统,如果输入信号可能超过4.0V,BAV99仍能提供保护。但如果你追求更精细的过压保护(比如紧贴3.6V),或者信号电压本身就很接近3.3V,那么低Vf的肖特基二极管是更好的选择。此外,BAV99的结电容相对较大,对高速信号可能有影响,但对于直流或低速电压采集完全够用。
3. CubeMX配置详解:每一个选项背后的意义
硬件电路准备妥当后,我们进入软件配置环节。使用STM32CubeMX可以极大简化初始化过程,但如果不理解每个选项的含义,就等于在闭着眼睛开车。
3.1 时钟树与ADC时钟:速度与精度的平衡
打开CubeMX,在Pinout & Configuration标签页中找到ADC1。首先需要配置的是ADC时钟。STM32F103的ADC时钟源自APB2总线时钟(PCLK2),并经过一个可编程的分频器。ADC有一个最大时钟频率限制,对于STM32F103,通常是14MHz(具体请查对应型号的数据手册)。假设你的PCLK2是72MHz(系统常用配置),那么分频系数至少需要设为6(72/6=12MHz)才能保证ADC正常工作。
核心原则:在不超过最大额定频率的前提下,ADC时钟越慢,通常对抗噪声的性能越好,但转换速度会下降。对于追求精度的直流或低速测量,我习惯将ADC时钟设置在12-14MHz的下限附近,比如设置分频为6,得到12MHz的ADC时钟。
接下来,在Parameter Settings选项卡中,我们会遇到一系列关键设置:
- Resolution(分辨率):选择12位。这是硬件支持的最高精度。
- Scan Conversion Mode(扫描转换模式):如果你需要循环采集多个通道,必须启用这个模式。启用后,ADC会按照
Rank的顺序自动扫描所有使能的通道。 - Continuous Conversion Mode(连续转换模式):如果启用,ADC在完成一次(或一轮扫描)转换后会自动开始下一次,适合连续监控。如果禁用,则需要每次触发(软件或硬件)一次转换。通常,单次采集用非连续模式,配合DMA的连续采集用连续模式。
- Discontinuous Conversion Mode(间断转换模式):这是一个高级功能,可以将一组通道分成若干子组来触发。在初期可以保持禁用。
- DMA Continuous Requests(DMA连续请求):如果你使用了DMA,这个选项至关重要!当启用DMA且ADC为连续转换模式时,必须勾选此项,以确保DMA能持续不断地将转换数据搬运到内存,否则DMA只搬运一次就停止了。
- End Of Conversion Selection(转换结束选择):选择EOC(转换结束)标志在每次单个通道转换完成后置位,还是在所有序列通道转换完成后置位。配合DMA时,通常选择每个通道转换后产生EOC。
3.2 通道配置与采样时间:精度调优的关键
在Configuration标签页点击ADC1,进入详细配置。这里最重要的就是Channel Configuration。
假设我们配置了三个通道:通道0(PA0)测量外部电压,通道16(温度传感器)和通道17(内部参考电压VREFINT)。我们需要为每个通道分配一个Rank(顺序),并设置其Sampling Time(采样时间)。
采样时间的选择是一个权衡。时钟周期数越多,采样时间越长,精度越高,尤其是对高阻抗源。STM32F103的采样时间可以从1.5个周期到239.5个周期。如何选择?
- 对于低阻抗源(如运放输出、分压后的信号,源阻抗<10kΩ):1.5到7.5个周期通常足够。
- 对于中等阻抗源:可能需要28.5或41.5个周期。
- 对于高阻抗源或要求高精度:建议使用最长的239.5个周期。
一个实操技巧:你可以先设置一个较长的采样时间(如239.5周期)确保读数稳定,然后逐步减小采样时间,同时观察ADC读数的波动情况,直到找到在可接受噪声水平下的最短采样时间,这样可以优化采样速率。
对于内部通道(温度传感器和VREFINT),必须使用更长的采样时间,因为它们的输出阻抗较高。数据手册明确要求采样时间不低于17.1μs。在12MHz ADC时钟下(一个周期约83.3ns),你需要至少设置17.1μs / 83.3ns ≈ 205个周期。所以设置为239.5个周期是安全且常见的做法。
最后,在DMA Settings标签页添加DMA。对于ADC1,通常选择DMA1 Channel1。模式设为Circular(循环模式),这样DMA会周而复始地将数据搬运到指定数组,无需软件干预。数据宽度都选择Word(32位),因为HAL库的ADC值是通过一个32位变量返回的。
4. 代码实现与DMA传输:构建高效的数据流水线
配置生成代码后,我们进入编程环节。HAL库封装得很好,但只有理解其流程,才能用得顺手并处理异常。
4.1 基础单次采集与轮询
对于最简单的单通道单次采集,流程如下:
// 1. 启动ADC HAL_ADC_Start(&hadc1); // 2. 等待转换完成,超时时间根据采样时间设置,通常10-50ms足够 if (HAL_ADC_PollForConversion(&hadc1, 50) == HAL_OK) { // 3. 读取转换值 uint32_t adc_value = HAL_ADC_GetValue(&hadc1); // 4. 转换为电压 (假设Vref=3.3V) float voltage = (adc_value * 3.3f) / 4095.0f; } // 5. 停止ADC(对于单次模式,HAL_ADC_GetValue后会自动停止,但显式停止是好习惯) HAL_ADC_Stop(&hadc1);这种方式简单,但效率低下,因为CPU大部分时间在等待(PollForConversion)。它只适用于对实时性要求极低、偶尔读取一下的场景。
4.2 中断方式与多通道扫描
中断方式可以让CPU在ADC转换期间去处理其他任务。
// 启动ADC中断转换 HAL_ADC_Start_IT(&hadc1); // 转换完成中断回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { uint32_t adc_value = HAL_ADC_GetValue(hadc); // 处理数据... } }对于多通道扫描,你需要在Rank中配置好顺序,然后ADC会按顺序转换,每转换完一个通道就会进入一次中断(如果EOC选择是每通道)。这样你可以在中断里读取值并存入数组。但频繁的中断会消耗大量CPU资源,当通道多、采样快时,系统可能就被中断“淹没了”。
4.3 DMA传输:解放CPU的终极方案
对于稳定、连续的电压采集,DMA是必选项。它能在ADC转换完成后,自动将数据搬运到你指定的内存数组中,完全不需要CPU参与。
初始化后,你只需要启动一次:
// 定义一个数组用于存放DMA搬运的数据 #define ADC_BUFF_SIZE 100 uint32_t adc_buffer[ADC_BUFF_SIZE]; // 启动ADC,并开启DMA传输到指定数组,长度设为缓冲区大小,模式为循环 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFF_SIZE);启动后,ADC就会按照配置(连续转换、扫描模式)不停地工作,DMA则循环地将转换结果填入adc_buffer。对于多通道扫描,情况稍微复杂:DMA搬运的数据是交替的通道数据流。
假设你配置了3个通道(Rank1, Rank2, Rank3),那么adc_buffer里的数据排列将是:[Ch1, Ch2, Ch3, Ch1, Ch2, Ch3, ...]。你需要根据这个规律来解析数据。
// 假设有三个通道,DMA循环填充了100个数据 for(int i = 0; i < ADC_BUFF_SIZE; i+=3) { float voltage_ch1 = (adc_buffer[i] * 3.3f) / 4095.0f; float voltage_ch2 = (adc_buffer[i+1] * 3.3f) / 4095.0f; float voltage_ch3 = (adc_buffer[i+2] * 3.3f) / 4095.0f; // 进行后续处理,如滤波、存储等 }这里有一个极易出错的地方:DMA缓冲区的大小和溢出。在循环模式下,DMA会不停地覆盖缓冲区中旧的数据。如果你的主程序处理数据的速度跟不上ADC采集的速度,还没等处理完一轮数据,新数据就把旧数据覆盖了,就会导致数据丢失。解决方法是使用双缓冲区(Double Buffer),或者确保处理逻辑足够快。HAL库提供了HAL_ADC_ConvHalfCpltCallback(半传输完成中断)和HAL_ADC_ConvCpltCallback(传输完成中断)回调函数,可以利用它们实现“乒乓操作”,安全地处理数据。
5. 软件滤波与校准:从“有读数”到“读得准”
原始的ADC值通常是充满噪声的。直接使用它,你的电压读数可能会在小范围内不断跳动。此外,由于基准电压Vref的微小偏差、ADC本身的增益和偏移误差,计算出的电压值与真实值之间可能存在系统误差。这就需要软件滤波和校准。
5.1 数字滤波算法:平滑数据的艺术
滤波的目的是在保持信号真实变化趋势的前提下,抑制随机噪声。没有一种滤波算法是万能的,需要根据信号特性选择。
均值滤波:最简单有效。连续采样N次,取算术平均值。它能有效抑制白噪声,但会引入滞后,且对脉冲噪声(偶发的跳变)无能为力。
#define SAMPLE_TIMES 10 uint32_t adc_sum = 0; for(int i=0; i<SAMPLE_TIMES; i++) { adc_sum += HAL_ADC_GetValue(&hadc1); // 假设单次采样 HAL_Delay(1); // 适当间隔 } uint32_t adc_average = adc_sum / SAMPLE_TIMES;滑动平均滤波:维护一个长度为N的队列,每次新采样值入队,最旧值出队,然后计算队列均值。实时性比一次性均值滤波好,但同样有滞后。
#define FILTER_LEN 20 uint32_t adc_buf[FILTER_LEN] = {0}; uint8_t buf_index = 0; uint32_t adc_sum = 0; // 每次获取新数据时调用 uint32_t get_filtered_value(uint32_t new_val) { adc_sum -= adc_buf[buf_index]; // 减去即将被覆盖的旧值 adc_sum += new_val; // 加上新值 adc_buf[buf_index] = new_val; // 更新缓冲区 buf_index = (buf_index + 1) % FILTER_LEN; // 索引循环 return adc_sum / FILTER_LEN; }中值滤波:连续采样N次(N为奇数),将这N个值排序,取中间值作为输出。对脉冲噪声有奇效。比如你的ADC偶尔会受到干扰产生一个极大或极小的值,均值滤波会被这个坏值严重影响,而中值滤波能直接把它剔除。可以结合均值滤波使用,先中值滤波去脉冲,再均值滤波平滑。
一阶滞后滤波(低通滤波):
Y(n) = α * X(n) + (1-α) * Y(n-1)。其中α是滤波系数(0<α<1),X(n)是新采样值,Y(n-1)是上次滤波输出。α越接近1,新采样值权重越大,响应快但噪声大;α越接近0,滤波效果平滑但滞后严重。这种滤波计算量小,非常适合在MCU上实现。float alpha = 0.1f; // 滤波系数 float filtered_val = 0.0f; float first_order_lpf(float new_sample) { filtered_val = alpha * new_sample + (1 - alpha) * filtered_val; return filtered_val; }
我的经验是:对于变化缓慢的直流电压(如电池电压),采用“中值滤波+滑动平均”的组合拳效果非常好。先采集5-7个值做中值滤波剔除野值,再将这个中值送入一个长度为10-20的滑动平均窗口,可以得到极其稳定平滑的结果。
5.2 内部参考电压校准:消除芯片个体差异
这是提升绝对精度的最重要步骤,但也是最容易被忽略的。公式电压 = (ADC_Value / 4095) * Vref假设Vref是精确的3.3V,且ADC的12位刻度是完美的。现实并非如此。VDDA可能实际是3.28V或3.32V,ADC本身也存在零点偏移和增益误差。
STM32提供了一个工厂预校准的内部参考电压VREFINT。它在芯片出厂时被测量并存储在系统存储区(通常地址为0x1FFFF7BA和0x1FFFF7C0,对于F103,具体地址需查参考手册)。这个电压非常稳定,约1.2V,不随外部VDDA变化。我们可以利用它来反向推算出实际的VDDA。
校准步骤如下:
- 在ADC中启用
VREFINT通道,并设置足够长的采样时间。 - 采集
VREFINT通道的ADC原始值,记为ADC_RAW_VREFINT。 - 从系统存储区读取工厂校准值,记为
VREFINT_CAL(这是一个在VDDA=3.3V、30°C环境下测量VREFINT通道得到的12位ADC值)。 - 计算实际
VDDA:VDD_ACTUAL = (3.3V * VREFINT_CAL) / ADC_RAW_VREFINT。 - 后续计算任何外部通道电压时,使用这个计算出的
VDD_ACTUAL作为参考电压,而不是理想的3.3V。
// 1. 读取校准值 (地址请根据具体型号查Reference Manual) #define VREFINT_CAL_ADDR ((uint16_t*)0x1FFFF7BA) uint16_t vrefint_cal = *VREFINT_CAL_ADDR; // 2. 假设你已经通过DMA或中断获得了VREFINT通道的原始值 adc_raw_vrefint float vdda_actual = (3.3f * vrefint_cal) / adc_raw_vrefint; // 3. 计算外部通道电压 float external_voltage = (adc_raw_external * vdda_actual) / 4095.0f;通过这个方法,即使你的VDDA因为电源波动变成了3.25V,你也能准确计算出外部电压,因为它所用的参考基准vdda_actual是实时测算出来的。这相当于给ADC做了一次“在线标定”。
5.3 两点校准法:追求极致精度
如果内部参考电压校准后,你发现测量值与高精度万用表读数之间还存在固定的线性误差(比如整体偏大1%),这可能是因为ADC的增益误差。此时可以进行两点校准。
你需要两个已知的、稳定的精确电压源,最好一个在量程低端(如0.5V),一个在高端(如3.0V)。分别用你的ADC系统测量它们,得到原始值ADC_RAW_LOW和ADC_RAW_HIGH。
设理想情况下,电压V和ADC原始值RAW的关系是:V = k * RAW + b。 已知V_LOW对应RAW_LOW,V_HIGH对应RAW_HIGH,可以解出:k = (V_HIGH - V_LOW) / (RAW_HIGH - RAW_LOW)b = V_LOW - k * RAW_LOW
将k和b存入Flash,以后所有采集的原始值都通过这个公式V = k * RAW + b来计算电压。这可以同时补偿偏移误差和增益误差,将系统精度提升到硬件极限。
6. 高级话题与实战排坑
掌握了基础配置、DMA和校准,你的ADC采集系统已经相当可靠了。但在一些复杂场景下,还会遇到更深层次的问题。
6.1 多ADC交替采样与高采样率
热词中提到了“双ADC交错采集实现4M采样率”。STM32F103的ADC支持双模式(Dual Mode),即ADC1和ADC2可以协同工作。在“交替采样”模式下,两个ADC可以交替对同一通道进行采样,理论上可以将采样率翻倍。但请注意,STM32F103的ADC转换时间(采样时间+12.5个周期的固定转换时间)是有限的。在12MHz ADC时钟、239.5周期采样时间下,一次转换就需要(239.5+12.5)/12e6 ≈ 21μs,即单通道采样率不到50ksps。即使使用最短采样时间1.5周期,单通道极限采样率也在1/((1.5+12.5)/12e6) ≈ 857ksps左右。要实现更高的采样率,必须使用多ADC并行、降低分辨率(如降到8位)或使用更快的ADC时钟(需确保不超过芯片极限)。对于STM32F103,追求Msps级别的采样率是比较困难的,需要仔细评估时序。
6.2 PCB布局与噪声抑制
所有精心的软件滤波,都可能被糟糕的硬件布局毁掉。ADC精度是“毫伏”甚至“微伏”级别的较量,PCB上的噪声很容易淹没信号。
- 电源隔离:
VDDA和VSSA必须通过磁珠或0Ω电阻从VDD和VSS隔离,并在靠近ADC引脚处放置一个10μF的钽电容和一个0.1μF的陶瓷电容进行去耦。 - 地平面:保持完整的地平面,模拟部分和数字部分的地在一点连接(通常在磁珠或0Ω电阻处单点接地)。
- 信号走线:模拟信号线(ADC输入线)应远离高频数字信号线(如时钟线、PWM线)。如果必须交叉,尽量垂直交叉。可以在模拟信号线两侧布置地线进行屏蔽。
- 元件摆放:分压电阻、滤波电容应尽可能靠近ADC输入引脚。
6.3 典型问题排查清单
当你的ADC采集出现问题时,可以按以下清单排查:
- 读数完全不对或为0/4095:
- 检查引脚配置是否正确(模拟输入模式)。
- 检查
VDDA和VSSA是否已正确连接电源和地。 - 用万用表测量ADC引脚实际电压,确认是否在0-
VDDA范围内。如果超过,可能已损坏。
- 读数不稳定,跳动大:
- 首先检查采样时间是否足够,尤其是对高阻抗信号源。
- 检查电源
VDDA的纹波。可以用示波器交流耦合档观察。 - 检查PCB布局,是否存在数字噪声干扰。
- 在ADC输入引脚对地加一个0.1μF~1μF的电容(注意:这会增加源阻抗,可能需要更长的采样时间)。
- 启用并检查软件滤波。
- 多通道采集数据错乱:
- 确认扫描模式(Scan Conversion Mode)已启用。
- 确认DMA配置正确,且缓冲区大小和通道数匹配。检查数据解析逻辑是否正确(通道顺序)。
- 如果是中断方式,检查是否在中断中及时读取了数据,或者中断是否被更高优先级中断打断导致数据丢失。
- 测量值有固定偏差:
- 执行内部参考电压校准。
- 检查分压电阻的精度。
- 考虑进行两点校准。
最后,关于热词中“电池包采集电芯电压和实际测量电压不一致”的问题,除了上述ADC本身的误差,还可能源于:采集回路中的接触电阻、采样线缆的压降、多路复用开关(如果用了模拟开关)的导通电阻、以及BMS采集芯片与MCU ADC之间的电平转换或隔离电路带来的误差。这需要从整个信号链的每一个环节去分析和校准。
ADC采集是一个从硬件到软件都需要精心设计的系统工程。它不像点灯那样立刻就有反馈,每一个环节的疏忽都会在最终结果上放大。耐心地理解原理,细致地配置参数,严谨地处理数据,你就能让STM32F103这颗经典的芯片,发挥出令人满意的测量性能。
