三位半LCD数字电压表设计:ADC选型、校准与显示驱动
2026/9/16 13:08:53 网站建设 项目流程

简介:一个基于STM32与LCD1602的三路LCD数字电压表嵌入式工程,面向电子爱好者、嵌入式初学者及相关专业学生,用于实现多通道模拟电压采集、ADC转换与液晶实时显示,可灵活调节AD通道以适配不同量程与精度。压缩包共183个文件、约2.95MB,除.c/.h源程序外,还包含.o/.d/.crf等编译中间文件、UV4工程配置文件、hex烧录镜像与axf调试文件,便于直接编译、下载与仿真。目前已吸引386人学习浏览,工程结构清晰,可帮助读者系统理解ADC工作原理、LCD1602时序控制以及STM32外设初始化流程。通过阅读源码与对照调试文件,还可掌握Keil工程组织、编译链接细节和常见排错思路,适合作为课程设计或入门项目参考。

1. 三位半 LCD 数字电压表的难点不在显示,而在稳定

把「3 LCD 数字电压表」这个标题拆开看,3 指的是三位半(3 1/2 digit):最大显示 ±1999,最高位只出现 0 或 1,LCD 是显示载体,嵌入式是落地形态。这套东西在课程设计和量产仪表里都常见,但两者的评价标准完全不同。课程设计版的常见搭配是 STC89C52 加 ADC0809,只要数码管能走出数字就算交差;量产版要盯的是最后一位数字跳不跳、温度变了漂不漂、换挡之后回不回得来。所以这篇文章不打算把「3 LCD 数字电压表」当单片机入门题讲,而是按「显示位数、ADC 有效位数、前端分压、基准源、采样滤波、校准系数」这条完整链路展开:先理清三位半对 ADC 的真实要求,再给出一套 STM32 或国产 51 内核驱动 LCD1602 / 段码 LCD 的可复现方案,最后落到校准和验证方法上。适合两类人:一类是在嵌入式开发里第一次碰模拟前端,另一类是做过多年 MCU 但被「显示出来了,精度却不对」卡过。

2. 三位半数字电压表的精度门槛:从显示位数反推 ADC 选型

2.1 三位半的含义与有效分辨率

三位半数字电压表,显示范围 0 到 1999,满量程通常是 2000 个计数。2V 档分辨率 1mV,20V 档分辨率 10mV,200V 档分辨率 100mV。很多人直接拿「满量程 / 1999」当精度用,这是第一个误区:显示位数只决定读数能分辨到多细,真正的误差由 ADC 的线性度、基准源温漂、分压电阻比例和接触电阻共同决定。

以 2V 档为例,目标精度 ±(0.5% + 1d) 时,允许误差是 2V × 0.5% + 1mV = 11mV,约 11 个 LSB(按 1mV 一个字算)。目标精度提到 ±(0.1% + 1d) 时,允许误差只有 3mV,也就是 3 个字。这就是选型的分水岭:0.5% 精度级用 MCU 内置 12 位 ADC 就能做,0.1% 级必须上 16 位外部 ADC 或 24 位 Σ-Δ,并配合校准。

显示位数满量程计数对应分辨率目标精度ADC 建议
3 1/2 位19992V 档 1mV±0.5% + 1d12 位内置(校准后)
3 1/2 位19992V 档 1mV±0.1% + 1d16 位 ADS1115 / MCP3421
4 1/2 位199992V 档 0.1mV±0.05% + 2d24 位 Σ-Δ + 精密基准
5 1/2 位1999992V 档 0.01mV±0.01% + 5d24 位 Σ-Δ + 计量级基准

需要说明的是,内置 12 位 ADC 在 3.3V 基准下每 LSB 约 0.8mV,显示分辨率 1mV 时还有约 1.2LSB 的余量;但余量只够「读出这个数」,不保证「这个数是对的」。所以只要目标不是玩具级别,外置基准和校准系数不能省。

2.2 前端信号链路:分压、跟随与滤波

常见做法是三个量程共用一个输入网络,2V 档直接进运放,20V 和 200V 档先经分压器衰减。分压电阻按 10MΩ 输入阻抗设计,20V 档用 9MΩ + 1MΩ,200V 档用 9.9MΩ + 0.1MΩ;分压比的精度直接影响读数的比例误差,电阻至少要 0.1% 精度金属膜,温漂选 ±25ppm/°C 以内。如果手头只有 1% 电阻,不要直接焊上去,后面校准阶段用软件系数把分压比的偏差吃掉,这也是嵌入式仪表常见的「软件修硬件」做法。

运放跟随的作用是隔离分压网络的高阻抗和 ADC 的采样电容。STM32 内置 ADC 的采样电容只有几 pF,但输入阻抗依然不够高;用一片国产轨到轨运放(如 GS8552、SGM8521)做电压跟随,输入偏置电流在 pA 级,对 10MΩ 分压网络几乎不产生额外压降。运放输出到 ADC 引脚之间串一个 RC 低通,R 取 1kΩ、C 取 100nF 时,截止频率约 1.6kHz,能压掉大部分开关电源纹波和 LCD 刷新耦合进来的毛刺;如果想对 50Hz 工频有更强抑制,把 R 加到 10kΩ、C 加到 1μF,截止频率约 16Hz,代价是响应变慢,上电后要等两三秒读数才稳定。

输入保护是很多人漏掉的一环。档位切错、探针碰到高压时,分压电阻后面的运放和 ADC 引脚直接暴露在过压之下。标准做法是在运放输入端并联两个二极管到电源轨,或用 BAV99 钳位;再串一个 10kΩ 限流电阻,让二极管能吃掉冲击电流。

2.3 基准源与供电:为什么不能直接用 VDDA 当参考

MCU 的 VDDA 来自 LDO 输出,LDO 的温漂、负载调整率以及数字电路开关带来的纹波,都会直接折算成读数的比例误差。内部基准同样不建议用:STM32F1 的内部带隙基准(VREFINT)只有约 1.2V,而且校准系数在出厂时只保证 ±几 mV 的水平,作为电压表参考远不够。

推荐在模拟电源轨上放一颗专用基准,REF3033(3.3V 输出,温漂 20ppm/°C)或者国产的 TL431 加精密分压网络。这里要特别注意基准的噪声和输出能力:REF3033 输出电流只有几十 mA,只能给 ADC 的 VREF+ 引脚供电,不能同时给 LCD 背光和运放供电。运放供电直接从模拟 LDO 取,背光驱动另走数字电源,避免大电流走线串进基准回路。

提示:ADC 的 VREF+ 引脚上并一个 1μF 陶瓷电容和 0.1μF 高频电容,位置尽量靠近引脚;基准芯片的输出电容按数据手册推荐值来,不要盲目加大,否则会引入环路不稳定,表现为读数在最后一位上来回跳。

3. LCD 显示方案选型与嵌入式驱动:从 LCD1602 引脚到 TM1622 段码屏

3.1 三类 LCD 显示方案对比

「LCD 数字电压表」的显示方案有三种常见路线:LCD1602 字符屏、段码 LCD 加驱动芯片、SPI/I2C 接口的 OLED 或 IPS 彩屏。三者对应不同的产品定位,功耗、成本、开发难度差别很大。LCD1602 是课程设计和快速原型最常见的载体,内置 HD44780 控制器,4 位总线模式只占 6 个 IO,带背光和对比度调节;段码 LCD 是万用表、温控仪这类电池供电设备的首选,自身不发光,功耗在微安级,但必须配 HT1621、TM1622 这类专用驱动芯片;OLED 和 IPS 屏接口简单、显示内容自由,适合做波形或调试信息展示,但功耗和成本最高。

显示方案接口功耗成本适用场景
LCD1602并口 4/8 位背光约 20mA快速原型、学习板
段码 LCD + HT1621/TM1622SPI 或三线串行液晶 μA 级手持仪表、电池设备
OLED SSD1306I2C/SPI10~30mA调试面板、富文本显示

实际开发中我一般这么定:做样机验证逻辑用 LCD1602,因为初始化简单、字符显示不需要做字库;做量产前的手持表形态换成段码 LCD,提前把 COM 和 SEG 映射关系定下来,PCB 和玻璃液晶需要一起设计。LCD1602 和段码屏之间不是代码替换关系,而是整个显示架构的切换,所以下面两小节分开给驱动思路。

3.2 LCD1602 四线驱动:初始化时序、引脚与背光亮度

LCD1602 的引脚功能有固定约定:1、2 脚是电源,3 脚 VL 是液晶对比度调节,4 脚 RS 选寄存器(0 指令、1 数据),5 脚 RW 读写选择,6 脚 E 是使能脉冲,7~14 脚是 D0~D7 数据线,15、16 脚是背光正负极。四线模式下只用 D4~D7,初始化必须按 HD44780 规定的一套序列来,很多初学者直接在 4 位模式下发 0x28,结果屏上全是乱码,原因就是缺少从 8 位模式切换回 4 位模式的引导过程。

// 四线模式初始化:必须先通过三次 0x03 让控制器回到 8 位模式 void lcd_init(void) { GPIO_InitTypeDef gpio = {0}; // RS=PA0, E=PA1, D4~D7=PA2~PA5 // ... GPIO 初始化代码 delay_ms(50); // 上电等待,HD44780 要求 >40ms lcd_nibble(0x03); // 第一次发 0x03,8 位模式 delay_us(5000); // 等待 >4.5ms lcd_nibble(0x03); // 第二次发 0x03 delay_us(200); // 等待 >150us lcd_nibble(0x03); // 第三次发 0x03 lcd_nibble(0x02); // 关键切换:0x02 表示进入 4 位模式 lcd_cmd(0x28); // 4 位总线、2 行显示、5x8 点阵 lcd_cmd(0x0C); // 显示开、光标关、不闪烁 lcd_cmd(0x06); // 写入后地址自动加 1,整体不移动 lcd_cmd(0x01); // 清屏,等待 >1.52ms }

lcd_nibble 和 lcd_cmd 的核心是 E 脉冲时序:先把 RS 和 D4~D7 数据放好,E 拉高,保持至少 450ns,再拉低,控制器在 E 下降沿锁存数据。HD44780 要求 E 高电平时间最小 450ns,STM32 在 72MHz 下一条 NOP 约 13ns,所以写进时序里的延时不能用几条空语句糊弄,建议用定时器或软件延时库,且对照数据手册确认。

背光亮度控制有两种方式:直接串电阻限流,或者用 PWM 调光。PWM 调光时注意背光 LED 的驱动电流是几十 mA,不能直接用 GPIO 灌入,需要三极管或 P-MOS 管做开关;PWM 频率选 1kHz 以上,低于 500Hz 会看到明显闪烁。液晶的对比度是调 VL 引脚的电压,这个电压不能用 PWM 调,VL 脚的等效电容会让 PWM 波形积分成直流,反而可用这一点做个简易的软件对比度调节。

3.3 段码 LCD 驱动:HT1621 与 TM1622 的寄存器兼容坑

段码 LCD 的嵌入式驱动思路和字符屏完全不同:液晶玻璃上没有现成的字符库,所有数字和单位都是 COM/SEG 交叉点上的像素。驱动芯片的作用是把 MCU 通过串行接口发来的显示数据锁存,再输出多路 COM 扫描和多路 SEG 波形。国产芯片里 HT1621 和 TM1622 用得最多,两者都支持 1/3 偏置、4 COM 模式,但寄存器映射和写命令格式有差异。

// TM1622 写入 3 位命令 + 9 位数据(HT1621 是 3 位 ID + 6 位命令,注意区分) void tm1622_write_cmd(uint8_t cmd) { cs_low(); for (int i = 0; i < 3; i++) { // 前 3 位:命令 ID=101 clk_pulse((cmd >> 5) & 1); // 实际从高位开始,逐位移出 } // 第 4 位为 0 表示写命令 for (int i = 0; i < 8; i++) { clk_pulse((cmd >> (7 - i)) & 1); // 8 位命令数据 } cs_high(); }

写这段代码时最容易踩的坑是把 TM1622 的指令系统当成 HT1621 直接抄。HT1621 的命令是 3 位 ID 加 6 位数据,TM1622 是 3 位 ID 加 9 位数据,system enable、bias setting、display on 这些命令的位位置都不完全一样。替换芯片时不能用「引脚兼容」想当然,必须逐位核对数据手册;量产设计时建议把驱动层封装成 lcd_display_set(seg, com, on) 这样的抽象接口,底层是 HT1621 还是 TM1622 只影响最下面一层,上层显示逻辑完全不动。

亮度在段码屏上通常不是调 PWM,而是调 LCD 的偏置电压(V0/VLCD)。驱动芯片内部有偏置电阻产生 1/3 偏置电压,外部 VLCD 引脚串联一个可调电阻到电源,调整对比度。这个电阻的取值和液晶玻璃的驱动电压有关,3V 液晶和 5V 液晶阻值范围差很多,参考驱动芯片数据手册里的公式算一遍再选型。

4. 采样滤波与校准系数:让末位跳动停下来

4.1 工频抑制与去极值平均

数字电压表显示的不是瞬时值,而是一个时间段内的统计量。ADC 采到的原始数据包含工频感应、开关电源纹波、运放噪声和偶发毛刺。常见的做法是把 20ms 内的采样点做平均,20ms 是 50Hz 工频的整数倍周期,平均后工频分量被大幅抑制;如果系统在 60Hz 地区使用,窗口就要取 16.67ms 或它的整数倍。

#define SAMPLE_N 32 // 32 点去极值平均:去掉最大最小值,避免偶发毛刺拉偏均值 uint32_t adc_sample_filter(void) { uint32_t sum = 0; uint16_t buf[SAMPLE_N]; uint16_t minv = 4095, maxv = 0; for (int i = 0; i < SAMPLE_N; i++) { buf[i] = adc_read_once(); // 每次转换约 12us,32 次约 400us if (buf[i] < minv) minv = buf[i]; if (buf[i] > maxv) maxv = buf[i]; sum += buf[i]; } sum -= minv + maxv; // 去掉一个最大一个最小 return sum / (SAMPLE_N - 2); }

这个滤波策略分两层理解:去掉极值是防偶发干扰,比如 LCD 刷新瞬间数字电路窜进来的毛刺;平均是压低平稳噪声,等效于给测量加了一个低通滤波。32 个点去 2 个极值后剩 30 个点参与平均,如果噪声是白噪声,均值的标准差降为原来的 1/√30,约 5.5 倍。采样点数再往上加,改善幅度变缓,但响应时间变长,电压表每秒刷新 10 次的情况下,每个显示周期做 32 点采样已经够用。

提示:ADC 采样窗口要避开 LCD 的每帧刷新时刻。做 LCD1602 清屏或写大字符串时,数据总线翻转产生的数字噪声会耦合到模拟回路,表现为读数周期性跳动。一个简单办法是「先更新显示,再采样,再算数值」,把采样放到显示指令完全结束之后。

4.2 两点校准与三点校准:系数存 Flash 而非硬编码

任何 ADC 都有失调和增益误差,校准的目标就是用软件把实测曲线修正到理想直线。两点校准是最小可用方案:输入 0V 记录零点码值,输入一个精确参考电压记录满度码值,然后按 y = (x - zero) × (ref - zero) / (code_ref - zero) 计算出电压。这里的零点和满度码值就是校准系数 K 和 B,存在 MCU 的 Flash 或 EEPROM 里,不能写死在源代码中,否则换一台设备就得改代码重新编译。

typedef struct { float gain; // 增益系数,单位 V/LSB float offset; // 零点偏移,单位 V uint16_t magic; // 标记 0xA5A5,校验数据有效性 } cal_param_t; // 校准后电压 = adc_code * gain + offset float calc_voltage(uint16_t code) { return ((float)code) * cal.gain + cal.offset; }

三点校准比两点校准多覆盖一个中间点,把 ADC 的积分非线性拉直。做法是利用高精度电压源依次加 0V、1/2 满度、满度三个值,记录对应码值,后续读数时先查表找到当前码值落在哪个区间,再用区间两端做线性插值。五点左右的分段线性校准可以覆盖大多数 12 位 ADC 的线性度短板,代价是 Flash 里多存几张表。注意校准系数表的写入次数:校准触发不要做成每次上电都写,只在进入校准模式并确认数据变化后才擦写一次,否则 Flash 寿命会被快速耗尽。

4.3 零漂、温漂与热电势:三位半最容易忽略的三个坑

零点漂移指的是无输入信号时读数不为零。来源包括运放的输入失调电压温漂、ADC 的失调误差以及 PCB 上的热电势。前两者可以通过软件校准掉,但校准时的温度和使用时的温度不同,就会产生残余零漂。工程上常用「开机自校准」缓解:上电后把输入继电器切换到内部短接状态,采样一次零点码值,替换掉校准表里的 offset,再开始正常测量。

热电势是低量程(200mV、2V 档)精度上不去的隐形杀手。PCB 铜箔、焊盘、引脚和探针端子由不同金属材料连接,两边存在温度差时会产生微伏级电压,塞贝克系数大约每摄氏度几到几十微伏。对 2V 档来说微伏级热电势几乎无影响,但做 200mV 档时,焊点上差 1°C 就可能产生 1~2 个字(按 0.1mV/字计算)的误差。缓解方法是让输入回路的焊点尽量等温,用大面积铜箔把两个输入端子连起来,或者在结构上让探头端子靠近,减小温度梯度。这个细节在绝大多数课程设计里没人讲,但量产的万用表电路里都会体现。

5. 精度验证与自动量程:从「能显示」到「敢读数」

5.1 用固定电压基准验证精度:三组测试定位误差来源

校准和验证是两回事:校准是修正,验证是确认修正后的误差在允许范围内。最省钱的办法是买一颗 AD584 或国产电压基准模块,输出 2.5V、5V、7.5V、10V 四档;想要更细的验证点,用六位半台式万用表作为标准表,配合可调直流电源从 0V 到满量程扫描。按照下面三组测试跑一遍,基本能把误差来源定位清楚。

测试项方法判定标准失败时优先排查
零点短接测试输入端短接,记录 1 分钟读数最后一位跳动不超过 ±1 个字运放失调、PCB 热电势
线性度扫描输入 0/25/50/75/100% 满量程各点误差在 ±(0.1%+1d) 内分压电阻比例、ADC 非线性
电源变化测试供电从额定值 ±10% 变化读数变化小于 0.05%基准源抑制比、LDO 稳压能力

零点短接测试时用铜导线直接短接输入端,不要用夹子夹两根独立的表笔线,那样等于在一个回路里引入两个不同金属的接触点,热电势反而把零点顶起来。线性度扫描时每次改变输入后等 2 秒再读数,给前端 RC 滤波和软件去极值平均一个稳定时间;如果中间点误差明显大于两端,说明 ADC 的 DNL 偏大,优先考虑外部高位数 ADC,而不是继续堆滤波。

5.2 自动量程切换与滞回控制

手持电压表没有档位旋钮时就要做自动量程。切换逻辑的核心是滞回:升档和降档使用不同阈值,防止读数在档位边界反复横跳。常见的做法是用量程上限的 90% 作为升档点,用上限的 10% 作为降档点。比如当前 20V 档,读数超过 18V 升到 200V 档;低于 2V 降到 2V 档。如果升档和降档都用同一个阈值,10.01V 的输入会在 10V 档和 100V 档之间来回切换,LCD 上的小数点闪个没完。

换挡瞬间还要处理一个问题:分压电阻网络的建立时间。9.9MΩ 和 100kΩ 的电阻网络等效阻抗很高,加上后端滤波电容后时间常数可能到几十毫秒;切完档位不能马上采样,要等 50~100ms 再做第一次读数。软件里可以加一个 ch_range_change_pending 标记,换挡后先把显示清掉或显示「---」,等采样稳定后再刷新数值,避免显示一个明显错误的中间态。

5.3 把电压表变成采集节点:串口输出与记录

做嵌入式开发时,一个只能看不能存的数字电压表价值有限。把读数通过串口输出,接上位机或记录仪,就变成简易数据采集系统。输出格式用纯文本 TSV 最省事,Excel 或 Python 脚本都能直接解析:

// 主循环每秒输出 10 次,逗号分隔时间戳和电压值 printf("t=%lu ms, V=%.3f V\r\n", millis(), calc_voltage(adc_sample_filter()));

printf 在嵌入式中占用不少资源,重定向到串口后注意每个字符的发送耗时,115200 波特率下每字符约 87us,一帧 30 字符要 2.6ms,对 10Hz 刷新率毫无压力。如果后续要接 ESP32 做无线数据上传,建议在 MCU 端只做采样和格式封装,把串口数据通过 ESP32 的 UART 透传或 AT 指令转发,这样电压表本身不依赖网络,断电单机也能用。这正好和嵌入式学习路线里「裸机显示 → ADC 采集 → 通信协议 → 无线组网」的成长路径衔接起来。# 3 LCD数字电压表_电压表_LCD_嵌入式数字电压表_

1. 三位半LCD数字电压表:从档位到末位

「3 LCD数字电压表」这个标题拆开看:3 代表三位半(3 1/2 digit),也就是最大显示 ±1999;LCD 是显示载体;嵌入式是它的实现形态。很多人第一次做这个项目是在 51 单片机课上,用 STC89C52 加 ADC0809 加 LCD1602,焊一块洞洞板,把电位器的分压读出来显示成电压。那套东西能跑,但离「数字电压表」这四个字还有一段距离。真正的数字电压表要考虑量程切换、输入阻抗、基准源温漂、工频干扰、校准系数存哪、末位跳动怎么压。这不是一个入门级小制作,而是一个典型的嵌入式数据采集系统——ADC 选型、信号调理、显示驱动、标定算法全都涉及。这篇文章按工程落地的顺序把这套方案拆开讲:前面先解决「精度从哪来」的硬件问题,中间把 LCD 驱动写透,后面给出采样滤波与校准的完整代码思路。适合已经在做嵌入式但第一次碰模拟前端的开发者,也适合想把手上的课设方案从「能显示」做到「大概准」的人。

2. 三位半精度不等于ADC位数:先把硬件链路理顺

2.1 三位半的计数范围与有效分辨率

三位半数字电压表的核心指标是计数范围。满量程 1999,意味着 2V 档分辨率是 1mV,20V 档是 10mV,200V 档是 100mV。这个分辨率是由显示位数决定的,但它只是「最小读数变化」,不等于测量精度。举个具体例子:12 位 ADC 满量程 4096,理论上能分辨 3.3V/4096≈0.8mV,看起来够用;但 ADC 的积分非线性(INL)、增益误差、基准源温漂叠加起来,末位跳动 2~3 个字很正常。所以精度要看整个链路的误差预算,不能只看 ADC 位数。

嵌入式数字电压表常见目标是做到 ±(0.5% + 1d) 或 ±(0.1% + 1d) 的精度等级。0.5% 精度对 2V 档是 ±10mV 加 1 个字,12 位内置 ADC 配合校准完全能做;0.1% 精度是 ±2mV 加 1 个字,12 位 ADC 的 INL 已经把预算耗掉大半,这时候需要外部 16 位 ADC,或者用内置 ADC 做多点分段校准。选型时有两条路:STC89C52 这类 51 内核没有内置 ADC,经典做法是外挂 ADC0809,但 8 位分辨率只有 256 步,做三位半显示非常勉强;STM32F103 这类 Cortex-M 内核自带 12 位 ADC,采样保持外部加一级运放跟随,是最省事的方案。

2.2 量程电阻网络与输入阻抗:从 2V 到 200V 靠分压

数字电压表必须有多个量程,或者至少把输入范围做得够宽。常见做法是 10MΩ 输入阻抗,通过电阻分压网络把 200V 缩小到 ADC 可接受的范围。以 STM32 为例,ADC 输入范围 0~3.3V,2V 档用 1MΩ + 1MΩ 分压,20V 档用 9MΩ + 1MΩ,200V 档用 99MΩ + 1MΩ。电阻精度直接影响读数比例误差,分压电阻用 0.1% 精度金属膜电阻,温漂选 ±25ppm/°C 以内,否则温度一变读数就飘。

分压网络后面要加一级电压跟随器。因为 ADC 采样电容在采样瞬间会抽取电流,直接接高阻分压网络会导致采样值偏低;用运放做缓冲后,输入阻抗由电阻网络决定,输出阻抗被运放压到很低。运放选型看三点:输入偏置电流要小、轨到轨输出、带宽不必高,1MHz 级足够。选型表格供参考:

方案ADC 位数精度目标成本适用场景
STC89C52 + ADC08098 位±1% + 1d最低51 入门课设
STM32F103 内置 ADC12 位±0.5% + 1d通用嵌入式仪表
STM32 + ADS1115 外部 ADC16 位±0.1% + 1d高精度便携表
STM32 + 24 位 Σ-Δ ADC24 位±0.05% + 1d较高台式精密表

分压电阻的阻值精度可以做在硬件上,也可以留给软件校准。硬件的坑在于:高阻值电阻噪声大,99MΩ 分压到 3.3V 时源阻抗接近 100MΩ,必须在分压后立即接运放,不能直接拉进 ADC 引脚。软件坑在于:分压比误差是固定比例误差,校准系数能吃掉大半。

2.3 基准源:VREF 飘多少,读数就飘多少

ADC 的参考电压直接决定转换结果。STM32 的 VDDA 如果直接当 VREF+,电源纹波和 LDO 温漂会无保留地进到测量结果里。所以正经做法是给 ADC 单独配一颗基准芯片。三位半级别用 TL431 就够,但 TL431 的初始精度和温漂一般,最好选 REF3033 这类 3.3V 固定输出基准,温漂 20~30ppm/°C。如果需要 5V 参考配合 5V 系统,REF195 或 ADR4550 也是常见选择。

基准源布局要单独处理:基准输出引脚靠近 ADC 的 VREF+,中间尽量短;模拟地和数字地在 ADC 引脚附近单点连接;基准芯片下方不要走数字信号线。实测中电源纹波对读数的影响通常在 1~3 个 LSB,如果你发现某一档读数稳定但换另一档偏差变大,先量 VREF 而不是怀疑 ADC。

提示:不要在 VREF+ 上并联大电容。基准芯片的输出电容按数据手册选,通常 1~10μF,过大可能导致环路振荡,表现为末两位数不规则跳变。

3. LCD显示驱动:LCD1602到HT1621的嵌入式写法

3.1 LCD1602 四线驱动与引脚功能

LCD1602 是最常见的字符型液晶,16 列 2 行,内置 HD44780 控制器。接口分 8 位和 4 位两种模式,嵌入式里为了省引脚几乎都用 4 位模式:D4~D7 传数据,RS 区分指令/数据,RW 控制读写,E 是使能脉冲。这里有个初学者常踩的坑:4 位模式下初始化必须先把控制器切回 8 位模式,再切回 4 位,否则后续所有命令都错位。

引脚功能要记清楚:1 脚 VSS 接地,2 脚 VDD 接 5V,3 脚 VO 是对比度调节,通常接一个 10kΩ 电位器到地;4 脚 RS,5 脚 RW,6 脚 E,7~14 是 D0~D7,15 脚背光正极,16 脚背光负极。背光电流约 20~40mA,不能直接怼在 GPIO 上,要串联限流电阻或用一个三极管开关控制。背光亮度可以用 PWM 调,但注意 PWM 频率要高于 1kHz,否则人眼能看到闪烁;这个需求在嵌入手持设备里很常见。

LCD 的读写是异步时序,E 引脚每来一个下降沿,控制器锁存当前数据引脚上的电平。写命令前要把 RS 拉低,写数据前要把 RS 拉高;RW 拉低表示写。4 线模式初始化序列必须严格按数据手册走,下面是基于 STM32 的参考实现:

// 4位模式初始化,RS=PA0 RW=PA1 EN=PA2 D4~D7=PA4~PA7 static void lcd_nibble(uint8_t nibble, uint8_t rs) { GPIO_WriteBit(GPIOA, GPIO_Pin_0, rs ? Bit_SET : Bit_RESET); // RS GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET); // RW = 写 GPIO_Write(GPIOA, // D4~D7 同时输出 (GPIO_ReadOutputData(GPIOA) & 0xFF0F) | (nibble << 4)); GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_SET); // E 拉高 delay_us(1); GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_RESET); // E 拉低,数据被锁存 delay_us(50); } void lcd_init(void) { delay_ms(50); // 上电等待 lcd_nibble(0x03, 0); // 切回8位模式 delay_us(4500); lcd_nibble(0x03, 0); delay_us(4500); lcd_nibble(0x03, 0); delay_us(150); lcd_nibble(0x02, 0); // 正式切4位模式 lcd_cmd(0x28); // 4位、2行、5x8点阵 lcd_cmd(0x0C); // 显示开、光标关 lcd_cmd(0x01); // 清屏 delay_ms(2); }

这段代码的关键在于:HD44780 上电后默认是 8 位模式,如果直接发 4 位命令,高 4 位和低 4 位会错乱。所以先发三次 0x03 让控制器稳定在 8 位模式,再发 0x02 切换到 4 线。E 引脚的高电平维持时间要大于数据手册要求的 450ns,这里用 delay_us(1) 足够。清屏指令 0x01 需要等待约 1.52ms,初始化时不能省,否则后面命令丢。

3.2 LCD1602 显示内容排版与刷新策略

数字电压表的显示页面按功能分区:主读数用大字,单位 V 跟随小数点位;档位信息(2V/20V/200V)、校准状态、低电量提示放副行。LCD1602 的行宽只有 16 字符,一个「199.9 V」加单位就占了 7 个字符,剩下的空间刚好放档位和状态。排版时用 tab 键思路不行,LCD 是字符坐标寻址,直接在指定行列写即可。

刷新策略与 ADC 采样协调:不要在 ADC 采样期间刷新 LCD。LCD 的读写时序会让数据线和控制线剧烈翻转,如果 PCB 布局不理想,数字噪声会耦合进模拟前端,导致末位读数跳 1~2 个字。常见的做法是采样和显示分时:先采完一批数据,滤波,再刷新一次 LCD,然后进入下一轮采样。10Hz 的显示刷新率对电压表已经足够,肉眼看起来是连续更新,还能自然滤掉一部分抖动。

如果需要显示中文,LCD1602 的字符 ROM 里只有 ASCII 和日文假名,没有汉字。解决方案有两个:自己用 5x8 点阵定义少量汉字字模,适合「电压」「档位」这种固定词;或者换成带中文字库的 LCD12864,SPI 接口,成本稍高但省心。如果标题里出现中文,说明大概率用的是带字库方案,这块在嵌入式里是另一个选型分支。

3.3 HT1621 驱动段码 LCD:TM1622 兼容与注意点

段码 LCD 适合显示数字和固定图标,特点是功耗极低,适合电池供电的便携万用表。驱动芯片常见的是 HT1621,兼容型号 TM1622。HT1621 通过三线串口(CS、WR、DATA)控制,内部有 RAM 映射到 LCD 的 COM 和 SEG。对嵌入式来说,关键是弄清「每一位数字由哪几个段组成」——3 位半显示需要 4 个数字位,每个数字 7 段,加上小数点、单位符号,总共 30~40 个段。

TM1622 和 HT1621 的指令格式略有差异,但驱动思路一样:初始化设置偏置和占空比,然后往显示 RAM 写段数据。可以参考这个初始化序列:

void ht1621_init(void) { ht1621_cmd(0x30); // 系统使能,内部 RC 振荡 ht1621_cmd(0x52); // 1/3 偏置,4 COM 公共端 ht1621_cmd(0x02); // 关闭 LCD 偏置发生器 ht1621_cmd(0x06); // 打开偏置发生器 } void ht1621_display_digit(uint8_t pos, uint8_t seg_data) { // 每个数字占 7 段,加上小数点一共 8 位,按位写入对应 RAM for (uint8_t i = 0; i < 8; i++) { ht1621_write_bit((seg_data >> i) & 0x01); } }

段码 LCD 的调试比 LCD1602 麻烦,因为段和 RAM 的映射关系要对着数据手册逐位核对。一个值得用的技巧是「全显测试」:上电后把所有段全部点亮,对照实物逐个标注 COM 和 SEG 位号,这样再写段码映射表就不会搞混。另外注意 HT1621 使用的 32.768kHz 晶振和内部 RC 振荡精度差异,段码 LCD 时钟源不稳定会导致显示闪烁,优先用外部晶振。

4. 采样滤波与校准:末位稳定靠的不是运气

4.1 均值滤波、去极值与工频抑制的取舍

ADC 原始采样值抖动来源有两种:随机噪声和工频干扰。随机噪声用多次采样求平均能有效压低,均值滤波 N 次,信噪比提升 sqrt(N) 倍;工频干扰则需要采样窗口长度正好是 20ms 的整数倍,这样 50Hz 及其谐波能在平均过程中对消。嵌入式数字电压表里,最实用的组合是「去极值 + 滑动平均」:先去掉最大最小值,防止毛刺把均值拉偏,再对剩余样本做滑动平均。

#define ADC_SAMPLE_N 32 uint16_t adc_read_filtered(void) { uint32_t sum = 0; uint16_t maxv = 0, minv = 4096; uint16_t buf[ADC_SAMPLE_N]; for (int i = 0; i < ADC_SAMPLE_N; i++) { buf[i] = adc_read_once(); if (buf[i] > maxv) maxv = buf[i]; if (buf[i] < minv) minv = buf[i]; sum += buf[i]; } return (uint16_t)((sum - maxv - minv) / (ADC_SAMPLE_N - 2)); }

这里的采样点数选 32,去掉一个最大一个最小,剩下 30 个点参与平均。如果采样频率固定,32 次采样的总时间要尽量接近 20ms 的整数倍,这样工频对消效果最好。具体做法是:先用定时器把每次采样的间隔固定下来,比如采样率 1.6kHz,32 点正好 20ms。不要用「连续转换 + 轮询标志」的方式,那样各次采样的时间间隔受 CPU 干扰,工频抑制能力会变差。

在嵌入式面试里经常被问到「ADC 采样如何滤除 50Hz 工频」,答案核心就是采样窗口与工频周期同步。更严格的做法是加一个硬件低通滤波器,RC 的截止频率设在 10Hz 以下,但那样响应时间会变慢,适合稳态测量。对电压表这种慢变信号,RC 低通加软件滑动平均属于标准做法。

4.2 校准流程:两点校准与系数存储

硬件器件都有离散性,分压电阻标称 1MΩ 实际可能 997kΩ,运放有输入失调电压,ADC 有增益误差。这些误差如果不校准,读数偏差可能到几十个字。校准的本质是用一个已知电压源建立「ADC 码值 → 实际电压」的映射关系。最简单的是两点校准:输入 0V 记录零点码值,输入一个参考电压记录增益码值,然后线性插值。

typedef struct { float k; // 增益,单位 V/LSB float b; // 零点偏移,单位 V int32_t zero_code; int32_t ref_code; } cal_param_t; cal_param_t cal; void cal_calculate(void) { // ref_voltage 是校准源实际输出电压,必须用万用表量过 cal.k = (float)REF_VOLTAGE / (cal.ref_code - cal.zero_code); cal.b = 0.0f - (float)cal.zero_code * cal.k; } float cal_to_voltage(uint16_t code) { return (float)code * cal.k + cal.b; }

校准系数需要掉电保存。常见做法是存进 STM32 内部 Flash 的最后一个扇区,或者外挂 AT24C02。问题是参数不是每次上电都是有效的:如果校准系数全是 0xFF,说明 Flash 是空的,不能用;如果校准源当时没接好,校准出的 k 值会异常偏大或偏小。所以存储结构里要加一个 magic number 和 CRC,加载时校验通过才采用,否则退回默认系数。这一点在做产品时要认真处理,开发板上无所谓,但用户手里设备一旦校准数据损坏,读出的电压就是错的。

提示:校准时的参考电压源精度决定了整个系统的精度上限。推荐用 AD584 这类电压基准模块,输出 2.5V/5V/7.5V/10V 四档,比用可调电源的电压表读数当基准可靠得多。

4.3 蓝桥杯与课设场景下的常见坑

这类题目在蓝桥杯嵌入式国赛和 51 单片机课设里出现频率很高。国赛真题的套路是用 ADC 采电位器电压,LCD 显示,再加上按键切换量程。常见的坑往往不是算法,而是「初始化顺序」:LCD 没初始化就写数据、ADC 的采样时间没配置导致转换结果偏小、分压电阻没接运放导致测量值受负载影响。

课设场景还有一个高发问题:电源用的是 USB 5V,而 ADC 参考是 VDDA=3.3V,LDO 输出的 3.3V 纹波直接进到 VREF,造成读数在末位跳个不停。判断方法很简单:把输入短路到 GND,如果读数仍然上下跳,说明是参考或电源问题而不是信号问题。修正方法是把 VREF+ 和 VDDA 用 π 型滤波隔开,或者干脆外接基准芯片。这些点在你投嵌入式简历时经常作为项目经验被追问,能说清楚「误差从哪来、怎么消除」比单纯说「做了一个电压表」有说服力得多。

5. 自动量程与精度验证:让电压表真正可用

5.1 自动量程切换逻辑与判定阈值

多档位电压表如果只靠手动切换,体验大打折扣。自动量程的思路是:先放在最高量程测一次,如果读数很小,逐级降档;如果读数接近满量程,升档重测。切换带滞回,避免在临界点来回跳。参考实现逻辑:

// 档位枚举:2V / 20V / 200V void auto_range_update(float voltage) { switch (cur_range) { case RANGE_2V: if (voltage > 1.9f) range_set(RANGE_20V); // 满量程90%升档 break; case RANGE_20V: if (voltage > 19.0f) range_set(RANGE_200V); if (voltage < 0.2f) range_set(RANGE_2V); // 10%降档 break; case RANGE_200V: if (voltage < 2.0f) range_set(RANGE_20V); break; } }

升档阈值取满量程的 90%,降档阈值取 10%,中间留滞回区,避免 1.95V 时在 2V 档和 20V 档之间反复切换。切换档位时分压网络切换需要时间稳定,切完后要跳过几次采样或做一次短延时,等 RC 建立后再读数。滞回区间的大小和档位切换稳定时间,是自动量程体验好坏的关键参数。

5.2 精度验证的方法与误差来源定位

校准完不能直接交付,要做精度验证。手头没有六位半万用表时,用 AD584 基准模块就可以完成大部分验证:在 2V 档输入 1.0000V,20V 档输入 10.000V,200V 档输入 100V(需要外部分压或更高电压源),对比实测显示值和标准值的偏差。误差分析时注意区分比例误差和偏移误差:比例误差表现为「读数越大偏差越大」,通常是分压电阻或增益校准不准;偏移误差表现为「输入 0V 时显示不为零」,是零点未校准或运放失调。

误差来源定位有个实用方法:把输入短路到 GND 看零点,再输入 90% 满量程看增益,两点一算基本能把系统误差拆开。温度变化引起的误差要用温漂数据估算,比如分压电阻 100ppm/°C,温度变化 10°C,比例误差就是 0.1%,对三位半精度来说已经需要重视。如果这类评估出现在开源项目的 README 或文章里,读起来比「精度很高」四个字可信得多。

5.3 低成本扩展:串口输出与上位机联动

嵌入式数字电压表做完显示和校准后,加一个串口输出可以让它从「一个表」变成一个「数据采集节点」。通过 UART 把滤波后的电压值以 ASCII 格式输出,电脑端用串口助手、Python 或 Qt 都能接收。如果后续打算做嵌入式学习路线的进阶,这套「传感器 → 采样 → 滤波 → 显示 → 通信」的组合是贯穿很多项目的主线。

串口输出还有一个实际好处:校准和联调时可以看原始码值和滤波后的码值,排查哪一级引入误差。示波器能看到信号波形,但看不到 ADC 码值的统计分布;串口把采样码值打出来,用 Python 数一下标准差,滤波效果一目了然。这个习惯比任何示波器都更适合判断「末位跳动是被噪声拉动的还是被算法没滤干净」——前者靠硬件,后者靠软件,定位错了方向就全错了。

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

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

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

立即咨询