简介:这是一份面向汽车技术、新能源汽车方向工程技术人员与高校师生的专业参考文献,聚焦电动汽车充电站功率平衡分配这一实际难题。资源围绕慢、快充电双车位互补的整体框架展开,讲解充电前准备与充电过程两阶段策略,并给出充电站控制系统、电池检测系统、人机交互界面与车位管理系统的结构设计,以及动态功率分配算法与新车加入分配算法的执行逻辑,读者可据此理解激励因子分档、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 级 | 30~360kW | 最高 | 高 | 大功率超充、公交场站 |
| 分布式 | 整桩级 | 桩额定值以下降额 | 低(轻载效率差) | 无 | 小区慢充、分散桩 |
| 混合式 | 30/60kW 级 | 60~240kW | 中高 | 中 | 城市公共快充站 |
选型时先算两个数:站内同时充电车辆数的期望峰值,以及变压器长期允许的负载率。前者决定你要不要矩阵,后者决定功率池上限设多少。我一般会把池上限按变压器容量的 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, timeout=2.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), timeout=1.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、ChargePointMaxProfile;OCPP 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_min=0.0, step=1e-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, key=lambda 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_pool=240, p_min=15))跑出来大约是[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 的部分用一个独立的"补充分配通道"处理(很多站会在每把枪里保留一个 10~20kW 的低压辅助模块)。 - 迟滞:只有当新目标与当前分配差超过阈值(如 15kW)才真正动作,否则保持不变。
- 最小保持时间:一次切换后,该枪的分配至少维持 60 秒,避免继电器抖动。
- 切换惩罚:把"切换次数"作为惩罚项并入目标函数,
score = 公平性损失 - λ * switch_count,λ 按继电器寿命单价折算。
| 参数 | 建议取值 | 说明 |
|---|---|---|
| 调度周期 | 1s | 与 OCPP 上报周期错开,避免同时占用 CPU |
| 量化步长 | 30kW 或 40kW | 等于单个功率模块额定值 |
| 迟滞阈值 | 15kW(步长的 0.5 倍) | 过小会频繁切换,过大则响应迟钝 |
| 最小保持时间 | 60s | 低于此值不允许再次切换同一把枪 |
| 平滑滤波 | EMA,α=0.3 | 对需求侧抖动做平滑,防止 SOC 跳变引起功率振荡 |
| 池上限系数 | 0.8×变压器容量 | 预留站内辅助负载与损耗 |
量化之后要重新检查总和是否超池:如果量化后向上取整导致超额,按权重从高到低把某些枪降一档。这一步不能省,否则会在电表侧看到尖峰。
4. 现场部署:参数整定、数据链路和最容易踩的坑
4.1 调度周期怎么定
调度周期是现场最容易拍错脑袋的参数。定太短,CPU 和总线压力大,接触器来不及动作;定太长,车辆启动和退出时功率回补慢,用户体验差。
| 调度周期 | 优点 | 缺点 | 适配场景 |
|---|---|---|---|
| 200~500ms | 响应快,池利用率高 | 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(host="192.168.1.10", unit=1): """读取进线电表三相有功功率,返回总 kW""" client = ModbusTcpClient(host, port=502, timeout=1) if not client.connect(): raise ConnectionError("电表连接失败") # 常见电表用 0x0000 起的三相有功功率寄存器,32 位浮点,大端字序 rr = client.read_holding_registers(address=0x0000, count=6, slave=unit) client.close() if rr.isError(): raise IOError("Modbus 读取错误") a, b, c = (struct.unpack(">f", struct.pack(">HH", rr.registers[i], rr.registers[i+1]))[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_pool=240, dt=10): 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的计算参数作为配置项下发,而不是硬编码在站控代码里,这样换一个站点、换一批车型时只需要改配置,不用重新烧录固件。
本文还有配套的精品资源,点击获取