简介:这是一份基于STM32F103R6的4位LED数码管数字电压表设计资料包,面向嵌入式入门学习者、电子竞赛备赛者及课程设计学生。资料内含完整的Proteus仿真电路图和Keil5工程源码,实现0—5V电压测量、按键量程切换与数码管实时显示,可直接在Proteus8.9与Keil5环境中打开运行。包内共160个文件,涵盖Proteus仿真工程(pdsprj)、Keil工程配置(uvprojx)与嵌入式C/C源码、驱动库头文件,以及编译生成的hex、map、axf等烧录与调试文件,压缩包约5.65MB,目录结构清晰便于查找。已有3600人学习下载。借助这份资料可快速理解STM32的ADC采集、按键输入与数码管动态扫描逻辑,熟悉标准外设库的调用方式;仿真与代码配合呈现从初始化到量程切换的完整设计流程,既可用于直接验证测量结果,也方便在此基础上修改量程范围、显示位数或增加报警功能,适合作为课程设计、电子实训或嵌入式入门进阶的参考样例。 从课程设计到实际项目,基于STM32F103R6的数字电压表是我这几年见过最适合练手嵌入式综合能力的选题之一。它不像点个流水灯那样单薄,也够不上工业仪表的复杂度,但恰好把模拟前端、ADC采样、数据处理、显示输出、误差标定这几个环节全部串了起来。这篇文章我就拿自己做过的方案为例,从选型逻辑讲到硬件计算,再到软件标定和调试踩坑,尽量把每个“为什么这么做”都说清楚。如果你正准备做类似的题目,或者刚学完单片机想找个有含金量的项目,这篇应该能帮你省下不少走弯路的时间。
1. 整体设计思路与方案选型
1.1 为什么选STM32F103R6:资源不多,但刚好够用
很多人在选型时会纠结,做电压表用个几块钱的8位单片机不就行了?理论上确实能测,但你会发现从ADC精度到软件扩展性都憋屈。STM32F103R6这颗芯片看起来不怎么起眼,Cortex-M3内核、主频72MHz、32KB Flash、10KB SRAM,放在今天甚至有点“上古”的味道。但正因为这个配置门槛不高,反而很适合把一个项目的底层逻辑吃透。它的12位逐次逼近型ADC,分辨率比多数8位机高了一个台阶,配合DMA和中断机制,做连续采样处理很从容。而且F103系列生态资料多到溢出来,无论是标准外设库还是HAL库,遇到问题搜一圈基本都有答案。
如果不是课程设计要求指定型号,我个人其实也建议用R6这类小容量版本起步。原因很简单:Flash小会逼着你写精简代码,SRAM小会让你认真管理缓冲区,等到真把这颗芯片的资源榨干用好,你再去用大容量芯片就是降维打击。反过来一上来就上F103ZET6或者H7,容易陷入“资源多、随便造”的心态,反而不利于抠细节。
1.2 整体方案链路:从被测电压到屏幕读数
一个数字电压表,本质上做的事情只有三步:把待测电压调理到ADC能量程的范围内,用ADC量化成数字,再由软件换算成真实电压值并展示。听起来简单,但每一环节都有讲究。
我这次设计的方案链路是这样的:被测电压先经过电阻分压网络,将最高20V的输入电压等比缩小到3.3V以内,再经过RC低通滤波和钳位保护电路,送入STM32F103R6的ADC引脚。片内ADC完成12位量化后,由软件做数字滤波、标定换算,最后把结果送到1602液晶屏上显示。为什么量程设成20V?因为日常实验和维修场景里,0到20V基本覆盖了绝大多数低压直流测量需求,留到20V也方便以后扩展。
显示方案我对比过几种:数码管动态扫描最省钱但信息量少,OLED显示效果好但I2C时序和驱动稍绕,1602字符屏接线不多、驱动资料大把,还能直接显示“U= xx.xxV”这样的完整字符串。权衡之下选了1602。如果你手头有0.96寸OLED也可以替代,逻辑完全一致,只是初始化函数换一下。
2. 硬件电路拆解与关键计算
2.1 分压网络:先算清楚“缩比”再动手
硬件上最核心的电路就是分压网络,它决定了整机的量程和测量精度。STM32F103R6的ADC参考电压接VDDA,默认满量程是3.3V,所以输入电压必须先缩小到不超过3.3V。做设计时我习惯留出约20%的余量,按最高输入24V对应ADC满量程3V来算分压比。
分压比等于3V除以24V,也就是1/8。如果采用经典的上臂串联电阻R1、下臂接地电阻R2结构,需要满足R2/(R1+R2)=1/8。我选的是R1=220kΩ(上臂)、R2=30kΩ(下臂),总输入阻抗约250kΩ。这里要注意,输入阻抗不是越大越好,太大了ADC输入泄漏电流带来的误差会变明显,太小了又会给被测电路增加额外负载,250kΩ在常规场景下是个不错的平衡点。电阻精度我直接用1%金属膜电阻,想提升精度可以换成0.1%精密电阻,成本也就多几毛钱。
这里有一个新手很容易忽略的坑:分压电阻的阻值虽然算好了,但ADC内部采样电容在采样瞬间会从分压网络抽取电流,如果分压网络输出阻抗太高,采样值会被拉低,测量结果就会偏小。解决思路有两种:要么把分压电阻整体调小(比如R1=22kΩ、R2=3kΩ),要么在分压输出后加一级运放跟随器做缓冲。我实测下来,24kΩ级别的输出阻抗配F103的ADC还是能用的,但稳定性一般,想读数稳上加大电容。方案我用的是RC低通滤波,在分压输出节点对地接一个0.1μF电容,和30kΩ下臂组成低通网络,同时滤掉高频噪声。
2.2 输入保护与电源设计:防止“一测就烧”
测电压最怕的就是手一抖接过压,ADC引脚直接冒烟。所以分压网络后面我加了BAV99双二极管钳位保护,把节点电压钳制在GND-0.3V到3.3V+0.3V之间。之前有一版电路省掉了保护二极管,结果调试时误接过一次12V反极性电压,芯片ADC引脚当场报废,后来再也不敢省这个元件。如果校准时测的是外部直流稳压源,输入线尽量用屏蔽线,避免感应到工频噪声。
电源部分同样不能马虎。STM32F103R6的模拟电源引脚VDDA和数字电源VDD虽然外部可以共用一个3.3V源,但中间必须做隔离。我习惯在VDDA引脚前串一个磁珠或者10Ω小电阻,再对地接0.1μF和10μF两个退耦电容,让ADC的参考电压更稳定。ADC的参考电压稳定性会直接影响测量结果,特别是做精确校准时,如果VDDA上有毛刺,读数的低位就会一直跳。这个细节在原理图阶段就要考虑到,等PCB打样回来再改就麻烦了。
2.3 关键器件清单
为了让你看得清楚,我把这版用到的核心器件整理成一个清单:
| 器件 | 型号/参数 | 用途 | 数量 |
|---|---|---|---|
| 主控MCU | STM32F103R6 | 数据采集与处理 | 1 |
| 分压电阻 | 220kΩ、30kΩ,1% | 电压量程缩比 | 各1 |
| 保护二极管 | BAV99 | 过压钳位 | 1 |
| 滤波电容 | 0.1μF、10μF | 低通滤波、退耦 | 若干 |
| 运放(可选) | MCP6002 或 LM358 | 分压缓冲 | 1 |
| 液晶屏 | 1602字符屏 | 数据显示 | 1 |
| 对比度调节 | 10kΩ电位器 | 调节LCD显示效果 | 1 |
有一件事必须提醒:1602液晶屏很多是5V逻辑电平的模块,而STM32是3.3V系统,两者直连虽然短期内能用,但长期可靠性没保障。如果你的模块是5V版本,建议加I/O电平转换或者选用3.3V供电版本,别偷懒直接怼上去。
3. 软件核心逻辑与标定实现
3.1 ADC初始化与采样配置:先把参数吃透
硬件搭好后就进入软件部分。F103R6的ADC是12位分辨率,转换结果范围0到4095。我的初始化思路是:ADC时钟分频设为6分频(PCLK2为72MHz,得到12MHz的ADC时钟),采用单次转换模式,由定时器触发或者软件触发采样,每次采样序列读取一通道。采样时间我习惯配成55.5周期,也就是足够长的时间让内部采样电容充饱,这样读数更稳。如果你配成1.5周期最快速度,转换是快了,但参考电压稍有波动读数就飘,得不偿失。
下面是基于HAL库的ADC初始化核心代码,标准外设库的写法也类似,只是函数名不同:
void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } }每次读取采样的代码很简单,先校准一次(首次上电时执行),然后启动转换,等待转换完成,读取结果寄存器:
uint16_t ADC_Read(void) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); return HAL_ADC_GetValue(&hadc1); }实际生产级代码我不会用轮询方式,会改成“定时器触发+DMA搬运”的模式,让CPU完全不用管ADC。课程设计和首版验证阶段,轮询足够直观,理解起来也顺手。等你跑通了这套逻辑,再上DMA就顺理成章了。
3.2 数字滤波:为什么读数总是跳
ADC采样出来的原始值通常不会很干净。即使输入是纯直流,读数也可能在几个LSB之间抖动,原因可能来自参考电压噪声、外界电磁干扰、甚至是测量线接触电阻的变化。这时候就需要软件滤波登场。
我在实际项目中常用“去极值均值滤波”:连续采20个点,去掉一个最大值和一个最小值,然后对剩余18个点求平均。这种组合滤波方式比单纯的均值滤波强很多,能有效剔除突发性尖峰干扰。如果你采集过程中存在周期性工频干扰,还可以把采样窗口对齐到20ms,也就是一个工频周期,有些老工程师会故意用50Hz整倍数时间去采样,让干扰在一定程度上被平均掉。这个技巧在我做交流供电系统测量时特别管用,直流电压表场景下也能改善稳定性。
做个简单对比:
| 滤波方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 单次采样 | 最快 | 噪声大、不稳定 | 粗略显示 |
| 算术平均 | 平滑效果好 | 对尖峰敏感 | 噪声均匀的场合 |
| 中位值滤波 | 抗脉冲干扰 | 平滑度一般 | 有随机尖峰时 |
| 去极值均值 | 抗尖峰+平滑 | 需要内存存窗口 | 本次采用 |
3.3 两点标定法:让读数真正准起来
硬件电阻的精度和ADC参考电压的偏差,会导致理论计算值跟实际测量值有一定差距。比如分压电阻虽然是1%精度,但两只电阻的误差可能叠加,实际分压比就不是精确的1/8。这时候就要做标定。
标定的核心是建立一个ADC读数到实际电压的线性映射。我用的方法很简单:用一台精度较高的万用表或可调电源,给系统输入两个已知电压,比如5V和20V,分别记录对应的ADC转换数值,然后算出斜率和截距:
// 已知两个标定点:(adc1, volt1) 和 (adc2, volt2) gain = (volt2 - volt1) / (adc2 - adc1); offset = volt1 - gain * adc1; // 之后实时换算 real_voltage = gain * adc_value + offset;这里的gain相当于每个ADC码对应的实际电压值,注意不是直接用“阻值比”和“参考电压”算出理论值,而是用实测点去反推。我在实际调试中对比过,理论计算值和标定后的值最大能差出0.2V到0.3V,标定之后读数明显贴近标准表。如果还想再精细一点,可以多做几个标定点,分区间做线性插值。比如在0-5V、5-10V、10-20V三个区间分别标定,非线性误差能进一步压下去,我在做测量范围比较大的电压表时就是这么处理的。
3.4 主循环与显示刷新:别让LCD拖累采样
主循环的逻辑不复杂:周期启动ADC转换 → 读取数据 → 滤波 → 标定换算 → 格式化输出到LCD。但刷新频率要留意,LCD写字符其实是个相对费时的操作,虽然对72MHz主频来说也就几百微秒,但如果采样和显示放在一起,采样率会被显示拖累。我的做法是把显示刷新频率控制在每秒两次到四次,也就是每次更新前做一次时间判断,没到更新周期就只采样,不刷屏。这样既保证读数连续性,也避免字符屏显示抖动。
超量程处理也要提前想好。当电压超过20V时,屏幕上显示“OL”而不是一堆乱码或负数。代码里加一个判断分支,超过量程就点亮超量程提示,同时禁止ADC继续输出非正常数据。这个小细节在答辩时很加分,因为说明你考虑了异常场景。
4. 常见问题排查与调试实录
4.1 采样值跳变,稳定不下来
现象是输入一个稳定电压,数码管或LCD上的末位数字不停跳,甚至个位也在飘。排查时先别急着改代码,用万用表测ADC引脚对地电压,看硬件上是否已经稳定。如果引脚电压本身就不稳,重点查分压电阻接触、电源纹波、基准电压这三个方面。我遇到过一次是某颗220kΩ电阻虚焊,引脚电压直接是方波,找了好半天才用示波器定位到。如果硬件电压稳了但软件读数还跳,多半是采样时间太短或者滤波不够,把ADC采样时间调到55.5周期以上,再配合去极值均值滤波,基本能压住。
4.2 输入短路时读数不是0,而是几十毫伏
输入端接地,显示却是0.05V甚至更高,这通常是三部分原因叠加:ADC本身存在偏移误差、分压网络的GND参考点有压差、运放(如果用了)存在失调电压。我当时的处理方式是软件零点校准:上电后先测一次接地电压,得到偏移值,之后的每一个测量结果都减去这个偏移值。硬件调零比较麻烦,软件补偿两条指令的事,效果立竿见影。不过零点校准的前提是GND参考电位本身是干净的,如果地线上有大电流流过形成压差,光靠软件补偿是不够的,要从PCB布局上解决问题。
4.3 大电压下测不准,误差成比例增大
只在小量程准确,一测十几伏就偏差大,基本可以锁定是分压电阻精度或者参考电压问题。1%电阻在20V时可能带来0.2V左右误差,这个不稀奇。想提高精度,一是换0.1%精密电阻,二是用前面提到的多点标定修正。如果误差不是线性关系,而是电压越高偏差越大,还要检查分压后节点电压是不是超过了ADC参考电压,导致量程头部压缩,这时候应调整分压比或者换更高耐压的输入设计。
4.4 屏幕显示闪烁或出现乱码
1602出现乱码,大概率不是代码逻辑问题。我遇到过一次,是LCD的RW引脚悬空导致读写状态不稳定,接一个下拉电阻到地就恢复正常了。闪烁则多半是刷新频率和内部处理不同步,可以试着降低刷新频率。另外1602模块的对比度电位器也很关键,如果对比度调到极限还是看不清,先检查模块供电电压是否达标,5V模块用3.3V供电就会出现对比度不足、显示模糊的现象,这时候要么换3.3V模块,要么用DC-DC升压给屏供电。
4.5 ADC读数始终是4095,像“卡满”一样
这个现象说明ADC引脚电压超过了参考电压,也就是输入过压了。先查分压电阻焊接有没有短路,再查输入电压是不是远大于设计量程。还有个隐蔽的问题:如果ADC的通道引脚配置成了复用推挽输出而不是模拟输入,也会出现异常满码值。检查一下GPIO初始化的模式是否正确,F103的ADC引脚必须配成模拟输入模式,否则内外电路打架,读数肯定不准。
我把上面这些问题整理成一张速查表,方便你调试时对照:
| 故障现象 | 可能原因 | 排查顺序 | 解决手段 |
|---|---|---|---|
| 读数跳动 | 噪声、采样时间短 | 硬件电压→采样配置 | 加滤波、延长采样时间 |
| 零点偏移 | ADC失调、地线压差 | 短接输入测零点 | 软件/硬件零点校准 |
| 大电压误差大 | 分压电阻精度、参考电压 | 标准表对比 | 精密电阻、多点标定 |
| LCD乱码/闪烁 | RW悬空、刷新过快 | 查接线→降刷新率 | 加下拉、调整时序 |
| ADC卡在4095 | 输入过压、引脚配置错 | 查输入电压→查GPIO | 调整量程、修正配置 |
调试这类项目最大的体会是:不要一上来就怀疑软件。先用万用表把每个硬件节点的电压量一遍,再让示波器看关键波形,最后才回到代码里分析。数字电压表看起来是个入门项目,真要把精度和稳定性做到位,模拟电路、软件算法、系统调试全都得跑一遍,这也是我为什么一直推荐身边的人拿它当第一个综合项目。做完它,你后面再接触传感器采集、仪器仪表类的开发,会发现很多东西都是相通的。
本文还有配套的精品资源,点击获取