首页 / 资讯中心 / 文章详情

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

三相交流电压表程序全解析:从ADC采样到校准的完整闭环 ★ FEATURED ARTICLE
简介面向电子测量与电力监控初学者的三相交流电压表完整程序包涵盖三相电压数据采集、同步采样、有效值/峰值计算、相位分析及过压、欠压、失衡等故障检测的核心功能。程序基于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.65VADC码的“零点”大约在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个半周期平均点数对应的采样长度。为了防止噪声在零点附近反复触发过零检测可以加一点滞回比如把阈值设成“大于mid4才算高、小于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初始为02 增益校准标准源输出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调电压表时就吃过一次愧校准用的标准源没有预热读出的基准值整体偏低折腾了一下午最后才发现是标准源输出还没稳定。后来每次校准都先等标准源稳定半小时再动手这个习惯一直保留到现在。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站