ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

51单片机交流电压测量:从信号调理到有效值计算的工程实践

51单片机交流电压测量:从信号调理到有效值计算的工程实践

简介:这是一份面向嵌入式初学者与电子设计实践者的51单片机交流电压测量完整项目资料,围绕交流电压采集、ADC转换与1602液晶显示展开,帮助读者理解模拟电路与数字电路结合的实际测量方案。压缩包共72个文件,约410KB,包含11个C源文件、7个头文件、11个OBJ与LST编译中间文件,以及HEX烧录文件、Proteus仿真工程和Keil工程配置,覆盖从代码编写到仿真验证的完整链路。资源中涉及分压电路设计、ADC0809采样、电压值换算与液晶格式化显示等关键环节,并包含中断与轮询两种采样思路的代码实现,便于对照学习。目前已有1744人学习下载,适合作为课程设计、电子竞赛或自学嵌入式开发的参考案例,可帮助读者快速搭建测量系统、理解软硬件联调流程并积累排错经验。

1. 基于51单片机的交流电压测量:从“能读数”到“敢信数”的那道坎

很多人第一次做基于51单片机的交流电压测量,都会掉进同一个坑:拿ADC读了个瞬时值,乘个系数就敢往屏幕上写220V。结果一接负载,读数乱跳;换个插座,误差能差出十几伏。问题不在单片机,在于交流信号和直流信号完全是两套逻辑——交流有正负半周、有频率、有波形畸变,而51单片机自带的ADC(或者外挂的ADC0809)只能读0到5V的单极性直流。你要做的不是“读一个数”,而是把交流信号搬到一个能被安全采样的直流区间里,再通过算法把有效值还原出来。这篇笔记面向的是手里已经有51核心板、想做一个能真正用起来的交流电压表的人,不管你是跟着江科大51单片机笔记入门的新手,还是已经调过几版硬件的老手,下面这套从信号调理到有效值计算的路径,都能直接抄作业。

2. 交流电压怎么“塞”进51单片机:信号调理与硬件选型

2.1 为什么不能直接把220V接到单片机引脚上

先说结论:直接接必炸。51单片机的IO口耐压是-0.3V到VCC+0.3V,ADC输入范围通常也是0到5V。220V交流电的峰值是311V,有效值220V,别说接进去,就是感应一下都能让引脚进入闩锁状态。所以第一步永远是隔离和降压。

常见做法有三种:电压互感器(PT)、电阻分压加隔离运放、霍尔电压传感器。电压互感器是最稳妥的方案,比如ZMPT101B,它把一次侧和二次侧完全隔离,输出是一个跟随输入波形的小交流信号,典型变比是1000:1000,二次侧串联一个采样电阻就能得到几百毫伏到几伏的交流电压。这个信号是纯交流,有正有负,仍然不能直接进51的ADC。

我一般会选ZMPT101B加一个偏置电路。偏置的作用是把以0V为中心的交流信号抬升到以2.5V为中心,这样正半周在2.5V以上,负半周在2.5V以下,整个波形都落在0到5V的ADC量程内。偏置电压的稳定性直接决定测量精度,所以不能用两个电阻随便分压,最好用TL431或者REF3025这类基准源产生2.5V,再通过运放跟随器缓冲后作为偏置。

2.2 偏置电路和ADC参考电压的配合

偏置电路的核心是一个加法器或者减法器。以ZMPT101B为例,二次侧输出经过采样电阻后得到一个交流电压Vac,你需要把它和2.5V基准相加,再除以2,让输出范围落在0.5V到4.5V之间。这样做的目的是留出余量,避免运放输出饱和到轨到轨的两端,因为运放在接近电源轨时线性度会急剧下降。

具体电路:用一颗LM358或者OPA2340,一路做偏置电压跟随,一路做加法器。加法器的两个输入分别是Vac和2.5V基准,反馈电阻和输入电阻的比例决定增益。假设Vac的峰值是1V,你想让输出摆幅是2V峰峰值,那么增益设为1,输出就是以2.5V为中心、峰峰值2V的信号,范围1.5V到3.5V,非常安全。

ADC参考电压必须和偏置基准同源。如果你用51单片机外挂ADC0809,它的参考电压引脚Vref要接到和偏置电路同一个2.5V基准上,否则偏置电压漂移,读数就跟着漂。很多新手用USB的5V做Vref,又用电阻分压做偏置,结果USB电压一波动,测量值就跳,这就是典型的“玄学跳变”来源。

2.3 采样电阻和运放选型的具体参数

ZMPT101B的二次侧额定电流是2mA,所以采样电阻R_sample = Vout / 2mA。如果你想要二次侧输出1V有效值,R_sample就是500Ω。但实际电路中,互感器二次侧还有内阻,通常几百欧姆,所以采样电阻要选小一点,比如200Ω,然后靠运放放大。这样做的目的是让互感器工作在接近短路的状态,线性度最好。

运放选型上,LM358便宜但失调电压大,典型值7mV,放大100倍就是0.7V误差,对于测量220V来说,0.7V对应大约3V的读数误差,不能忍。我一般用OPA2340或者AD823,失调电压在几百微伏级别,温漂也小。如果成本敏感,至少用LM324里挑一个失调小的,或者加调零电路。

采样率方面,51单片机跑12MHz晶振,机器周期1us,ADC0809转换时间100us,所以最高采样率大约10kHz。对于50Hz交流电,一个周期20ms,采样200个点完全够用。但要注意,ADC0809是逐次逼近型,转换期间输入电压如果变化,会影响结果,所以要在信号调理输出加一个低通滤波,截止频率设在1kHz左右,把高频噪声滤掉。

3. 从采样点到有效值:51单片机上的算法实现与代码拆解

3.1 均方根计算的数学本质和定点数处理

交流电压的有效值定义是:在一个周期内,瞬时电压平方的平均值再开方。公式是Vrms = sqrt( (1/N) * Σ (Vadc[i] - Voffset)^2 )。注意这里减去的Voffset是偏置电压对应的ADC码值,不是0。很多新手直接对ADC码值平方求和,忘了减偏置,结果算出来的是“带直流分量的有效值”,读数会偏高。

51单片机是8位机,做浮点运算非常吃力。Keil C51支持float,但一次浮点开方可能要几百个机器周期,对于实时性要求不高的电压表来说可以接受,但如果你还想做显示刷新、按键处理,就会卡顿。我一般用定点数:把ADC码值当成Q8格式的定点数,平方后用32位累加,最后开方用牛顿迭代法或者查表法。

具体做法:ADC0809输出8位,0到255对应0到5V。偏置电压2.5V对应码值128。所以实际交流瞬时值 = (adc - 128) * (5.0 / 255)。为了定点化,把(adc - 128)左移8位变成Q8格式,平方后得到Q16格式,累加N次后除以N,再开方。开方用牛顿迭代:x_{n+1} = (x_n + a/x_n)/2,迭代3次就能收敛到整数精度。

3.2 采样时序和周期同步的代码实现

采样必须和信号周期同步,否则会引入频谱泄漏。最简单的方法是用过零检测:在偏置后的信号上接一个比较器,输出方波接到51的外部中断INT0。每次过零触发中断,在中断里启动ADC采样,采样N个点后停止。N取200,采样间隔100us,正好覆盖20ms一个周期。

// 51单片机交流采样核心代码(Keil C51) #include <reg52.h> #include <intrins.h> #define SAMPLE_NUM 200 #define ADC_PORT P1 sbit ADC_START = P3^0; sbit ADC_EOC = P3^1; sbit ADC_OE = P3^2; unsigned char adc_buf[SAMPLE_NUM]; unsigned int sum_sq = 0; // ADC0809读取一个字节 unsigned char adc_read() { unsigned char val; ADC_START = 0; ADC_START = 1; // 上升沿启动转换 ADC_START = 0; while(ADC_EOC == 0); // 等待转换结束 ADC_OE = 1; val = ADC_PORT; // 读取结果 ADC_OE = 0; return val; } // 定时器0中断服务,采样间隔100us void timer0_isr() interrupt 1 { static unsigned char idx = 0; TH0 = 0xFF; // 重装初值,12MHz下100us TL0 = 0x9C; if(idx < SAMPLE_NUM) { adc_buf[idx] = adc_read(); idx++; } else { TR0 = 0; // 采样完成,关闭定时器 } } // 计算有效值,返回0.1V为单位的整数 unsigned int calc_rms() { unsigned int i; unsigned long acc = 0; int diff; unsigned int rms; for(i = 0; i < SAMPLE_NUM; i++) { diff = (int)adc_buf[i] - 128; // 减偏置 acc += (unsigned long)(diff * diff); // 平方累加 } acc = acc / SAMPLE_NUM; // 均方 // 牛顿迭代开方 rms = 128; // 初始猜测 for(i = 0; i < 3; i++) { rms = (rms + acc / rms) / 2; } // rms是ADC码值单位的有效值,转换为电压:rms * 5.0 / 255 / 1.414 * 变比 // 假设变比后满量程对应250V,则 rms * 250 / 128 return (unsigned int)((unsigned long)rms * 250 / 128); }

这段代码的逻辑说明:定时器0每100us触发一次中断,在中断里读取ADC0809。采样200个点后关闭定时器,主循环调用calc_rms计算。calc_rms先减去偏置128,然后平方累加,除以N得到均方值,再用牛顿迭代开方。最后把ADC码值单位的有效值转换为实际电压值。参数说明:SAMPLE_NUM决定一个周期的采样点数,50Hz下200点对应20ms;定时器初值0xFF9C对应100us,前提是12MHz晶振;牛顿迭代3次足够8位精度,如果ADC是12位,需要迭代4到5次。

3.3 显示刷新和滤波策略

算出来的有效值不能直接显示,因为电网电压本身有波动,而且ADC每次采样都有随机噪声。我一般做两级滤波:第一级是滑动平均,把最近8次的计算结果取平均;第二级是限幅滤波,如果本次值和上次值差超过5V,就认为是干扰,丢弃本次结果。

显示用LCD1602或者数码管都行。刷新率不用太高,每秒2到3次就够了,因为人眼对电压读数的变化不敏感,刷新太快反而显得跳。如果要做串口上传,可以用51的UART,波特率9600,每100ms发一次数据,格式是“V:220.5\r\n”。

4. 交流电压测量避坑:从跳变到精度崩盘的5个真实翻车现场

4.1 读数一直在跳,万用表却稳如泰山

现象:LCD上的电压值在215到225之间来回跳,换一个插座跳得更厉害,但用万用表测同一个插座,读数稳定在220V。

原因:偏置电压不稳定。很多新手用两个10k电阻分压5V得到2.5V,但5V来自USB或者7805,本身就有几十毫伏的纹波和负载调整率。偏置一漂,整个波形上下移动,有效值计算就跟着变。

解决:偏置电压必须用基准源。TL431加两个电阻就能产生2.5V,温漂只有几十ppm。如果成本允许,用REF3025,初始精度0.05%。另外,偏置电压的滤波电容不能省,10uF钽电容并联100nF陶瓷电容,放在运放输入端。

4.2 测自己家的电是220V,测隔壁插座变成180V

现象:同一个电压表,插在A插座读220V,插在B插座读180V,两个插座用万用表测都是220V。

原因:ADC参考电压和偏置电压不同源。如果ADC的Vref接的是5V,而偏置是2.5V,当5V波动时,ADC的满量程变了,但偏置没变,导致计算出的电压值错误。更隐蔽的情况是,ADC的Vref引脚有接触电阻,或者走线太长,导致实际Vref低于5V。

解决:ADC的Vref和偏置基准必须来自同一个基准源。如果ADC0809用5V做Vref,那偏置也要用5V分压得到2.5V,并且5V要经过LC滤波。更好的做法是都用2.5V基准,ADC0809的Vref接2.5V,但这样输入范围变成0到2.5V,信号调理的偏置要改成1.25V。

4.3 采样点数和定时器初值算错,测出来是实际值的一半

现象:读数是110V,实际是220V,正好差一倍。

原因:定时器初值算错,采样间隔不是100us,而是200us,导致一个周期只采了100个点,但程序里SAMPLE_NUM还是200,多采的100个点落在下一个周期,均方根计算时把两个周期的数据混在一起,有效值就偏了。

解决:先确认晶振频率。12MHz晶振,机器周期1us,定时器模式1是16位,初值 = 65536 - 100 = 65436 = 0xFF9C。如果晶振是11.0592MHz,机器周期是1.085us,初值要重新算。另外,采样点数最好用整数个周期,50Hz取200点,60Hz取167点,不要凑整。

4.4 运放输出削顶,大电压时读数反而变小

现象:测200V时读数正常,测230V时读数反而降到210V。

原因:运放输出饱和。信号调理的增益设得太大,或者偏置电压设得太高,导致大电压时运放输出撞到电源轨,波形被削顶。削顶后的波形有效值比实际值低,所以读数反而变小。

解决:用示波器看运放输出波形,确保最大电压时波形仍然完整。留出至少0.5V的余量,比如运放供电5V,输出最大不要超过4.5V,最小不要低于0.5V。如果余量不够,要么降低增益,要么提高运放供电电压到9V或12V。

4.5 51单片机复位后第一秒读数乱飞

现象:每次上电或者按复位键,LCD显示乱码或者读数从0跳到300再稳定。

原因:上电时偏置电路的电容还在充电,偏置电压从0慢慢升到2.5V,这段时间ADC采到的值全是错的。另外,LCD初始化也需要时间,如果初始化没完成就写数据,会显示乱码。

解决:上电后延时500ms再开始采样,等偏置电压稳定。LCD初始化后也要延时100ms再写第一条指令。如果用的是ADC0809,启动转换前先做一次空转换,丢弃第一个结果。

5. 把精度再压一压:校准、温漂补偿和串口上传的进阶技巧

5.1 两点校准法:用万用表做基准

再好的硬件也有误差,出厂校准是必须的。我一般用两点校准:找一个已知准确的交流电压源,比如用调压器输出100V和220V两个点,分别记录ADC计算出的原始值,然后拟合一条直线。具体做法是在代码里加两个校准系数k和b,实际电压 = k * 计算值 + b。k和b存在EEPROM里,比如AT24C02,上电时读出来。

校准步骤:先测100V,记录读数V1_calc;再测220V,记录读数V2_calc。然后k = (220 - 100) / (V2_calc - V1_calc),b = 100 - k * V1_calc。把k和b写入EEPROM。以后每次上电,从EEPROM读k和b,代入计算。这样可以把精度从±5%压到±1%以内。

5.2 温漂补偿:热敏电阻和查表法

运放和基准源的温漂是精度的隐形杀手。LM358的失调电压温漂是7uV/°C,放大100倍就是0.7mV/°C,对应ADC码值0.036个LSB,看起来不大,但如果你测的是小电压,比如10V,这个漂移就占到了0.07%。如果环境温度变化20°C,误差就上来了。

补偿方法:在偏置电路附近贴一个NTC热敏电阻,用51的另一个ADC通道读温度,然后查表修正。表格可以事先在恒温箱里标定:每5°C一个点,记录该温度下的零点偏移,补偿时线性插值。如果不想这么麻烦,至少把运放换成低温漂的,比如OPA333,失调温漂0.05uV/°C,基本可以忽略。

5.3 串口上传和上位机显示

51的UART配置很简单:TMOD = 0x20,TH1 = 0xFD,TL1 = 0xFD,SCON = 0x50,波特率9600。发送数据时用printf重定向,或者直接写SBUF。格式建议用ASCII,比如“Vrms:220.5V\r\n”,方便上位机解析。

上位机可以用Python的pyserial读串口,实时画波形。下面是一个最小示例:

import serial import time ser = serial.Serial('COM3', 9600, timeout=1) while True: line = ser.readline().decode('ascii', errors='ignore').strip() if line.startswith('Vrms:'): voltage = float(line.split(':')[1].replace('V', '')) print(f'当前电压: {voltage:.1f} V') time.sleep(0.1)

这段代码的逻辑:打开串口,逐行读取,解析“Vrms:”后面的数值。参数说明:COM3根据实际设备改,波特率必须和单片机一致。如果数据乱码,检查晶振频率和波特率是否匹配,11.0592MHz晶振才能产生准确的9600波特率,12MHz晶振会有2%误差,短距离通信没问题,长距离可能丢包。

5.4 一个我踩过的坑:别用USB供电做基准

最后说一个血泪教训。我曾经用USB的5V给整个板子供电,包括ADC的Vref和偏置基准。实验室里测得好好的,拿到现场一测,读数偏了8V。后来发现现场的USB充电头输出是5.3V,而且纹波很大。从那以后,我所有测量板都单独用LDO稳压到5V,再用基准源产生2.5V,USB只用来通信,不参与模拟供电。这个习惯帮我省掉了至少三次返工。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进