ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

充电站动态功率分配:功率池、OCPP与ISO 15118调度实现

2026/9/17 14:05:20 拓冰建站 浏览量
充电站动态功率分配:功率池、OCPP与ISO 15118调度实现 简介这是一份面向汽车技术、新能源汽车方向工程技术人员与高校师生的专业参考文献聚焦电动汽车充电站功率平衡分配这一实际难题。资源围绕慢、快充电双车位互补的整体框架展开讲解充电前准备与充电过程两阶段策略并给出充电站控制系统、电池检测系统、人机交互界面与车位管理系统的结构设计以及动态功率分配算法与新车加入分配算法的执行逻辑读者可据此理解激励因子分档、SOC 计算与最优功率分配的实现思路。压缩包共 1 个文件为 953KB 的 PDF 文档便于直接阅读、打印与引用。目前已有 107 人学习下载适合从事充电站设计与电网接入研究的中高级技术人员参考借鉴。1. 为什么一个 630kVA 的站点敢装 8 把 120kW 的枪一个典型的城市快充站进线变压器 630kVA按常规算法顶多带 4 台 120kW 直流桩。可实际投运时运营商往往要装 8 把甚至 12 把枪——因为车不是同时来的也不是全程都要满功率。动态功率分配要解决的正是这个矛盾把站内有限的功率当作一个共享池由站控系统按每辆车的 SOC、剩余充电时间、排队顺序秒级地重新切分每一把枪能拿到的功率。车少时单枪吃满 120kW车多时每把枪分到 40kW 也不至于把变压器拉爆。它落在三个岗位手里桩企做群充群控的嵌入式工程师、运营商做站级能源管理的平台开发、以及做微电网与有序充电的算法同学。这个标题里的设计二字重点不在硬件选型本身而在电气拓扑、控制通路和分配算法这三条线怎么咬合。2. 动态功率分配在电气侧和控制侧怎么落地2.1 站级功率池的三种硬件拓扑与选型对比动态功率分配不是纯软件功能它能做到多细的颗粒度取决于整流器和枪之间怎么连。常见做法有三大类集中式整流柜加直流母线加分配矩阵模块化分布式每把枪自带功率模块以及介于两者之间的混合式。集中式是分配颗粒度最细的一种。整流柜里放 N 个 30kW 或 40kW 的功率模块输出汇到一条直流母线上母线到每把枪之间串一个由接触器或继电器构成的开关矩阵。调度器决定1 号枪用 3 号、4 号、7 号模块矩阵就闭合对应的触点。这种结构单枪功率可以做到 30kW 的整数倍30/60/90/120kW模块利用率高缺点是矩阵触点多、成本高、切换有寿命损耗。分布式结构里每把枪有独立的 AC/DC 模块枪与枪之间没有直流母线只能靠降额来协调——本质上是用软件限制每把枪的电流上限。它便宜、可靠、易维护但颗粒度是整桩级别且模块轻载时效率掉得厉害。混合式通常指双枪共享一组模块或多枪共享一个功率柜颗粒度介于两者之间是当前做群充群控项目里性价比最高的方案。拓扑分配颗粒度单枪功率范围模块利用率矩阵/接触器成本适用场景集中式30kW 级30360kW最高高大功率超充、公交场站分布式整桩级桩额定值以下降额低轻载效率差无小区慢充、分散桩混合式30/60kW 级60240kW中高中城市公共快充站选型时先算两个数站内同时充电车辆数的期望峰值以及变压器长期允许的负载率。前者决定你要不要矩阵后者决定功率池上限设多少。我一般会把池上限按变压器容量的 0.8 倍取值再留 10% 给站内照明、空调和损耗。2.2 功率模块与分配矩阵的控制通路电气拓扑定了控制通路就有了骨架。站控单元常见是一块工控板或 PLC要同时对下控制功率模块和矩阵对上对接运营平台。对功率模块的控制主流走 CAN 总线。每个模块作为一个从节点站控周期性下发电压电流设定值模块回传实际输出电压、电流、温度和故障码。设定值下发频率通常 100ms 到 1s模块内部有自己的电流环站控不需要做快速闭环。# 用 candump 观察功率模块总线的实际报文确认模块地址与心跳 # 假设模块厂商用 0x180 nodeId 作为发送帧 ID candump can0,0x180:0x7FF -n 200 # 典型输出每 100ms 一帧8 字节 # can0 181 [8] 00 64 01 F4 00 00 00 00 # 字节 0-1: 模块状态 字节 2-3: 设定电压(0.1V) 字节 4-5: 设定电流(0.1A)上面这条命令本身不做分配它的作用是现场调试时先确认站控发的设定值模块到底收没收到、有没有被限流。很多分配不生效的问题最后查出来是模块被自身的限流参数卡住了跟调度算法无关。分配矩阵这边继电器或接触器通常由站控的 IO 扩展模块驱动。这里有一条硬约束直流侧带载切换触点会拉弧寿命会断崖式下跌。所以开关矩阵的动作必须遵循先降功率到零再断再合再升功率的顺序且要有最小保持时间避免一把枪在 30kW 和 60kW 之间来回跳。# 矩阵切换的安全时序伪代码运行在站控的实时任务里 def switch_matrix(gun_id, target_modules): # 1. 把该枪功率设定降到 0等待实际电流低于阈值 set_module_current(gun_id, 0) wait_until(lambda: read_actual_current(gun_id) 1.0, timeout2.0) # 2. 断开当前触点等待灭弧 open_contactors(gun_id, current_modules[gun_id]) time.sleep(0.05) # 3. 闭合目标触点等待接触器辅助触点反馈 close_contactors(gun_id, target_modules) wait_until(lambda: contactor_feedback(gun_id), timeout1.0) # 4. 记录本次切换供寿命统计使用 log_switch(gun_id, target_modules)这段逻辑的关键参数是timeout和灭弧等待时间不同继电器差异很大必须按器件手册设。切换计数要落库累计到器件额定操作次数的一定比例就提示更换。2.3 站控、车端和平台之间的协议分工站内分配算法要和两个外部协议打交道车端和服务端。车端方向ISO 15118 定义了电动汽车与充电站之间的通信协议国际标准其中 ISO 15118-2 走 PLC电力线载波配合 CCS 接口能做 Plug Charge 和双向能量传输协商ISO 15118-20 扩展到无线与更多功率等级。它最大的价值不是通信本身而是让车把电池 SOC预计出发时间可接受的最大功率告诉桩。站控拿到这些信息就能把功率优先分给着急走的车而不是简单地按先到先得。服务端方向是 OCPP。OCPP 1.6J 用SetChargingProfile下发TxProfile、TxDefaultProfile、ChargePointMaxProfileOCPP 2.0.1 把它统一成ChargingProfile并引入ChargingStationMaxProfile。三种 profile 的优先级从高到低是单次充电会话的 TxProfile、桩默认的 TxDefaultProfile、站级的 MaxProfile。站内动态分配算法生成的功率上限必须和这些 profile 取最小值否则平台侧的限制会被本地算法覆盖。{ connectorId: 1, csChargingProfiles: { chargingProfileId: 101, stackLevel: 2, chargingProfilePurpose: TxProfile, chargingProfileKind: Absolute, recurrencyKind: Daily, chargingSchedule: { chargingRateUnit: W, chargingSchedulePeriod: [ { startPeriod: 0, limit: 60000, numberPhases: 3 }, { startPeriod: 1800, limit: 120000, numberPhases: 3 } ] } } }这是平台侧下发的典型消息前 30 分钟限制 60kW之后放开到 120kW。站控本地再根据实时功率池和车辆 SOC 把这个 60kW 的天花板往下压。要特别注意的是stackLevel同一 purpose 下数值大的覆盖数值小的现场调不通十有八九是 stackLevel 撞了。提示ISO 15118 和 OCPP 是两条独立的链路前者管车与桩后者管桩与云。站内调度器同时订阅这两边的约束最终输出给功率模块的只有一个值。3. 用 Python 写一个可复现的动态功率分配调度器3.1 把分配问题写成带约束的优化式先形式化。设站内有 n 把枪在充电第 i 把枪的需求功率为 d_i由车端 SOC 曲线或用户设定决定功率池上限为 P_pool单枪最小功率 p_min低于这个值车辆 BMS 可能报错单枪额定上限 p_max_i。要求解的是向量 p (p_1, …, p_n)满足0 或 p_min ≤ p_i ≤ min(d_i, p_max_i)即与 0 之间不允许取中间值Σ p_i ≤ P_pool目标函数通常选最大最小公平最大化 min(p_i / d_i)也就是让每辆车拿到的需求满足率尽可能均衡这个目标函数比总功率最大化更符合现场体验。总功率最大化会把功率全给先来的车后到的车拿不到用户投诉最大最小公平则保证每辆车都至少能充上只是在快慢上有区别。另一种目标函数是加权最大最小公平权重给剩余时间紧张的车。权重可以由 SOC 和出发时间算出来w_i (1 - SOC_i) / max(T_depart_i - now, 0.1)。出发越近、SOC 越低的权重越大。3.2 最大最小公平分配的贪心实现下面是可直接跑的 Python 实现采用水位线法从 0 开始抬高公共满足率 water level直到总功率刚好用完。def fair_allocate(demand, weight, p_pool, p_min0.0, step1e-4): demand: 每把枪的需求功率列表 (kW) weight: 每把枪的权重列表越大越优先 p_pool: 站级功率池上限 (kW) p_min: 单枪最小可用功率 (kW)低于此值则分配 0 返回: 每把枪的分配功率列表 n len(demand) alloc [0.0] * n # 有效需求需求本身要大于最小功率否则该枪直接不参与 active [i for i in range(n) if demand[i] p_min] if not active: return alloc # 按需求从低到高排序逐个填平到下一个需求水位 sorted_idx sorted(active, keylambda i: demand[i] / weight[i]) remaining p_pool level 0.0 # 当前水位 (kW / 单位权重) allocated_cnt 0 for pos, i in enumerate(sorted_idx): cap demand[i] / weight[i] # 该枪能接受的水位上限 nxt cap # 假设水位从 level 抬到 nxt需要额外功率 need sum(weight[j] * (nxt - level) for j in sorted_idx[:pos 1]) if need remaining: remaining - need level nxt allocated_cnt pos 1 else: # 剩下的功率在这批之间按权重分 extra remaining / sum(weight[j] for j in sorted_idx[:pos 1]) level extra remaining 0.0 allocated_cnt pos 1 break for i in sorted_idx[:allocated_cnt]: alloc[i] weight[i] * level for i in active: alloc[i] min(alloc[i], demand[i]) # 低于最小功率的直接置 0同时把省下的功率不回收保持简单 for i in active: if alloc[i] p_min: alloc[i] 0.0 return alloc if __name__ __main__: d [120, 120, 90, 60, 80] # 五把枪的需求 w [1.0, 1.0, 1.2, 0.8, 1.0] # 3 号枪最着急 print(fair_allocate(d, w, p_pool240, p_min15))跑出来大约是[54.6, 54.6, 65.5, 43.7, 54.6]附近的结果3 号枪因为权重高拿到了更多。这段代码里p_pool是站级上限p_min是单枪最小可用功率weight是把 SOC 和出发时间耦合进来的地方。3.3 加上模块颗粒度、最小保持时间和切换惩罚上面算出的是连续值真实硬件上必须量化到 30kW 模块的整数倍而且要抑制抖动。做法分三层先量化再做迟滞最后做速率限幅。量化p_quant round(p / 30) * 30不足 30kW 的部分用一个独立的补充分配通道处理很多站会在每把枪里保留一个 1020kW 的低压辅助模块。迟滞只有当新目标与当前分配差超过阈值如 15kW才真正动作否则保持不变。最小保持时间一次切换后该枪的分配至少维持 60 秒避免继电器抖动。切换惩罚把切换次数作为惩罚项并入目标函数score 公平性损失 - λ * switch_countλ 按继电器寿命单价折算。参数建议取值说明调度周期1s与 OCPP 上报周期错开避免同时占用 CPU量化步长30kW 或 40kW等于单个功率模块额定值迟滞阈值15kW步长的 0.5 倍过小会频繁切换过大则响应迟钝最小保持时间60s低于此值不允许再次切换同一把枪平滑滤波EMAα0.3对需求侧抖动做平滑防止 SOC 跳变引起功率振荡池上限系数0.8×变压器容量预留站内辅助负载与损耗量化之后要重新检查总和是否超池如果量化后向上取整导致超额按权重从高到低把某些枪降一档。这一步不能省否则会在电表侧看到尖峰。4. 现场部署参数整定、数据链路和最容易踩的坑4.1 调度周期怎么定调度周期是现场最容易拍错脑袋的参数。定太短CPU 和总线压力大接触器来不及动作定太长车辆启动和退出时功率回补慢用户体验差。调度周期优点缺点适配场景200500ms响应快池利用率高CAN 负载高矩阵切换频繁无矩阵、纯降额式软件分配1s与电表采样周期匹配实现简单突变场景有延迟大多数公共快充站5s 以上总线轻、切换少车辆插拔后功率回补明显滞后慢充站、园区有序充电我的默认选择是 1 秒同时把车辆插枪/拔枪作为事件触发一次立即调度兼顾响应和稳定。4.2 电表与 BMS 数据链路分配算法的输入质量决定输出质量。站级总功率从进线多功能电表读走 Modbus TCP 或 RTU单枪实际输出从模块回传的 CAN 报文读车辆 SOC 优先从 ISO 15118 会话里取取不到就退化为按充电时长估算。from pymodbus.client import ModbusTcpClient import struct def read_grid_power(host192.168.1.10, unit1): 读取进线电表三相有功功率返回总 kW client ModbusTcpClient(host, port502, timeout1) if not client.connect(): raise ConnectionError(电表连接失败) # 常见电表用 0x0000 起的三相有功功率寄存器32 位浮点大端字序 rr client.read_holding_registers(address0x0000, count6, slaveunit) client.close() if rr.isError(): raise IOError(Modbus 读取错误) a, b, c (struct.unpack(f, struct.pack(HH, rr.registers[i], rr.registers[i1]))[0] for i in (0, 2, 4)) return a b c这里三个要点一是寄存器地址和字序一定对着电表手册改HH和HH搞反会读出天文数字二是采样要做滑动平均单次读数的尖峰不能直接喂给调度器三是 Modbus 读取失败要返回上一次有效值并打告警不能抛异常把整个调度线程搞崩。注意SOC 从车端取不到时不要直接按额定功率满额分配按充电时长 电压平台做个粗略估算更安全否则低压车会被大电流顶到保护。4.3 典型故障与排查路径现象可能原因排查动作某把枪始终分不到功率权重算出为 0、需求低于 p_min、接触器反馈异常打印该枪的 demand/weight/alloc 三元组查接触器辅助触点总功率超过变压器设定值量化后未重新校验、电表读数滞后加量化后二次校验电表滑窗取 3 次平均功率在相邻档位反复跳迟滞阈值设太小、需求侧抖动大加大迟滞阈值需求侧加 EMA 滤波OCPP 下发的限制不生效stackLevel 冲突、profile purpose 搞错抓 OCPP 报文确认 Rx/Tx profile 的 stackLevel 最大者车辆报 BMS 通信错误单枪功率低于 p_min 或电流变化率过大提高 p_min给分配值加变化率限幅变化率限幅这个点容易被忽略。BMS 对电流爬升速率有要求站控从 0 拉到 120kW 如果发生在 1 秒内部分车型会直接报错断充。做法是给输出加一阶限幅比如每秒不超过 30kW把alloc和目标值之间插一个斜坡。5. 用历史负载曲线回放验证分配策略到底行不行上线前最便宜的验证方式不是拉车来试而是拿站内历史充电记录做回放。导出一段时间的会话数据字段至少有到达时间、离开时间、需求功率曲线或 SOC 曲线、电池容量。然后按 1 秒或 10 秒步长推进每一步调用一次分配函数统计三个指标需求满足率实际分配/需求的时间积分比、平均排队等待时长、累计矩阵切换次数。import pandas as pd def replay(csv_path, alloc_func, p_pool240, dt10): df pd.read_csv(csv_path) # arrival, departure, demand_kw t, rows 0, [] active [] while t df.departure.max(): active [r for _, r in df.iterrows() if r.arrival t r.departure] demand [r.demand_kw for r in active] weight [1.0] * len(active) alloc alloc_func(demand, weight, p_pool) for r, a in zip(active, alloc): rows.append({t: t, gun: r.gun, demand: r.demand_kw, alloc: a}) t dt res pd.DataFrame(rows) sat res[alloc].sum() / res[demand].sum() # 需求满足率 switch (res.groupby(gun)[alloc].diff().abs() 0).sum() return {satisfaction: round(sat, 3), switches: int(switch)} print(replay(sessions.csv, fair_allocate))跑完一轮重点看满足率随池上限的变化曲线把p_pool从 240kW 调到 360kW如果满足率提升不足 5%说明瓶颈不在池容量而在需求本身或 p_min 设置过高。反过来如果切换次数随池容量上升而急剧增加就该回头调迟滞阈值和最小保持时间。真正上线时把回放结果和现场首周的实际电表数据做一次对账两者偏差超过 15% 就要查需求曲线是怎么算的——多数偏差来自 SOC 估算和 BMS 实际接受功率之间的差距。最后一步是把weight的计算参数作为配置项下发而不是硬编码在站控代码里这样换一个站点、换一批车型时只需要改配置不用重新烧录固件。本文还有配套的精品资源点击获取