简介:ISO 15118-20-2022是国际标准化组织于2022年4月发布的车辆与电网通信接口标准,重点界定第二代网络层和应用层技术要求,为电动汽车充电场景提供统一通信框架。内容面向充电设施研发、测试验证、标准合规及学术研究等场景的工程师与技术人员,可用于充电通信协议设计、互操作性评估与安全风险分析。包体为一份PDF标准全文,共1个文件,大小约34.74MB,目录章节完整,涵盖范围、规范性引用文件、术语定义、网络层与应用层需求、XML架构示意等内容,便于离线检索与打印。目前已有157人学习下载,适合需要系统掌握充电通信细节或跟进国际标准更新的读者。标准内容具体涉及身份验证、计费、充电状态监控等应用层服务,以及数据路由、错误检测等网络层机制;同时关联互操作性、安全性、充电效率提升与智能电网支持等重要方向,研读该标准可获得从底层通信到上层业务的系统性认知,为实际项目开发、协议测试或标准符合性认证提供直接依据。
1. 把车插上充电桩的那一刻,手里的协议栈藏了多少坑
做电动汽车充电通信的工程师大概都有过这种经历:车连上桩,屏幕上已经显示“握手成功”,可后台日志里全是SupportedAppProtocol超时,或者TLS握手在证书链还没发完时就断了。我起初以为车辆与电网通信接口的网络协议只是把旧版15118-2的消息定义换个Schema,等真正把2022年4月发布的这一版“第二代网络层和应用层要求”逐条读完,才发现端口、超时、EXI压缩、证书链、消息时序全都不能照抄旧代码。这份标准解决的是电动汽车充电场景下,车端EVCC和桩端SECC从“发现彼此”到“完成充电会话”的完整通信问题,适合正在写充电协议栈、做充电桩主控固件、或要对接互操作性测试的工程师:新手按章节顺序读,熟手直接看参数边界和避坑清单。
2. 标准骨架:从OSI七层视角看ISO 15118-20到底规定了什么
ISO 15118-20之所以叫“Part 20”,是因为它把网络层和应用层作为独立卷来写,区别于Part 2第一代标准。整份标准的正文可以拆成两大部分:第7章描述通信基础要求,第8章描述应用层消息定义。做实现的时候,第7章决定你这套协议栈跑在什么端口、用什么安全机制;第8章决定你把消息解析成什么结构、按什么顺序发。
2.1 网络层与传输层:端口号、超时参数与地址协商
标准在第7.6、7.7节里给出了网络层和传输层的要求。V2G通信在两代标准里都建立在IP之上,物理层通常走以太网、WLAN或者电力线通信,网络层固定用IPv4或IPv6,但15118-20里对IP地址协商有明确约定:车端通过SECC Discovery Protocol去找桩端,而不是自己硬填一个网关地址。
SDP是整个会话的“开胃菜”。我一般会把SDP做成一个独立模块,它跑在UDP之上,发送方广播一个Discovery请求,桩端收到后回一个包含SECC IP和端口号的响应。常见做法是UDP端口沿用15118,这个端口在充电行业里用得很成熟。拿到SECC地址之后,EVCC再发起TCP连接,V2GTP(V2G传输协议)的默认目标端口是841,TLS加密之后走8443。三层之间严格分层:SDP负责发现、TCP负责可靠传输、V2GTP负责在TCP流上切分出一个个消息帧。
这里最容易被忽略的是超时参数。标准不会给你一个并列的“总超时表”,而是在不同会话阶段分别定义。我第一版协议栈踩过的坑是:把TCP连接超时设成了10秒,桩端那边却是5秒,结果高并发充电时车端还在等TCP的SYN-ACK,桩端已经把连接释放了。后来我们统一为:TCP连接建立5秒、TLS握手8秒、应用层响应等待5秒,这些值再拿到互操作性测试环境里去微调。
常见默认参数如下表,按你的协议栈实际资源可以调整。
| 参数项 | 常见默认值 | 说明 |
|---|---|---|
| SDP端口 | UDP 15118 | 车端广播,桩端响应 |
| V2GTP明文端口 | TCP 841 | EVCC到SECC |
| V2GTP加密端口 | TCP 8443 | TLS承载 |
| TCP连接建立超时 | 5s | 超过即释放连接 |
| TLS握手超时 | 8s | 证书链交换可能较慢 |
| 应用层响应超时 | 5s | 按消息类型可细分 |
注意:端口号并非在这份标准里写成一个醒目的常量表,而是散落在各层描述中。上面这组值是行业内沿用下来的常见配置,接入新硬件时一定先和对方确认端口清单。
关于IP地址协商,IPv6场景下需要处理链路本地地址,车端要从SDP响应里解析SECC的IPv6地址,这和IPv4的处理逻辑完全是两条分支。别把IPv4的地址解析代码直接复用过去,IPv6地址字段长度、端口拼接方式、地址作用域ID都不一样,我在项目里为此单独写过一段地址归一化函数,专门处理这两种格式。
2.2 安全层:TLS、证书链与根证书数量的硬约束
第7.3节是整份标准里改动最大的一节,它规定了安全概念、证书与密钥管理、根证书数量和有效期、TLS支持方式、防火墙、密钥保护以及随机数生成。对做协议栈的人来说,这节内容直接决定你需不需要接一套PKI,而不是“可选项”。
首先,TLS是强制性的。应用层消息不允许明文跑在充电会话里,EVCC和SECC之间必须先建立TLS通道再交换V2G消息。协议栈最低要支持TLS 1.2,我建议直接按TLS 1.2颗粒度去配置密码套件,不要依赖系统默认套件,因为很多桩端主控板的系统库默认套件列表很长,里面混着老旧的CBC类套件,兼容性测试时反而容易出问题。
证书管理上,15118-20要求车端安装至少一个受信任的根证书,桩端也有自己的证书链,两者在TLS握手阶段完成双向认证。这里有个容易被忽略的约束:标准对根证书数量做了明确限制,因为车端和桩端存储空间都有限,而且每次TLS握手都会把证书链全量发出去,根证书越多,握手包越大,握手超时风险越高。我见过某实验室的互操作性文档里写支持6个根证书,但实际测试CA轮换时只装了两个根,第三个根就导致桩端内存溢出。后来我们按“生产环境最多3个根、TLS握手前先校验链长”来做,才稳定下来。常见做法是:证书链深度控制在3到4层,根证书轮换时新旧根并存不超过一个轮换周期。
随机数生成也写进了标准,这对做嵌入式的同行是个坑:主控板上如果直接用库自带的rand(),在TLS握手里生成随机数时会因为熵不足导致握手失败。标准把这部分作为安全要求提出来,意味着你要么接硬件随机数发生器,要么至少用系统级的熵池接口,不能为了省事用伪随机。
2.3 表示层与应用层:EXI压缩和supportedAppProtocol
第7.9和第7.10节定义了表示层和应用层的基础。15118-20里应用层消息的编码方式从XML换成了EXI,这是一个非常关键的变化。XML消息换行加标签,一条ServiceDiscovery请求动辄好几KB,在充电这种低带宽场景里不划算。EXI把XML Schema编译成文法表,再对消息做二进制编码,压缩比通常能到10倍以上。
但EXI有个实现成本:车端和桩端必须基于同一份Schema文件编译EXI文法,双方文法表不一致,解码就会错位。我第一版实现踩过一次:直接从网上找了一个EXI库,用默认参数去解桩端发来的消息,结果浮点字段全错。原因是15118-20的Schema里有很多带小数位约束的数字类型,EXI库的浮点精度选项没对齐。后来我在编解码模块里强制指定了浮点编码精度和字符串编码方式,并且把Schema版本号写进产品配置里,才解决了这个问题。
应用层入口是supportedAppProtocol,它不是一个普通消息,而是一个版本协商机制。EVCC在TLS建立之后发出的第一条应用层消息就是它,里面携带自己支持的协议版本列表;SECC从中选择双方都能支持的最高版本返回。这里注意:版本协商成功之后,整个会话都按这个版本走,不允许中途再切版本。我见过有人把supportedAppProtocol做成每帧都发,这是对协议的误解,它只在会话最开始发一次。
3. 把规范读成实现:一条DC充电会话的消息时序拆解
第8章定义了应用层消息的完整集合,从握手到会话结束大概有十几个消息对。对于做协议栈的人,最有效的读法不是按消息字典顺序背,而是按一条真实充电会话的时序走一遍。
3.1 会话建立:从TCP到SupportedAppProtocol再到SessionSetup
一条完整的DC充电会话,消息顺序大致如下:
- SDP发现:EVCC广播,SECC响应
- TCP连接建立:EVCC连接SECC的V2GTP端口
- TLS握手:双向证书认证
- SupportedAppProtocol.req / res:协商协议版本
- SessionSetup.req / res:建立V2G会话,分配SessionID
- ServiceDiscovery.req / res:获取桩端支持的充电服务
- ServiceSelection.req / res:选择DC服务或AC服务
- ChargeParameterDiscovery.req / res:交换电压、电流、电池容量等充电参数
- CableCheck:检查DC充电电缆连接状态,直流特有
- PreCharge、PowerDelivery:进入充电状态
- SessionStop.req / res:结束会话并断开连接
这个时序里,第4步和第5步最容易出错。SupportedAppProtocol的req里有一个参数叫协议版本列表,每个版本包含版本号、优先级、Schema哈希。SECC会拿自己支持的版本做匹配,优先选优先级最高的那个。很多协议栈在实现时只比较版本号大小,把优先级字段忽略了,导致双方都选了一个对方其实没编译好的Schema版本。
SessionSetup.req里带有EVCC的标识信息,SECC收到后分配一个8字节的SessionID,这个ID会用于之后所有消息的关联。SessionID在消息头里出现,协议栈要做的是把它从交互式上下文里提取出来,而不是每次都重新生成。我见过有同事把SessionID当成普通随机数,在每一条请求里都重新生成,结果桩端一直返回FAILED_UnknownSession。
还有一个时序上的细节:15118-20的DC和AC充电,消息交换要和IEC 61851-1的控制导引状态机配合。表现在代码里就是:PWM占空比变化了,V2G消息才能往下走;如果占空比还没稳定,消息到了桩端会被判定为时序错误。我一般会在状态机入口加一个“等待物理信号稳定”的守护条件,再放行应用层消息。
3.2 服务发现与充电参数协商:AC、DC和WPT的分支逻辑
ServiceDiscovery返回的是桩端支持的充电服务列表。服务类别里至少有AC、DC、WPT这几类,在15118-20里还有VAS增值服务类别。EVCC根据自己的物理接口类型决定选哪个服务。这里容易翻车的点是:AC和DC用的服务参数完全不一样,不能用同一个结构体去解析。
我通常会为AC、DC、WPT分别建一套消息参数上下文,避免互相污染。比如DC充电需要交换CableCheck阶段的电压检测结果、PreCharge阶段的电压变化率;AC充电关注的是电流限值和相位信息;WPT充电还要处理对位精度和功率传输效率。如果混在一个结构体里,后面扩展时字段冲突会很难查。
ChargeParameterDiscovery是决定充电能不能开始的关键消息。EVCC在req里带上电池的额定容量、当前SOC、最大允许电压电流;SECC返回的res里给出输出能力、最大功率,还有充电过程中的电压电流目标值。这里的字段精度非常高,电压值可能到小数点后三位,EXI解码时精度不对,会让桩端计算出错误的充电曲线。我建议在协议栈里为这些参数单独定义带精度的数值类型,不要直接用浮点类型一路裸传。
3.3 消息头与响应码:实现MessageHandler时最容易忽略的字段
V2G消息在TCP流上是靠V2GTP帧切分的,V2GTP头包含协议版本、报文类型、长度。每个V2G消息还有一个统一的消息头,里面至少包含SessionID和签名信息。我实现消息分发时,会先解析V2GTP头拿到消息长度,再解析V2G消息头拿到SessionID和上下文ID,最后才把消息体丢给业务逻辑。
如果要用代码验一下消息头解析逻辑,我一般会写一个最小骨架来跑测试用例:
import struct def parse_v2gtp_header(payload: bytes): # V2GTP header: 8 bytes # byte0: protocol version, byte1: message type # byte2-3: reserved, byte4-7: payload length version, msg_type = payload[0], payload[1] length = struct.unpack(">I", payload[4:8])[0] return version, msg_type, length def parse_v2g_message_header(body: bytes): # 简化示例:SessionID 为 8 字节,MessageID 为 2 字节 session_id = body[0:8] message_id = struct.unpack(">H", body[8:10])[0] return session_id.hex(), message_id这段代码的逻辑说明和参数含义:V2GTP头的8字节里,前两个字节分别是协议版本和报文类型,中间两个字节是保留字段,最后四个字节是后续数据的长度。我一般会先用这个骨架去验证厂商的抓包文件,看版本号和长度字段读得对不对,再决定要不要往下接业务层。V2G消息头的前8字节是SessionID,之后是MessageID和保留位,不同消息类型在这里区分请求和响应。测试时如果发现session_id解析出来不是装配时的值,优先检查字节序,标准里网络字节序是大端,代码里的“>H”就表示按大端读无符号短整型。
提示:EXI编解码器必须和Schema版本绑定,升级Schema时先跑一遍固定消息的编解码回归,再放量联调,否则浮点字段错位这类问题很难查。
4. 协议栈落地避坑:五个让互操作性测试翻车的真实问题
互操作性测试和单机自测完全是两回事。单机自测时车端桩端都是自家代码,问题好定位;一旦把两家的协议栈接在一起,超时、编码、证书任何一个不对都能让整个会话卡死。下面这五个问题是我在项目里真实踩过的,按“现象→原因→解决”记录。
4.1 现象:TCP连接一直建立不起来,日志里只有SYN重传
原因基本都在超时参数和端口上。常见的有三种:SDP发现成功后EVCC拿到的IP地址过期;SDP返回的端口和实际监听端口不一致;TCP连接超时太短,桩端还在处理上一条会话的TLS释放,连接就被车端放弃了。
解决:在EVCC侧把SDP结果缓存时间缩短,每次充电前强制重新发现一次。同时把桩端监听端口和防火墙放行规则固定成同一份配置表,避免“代码里写841,防火墙放行8443”这种不一致。TCP连接超时我统一按5秒调,桩端和车端保持一致。
4.2 现象:EXI解码后浮点字段全部偏差,报文内容看起来像乱码
原因:EXI Schema版本不一致,或者EXI库的浮点编码选项没对齐。15118-20的Schema里数字类型很多,EXI的浮点数编码有严格的精度控制,双方如果用了不同Schema编译参数,解出来的字节和原始值会对不上。
解决:把Schema文件纳入配置管理,车端和桩端用同一个Schema哈希做校验。我在代码里强制指定了EXI解码器的浮点精度选项为“与Schema一致”,并且把编码端和解码端做成同一个库,避免一端用A库、一端用B库。测试时先用Schema里一条固定消息做编解码回归,确认浮点字段一致后再跑完整流程。
4.3 现象:TLS握手成功,但应用层消息发过去之后桩端直接断开连接
原因:TLS密码套件虽然握手通过了,但应用层消息在传输时使用的加密模式不匹配。15118-20要求使用AEAD类密码套件,如果车端用了老式的CBC套件,桩端虽然兼容握手,但在解密时发现报文认证失败,就会直接断开。
解决:协议栈里固定TLS密码套件列表,只保留AEAD类(比如GCM模式),并把配置文件里系统默认的CBC套件全部排除。这个配置要同时写入EVCC和SECC两侧,不能只在桩端做。
4.4 现象:AC、DC切换后状态机错乱,桩端一直返回FAILED_SequenceError
原因:AC和DC两套时序共用了同一个状态机,切换服务时没有重置充电参数上下文。标准里对AC、DC、WPT的时序是有区分的,共用状态机在ServiceSelection切换后会残留上一服务的状态。
解决:我重构为按服务类型分别建立状态机实例,ServiceSelection结束时就完成上下文切换。现在AC切DC或者DC切WPT,各走各的状态机,不再共享会话状态。
4.5 现象:响应码总是FAILED_SequenceError,但消息本身解析没问题
原因:消息顺序和业务时序对不上,或者消息头里的SessionID和当前会话的SessionID不匹配。这类错误在自测时很少出现,因为自测时消息顺序自己能控制;但在双方联调时,只要有一方上一条响应还没回完、另一方就发了下一条请求,就会出现时序错误。
解决:在协议栈的应用层加一个消息序列表,校验每条消息的上文是否完整。我设计了一个简单的期望消息映射,当前状态只接受规定的那几个消息类型,其它类型一律回报FAILED_SequenceError并记录到日志。互操作性测试时这个表就是调试地图,哪一步报错直接看表里的前后两条消息就能定位是谁先越界了。
5. 进阶验证:用抓包和方法论快速定位协议栈问题
互操作性测试中最有效的工具还是抓包。把车端和桩端之间的流量镜像出来,用Wireshark过滤V2GTP和TLS,基本能覆盖八成以上的问题。
5.1 Wireshark过滤与关键字段定位
抓包过滤器我一般这样写:
tcp.port == 841 || tcp.port == 8443 || udp.port == 15118这个过滤表达式的逻辑说明:841是V2GTP明文端口、8443是TLS加密端口、15118是SDP的UDP发现端口。先看SDP能不能通,再判断TCP和TLS层级是否正常。遇到TLS加密后的V2G消息,如果Wireshark支持TLS密钥日志,可以导入密钥文件把V2G消息也解出来,否则就只能看到加密流。
定位问题时不要从头看到尾。先看会话建立阶段有没有TLS Alert,再看SupportedAppProtocol处于哪一帧,最后看响应码。响应码本身就是标准定义的状态机指针,一个FAILED_SequenceError和一个FAILED_UnknownSession对应的是完全不同的故障域,比看一堆十六进制快得多。
5.2 区分规范性的Mandatory与参考性的Informative附录
15118-20的附录分成两类:带“normative”字样的是必须实现的内容,带“informative”的只是参考信息。新手最常犯的错是把附录里ACDP的实现细节当成必做项,或者把Basic PPD这类互操作性参考内容当成协议要求做成产品功能,结果和桩端一对接就互相不认。我的习惯是:写代码前先把附录清单圈出来,normative的全量实现,informative的只做阅读理解,不做实现依据。
还有一个小技巧:每轮互操作性测试之后,把对方的响应码和消息序列导出来,和标准第8章的消息序列图逐一比对。消息序列图虽然被列在正文后面,但它才是判断“谁先越界”的最终依据。我后来养成的习惯是:每次联调前,先用SDP加TCP加TLS跑一遍连通性测试,确认链路没问题了再进应用层;只要链路层有问题,绝不带着它去调消息时序。从那以后,我每次新项目启动都强制走一遍这个步骤,希望帮到你。
本文还有配套的精品资源,点击获取