电池状态机设计——储能系统的“大脑“状态管理
一、为什么储能系统必须要有状态机
我在做储能项目的早期,曾经见过一段"面条式"的充放电控制代码。逻辑大概是这样的:
// 反面教材,请勿模仿
void BatteryControl(void){
if (bms_ok && soc < 90 && !charge_forbidden && power > 0){
start_charging();
} else if (soc >= 90 || charge_forbidden){
stop_charging();
}
if (!charge_ok && soc > 20 && discharge_power > 0){
start_discharging();
}
// ... 还有几百行类似的逻辑
}
这种写法在实验室能跑通,但一到现场就开始出问题:充放电同时触发、BMS告警时继续充电没有停、停充之后立刻又重启……所有问题的根源只有一个——代码没有"现在系统处于什么阶段"的概念,所有判断都是无状态的瞬时快照,逻辑之间相互踩踏。
状态机解决的正是这个问题。它给系统一个"自我认知":我现在在充电,所以放电的条件我根本不去看;我现在在等待PCS响应,所以不要重复发指令……这种显式的状态管理,是工业控制软件从玩具走向生产的分水岭。
我们项目的状态机代码在 st_batt_statemachine.c 里,它管理的是储能系统中每一组电池单元的运行状态。让我从头到尾把它拆开来讲。
二、六状态全景图:比教科书更复杂的现实
标准的充放电状态机教科书只给三个状态:空闲、充电、放电。而我们的实际工程里跑的是六个:
STBATT_STATE_IDLE → 空闲
STBATT_STATE_WAIT_CHARGE → 等待充电确认
STBATT_STATE_IN_CHARGING → 充电中
STBATT_STATE_IN_DISCHARGING→ 放电中
STBATT_STATE_FAULTED → 故障
STBATT_STATE_OFFGRID → 离网(孤岛运行)
为什么要多出 WAIT_CHARGE 和 OFFGRID 这两个状态?这背后是两个非常具体的工程问题。
关于 WAIT_CHARGE: 当EMS发出充电启动指令(GRP_ACDC_RECTIFY_START)给PCS变流器后,PCS需要时间完成母线预充、继电器吸合、PWM调制启动等一系列硬件动作,这个过程在我们的现场大约需要5到30秒不等。在这段时间内,电池电流还没有真正流动,但如果系统还待在IDLE状态,它可能会
因为"充电还没开始"而再次触发启动逻辑,造成重复发指令、PCS报通信异常。WAIT_CHARGE 就是这个"等待PCS响应"的中间缓冲状态,系统进入它之后,对外屏蔽所有新的启动触发,只等一件事:PCS电流是否真的上来了。
关于 OFFGRID: 这是电网侧断开、系统转为孤岛运行时的专属状态,下面会有专门一节详细讲。
六个状态的完整转换关系可以用这张图来理解:
┌────────────────────────┐
│ │
故障告警/通信断开 故障消除
│ │
┌──── IDLE ────►FAULTED◄───────────────────┘
│ │
│ [充电条件满足]──►WAIT_CHARGE──►IN_CHARGING
│ │ │
│ [90s超时/故障] [停充条件]
│ │ │
│ └──────────────┘
│ │
│ IDLE
│
│ [放电条件满足]──────────►IN_DISCHARGING
│ │
│ [停放条件/SOC低]
│ │
└───────────────────────────────┘任意状态 + [电网断开] ──────────────►OFFGRID
OFFGRID + [电网恢复] ──────────────►IDLE
三、为何需要分层状态机(HSM),而不是扁平枚举
刚才展示的六状态图,如果直接用一个 switch-case 实现,你会发现一个让人头疼的问题:有些条件检查在每个状态下都要做。
比如,无论系统处于IDLE、WAIT_CHARGE、IN_CHARGING还是IN_DISCHARGING,只要检测到电网断开,就必须立刻跳转到OFFGRID。如果是扁平状态机,你得在每个状态函数里各写一遍:
// 扁平状态机的噩梦:每个状态都要重复写
STBATT_STATE OnIdle(...) {
if (JudgeIfNeedEnterOffGrid(...)) return STBATT_STATE_OFFGRID;
// ...
}
STBATT_STATE OnWaitCharge(...) {
if (JudgeIfNeedEnterOffGrid(...)) return STBATT_STATE_OFFGRID;
// ...
}
STBATT_STATE OnCharging(...) {
if (JudgeIfNeedEnterOffGrid(...)) return STBATT_STATE_OFFGRID;
// ...
}
// 以此类推...
看看真实代码,确实是这么写的——JudgeIfNeedEnterOffGrid 出现在第516、716、911、1032行,每个正常运行状态里各一次。这是分层状态机(Hierarchical State Machine,HSM)思想的一种手工实现。
HSM的核心思想是:把共享的转换逻辑提升到父状态层,子状态只处理自己特有的逻辑。 在我们这个工程里,IDLE/WAIT_CHARGE/CHARGING/DISCHARGING可以看作一个"正常运行"父状态的子状态,而"电网断开→OFFGRID"这条转换属于父状态层。
真正的HSM框架(如Miro Samek的QP框架)会通过事件冒泡机制自动处理这种层次结构。但在嵌入式C工程里,受限于工具链和代码习惯,我们选择了更直白的方式:在每个子状态函数里显式调用共享检查函数。这虽然有重复,但代码的可读性和可调试性反而更好——每个状态函数打开来就是一张清单,任何人都能读懂它的行为,不需要理解框架的分发机制。这是一个典型的"工程实用主义"选择:完美的架构 vs 可维护的实现,在工业软件里,后者往往胜出。
四、OnCharging() 的多重守卫:一道精密的安全过滤器
OnCharging 是整个状态机里逻辑最复杂、安全等级最高的函数。它做的最核心的一件事,是决定"充电状态下,什么时候应该停止充电"。
这个停止条件不是一个简单的 if,而是一个由七个条件组成的 OR 门,只要其中任何一个成立,就立刻停充:
if (pSingleRunInfo->bIsBatt_ChargeForbidden || // ① BMS下发禁充标志
IsBatt_Plan2_StopCharge(pSTBatt_Rough, nBattIdx) || // ② EMS计划停充(SOC到达上限)
(pSingleRunInfo->emBattStatus_Alm != STBATT_ALMSTAT_NOMAL) || // ③ BMS告警
(pSingleRunInfo->emBattStatus_Run <= STBATT_RUNSTAT_COMM_FAIL) || // ④ BMS通信故障
(pSTBatt_Rough->ccuStatu != CCU_STATE_IDLE) || // ⑤ PCS控制器状态异常
(pSingleRunInfo->fAllow_MaxChargeCurr < 5.0) || // ⑥ BMS允许最大充电电流过低
((pSingleRunInfo->fPackTotalVolt - pSingleRunInfo->fAllow_LmtChargeVolt) > 10) || // ⑦ 电压超限
(pSTBatt_Rough->calcNodeData[nBattIdx].emBatt_WorkMode != BATT_WORKMODE_FIRST_CHRGING) // ⑧ 工作模式切换
)
我把这八个条件逐一拆解,每一个背后都有具体的工程故事:
① bIsBatt_ChargeForbidden:BMS的最高裁决权
这个标志来自BMS(电池管理系统)通过CAN总线下发的实时指令。当电池出现单体过温、单体过压、绝缘阻值过低等危险情况时,BMS会主动置位这个标志。它是整个充放电系统里优先级最高的信号,没有任何其他逻辑可以覆盖它。
在代码层面,bIsBatt_ChargeForbidden 由CAN报文解析层(can_pl_batt.c、can_infy_batt.c等)实时更新,OnCharging 每个控制周期都读取最新值。这是一个典型的"安全第一"设计:硬件层面的保护信号,永远优先于软件逻辑。
② IsBatt_Plan2_StopCharge:EMS计划层的SOC上限控制
static BOOL IsBatt_Plan2_StopCharge(STBATT_SAMPLER_ROUGH *pSTBatt_Rough, int nBattIdx)
{
float fJudgeSOC = (float)pSingleRunInfo->nSOC + 0.05; // 留0.05%余量
if (fJudgeSOC >= ((float)pEmsCfgPlan->emsLmtSectors[n].nStopSOC))
{
return TRUE;
}
return FALSE;
}
注意这里有一个细节:SOC判断值加了0.05的偏置(fJudgeSOC = nSOC + 0.05)。这是为了处理SOC传感器的测量抖动——如果SOC恰好在停充阈值附近来回跳变,没有这个偏置的话,充电会频繁地启停,对PCS和电池都是伤害。加了0.05之后,一旦判断停充,SOC至少要降到 nStopSOC - 0.05
以下才有可能再充,形成了一个微小的滞回区间。
③④ BMS告警与通信故障:分开处理的用意
告警(emBattStatus_Alm)和通信故障(emBattStatus_Run <= STBATT_RUNSTAT_COMM_FAIL)是两类不同性质的异常:
- 告警意味着BMS还在运行,只是检测到了电池层面的问题(如温度高、电压偏差大)
- 通信故障意味着EMS和BMS之间的CAN链路断开,EMS看不到电池任何信息
这两种情况都必须停充,但原因完全不同。如果通信断开时还继续充电,EMS将失去对电池状态的感知,一旦电池真的出问题,系统将毫无反应。
⑥ fAllow_MaxChargeCurr < 5.0:BMS动态电流限制
BMS会根据当前电池的SOC、温度、老化状态,实时更新允许充电的最大电流。当这个值低于5A时,意味着电池目前只接受极小的电流,已经接近充电截止状态(通常是CV阶段末期)。继续保持PCS运行的意义不大,还会浪费设备待机功耗,不如直接停机等下一个调度周期。
⑦ 电压超限保护:最后一道硬件防线
((pSingleRunInfo->fPackTotalVolt - pSingleRunInfo->fAllow_LmtChargeVolt) > 10)
当电池包总电压超过BMS设置的限压上限10V时强制停充。这个10V的裕量不是随意取的:BMS的限压值本身已经留有安全余量,超过10V意味着BMS的软件保护可能已经失效或数据异常,需要EMS在硬件层面兜底。
⑧ 工作模式检查:防充放并行
这条守卫是2024年11月新加的(注释里有 LJH241120 fixChrgAndDisTogether):
(pSTBatt_Rough->calcNodeData[nBattIdx].emBatt_WorkMode != BATT_WORKMODE_FIRST_CHRGING)
当EMS上层决策从"充电优先"切换到"放电优先"时,这个工作模式标志会更新。通过这条检查,充电状态函数能够感知到上层决策的变化,在下一个控制周期主动退出充电——而不是等到BMS或SOC触发停止。这修复了一个真实的现场问题:在充放电交替的调度场景下,工作模式切换后PCS会短暂地同时保持充电和放电的指令,造成功率抵消和异常发热。
五、状态转换的原子性:一个容易被忽视的数据一致性问题
在多线程嵌入式系统里,状态机的状态转换不是瞬间完成的。从"当前状态函数返回新状态值"到"状态变量被更新",中间存在一个窗口期。如果在这个窗口期内,另一个线程读取了状态变量,它看到的是旧状态,但系统实际上已经在向新状态迁移了。
我们的状态机通过一种简单但有效的机制来保障原子性:
// 来自 st_batt_sampler.c 的状态机调度循环(概念简化版)
STBATT_STATEMACHINE_STATE emNextState = STBATT_STATE_IDLE;switch (pSTBatt_Rough->emCurState[nBattIdx])
{
case STBATT_STATE_IDLE:
emNextState = OnIdle(pSTBatt_Rough, nBattIdx);
break;
case STBATT_STATE_WAIT_CHARGE:
emNextState = OnWaitCharge(pSTBatt_Rough, nBattIdx);
break;
case STBATT_STATE_IN_CHARGING:
emNextState = OnCharging(pSTBatt_Rough, nBattIdx);
break;
// ...
}
// 状态转换发生在这一行,且只有这一行
pSTBatt_Rough->emCurState[nBattIdx] = emNextState;
状态转换被压缩到一次赋值操作。在C语言里,对一个枚举变量的赋值(通常是4字节整数赋值)在绝大多数平台上是原子的。只要确保没有其他线程在这条赋值的同时读取 emCurState,就不会出现状态撕裂(State Tearing)。
另一个保障来自于 Log_CtrlPcsEventStatus 函数——它在每次状态相关的PCS指令下发时,同步记录三元组(状态、指令、原因):
void Log_CtrlPcsEventStatus(...,
IN STAT_BMSMGMT_STATUS emMgmtStatus,
IN CMD_BMSMGMT_PCSCTRL emMgmtPcsCtrlCmd,
IN REASON_BMSMGMT_EXEC emMgmtExecReason)
{
pSingleRunInfo->emMgmtStatus = emMgmtStatus;
pSingleRunInfo->emMgmtPcsCtrlCmd = emMgmtPcsCtrlCmd;
pSingleRunInfo->emMgmtExecReason = emMgmtExecReason;
}
这个三元组是整个状态转换的"事务记录"——发了什么指令、为什么发、发的时候系统是什么状态,三者绑定存储。任何时刻调试或事后追溯,都能从这个三元组还原出转换现场,极大降低了问题定位的难度。
六、OFFGRID:最特殊的状态,也是最考验系统设计的状态
STBATT_STATE_OFFGRID 是整个状态机里最特殊的一个,它对应的场景是:电网侧断电或主动解列,储能系统转为孤岛模式,独立为本地负荷供电。
先看 OnOffGrid 的代码结构:
STBATT_STATEMACHINE_STATE OnOffGrid(STBATT_SAMPLER_ROUGH* pSTBatt_Rough, int nBattIdx)
{
// 第一优先:检查安全条件,不满足就紧急停机
if ((pSingleRunInfo->bIsBatt_DisChargeForbidden == TRUE) ||
(pSingleRunInfo->bIsBatt_ChargeForbidden == TRUE) ||
(pSingleRunInfo->emBattStatus_Alm != STBATT_ALMSTAT_NOMAL) ||
(pSingleRunInfo->emBattStatus_Run <= STBATT_RUNSTAT_COMM_FAIL) ||
(pSTBatt_Rough->ccuStatu != CCU_STATE_IDLE))
{
Send_Ctrl_CCU(pSTBatt_Rough, GRP_ACDC_STOP, nBattIdx);
}// 第二优先:检查是否已恢复并网
if (JudgeIfNeedEnterOffGrid(pSTBatt_Rough, nBattIdx) == FALSE)
{
return STBATT_STATE_IDLE; // 电网恢复,退出离网状态
}// 第三:SOC保护——电量耗尽时停止逆变,防止电池过放
if ((pSTBatt_Rough->blobSingleBattInfo[nBattIdx].nSOC <= pSTBatt_Rough->nProtectSOC)
&& (pSTBatt_Rough->branchGrpData[nBattIdx].emACDC_WorkStatu != STATU_ACDC_IDLE))
{
Send_Ctrl_CCU(pSTBatt_Rough, GRP_ACDC_STOP, nBattIdx);
}
else
{
// 正常离网运行:维持PCS热备待机状态
OnHandle_PCSHotWaiting(pSTBatt_Rough, nBattIdx);
}return STBATT_STATE_OFFGRID;
}
OnOffGrid 的逻辑层次非常清晰,三个优先级依次递进:
第一层:安全兜底。 即使在离网模式下,BMS的禁充禁放和告警信号依然有效,一旦触发就立刻停机。这保证了"孤岛运行"不会成为安全保护的盲区。
第二层:并网恢复检测。 JudgeIfNeedEnterOffGrid 持续检测STS(静态开关)的并离网状态位。一旦电网恢复,立刻返回IDLE,重新参与EMS的并网调度。这里有一个工程细节值得注意:离网恢复后直接回到IDLE而不是回到充电或放电状态,由IDLE的逻辑重新评估条件,做一次干净的"冷启动判断"——这避免了并网瞬间因历史状态残留而造成的功率冲击。
第三层:离网SOC保护。 当电量耗尽(SOC降至保护阈值)时,停止逆变输出。注意这里的判断条件同时要求 emACDC_WorkStatu != STATU_ACDC_IDLE——如果PCS已经停了,就不需要再发停机指令,避免无效的CAN通信。
关于黑启动(Black Start)
OnHandle_PCSHotWaiting 函数是离网状态下"热备待机"的核心实现:
static void OnHandle_PCSHotWaiting(STBATT_SAMPLER_ROUGH* pSTBatt_Rough, int nBattIdx)
{
if (JudgeIfPCS_OnHotWaiting(pSTBatt_Rough, nBattIdx) == TRUE)
{
BOOL bPCSAvaliable = ((SysStatu.Byte0_Bit0to2 == 0) ||
(SysStatu.Byte0_Bit0to2 == 4)) ? TRUE : FALSE; // 0=正常, 4=告警if ((FaultState == 0) && // PCS无故障
(emACDC_WorkStatu == STATU_ACDC_IDLE) && // PCS当前空闲
(bPCSAvaliable == TRUE) &&
(pSTBatt_Rough->bBattProt == FALSE)) // 无电池保护
{
// 发送逆变器热备指令
Send_Ctrl_CCU(pSTBatt_Rough, GRP_ACDC_INVERT_PENDING, nBattIdx);
}
}
}
GRP_ACDC_INVERT_PENDING 是一个"热备"指令——让PCS进入逆变器就绪状态但不立刻输出功率,相当于"引擎已经点火但还没挂档"。当本地负荷有需求时,PCS可以在毫秒级内从热备状态转为全功率输出,实现对负荷的无感切换。
这就是工业现场黑启动能力的软件实现基础:系统不是等电网断了再慢慢启动,而是在检测到离网信号的瞬间,就开始准备好随时承担本地供电的能力。
七、状态机测试策略:状态覆盖 vs 转换覆盖
在工程实践中,我见过两种截然不同的状态机测试思路,它们各有侧重,需要结合使用。
状态覆盖(State Coverage): 确保每个状态都被执行到。这是最基本的要求,相当于行覆盖。但它有一个严重的盲点:一个状态可以通过不同的转换路径进入,覆盖了这个状态,不代表覆盖了所有到达它的转换。
转换覆盖(Transition Coverage): 确保每条状态转换边都被执行到。这比状态覆盖更强,但在实际工程中还不够——因为一条转换可能有多个触发条件(OR关系),需要单独测试每个条件的触发。
在我们的项目里,针对 OnCharging 的八个停止条件,真正完备的测试策略应该是条件组合覆盖:
测试矩阵(部分):

另一个在工程现场特别有价值的测试手段是时间序列录制回放:
在正常运行时,把每个控制周期的状态、BMS数据、PCS状态等关键变量序列化到CSV文件(这正是 st_batt_testmode.c里的功能)。然后在测试环境里用录制的数据驱动状态机,不依赖真实硬件就能复现任意历史场景。这个方法救过我们团队好几次——客户现场报了一个偶发的异常状态转换,把他们的数据日志拿回来一回放,问题10分钟内定位。
八、AI结合:用状态转换序列预测电池健康退化
讲完工程实现,我想把视角往前延伸一步——如果用AI来观察这台状态机,我们能得到什么?
核心观点:状态转换序列本身就是电池健康状态的时序特征。
一块健康的电池,它的状态转换序列应该呈现规律的节律:
IDLE → WAIT_CHARGE → IN_CHARGING → IDLE → IN_DISCHARGING → IDLE → ...
整个充电过程持续时间稳定,充电电流调整次数少,BMS告警出现频率极低。但随着电池老化或出现潜在问题,状态转换序列会开始出现异常特征:

将这些观察系统化,可以构建一个基于LSTM的状态序列异常检测模型:
# 特征工程:把状态转换序列编码为时序特征向量
def encode_state_sequence(df):
features = []
for cycle in df.groupby('cycle_id'):
features.append({
'wait_charge_duration': cycle['WAIT_CHARGE'].sum(), # 等待充电时长
'charge_duration': cycle['IN_CHARGING'].sum(), # 充电时长
'discharge_duration': cycle['IN_DISCHARGING'].sum(), # 放电时长
'fault_count': cycle['FAULTED'].count(), # 故障次数
'charge_stop_reason_distribution': # 停充原因分布
cycle['stop_reason'].value_counts().to_dict(),
'bms_forbidden_ratio': # BMS禁充触发比例
cycle['bms_forbidden'].mean()
})
return pd.DataFrame(features)# LSTM模型:输入30天的循环特征,预测未来7天的健康状态
model = Sequential([
LSTM(64, return_sequences=True, input_shape=(30, n_features)),
LSTM(32),
Dense(1, activation='sigmoid') # 输出:异常概率
])
这个方法在理论上有几个很好的特性:
一、不依赖传感器绝对精度。 SOC估算可能有±5%的误差,但状态转换的时序特征(充电持续多少分钟、BMS触发了几次限流)是相对稳健的,误差不会累积。
二、能捕捉多变量交互的模式。 人工看日志可以发现"BMS禁充越来越频繁",但很难同时发现"BMS禁充频率与当天最高温度的相关性在升高"——这种多维度的隐含关联正是LSTM擅长的。
三、天然适合增量学习。 每个充放电循环产生新数据,模型可以定期(如每周)用新数据fine-tune,无需重新训练,适合部署在边缘端的控制器上。
当然,这套方案目前还在理论和小规模验证阶段。把它部署到实际储能系统面临的最大挑战是:标注数据稀缺。电池真正失效的案例在现场相对罕见,做监督学习的正负样本极度不均衡,需要结合无监督的异常检测(如Isolation Forest)来弥补。
九、回头看:状态机设计的本质
写完这篇文章,我意识到 st_batt_statemachine.c 这个文件教给我最重要的一件事,不是某个具体的技术细节,而是一种把不确定的世界变得可预测的思维方式。
电池会有各种奇怪的状态,PCS会在意想不到的时候抖一下,BMS的CAN帧可能迟来或乱序——这些不确定性是工业现场的常态。状态机的价值不是消灭这些不确定性,而是把系统对不确定性的响应,收敛到一个有限的、可枚举的、可测试的状态转换集合里。
任何时刻,如果系统出现异常,你只需要知道两件事:当前在哪个状态、触发了哪个转换。定位到这两件事,问题就解决了一半。
这,是状态机最本质的工程价值。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)