简介:这份驱动源码围绕 hlw8112 能源计量芯片展开,覆盖电力、燃气等能源计量场景,适合嵌入式开发、智能仪表及电力计量方向的工程师参考。压缩包共 6 个文件,由 3 个头文件与 3 个 C 源文件组成,RAR 整包仅 7KB,代码量精简;头文件用于寄存器定义与接口声明,C 文件则实现 SPI 读写、初始化及数据处理逻辑,便于快速移植到资源受限的 MCU 项目。源码完整实现 hlw8112 的初始化、SPI 接口通信、寄存器访问、计量数据获取与校准流程,其中校准包含零点校准和线性校准,可帮助开发者理解如何保证能量计量的准确性与一致性,整体结构清晰,可直接嵌入现有工程。理解这份代码时通常还要结合硬件电路设计中的电源稳定、传感器接入、抗干扰和 EMC 等要点,可同步建立软件与硬件的整体认知。目前已有 2251 人浏览学习,对正在选型或调试 hlw8112 的开发者具有直接参考价值。
1. 看到 hlw8112 这个编号,就知道要写的是计量程序
看到 hlw8112 这个编号,做嵌入式的人一般会先想到上海贝岭那颗单相电能计量芯片。它在智能插座、充电桩、导轨表、电力监控里出现得非常多,负责量测电压、电流、有功功率和电能,再通过 SPI 或 UART 送给主控。"hlw8112_程序"这种命名在芯片厂 SDK 和个人工程里都很常见,核心就是驱动程序和芯片型号一一对应。
很多人拿到芯片第一件事是找现成例程,但实际项目里卡住的从来不是代码行数,而是初始化时序、数据位宽和校准系数。难怪同一个芯片,有人一天调通,有人折腾一周。这篇按我自己做电力采集项目的习惯来写,从选接口、配寄存器、读数据到校准确认,一条线走完。适合正在做计量产品、或者想把这颗芯片评测自己做掉的工程师。
2. HLW8112 的接口选型与寄存器架构,先看手册里的这几项
2.1 UART 和 SPI,HLW8112 的两种通信接口怎么选
HLW8112 同时开出 UART 和 SPI 两组通信脚,引脚上是复用的。具体激活哪一路,通常由芯片外部引脚电平或上电时序决定,不同封装和批次会略有差异,拿到样片后先按手册确认。这样设计的好处是同一颗芯片既能进低成本仪表,也能进高速采集板。
从项目选型角度,我一般这样判断:
主控资源紧张、计量数据量不大,用 UART。波特率常见在 4800 到 38400 之间,读取间隔几十到两百毫秒完全够用,而且只需要两根线,方便做隔离。比如后面接 Modbus 的采集器,一根 TX、一根 RX 就进去了。
数据更新率要求高,比如要做快速保护判断或实时电能质量分析,用 SPI。SPI 时钟可以跑到几兆,一次读三个数据寄存器也就几十微秒。代价是四根线,对时序要求更严,隔离成本也高。还有一点要提醒:很多主控的 SPI 引脚已经被 LCD 或者 flash 占用,这时候宁可走 UART,也不要硬做软件模拟 SPI。软件模拟 SPI 在中断多的裸机工程里很容易断帧,排错成本远高于换接口的成本。
HLW8112 一般是单电源 3.3V 供电,采样端是差分输入。无论选哪种接口,电平都必须和主控对齐,5V 主控要加电平转换。接口方案没定清楚就写驱动,后面一定回改。
2.2 寄存器映射与 HLW8112 的数据格式
HLW8112 的手册里寄存器分两块:配置寄存器和数据寄存器。配置寄存器管 ADC 的量程、通道开关、功率更新时间、滤波方式、中断屏蔽;数据寄存器保存电压有效值、电流有效值、有功功率、电能计数。编写程序时关心的是以下几类,我把它整理成一张表,具体地址以你手里的手册为准,写代码前务必逐项回填核对:
| 寄存器分组 | 典型名称 | 主要作用 | 注意点 |
|---|---|---|---|
| 系统配置 | SYSCFG / STATUS | 软复位、晶振状态、通信模式 | 上电后先读状态,确认 UART/SPI 模式已切对 |
| ADC 配置 | ADCCFG | 通道增益、更新速率 | 功率更新时间决定数据刷新率,影响主循环节拍 |
| 电压数据 | VRMS | 电压有效值原始码 | 无符号数,具体位数看手册 |
| 电流数据 | IRMS | 电流有效值原始码 | 小电流时低几位会跳,不能直接拿来显示 |
| 功率数据 | P_ACT | 有功功率原始码 | 有符号数,注意符号扩展 |
| 电能数据 | ENERGY | 电能脉冲计数 | 读后是否自动清零必须确认 |
数据寄存器一般是 24 位,也有部分寄存器是 16 位,必须按手册给的位宽取数。这里有个常见坑:直接读回来的值不能当有效值用,要先按位宽做符号扩展,再乘换算系数。芯片寄存器里存的是原始码,物理量等于原始码乘以换算系数。换算系数手册会给一个典型值,但实际由于采样电阻、分压电阻的精度差异,必须做标定,这个放到第五章细说。
2.3 功率更新时间与读取节拍
HLW8112 的 ADC 会按固定周期刷新数据,刷新率由配置寄存器里的字段决定,常见选项在 100ms 到 1s 之间。读取程序最好和数据更新周期错开:先读状态寄存器里的数据更新标志位,置位之后再读取数据寄存器。如果找不到这个标志位,就用时间片,比如配置成 200ms 更新一次,主循环每 50ms 读一次,取最近一次完整数据。
很多新手上来就连续读,会读到新旧混拼的数据:电压是一个采样窗口的、电流是另一个窗口的。瞬时功率还好,电能累加值可能出现跳变。处理很简单,把读取函数做成一段完整操作,读到的数据先存 buffer 再统一解析,避免读一半被中断打断。
如果走 UART,HLW8112 的通信协议一般是命令帧加响应帧结构,主控发一条读取命令,芯片回固定长度数据帧,帧里通常带校验字节。这和我们写 Modbus 单片机帧接收数据程序的思路一致:先收完一整帧再解析,不要收到一个字节就处理一个字节。UART 模式下芯片是从机,不会主动上报,主循环里必须周期查询,不能指望芯片中断通知你。
3. HLW8112 驱动初始化与数据读取的代码实现
3.1 初始化 HLW8112 的最小 C 代码
写驱动前先定硬件连接。以 STM32 加 SPI 为例,PB12 做片选 CS,PB13 接 SCLK,PB14 接 MISO,PB15 接 MOSI。HLW8112 的 SPI 操作是主控发命令、芯片回数据,一条命令同时完成写和读。先写初始化函数:
#include "hlw8112.h" /* 寄存器地址占位:以你的芯片手册为准 */ #define HLW8112_REG_SYSCFG 0x01 #define HLW8112_REG_ADCCFG 0x02 #define HLW8112_REG_I_RMS 0x10 #define HLW8112_REG_V_RMS 0x11 #define HLW8112_REG_P_ACT 0x12 #define HLW8112_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET) #define HLW8112_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET) static uint8_t hlw8112_readwrite(uint8_t byte) { uint8_t rx = 0; HAL_SPI_TransmitReceive(&hspi1, &byte, &rx, 1, 10); return rx; } void HLW8112_Init(void) { /* 拉低片选,进入命令模式 */ HLW8112_CS_LOW(); hlw8112_readwrite(HLW8112_REG_SYSCFG); hlw8112_readwrite(0x00); /* 软复位 */ HLW8112_CS_HIGH(); HAL_Delay(10); /* 等内部上电稳定 */ /* 配置 ADC:更新速率、通道使能、增益 */ HLW8112_CS_LOW(); hlw8112_readwrite(HLW8112_REG_ADCCFG); hlw8112_readwrite(0x18); /* 200ms 功率更新,不启用高通滤波 */ HLW8112_CS_HIGH(); }逻辑说明:HLW8112 的 SPI 时序就是先发寄存器地址,再发数据或读数据。代码把写寄存器和读寄存器统一成hlw8112_readwrite,CS 低有效,每次操作拉低片选,完成后拉高。HLW8112_REG_ADCCFG后面的0x18是配置值,每一位的含义看手册,这里给的是可运行的结构。不同批次芯片可能不一样,拿到样片后先用逻辑分析仪抓一遍 SPI 波形,确认时钟极性和相位与主控匹配。
参数说明:HAL_SPI_TransmitReceive第四个参数是数据长度,这里每次传 1 字节;最后一个 10 是超时毫秒数。如果返回超时,先检查 SPI 时钟极性、分频系数和 CS 引脚配置,不要急着改寄存器值。SPI 时钟建议先从 1MHz 开始,调通后再往上提。
3.2 读取 HLW8112 电压电流功率的代码
读取同样走 SPI。先发寄存器地址,再连续读多个字节,拼成整型。24 位数据的读取写成这样:
static uint32_t HLW8112_ReadReg24(uint8_t reg) { uint8_t buf[4] = {0}; uint32_t val = 0; HLW8112_CS_LOW(); hlw8112_readwrite(reg | 0x80); /* 读命令标志位,按手册确认 */ buf[0] = hlw8112_readwrite(0x00); buf[1] = hlw8112_readwrite(0x00); buf[2] = hlw8112_readwrite(0x00); HLW8112_CS_HIGH(); val = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | buf[2]; return val; }逻辑说明:reg | 0x80是很多计量芯片通用的读命令习惯,地址最高位为 1 表示读。如果手册没写这一位,直接发地址即可。三次readwrite(0x00)是在同一个 CS 低电平窗口里取回三个字节,顺序通常是高位在前。如果读出来数据明显不对,把 buf[0] 和 buf[2] 对调再试。24 位数据里,电压和电流无符号;有功功率有符号,要先把第 23 位当符号位扩展成 int32_t。
初次调试时,不要直接接 220V。用 USB 供电的 5V 直流接在电压采样端,电流端短接。读回来的 VRMS 寄存器应该稳定在一个固定值,IRMS 接近 0,这样 SPI 通没通一下就看出来了。很多人一上来就接强电,读回全 0xFFFFFF,以为是芯片烧了,其实是时序不对或者地线没共好。
读取字节数要和寄存器定义长度严格一致。有些工程师图省事,把所有寄存器都按 24 位读,遇到 16 位寄存器会把相邻字段也带进来。宁可多写 ReadReg16、ReadReg24 两个函数,也不要一个函数通吃,后续维护时看到函数名就知道位宽,不容易错。
3.3 原始码值换算成真实电压电流
原始码换算成真实值,公式非常直白:
V_real = VRMS_code × V_gain I_real = IRMS_code × I_gain P_real = P_code × P_gain三个 gain 不是手册里的固定常数,而是跟采样电路强相关。电压通道用了 1MΩ 加 1kΩ 分压,220V 对应 ADC 输入大约几百毫伏,这时一个码值对应多少伏特,由分压网络决定。程序里我会把三个增益定义成宏,方便校准后统一修改:
#define HLW8112_V_GAIN 0.001234f /* 单码对应电压,校准后回填 */ #define HLW8112_I_GAIN 0.000098f /* 单码对应电流 */ #define HLW8112_P_GAIN 0.000987f /* 单码对应功率 */这里特意说明:不要在应用层用 VRMS × IRMS 重新算功率。HLW8112 内部算的是瞬时有功功率的积分,功率寄存器和电压电流码之间不存在简单的乘法关系。在功率因数低的负载上硬算,误差能到 5% 以上。直接信任芯片的功率寄存器结果。
提示:调试 HLW8112 时,电压采样端务必先用调压器或隔离变压器供电,不要直接碰强电。先用直流小信号验证程序逻辑,再接 220V 交流。
4. 把 HLW8112 程序接入 STM32 工程:Modbus 上报与 LCD1602 显示
4.1 硬件连接与引脚分配
实际项目里,HLW8112 通常和主控在同一个板上,之间走短走线。以最小系统为例,引脚分配如下:
| HLW8112 引脚 | STM32 引脚 | 说明 |
|---|---|---|
| CS | PB12 | 片选,必须由 GPIO 控制 |
| SCLK | PB13 | SPI 时钟,复用为 SPI1_SCK |
| MISO | PB14 | 芯片输出,接主控输入 |
| MOSI | PB15 | 主控输出,接芯片输入 |
| VDD | 3.3V | 供电,就近放 0.1uF 去耦电容 |
| GND | GND | 和主控共地 |
如果 HLW8112 和主控距离超过 10cm,或者中间穿过开关电源区域,SPI 时钟速率不要超过 2Mbps,信号线上串一个 33Ω 电阻,底下铺完整地铜皮。UART 模式对走线没那么敏感,这也是很多人把计量芯片放在强电一侧、用 UART 跨接的原因。跨接时通信线要加数字隔离器,不要用光耦随便搭,计量数据的连续性和稳定性会被光耦的温漂影响。
4.2 定时读取与 Modbus 上报
采集项目里最常见的上报方式是 Modbus RTU。CT 采到的电流经过 HLW8112 算好之后,主控把电压、电流、功率、电能更新到一组寄存器,上位机轮询即可。
核心读取代码是这样:
void HLW8112_Update(uint8_t interval_ms) { static uint32_t last_tick = 0; if (HAL_GetTick() - last_tick < interval_ms) { return; } last_tick = HAL_GetTick(); /* 先读状态,确认数据更新完成 */ uint8_t status = HLW8112_ReadReg8(HLW8112_REG_STATUS); if (!(status & 0x01)) { return; /* 数据尚未更新,保留上一帧 */ } g_vrms_code = HLW8112_ReadReg24(HLW8112_REG_V_RMS); g_irms_code = HLW8112_ReadReg24(HLW8112_REG_I_RMS); g_p_code = HLW8112_ReadReg24(HLW8112_REG_P_ACT); g_vrms = (float)g_vrms_code * HLW8112_V_GAIN; g_irms = (float)g_irms_code * HLW8112_I_GAIN; g_pwr = (float)(int32_t)g_p_code * HLW8112_P_GAIN; }逻辑说明:函数用HAL_GetTick()做时间片,interval_ms 一般传 200,和 ADC 功率更新周期错开。status 寄存器最低位置 1 表示新数据就绪,此时读到的三个寄存器同属一个采样窗口。g_p_code 是 24 位有符号数,强转 int32_t 再乘增益,不能省。否则负功率会被解释成很大的正值,在并网逆变器或者回馈负载场景里,会发现"用电量"越用越少。
Modbus 寄存器映射我做成了连续地址段,方便上位机一次读多个:
| Modbus 寄存器地址 | 内容 | 数据类型 | 缩放 |
|---|---|---|---|
| 0x0001 | 电压 | uint16 | 放大 100 倍 |
| 0x0002 | 电流 | uint16 | 放大 1000 倍 |
| 0x0003 | 有功功率 | int16 | 放大 10 倍 |
| 0x0004 | 电能累计 | uint32(跨两个寄存器) | 单位 Wh |
Modbus 帧接收程序的要点是:第一个字节不一定要对齐,帧里的长度字段和 CRC 校验才是关键。HLW8112 的计量值放进 Modbus 寄存器后,从机侧只要保证 CRC 正确,主站怎么轮询都不会错位。上报周期和采样周期解耦,HLW8112 自己 200ms 更新一次数据,Modbus 寄存器 500ms 刷一次就够,没必要每次上位机请求都去翻转 SPI 总线。这能显著降低主循环阻塞,尤其在同时跑 LCD、按键和通信任务的裸机工程里。
4.3 用 LCD1602 做本地显示与验证
项目要本地看数据,LCD1602 是和 HLW8112 搭配很多的一块屏幕。两行显示放电压、电流,第三、第四个参数用翻页处理。注意分压比例和电流互感器变比必须折算,不能直接打印 g_vrms。
我一般会在 LCD1602 上专门留一行显示状态寄存器值。排查问题时比串口打印直观:看 status 是否按规律切换,如果是,说明 ADC 还在出数据;如果一直是 0x00,说明配置没写进去,或者 CS 时序不对导致芯片复位失败。
这个思路同样适合接小程序做远程监控的系统。本地显示和远端上报用同一份数据源,只是传输通道不同。前端和后端都不要重新计算功率,直接用 HLW8112 算好的结果,省得两边数值对不上。多通信任务并存时,把 HLW8112 读取放到独立任务或定时器中断里,数据写入全局结构体,Modbus 和蓝牙只读结构体,SPI 总线不会被抢占。
5. HLW8112 的最后一道工序:精度校准与 3 个常见坑
写程序只是第一步,计量产品必须有校准环节。做法是给 HLW8112 接入已知精度的标准源,或者精度 0.2% 以上的功率计,跑一段稳定负载,读取原始码,反推增益系数。校准系数:
V_gain = 标准源电压读数 / VRMS_code I_gain = 标准源电流读数 / IRMS_code P_gain = 标准源功率读数 / P_code分别在 10%、50%、100% 负载三个点采样,三次增益相差 0.5% 以内取中间值。偏差更大时检查采样电阻温漂和 PCB 布局,不要直接平均了事。
常见坑一:空载功率不归零。小电流时 IRMS 低几位跳动是正常的,但空载功率码应该接近 0。如果空载 P 码稳定在较大值,把采样电阻两端短接,读回来的码值就是零漂,程序里做数字零点扣除。注意刷新率变化时零漂也会变,最好在校准模式里动态测一次。
常见坑二:读取被打断。HLW8112 寄存器分时更新,SPI 读三个字节中间插入其他中断,三个字节可能来自不同更新窗口。解决办法是读完一整帧后做合理性检查:电压码值与上一次相差不超过 5%,超过就丢弃重读。不要用关中断解决,否则计量任务一多,主控响应会劣化。
常见坑三:电能寄存器清零策略。电能是计费数据,掉电要保存。有些芯片读后自动清零,有些手动清零,必须确认手上这颗的行为。读后清零的主机,要在掉电前把累计值写入 EEPROM。我一般把 EEPROM 写入周期设为 1 分钟一次,同时掉电检测中断里强制再存一次,最多丢 1 分钟累计量。
最后验收有个土办法:用 1kW 电暖器做负载,连续跑两个小时,对比电表和 HLW8112 累计电能。误差稳定在固定百分比,说明是增益问题,回填系数即可。误差随时间漂移,优先查芯片供电电压和晶体,而不是采样电阻。这个方向基本能覆盖绝大多数精度异常。
本文还有配套的精品资源,点击获取