EMS 知识库
首页 / knowledge/代码解析/midware/meter

backupbox.c

源文件路径: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_lxstatic(backupbox.c:35)组 Modbus 写保持寄存器帧(功能码 0x10,起始地址 0x0020,2 个寄存器)下发三相状态,最多重试 3 次
bb_grid_connected_handlestatic(backupbox.c:84)并网状态下的单相版负载管理:按 SOC 门限与过载情况决定各相开关
bb_grid_disconnected_handlestatic(backupbox.c:231)离网状态下的单相版负载管理,逻辑同上但增加 state_lx 翻转前置条件
single_phase_bb_load_manage全局(backupbox.c:398)单相机型入口:算负载功率、做过载计数、按运行模式分派到并网/离网处理
bb_on_grid_handlestatic(backupbox.c:472)三相并网版负载管理:按每相负载功率是否超过 1/3 额定放电功率切相,并带 10 秒恢复延时
bb_off_grid_handlestatic(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 为真时:

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)与并网版的差异在两处:

  1. 一开始就比较 *cmd_last 与重新构造的 state_lx 位图,不等就置 modif = 1(backupbox.c:257),保证离网下软件记录与开关状态一致性被强制性纠正。
  2. 在 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 就不再清零,该相被永久切出。

两个三相函数都在最后按「位图有变化才下发」的规则发帧:

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_23Kmidware/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 / cUPSmidware/inverter/inverter.h:1966、:2011、:1992、:1999、:2038
bms_read_total_soc()midware/bms/bms.h:1190
GetCrcCode_Modbuscommon/crc.h
OSTimeGetUCOSII
printf(仅 BB_DEBUG_INFO == 1)C 标准库

6. 依赖关系

依赖

backupbox.h、meter.h、inverter.h、bms.h、crc.h(backupbox.c:13-backupbox.c:17)。

被依赖

7. 备注与风险点

  1. 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;。
  2. 发送结果无人检查:四处调用 change_state_bb_lx() 都不看返回值,也没有失败计数/告警。若备电盒不在线,软件仍会把 u8_cmd_last 更新为「已下发」的位图,与实际状态脱节,直到 u8_grid_state == 2 时才可能被纠正。
  3. 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,逻辑与并网版不一致。
  4. 离网过载不可恢复(严重的行为不对称):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,否则该相负载永远不会被重新投入。若这是有意设计(离网容量受限,宁可保守),建议加注释说明;否则应补上延时恢复。
  5. 三相版 u16 截断与符号:triphase_bb_load_manage() 把 s32 负载功率截成 u16(backupbox.c:685-backupbox.c:692),只对负值做了归零;若某相功率超过 65535(例如 23K 备电盒单相 8kW 不会超,但未来更大机型会),u16 会回绕导致误判为不过载。
  6. 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 走独立测量),但没有注释,容易被后人误改。
  7. 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] 门限风格不一致。
  8. 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,无功能差异,但类型不统一。
  9. prio_lx[i] == 2 || prio_lx[i] == 3 魔法值(backupbox.c:136、backupbox.c:288):优先级语义只用裸数字 2/3 表达,backupbox.h 中没有枚举或宏说明其含义。
  10. 0.8 浮点系数:l_s32_load_power > (inverter_read_rated_ac_discharge_power() * 0.8)(backupbox.c:420)在无 FPU 场景下引入软浮点运算;且在 printf 里又被转成 s16(backupbox.c:440)打印,两处口径可能不一致。
  11. 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)并即时置位。两者互不同步,若相数在运行中变化(理论上不会)会遗留脏状态。
  12. s_u16_voltage_grid_idx 之类无关:本文件里的处理函数都在 task_meter 上下文中同步执行,s_u8_overload_flag / s_delay_overload_tick 无需加锁;但若将来把这套逻辑挪到别的任务,会与外层 bb_load_manage() 产生竞态(目前无保护)。