做UDS诊断测试的工程师,每天少不了发那一帧02 10 03。这帧报文短得不能再短,但它的作用是把ECU从默认会话切到扩展会话,后面能不能读敏感数据、写标定参数、做例程控制,全都取决于这次切换有没有成功。UDS里的Session(会话)虽然只是Service 0x10下的一个子功能,但它是整条诊断链路的"总开关",很多看起来莫名其妙的负响应(NRC)最终都能追溯到会话状态不对。
我会结合自己在台架测试中真实跑过的报文和踩坑记录,把Session切换从头到尾讲透:ECU为什么要设计会话机制、0x10服务的请求/响应报文怎么解析、从默认会话切到扩展会话再进编程会话会经历什么,以及切换之后为什么会出现"看着成功、实则没权限"的怪事。刚接触UDS协议、在实验室跑诊断、或者正在写刷写脚本的朋友,这篇应该能省下你不少翻规格书的时间。
1. 一个测试工程师眼中的Session:这玩意解决的是权限与状态问题
刚碰UDS那会儿,我最不理解的就是为什么ECU这么"不信任人":想读个VIN码直接读就行,但想写一个标定参数或者执行一次例程,却要先"换个会话"。后来在台架上亲眼看到一次误刷事故才明白,会话机制本质上是ECU给自己设的一道权限门禁。
1.1 从"ECU不信任你"说起
ECU在车上连着CAN总线,任何挂在总线上的节点都能往总线上发报文。如果每个节点都能随意写Flash、改标定、执行自检,整个车辆系统就没什么安全性可言。会话机制做的第一件事就是限定:只有诊断仪(Tester)主动请求进入特定会话之后,ECU才开放对应权限。这就像机房的门禁系统,默认会话只让你隔着玻璃看设备状态,想进去操作必须刷卡换权限。
在ISO 14229-1里,诊断会话(Diagnostic Session)被定义为一个ECU内部运行的持续状态。不同的会话决定了当前允许使用哪些诊断服务、安全访问等级、通信参数(比如响应超时时间)。这种设计在外行人看来是"繁琐",但在工程上恰恰是防呆和防误操作的核心手段——默认会话下只能做读操作和部分基础服务,避免一个手滑把关键数据写坏;非默认会话才开放写操作、例程控制、刷写这些"危险动作"。
1.2 三种标准会话和它们能做什么
ISO 14229-1规定了三种标准会话:默认会话(Default Session,子功能0x01)、编程会话(Programming Session,子功能0x02)和扩展诊断会话(Extended Diagnostic Session,子功能0x03)。除了这三个之外,OEM还可以在0x40~0x7F范围内定义自己的会话,比如某些厂商的工厂模式会话、特殊安全等级会话。
三种标准会话的权限边界大致是这样的:
| 会话类型 | 子功能 | 主要用途 | 典型可用服务 |
|---|---|---|---|
| 默认会话 | 0x01 | 上电后的初始状态,基础诊断 | 0x10、0x19、0x22、0x3E等 |
| 编程会话 | 0x02 | Flash刷写、底层重编程 | 0x10、0x27、0x31、0x34、0x36、0x37等 |
| 扩展会话 | 0x03 | 标定、写参数、例程、高级诊断 | 0x10、0x27、0x2E、0x28、0x31等 |
注意这张表不是绝对严格的,具体能执行哪些服务还得看ECU的CDD(CANdela Diagnostic Description)文件或者诊断调查表。但有一个共同点:默认会话下几乎不含任何"写"和"例程"类服务,安全访问(0x27)在默认会话下也通常不允许执行。换句话说,不管你想刷写还是标定,第一步都是先把会话切到对应的非默认会话。
1.3 会话切换不是改一个标志位那么简单
从软件实现上看,会话切换会引发一串连锁反应:已解锁的安全等级被重置(标准推荐行为,具体看OEM实现)、S3定时器重新计时、通信参数可能切换(尤其进入编程会话后P2*会变)、DTC记录行为可能被暂停,甚至应用层的某个线程会被挂起。我在做刷写测试时发现,切到编程会话后,有些ECU连本身的周期报文都不发了,总线安静得像断电——这就是应用层任务被挂起的结果。
所以Session切换虽然只是0x10服务下的一个子功能,但它牵动的状态非常多。这也是为什么排查诊断问题时,第一条永远要确认"当前ECU到底在哪个会话"。
2. 把0x10服务的报文拆开看:请求、正响应、负响应各自的门道
光知道概念不够,报文才是诊断测试真正的"语言"。这一节把0x10服务的请求帧和响应帧逐字节拆开,方便你以后对着日志能一眼看出问题。
2.1 请求报文:一帧装满所有信息
在CAN/CAN FD上发0x10服务请求,报文结构很简洁。以标准CAN、物理寻址为例,假设诊断请求ID是0x7E0,ECU响应ID是0x7E8,从默认会话切到扩展会话的请求帧是:
CAN ID: 0x7E0 Data: 02 10 03 00 00 00 00 00逐个字节看:
0x02:PCI(Protocol Control Information),表示后续跟随2个数据字节;0x10:服务ID(SID),也就是DiagnosticSessionControl;0x03:子功能,目标会话ID,这里指扩展会话;- 后面5个字节补0,凑满标准CAN的8字节长度。
如果要切到编程会话,把子功能改成0x02即可:02 10 02 ...。如果ECU用的是CAN FD,PCI的编码规则会略有不同(单帧长度指示符换成了SF_DL),但服务字节的含义完全不变。
这里有个实际建议:会话切换请求最好用物理寻址发给具体ECU。虽然ISO标准里0x10服务理论上可以通过功能寻址(0x7DF)发送,但实际项目里OEM一般要求逐台控制。用功能寻址可能让总线上多个ECU同时切换会话,随后的常规通信直接乱套。我见过测试员图省事发广播请求,结果整个CAN网段的ECU全部切到编程会话,后面所有应用报文都停了,排查了半天才缓过来。
2.2 正响应:ECU告诉你"我切好了"以及新的时间参数
正常情况下,ECU会立刻回一帧正响应。比如从默认会话切到扩展会话后:
CAN ID: 0x7E8 Data: 06 50 03 00 32 01 F4 00逐字节解释:
0x06:PCI,表示后续6个数据字节;0x50:正响应SID,即0x10 + 0x40;0x03:回显子功能,确认当前已切到扩展会话;0x00 0x32:P2_Server_max,这里0x0032即50ms,表示本会话下ECU对一般诊断请求的响应时间上限;0x01 0xF4:P2*_Server_max,0x01F4即500ms,表示ECU处理较长任务时的"增强响应时间"上限。
P2和P2这两个参数很多人会忽略,但测试脚本里极其重要。后续每个诊断请求,如果超过P2还没收到正响应,才应该判定超时;如果只超过P2但还没超过P2*,ECU通常会先回一帧0x7F ... 0x78(Response Pending),告诉你"我再忙一下,别急"。而且这个参数不是ECU随便填的,不同会话差别很大。编程会话下P2*经常被设成5秒甚至更长,因为擦Flash确实很慢。
2.3 负响应:切换失败时ECU会说些什么
如果切换失败,ECU会返回负响应,格式是03 7F 10 NRC。其中0x7F是负响应服务ID,0x10是被拒绝的服务ID,最后一个字节是NRC。我把会话切换中最常碰到的几种NRC整理成了表:
| NRC | 含义 | 常见触发场景 | 排查方向 |
|---|---|---|---|
| 0x12 | 子功能不支持 | 发了OEM未定义的会话子功能 | 查CDD里0x10服务支持的会话列表 |
| 0x13 | 报文长度或格式错误 | 请求数据字节长度不对、PCI错误 | 检查PCI字节和服务数据长度 |
| 0x22 | 条件不满足 | 切换编程会话但电压超范围、总线负载过高、未满足前置条件 | 查切换前置条件,尤其是电源电压 |
| 0x78 | 响应待处理 | ECU正忙,内部任务尚未完成 | 等待P2*时间后重试或继续请求 |
其中0x22最值得注意。很多ECU切编程会话要求电源电压稳定在特定范围,台架上用低压电源供电就会出现"会话切不过去但又没报0x12"的怪现象,实际一查是条件不满足。这时候别急着改脚本,先看供电和点火信号是否正常。
3. 跟着报文走一遍:默认会话切扩展会话的完整时间线
理论知识说完了,来点真刀真枪的。我用CANoe环境演示一次最典型的切换:默认会话切到扩展会话,验证生效,再等S3超时观察ECU回跳。每一步都有报文和时间戳,你可以直接在台架上照着复现。
3.1 把测试环境准备好
我用的是CANoe加VN1640A接口卡,总线波特率500kbps,DUT是一块国产控制器。接线不复杂:CAN-H、CAN-L分别接到ECU对应的诊断CAN引脚,PC端打开一个CAN报文发送窗口,发送ID设为0x7E0,接收ID设为0x7E8。
没有CANoe也没关系,PCAN-View、Vehicle Spy或者诊断工具自带的"自定义发送"功能都能做同样的操作,关键是必须能看到报文和时间戳。启动前先确认ECU上电完成、总线处于静默状态。如果ECU已经在发应用报文,最好等它跑完启动流程,避免诊断会话切换和应用层初始化打架。
3.2 正常切换的报文时间线
下面是一段真实抓到的报文,时间戳是CANoe里的相对时间:
t=0.000s TX 0x7E0 02 10 03 00 00 00 00 00 t=0.012s RX 0x7E8 06 50 03 00 32 01 F4 00 t=0.015s TX 0x7E0 03 22 F1 A0 00 00 00 00 // 读取扩展会话专用DID t=0.028s RX 0x7E8 05 62 F1 A0 01 0C 00 00 // 读回数据,说明会话已生效第1帧是切换请求,第2帧是ECU确认。注意从请求到正响应只隔了12ms,小于P2=50ms,说明ECU处理很快。第3帧我读了一个厂商自定义的DID(F1A0,假设它是刷写计数这一类只有扩展会话才开放的数据),既然能正常读回数据,就证明扩展会话权限已经生效——这类DID在默认会话下通常会直接拒绝。
如果你用的是诊断工具而不是裸发报文,工具会在后台自动处理PCI和请求长度,你只需要选服务0x10、子功能0x03然后点发送。但对于写脚本调试,我反而建议直接看裸报文,因为能看到工具帮你隐藏的细节,比如时间间隔、P2参数变化。
3.3 关键时刻:停发请求,看S3定时器怎么把会话拉回默认
扩展会话切成功了,先别急着做别的。把诊断请求全部停掉,观察ECU的行为。这里有个新手容易误解的地方:ECU回默认会话时不会主动发任何提示,S3超时后它只是默默回到默认状态,你不发请求,它也不会开口。
所以正确的验证方式是:
t=0.000s TX 0x7E0 02 10 03 00 00 00 00 00 t=0.012s RX 0x7E8 06 50 03 00 32 01 F4 00 t=5.500s TX 0x7E0 03 22 F1 A0 00 00 00 00 // 超过S3默认5秒后读同一个DID t=5.512s RX 0x7E8 03 7F 22 22 00 00 00 00 // NRC 0x22,会话已回到默认这个现象在测试报告里很有价值:它证明了ECU的S3超时回默认逻辑是正常工作的。实际量产中,S3时间不一定正好是5秒,OEM可能设成3秒、10秒甚至更长,具体以CDD为准。但原则是一样的:非默认会话不能"躺平"太久,必须持续有请求来保活。
提示:S3计时从收到任意诊断请求(包括0x3E保活)后重新开始。如果测试中需要长时间停留非默认会话,务必周期发送0x3E,否则S3超时会让你"悄悄掉回默认会话"。
3.4 主动切回默认会话的正确姿势
超时回跳是被动行为,工程上更常用主动切换:直接发02 10 01,让ECU立刻回到默认会话。正响应和切换扩展会话类似,子功能回显0x01:
t=0.000s TX 0x7E0 02 10 01 00 00 00 00 00 t=0.010s RX 0x7E8 06 50 01 00 32 01 F4 00切回默认后,之前开放的写权限和例程权限会立即关闭,已解锁的安全等级一般也会被清除。有些ECU在切回默认时会做一次内部自检,响应时间会稍长,但通常不会超过P2*。如果你发现主动切回去偶尔收不到正响应,不要直接判定失败,等够P2*时间再看有没有0x78或者迟到的正响应。
4. 刷写场景里的会话切换:从默认切编程会话前后的隐藏变化
刷写(Flash Reprogramming)是Session切换最有存在感的场景。整车OTA、售后升级、产线下线刷写,都离不开那一句02 10 02。这一章我聊聊刷写流程中会话是怎么被编排的,以及切进编程会话后ECU身上悄悄发生的那些变化。
4.1 一次完整刷写里,Session是怎么被"编排"的
我拆过不少OEM的刷写流程,各家细节不同,但骨架高度一致:
- 预编程阶段:默认会话切到扩展会话(
02 10 03),做安全访问解锁(0x27)、读版本号、清DTC等准备工作; - 正式编程阶段:从扩展会话切到编程会话(
02 10 02),然后执行例程控制(0x31)擦除Flash、请求下载(0x34)、数据传输(0x36),最后请求传输退出(0x37); - 收尾阶段:发送0x11 ECU复位,让ECU以默认会话重新启动。
为什么不是从默认会话直接切编程会话,而是先切扩展会话再做预编程检查?原因有两个。第一,很多ECU的安全解锁逻辑只允许在扩展或编程会话下执行,从默认直接进编程会话可能绕不过安全校验;第二,扩展会话下可以先做"预编程检查"——电压检测、固件版本确认、DTC读取——全部通过后再切编程会话,如果前置检查失败就不入场,风险完全可控。
4.2 切进编程会话后,ECU身上发生的"隐藏变化"
很多人只看到02 10 02的正响应就以为万事大吉,其实ECU内部已经翻江倒海:
- 应用层任务挂起:正常行驶相关的控制报文、状态报文可能停止发送,总线明显安静;
- DTC记录暂停:编程期间通常不记录新故障,防止刷写过程自身干扰误报;
- 通信参数切换:P2/P2*往往比扩展会话更宽松,因为Flash操作确实慢,响应超时判断要用新参数;
- 安全访问状态可能保留也可能被重置:ISO标准里诊断会话切换通常会重置安全等级,但不同OEM实现有差异,有的切进编程会话后需要重新做0x27解锁,有的会保留之前的解锁等级。务必以规格书为准。
我在一次台架测试中遇到过比较特殊的现象:ECU切到编程会话后,应用报文确实停了,但DTC状态位里多了一个"编程会话已激活"的故障码。一开始我以为是异常,后来翻厂商协议才知道是故意设计的——用DTC状态位标记刷写过程,回头排查问题能看清刷到哪一步。所以"编程会话DTC完全不更新"这种说法在具体ECU上不一定成立,得看OEM实现。
4.3 一段典型的刷写切换序列,可直接参考
下面这段CAPL脚本模拟刷写前段序列,API名称以你用的诊断工程为准,重点看Session切换的位置:
// 预编程:默认 -> 扩展 DiagRequest_DiagnosticSessionControl.SetSubFunction(0x03); DiagSendRequest(DiagRequest_DiagnosticSessionControl); // 安全访问:假设seed/key算法是pass-through DiagRequest_SecurityAccess.SetSubFunction(0x01); DiagSendRequest(DiagRequest_SecurityAccess); // 收到seed后,用0x02子功能发送计算好的key DiagRequest_SecurityAccess.SetSubFunction(0x02); DiagRequest_SecurityAccess.SetIdent(0x03, key); DiagSendRequest(DiagRequest_SecurityAccess); // 正式编程:扩展 -> 编程 DiagRequest_DiagnosticSessionControl.SetSubFunction(0x02); DiagSendRequest(DiagRequest_DiagnosticSessionControl); // 后面就是0x31擦除、0x34请求下载、0x36传数据...这段脚本看着不难,但我实际调试时栽过跟头:切到编程会话后,如果某两步请求间隔超过了S3时间,ECU会退回默认会话,后续的0x34请求下载就会连续报NRC 0x22,整个刷写直接崩掉。解决办法是两条路:要么在0x34/0x36/0x37之间插入0x3E保活请求,要么通过0x10子功能参数(如果OEM支持)把S3定时器调长,确保刷写过程中不"掉会话"。
4.4 刷写完别忘了复位
刷写结束后,一般会发0x11 ECU复位(子功能0x01表示硬复位,0x03表示下电再上电),让ECU从编程会话回到默认会话。有的ECU也支持直接用0x10 01切回默认,但刷完固件后强烈建议复位——因为新版本的应用程序要重启才能生效,而且编程会话下有些资源没有释放干净,不重启直接切默认可能存在隐患。
5. 切了会话却没生效?三个我实际遇过的坑
会话切换看起来就一帧报文,但恰恰是这一帧引出的问题最多。我挑三个自己踩过的坑,按"现象、原因、解决"讲明白。
5.1 坑一:S3超时悄悄回默认,后续操作连环NRC 0x22
有一次给客户做标定测试,脚本逻辑是先切扩展会话,再做0x2E写参数。单独跑一条用例没问题,但放到一整晚的长测里,第二天一看日志,凌晨3点之后全是0x2E被NRC 0x22拒绝。查了挺久才发现问题:切完扩展会话后有一处下载标定数据的操作比较耗时,超过了几秒——正好踩中ECU的S3超时,ECU回到默认会话,后续写操作自然全部被拒。
解决方法是:在长间隔操作前主动发一次02 10 03重新确认会话,或者用03 3E 80周期性保活。更稳妥的做法是写自动化脚本时,在每次关键操作前加一个"检查当前会话"的步骤——读一个只有非默认会话才能读的数据,如果失败就重新切会话再继续。这个做法比盲目重试可靠得多。
5.2 坑二:会话切了,但安全访问没做,照样没权限
还有一次,同事在台架上给ECU切到扩展会话,然后直接发0x31例程控制执行某个自检,结果收到NRC 0x33(securityAccessDenied)。他很奇怪:会话都切了,怎么还没权限?
这里要理清一个概念:会话切换只解决"ECU允许你进入某个诊断模式"的问题,安全访问(0x27)解决的是"ECU确认你是有权限执行特定动作的操作者"的问题。会话是通道,安全访问是钥匙。扩展会话下,读部分数据可以不解锁,但执行例程、写标定数据这类"危险操作"通常必须先做0x27安全访问。如果CDD里要求"扩展会话+安全等级=某级"才算完整权限,那会话切换只是第一步。
这种坑的隐蔽之处在于:有的服务在未解锁时也会返回正响应,等真正执行时才在自定义NRC里告诉你权限不足。所以测试用例里的"前置条件"必须写清楚:不仅写Session=Extended,还要写SecurityLevel=xxx,两者缺一不可。
5.3 坑三:OEM自定义会话子功能把标准流程打乱
标准会话只有0x01/0x02/0x03,但OEM经常加私货。比如某供应商把0x40定义为"工厂模式会话",权限模型和标准会话完全不同,有的甚至不要求标准的安全访问流程就能进入。问题往往出在测试脚本复用上:我原来拿标准UDS用例直接跑,脚本里写死了"切到0x03就能做0x31",但某台ECU的CDD里0x31只在0x40下开放。结果就是切换正响应正常,后续服务全部给你NRC 0x12或者0x31,莫名其妙。
最坑的是,这种权限映射不写在ISO标准里,全在CDD/ODX文件里,不翻文件根本猜不到。所以拿到新ECU的第一件事,永远是用诊断工具读一遍CDD,把会话-服务权限矩阵整理出来,再动脚本。我后来给自己定了个规矩:凡是支持自定义会话的ECU,测试脚本里不硬编码会话ID,全部从配置文件读取。
5.4 排查会话问题的通用思路
如果遇到"会话相关服务不工作",我习惯按下面顺序排查:
- 看最近一次成功/失败的0x10切换时间戳,确认当前会话状态;
- 看有没有S3超时,相邻两次诊断请求的时间差是否超过了ECU的S3时间;
- 看安全访问状态,是否需要重新解锁;
- 看CDD里的会话-服务权限矩阵,当前会话是否真的开放目标服务;
- 看P2/P2*参数,你的超时判断是否合理。
这套思路用了好几年,基本没失手过。很多"玄学"问题到最后都落在会话状态或安全状态这两个点上。
6. 一点测试经验:会话切换相关的工具配置和脚本套路
最后一节说点实际操作层面的东西,包括工具的隐藏设置和一个能直接用的保活脚本骨架。
6.1 工具配置里容易忽略的三个地方
CANoe、CANape、PCAN这类工具发送诊断请求时都有一些"贴心"功能,但用不好反而会掩盖真实问题:
- 自动等待正响应:很多工具默认发送请求后会等待一段时间,如果这个等待时间小于ECU的P2*,真正慢的响应会被工具抢先判定为超时,造成测试误报。建议把响应等待时间配置成大于ECU最大P2*,一般设3秒比较稳;
- 自动切换会话:有些诊断工具有"切换到非默认会话"的快捷按钮,点一下确实能切,但不会提醒你操作完要切回默认,有的甚至不在日志里记录这次切换。写报告时没有报文佐证,会很被动;
- 自动保活:部分工具可以周期发送0x3E,长时间标定测试时这个功能很实用。但如果保活请求本身没被记录进日志,后面查S3超时就会缺少证据。我一般会关掉工具的自动保活,改用脚本显式发送,让日志完整可追溯。
6.2 一个可复用的会话保活脚本骨架
下面这段CAPL脚本的逻辑是:先切到扩展会话,确认成功后进入保活循环,直到测试结束。你可以在CANoe里新建一个CAPL节点,把这段代码塞进去改改诊断对象名就能跑。
variables { msTimer tKeepAlive; int sessionActive = 0; } on start { setTimer(tKeepAlive, 50); // 50ms后先发切换请求 } on timer tKeepAlive { if (sessionActive == 0) { // 发送 0x10 03 切到扩展会话 DiagRequest_DiagnosticSessionControl.SetSubFunction(0x03); DiagSendRequest(DiagRequest_DiagnosticSessionControl); sessionActive = 1; } else { // 周期发送 0x3E 80,抑制正响应并保持会话 DiagRequest_TesterPresent.SetSubFunction(0x80); DiagSendRequest(DiagRequest_TesterPresent); } setTimer(tKeepAlive, 1000); // 1秒间隔保活 } on diagResponse DiagResp_DiagnosticSessionControl { // 切换失败时重置状态,下个周期会重试 if (diagResp_DiagnosticSessionControl.GetResult() != 0) { sessionActive = 0; } }这段脚本比较糙,当脚手架用完全足够。实际工程里我会把保活间隔设成S3时间的一半,比如S3=5秒,就每2.5秒发一次0x3E;如果S3不可控,就每1秒发一次,最保险。注意0x3E子功能0x80表示"抑制正响应",ECU只接收请求不回正响应,能有效减少总线报文数量;如果你希望每次确认ECU还活着,可以发0x3E 00让ECU回应。
6.3 写测试用例时的三个习惯
最后分享几个工作习惯。第一,测试用例的"前置条件"里永远写清楚两个值:Session=xxxx和SecurityLevel=xxxx,否则用例在不同ECU之间移植时立刻翻车。第二,凡是涉及会话切换的用例,断言里必须包含"S3超时后回到默认会话"这一条,这是ECU诊断状态机的基本行为,不测等于没测。第三,测试报告里附报文时间线时,至少截三段:切进会话的那一帧、中间操作的一帧、切回默认或者超时后的那一帧。别人审报告能直接看懂你的操作序列,不用回头翻原始log。
从我自己的体会来说,会话切换在UDS诊断体系里的位置,远比刚入行时以为的重要。它是一根总闸,连着权限、定时器、通信参数、DTC行为和安全等级,任何一根线没理清,后续操作都可能卡壳。遇到那种"明明切成功了一切却没反应"的问题,我习惯把供电电压、总线状态、CDD权限矩阵三样东西一起拉出来看,十有八九答案就藏在这三样里。