ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

三相交流电压表程序全解析:从ADC采样到校准的完整闭环

三相交流电压表程序全解析:从ADC采样到校准的完整闭环

简介:面向电子测量与电力监控初学者的三相交流电压表完整程序包,涵盖三相电压数据采集、同步采样、有效值/峰值计算、相位分析及过压、欠压、失衡等故障检测的核心功能。程序基于Keil工程组织,包含C语言源码、头文件、编译生成的hex/bin/obj/lst文件以及仿真磁盘镜像,共15个文件,整体仅46KB,便于直接编译烧录或二次移植。已有713人学习下载。通过阅读源码与工程结构,可掌握三相电压的数字化测量流程、ADC同步采样策略、真实有效值算法、相序判断及报警阈值设定方法,同时了解数据记录与存储、串口/网络通信等扩展功能的实现思路,为开发工业电力监控、自动化仪表或智能电网终端提供一份可直接借鉴的完整样板。

1. 三相交流电压表完整程序,到底“完整”在哪些地方

电工测量里要做三相交流电压表,看起来只是把三根火线接进来、屏幕上显示三个电压值,但一台表能不能稳定运行、能不能过计量验收,软件“完整”的程度才是分水岭。所谓完整程序,很少只是一个计算有效值的函数,它要覆盖从ADC采样、真有效值计算、频率跟踪、校准参数存取,到显示刷新和通信协议的全套闭环。这类压缩包解压后通常是一整套按模块分好的C源码,附带接线图和校准确认文件,是电测仪表开发里非常典型、也特别适合改造复用的一份“全套作业”。如果你是做嵌入式仪表、工控二次开发,或者正在搭毕业设计里的电测项目,这份程序拆解可以帮你少走很多弯路。

2. 采样链路设计:从380V到ADC引脚,参数怎么定才不算错

2.1 为什么首选电压互感器,而不是电阻直接分压

不想把程序烧进板子后发现显示值和万用表差一大截,就得先想清楚信号怎么进MCU。三相四线系统的线电压是380V、相电压是220V,而MCU的ADC输入范围通常只有0~3.3V,第一个问题是“怎么把220V降到ADC量程内”。常见做法有三种:电阻直接分压、电压互感器取样、霍尔电压传感器取样。对电测仪表来说,我不太建议电阻直接分压,有一条是硬性原因——安全绝缘。直接分压意味着低压侧和高压侧共地,一旦隔离失效,仪表外壳会带电;同时开关柜里的共模干扰会顺着地线进入ADC,采样值里混入大量毛刺,软件怎么滤波都干净不了。常见做法是用1:1的精密微型电压互感器,把一次侧电流限制在1~2mA,二次侧并联采样电阻取电压,再送到运放调理进ADC。霍尔电压传感器精度和带宽都不错,但成本高一个量级,通常只在需要做谐波分析的仪表里用。

三种方案对比下来,电压互感器在精度、成本、隔离安全性上最均衡:

方案隔离方式精度成本适用场景
电阻直接分压无隔离依赖电阻精度最低低压实验板,不建议产品用
1:1精密电压互感器磁隔离0.2%可达中主流三相电压表方案
霍尔电压传感器磁隔离高,带宽大高谐波分析、宽频测量

2.2 一套常用参数表:限流电阻、采样电阻与放大倍数

我一般按这个思路推参数:输入相电压有效值220V,互感器一次侧电流取1mA,那么限流电阻R限 = 220V / 1mA = 220kΩ。实际选型时常用两颗100kΩ、0.5W的金属膜电阻串联,既把耐压分摊开,又留足功率余量。二次侧并联采样电阻R2决定互感器输出电压,例如取200Ω,二次电流1mA产生0.2Vrms,峰值约0.566V,离ADC满量程还很远,于是再接一级运放放大6倍左右,使信号峰值接近3.3V,把12位ADC的分辨率尽量用满。

参数推荐值作用
限流电阻两颗100kΩ串联,0.5W金属膜限制互感器一次电流≈1mA,分摊耐压
互感器1:1精密电压互感器隔离高压侧与低压侧
采样电阻200Ω,0.1%精度二次电流转换为0.2Vrms电压
运放增益6倍左右,反相放大加偏置把信号幅值抬到接近ADC满量程
ADC参考3.3V,外部基准更稳决定满量程电压和分辨率

如果你想做0.5级以上的仪表,外部基准基本是必选项。内部参考电压温漂大,很容易造成读数随温度慢慢漂移,这在电测仪表里属于硬伤,后面排查章节会专门讲。运放部分建议用低失调、单电源供电的型号,输出端串一颗200Ω电阻,再并两颗背靠背的钳位二极管到电源轨,防止误接高压时打坏ADC引脚。这个钳位细节很多参考设计里没有,但现场操作一旦接错线,它能救回一整块板子。

2.3 偏置与中点电压:单电源运放处理交流信号的标配

运放用3.3V单电源供电时,交流负半周无法被ADC采集,必须给信号加上直流偏置,把零点抬到ADC量程的中点。常见做法是把“信号地”接到约1.65V,用一个电压跟随器稳定这个中点电压,互感器二次信号的零轴就等于1.65V,ADC码的“零点”大约在2048附近。这块电路决定了后面所有软件算法的基准,如果硬件上中点电压不稳,软件里再怎么校准都是治标不治本。

ADC初始化对应的骨架代码如下:

/* ADC初始化骨架:三相电压表三通道轮询采样 */ void adc_init(void) { // 1. 配置三个GPIO为模拟输入,分别接入三相调理电路输出 gpio_analog_init(); // 2. ADC配置:12位、右对齐,采样时间尽量放宽 adc_channel_setup(ADC_CH0, ADC_SAMPLE_TIME_MAX); adc_channel_setup(ADC_CH1, ADC_SAMPLE_TIME_MAX); adc_channel_setup(ADC_CH2, ADC_SAMPLE_TIME_MAX); // 3. 用定时器更新事件触发ADC,采样率=10kHz adc_set_trigger(ADC_TRIGGER_TIMER2_UPDATE); timer_set_prescaler(72); // 假设主频72MHz,分频到1MHz timer_set_period(100); // 1MHz / 100 = 10kHz timer_enable(); }

这里有两个参数值得说。采样时间开到最大,是为了降低信号源输出阻抗带来的建立误差,调理电路输出端有钳位电阻和分布电容,采样瞬间会拉低一点电压,采样时间越长误差越小。采样率10kHz对50Hz工频意味着每周波200个点,对纯有效值测量非常宽裕,RMS积分精度足够;如果以后要扩展谐波分析,10k采样率也勉强能覆盖到50次谐波,但那时就要换同步采样方案了。

2.4 三相共用一路ADC:分时采样够不够用

很多“完整程序”不加同步采样芯片,原因是三相电压表的计量对象是有效值。稳态交流下,每个通道分时采样几十毫秒,对RMS计算精度几乎没有影响,只要保证每个通道都采到整数个工频周期就行。但如果工程以后要扩展成功率表、相位角测量,就一定要上同步采样,因为三相同时刻的瞬时值才有意义。这个边界想清楚,硬件成本能省下一大块,而且不会影响当前产品的精度指标。

3. 程序核心代码:测频、整周期截取与有效值计算的落地写法

3.1 工程模块怎么拆:五个模块的职责边界

拿到一套完整程序,先别急着从头读,按模块拆开看效率最高。我习惯把三相电压表工程分成五块:初始化、采样、计算、校准存储、显示通信。每块之间只留几个接口函数,主循环只做调度,不掺和具体逻辑。

模块职责对外接口
init时钟、GPIO、ADC、定时器、串口sys_init()
capture定时器中断搬运ADC数据进缓冲区capture_start()
calc过零测频、整周期截取、RMS计算calc_three_voltage()
calib校准参数存取、校验cal_save(), cal_load()
ui数码管/LCD刷新、按键处理ui_update()

主循环的骨架可以写成这样:

void main_loop(void) { while (1) { if (g_frame_ready) { // 采样满一轮,由定时器中断置1 calc_three_voltage(); // 计算三相有效值(ADC码) cal_apply_to_display(); // 套用校准系数,得到显示电压 ui_update(); // 刷新显示和通信寄存器 g_frame_ready = 0; // 清除标志,进入下一轮 } } }

这段代码的核心思想是:主循环永远只在“数据完整”的时候才处理,中断置标志,主循环消费标志。这样即使显示或通信函数偶尔卡一下,也不会破坏采样的连续性。

3.2 中断只搬数据,不在中断里算电压

有一类典型的翻车:程序把RMS计算写进定时器中断,中断服务函数执行时间变长,下次采样间隔被拉大,波形点数分布不均,计算结果也跟着抖动。正确做法是中断里只做“搬数据”这一件事,浮点运算全部放到主循环。

/* 定时器中断服务:只搬运ADC结果,不计算电压 */ void TIM2_IRQHandler(void) { adc_buf[0][write_idx[0]++] = adc_get(ADC_CH0); adc_buf[1][write_idx[1]++] = adc_get(ADC_CH1); adc_buf[2][write_idx[2]++] = adc_get(ADC_CH2); if (write_idx[0] >= BUF_LEN) { // 三个通道写满一轮 write_idx[0] = 0; write_idx[1] = 0; write_idx[2] = 0; g_frame_ready = 1; // 通知主循环可以计算了 } }

每次中断读三个通道,累计写满一轮就置标志。注意缓冲区长度BUF_LEN要能容纳至少10个工频周期,也就是2000点以上,这样主循环做整周期截取时才有足够的数据可选。

3.3 过零测频:避免非整周期采样误差

真有效值的离散计算公式是RMS = sqrt(1/N * Σv²)。这个公式成立的前提是积分区间等于整数个信号周期。如果写死每周期采200点,但实际电网频率是49.8Hz,窗口误差就有0.4%,显示值会周期性跳动,尤其是喜欢用固定点数算RMS的程序,几乎都会在这里踩坑。处理方法:先做过零检测算出实际频率,再按整数个周期截取数据。

/* 从缓冲区里找所有上升沿过零,估算每周波平均点数 */ uint32_t period_points_estimate(const uint16_t *buf, uint32_t len, uint16_t mid) { uint32_t crossings = 0; uint32_t sum = 0; uint32_t last_zc = 0; for (uint32_t i = 1; i < len; i++) { if (buf[i - 1] < mid && buf[i] >= mid) { if (last_zc != 0) { sum += i - last_zc; // 相邻过零间隔 = 半周期点数 crossings++; } last_zc = i; } } if (crossings == 0) return 200; // 默认值,按50Hz估算 return sum / crossings; // 半周期平均点数 }

这段函数返回的是半周期点数,真正的信号周期是它的2倍。做RMS时取2个完整周期即可,也就是用4个半周期平均点数对应的采样长度。为了防止噪声在零点附近反复触发过零检测,可以加一点滞回,比如把阈值设成“大于mid+4才算高、小于mid-4才算低”。

3.4 有效值计算函数:先去掉直流偏置再平方平均

得到整周期窗口后,有效值计算本身很直接,但有一个细节必须处理:信号叠加了1.65V直流偏置,如果直接拿原始ADC码做平方平均,算出来的值会包含直流分量,结果偏大。正确做法是先减去中点码值mid,再平方累加:

/* 计算有效值,单位是ADC码 */ float compute_rms(const uint16_t *buf, uint32_t start, uint32_t points, uint16_t mid) { float sum_sq = 0.0f; for (uint32_t i = 0; i < points; i++) { float v = (float)(int32_t)(buf[start + i] - mid); sum_sq += v * v; } return sqrtf(sum_sq / points); }

这里的mid是关键参数,它对应硬件上1.65V偏置在ADC里的码值,理论上约等于2048,但实际会受电阻精度影响,建议在校准时实测并保存。返回单位是ADC码,真正常显示的电压还要套用下一节的校准函数换算。

3.5 校准函数为什么单独成模块

校准最常用的数学模型是一次线性:显示电压 = raw_adc × k + b。三相各配一组参数,因为每相的互感器、采样电阻、运放增益不可能完全一致,独立的k和b能把三相误差分别校回来:

/* 校准参数结构体,三相各一份 */ typedef struct { float k; // 增益系数,默认1.0 float b; // 偏移量,默认0.0 } CalParam; CalParam g_cal[3]; /* 把ADC码转换为显示电压 */ float cal_apply(float raw_adc, const CalParam *p) { return raw_adc * p->k + p->b; }

k和b的标定流程放在下一章,这里要说的是存储策略:保存时一定要附带一个标志字节和校验值,上电时校验失败就启用默认参数并点亮报警标识,避免现场出现“满量程乱跳”又找不到原因的尴尬局面。

3.6 显示刷新与软件滤波:500ms刷新更稳

数码管或LCD的刷新周期建议500ms,不需要一毫秒一刷。刷太快反而会把数值低位抖动暴露出来,现场观感很差。更稳的做法是对最近4次计算结果做平均再显示,相当于一个简单的一阶低通。如果你的产品对响应速度有要求,比如做过压告警,那告警判断用原始计算结果,显示用滤波结果,两者分开处理,互不干扰。

4. 校准与避坑:把精度从“有显示”做到“能交货”的五个教训

4.1 校准必须先做零点,再做增益

校准前先准备一台标准交流信号源,等级至少0.2级。校准顺序有讲究:先把输入短接,记录此时ADC码作为mid,再输入100.0V标定增益k。先做零点,可以让增益校准不受零点偏移污染,否则k和b会互相牵制,怎么调都差一点。

步骤输入条件记录/计算
1 零点校准三相输入全部短接记录每相mid码,b初始为0
2 增益校准标准源输出100.0Vk = 100.0 / (raw - mid)
3 交叉验证标准源输出20V、200V检查是否落在允许误差带内

4.2 校准参数的保存与恢复:别让设备现场“变砖”

保存参数时,除了写k和b,还要写一个约定的标志字和CRC校验字节。上电时如果校验失败,直接启用默认参数,并把“校准失效”的状态写到显示区。这个习惯能避免一种很坑的情况:用户现场校准到一半断电,参数只写了一半,设备重启后显示满量程,还以为是硬件坏了。

提示:校准参数建议按通道独立存储,不要三个通道共用一个k,否则A相校好了B相还是偏。

4.3 踩坑一:三相电压显示全偏,但用万用表对照又正常

现象:程序算出的电压比实际输入低3%左右,三相都有偏差。 原因:调试时发现采样电阻选成了5%精度的普通碳膜电阻,每相阻值批差很大,校准系数只能盖住固定偏差,盖不住随温湿度变化的阻值漂移。 解决:换成0.1%或0.5%的金属膜电阻;校准前用万用表实测每相采样电阻,记录在调试笔记里。硬件底子不好,软件校准就是无底洞。

4.4 踩坑二:显示数字每秒钟跳几个字

现象:所有通道在最后一位上不停跳动,每秒钟跳2到5个字。 原因:频率跟踪没生效,程序用固定点数截取波形,但实际工频在49.9到50.1Hz之间浮动,窗口误差造成RMS结果周期性波动。 解决:用本章3.3节的period_points_estimate做整周期截取,再对RMS结果做一阶低通滤波。这里要提醒的是,光滤波不解决根因,窗口误差是系统性偏差,滤波只是让数字看起来平了,精度问题还在。

4.5 踩坑三:校准时很准,热机半小时后变了

现象:冷机校准全部通过,设备运行一段时间后显示值偏高或偏低0.3%以上。 原因:一次侧限流电阻功耗较大,自身温度升高导致阻值变化,进而改变互感器一次电流;运放和采样电阻也有温漂,多路误差叠加。 解决:一次侧限流电阻选功率余量大的金属膜电阻,并做额定电压老化测试;有条件的话做高低温箱验证,看温漂方向后再决定要不要加温度补偿查表。这个坑最容易出现在“验收时准、运行三个月后不准”的返工现场。

4.6 踩坑四:上电瞬间ADC读满量程,显示跳高

现象:上电后前几百毫秒,三相电压显示接近最大值,随后慢慢恢复正常。 原因:调理电路的偏置电压还没建立稳定,运放输出瞬间饱和到电源轨,ADC先采到了错误的满量程值。 解决:采样启动延时300到500ms,等偏置稳定后再开中断;主循环里对初始几个帧做数据有效性检查,如果连续两帧变化超过阈值就继续等待,不做显示更新。这个小逻辑还能顺带处理电源跌落恢复的场景。

4.7 踩坑五:现场接线后读数是输入的1.73倍

现象:表显示电压比实际值高约1.73倍,用户以为是仪表坏了。 原因:相电压和线电压设置混淆。程序里默认显示线电压,但现场实际接的是相电压,或者反过来。 解决:在固件里提供一个“相电压/线电压”模式设置项,通过按键或烧录配置写入,不要写死在代码里。现场安装人员不一定看得懂源码,但一定会需要一个切换入口。

5. 不接高压先把程序调准:信号源测试与串口诊断技巧

5.1 用信号发生器模拟工频输入

在接触380V之前,把全部逻辑在低压下调通是必须养成的习惯。让信号发生器输出10V、50Hz正弦波,经过一个隔离变压器或直接串联电阻接进调理板的“电压输入端”,观察ADC原始码是否在预期区间内对称变化。没有信号发生器时,用交流变压器的低压侧也能粗测,但波形失真大,只能验证链路通不通,不能验证精度。

5.2 离线正弦表自检算法:先把算法从硬件里剥出来

程序里保留一个自检函数,用固定正弦表喂给compute_rms,对比理论值。这是把“软件问题”和“硬件问题”分开的最快方法:

/* 自检函数:用固定正弦波验证RMS算法 */ void self_test_rms(void) { static uint16_t tbl[200]; for (int i = 0; i < 200; i++) { float v = 2048 + 3000 * sinf(2.0f * 3.14159265f * i / 200); tbl[i] = (uint16_t)v; } float rms = compute_rms(tbl, 0, 200, 2048); /* 理论RMS = 3000 / sqrt(2) ≈ 2121.3 */ }

如果这里算出来的值和2121.3偏差超过0.1%,问题就在算法层,可能是数据类型溢出、窗口点数不对或mid取值错误;如果离线算得准、接了调理板就不准,问题就在采样链路。这个定位方法能省下一大半查bug的时间。

5.3 串口诊断:把原始ADC码打出来看

调试时通过串口每秒打印一行:三相原始码均值、计算频率、RMS码、校准后电压。如果频率正常、原始波形区间正确但电压不对,问题在校准系数;如果原始数据一侧倒、波形削顶,问题在硬件调理。我在所有仪表项目里都保留这个“调试模式”,平时用编译宏关掉,出问题时打开,比拿示波器到处点快得多。

之前做模拟项目X调电压表时,就吃过一次愧:校准用的标准源没有预热,读出的基准值整体偏低,折腾了一下午,最后才发现是标准源输出还没稳定。后来每次校准都先等标准源稳定半小时再动手,这个习惯一直保留到现在。希望帮到你。

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

RELATED READING

延伸阅读

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