从一个具体的问题说起
如果有人在机房里指着一台工商业储能电源柜问你,这台柜子是怎么被控制起来的,你会怎么答?
这个问题看着简单,真要答清楚,却得一层层往下挖。因为控制这台柜子的并不是一套软件,而是两套跑在完全不同硬件上的系统:上层是一颗异构多核处理器,跑 Linux,负责"想清楚要干什么";下层是一块工业级单片机,跑实时操作系统,负责"把想清楚的事真正做出来"。两者之间,靠一条自定义的通信协议连起来。
我最近把这两套代码都完整读了一遍。读完才真正理解,储能系统的控制架构为什么会长成这个样子,以及"主控"和"执行单元"这两个词,各自到底承担了什么。
这篇文章想把这件事讲清楚。我不从"储能行业背景"这类大帽子讲起,而是贴着代码,讲这两套系统各自是什么、怎么分工、靠什么连起来,以及一条指令从主控发出去之后,是怎么一步步变成电池的充放电、变成光伏的输出功率的。
这台设备是什么
先给这台设备一个准确的位置。
它是一台工商业储能电源柜,名字里带"光储充"三个字。它不是单纯的电池柜,而是把三样东西塞进了同一个柜子里:光伏输入、储能电池、充电桩输出。业内称之为光储充一体机。
为什么要做成一体机?因为光伏发电有波动,电网容量有限,充电桩又是大功率负载。三者绑在一起,光伏发出来的电既可以直接充进电池,也可以直接给车充电;不够了再从电网取,多余了再卖给电网。这套调度逻辑,正是储能能量管理系统要解决的问题。
控制这台柜子,硬件上分了两层。上层是一颗异构多核处理器,里面有一个应用核跑 Linux,还有一个实时核跑实时操作系统,两者通过核间通信交换数据。下层的柜内控制,交给一块工业级单片机。上层跑的那套软件是"主控",下层跑的是"执行单元"。前者是大脑,后者是神经中枢加肌肉。
下面这张图是整机的控制架构总览:
#mermaid-svg-zPPsX9EFVJmQ6vNt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zPPsX9EFVJmQ6vNt .error-icon{fill:#552222;}#mermaid-svg-zPPsX9EFVJmQ6vNt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zPPsX9EFVJmQ6vNt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .marker.cross{stroke:#333333;}#mermaid-svg-zPPsX9EFVJmQ6vNt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zPPsX9EFVJmQ6vNt p{margin:0;}#mermaid-svg-zPPsX9EFVJmQ6vNt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .cluster-label text{fill:#333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .cluster-label span{color:#333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .cluster-label span p{background-color:transparent;}#mermaid-svg-zPPsX9EFVJmQ6vNt .label text,#mermaid-svg-zPPsX9EFVJmQ6vNt span{fill:#333;color:#333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .node rect,#mermaid-svg-zPPsX9EFVJmQ6vNt .node circle,#mermaid-svg-zPPsX9EFVJmQ6vNt .node ellipse,#mermaid-svg-zPPsX9EFVJmQ6vNt .node polygon,#mermaid-svg-zPPsX9EFVJmQ6vNt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zPPsX9EFVJmQ6vNt .rough-node .label text,#mermaid-svg-zPPsX9EFVJmQ6vNt .node .label text,#mermaid-svg-zPPsX9EFVJmQ6vNt .image-shape .label,#mermaid-svg-zPPsX9EFVJmQ6vNt .icon-shape .label{text-anchor:middle;}#mermaid-svg-zPPsX9EFVJmQ6vNt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zPPsX9EFVJmQ6vNt .rough-node .label,#mermaid-svg-zPPsX9EFVJmQ6vNt .node .label,#mermaid-svg-zPPsX9EFVJmQ6vNt .image-shape .label,#mermaid-svg-zPPsX9EFVJmQ6vNt .icon-shape .label{text-align:center;}#mermaid-svg-zPPsX9EFVJmQ6vNt .node.clickable{cursor:pointer;}#mermaid-svg-zPPsX9EFVJmQ6vNt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .arrowheadPath{fill:#333333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zPPsX9EFVJmQ6vNt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zPPsX9EFVJmQ6vNt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zPPsX9EFVJmQ6vNt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zPPsX9EFVJmQ6vNt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zPPsX9EFVJmQ6vNt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zPPsX9EFVJmQ6vNt .cluster text{fill:#333;}#mermaid-svg-zPPsX9EFVJmQ6vNt .cluster span{color:#333;}#mermaid-svg-zPPsX9EFVJmQ6vNt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zPPsX9EFVJmQ6vNt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zPPsX9EFVJmQ6vNt rect.text{fill:none;stroke-width:0;}#mermaid-svg-zPPsX9EFVJmQ6vNt .icon-shape,#mermaid-svg-zPPsX9EFVJmQ6vNt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zPPsX9EFVJmQ6vNt .icon-shape p,#mermaid-svg-zPPsX9EFVJmQ6vNt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zPPsX9EFVJmQ6vNt .icon-shape .label rect,#mermaid-svg-zPPsX9EFVJmQ6vNt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zPPsX9EFVJmQ6vNt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zPPsX9EFVJmQ6vNt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zPPsX9EFVJmQ6vNt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 对外
下层:执行单元(跑在工业单片机上的固件)
上层:主控(跑在 Linux 的异构多核处理器上)
私有通信协议(以太网)
标准工业协议
电池状态
功率输出
能量调度与决策
数据采集与上报
通信抽象层
电池管理单元通信
功率模块控制
充电子单元管理
电气安全执行
云平台 / 上级调度系统
上层:主控想清楚要干什么
主控的职责可以浓缩成一句话:采集所有设备的状态,算出该让系统怎么跑,再把指令发下去,把结果报上去。
先说它跑在什么环境里。它跑在异构多核处理器的应用核上,Linux 系统。这套软件是后来才从旧平台迁移过来的,迁移的核心动作,是把原本和底层耦合在一起的业务程序解耦,做成一个独立的可运行镜像,加上完整性校验和 OTA 升级机制。这些是工程基建,保证了现场能安全地远程升级。
再看它的进程模型。它有两个进程,一个是守护进程,一个是主进程。守护进程负责安装扩展、监控心跳、代理系统命令,通过共享内存和信号量跟主进程通信。主进程一段时间收不到心跳,会记一条日志,但不会重启整机。这个设计取向值得注意:应用故障不重启整机,重启应用这件事交给独立的管理机制。这跟很多"看门狗一失联就复位"的做法不一样,是刻意的取舍。
主进程的启动分六步,这六步其实就是它整个架构的骨架:先建数据中心,再加载配置和数据,再加载数据访问接口,再加载通信模块,再加载采样器,最后加载上报器。
这里面最值得讲的是它的三层数据流。所有东西都围绕一个方向流动:设备、采样器、数据中心、上报器、云端或本地。
#mermaid-svg-GnEEiJPBkggQaMvc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GnEEiJPBkggQaMvc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GnEEiJPBkggQaMvc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GnEEiJPBkggQaMvc .error-icon{fill:#552222;}#mermaid-svg-GnEEiJPBkggQaMvc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GnEEiJPBkggQaMvc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GnEEiJPBkggQaMvc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GnEEiJPBkggQaMvc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GnEEiJPBkggQaMvc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GnEEiJPBkggQaMvc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GnEEiJPBkggQaMvc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GnEEiJPBkggQaMvc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GnEEiJPBkggQaMvc .marker.cross{stroke:#333333;}#mermaid-svg-GnEEiJPBkggQaMvc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GnEEiJPBkggQaMvc p{margin:0;}#mermaid-svg-GnEEiJPBkggQaMvc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GnEEiJPBkggQaMvc .cluster-label text{fill:#333;}#mermaid-svg-GnEEiJPBkggQaMvc .cluster-label span{color:#333;}#mermaid-svg-GnEEiJPBkggQaMvc .cluster-label span p{background-color:transparent;}#mermaid-svg-GnEEiJPBkggQaMvc .label text,#mermaid-svg-GnEEiJPBkggQaMvc span{fill:#333;color:#333;}#mermaid-svg-GnEEiJPBkggQaMvc .node rect,#mermaid-svg-GnEEiJPBkggQaMvc .node circle,#mermaid-svg-GnEEiJPBkggQaMvc .node ellipse,#mermaid-svg-GnEEiJPBkggQaMvc .node polygon,#mermaid-svg-GnEEiJPBkggQaMvc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GnEEiJPBkggQaMvc .rough-node .label text,#mermaid-svg-GnEEiJPBkggQaMvc .node .label text,#mermaid-svg-GnEEiJPBkggQaMvc .image-shape .label,#mermaid-svg-GnEEiJPBkggQaMvc .icon-shape .label{text-anchor:middle;}#mermaid-svg-GnEEiJPBkggQaMvc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GnEEiJPBkggQaMvc .rough-node .label,#mermaid-svg-GnEEiJPBkggQaMvc .node .label,#mermaid-svg-GnEEiJPBkggQaMvc .image-shape .label,#mermaid-svg-GnEEiJPBkggQaMvc .icon-shape .label{text-align:center;}#mermaid-svg-GnEEiJPBkggQaMvc .node.clickable{cursor:pointer;}#mermaid-svg-GnEEiJPBkggQaMvc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GnEEiJPBkggQaMvc .arrowheadPath{fill:#333333;}#mermaid-svg-GnEEiJPBkggQaMvc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GnEEiJPBkggQaMvc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GnEEiJPBkggQaMvc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GnEEiJPBkggQaMvc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GnEEiJPBkggQaMvc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GnEEiJPBkggQaMvc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GnEEiJPBkggQaMvc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GnEEiJPBkggQaMvc .cluster text{fill:#333;}#mermaid-svg-GnEEiJPBkggQaMvc .cluster span{color:#333;}#mermaid-svg-GnEEiJPBkggQaMvc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GnEEiJPBkggQaMvc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GnEEiJPBkggQaMvc rect.text{fill:none;stroke-width:0;}#mermaid-svg-GnEEiJPBkggQaMvc .icon-shape,#mermaid-svg-GnEEiJPBkggQaMvc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GnEEiJPBkggQaMvc .icon-shape p,#mermaid-svg-GnEEiJPBkggQaMvc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GnEEiJPBkggQaMvc .icon-shape .label rect,#mermaid-svg-GnEEiJPBkggQaMvc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GnEEiJPBkggQaMvc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GnEEiJPBkggQaMvc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GnEEiJPBkggQaMvc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主控的三层数据流
设备
电池 / 电表 / 变流器 / 温控
采样器层
负责跟具体设备打交道
数据中心
配置 / 事件 / 告警 / 表达式
上报器层
对接云平台 / 上级系统
云平台 / 本地监控
采样器是"采集"这一层,负责跟具体设备打交道。代码里有十个采样器,管电表的、管串口设备的、管系统状态的、管主控单元的、管电池的、管网络交互的、管串口服务器的、管空调的、管辅助设备的、管温控的。每个采样器是一个动态库,运行时按需加载,导出初始化、采样、控制、自检四个接口。
数据中心是中间那一层,也是最重的一层。配置、事件、告警、表达式,全在这里处理。配置不走文本文件,而是存嵌入式数据库。事件驱动是整个系统的骨架,采样器把数据推进来,数据中心根据事件和表达式算出告警和状态变化,再通知上报器。
上报器是"上报"这一层,共八个,各管一路:MQTT 上报云平台、Modbus 对接上级调度系统、两种电力规约做标准接入、Web 做本地监控、维护后台做运维。这里面 Modbus 那个上报器最典型,它把内部的信号映射成标准的寄存器,让上级系统能像读标准设备一样读这台柜子的数据。
通信层还有一块独立的东西:九个通信抽象模块,统一成一个六函数的接口,打开、接收、读、写、控制、关闭。串口、网络、CAN、文件传输、WebSocket、进程间通信、核间通信,都套在这个接口下面。这块设计让上层的采样器和上报器不用关心底层是串口还是网口,换传输方式只需要换一个动态库。
核间通信也在这九个模块里,它是应用核跟实时核之间传数据的通道。实时核跑实时操作系统,管着一些对时序要求更高的东西,应用核的 Linux 通过它交换电池状态和控制指令。
下层:执行单元把指令真正做出来
如果说主控是"想清楚要干什么",那执行单元就是"把这件事做出来"。它是柜内的控制板固件,跑在工业单片机上,实时操作系统,配一套轻量网络协议栈和文件系统。
先看它的任务表。实时操作系统里挂了十来个任务,按优先级排开:时钟任务、自检任务、以太网任务、功率模块通信任务、电池通信任务、网络应用任务、测量任务、告警任务、存储任务、串口任务。
这些任务背后是三路通信,正好对应柜子里的三类设备。
第一路是电池的 CAN 总线。柜子里最多挂四组电池,每组电池的管理单元接在 CAN 总线上,地址从 0x32 到 0x35。电池任务周期性地去收这些电池的报文,把保护状态、工作状态、充放电电流功率电压限额、总电压电流功率、数字量状态都解出来。十秒收不到某组电池的报文,就把它标记成通信失败,数据作废。
第二路是功率模块的 CAN 总线。柜子里的整流模块、直流变换模块、光伏模块都挂在上面,是大功率模块。执行单元通过这条总线给模块下发启停命令、电压电流目标值,也读回模块的状态。模块回的状态是位域编码的,过压、欠压、过温、缺相、不平衡、风扇故障、整流失败,每个故障占一个 bit。这些位就是储能安全保护的最底层信息来源。
第三路是串口和以太网。串口接充电侧的充电子单元,以太网对外提供 Modbus 服务器、MQTT 客户端、Web 服务器和网络管理。柜子对外的监控和云端上报,都走以太网。
下面这张图是执行单元的通信拓扑:
#mermaid-svg-go1SKi0LHILUyoKK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-go1SKi0LHILUyoKK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-go1SKi0LHILUyoKK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-go1SKi0LHILUyoKK .error-icon{fill:#552222;}#mermaid-svg-go1SKi0LHILUyoKK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-go1SKi0LHILUyoKK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-go1SKi0LHILUyoKK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-go1SKi0LHILUyoKK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-go1SKi0LHILUyoKK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-go1SKi0LHILUyoKK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-go1SKi0LHILUyoKK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-go1SKi0LHILUyoKK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-go1SKi0LHILUyoKK .marker.cross{stroke:#333333;}#mermaid-svg-go1SKi0LHILUyoKK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-go1SKi0LHILUyoKK p{margin:0;}#mermaid-svg-go1SKi0LHILUyoKK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-go1SKi0LHILUyoKK .cluster-label text{fill:#333;}#mermaid-svg-go1SKi0LHILUyoKK .cluster-label span{color:#333;}#mermaid-svg-go1SKi0LHILUyoKK .cluster-label span p{background-color:transparent;}#mermaid-svg-go1SKi0LHILUyoKK .label text,#mermaid-svg-go1SKi0LHILUyoKK span{fill:#333;color:#333;}#mermaid-svg-go1SKi0LHILUyoKK .node rect,#mermaid-svg-go1SKi0LHILUyoKK .node circle,#mermaid-svg-go1SKi0LHILUyoKK .node ellipse,#mermaid-svg-go1SKi0LHILUyoKK .node polygon,#mermaid-svg-go1SKi0LHILUyoKK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-go1SKi0LHILUyoKK .rough-node .label text,#mermaid-svg-go1SKi0LHILUyoKK .node .label text,#mermaid-svg-go1SKi0LHILUyoKK .image-shape .label,#mermaid-svg-go1SKi0LHILUyoKK .icon-shape .label{text-anchor:middle;}#mermaid-svg-go1SKi0LHILUyoKK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-go1SKi0LHILUyoKK .rough-node .label,#mermaid-svg-go1SKi0LHILUyoKK .node .label,#mermaid-svg-go1SKi0LHILUyoKK .image-shape .label,#mermaid-svg-go1SKi0LHILUyoKK .icon-shape .label{text-align:center;}#mermaid-svg-go1SKi0LHILUyoKK .node.clickable{cursor:pointer;}#mermaid-svg-go1SKi0LHILUyoKK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-go1SKi0LHILUyoKK .arrowheadPath{fill:#333333;}#mermaid-svg-go1SKi0LHILUyoKK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-go1SKi0LHILUyoKK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-go1SKi0LHILUyoKK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-go1SKi0LHILUyoKK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-go1SKi0LHILUyoKK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-go1SKi0LHILUyoKK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-go1SKi0LHILUyoKK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-go1SKi0LHILUyoKK .cluster text{fill:#333;}#mermaid-svg-go1SKi0LHILUyoKK .cluster span{color:#333;}#mermaid-svg-go1SKi0LHILUyoKK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-go1SKi0LHILUyoKK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-go1SKi0LHILUyoKK rect.text{fill:none;stroke-width:0;}#mermaid-svg-go1SKi0LHILUyoKK .icon-shape,#mermaid-svg-go1SKi0LHILUyoKK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-go1SKi0LHILUyoKK .icon-shape p,#mermaid-svg-go1SKi0LHILUyoKK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-go1SKi0LHILUyoKK .icon-shape .label rect,#mermaid-svg-go1SKi0LHILUyoKK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-go1SKi0LHILUyoKK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-go1SKi0LHILUyoKK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-go1SKi0LHILUyoKK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 执行单元固件
(工业单片机 + 实时操作系统)
CAN 总线 1
电池管理单元 × 4
CAN 总线 2
整流 / 直流变换 / 光伏模块
串口
充电子单元
以太网
Modbus / MQTT / Web
云平台
本地监控
除了通信,执行单元还管着一堆"执行"动作。接触器分合、风扇调速、漏电检测、绝缘检测、防雷状态、门磁、急停。这些是电气安全的执行层,每一路都对应代码里的一个控制点和一串告警逻辑。
存储这块也有讲究。参数存电可擦写存储器,充电记录和故障日志存闪存。故障日志用了断电不丢失的内存段,复位后还能翻出来看上次是为什么停的。这套"掉电不丢故障现场"的设计,是工业产品跟演示程序的一个分水岭。
连接两者的那条通信协议
两套系统靠什么说话?靠一套私有通信协议。仓库里存了它从早期到后期的五个版本,迭代了两三年,每次升级都往里面加东西,比如加辅助单元状态、加变流器控制、加 CAN 原始报文透传。
传输层是网络。主控做服务器,固定内网地址和端口。一体机做客户端,主动连上去。有意思的是,代码里这个协议的接收缓冲还留着串口的命名,说明它最早是串口版的,后来迁到了网络,名字没改。
帧格式很简洁,是一个典型的主从式私有协议:
帧头(3字节) | 命令(1字节) | 数据长度(2字节) | 数据 | 校验和(2字节) | 帧尾(1字节)
开头三个字节是帧头,结尾一个字节是帧尾,中间一个字节命令,两个字节数据长度,然后是数据,最后两个字节校验和。校验和是命令、长度、数据累加后取反。
命令这一层定义了一套清晰的表。读配置、写配置、下发控制命令、读模块信息、透传原始报文,各占一个命令号。还有两类周期上报:主控每秒广播一次上层状态,一体机每秒回一次下层状态,每十分钟报一次设备信息。查询和设置变流器各占一个命令号。升级占两个,一个告诉对方升级包多大,一个传升级数据。
写配置这条命令下面还有子命令,就是六组属性位图。主控通过它们告诉一体机:你下面挂了哪几组电池、有没有辅助单元、有没有充电子单元、光伏接触器和防雷器使没使能、漏电传感器开没开。这套位图就是主控对执行单元的"配置下达"。
主控和一体机之间的信息流,其实是不对称的。主控往下发的是指令和配置,一体机往上回的是状态和故障。这个不对称,正好对应了"大脑"和"神经中枢"的分工。
一条放电指令的完整旅程
前面是分块讲,现在串起来看一个具体场景:主控决定让电池放电给充电桩用。
第一步发生在主控里。数据中心的表达式引擎根据当前时段、电量、电价,算出应该放电,得到工作模式是放电、目标功率是多少。这个结果经过功率分配,落到每个节点的放电功率限额上。
第二步,这个决策变成一条控制命令,从 Linux 发出去,走以太网,到一体机的端口。
第三步,执行单元收到这条命令,解析帧头、长度、校验和,解出"放电"这个指令和对应的功率值。它先查一下电池管理单元给的最大放电电流和电压限额,确认这次放电不会超电池的能力,然后通过 CAN 给功率模块下发"逆变"指令和目标电压电流。
第四步,功率模块收到指令,把电池的直流电逆变成交流,供给充电桩侧的负载。同时模块把自己的实时电压、电流、温度、故障位报回执行单元,执行单元再把这些状态打包,通过私有协议回传主控。
第五步,主控收到回传,更新数据中心的状态,如果有告警就触发告警逻辑,再通过 Modbus 或 MQTT 把状态同步给上级系统和云平台。
这条链路用时序图看更清楚:
上级系统/云 功率模块 电池管理单元 执行单元(单片机) 主控(Linux) 上级系统/云 功率模块 电池管理单元 执行单元(单片机) 主控(Linux) #mermaid-svg-Kn17zmB6bwcwQnF5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Kn17zmB6bwcwQnF5 .error-icon{fill:#552222;}#mermaid-svg-Kn17zmB6bwcwQnF5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Kn17zmB6bwcwQnF5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Kn17zmB6bwcwQnF5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Kn17zmB6bwcwQnF5 .marker.cross{stroke:#333333;}#mermaid-svg-Kn17zmB6bwcwQnF5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Kn17zmB6bwcwQnF5 p{margin:0;}#mermaid-svg-Kn17zmB6bwcwQnF5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kn17zmB6bwcwQnF5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Kn17zmB6bwcwQnF5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Kn17zmB6bwcwQnF5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Kn17zmB6bwcwQnF5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Kn17zmB6bwcwQnF5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Kn17zmB6bwcwQnF5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Kn17zmB6bwcwQnF5 .sequenceNumber{fill:white;}#mermaid-svg-Kn17zmB6bwcwQnF5 #sequencenumber{fill:#333;}#mermaid-svg-Kn17zmB6bwcwQnF5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Kn17zmB6bwcwQnF5 .messageText{fill:#333;stroke:none;}#mermaid-svg-Kn17zmB6bwcwQnF5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kn17zmB6bwcwQnF5 .labelText,#mermaid-svg-Kn17zmB6bwcwQnF5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Kn17zmB6bwcwQnF5 .loopText,#mermaid-svg-Kn17zmB6bwcwQnF5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Kn17zmB6bwcwQnF5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Kn17zmB6bwcwQnF5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Kn17zmB6bwcwQnF5 .noteText,#mermaid-svg-Kn17zmB6bwcwQnF5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Kn17zmB6bwcwQnF5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kn17zmB6bwcwQnF5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kn17zmB6bwcwQnF5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kn17zmB6bwcwQnF5 .actorPopupMenu{position:absolute;}#mermaid-svg-Kn17zmB6bwcwQnF5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Kn17zmB6bwcwQnF5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kn17zmB6bwcwQnF5 .actor-man circle,#mermaid-svg-Kn17zmB6bwcwQnF5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Kn17zmB6bwcwQnF5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 根据时段/电量/电价算出放电功率下发"放电"控制命令(私有协议)解析帧,校验查询最大放电电流/电压限额返回限额下发逆变指令 + 目标电压电流回传实时状态和故障位回传下层状态(私有协议)同步状态(Modbus/MQTT)
这条链路走完,你会发现一件事:真正的"大脑"只在第一步和第五步动脑子,中间三步全是"神经中枢"在忠实地执行和回传。这就是分层控制的意义,每一层只做自己最擅长的事。
这套架构为什么长成这样
读完整套代码,最让我觉得值得琢磨的,是它为什么要把控制拆成两层,而不是在一个系统里做完所有事。
第一个原因,是实时性的分野。Linux 能跑复杂的调度算法、能连数据库、能跑 MQTT 和 Modbus,但它的实时性靠不住,一个调度延迟就能让控制指令晚到几十毫秒。而单片机上的实时操作系统,任务切换是微秒级的,CAN 和串口中断能精确到几个毫秒。功率模块的启停、接触器的分合,这些动作必须由时序有保证的那层来做。所以"想"交给 Linux,"做"交给单片机,各得其所。
第二个原因,是故障隔离。如果柜子里的功率模块出故障、电池失联,执行单元能第一时间在本地做出反应,切断输出、报故障,不用等主控反应过来。反过来,主控死机了,柜子也不会立刻乱套,因为底层的保护逻辑在单片机上独立跑着。这种"下层能自治"的设计,是储能安全的关键。
第三个原因,是可扩展性。一体机可以多台并柜,通过环网连起来。每台柜子有自己的执行单元,管自己柜内的事;主控统一调度所有柜子。加一台柜子,就是加一个执行单元,主控的架构不用动。
这套架构也解释了为什么两份代码的风格差异这么大。主控是插件化、动态库、嵌入式数据库、三层数据流,追求的是灵活和可配置。执行单元是全局结构体、位域、位图、断电不丢失段,追求的是确定和可靠。一个在 Linux 的舒适区里挥霍资源,一个在单片机的紧约束里精打细算。这种反差本身就是嵌入式系统分层的一个缩影。
这套东西值不值得学
我的答案是值,但要带着目的学。
值得的理由,是它把储能控制系统的三个层次都完整覆盖了:上层的主控调度、中间的通信协议、下层的执行控制。这三层放到一起,就是一套完整的"从大脑到肌肉"的认知。单独看任何一层,都只是半张地图。
更现实的价值,是它的可迁移性。CAN 协议解析、Modbus、MQTT、私有主从协议、实时操作系统任务设计、状态机、故障日志掉电保护,这些能力在储能、充电桩、新能源这个行业里几乎处处通用。一个能同时读懂 Linux 主控和单片机执行单元的人,在这个行业里不多。
学的时候有一条主线要抓住:沿着一条指令的流向读,而不是按文件读。从主控怎么算出决策开始,到协议怎么打包,再到单片机怎么解包、怎么控制模块、怎么回传。这条线读通了,整个架构就通了。
避开那些低价值的枝节。显示驱动上万行,废弃的网络旧代码,这些是历史包袱,陷进去只会消耗时间。把精力放在通信协议和能量管理上,这才是能带走的东西。
写在最后
回到开头那个问题:一台光储充一体机是怎么被控制起来的?
答案是,它被分成了两半。一半在多核处理器的 Linux 上,负责想清楚该充还是该放、充多少放多少;另一半在单片机上,负责把这个决定变成接触器的分合、功率模块的启停、电压电流的精确输出。中间一条帧格式简洁的私有协议,把"想"和"做"缝在一起。
读这套代码,真正读懂的其实不是某个协议或者某个函数,而是一种分工。什么交给有资源的地方,什么交给有时序保证的地方,什么留在本地兜底,什么报给上层决策。这些取舍,比任何一行代码都更接近这套系统的本质。