backupbox.c
soc_offline 边沿去抖(单相版关键算法)4.4 单相并网判据(SOC 门限 + 过载)4.5 单相离网判据(更保守)4.6 三相版:按 1/3 额定功率切相4.7 Modbus 组帧与重试(change_state_bb_lx)5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
midware/meter/backupbox.c所属模块:midware/meter(备电盒 / BackupBox 负载管理) 代码行数:731
1. 文件职责
本文件实现 AlphaESS 备电盒(BackupBox,一种带分相负载投切开关的市电侧配电盒)的负载管理逻辑:按电池 SOC、分相优先级(L1/L2/L3)与各相负载功率,决定三分相负载开关的开/合,并把结果通过 Modbus 写寄存器帧下发到备电盒(地址 GRID_BACKUPBOX_ADDR = 0x01)。它是「电表通讯任务」的一个下游决策环节:midware/meter/meter.c 在 task_meter 中每读到一次 GRID 电表数据,若电表类型是 BACKUPBOX_5K/10K/23K 就调用 bb_load_manage(),后者再按逆变器相数分派到单相或三相逻辑。因此本文件处在「电表数据 → 负载投切决策 → Modbus 写帧」这条链路的中间,只读 g_t_meter 的运行数据与配置,输出通过 meter_write_data() 直接落到串口。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 |
|---|---|---|
BB_DEBUG_INFO | 宏 | 0(backupbox.h:20),置 1 打开各状态迁移点的 printf 调试输出 |
BACKUP_BOX_CONFIG_T | 结构体(定义于 backupbox.h:23) | 备电盒配置:u8_enable(是否启用备电盒)、u8_prio_lx[3](L1/L2/L3 优先级,2/3 视为可切负荷)、u8_soc_lx[3](各相 SOC 门限)、u32_sn(序列号)、u16_ver(版本) |
BACKUP_BOX_RUN_T | 结构体(定义于 backupbox.h:33) | 备电盒运行状态:u8_state_lx[3](各相开关实际状态)、u8_soc_offline(因 SOC 被切出的相位位图,bit0=L1…)、u8_soc_offline_last(上一次位图,用于边沿去抖)、u8_cmd_last(上次下发的三相命令位图)、u8_grid_state(电网状态,2 表示需用 u8_state_lx 覆盖,否则沿用 u8_cmd_last)、u8_overload_flag[3]、s8_overload_cnt[3]、s16_power_lx[3]、u8_bb_running_state |
GRID_BACKUPBOX_ADDR | 宏 | 0x01(meter.h:26),备电盒的 Modbus 从站地址 |
BACKUPBOX_5K / BACKUPBOX_10K / BACKUPBOX_23K | 枚举值 | METER_TYPE_E 中的备电盒型号(meter.h:148 起),本文件用它们判定是否走备电盒逻辑 |
LX_OFF / LX_ON | 局部 const | 值 0 / 1,表示某相负载开关的关/开(backupbox.c:87-backupbox.c:88 等) |
s_u8_overload_flag[3] | static 全局变量 | 各相过载锁存标志(backupbox.c:22)。置 1 后该相被切出且不再按 SOC 逻辑动作 |
s_delay_overload_tick[3] | static 全局变量 | 各相过载/恢复计时基准 tick(backupbox.c:23) |
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
change_state_bb_lx | static(backupbox.c:35) | 组 Modbus 写保持寄存器帧(功能码 0x10,起始地址 0x0020,2 个寄存器)下发三相状态,最多重试 3 次 |
bb_grid_connected_handle | static(backupbox.c:84) | 并网状态下的单相版负载管理:按 SOC 门限与过载情况决定各相开关 |
bb_grid_disconnected_handle | static(backupbox.c:231) | 离网状态下的单相版负载管理,逻辑同上但增加 state_lx 翻转前置条件 |
single_phase_bb_load_manage | 全局(backupbox.c:398) | 单相机型入口:算负载功率、做过载计数、按运行模式分派到并网/离网处理 |
bb_on_grid_handle | static(backupbox.c:472) | 三相并网版负载管理:按每相负载功率是否超过 1/3 额定放电功率切相,并带 10 秒恢复延时 |
bb_off_grid_handle | static(backupbox.c:567) | 三相离网版负载管理:同样按 1/3 额定功率切相,但无恢复延时 |
triphase_bb_load_manage | 全局(backupbox.c:656) | 三相机型入口:逐相算负载功率、按运行模式分派到并网/离网处理 |
bb_load_manage | 全局(backupbox.c:721) | 总入口:按 inverter_get_phase_number() 分派单相/三相实现 |
4. 核心逻辑解析
4.1 三层调用结构
meter.c task_meter (每轮读 GRID 电表数据后)
└─ bb_load_manage() // 仅当电表类型为 BACKUPBOX_5K/10K/23K
├─ inverter_get_phase_number() == 1 → single_phase_bb_load_manage() // 单相机
│ ├─ inverter_read_run_mode() == cNormalMode → bb_grid_connected_handle()
│ └─ inverter_read_run_mode() == cUPS → bb_grid_disconnected_handle()
└─ 否则 → triphase_bb_load_manage() // 三相机
├─ cNormalMode → bb_on_grid_handle()
└─ cUPS → bb_off_grid_handle()
四种处理函数(bb_grid_connected_handle / bb_grid_disconnected_handle / bb_on_grid_handle / bb_off_grid_handle)是「单相/三相 × 并网/离网」的两两组合,彼此逻辑高度重复但判据不同:单相版用电池 SOC 门限切相并带 soc_offline 位图边沿去抖,三相版用每相负载功率 vs 额定放电功率/3 切相并带过载锁存。
4.2 状态位图约定
所有处理函数都用一个 3 元组 state_lx[3] 表示三相开关,并压缩成 3 bit 位图写入 u8_cmd_last:
u8_cmd_last = (state_lx[2] << 2) | (state_lx[1] << 1) | state_lx[0]
位图与 soc_offline 位图的 bit 序号语义一致(bit0 → L1,bit1 → L2,bit2 → L3)。数组下标与 bit 序号是反序的(state_lx[2] 对应 bit2),这是全文件的一贯约定。
state_lx 的初值由 g_t_meter.t_bb_info.u8_grid_state 决定(backupbox.c:100、backupbox.c:247、backupbox.c:486、backupbox.c:581):
if (u8_grid_state != 2) state_lx[i] = (u8_cmd_last >> i) & 0x01; // 沿用上次命令
else state_lx[i] = onoff_lx[i]; // 用实际开关状态刷新
这意味着 u8_grid_state == 2 时上位/备电盒的实际开关状态被视为权威,其他情况下以软件记录的 u8_cmd_last 为准,避免通讯抖动导致的反复投切。
4.3 soc_offline 边沿去抖(单相版关键算法)
bb_grid_connected_handle 与 bb_grid_disconnected_handle 中都有一段相同的 3 相循环(backupbox.c:110-backupbox.c:123、backupbox.c:262-backupbox.c:275):
if ( ((*soc_offline & (1<<i)) && !(*soc_offline_last & (1<<i)) && state_lx[i] == LX_ON) ||
((!(*soc_offline & (1<<i)) && (*soc_offline_last & (1<<i)) && state_lx[i] == LX_OFF)) )
{
*soc_offline &= ~(1<<i);
*soc_offline |= *soc_offline_last & (1<<i); // 回滚本次变化
}
else
{
*soc_offline_last &= ~(1<<i);
*soc_offline_last |= *soc_offline & (1<<i); // 接受本次变化
}
含义是:如果 soc_offline 相对 soc_offline_last 发生了变化,但这个变化方向与当前实际开关状态 state_lx[i] 矛盾(本该关却显示开、本该开却显示关),就把 soc_offline 回滚到上一次的值,不认可这次变化。只有变化与实际开关状态一致时才把 last 更新为当前值。相当于用「软件期望位图」与「实际开关状态」互相校验,滤掉单次通讯/采样毛刺。
4.4 单相并网判据(SOC 门限 + 过载)
bb_grid_connected_handle(backupbox.c:84)在 using_backupbox 为真时:
- 过载分支(
overload_flag == TRUE,backupbox.c:132):遍历三相,凡priority_lx[i] == 2 || priority_lx[i] == 3(即该相被标为可切负荷)的一律LX_OFF并从soc_offline位图中清除该位。 - 正常分支(
backupbox.c:151):soc_lx[i] > soc_bat→ 该相电池 SOC 低于本相门限,断开 L(i),soc_offline |= (1<<i)。soc_lx[i] + 10 < soc_bat→ SOC 已高出本相门限 10 个百分点,合上 L(i),soc_offline &= ~(1<<i)。- 中间 10 个百分点的死区保持不变,避免在门限附近抖动。
using_backupbox == FALSE(backupbox.c:182)时无条件把三相全开、soc_offline = 0——即用户没装备电盒时所有负载必须常供。
过载标志来自 single_phase_bb_load_manage()(backupbox.c:420-backupbox.c:437):当 l_s32_load_power > 额定放电功率 * 0.8 时 s8_overload_cnt[0]++,累计到 3 次置 u8_overload_flag[0] = TRUE;否则每轮 --,降到 0 时清标志。这里 soc_lx 用的是 bms_read_total_soc()/10(把 0.1% 单位换算成 %)。
单相机型的负载功率计算:
if (cNormalMode) load = inverter_read_inv_actpower(PHASE_ALL)
+ meter_read_actpower(GRID_METER, PHASE_T)
+ meter_read_actpower(PV_METER, PHASE_T);
else if (cUPS) load = inverter_read_inv_actpower(PHASE_ALL);
即并网时负载 = 逆变器输出 + 电网输入 + PV 输入;离网时只看逆变器输出。
当运行模式既非 cNormalMode 也非 cUPS(例如待机/故障,backupbox.c:413)时,不做任何负载决策,而是把 soc_offline/soc_offline_last 复位为 0x07、u8_cmd_last 复位为 0x0F,即「软件期望三相全开」,把实际投切交给硬件/默认状态。
4.5 单相离网判据(更保守)
bb_grid_disconnected_handle(backupbox.c:231)与并网版的差异在两处:
- 一开始就比较
*cmd_last与重新构造的state_lx位图,不等就置modif = 1(backupbox.c:257),保证离网下软件记录与开关状态一致性被强制性纠正。 - 在 SOC 判据里各加了一层
if (state_lx[i] == LX_ON)/if (state_lx[i] == LX_OFF)的前置条件(backupbox.c:306、backupbox.c:320):只有开关当前处于相反状态时才需要动作。且合上负载时还额外要求(*soc_offline) & (1<<i)为真(backupbox.c:323),即必须是「先因 SOC 被切出过」的相位才允许恢复——离网下电源容量有限,不会主动投入从未投过的负载。
4.6 三相版:按 1/3 额定功率切相
三相版调用的 bb_on_grid_handle(backupbox.c:472)与 bb_off_grid_handle(backupbox.c:567)共用同一门限:
l_u16_triphase_over_power = inverter_read_rated_ac_discharge_power() / 3;
即每相负载超过逆变器额定放电功率的三分之一就认为过载。
并网版 bb_on_grid_handle(三相,backupbox.c:496-backupbox.c:533):
if (power_load[i] > triphase_over_power) // 过载 → 切出该相
{
s_delay_overload_tick[i] = OSTimeGet();
state_lx[i] = LX_OFF; s_u8_overload_flag[i] = 1; modif = 1;
}
else if (power_load[i] < triphase_over_power) // 未过载
{
if (abs(OSTimeGet() - s_delay_overload_tick[i]) >= 1000*10) // 10 秒恢复延时
{
s_delay_overload_tick[i] = OSTimeGet();
state_lx[i] = LX_ON; s_u8_overload_flag[i] = 0; modif = 1;
}
}
if (s_u8_overload_flag[i] != 1) // 非过载锁存时才按 SOC 处理
{
if (soc_lx[i] > soc_bat && state_lx[i] == LX_ON) state_lx[i] = LX_OFF, modif = 1;
else if (soc_lx[i] + 10 < soc_bat && state_lx[i] == LX_OFF) state_lx[i] = LX_ON, modif = 1;
}
10 秒恢复延时的注释说明了动机:「增加延时判断,防止在切入离网时由于延时导致再次切入负载」。abs() 保证 tick 回绕时也成立。
离网版 bb_off_grid_handle(三相,backupbox.c:591-backupbox.c:625):只有「过载 → 切出」和「SOC 判据」,没有延迟恢复分支:
if (power_load[i] > triphase_over_power && state_lx[i] == LX_ON)
{
s_delay_overload_tick[i] = OSTimeGet();
state_lx[i] = LX_OFF; s_u8_overload_flag[i] = 1; modif = 1;
}
else
{
if (s_u8_overload_flag[i] != 1) { /* SOC 判据,同并网版 */ }
else { s_delay_overload_tick[i] = OSTimeGet(); } // 仅刷新 tick
}
即离网下过载标志一旦置 1 就不再清零,该相被永久切出。
两个三相函数都在最后按「位图有变化才下发」的规则发帧:
bb_on_grid_handle(backupbox.c:551):if (modif) { if (*cmd_last != new) { *cmd_last = new; change_state_bb_lx(...); } }bb_off_grid_handle(backupbox.c:643):if (modif) { *cmd_last = new; change_state_bb_lx(...); }(少了相等比较,见备注)
4.7 Modbus 组帧与重试(change_state_bb_lx)
change_state_bb_lx(u8 state_l3, u8 state_l2, u8 state_l1)(backupbox.c:35)负责把三相状态写成 Modbus RTU 写多个保持寄存器帧:
帧格式(13 字节):
[0] 从站地址 = GRID_BACKUPBOX_ADDR = 0x01
[1] 功能码 = 0x10(Write Multiple Registers)
[2] 起始地址高 = 0x00
[3] 起始地址低 = 0x20
[4] 寄存器个数高 = 0x00
[5] 寄存器个数低 = 0x02
[6] 字节数 = 0x04
[7] 数据 = 0x00(保留)
[8] 数据 = state_l3
[9] 数据 = state_l2
[10] 数据 = state_l1
[11] CRC 低字节 = (u8)GetCrcCode_Modbus(cmd, 11)
[12] CRC 高字节 = (u8)(crc >> 8)
注意 CRC 覆盖长度是 11(从站地址到第 10 字节),CRC 本身按低字节在前排列(backupbox.c:48-backupbox.c:49),这是标准 Modbus RTU 顺序。
发送与校验(backupbox.c:60-backupbox.c:72):
for (int i = 0; i < 3; i++)
{
meter_write_data(l_u8_bb_cmd, 13);
meter_recv_data(g_u8_recv_buf, &l_u16_rx_num);
if ((memcmp(l_u8_state_ok, g_u8_recv_buf, 8) == 0) && (l_u16_rx_num == 8))
{
l_u8_res = TRUE;
break;
}
}
期望应答是固定的 8 字节常量:
const u8 l_u8_state_ok[8] = {GRID_BACKUPBOX_ADDR, 0x10, 0x00, 0x20, 0x00, 0x02, 0x40, 0x02};
即 0x01 0x10 0x00 0x20 0x00 0x02 0x40 0x02。这里用 memcmp 把含 CRC 的 8 字节整体做常量比较,等价于「地址 + 功能码 + 起始地址 + 寄存器数 + CRC」完全匹配才算成功——比逐字节比较更严格,但也意味着任何 CRC 算法细节变化(例如表驱动 CRC 与位运算 CRC 结果不一致)都会导致应答永远匹配不上。
重试上限 3 次,meter_write_data() / meter_recv_data() 内部自带串口超时(midware/meter/meter.c:51、meter.c:66),因此最坏耗时约为 3 个「发送 + 接收超时」周期。返回值 l_u8_res 未初始化(见备注 1),且调用方 bb_grid_connected_handle / bb_grid_disconnected_handle / bb_on_grid_handle / bb_off_grid_handle 完全没有检查返回值,属于「发出去就不管」的写法。
5. 对外接口
本文件导出(供外部调用)
| 名称 | 类型 | 说明 |
|---|---|---|
bb_load_manage() | 全局函数(backupbox.h:47 声明) | 唯一对外入口。由 midware/meter/meter.c:5432 在 task_meter 中调用,前置条件是 GRID 电表类型为 BACKUPBOX_5K/10K/23K |
single_phase_bb_load_manage() | 全局函数(backupbox.c:398,未在头文件声明) | 仅供 bb_load_manage() 调用(backupbox.c:725) |
triphase_bb_load_manage() | 全局函数(backupbox.c:656,未在头文件声明) | 仅供 bb_load_manage() 调用(backupbox.c:729) |
本文件调用的外部函数/变量
| 名称 | 来源模块 |
|---|---|
meter_write_data(u8*, u16) / meter_recv_data(u8*, u16*) | midware/meter/meter.h:457-meter.h:458(实现 meter.c:51、meter.c:66) |
meter_get_meter_type_now(u8 site) | midware/meter/meter.h:480 |
meter_read_actpower(u8 site, u8 phase) | midware/meter/meter.h:471 |
g_u8_recv_buf[METER_RECV_BUFFER_SIZE] | midware/meter/meter.h:451(METER_RECV_BUFFER_SIZE = 256) |
g_t_meter.t_config_data.t_bb(BACKUP_BOX_CONFIG_T)、g_t_meter.t_bb_info(BACKUP_BOX_RUN_T) | midware/meter/meter.h:397、meter.h:443 |
GRID_METER / PV_METER / PHASE_A / PHASE_B / PHASE_C / PHASE_T / PHASE_L1 / PHASE_L2 / PHASE_L3 / PHASE_ALL / GRID_BACKUPBOX_ADDR / BACKUPBOX_5K / BACKUPBOX_10K / BACKUPBOX_23K | midware/meter/meter.h |
inverter_get_phase_number / inverter_read_run_mode / inverter_read_inv_actpower / inverter_read_backup_actpower / inverter_read_rated_ac_discharge_power / cNormalMode / cUPS | midware/inverter/inverter.h:1966、:2011、:1992、:1999、:2038 |
bms_read_total_soc() | midware/bms/bms.h:1190 |
GetCrcCode_Modbus | common/crc.h |
OSTimeGet | UCOSII |
printf(仅 BB_DEBUG_INFO == 1) | C 标准库 |
6. 依赖关系
依赖
backupbox.h、meter.h、inverter.h、bms.h、crc.h(backupbox.c:13-backupbox.c:17)。
被依赖
midware/meter/meter.c:5432—task_meter中读 GRID 电表数据后调用bb_load_manage()midware/meter/meter.h:443— 引用BACKUP_BOX_RUN_T作为METER_TABLE_T.t_bb_info的类型- 间接:
hmi/菜单与net/ems.c上报会读取g_t_meter.t_bb_info展示备电盒状态
7. 备注与风险点
change_state_bb_lx()的返回值可能未初始化(严重):u8 l_u8_res;(backupbox.c:41)没有初值,只有「电表类型不是 BACKUPBOX_*」这一分支才显式赋FALSE(backupbox.c:57)。当类型确实是备电盒时,若 3 次重试全部失败,函数返回的是栈上未定义值。虽然当前所有调用者都忽略返回值(backupbox.c:207、backupbox.c:375、backupbox.c:556、backupbox.c:646),但一旦后续有人加上返回值判断就会引入随机行为。建议改为u8 l_u8_res = FALSE;。- 发送结果无人检查:四处调用
change_state_bb_lx()都不看返回值,也没有失败计数/告警。若备电盒不在线,软件仍会把u8_cmd_last更新为「已下发」的位图,与实际状态脱节,直到u8_grid_state == 2时才可能被纠正。 bb_off_grid_handle()会无条件重发帧:if (l_u8_modif) { *cmd_last = ...; change_state_bb_lx(...); }(backupbox.c:643)缺少与bb_on_grid_handle相同的if (*cmd_last != new)比较,只要l_u8_modif为真就会发送一次 Modbus 帧,即使位图没有变化。另外该函数也没有直接return,而是在三相循环外调用change_state_bb_lx,逻辑与并网版不一致。- 离网过载不可恢复(严重的行为不对称):
bb_off_grid_handle()中一旦s_u8_overload_flag[i] = 1,就再也没有把它清 0 的分支(backupbox.c:605-backupbox.c:622的else只刷新s_delay_overload_tick[i])。并网版有 10 秒延时恢复路径(backupbox.c:508-backupbox.c:517),离网版没有。结果是:离网期间某相因为一次瞬时过载被切出后,除非运行模式切回并网或进cNormalMode,否则该相负载永远不会被重新投入。若这是有意设计(离网容量受限,宁可保守),建议加注释说明;否则应补上延时恢复。 - 三相版
u16截断与符号:triphase_bb_load_manage()把s32负载功率截成u16(backupbox.c:685-backupbox.c:692),只对负值做了归零;若某相功率超过 65535(例如 23K 备电盒单相 8kW 不会超,但未来更大机型会),u16会回绕导致误判为不过载。 l_s32_powerload非镜像负载算法:三相版并网时power_load[i] = inverter_read_inv_actpower(PHASE_Lx) + meter_read_actpower(GRID_METER, PHASE_x)(backupbox.c:666-backupbox.c:668)——把逆变器分相输出直接当负载,与单相版「逆变器 + 电网 + PV」的叠加口径不同(少了 PV 侧)。这可能是刻意的(三相备电盒场景 PV 走独立测量),但没有注释,容易被后人误改。soc_lx[i] + 10 < soc_bat的 10 个百分点死区是硬编码(backupbox.c:164、backupbox.c:318、backupbox.c:526、backupbox.c:612),与backupbox.h中可配置的u8_soc_lx[3]门限风格不一致。state_lx类型不一致:单相版用u16 state_lx[3](backupbox.c:91、backupbox.c:238),三相版用u8 l_u8_state_lx[3](backupbox.c:478、backupbox.c:573)。取值只有 0/1,无功能差异,但类型不统一。prio_lx[i] == 2 || prio_lx[i] == 3魔法值(backupbox.c:136、backupbox.c:288):优先级语义只用裸数字 2/3 表达,backupbox.h中没有枚举或宏说明其含义。0.8浮点系数:l_s32_load_power > (inverter_read_rated_ac_discharge_power() * 0.8)(backupbox.c:420)在无 FPU 场景下引入软浮点运算;且在printf里又被转成s16(backupbox.c:440)打印,两处口径可能不一致。s8_overload_flag与s_u8_overload_flag是两套独立机制:单相版用g_t_meter.t_bb_info.u8_overload_flag[0]+ 三次计数(backupbox.c:420-backupbox.c:437),三相版用文件级 static 数组s_u8_overload_flag[3](backupbox.c:22)并即时置位。两者互不同步,若相数在运行中变化(理论上不会)会遗留脏状态。s_u16_voltage_grid_idx之类无关:本文件里的处理函数都在task_meter上下文中同步执行,s_u8_overload_flag/s_delay_overload_tick无需加锁;但若将来把这套逻辑挪到别的任务,会与外层bb_load_manage()产生竞态(目前无保护)。