STM32智能电表制作全攻略:从硬件电路到电能计量算法
简介基于STM32的简易智能电表项目资源面向嵌入式学习者和电力监测开发人员演示如何用STM32实时采集电流电压计算功率、功率因数并累计用电量。资源包共440个文件、约199.59MB主要包含C源码stm32f10x_adc.c、stm32f10x_tim.c、lcd.c等、H头文件、原理图与PCB文件schdoc/pcbdoc、PDF说明文档及编译中间文件覆盖设计文档、硬件连线与工程代码。已有1368人学习/下载。压缩包内代码覆盖时钟与GPIO配置、ADC采样、定时器辅助积分计算、LCD显示等关键逻辑并涉及功率因数相位处理与电能累积校准思路原理图则直观呈现微控制器与电流/电压传感器、显示模块的接法。对于打算深入STM32开发或电力计量领域的读者这是一份不错的实战案例能帮助理解从模拟信号采集到数字计算与显示的完整流程也可作为智能电表项目的设计起点。1. 基于STM32的简易智能电表从宿舍限电到设备计费都能用的计量方案如果你只想给一个小型充电桩、宿舍床位或者工作室设备做用电统计并不需要直接买一个成品智能电表。拿一块常见的 STM32F103 最小系统板配合电流互感器、电阻分压和继电器就能在几天内搭出一个能测电压、电流、功率还能累计电量的计量设备。这个项目里最核心的问题不是“怎么点亮屏幕”而是把220V市电安全地变换成单片机可以处理的模拟信号再在固件里完成有效值计算、电能积分和误差修正。整套方案适合拿来练手也适合作为智能插座、能耗监测节点的改造基础。下面直接按硬件链路、固件实现、烧录避坑和二次开发四个部分拆开讲。2. 硬件设计拆解从220V交流到ADC采集的完整链路很多第一次接触电表项目的开发者第一反应是“用一个电压互感器把220V变成3.3V以下”。方向对但没有考虑ADC输入范围和直流偏置结果样机一上电采样值一直顶到4095。这一章的思路是先把每级信号幅度算清楚再决定选型。2.1 系统框图与器件选型F103最小板 电流互感器 分压电阻整个系统的信号流可以概括为市电经过电流互感器和电阻分压后变成两个幅值在0~3.3V以内的小信号送入STM32的ADC1单片机每20毫秒采集一组波形点计算出电压有效值和电流有效值再互相相乘得到有功功率功率随时间积分得到电能最后把数值显示在OLED上并控制继电器开断负载。器件选型上我倾向于用一块STM32F103C8T6最小系统板而不是直接画核心板。原因很简单项目调试期经常需要插拔SWD、换个晶振或者飞线改采样电阻最小板坏了就换成本低且不容易把主控焊坏。电流采样用常规的穿心式电流互感器规格可以按你的负载范围选择常见的有5A/5mA和30A/30mA两种档位电压采样不用买昂贵的电压传感器用两个高阻值电阻分压即可显示部分用0.96寸IIC接口OLED控制负载用5V继电器驱动级加三极管和续流二极管。下面是这套方案里比较典型的选型表功能模块选型建议关键参数主控STM32F103C8T672MHz主频ADC 12bitPA0/PA1等引脚可用电流采样穿心式电流互感器 30A/30mA次级接33Ω采样电阻额定电流下输出约0.99V有效值电压采样2个120kΩ与1个1kΩ电阻分压把220V有效值衰减到约1.3V以内的交流小信号显示SSD1306 0.96寸OLEDIIC地址多为0x3C或0x3D继电器驱动S8050三极管 5V继电器线圈必须并联续流二极管供电220V转5V AC-DC模块 AMS1117-3.3后级用线性稳压给MCU提供3.3V2.2 电流和电压采样电路先把信号幅度算到ADC安全区电压采样看起来像是“两个电阻把220V分下来”但交流信号是正负变化的直接接到单片机上会出现负电压即使接在ADC引脚内部也会对芯片产生损坏。我这里采用的办法是先用两个120kΩ电阻串联作为分压上臂再用一个1kΩ电阻作为下臂。市电电压有效值220V时峰值约311V分压后峰值约为1.29V。这个信号仍然有正负所以要在ADC输入脚叠加一个1.65V直流的偏置电压把波形整体抬到约0.36V到2.94V的范围内。偏置电路实现起来也不复杂用两个10kΩ电阻把3.3V分压成1.65V接到运算放大器同相输入端运放输出端再接一个跟随器推动ADC输入。运放我习惯选通用型轨到轨单电源运放低成本且能接近0V输出。在ADC输入脚到地之间加一个0.1μF电容起到高频滤波作用这也能明显减小ADC读数抖动。电流互感器部分相对简单。以30A/30mA规格为例一次侧穿一根负载线二次侧经过33Ω采样电阻接地。当一次侧流过额定30A电流时二次侧输出30mA采样电阻两端电压幅值约为0.99V有效值。同样这个交流电压也要先经过偏置电路抬升到1.65V上下再送入ADC的另一路通道。这里有一个容易被忽略的细节直流通路不能开路如果采样电阻悬空二次侧会产生很高的感应电压容易击穿后级运放。我一般会在电压通道和电流通道各接一个运算放大器做成电压跟随器而不是直接并联电阻到MCU引脚。这样做的目的是降低ADC输入阻抗变化对采样电阻分压比的影响避免因为单片机引脚等效电容而让波形发生相位偏移。2.3 电源、继电器和显示把3.3V控制域和5V外设隔离清楚整个系统有两种电压域MCU和外设的3.3V继电器模块需要5V。我的建议是AC-DC模块输出5V后先给继电器供电再经过AMS1117-3.3给MCU供电。这样做的好处是三极管驱动继电器时即使瞬间电流波动较大也不会直接影响到MCU的供电。继电器驱动电路建议给MCU的GPIO加一个下拉电阻防止上电瞬间GPIO输出高电平导致继电器误动作。我在调试阶段犯过这个问题程序刚下载进去GPIO还没初始化继电器已经吸合了。后续改进是在GPIO和基极之间串一个1kΩ电阻再在基极并一个10kΩ下拉电阻让默认状态确定为低电平。显示部分使用IIC接口的OLED屏占用PA9和PA10以外的两个引脚即可通常用PB6和PB7的I2C1外设。OLED屏只需要VCC、GND、SCL、SDA四根线非常适合在飞线阶段使用。按键可以只做两个一个负责切换显示页面一个负责清零累计电能。按键部分不需要额外芯片用GPIO加上拉输入模式就行。3. 固件代码模块详解从ADC采集到电能积分的实现思路硬件搭好以后真正决定电表准不准的是固件里的采样策略和计算算法。这个项目的固件建议直接使用STM32CubeMX生成初始化工程再在应用层叠加计算逻辑。下面我把最核心的代码拆成三个模块讲。3.1 CubeMX初始化ADC通道、定时器和串口的配置CubeMX里需要做的配置比较固定我列一下最关键的几项首先把时钟配置成外部8MHz晶振倍频到72MHzADC1开启两个通道PA0作为电压采样输入PA1作为电流采样输入采样时间建议选239.5个周期这样能通过采样电容把前级阻抗带来的误差压到较低水平USART1打开用于串口调试和协议输出I2C1打开用于与OLED屏通信。ADC初始化代码一般长这样void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); }这段代码把ADC1配置为单次转换模式每次启动后只采集一个通道。这样配置的好处是切换通道时不会出现连续DMA采集带来的缓冲区覆盖问题代码逻辑清晰适合电表这类多通道轮询场景。缺点是需要手动切换通道采样速度不够高但对于50Hz交流采样来说已经够用。如果后续你打算做更高精度的计量再改成双通道DMA循环采集也不迟。实际读取一个通道数据时我用一个通用函数封装uint16_t adc_read_channel(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 5) ! HAL_OK) { HAL_ADC_Stop(hadc1); return 0x0000; } uint16_t value HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return value; }这组函数的参数含义很直接channel选择ADC通道例如ADC_CHANNEL_0对应PA0电压采样ADC_CHANNEL_1对应PA1电流采样。PollForConversion里的5是超时时间单位是毫秒如果转换没完成就返回错误。读取时需要注意配置通道后先启动ADC转换完成后立即停止避免下一次配置通道时上一次转换还没结束而产生脏数据。3.2 电量计算RMS有效值、功率和电能累加电表不能直接拿瞬时值当成有效值。交流信号是正弦波如果只是简单算平均值对于纯正弦波会得到0毫无意义。所以固件里必须做均方根计算也就是对一个完整周期内所有采样点先平方再求平均最后开根号。这也是这个项目里最核心的算法。下面的函数实现了RMS计算float calc_rms(uint16_t *adc_buf, uint32_t sample_count) { float sum_square 0.0f; float voltage, voltage_ac; for (uint32_t i 0; i sample_count; i) { voltage (float)adc_buf[i] * 3.3f / 4095.0f; voltage_ac voltage - 1.65f; sum_square voltage_ac * voltage_ac; } return sqrtf(sum_square / sample_count); }这段代码的关键在于先将ADC原始值换算成实际电压值然后减掉1.65V直流偏置最后对交流分量做平方累加。3.3f / 4095.0f是ADC参考电压与12位分辨率之间的换算系数。采样点数越多RMS结果越接近真实值。我一般每个周期采128个点也就是20毫秒内用ADC连续转换128次然后对这128个点求RMS。有了电压有效值和电流有效值后有功功率按近似公式计算float calc_power(float voltage_rms, float current_rms) { return voltage_rms * current_rms; }这个公式假设负载是纯阻性电流和电压相位一致。实际上电机、开关电源这类负载会有明显相位差会看到功率计算值偏低。这里我留了一个补偿入口后续通过电流采样点序列整体移位相当于对相位差做提前量校正。你可以在代码里加一个PHASE_SHIFT常量把电流数组整体偏移几个采样点再和电压数组对应相乘。电能累加用定时器实现。我让定时器每秒钟产生一次中断在中断回调里读取当前功率然后累加void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM7) { float power_w calc_current_power(); // 每秒积一次转化成kWh需要除以3600和1000 g_meter.energy_kwh power_w / 3600.0f / 1000.0f; } }电能累加的公式是energy_kwh power_w / 3600000.0f。因为功率单位是瓦时间单位是秒1瓦秒等于1焦耳1千瓦时等于3600000焦耳。如果你把积分周期改成100毫秒分母也要跟着变改成power_w * 0.1f / 3600000.0f。这个细节是电量越走越快或越走越慢最常见的原因后面避坑章节会专门提到。3.3 显示、按键和串口协议把计量结果变成可交互的页面应用层不需要太复杂。OLED屏分四个区域第一行显示电压第二行显示电流第三行显示功率第四行显示累计电量。按键按下时切换显示模式例如显示本小时的用电量、当天的用电量等。在STM32中显示字符串的代码建议用snprintf格式化void lcd_update_page(void) { char line1[16]; char line2[16]; snprintf(line1, sizeof(line1), U:%5.1fV I:%5.3fA, g_meter.voltage_rms, g_meter.current_rms); snprintf(line2, sizeof(line2), P:%5.1fW E:%.3fkWh, g_meter.power_w, g_meter.energy_kwh); OLED_ShowString(0, 0, line1); OLED_ShowString(0, 2, line2); OLED_Refresh(); }这里的%.1f表示保留一位小数%.3f表示保留三位小数显示电流时保留小数点后三位可以看清小电流设备的工作状态。OLED刷新频率不用太高每秒刷新两到三次就足够频繁刷新反而容易让人感觉闪烁。串口协议我采用的是最简单的ASCII帧。外部设备比如上位机发送一帧ATMETER?\r\n单片机解析到这条指令后回复一组文本sprintf(buf, V220.1,I0.520,P114.5,E12.345\r\n);这种方式很适合后期接ESP8266模块把数据转发到云平台或者局域网监控系统。因为你只需要在中断里判断帧头和结尾不需要解析复杂二进制协议调试起来也直观。4. 下载烧录与调试避坑从BOOT配置到标定校准的五个常见问题这一章把电表项目最常见的五类翻车现场集中整理出来。每条都按“现象、原因、解决”三个步骤来描述方便你排查时对照。4.1 烧录不进去先查BOOT引脚和SWD接线现象是程序第一次调试时Keil里点击下载按钮后提示找不到芯片或者连接超时。很多人第一反应是换个ST-Link驱动实际上多半是BOOT引脚状态不对。STM32F103有BOOT0和BOOT1两个引脚BOOT0接地可以正常使用SWD下载如果BOOT0被悬空或者拉高芯片会进入ISP模式此时SWD调试口可能需要重新上电才能识别。解决办法是先检查四根线SWDIO、SWCLK、GND、3.3V确认没有接触不良。然后确认BOOT0通过一个10kΩ电阻接地。如果还是没有连接把BOOT0跳到1接上串口模块的PA9和PA10用串口ISP工具烧录一个测试固件再跳回0继续调试。虽然麻烦但这是排除硬件问题最快的一个路线。另外要注意STM32最小系统板的3.3V和GND必须确认和ST-Link共地否则SWD虽然有信号但电平参考点不对始终会出现连接不稳定。4.2 ADC读数剧烈跳动必须是足够长的采样窗口现象是OLED上电压显示值不稳定在217V到224V之间来回跳而用万用表测量市电明明很稳定。原因有两层第一种是采样点数太少你只采了32个点每个点之间的相位间隔不均匀导致RMS计算结果含有较大噪声第二种是采样窗口没有覆盖完整的50Hz周期导致在波形不同位置重复采样时结果不一致。解决方法是把采样点数提高到128或者256并且确保总采样时间尽量接近20毫秒的整数倍。同时在ADC输入引脚到地之间接0.1μF电容把高频噪声滤掉。如果你的主频和时间基准足够准确建议在定时器溢出中断里触发一次DMA采集这样能保证采样点均匀分布在一个完整周期内。这组处理后电压波动范围通常能控制在±1V以内。4.3 空载电流不归零偏置和零漂一定得标定让我有点意外的一次经历是接上电表但没接任何负载电流居然显示0.23A。查到最后发现是运放输出偏置电压并不是精确的1.65V实际可能只有1.58V或者1.72V而这些偏差在RMS计算中无法被完全抵消。解决办法是开机后做一个“零点采集”先确保负载断开连续采集1000个电流采样点计算出一个背景RMS值zero_offset之后每次真实计算的RMS都减去这个值。如果减完以后出现了负数就强制为0。这个操作在代码里就是一行current_rms current_rms zero_offset ? current_rms - zero_offset : 0.0f;这个技巧在低电流场景下非常关键否则你测电风扇这类几十瓦的小负载时误差会被放大到不可接受的程度。4.4 功率偏低的隐藏原因电流互感器相位滞后现象是在电表后面接一个纯电阻的加热器用万用表量电压用钳表量电流算出来是2000W电表只显示1700W。按普通误差来说不该差这么多。排查后发现问题出在互感器本身电流互感器二次侧输出电流与一次侧电流之间存在相位差虽然电压和电流都是有效值但乘法计算时如果没对准相位乘积会偏低。解决方式是给电流数组增加一个移位补偿。比如你已经采集了一组电流数据和一组电压数据确定电流滞后电压几百微秒就通过代码把电流数组整体向后移动若干个采样点。移位量可以在程序里用一个常量配置然后在纯阻性负载下反复试几次。对于常用的微型电流互感器这个角度通常在0.7度到2度之间。换算成128个点的采样数组相位补偿大概就是移动一两个点。以下代码示意了如何做循环移位void current_phase_compensate(uint16_t *adc_buf, uint16_t len, uint16_t shift) { uint16_t temp[256]; for (uint16_t i 0; i len - shift; i) { temp[i] adc_buf[i shift]; } for (uint16_t i 0; i shift; i) { temp[len - shift i] adc_buf[i]; } memcpy(adc_buf, temp, len * sizeof(uint16_t)); }这个移位操作会消耗一点CPU时间但放在每次采样完成后执行一次对整体性能影响不大。4.5 电量累计误差大标定系数应该这样校准现象是电表本身数值稳定但和家里的标准计费电能表对比累计电量在一天后偏大或者偏小几个百分点。原因在于分压电阻、采样电阻和互感器变比都不是绝对精确的你必须用标准仪器做一次标定。最实用的标定方法是给电表接一个确定功率的负载比如一个功率相对稳定的白炽灯或者加热器同时用标准功率计输出理论功率值。对比串口日志里的测量功率和真实功率把真实功率除以测量功率得到一个修正系数。然后把修正系数乘到电压或电流的RMS计算里重新验证一遍。我通常会在代码里把修正系数单独定义成宏方便随时修改#define VOLTAGE_CAL 1.015f #define CURRENT_CAL 0.982f这两个数值分别来自电压通道和电流通道的校准。标定完成后再把负载换成不同功率大小验证一次确保修正系数在多个点都接近一致才能确认误差来源于系统增益而非非线性。5. 二次开发扩展把电表升级成远程抄表节点或智能插座控制器基础电表跑通以后这个项目真正有价值的地方在于改造空间。我建议从三个方向选一个继续做第一个是加远程通信用ESP8266模块通过串口转发数据到云平台把设备变成可远程查看的能耗监测节点。改造时注意ESP8266模组和STM32共用3.3V供电但串口电平要确认一致最好用带电平转换的模块避免通信乱码。第二个方向是改成超限断电控制。在定时器中断里判断功率值如果检测到功率超过设定阈值且持续超过三秒就拉低继电器控制引脚断开负载同时通过OLED显示告警状态。这里为了防抖我一般设置一个over_power_count计数器只有连续多次超过阈值才动作避免电机启动瞬间误跳闸。第三个方向是增加掉电存储。虽然看起来只是多写一两个函数但实际体验差异很大。基础版本断电后电量清零再次上电只能从0开始累计完全没有电表该有的“累计”概念。可以引入外部EEPROM比如IIC接口的AT24C02在每次电能累加值变化超过0.01kWh时写入一次同时把当前电压、继电器状态一起保存。代码上我习惯在掉电前把数据写入EEPROM。STM32F103可以用一个RTC闹钟或者外部电压检测引脚配合上电初始化来实现。如果不想加外部芯片也可以用内部Flash的末尾几页存储但需要自己处理擦写寿命和扇区对齐问题简单项目建议直接用EEPROM。改造时还有一个容易忽略的点220V高压部分一定不要用手直接接触。调试时我习惯把AC-DC模块、继电器和采样电阻放在一个隔离的测试盒里MCU板和传感器之间通过长线连接方便在桌上拨弄程序而不用总是靠近市电。因为没有隔离变压器示波器探头直接夹在电源线上也可能导致短路甚至损坏仪器这部分需要格外留意。从那以后我每次给这种带市电的项目改代码都会先用串口把原始ADC值打印出来观察一个完整周期的波形是否稳定确认无误后再让继电器动作。这次做简易智能电表的最大体会是计量精度不是光靠硬件堆出来的固件里的采样策略、相位补偿和标定系数才是决定它能不能被当作一个“电表”来用的关键。希望这篇拆解能帮你的项目少走一段弯路。本文还有配套的精品资源点击获取