储能 EMS 的功率策略:从一块电表到一次逆流的 200 毫秒

写在前面

我是薛定谔的悦,储能领域的工程师。最近系统里最让我有表达欲的,不是那些花哨的协议栈,而是一套看起来特别"土"的东西------几块电表、一个信号量、两个预保护值,拼出来的防逆流和防过载。

这套逻辑代码不多,核心就分布在 ccu_samplerkwhmeter_sampler 两个模块里,加起来可能不到两千行。但它是整个储能系统"并网安全"的底线:充电的时候不能把变压器顶爆,放电的时候不能把电倒灌回电网。

这篇文章我把这套逻辑从头理一遍。

大纲

  1. 为什么要盯着一块电表
  2. 两个模块之间的"门铃":System V 信号量
  3. 快速通道:从轮询到事件驱动
  4. 防逆流:别把电倒回电网
  5. 防过载:别把变压器顶爆
  6. 两个公式,一个思想
  7. 工程里真正让人头疼的坑
  8. 写在最后

1. 为什么要盯着一块电表

一个工商业储能的典型拓扑是这样的:变压器出来,接一路电网侧关口表(Grid Meter),然后往下分两条支路,一条接普通负载,一条接储能机柜。储能机柜自己还挂着一块柜内表(Cab Meter)。

为什么要装两块表?因为 EMS 需要实时知道"负载到底吃了多少电",而这个值没法直接测,只能靠两块表做减法:

复制代码
负载功率 = 网侧表功率 - 柜内表功率

网侧表测的是变压器进线的总功率,柜内表测的是储能这条支路的功率。两个一减,剩下的就是纯负载消耗。

这个减法公式是整个功率控制的地基。充电和放电的"跟随负载",全都是在这行减法之上算出来的。

我第一次读这块代码的时候,其实没觉得有什么。不就是两块表嘛。直到后来在一次现场联调里,客户现场只装了柜内表、没装网侧表,结果防逆流直接不干活了。我才意识到,这套逻辑对硬件是有前提的。这一点放到第七节细说。

2. 两个模块之间的"门铃":System V 信号量

电表的数据是谁采的?kwhmeter_sampler,一个跑在 RS485 上的采样器。防逆流、防过载的策略是谁算的?ccu_sampler,另一个采样器。

问题来了:这两个是独立的模块,各自一个线程在跑。电表采样器发现"逆流了",怎么第一时间告诉 CCU 采样器?

答案是一个 System V 信号量。代码里就一行:

c 复制代码
pCCU->nEssSemCCUWith485Meter = Creat_SemID(SEM_CCU_485_PATH, SEM_CCU_485_PROJ_ID, 1, IPC_CREAT | 0666);

展开来,Creat_SemID 就干两件事:

c 复制代码
int Creat_SemID(const char *pathname, int proj_id, int nsems, int semflg)
{
    key_t key   = ftok(pathname, proj_id);   // 用路径+字符生成一个 IPC key
    int   semid = semget(key, nsems, semflg); // 用 key 拿到信号量
    return semid;
}

其中 SEM_CCU_485_PATH/varSEM_CCU_485_PROJ_ID 是字符 'S'。关键在于 ftok:只要两个进程用同一个路径加同一个字符,算出来的 key 就一定相同,于是它们拿到的是内核里同一个信号量。

我第一次接触这个,脑子里冒出来的问题是:这跟普通的锁有啥区别?

区别在于作用域。pthread 的互斥锁只能在一个进程内用,而 System V 信号量是内核对象,跨进程。CCU 采样器和电表采样器是两个 .so,各自独立,普通锁管不到彼此,只有这种内核级的东西能当桥梁。

然后就是两个操作,一个等、一个发:

c 复制代码
void sem_wait_op(int semid)   // P 操作,减一,减到 0 以下就阻塞
{
    struct sembuf sop = {0, -1, 0};
    semop(semid, &sop, 1);
}

void sem_post_op(int semid)   // V 操作,加一,唤醒等待者
{
    struct sembuf sop = {0, 1, 0};
    semop(semid, &sop, 1);
}

我把它理解成一个门铃。电表采样器那边按一下 sem_post_op,CCU 采样器这边一直睡在 sem_wait_op 上,门铃一响就被内核叫醒。

这里有个容易误解的点:信号量不传数据。它只传"有事情发生了"这一个信号。真正的功率值、电压电流,是通过 DPR 数据模型共享的。信号量只是负责"敲门"。

ftok 这个函数也值得说两句。它是拿一个文件路径加一个字符,算出一个整数 key。这里有个隐蔽的坑:路径必须是真实存在的文件,不存在就会返回 -1,然后 semget 拿到的是个非法值,信号量静默失效。项目里用的是 /var,一个稳定的系统目录,而不是某个可能被清理的临时文件。另外,路径加字符这个组合,只要两个模块各写错一个字符,就会各拿到各的信号量,谁也等不到谁------这种 bug 不报错,就是"永远不触发",比崩溃还难查。

还有一点我一开始没想明白:为什么不用 POSIX 命名信号量,非要用 System V 这套 ftok + semget + semop 的老古董。后来看代码,发现这个项目里 System V 信号量已经在 syshand 守护进程和主进程之间用着了,是一套既有的 IPC 约定。新模块沿用同一套,比再引入一种信号量风格要省心。工程里很多"为什么这么写"的答案,其实是"因为这里已经这么写了"。

3. 快速通道:从轮询到事件驱动

为什么要费劲搞一个信号量?直接让 CCU 采样器定时去读电表数据不行吗?

行,但是慢,而且浪费 CPU。

定时轮询的问题在于响应延迟取决于轮询周期。周期设短了,CPU 忙等;周期设长了,逆流都发生几百毫秒了 CCU 还没反应过来。而逆流这件事,等不起。电网那边对逆流的容忍度是按"一次调频"级别来要求的,端到端要压到 200 毫秒以内。

信号量是阻塞的。CCU 采样器的线程平时就睡在 sem_wait_op 上,不占 CPU。电表那边一 sem_post_op,内核立刻把它唤醒,微秒级的延迟。这就是"快速通道"的由来,代码里对应一个 bQuickChannle 标志。

整条端到端的时序,我拆开来算过。电表采样器那边,RS485 采集一轮网侧表功率,大概几十毫秒;发现两个功率都为负,sem_post 一下,微秒级;CCU 采样器被唤醒,跑一遍能量管理策略算出发电功率上限,再通过 CAN 或 Modbus 把指令下给 PCS,又是几十毫秒。全部加起来,从"逆流发生"到"PCS 开始降功率",控制在 200 毫秒以内。这 200 毫秒就是电网给储能定的硬指标,也是"快速响应"这个说法的来源。

这里面的关键,是信号量那一段几乎不耗时。如果换成轮询,哪怕周期压到 100 毫秒,光是"等下一轮"就要吃掉一半的预算。事件驱动和轮询的差距,在这种硬实时场景里会被放大得特别明显。

触发条件在 kwhmeter_samplerJudge_ActvPower_Reflux 里:

c 复制代码
if ((fCabPwr_Sum < 0) && (fGridPwr_Sum < 0))
{
    sem_post_op(pWhim_Rough->nSemCCUWith485Meter);   // 发信号唤醒 CCU
}

柜内功率和网侧功率同时为负,说明功率在往电网倒灌,这时候按下门铃。

CCU 采样器被唤醒后,先检查一个前置条件:

c 复制代码
if (pCCU->emPMCtrlMode != AUTO_CTRL_PM_MODE)   // 非自动模式,直接跳过
    continue;

必须是自动功率管理模式才会响应。手动模式下,即使逆流了,系统也不该自作主张去调 PCS。这是安全上的考虑------操作员在手动调试的时候,最怕设备自己乱动。

4. 防逆流:别把电倒回电网

放电的时候,储能给负载供电。理想情况是"负载吃多少,储能放多少"。但负载是实时波动的,万一负载突然掉下去,储能还在按原来的功率放,多出来的电就会倒灌回电网。这就是逆流。

判断逻辑在 JudgeIfEnterAnti_RefluxOrOverload 里:

c 复制代码
BOOL bAntiReflux = (fAnti_RefluxMeter - pCCU->blobBackendCfg.fPreProt4RefluxAndOverLoad
                    < (pEMS->fAllowedReverseValue * -1.0f));

拆开看,三个量:

  • fAnti_RefluxMeter:防逆流表的实时有功功率,就是网侧表功率
  • fPreProt4RefluxAndOverLoad:放电预保护值,一个提前量
  • fAllowedReverseValue:逆流允许值,允许倒灌多少

翻译成人话:当网侧功率负到「低于逆流允许值 + 预保护值」的时候,判定进入防逆流。

为什么要有预保护值?注释里写得很直白:Pre-Sett Value, Due To EMS Adjust Power is Too Slow------EMS 调节功率太慢了,所以要提前动手。等真正越界了再去调,传输和计算延迟早就让逆流多倒灌了几十毫秒。

fAllowedReverseValue 是给奥地利认证这种场景留的。有些电网允许一点点逆流,这个值可以配置成 3KW、5KW 之类。

真正算放电功率上限的地方在 Calc_ESS_DischgLmtPower

c 复制代码
pEMS->fTotal_PessDischgLmt =
    (pEMS->fPacload4Dischg - fProt) * pCCU->blobBackendCfg.fOutput2AC_PwrRate;

pEMS->fTotal_PessDischgLmt += pEMS->fAllowedReverseValue;

这里 fPacload4Dischg 就是前面说的负载功率(网侧表减柜内表),fProt 是放电预保护值,fOutput2AC_PwrRate 是放电跟随百分比。最后再加回逆流允许值。

举个例子。负载 60KW,放电预保护值 5KW,跟随百分比 0.9,逆流允许值 3KW:

复制代码
放电上限 = (60 - 5) × 0.9 + 3 = 52.5 KW

储能最多放 52.5KW,负载吃 60KW,中间还差 7.5KW 的余量,不会倒灌。

上面说的是三相总量的防逆流。还有一个单相的场景,是奥地利认证项目带出来的。欧标对单相逆流管得比国内严,光看三相总量不够,得看每一相。代码里 fAtRflxMtr_kWPhMin 就是三相功率里的最小值,单相防逆流的触发条件是:

c 复制代码
if (pEMS->fAtRflxMtr_kWPhMin < pCCU->blobBackendCfg.fPreProt4Reflux_Phase)
    pEMS->bEnterAnti_Reflux_Phase = TRUE;

三相里只要有一相的功率负到超过单相预保护值,就触发。这个场景的坑在于,三相负载往往是不平衡的,某一相可能已经逆流了,三相总量还是正的。如果只做总量防逆流,单相逆流根本抓不住。奥地利认证要求"三相总量 + 单相最小值"双通道同时检测,缺一不可。

5. 防过载:别把变压器顶爆

充电的时候,问题反过来了。储能充电相当于一个大负载,充电功率加上原本的负载功率,不能超过变压器的容量。超了,变压器过载跳闸,整条线路都断。

判断逻辑同样在那个函数里:

c 复制代码
BOOL bAntiOverLoad = ((pCCU->blobBackendCfg.fSysACInput_PwrLmt - fAnti_OverLoadMeter)
                      < pCCU->blobBackendCfg.fPreProt_Chrg);

fSysACInput_PwrLmt 就是变压器可用功率,注释直接写着"变压器可用功率(防过载)"。fAnti_OverLoadMeter 是网侧表功率,fPreProt_Chrg 是充电预保护值。

翻译:变压器功率减去网侧表功率,剩下的可用功率如果已经小于充电预保护值了,就判定进入防过载。

充电功率上限的完整计算在 Calc_ESS_ChgLmtPower

c 复制代码
// 剩余充电功率 = 变压器功率 - 负载功率
pEMS->fRemainPwr4Chrg = pEMS->fGridACInputPwr_Lmt - pEMS->fPacload4Chg;

// 再减预保护值,乘充电跟随百分比
pEMS->fGridRemainACPwr2Chrg =
    MAX(0.0f, (pEMS->fGridRemainACPwr2Chrg - fProt) * pCCU->blobBackendCfg.PowerRate_Chrg);

举个例子,变压器 100KW,负载 40KW,充电预保护值 5KW,跟随百分比 0.9:

复制代码
剩余 = 100 - 40 = 60
充电上限 = MAX(0, (60 - 5) × 0.9) = 49.5 KW
充电 49.5 + 负载 40 = 89.5,离 100 还有 10.5 的余量

这套逻辑就是"跟随负载"的核心。充电功率不是写死的,而是每一轮都根据当前的负载重新算,负载大就少充点,负载小就多充点,始终贴着变压器容量走,又留着一层安全垫。

6. 两个公式,一个思想

把充电和放电摆在一起看,会发现它们是一对镜像:

充电(防过载):

复制代码
充电功率 = (变压器功率 - 负载功率 - 充电预保护值) × 充电跟随百分比

放电(防逆流):

复制代码
放电功率 = (负载功率 - 放电预保护值) × 放电跟随百分比 + 逆流允许值

一个减的是"变压器剩余容量",一个减的是"负载需求"。方向相反,思想一样------都是在理论上能用的功率里,先扣掉一个预保护值当缓冲,再乘一个跟随百分比,让实际功率始终比理论极限小那么一截。

预保护值这个东西,我理解成"给系统留的反应时间"。EMS 从采样到计算到下发 PCS 指令,是有一个延迟的。如果贴着极限跑,这个延迟窗口里负载稍微动一下就越界了。预保护值就是把这个延迟窗口换算成功率,提前预留出来。

两个预保护值当初是共用的一个参数,后来拆开了(代码里能看到 LJH250703 的改动注释),因为充电和放电需要留的余量不一样,混用一个值两边都别扭。

7. 工程里真正让人头疼的坑

代码逻辑看懂了是一回事,现场跑起来又是另一回事。

第一个坑:电表没装齐。

前面说了,负载功率 = 网侧表 - 柜内表。这是硬前提。代码里 Judge_ActvPower_Reflux 开头就判断:

c 复制代码
if ((meterData[CAB_FIRST].emMeterType == METER_TYPE_NotInstalled)
 || (meterData[GRID_FIRST].emMeterType == METER_TYPE_NotInstalled))
    return 0;   // 直接不处理

注释写得很清楚:站级 EMS 快速防逆流必须接柜内表和网侧表。但现场总有各种情况------有的站从设备只装了柜内表没装网侧表,有的干脆把表接错了位置。这种时候防逆流是静默失效的,不报错,只是"永远不触发"。最怕的就是这种安静的错误。

第二个坑:电表安装位置决定负载怎么算。

负载功率不是固定用"网侧表减柜内表"这一种算法。代码里 Calc_ACLoadPwrByDiffMeterPosition 按电表位置分了五六种场景,工业用电、民用电、多负载支路,每种减法的表不一样。配置里 emAuxMeterPos(参数 1,7)设错了,负载算出来就是错的,后面全错。

第三个坑:迟滞防振荡。

逆流和过载的进入、退出都不是瞬时的,中间隔了一个 MAX_RETDIFF_PERIOD 时间窗。为什么?因为功率在阈值附近抖动的时候,如果进出没有迟滞,PCS 会疯狂地一会儿开一会儿关,功率振荡。这段代码的注释我记得特别清楚,防逆流退出要"超过时间窗",就是怕抖。

第四个坑:预保护值怎么调。

预保护值设大了,系统太保守,功率上不去,白白浪费容量;设小了,余量不够,负载一抖就越界。这个值没有银弹,只能现场一点一点试。我自己的经验是先设成变压器容量的 5% 左右,然后看功率曲线,有振荡就加大,有余量就减小。

第五个坑:手动模式和自动模式的边界。

被信号量唤醒后,第一步就是检查 emPMCtrlMode 是不是自动模式。这个检查不能省。现场调试的时候,操作员会切到手动模式去单独控制某一台 PCS,如果这时候自动策略还在跑,两边指令冲突,后果比逆流严重得多。

除了这五个坑,我还想记一个真实的联调事故。有一次客户现场的防逆流怎么都触发不了,日志里明明能看到网侧表功率已经负了,但 PCS 就是不动。排查了一整天,最后发现是电表安装位置那个参数配错了。客户现场是"多负载支路"的接法,负载功率应该用 网侧表 - 柜内表 + 辅助表 来算,但配置里还停在默认的"无辅助表",算出来的负载比真实值小了一截,导致逆流判断的阈值整个偏移了。表接对了,位置参数配错了,等于白接。

那次之后我养成一个习惯:现场上电之前,先把这几块表"谁是谁"在配置里对一遍,拿一个已知负载去验证减法公式对不对。表读数的正负号、安装位置、参数 ID,任何一个对不上,后面所有策略都是空中楼阁。功率控制这种东西,地基错了,楼越高塌得越狠。

关于这几个参数到底藏在哪,也顺带说清楚。变压器可用功率 fSysACInput_PwrLmt、充电预保护值 fPreProt_Chrg、放电预保护值 fPreProt4RefluxAndOverLoad、充电跟随百分比 PowerRate_Chrg、放电跟随百分比 fOutput2AC_PwrRate,这些全在同一个后端配置 blob A_BLOB_ES_BACKEND_CFG 里,参数 ID 是 nDevId=1, nParamId=26。也就是说,你在 Web 配置里改一个"后端配置"的 blob,这几个字段一起生效。

变压器功率这个值,代码里还留了三种来源:可以直接用手填的 fSysACInput_PwrLmt,也可以设成"固定值模式"读 fFixed_GridInput_Limit,或者"动态模式"读管理计划里每个时段的 nGridInputPwrLmt。如果现场是分时电价、不同时段变压器额度不同,就用动态模式;如果变压器容量恒定,直接禁用动态限制、手填一个值最省事。

我自己的习惯是:变压器功率填实际容量的值,预保护值先设成容量的 5%,跟随百分比设 0.9,然后上电看功率曲线再微调。这三个值没有一个"正确"答案,只有"适合这个现场"的答案。

8. 写在最后

这套防逆流、防过载的逻辑,代码层面没有太多花哨的东西。两块表做减法算负载,一个信号量当门铃,两个预保护值留余量,再加一个迟滞窗防振荡。全部加起来,就是储能系统并网安全的底线。

但它让我重新理解了什么叫"实时"。实时不是 CPU 跑得快,而是事件一发生,系统能在规定时间内响应。信号量那套"阻塞等待 + 内核唤醒",就是为此设计的------平时零开销地睡着,关键时刻微秒级地醒来。

也让我理解了"跟随"的含义。跟随负载不是把公式写对就完了,而是要接受负载永远在变这个事实,然后用预保护值去对冲不确定性,用迟滞窗去对抗抖动。工程和理论的区别,大概就在这些"留一手"的细节里。

最后说一句有点感慨的话。储能这个行业,安全从来不是一句口号。防逆流和防过载背后,是电网对每一个并网点的硬约束。我们写的每一行减法、每一个信号量、每一个预保护值,最终都是为了让系统在无人值守的凌晨三点,负载突变的瞬间,还能稳稳地待在自己该待的位置上。

相关推荐
2601_962218473 小时前
万象生鲜系统区块链溯源技术帮助生鲜企业搭建食品安全数字化体系
大数据·运维·微服务·云原生·架构
东方-教育技术博主3 小时前
自动编码在教育场景中的重要性:一项基于多源证据的深度综述
大数据·人工智能
新时代牛马3 小时前
Linux 内核入门地图:架构、源码目录与五大子系统
linux·运维·架构
liuqs3324 小时前
机器人产业加速发展,制造业正在迎来新的自动化探索
大数据·人工智能
腾讯云大数据4 小时前
DataBuddy数据语义驱动的企业Agent Runtime实践
大数据·人工智能·腾讯云·agent
Julien20044 小时前
控制 SELinux 文件上下文
linux·运维·服务器
cvcode_study4 小时前
Ollama+ComfyUI 多模态
大数据·人工智能·python
sulikey4 小时前
Linux Core Dump 完全指南:从原理到实战的崩溃调试手册。什么是core dump?程序报错core dumped怎么办?
linux·服务器·操作系统·调试·coredump
四方云5 小时前
把电话能力无缝嵌入企业自有CRM
大数据·人工智能