233 lines
8.2 KiB
C
233 lines
8.2 KiB
C
/* hlw8032.c - HLW8032 电量计量芯片驱动(UART5 只收,4800 8E1)
|
||
*
|
||
* 帧格式(24 字节,每 50ms 一帧,主动上报):
|
||
* [0] 状态寄存器 State REG 0x55=正常 / 0xAA=修调失效 / 0xFx=有寄存器溢出
|
||
* [1] 检测寄存器 Check REG 固定 0x5A
|
||
* [2:5] 电压参数寄存器(本模块不用)
|
||
* [5:8] 电压寄存器(不用)
|
||
* [8:11] 电流参数寄存器(不用)
|
||
* [11:14]电流寄存器(不用)
|
||
* [14:17]功率参数寄存器 PowerPar 芯片出厂修调常数,算电量要用
|
||
* [17:20]功率寄存器(不用)
|
||
* [20] 数据更新寄存器 bit7 = PF 溢出翻转标志
|
||
* [21:23]PF 寄存器 16bit 脉冲计数,溢出清零同时 bit7 翻转
|
||
* [23] 校验和 [2]~[22] 共 21 字节相加取低 8 位
|
||
*
|
||
* 手册电量公式:
|
||
* 1 度电脉冲数 = (1/PowerPar) × (1/(电压系数×电流系数)) × 10^9 × 3600
|
||
* 电量(kWh) = 总脉冲数 × PowerPar × 电压系数 × 电流系数 / (10^9 × 3600)
|
||
* 电压/电流系数在 main.h(HLW_U_COEF / HLW_I_COEF),由采样电阻和互感器变比决定。
|
||
*
|
||
* 溢出处理:PF 寄存器只有 16 位,溢出时 bit7 翻转。本驱动记住 bit7 上次值,
|
||
* 每帧比对,翻转一次说明溢出一次(+65536)。50ms 一帧 vs 溢出最快也要数小时,
|
||
* 不可能在一帧间隔内翻两次,不会漏。
|
||
*/
|
||
|
||
#include "hlw8032.h"
|
||
#include "uart.h"
|
||
#include "24c02.h"
|
||
#include "log.h"
|
||
#include <string.h>
|
||
|
||
#define HLW_FRAME_LEN 24
|
||
#define HLW_OVERFLOW_PULSES 65536u
|
||
#define HLW_EEP_ADDR 164 /* EEPROM 164~171: uint64 累计脉冲数(132~157 蓝牙缓存, 160 日志等级) */
|
||
|
||
static uint8_t s_frame[HLW_FRAME_LEN];
|
||
static uint8_t s_idx = 0; /* 帧同步状态:0=等首字节 1=等 0x5A >=2=收集中 */
|
||
|
||
static uint64_t s_total_pulses = 0; /* 累计脉冲(含 EEPROM 恢复的历史值) */
|
||
static uint64_t s_saved_pulses = 0; /* 上次已写入 EEPROM 的值 */
|
||
static uint32_t s_power_par = 0; /* 功率参数寄存器(芯片常数,收到帧后才有) */
|
||
static uint32_t s_u_par = 0, s_u_reg = 0; /* 电压参数/电压寄存器(显示电压用) */
|
||
static uint32_t s_i_par = 0, s_i_reg = 0; /* 电流参数/电流寄存器 */
|
||
static uint32_t s_p_reg = 0; /* 功率寄存器 */
|
||
static float s_u_f = 0, s_i_f = 0, s_p_f = 0; /* 电压/电流/功率 EMA 滤波值 */
|
||
|
||
static uint32_t s_k = 0; /* 本次上电以来 bit7 翻转(PF 溢出)次数 */
|
||
static uint8_t s_last_bit7 = 0;
|
||
static uint32_t s_last_chip = 0; /* 上一帧芯片侧总脉冲 k*65536+n */
|
||
static uint8_t s_synced = 0; /* 已确认同步(连续有效帧) */
|
||
static uint8_t s_streak = 0; /* 连续有效帧计数(防噪声假同步) */
|
||
static uint32_t s_last_valid_tick = 0; /* 上一有效帧时刻(帧间隔校验用) */
|
||
static uint32_t s_last_frame_tick = 0;
|
||
|
||
/*==== 帧校验与电量累计(中断上下文执行,保持轻量)====*/
|
||
static void hlw8032_parse(const uint8_t *f)
|
||
{
|
||
uint8_t sum = 0;
|
||
for (uint8_t i = 2; i <= 22; i++) sum += f[i];
|
||
if (sum != f[23]) return; /* 校验失败丢帧 */
|
||
|
||
uint32_t power_par = ((uint32_t)f[14] << 16) | ((uint32_t)f[15] << 8) | f[16];
|
||
if (power_par == 0) return; /* 参数寄存器异常,帧不可用 */
|
||
s_power_par = power_par;
|
||
|
||
/* 电压/电流/功率原始寄存器(调试用,每分钟日志显示) */
|
||
s_u_par = ((uint32_t)f[2] << 16) | ((uint32_t)f[3] << 8) | f[4];
|
||
s_u_reg = ((uint32_t)f[5] << 16) | ((uint32_t)f[6] << 8) | f[7];
|
||
s_i_par = ((uint32_t)f[8] << 16) | ((uint32_t)f[9] << 8) | f[10];
|
||
s_i_reg = ((uint32_t)f[11] << 16) | ((uint32_t)f[12] << 8) | f[13];
|
||
s_p_reg = ((uint32_t)f[17] << 16) | ((uint32_t)f[18] << 8) | f[19];
|
||
|
||
uint8_t bit7 = f[20] & 0x80;
|
||
uint16_t pf = ((uint16_t)f[21] << 8) | f[22];
|
||
|
||
if (!s_synced) {
|
||
/* 首帧只建立基准,不累计增量:
|
||
* 单片机重启而芯片一直在跑时,寄存器里攒的脉冲(几秒,约零点几 Wh)舍弃 */
|
||
s_last_bit7 = bit7;
|
||
s_k = 0;
|
||
} else if (bit7 != s_last_bit7) {
|
||
s_k++; /* PF 溢出一次 */
|
||
s_last_bit7 = bit7;
|
||
}
|
||
|
||
/* 防噪声:真芯片 50ms 一帧稳定到达,要求连续 3 帧且间隔 20~150ms 才算同步。
|
||
* PD2 悬空时的电磁干扰偶发也能凑出校验和正确的假帧(约 1/256),
|
||
* 但不可能连续 3 帧都卡在 50ms 节奏上 */
|
||
{
|
||
uint32_t now = HAL_GetTick();
|
||
uint32_t gap = now - s_last_valid_tick;
|
||
s_last_valid_tick = now;
|
||
if (gap >= 20 && gap <= 150) {
|
||
if (s_streak < 3) s_streak++;
|
||
} else {
|
||
s_streak = 1;
|
||
}
|
||
if (s_streak < 3) return; /* 未确认同步:帧内容一律不采用 */
|
||
}
|
||
|
||
/* EMA 滑动平均(系数 1/8,约 0.4s 响应):压空载噪声跳变,
|
||
* 真实负载变化延迟 <0.5s,不影响使用 */
|
||
{
|
||
float u = s_u_reg ? (float)((double)s_u_par / (double)s_u_reg * HLW_U_COEF) : 0.0f;
|
||
float a = s_i_reg ? (float)((double)s_i_par / (double)s_i_reg * HLW_I_COEF) : 0.0f;
|
||
float w = s_p_reg ? (float)((double)s_power_par / (double)s_p_reg * HLW_U_COEF * HLW_I_COEF) : 0.0f;
|
||
s_u_f += (u - s_u_f) * 0.125f;
|
||
s_i_f += (a - s_i_f) * 0.125f;
|
||
s_p_f += (w - s_p_f) * 0.125f;
|
||
}
|
||
|
||
uint32_t chip_total = s_k * HLW_OVERFLOW_PULSES + pf;
|
||
if (s_synced) {
|
||
/* 正常单调递增;变小说明芯片断电重启过(寄存器清零),从当前值重新累计 */
|
||
uint32_t delta = (chip_total >= s_last_chip) ? (chip_total - s_last_chip) : chip_total;
|
||
s_total_pulses += delta;
|
||
}
|
||
s_last_chip = chip_total;
|
||
if (!s_synced) {
|
||
s_synced = 1;
|
||
log_info("> 电能: HLW8032 在线, 功率参数=%lu", (unsigned long)s_power_par);
|
||
}
|
||
s_last_frame_tick = HAL_GetTick();
|
||
}
|
||
|
||
/*==== UART5 中断逐字节喂入:首字节同步 + 0x5A 二次确认 + 收满 24 字节解析 ====*/
|
||
void hlw8032_rx_byte(uint8_t b)
|
||
{
|
||
if (s_idx == 0) {
|
||
/* 首字节候选:0x55 / 0xAA / 0xFx */
|
||
if (b == 0x55 || b == 0xAA || (b & 0xF0) == 0xF0) {
|
||
s_frame[0] = b;
|
||
s_idx = 1;
|
||
}
|
||
return;
|
||
}
|
||
if (s_idx == 1) {
|
||
if (b == 0x5A) {
|
||
s_frame[1] = b;
|
||
s_idx = 2;
|
||
} else {
|
||
/* 第二字节不是 0x5A:伪同步,但它本身可能是真首字节 */
|
||
s_idx = 0;
|
||
if (b == 0x55 || b == 0xAA || (b & 0xF0) == 0xF0) {
|
||
s_frame[0] = b;
|
||
s_idx = 1;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
s_frame[s_idx++] = b;
|
||
if (s_idx >= HLW_FRAME_LEN) {
|
||
s_idx = 0;
|
||
hlw8032_parse(s_frame);
|
||
}
|
||
}
|
||
|
||
/*==== 初始化:EEPROM 恢复累计 + UART5 起收 ====*/
|
||
void hlw8032_init(void)
|
||
{
|
||
uint64_t v = 0;
|
||
eepromReadData(HLW_EEP_ADDR, &v, sizeof(v));
|
||
if (v == 0xFFFFFFFFFFFFFFFFULL) v = 0; /* 空白 EEPROM(出厂 0xFF) */
|
||
s_total_pulses = v;
|
||
s_saved_pulses = v;
|
||
|
||
MX_UART5_UART_Init();
|
||
UART5_StartRx();
|
||
if (s_total_pulses) {
|
||
log_info("> 电能: HLW8032 就绪 (UART5 PD2), 历史脉冲 %lu%09lu",
|
||
(unsigned long)(s_total_pulses / 1000000000ULL),
|
||
(unsigned long)(s_total_pulses % 1000000000ULL));
|
||
} else {
|
||
log_info("> 电能: HLW8032 就绪 (UART5 PD2)");
|
||
}
|
||
}
|
||
|
||
/*==== 累计电量 kWh ====*/
|
||
float hlw8032_get_energy_kwh(void)
|
||
{
|
||
if (!s_power_par) return 0.0f;
|
||
return (float)((double)s_total_pulses * (double)s_power_par *
|
||
(double)HLW_U_COEF * (double)HLW_I_COEF / 3.6e12);
|
||
}
|
||
|
||
/*==== 电压/电流/功率(手册:有效值 = 参数寄存器/寄存器 × 系数)====*/
|
||
float hlw8032_get_voltage_v(void)
|
||
{
|
||
if (!s_synced) return 0.0f;
|
||
return s_u_f;
|
||
}
|
||
|
||
float hlw8032_get_current_a(void)
|
||
{
|
||
if (!s_synced) return 0.0f;
|
||
float a = s_i_f - HLW_I_ZERO_A; /* 减零点偏移 */
|
||
return a > 0.0f ? a : 0.0f; /* 钳位到 0 */
|
||
}
|
||
|
||
float hlw8032_get_power_w(void)
|
||
{
|
||
if (!s_synced) return 0.0f;
|
||
if (s_p_f < HLW_P_DEAD_W) return 0.0f; /* 死区:空载噪声底显示 0 */
|
||
return s_p_f - HLW_P_ZERO_W;
|
||
}
|
||
|
||
/*==== 在线判断:1s 内收到过有效帧 ====*/
|
||
uint8_t hlw8032_online(void)
|
||
{
|
||
return s_synced && (HAL_GetTick() - s_last_frame_tick) < 1000;
|
||
}
|
||
|
||
/*==== 立即写 EEPROM(断电预警/计划复位前调用)====*/
|
||
void hlw8032_save(void)
|
||
{
|
||
if (s_total_pulses == s_saved_pulses) return;
|
||
uint64_t v = s_total_pulses;
|
||
if (eepromWriteData(HLW_EEP_ADDR, &v, sizeof(v)) == 0) {
|
||
s_saved_pulses = s_total_pulses;
|
||
}
|
||
}
|
||
|
||
/*==== 1s 周期:每攒够约 0.1 kWh 写一次 EEPROM,限制擦写频率 ====*/
|
||
void hlw8032_tick(void)
|
||
{
|
||
if (!s_power_par) return;
|
||
double per_01kwh = 3.6e11 / ((double)s_power_par * HLW_U_COEF * HLW_I_COEF);
|
||
if (per_01kwh < 1.0) per_01kwh = 1.0;
|
||
if (s_total_pulses - s_saved_pulses >= (uint64_t)per_01kwh) {
|
||
hlw8032_save();
|
||
}
|
||
}
|