1. 从一个“电费焦虑”说起:这个项目到底在做什么
去年夏天我收到一张电费单,比前年同期多了将近四百块。家里电器没添置,空调温度也没调低,钱到底花在哪儿了?我翻遍电力公司的App,只能看到每天的总用电量,至于哪个时段、哪台设备在“偷电”,完全是个黑盒。这种无力感相信很多人都有——你明明想省电,却连敌人在哪都不知道。
这就是我动手做Smart ElectricityGuard的直接原因。它的核心逻辑非常朴素:用一块ESP32作为主控,配合电流互感器(CT)和电压采样电路,实时采集入户或单台设备的电压、电流、功率、功率因数,把数据通过 Wi-Fi 上传到本地服务器或云平台,再结合分时电价算出实时电费和累计成本。你打开手机或电脑,就能看到“此刻客厅空调正在以 1.8kW 运行,今天已经花了 6.3 元”。
这个项目适合谁?如果你有基础的电工常识,会焊接、能看懂简单的电路图,并且玩过 Arduino 或 ESP-IDF,那你可以直接复现。如果你是完全零基础的小白,也不用慌,我会把每个环节拆到“为什么选这个电阻”“为什么代码里要乘这个系数”的程度。整套方案的成本控制在 80 到 150 元之间,取决于你手头已有的工具和模块。
需要提前说明的是,这个项目涉及220V 强电,安全永远是第一位的。我在后面会专门用一节讲强电隔离和校准的注意事项,这部分内容你务必逐字看完再动手。
2. 方案选型:为什么是 ESP32,而不是 Arduino Uno 或树莓派
2.1 主控芯片的取舍逻辑
市面上做电能监测的方案大致分三类:第一类是专用计量芯片(如 BL0937、HLW8032)配 8 位单片机;第二类是 ESP32 这类带 Wi-Fi 的 32 位 MCU;第三类是树莓派加 ADC 扩展板。我最终选 ESP32,理由有三个,而且每个都经过实测验证。
第一,内置 Wi-Fi 和蓝牙。电能监测的核心价值在于“远程可见”,如果主控没有网络能力,你还得额外挂一个 ESP-01 或串口转 Wi-Fi 模块,成本和复杂度都上去了。ESP32 把 Wi-Fi 和蓝牙集成在单芯片里,省掉至少 15 元的模块钱和一堆杜邦线。
第二,双核 240MHz 的处理能力。电能计算需要做均方根(RMS)运算和功率因数计算,采样率至少要 1kHz 以上才能保证精度。Arduino Uno 的 16MHz 单核在跑采样时几乎没法同时处理网络协议栈,而 ESP32 的一个核心专门跑采样算法,另一个核心跑 Wi-Fi 和 MQTT,互不干扰。
第三,ADC 和 DMA 的配合。ESP32 的 ADC 虽然线性度一般,但配合 DMA 可以实现连续采样,不用 CPU 频繁中断。这一点在做真有效值(True RMS)计算时非常关键,后面我会详细讲怎么用 I2S 接口读 ADC 来绕过 Wi-Fi 对采样的干扰。
树莓派的优势是算力强、能跑 Python,但它的 GPIO 没有模拟输入,必须外挂 ADC 芯片(如 MCP3008),而且树莓派本身功耗高、需要 5V 2A 供电,长期运行还得考虑散热和 SD 卡寿命。对于“7×24 小时挂机监测”这个场景,ESP32 的功耗和稳定性明显更合适。
2.2 电流采样方案:CT 互感器 vs 霍尔传感器 vs 分流电阻
电流采样是电能监测的“心脏”,选错了后面怎么校准都白搭。我试过三种方案,直接给结论:推荐开口式电流互感器(CT),型号选 SCT-013-030 或 ZMPT101B 配套的电流版本。
分流电阻方案(在火线上串一个毫欧级电阻测压降)精度最高,但它不隔离,强电直接进 ADC,一旦电阻烧断或焊接不良,220V 直接灌进 ESP32,芯片和人都危险。霍尔传感器(如 ACS712)隔离性好,但温漂大,零点漂移明显,测小电流时误差能到 10% 以上。CT 互感器通过电磁感应取电流,天然隔离,精度在 1% 到 3% 之间,价格只要十几块,是家用监测的甜点选择。
具体接线时,CT 的输出是交流小电流信号,需要在输出端并联一个burden resistor(负担电阻)把电流转成电压。以 SCT-013-030 为例,它的变比是 1800:1,额定输入 30A,输出 1V。如果你要测的最大电流是 20A,那么负担电阻选 62Ω 左右,输出峰值约 0.69V,正好落在 ESP32 ADC 的 0-3.3V 量程内。这个电阻的精度直接影响测量精度,建议用 1% 金属膜电阻。
2.3 电压采样:为什么不用简单的电阻分压
电压采样看起来简单——两个电阻分压,把 220V 降到 1V 以内就行。但这里有个坑:电阻分压不隔离,而且交流信号的负半周会被 ADC 截掉。我的做法是用ZMPT101B 电压互感器,它同样是电磁隔离,输出一个以 2.5V 为中心的交流信号,正好覆盖正负半周。
ZMPT101B 模块自带一个可调电位器,用来校准输出电压幅度。你需要在输入端接 220V,用万用表测输出端交流电压,调到约 1V AC(峰值约 1.414V),这样加上 2.5V 的直流偏置后,信号范围在 1.086V 到 3.914V 之间。等等,3.914V 超过了 ESP32 的 3.3V 上限!所以实际要调到输出峰值不超过 0.8V,即交流有效值约 0.57V。这个细节很多教程都不提,但它是烧 ADC 的常见原因。
3. 硬件搭建:从原理图到实际焊接的完整流程
3.1 核心元器件清单与选型理由
先列清单,再说每个东西为什么选它。
| 元器件 | 推荐型号 | 数量 | 选型理由 |
|---|---|---|---|
| 主控 | ESP32-WROOM-32 | 1 | 双核、Wi-Fi、ADC、I2S,性价比最高 |
| 电流互感器 | SCT-013-030 | 1 | 开口式,免断线,30A量程覆盖家用 |
| 电压互感器 | ZMPT101B | 1 | 隔离采样,输出可调,模块化方便 |
| 负担电阻 | 62Ω 1% 金属膜 | 1 | 将CT电流转为电压,精度关键 |
| 偏置电阻 | 10kΩ 1% | 2 | 生成1.65V直流偏置 |
| 滤波电容 | 10μF 16V | 2 | 稳定偏置电压,滤高频噪声 |
| 电源 | 5V 1A 隔离电源模块 | 1 | 给ESP32供电,隔离市电 |
| 显示屏 | SSD1306 OLED 0.96寸 | 1 | 本地显示实时数据,可选 |
| 外壳 | 阻燃ABS接线盒 | 1 | 强电部分必须封闭 |
这里重点说两个东西。隔离电源模块不是随便选的,它把 220V 转成 5V 的同时,初级和次级之间没有电气连接,这样即使后面电路出问题,也不会把市电引到 USB 口上。我用的是一款 5V 1A 的 AC-DC 隔离模块,输入 85-265V 宽电压,输出纹波小于 100mV。
阻燃接线盒是安全底线。所有强电接线(火线进、火线出、零线)都在盒子内部完成,盒子外面只留一个 USB 口给调试用。我见过有人把 CT 和分压电阻直接焊在洞洞板上,用热熔胶固定,这种做法的风险极高,一旦胶软化导致短路,后果不堪设想。
3.2 电路连接:一步步照着接
整个电路分三部分:强电侧、信号调理侧、数字侧。我按信号流向来说。
强电侧:220V 火线从接线盒的 L 端子进入,先经过一个 1A 保险丝,然后分成两路。一路接到 ZMPT101B 的输入端,另一路穿过 CT 互感器的穿孔(CT 是开口式的,火线从中间穿过即可),然后接到负载(比如空调插座)。零线直接接到负载的另一端。注意,CT 只能穿一根线,如果火线和零线同时穿过,磁场抵消,输出为零。
信号调理侧:CT 的输出两根线接到负担电阻两端,同时并联一个 10μF 电容滤高频。负担电阻的一端通过 10kΩ 电阻接到 3.3V,另一端通过另一个 10kΩ 电阻接地,这样在 CT 无电流时,ADC 读到的电压是 1.65V。ZMPT101B 模块的输出端同样需要加偏置,但模块本身已经带了偏置电路,你只需要把它的输出接到 ESP32 的另一个 ADC 引脚即可。
数字侧:ESP32 的 GPIO34 接电流信号,GPIO35 接电压信号。这两个引脚都是 ADC1 通道,因为 ADC2 在 Wi-Fi 工作时会被占用,这是 ESP32 的一个硬件限制,很多人踩坑。OLED 接 I2C,SDA 接 GPIO21,SCL 接 GPIO22。
注意:ESP32 的 ADC 参考电压是 1.1V,但开发板内部有分压,实际量程是 0-3.3V。不过 ADC 在接近 0V 和 3.3V 时非线性严重,所以信号要控制在 0.2V 到 3.1V 之间。
3.3 焊接与组装:强电部分的“三查三验”
焊接顺序建议从低压到高压。先把 ESP32、OLED、电阻电容焊在洞洞板或 PCB 上,用 USB 供电测试程序能跑通,再接信号调理部分,最后接强电。
强电部分焊完后,必须做三查三验:
- 查短路:用万用表蜂鸣档测 L 和 N 之间电阻,正常应该是无穷大(负载断开时)。如果导通,说明有短路,绝对不能上电。
- 查绝缘:测 L 和外壳金属部分之间的电阻,应该大于 10MΩ。
- 查极性:确认火线进了保险丝和 CT,零线直接到负载,不要接反。
三验是:验保险丝通断、验 CT 穿孔方向(虽然交流方向不影响有效值,但影响相位校准)、验电源模块输出电压是否为 5V。
我自己的习惯是,第一次上电用隔离变压器给整个装置供电,这样即使接错,也不会跳闸或烧东西。如果没有隔离变压器,至少要在插座前串一个漏电保护器。
4. 固件开发:采样、计算、上云的代码实现
4.1 开发环境搭建:Arduino 还是 ESP-IDF
这个问题我被问过无数次。如果你只是想快速跑通,用Arduino IDE配 ESP32 开发板包就够了,安装方法是在首选项里添加开发板管理器网址,然后搜索 esp32 安装。国内网络下载慢的话,可以用乐鑫的国内镜像源,或者直接下载离线包。
但如果你要做高精度采样和稳定运行,我建议用ESP-IDF,因为它的 ADC 驱动更底层,可以配置采样衰减和校准参数。Arduino 的analogRead()默认衰减是 11dB,量程 0-3.3V,但线性度没有校准。ESP-IDF 提供了adc_cali接口,可以根据芯片内部的 eFuse 参数做校准,精度能提升一个档次。
我最终用的是 PlatformIO 配 ESP-IDF 框架,在 VSCode 里开发。这样既有 Arduino 的库生态,又能调用 IDF 的底层 API。配置文件platformio.ini里指定framework = espidf,然后通过idf_component_register引入需要的组件。
4.2 采样策略:为什么用 I2S 而不是定时器中断
ESP32 的 ADC 在 Wi-Fi 工作时会受到射频干扰,读数跳动很大。我试过用定时器每 1ms 触发一次analogRead(),结果功率读数在 0 到 500W 之间乱跳,根本没法用。
后来改用I2S 接口读 ADC,问题解决了。I2S 本来是音频接口,但 ESP32 的 I2S 外设可以配置成从 ADC 连续采样,数据通过 DMA 直接搬到内存,CPU 不用干预。这样采样率可以稳定在 10kHz 以上,而且 Wi-Fi 射频对它的影响小很多。
具体配置是:I2S 模式设为I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN,采样率设为 16000Hz,DMA 缓冲区设 8 个,每个 256 字节。然后调用i2s_read()读取数据,每次读 256 个样本,做 RMS 计算。
4.3 真有效值计算:从采样点到功率值
交流电的有效值不是峰值除以根号二,那是理想正弦波。实际电网波形有畸变,必须用真有效值公式:
Vrms = sqrt( (1/N) * Σ(v[i]^2) ) Irms = sqrt( (1/N) * Σ(i[i]^2) ) P = (1/N) * Σ(v[i] * i[i])其中 N 是一个周期内的采样点数。50Hz 的周期是 20ms,采样率 16kHz 的话,N=320。我实际用 400 个点,覆盖 25ms,确保整周期。
代码里要注意,ADC 读到的原始值是 0-4095,需要先转成电压:
float voltage = (raw / 4095.0) * 3.3;然后减去偏置 1.65V,得到交流分量。电流通道还要除以负担电阻和 CT 变比,还原成实际电流。
功率因数 PF = P / (Vrms * Irms)。如果 PF 小于 0.5,说明负载是感性或容性,比如电机或开关电源,这时候电费计算要考虑无功功率,但家用电表通常只计有功,所以我们就按 P 算。
4.4 电费计算与数据上传
电费计算分两步:先算累计电量,再套分时电价。累计电量就是功率对时间积分:
energy_wh += power_w * (delta_t_ms / 3600000.0);delta_t_ms 是两次采样的间隔,我设 1000ms 算一次,这样 energy_wh 就是瓦时。然后根据当前时间判断处于峰段、平段还是谷段,乘以对应电价。我这边峰段 0.55 元/度,谷段 0.35 元/度,差价不小,所以分时统计很有意义。
数据上传我用 MQTT,主题是home/energy/room1,payload 是 JSON:
{"v": 220.3, "i": 1.82, "p": 401.2, "pf": 0.98, "wh": 1234.5, "cost": 0.68}本地服务器用 Mosquitto,然后 Node-RED 订阅这个主题,存 InfluxDB,Grafana 做可视化。这套组合是 IoT 项目的标配,稳定且免费。
5. 校准与精度验证:让读数对得起电费单
5.1 两点校准法:用已知负载反推系数
刚焊好的板子读数肯定不准,必须校准。我用的是两点校准法:先接一个已知功率的负载(比如 100W 白炽灯),记录 ADC 读数和实际功率;再换一个 1000W 的负载(比如电暖器),再记录一组。然后用线性回归算出增益和偏移。
具体操作是,在代码里定义两个校准系数cal_v和cal_i,初始值设为 1.0。接 100W 灯,串一个功率计看实际功率,调整cal_i直到读数一致。再换 1000W,再调。一般两次就能收敛。
电压校准更简单,用万用表测插座电压,比如 223.5V,然后调cal_v让显示值一致。
5.2 相位校准:感性负载下的误差修正
如果你测的是纯电阻负载(白炽灯、电热水壶),相位误差可以忽略。但测电机、空调、电脑电源时,电流波形滞后电压,如果采样通道之间有延迟,功率计算会偏小。
ESP32 的两个 ADC 是轮流采样的,不是严格同步,所以电流和电压之间有一个固定的时间差。这个时间差对应的相位误差可以用公式修正:
P_corrected = P_measured / cos(θ_error)θ_error 是采样延迟对应的角度。我实测下来,在 16kHz 采样率下,两个通道的延迟约 62.5μs,对应 50Hz 的 1.125 度,cos 值约 0.9998,影响很小。但如果你的采样率低,比如 1kHz,延迟 1ms,对应 18 度,cos 值 0.95,误差就有 5% 了。所以高采样率不仅是为了精度,也是为了相位一致性。
5.3 与电表对比:实测误差在 2% 以内
校准完后,我把它和家里的智能电表做了 24 小时对比。电表读数 8.32 度,我的装置读数 8.47 度,误差 1.8%。这个误差主要来自 CT 的非线性(小电流时误差大)和 ADC 的量化噪声。对于家用监测来说,这个精度完全够用,毕竟你不是拿它去跟电力公司打官司,而是看趋势、找耗电大户。
6. 踩坑记录与常见问题速查
6.1 读数跳动大、Wi-Fi 一开就乱
这是最典型的问题,原因有三个:一是用了 ADC2 引脚,Wi-Fi 工作时 ADC2 被占用;二是没有用 I2S 采样,CPU 中断被 Wi-Fi 打断;三是电源纹波大,5V 电源质量差。
解决办法:换到 ADC1 的 GPIO32-39;改用 I2S 采样;在电源输出端并一个 1000μF 电解电容和一个 0.1μF 陶瓷电容。
6.2 CT 输出始终为零
先检查火线是否真的穿过了 CT 的穿孔,而且只穿了一根。如果穿了两根(火线和零线一起),磁场抵消,输出为零。再检查负担电阻是否焊好,用万用表测 CT 输出端在带载时是否有交流电压。
6.3 电费计算不对,累计值飞涨
多半是delta_t_ms设错了。如果你用millis()算时间差,要注意它溢出后归零。我建议用esp_timer_get_time(),返回 64 位微秒数,几百年都不会溢出。另外,功率单位是瓦,时间单位要转成小时,别把毫秒当小时算。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读数跳动大 | ADC2被Wi-Fi占用 | 检查引脚号 | 换到ADC1引脚 |
| 电流始终为零 | CT穿孔错误 | 检查火线是否单根穿过 | 重新穿线 |
| 功率偏小 | 相位误差 | 对比纯电阻负载 | 提高采样率或加相位补偿 |
| 电费飞涨 | 时间单位错误 | 检查delta_t计算 | 用esp_timer_get_time |
| 上电跳闸 | 强电短路 | 万用表测L-N电阻 | 检查焊接,排除短路 |
| 数据不上传 | MQTT配置错误 | 串口打印连接状态 | 检查IP、端口、主题 |
特别提醒:调试强电部分时,千万不要用手直接碰电路板。我习惯用一把绝缘的塑料镊子来调整电位器,并且每次上电前都默念一遍“火线、零线、地线”。
7. 后续扩展:从单点监测到全屋能源管理
这个项目做完后,我发现它的扩展空间比想象中大。最简单的扩展是加一个SD 卡模块,把数据本地存一份,防止网络断了丢数据。再进一步,可以加继电器模块,当某个设备功率超过阈值时自动断电,比如电暖器忘了关,半夜自动切断。
如果想做全屋监测,可以部署多个 ESP32 节点,每个节点测一路,通过 MQTT 汇总到一个树莓派做总控。树莓派上跑 Node-RED 和 Grafana,手机随时看。我目前做了三个节点:空调、热水器、厨房插座,一个月下来发现热水器是真正的电费杀手,每天保温耗电 2 度多,后来加了个定时开关,电费直接降了 15%。
代码和原理图我整理成了开源仓库,包含 ESP-IDF 工程、校准脚本和 Grafana 仪表盘模板。如果你照着做遇到问题,大概率是强电接线或 ADC 引脚选错了,回头检查这两处,基本都能解决。