☰
STM32F103R6实战:基于逐次逼近型ADC的数字电压表设计与校准
2026/10/14 17:35:29 网站建设 项目流程

简介:基于STM32F103R6的数字电压表设计资源,面向嵌入式初学者、电子竞赛备赛及课程设计场景,解决通过按键切换量程、在4位LED数码管上实时显示0-5V电压测量结果的核心问题。压缩包共160个文件,以Keil5工程源码(c/h)和Proteus8.9仿真工程(pdsprj/pdsbak)为主体,附带编译产物hex、axf、map、lst及大量中间文件(d、o、crf),既便于直接烧录验证,也有助于分析编译链接全过程,整体约5.65MB。目前已有3606人学习下载。内含完整的按键扫描、ADC采样、数码管动态显示与量程切换逻辑,电路图与代码对应清晰,打开Proteus仿真即可观察不同量程下的电压测量结果,适合作为STM32入门实战、单片机课程设计的参考方案。资源目录规划明确,主程序、外设驱动与仿真配置分离,方便按模块学习或二次开发。

1. 从一块开发板和电位器开始:STM32F103R6数字电压表到底在做什么

调试电路时发现手头没有万用表,我做的第一件事是用STM32F103R6的ADC读取电位器电压,接到屏幕上显示数值。这个看起来像玩具的方案,其实就是基于STM32F103R6模拟的数字电压表的核心逻辑:用片上逐次逼近型ADC把连续变化的模拟电压转成12位数字码,再按分压比例、参考电压换算成真实电压值,最终显示成小数。它解决的是低成本、可定制、能记录数据的嵌入式测量需求,适合刚接触ADC的开发者,也适合想给自制电源加一个测量显示面板的玩票工程师。这中间真正的差距不在ADC本身,而在于分压网络设计、采样稳定性处理和校准方法。

2. 电压表的第一块地基:测量范围、分压网络与参考电压

2.1 为什么12位ADC不能直接测3.3V以上:量程与分辨率

STM32F103R6内置的ADC是12位逐次逼近型结构,输入电压范围由参考电压决定。按常规设计,参考电压取VDD=3.3V时,ADC通道可测0~3.3V,超过这个值,片上模拟输入即使不烧毁,也会被内部保护二极管钳位,读数满码且完全失真。因此,数字电压表要做的第一件事是确定目标量程,再用电阻分压把待测电压压到ADC的安全窗口内。

12位分辨率对应最小跳变:3.3V / 4096 ≈ 0.806mV。理想情况下能分辨约0.8mV的电压变化,但实际电路里电源纹波、参考电压噪声和采样时序抖动会把最低几位淹没。常见结果是:代码里看着是12位,稳定读数往往只有11位甚至10位。这个现实决定了后边的滤波策略不是“可有可无”,而是必须在软件里补上。

如果要做0~12V量程,分压比例大约取3.3 / 12 = 0.275。计算依据是R2 / (R1 + R2)。取R1=100kΩ、R2=30kΩ时,比例是30 / 130 ≈ 0.231,满量程12V对应ADC输入2.77V,留下约0.53V安全余量,避免电阻误差导致输入超过3.3V。要做0~5V量程时,比例可放到0.66左右,例如R1=20kΩ、R2=39kΩ,满量程约3.30V。目标量程越高,分压比的选取越要留足余量,而不是刚好卡在3.3V上。

目标量程分压比典型电阻组合满量程ADC电压输入等效阻抗
0~3.3V1直连3.30V极小
0~5V0.6620k + 39k3.31V约13.2kΩ
0~12V0.231100k + 30k2.77V约23.1kΩ
0~24V0.128150k + 22k3.07V约19.2kΩ

选取分压电阻时还要注意ADC输入端的等效阻抗。STM32F103R6的ADC内部有一个采样保持电容,采样期间要从外部信号源吸取电荷。如果信号源阻抗过高,电容还没充满就进入比较阶段,读数就会比真实值偏低,而且误差随采样时间设置而变化。这个等效阻抗是R1与R2的并联值,例如100kΩ与30kΩ并联后约23kΩ,明显高于芯片数据手册建议的10kΩ以内。所以要么整体缩小分压电阻,要么给ADC前加一级运放跟随器。

2.2 分压电阻怎么选:精度、温漂与等效阻抗

分压网络是电压表精度的地基。阻值大小同时影响输入阻抗、自身功耗和测量误差。选兆欧级电阻,对被测电路影响小,但等效阻抗太高,采样电容充电慢,读数偏低,需要在配置里把采样周期拉到最长。选千欧级电阻,等效阻抗低,但分压网络自己会从被测源分流,对高内阻信号源来说相当于额外负载。

以100kΩ + 30kΩ组合为例,待测电压12V时,分压网络自身消耗电流约12V / 130kΩ ≈ 0.092mA,大部分被测源都能接受。如果把阻值降到10kΩ + 3kΩ,功耗变成近1mA,但等效阻抗约2.3kΩ,ADC采样轻松不少。这个取舍没有绝对答案,我一般优先保证等效阻抗低于10kΩ,如果无法满足,就使用运放跟随器,让ADC看到一个低阻抗的电压源。

电阻的标称精度直接影响百分比误差。1%精度的电阻,两个电阻组合后最坏情况可能叠加出2%的误差,3位半电压表会直接“满量程不准”。我更倾向把R1和R2选成0.1%精度的金属膜电阻,并在焊板前用万用表实测阻值,把实测值写进换算系数。温漂同样不可忽略:25ppm/℃的电阻在环境温度变化30℃时,阻值变化约0.075%。日常测电池、电源够用,但放在工业现场就得换成低温漂电阻。

分压比的计算精度取决于实际阻值。如果只按电阻标称值算,量程高段会出现相同比例的偏移,这种偏移叫增益误差,单纯靠换电阻很难消除。所以我的习惯是:第一版硬件不必追求昂贵的电阻,先把ADC采样和显示跑通,再用后面第5章讲的两点校准法修正,比一开始堆料更有效。

2.3 参考电压:决定精度的天花板

STM32F103R6的ADC参考电压默认接到VREF+引脚,普通开发板直接连VDD。这时参考电压就是电源芯片输出的3.3V,但这个电压通常不是精确的3.300V,可能随负载和供电波动在3.25~3.35V之间移动。ADC码值等于Vin / VREF × 4096,VREF每偏差1%,所有测量结果就等比偏移1%。想让电压表准,要么保证VDD精准稳定,要么使用独立的外部基准芯片。

低成本做法是直接用稳压后的3.3V,并在VREF+引脚旁边放0.1μF和1μF的去耦电容。这样能做到显示到小数点后两位的普通电压表。如果要求稳定到毫伏级,建议换外部基准芯片,例如输出2.048V或4.096V的种类,把VREF引脚从基准芯片取电。需要注意,使用外部基准后ADC量程不再按3.3V计算,代码里的参考电压常量要同步改,否则满量程换算全是错的。

STM32F103内部还有一个约1.2V的带隙基准电压VREFINT,它本身不受供电电压波动影响。每次上电后可以读这个通道的ADC值,反推当前VDD的实际电压,替换固定3.3V。这意味着不需要外部基准也能获得接近外部基准的稳定性。代价是多占一个通道和一点CPU时间,对于不追求极端速度的电压表来说完全划算。

供电噪声对参考电压的影响是个看不见的坑。即使VDD平均值是3.3V,若开关电源的纹波有几十毫伏,ADC结果就会周期性摆动,显示值末位乱跳。常见处理是给模拟部分供电加LC滤波,数字地和模拟地单点连接,靠近芯片位置加磁珠。如果这些布局没做好,后边做再多软件滤波也是治标不治本。

3. 让ADC跑起来:寄存器配置、单次转换与均值滤波

3.1 引脚与时钟:让PA1变成模拟输入

在STM32F103R6上使用ADC1,第一步是打开对应引脚和ADC外设的时钟,再把这个引脚配置成模拟输入模式。这里以PA1作为电压采样通道为例,直接操作寄存器来完成初始化,不依赖任何特定固件库,方便移植到自己习惯的开发环境里。

// 开启GPIOA和ADC1时钟 // RCC->APB2ENR 里的 IOPAEN 位控制GPIOA时钟,ADC1EN控制ADC1时钟 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN; // 配置PA1为模拟输入 // CRL寄存器每4位控制一个引脚,PA1对应bit[7:4] // MODE[1:0]=00b 表示输入模式,CNF[1:0]=00b 表示模拟输入 GPIOA->CRL &= ~(0x0F << 4); // 先清空这4位 GPIOA->CRL |= (0x00 << 4); // CNF=00, MODE=00

这段代码里,RCC_APB2ENR_IOPAEN和RCC_APB2ENR_ADC1EN是寄存器预定义位,若开发环境没有这些宏,可以直接查数据手册使用(1 << 2)和(1 << 9)。PA1的CRL位段从bit4开始,覆盖bit7~bit4,所以要左移4位。模拟输入模式下,引脚内部的施密特触发器被关闭,避免数字输入电路引入额外噪声,这是电压表通道必须的设置。

时钟开启后,ADC1还需要经过一段稳定时间。常规做法是在配置完寄存器以后,等待大约几十微秒再启动第一次转换。有些开发者忽略这一步,上电立即采样,前几次读数会偏大,看起来就像噪声,实际是ADC内部电路没稳定。

3.2 ADC1初始化与单次转换:三个必调寄存器

ADC采样的核心配置是采样时间、转换通道和软件触发。采样时间越长,采样电容接入外部电路的时间越久,越适应高阻抗信号源;通道号决定了从哪个输入引脚读取模拟量;软件触发指通过设置寄存器位来启动一次转换。下面代码完成ADC1在通道1上的单次转换,并把结果存到变量里。

// 开启ADC1 ADC1->CR2 |= ADC_CR2_ADON; // 校准:F103系列推荐上电后做一次强制校准 ADC1->CR2 |= ADC_CR2_CAL; while (ADC1->CR2 & ADC_CR2_CAL); // 等待校准完成 // 设置采样时间:选择通道1的采样周期为239.5个ADC时钟 // SMPR2寄存器SMP1位段由bit[9:8]组成,111b表示239.5周期 ADC1->SMPR2 &= ~(0x07 << 8); // 清掉SMP1 ADC1->SMPR2 |= (0x07 << 8); // 采样时间设最大 // 设置规则序列:第一次转换选择通道1 // SQR3寄存器的SQ1位段是bit[4:0],填通道号1 ADC1->SQR3 = 1; // 软件触发启动转换 ADC1->CR2 |= ADC_CR2_SWSTART; // 等待转换完成,EOC标志位于SR寄存器bit1 while (!(ADC1->SR & (1 << 1))); // 读取转换结果 uint16_t adc_value = ADC1->DR;

代码里的校准步骤不是可选项。STM32F103的ADC上电后存在偏移误差,标准做法是开启ADON后,把CAL位置1,等待硬件自动校准,再开始正常转换。如果不做这一步,零点附近会有十几到几十个码的偏移,显示出来的电压在0V输入时不是0.000V。

采样时间设置成239.5个周期是最保守的选择。当ADC时钟为14MHz时,一次采样时间约17.1μs,转换速度降到不到50kHz,对电压表这种慢速测量来说完全够用。更短的采样时间适合高速采样,但会放大信号源阻抗带来的误差。这里给的是最不容易出问题的参数。

SQR3寄存器里填的是通道号,不是引脚号。PA1对应的ADC通道是ADC1_IN1,所以SQR3 = 1。如果把采样引脚换到PA0,这里是0,换到PA2则填2。通道号和引脚号容易混淆,我经常看到有新手填成引脚的GPIO号。

3.3 连续采样与均值滤波:电压数据稳定下来的关键

单次转换只是开始。由于电源噪声和电磁干扰,单片采样结果会上下跳动,尤其是靠近量程低端时,末位码值可能跳几十个LSB。最有效的软件手段是连续采样多次取平均值。以下代码循环触发64次转换,累加后取平均,得到相对稳定的ADC值。

uint32_t sum = 0; const uint8_t sample_count = 64; for (uint8_t i = 0; i < sample_count; i++) { // 每次转换前重新触发软件启动 ADC1->CR2 |= ADC_CR2_SWSTART; // 等待EOC置位 while (!(ADC1->SR & (1 << 1))); // 读取DR寄存器会同时清除EOC标志 sum += ADC1->DR; } uint16_t adc_avg = (uint16_t)(sum / sample_count);

取64次平均,相当于一个简单的低通滤波器,可以让末位稳定下来。但平均值对突发脉冲干扰很敏感,如果现场有电机、继电器切换,单个尖峰脉冲会抬高平均值。针对这种情况,可以在平均前先做中值滤波,比如每3个采样取中间值,再对中间值做滑动平均。

选择64次而不是256次,是因为电压表通常每秒刷新10~20次,64次采样在14MHz ADC时钟下耗时约1ms,刷新速度不受影响。如果每次刷新前先清空累加和,注意读取DR后EOC标志会自动清除,不需要软件额外操作。为了进一步提高稳定性,还可以让ADC工作在连续转换模式,配合DMA搬运结果,这样CPU不用卡在等待循环里。

滤波的本质是牺牲速度换精度。对于显示小数后两位的电压表,把ADC原始数据滤波后,读数能在最后一位±1以内稳下来,这已经达到普通万用表的观感。若滤波后仍跳动,问题多半不在软件,而是第2章说的参考电压噪声或接地布局问题。

4. 把电压码值变成屏幕上的数字:换算、显示与量程切换

4.1 从ADC码值到实际电压:换算公式与浮点陷阱

滤波完成后,下一步是把ADC码值换算成真实待测电压。公式是:实际电压 = ADC码值 / 4096 × 参考电压 × 分压系数。分压系数是 (R1 + R2) / R2,也就是分压比的倒数。以100kΩ + 30kΩ为例,分压系数是130 / 30 ≈ 4.333。这意味着ADC读到0x800即2048时,实际电压是2048 / 4096 × 3.3 × 4.333 ≈ 7.15V。

// 换算参数:参考电压VREF=3.30,分压系数DIV_RATIO=4.3333 const float VREF = 3.30f; const float DIV_RATIO = 4.3333f; float adc_voltage = (float)adc_avg * VREF / 4096.0f * DIV_RATIO;

这里有几个常见错误。第一,整数乘法溢出:如果写成adc_avg * 3300 * DIV_RATIO / 4096,adc_avg是uint16_t,3300是int,结果可能超过65535导致截断。第二,浮点除法顺序:用4096会触发整数除法后再隐式转换,得到0,必须写成4096.0f。第三,把所有常量都放进表达式会导致计算量偏大,对电压表倒无所谓,但养成使用显式浮点常量的习惯能少踩很多坑。

参考电压最好使用实测值。拿万用表量一下板子上VREF引脚的电压,例如实测3.28V,就把VREF改成3.28f。分压系数也一样,用实际量到的R1和R2阻值计算,而不是标称值。这样改完,不加任何校准,读数就可能从偏差2%降低到偏差0.3%以内。

对于显示,浮点转字符串时还要考虑小数位数。显示两位小数,应使用%.2f格式化;显示三位小数,则用%.3f。但ADC本身只有12位,在0~3.3V直连且参考电压稳定时,三位小数显示出来的末位主要是噪声,反而显得不稳定。因此建议根据量程调整显示位数:小量程显示三位,大量程显示两位。

4.2 用四位数码管显示电压:一位一位地扫描

数字电压表最常见的显示方案是四位共阴数码管,刷新函数负责把电压值拆成四个数字并动态扫描。PA1采样通道不变,显示占用另一组GPIO。这里给出一个最小扫描代码,假设段码接P0~P7,位选接P10~P13。

// 共阴数码管段码:0~9 static const uint8_t seg_code[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; // 待显示电压,比如12.34V,拆成digits[0]=1,digits[1]=2... void display_voltage(float voltage) { char buf[6]; snprintf(buf, sizeof(buf), "%4.2f", voltage); for (int i = 0; i < 4; i++) { // 字符转数字 uint8_t digit = buf[i] - '0'; if (buf[i] == '.') digit = buf[++i] - '0'; // 处理小数点 // 更新段码和位选 set_seg(seg_code[digit]); set_digit_position(i); delay_us(2000); } }

动态扫描的本质是利用人眼视觉暂留,每隔几毫秒切换一位。刷新频率必须大于50Hz,也就是每位点亮时间不能超过约5ms,否则屏幕会闪烁。很多新手只显示不延时,导致全部位同时点亮,看起来像重影;而延时太长又会闪烁。这里给2ms是经验值,四位数码管扫描周期8ms,刷新率125Hz,刚好无闪。

若用OLED,代码更简单:一次把整个字符串写到显示缓冲区,不需要动态扫描。但OLED需要初始化时序,且占用I2C或SPI接口。数码管的优势是阳光下可见、寿命长,缺点是占用IO多且扫描代码占CPU。对电压表这种低频应用,两者都可以,看手里现成模块是哪一种。

显示还有一个边界问题:电压超过当前量程时,比如12V量程显示“12.34”,再大就溢出为三位数。要在换算前判断是否超量程,超了就显示“OVER”或点亮溢出指示灯,否则会因为显示位数不够造成错误读数。

4.3 量程切换:一个通道不够就分两个通道

分压网络一旦固定,量程就固定了。想要一个电压表既能测3.3V,又能测12V,可以在ADC上接多个分压通道,用程序选择合适的一路。例如PA0接直连通道,PA1接5V分压通道,PA2接12V分压通道。每次测量时先读PA2,如果电压低于某个切换阈值,再读PA1或PA0。

阈值设计要考虑回差。比如12V量程在输入低于0.3V时分辨率不好,切换到5V量程后0.3V附近的ADC码值能提高几倍。但切换不能只根据单次值,否则在阈值附近来回跳。我一般设置回差:从高量程切低量程要求电压低于量程上限的0.85倍,从低量程切高量程要求电压高于量程上限的0.9倍。

#define V_TH_5V 4.5f // 高于4.5V用12V档,低于4.5V用5V档 #define V_TH_12V 11.0f // 高于11V强制12V档 float voltage_display = 0.0f; uint8_t range_index = 2; // 0:直连,1:5V,2:12V for (uint8_t ch = 0; ch < 3; ch++) { set_adc_channel(ch); adc_avg = adc_sample_average(64); voltage_display = adc_to_voltage(adc_avg, divider_ratio[ch]); if (range_index == 2 && voltage_display < V_TH_5V) range_index = 1; else if (range_index == 1 && voltage_display < 0.85f * 3.3f) range_index = 0; else if (voltage_display > V_TH_12V) range_index = 2; }

ADC多通道切换时,每次改SQR3通道号后要重新触发转换,且第一二次转换结果可能不稳定,建议丢弃前4个结果再采样。多通道的每个分压网络都会从被测源引入漏电流,因此被测源阻抗较高时,量程切换会产生读数变化,必要时用模拟开关切换分压网络,而不是同时把所有分压电阻挂在引脚上。

自动量程对算法要求不高,但硬件上多个分压电阻并联,输入端口等效阻抗会变,且未选中的分压网络也会对地形成漏电阻。更稳妥的方案是使用双通道ADC加两个独立分压级,或者用低导通电阻的模拟开关。对于只测常见电压的场景,固定一个量程反而更可靠,自动量程是锦上添花。

5. 校准与避坑:电压表常见的4个翻车现场

5.1 现象:测量值整体偏高或偏低,且比例偏移

原因几乎都是参考电压VREF不是理想值,以及分压电阻实际阻值与标称不符。这类误差是增益误差,所有读数按一个固定的比例偏移。比如VREF实际3.28V而程序用3.30V,读万用表3.000V时,自做表显示3.018V左右。

解决方法是做两点校准。准备一个稳定的可调电源,分别输入满量程约50%和90%的两个电压值,记录ADC码值和实际电压,反推出真正的VREF和分压系数。具体做法是:实测VREF后用该值替换常量,再用精密电阻实测值替换R1、R2计算分压系数。大部分情况改完这两个数,误差可从2%降到0.2%以内。

5.2 现象:显示屏数字末位乱跳,最后两位来回变

原因分两类。一是电源纹波通过VREF进入ADC,这属于硬件问题,软件滤波只能掩盖;二是采样时间太短,信号源阻抗高导致电容充电不足,每次充电程度不同,读数随机变化。

解决要先区分再处理:用示波器看VDD纹波,若纹波超过10mV,给模拟供电加LC滤波;若纹波正常,把采样时间调到239.5周期,并增加均值滤波次数。我习惯先用长采样时间,确认是否稳定,再逐步减短找速度和稳定的平衡点。跳过这个步骤直接上高级滤波算法,往往浪费调试时间。

5.3 现象:测电池、稳压电源很准,一测高内阻信号源就偏低

原因是分压网络等效阻抗太高,信号源内阻与分压网络形成电阻分压,改变了原本要测的电压。例如内阻10kΩ的信号源接到等效23kΩ的分压网络,信号源自己就分掉约30%的电压,读数自然偏低。

解决方法是加一级运放跟随器,放在分压网络之后、ADC引脚之前。运放输入阻抗高达兆欧级,输出阻抗只有几欧,把分压网络与ADC隔离。如果没有运放,就把分压电阻整体缩小,让等效阻抗低于信号源内阻的十分之一,但这样会增大被测源的负载电流。对通用的数字电压表来说,加运放是正确方案。

5.4 现象:断电后重新上电,相同电压读数变了

原因包括ADC零点偏移不一致,以及参考电压在上电后没有完全稳定。STM32F103的ADC偏移误差逐片不同,且随温度和供电变化,单纯在程序里写死一个零点补偿值,换一块板子就不准了。

解决方法是每次开机软件校准:先把ADC输入通道接地,读一组数据作为零点偏移,存储到静态变量里;再把通道切到VREFINT,读内部基准反推VREF。之后每次转换结果都先减去零点偏移,再乘校准系数。这样在0V输入时显示接近0.000V,且VREF漂移影响明显减小。校准流程放在主循环前,耗时几十毫秒,用户无感。

6. 进阶:过采样、两点校准与误差验证方法

想让这台电压表达到更高的精度,可以从过采样开始。STM32F103的ADC是12位,但借助过采样和抽取,可以获得更高有效位数。原理是:对同一电压快速采样累加,每增加4次累加,信噪比提升6dB,有效分辨率增加1位。比如把64次采样累加并右移两位,理论等效到13位;要做14位,需要累加256次。注意过采样只对均匀分布的随机噪声有效,而且输入信号必须先经过低通滤波,否则无法摊平噪声谱。

两点校正是消除剩余误差最实用的手段。准备一个精密的2.000V和一个精密的10.000V标准源,测量后分别得到ADC输出。线性校准公式是:真实电压 = (ADC码值 - 零点码) × 斜率。将这两组数据代入,解出零点和斜率,存进非易失存储区。之后每次换算就使用这两个系数。这个做法能同时校正零点偏移和增益误差,比单纯换精密电阻更有效。

误差验证方法也要养成习惯。用可调电源依次输出1.000V、2.000V、5.000V、10.000V和12.000V,记录自制表读数,与标准万用表对比。误差不超过当前量程的0.5%,对业余项目就算合格。如果误差呈非线性增长,多半是分压电阻热效应或ADC输入电压超出了建议范围;如果误差恒定偏移,是零点校准没做好。

我说一个自己的习惯:任何电压表项目,我都会在显示程序里留一个原始ADC码值调试接口,通过串口输出code、平均值、温度、校准系数。这是排查“为什么读数偏了”最快的方式,比一直盯着屏幕猜测有用得多。做这块表踩过的最大一个坑,是忘了加运放跟随器,结果测一个高内阻传感器信号时,读数始终低10%,折腾了半天。从那以后,凡是分压网络等效阻抗超过10kΩ,我第一件事就是加跟随器。希望这些笔记能帮你把基于STM32F103R6的数字电压表一次跑通。

本文还有配套的精品资源,点击获取

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

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

立即咨询