简介:这是一份面向嵌入式初学者与电子设计实践者的51单片机交流电压测量完整工程包,围绕交流电压采集、ADC转换与1602液晶显示展开,适合课程设计、电子竞赛训练及单片机综合项目练手。压缩包共72个文件、约410KB,以C语言源码、头文件、Keil工程文件、编译中间文件及Proteus仿真工程为主,另含hex烧录文件与工程配置,覆盖从源码到仿真的完整链路。资源中可看到分压采样、ADC0809驱动、电压换算、液晶显示与按键交互等模块,能帮助读者理解模拟与数字电路结合、采样量化及单位格式化的实现思路,并可直接在仿真环境中验证测量精度与稳定性。目前已有1744人学习下载,适合希望打通硬件搭建、软件编程与联调测试全流程的读者参考借鉴。
1. 交流电压测量到底难在哪:从一块51核心板说起
很多人第一次用51单片机做交流电压测量,都会掉进同一个坑:拿直流那一套直接往上套,结果读数乱跳、数值对不上、甚至烧了ADC引脚。交流信号和直流最大的区别在于它一直在变——工频50Hz意味着每秒要完成50次完整波形,峰值可能到311V(220V有效值),而51单片机的IO口只能承受5V。所以这个项目的核心不是“怎么读电压”,而是“怎么把高压交流信号安全、准确地变成单片机认识的低压直流信号”。
这份资源围绕的就是这件事:用51单片机(常见STC89C52或AT89S52)配合电压互感器、运放调理电路和ADC0809或TLC549这类外置ADC,完成一路交流电压的采集、有效值计算和数码管/LCD显示。适合正在做课设、电赛训练或者刚学完51想找个综合项目练手的人。它不涉及复杂的DSP算法,但涉及模拟前端设计、采样时序、有效值换算三个硬骨头,啃下来对“单片机+模拟”的整条链路会有完全不一样的理解。
2. 信号调理链路怎么搭:互感器、偏置与ADC选型
2.1 为什么不能直接分压后接ADC
理论上交流220V可以用两个电阻分压到5V以内,但这样做有两个致命问题。第一是共地:分压后的信号和市电没有隔离,一旦火线零线接反,整个单片机系统带高压,调试时摸到USB口就出事。第二是负半周:交流信号有正有负,而51系统是单电源0~5V,负半周会被ADC直接削掉,测出来的有效值完全不对。
常见做法是用电压互感器(比如ZMPT101B)做隔离和降压。ZMPT101B初级串一个限流电阻接交流,次级输出一个跟随输入的小电流信号,再通过一个采样电阻转成电压。典型接法是初级串47kΩ/1W电阻,次级并一个100Ω~200Ω采样电阻,这样次级电压大约在毫伏到几百毫伏级别,既安全又好调理。
2.2 单电源偏置电路怎么算
互感器出来的信号是围绕0V的正负交流,需要抬升到2.5V附近才能进ADC。最简单的方式是用两个等值电阻分压出2.5V,通过一个运放跟随器做偏置基准,再把互感器信号通过隔直电容耦合到运放输入端。
// 偏置电压计算参考(非代码执行,用于设计时估算) // VCC = 5V, R1 = R2 = 10kΩ // Vbias = VCC * R2 / (R1 + R2) = 2.5V // 互感器次级信号经隔直电容后叠加在Vbias上 // 运放输出范围应控制在0.5V ~ 4.5V之间,留出ADC线性区余量这里的关键参数是偏置电压的稳定性。如果直接用电阻分压而不加运放跟随,ADC采样瞬间的电流会把偏置点拉偏,读数就会周期性抖动。我一般会用LM358的一半做跟随器输出2.5V基准,另一半做信号放大或缓冲。放大倍数根据互感器次级输出幅度来定,目标是让ADC满量程附近对应最大被测电压,这样分辨率最高。
2.3 ADC选型:并行还是串行
51单片机本身没有内置ADC(STC12系列有,但经典51没有),所以必须外挂。常见选择有两种:并行接口的ADC0809和串行接口的TLC549/ADC0832。
ADC0809是8位8通道,并行输出接P1口,用ALE和START控制转换,EOC查询转换结束。优点是时序直观、资料多,缺点是占用IO多、转换速度一般(约100μs)。TLC549是8位串行,只用3根线(CS、CLK、DATA),节省IO但时序需要严格配合。
| 对比项 | ADC0809 | TLC549 |
|---|---|---|
| 分辨率 | 8位 | 8位 |
| 接口 | 并行8位 | 串行3线 |
| 转换时间 | 约100μs | 约40μs |
| IO占用 | 11根以上 | 3根 |
| 适合场景 | 教学、IO充裕 | IO紧张、多路复用 |
如果只是测一路交流电压,TLC549更简洁。但如果你用的是开发板配套教程,大概率是ADC0809,因为它的时序图在教材里讲得最细。
3. 采样时序与有效值计算:从瞬时值到真实电压
3.1 采样率怎么定才不混叠
交流50Hz,一个周期20ms。根据奈奎斯特采样定理,采样率至少100Hz,但实际做有效值计算时,采样率太低会导致波形信息丢失严重。常见做法是每个周期采64点或128点,也就是采样率3200Hz或6400Hz。对于51单片机来说,6400Hz意味着每156μs要完成一次ADC读取+存储,TLC549可以做到,ADC0809稍微紧张但也能跑。
// 定时器0初始化,设定采样间隔 // 假设晶振12MHz,机器周期1μs // 定时器初值 = 65536 - 采样间隔(μs) // 采样间隔156μs对应约6400Hz采样率 void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // 定时器0,模式1,16位 TH0 = (65536 - 156) / 256; // 高8位 TL0 = (65536 - 156) % 256; // 低8位 ET0 = 1; // 使能定时器0中断 EA = 1; // 开总中断 TR0 = 1; // 启动定时器0 }这段代码设定每156μs触发一次中断,在中断服务函数里启动ADC转换并读取结果。注意采样间隔要略大于ADC转换时间,否则会读到上一次的旧数据。TLC549转换时间约40μs,156μs绰绰有余;ADC0809约100μs,也够用但余量不大。
3.2 有效值计算的两种写法
有效值的定义是:在一个周期内,瞬时值平方的平均值再开方。离散化之后就是:
Vrms = sqrt( (1/N) * Σ (Vi - Vbias)^2 )
其中Vi是ADC读到的瞬时值,Vbias是偏置电压对应的ADC码值,N是一个周期内的采样点数。
// 有效值计算函数 // adc_buf[] 存储一个周期的采样值 // N 为采样点数,VREF为ADC参考电压,通常5V float Calc_RMS(unsigned char *adc_buf, unsigned int N, float VREF) { unsigned int i; float sum = 0.0; float v_bias_code = 128.0; // 2.5V对应8位ADC的码值约128 float temp; for(i = 0; i < N; i++) { temp = ((float)adc_buf[i] - v_bias_code) * VREF / 255.0; sum += temp * temp; } return sqrt(sum / N); }这里有几个参数需要根据实际电路校准。v_bias_code不一定是128,因为偏置电压可能不是精确的2.5V,电阻也有误差。我一般会在上电时先采一段没有信号输入的ADC值,取平均作为实际的偏置码值。VREF也不一定是5V,可以用万用表量一下实际值代入,精度会明显提高。
3.3 周期对齐:为什么你的读数一直在跳
如果采样窗口不是整数个周期,计算出来的有效值会随起始相位变化而波动。比如你固定采128个点,但采样率稍有偏差,128个点可能覆盖1.02个周期或0.98个周期,有效值就会周期性跳动。
解决办法有两种。第一种是过零检测:用比较器把交流信号整形成方波,接到外部中断引脚,在过零点启动采样,采满一个周期后停止。第二种是软件同步:先采一段数据,找到偏置码值的穿越点,估算周期长度,再截取整数个周期计算。第一种硬件成本高一点但稳定,第二种纯软件但代码复杂。
常见做法是过零检测+定时器捕获。用LM393做比较器,输出接INT0,下降沿触发中断,在中断里重置采样计数器,采满N点后置标志位让主循环计算。这样每次都是从同一个相位开始采,读数就稳了。
4. 显示与校准:让读数能信
4.1 数码管还是LCD
数码管动态扫描占用IO多但显示直观,LCD1602占用IO少但需要初始化序列。如果只是显示电压值,LCD1602更方便,因为可以同时显示“Vrms”单位和数值。数码管需要自己处理小数点位置,代码量大一些。
// LCD1602显示有效值 // Vrms_val 为计算得到的浮点电压值 void Display_Voltage(float Vrms_val) { unsigned int v_int; unsigned int v_dec; v_int = (unsigned int)Vrms_val; // 整数部分 v_dec = (unsigned int)((Vrms_val - v_int) * 10); // 一位小数 LCD_SetCursor(0, 0); LCD_WriteString("Vrms: "); LCD_WriteChar(v_int / 100 + '0'); // 百位 LCD_WriteChar(v_int % 100 / 10 + '0'); // 十位 LCD_WriteChar(v_int % 10 + '0'); // 个位 LCD_WriteChar('.'); LCD_WriteChar(v_dec + '0'); // 小数位 LCD_WriteString(" V "); }这段代码把浮点电压拆成整数和小数部分分别显示。注意LCD刷新不要太快,否则会闪烁,一般200ms更新一次就够了。
4.2 校准:用一个已知电压做基准
不管电路算得多准,电阻误差、互感器非线性、ADC增益误差都会让读数偏。最实用的校准方法是:输入一个已知的交流电压(比如用调压器输出100V),记录单片机显示的读数,算一个比例系数。
// 校准系数 float cal_factor = 1.0; // 初始值,校准后修改 // 校准后计算 Vrms_real = Vrms_calc * cal_factor; // 例如:输入100V,显示95V,则 cal_factor = 100.0 / 95.0 = 1.0526校准的时候要注意,输入电压最好选在你测量范围的中段,比如量程0~250V就选150V左右校准,这样两端误差比较均衡。如果只在一个点校准,其他点可能偏差较大,因为互感器和运放都有非线性。
5. 避坑与排查:那些让你调三天的问题
5.1 读数一直是0或者满量程
现象:LCD显示0V或者始终显示最大值,调输入电压没反应。
原因:ADC没有正常转换,或者偏置电压没起来。常见的是ADC0809的START脉冲太窄,或者TLC549的CS时序不对。另一个可能是运放供电没接,偏置电压为0,信号负半周被削掉后ADC读到0。
解决:先用万用表量运放输出引脚,应该在2.5V左右。如果不对,检查运放供电和反馈电阻。然后用示波器看ADC的EOC或DATA引脚,确认有转换动作。没有示波器的话,可以写一个最简单的测试程序,固定读ADC并显示原始码值,看是否随输入变化。
5.2 读数跳动超过5%
现象:输入稳定交流电压,显示值在±5V范围内乱跳。
原因:采样窗口不是整数个周期,或者偏置码值没有实时校准。另外,如果ADC参考电压用的是VCC,而VCC同时给数码管供电,数码管刷新时电流变化会拉低VCC,导致ADC基准波动。
解决:先确认过零检测是否正常工作,用示波器看INT0引脚有没有50Hz方波。如果没有,检查比较器输出。如果过零正常,检查ADC的VREF引脚是否接了独立的基准源,或者至少加一个100μF电解电容+0.1μF陶瓷电容滤波。偏置码值建议每次计算前重新采样一次无信号时的平均值。
5.3 测高压时单片机复位或死机
现象:输入电压升到150V以上时,单片机突然复位或者程序跑飞。
原因:高压通过互感器或分压电阻耦合到了单片机系统,或者电源被拉低。互感器隔离效果不好的话,次级和初级之间有漏电流。另外,如果运放供电和单片机共用一组电源,大信号时运放输出电流增大,可能拉低电源。
解决:检查互感器初级限流电阻是否足够大,47kΩ/1W是最低要求,电压更高时要增加到100kΩ以上。运放电源和单片机电源之间加磁珠或电感隔离,各自配100μF和0.1μF去耦电容。如果还不行,考虑用光耦或者隔离运放做完全隔离。
5.4 有效值算出来偏大或偏小
现象:输入100V,显示120V或者80V,但波形看起来正常。
原因:偏置码值不对,或者ADC参考电压不是5.00V。另外,如果采样点数不是整数周期,有效值会偏大(因为多采了峰值部分)或偏小。
解决:先用万用表量VREF引脚的实际电压,代入计算。然后检查采样点数是否覆盖整数周期,可以用过零检测确认。最后用已知电压校准,算一个比例系数。注意校准系数不要超过1.5,否则说明电路有问题,强行校准会导致线性度很差。
5.5 数码管显示闪烁或亮度不均
现象:动态扫描数码管时,某些位特别暗或者整体闪烁。
原因:扫描频率太低(低于50Hz)会闪烁,限流电阻太大或位选驱动能力不足会导致亮度不均。
解决:扫描频率设在100Hz~200Hz之间,每位点亮时间1~2ms。限流电阻一般选330Ω~1kΩ,位选用三极管驱动(如8550),不要直接用IO口拉电流。如果用的是LCD1602,检查对比度调节引脚V0的电压,通常在0.5V~1V之间。
6. 进阶技巧:用状态机重构采样流程与串口输出
6.1 为什么要把采样逻辑改成状态机
很多人的采样代码是“定时器中断里启动ADC,主循环里等标志位计算”,这种写法在单任务时没问题,但一旦加入按键、显示刷新、串口发送,就会互相阻塞。我后来习惯把整个测量流程拆成状态机:IDLE→WAIT_ZERO→SAMPLING→CALC→DISPLAY,每个状态只做一件事,用定时器统一调度。
// 采样状态机 typedef enum { STATE_IDLE, STATE_WAIT_ZERO, STATE_SAMPLING, STATE_CALC, STATE_DISPLAY } MeasureState; MeasureState state = STATE_IDLE; unsigned int sample_cnt = 0; unsigned char adc_buf[128]; void Measure_StateMachine(void) { switch(state) { case STATE_IDLE: if(zero_cross_flag) // 过零标志 { zero_cross_flag = 0; sample_cnt = 0; state = STATE_SAMPLING; } break; case STATE_SAMPLING: adc_buf[sample_cnt] = ADC_Read(); sample_cnt++; if(sample_cnt >= 128) { state = STATE_CALC; } break; case STATE_CALC: vrms_value = Calc_RMS(adc_buf, 128, 5.0); state = STATE_DISPLAY; break; case STATE_DISPLAY: Display_Voltage(vrms_value); state = STATE_IDLE; break; } }这个状态机每1ms调用一次,采样间隔由定时器中断控制。好处是每个状态执行时间短,不会阻塞其他任务。过零标志在INT0中断里置位,保证每次采样都从同一相位开始。
6.2 串口输出原始数据做波形观察
调试有效值算法时,光看LCD数字很难判断问题。我一般会加一个串口输出,把ADC原始码值按时间顺序发到电脑上,用串口示波器软件看波形。这样能直观看到偏置是否在中间、波形是否被削顶、有没有干扰毛刺。
// 串口发送ADC原始值 void UART_SendByte(unsigned char dat) { SBUF = dat; while(!TI); TI = 0; } void UART_SendBuffer(unsigned char *buf, unsigned int len) { unsigned int i; for(i = 0; i < len; i++) { UART_SendByte(buf[i]); } }串口波特率设9600或115200,发送时加一个帧头(比如0xAA 0x55),电脑端用Python或串口助手解析。我常用Python的matplotlib实时画波形,一眼就能看出问题。
6.3 一个容易被忽略的细节:ADC参考电压的稳定性
最后说一个血泪经验:ADC的参考电压直接决定测量精度,但很多人用VCC做参考,而VCC又给数码管、LCD背光供电。数码管刷新时电流突变,VCC波动几十毫伏,ADC读数就跟着跳。我后来的做法是给ADC单独用一个TL431做2.5V基准,或者至少用LM336,然后软件里把VREF写成实测值。从那以后我每次做ADC项目,都强制先用万用表量一遍VREF实际电压再写代码,这个习惯帮我省了至少三天调试时间。希望帮到你。
本文还有配套的精品资源,点击获取