☰
UDS 36服务(TransferData)实战拆解:从ISO-TP到ECU刷写全流程
2026/9/25 5:00:15 网站建设 项目流程

做UDS开发这几年,我觉得有一个服务很容易被轻视,但实际工程里几乎每次刷写都离不开它,那就是36服务(TransferData,数据传输)。很多人把刷写流程挂在嘴边,聊起来都是34请求下载、37退出传输,可真正在总线上把几十万字节固件一块一块“喂”给ECU的,恰恰是36服务。它不像19服务读DTC那样可以静态分析,也不像31服务那样只要发一个命令就完事,它拼的是机制理解、时序控制和异常处理。

这篇文章我想把36服务拆开讲透,包括它的数据块组织方式、与ISO-TP传输层的联动、在一次完整刷写流程里的实际协作,以及那些文档里不会写的超时参数和踩坑点。适合正在做UDS诊断协议栈、刷写工具或者ECU底层Bootloader的朋友,也适合刚接触诊断协议、想搞懂36服务为什么这么设计的测试工程师。

1. 36服务在诊断刷写链路中的角色定位

1.1 为什么刷写流程的核心是36服务

一次完整的UDS刷写,通常由一串服务串联起来:10服务切换会话,27服务做安全访问,34服务声明要写入的内存地址和长度,36服务真正搬运数据,37服务收尾,最后还可能用11服务复位ECU。整套流程里,34服务干的是“签约”的活,36服务才是“履约”的那个人。

我把这组服务的关系类比成快递柜投递:34服务告诉柜子“我准备投一个包裹,目标格口是0x00080000,包裹大小是256KB,分块投递,每个块最多256字节”;36服务则一次一次地把包裹内容塞进格口,每个块带上序号,等柜子确认;37服务最后说“包裹投完了,关闭格口”。如果没有36服务,34和37之间就是空的,固件数据根本进不了ECU的Flash。

所以看一个刷写工具或者Bootloader实现得专不专业,关键就看36服务这段:数据块怎么分包、要不要等流控帧、块序号怎么维护、超时时间怎么设。这些细节一旦处理不好,轻则刷写超时重试,重则ECU进入不可恢复的编程异常状态。

1.2 36服务的数据块组织方式

36服务最核心的机制是“分块确认”。假设一次刷写要传输128KB固件,ECU在34响应中给出了支持的最大块长度(maxNumberOfBlockLength),比如256字节。那Tester就要把整个固件切成若干块,每块单独发一个36请求,并且按顺序等待ECU逐块确认。

每个36请求里必须携带两个关键信息:块序号(blockSequenceCounter,后面简称BS)和数据参数(dataParameter)。块序号从0x01开始,每发一块就加1,到达0xFF后回绕到0x01继续循环。ECU通过这个序号来检查数据是否按顺序到达,如果同一个序号收到两次,或者序号跳变,ECU通常会回复一个负码拒绝接收。

这里有一个非常容易误解的点:34响应里协商的maxNumberOfBlockLength,是指一个36请求里“数据参数”的最大字节数,并不包含SID(0x36)和BS这两个字节。我见过不少新手拿这个块长度去算CAN报文总长度,结果每个请求都多算了3个字节,正好卡在ISO-TP多帧边界上,导致效率降低甚至触发ECU长度校验失败。实际计算时,36请求的整体长度 = 2字节(SID+BS) + 数据块长度。

1.3 ISO-TP传输层对36服务的直接约束

36服务跑在ISO-TP(ISO 15765-2)之上,所以它的真实传输效率要看底层用的是经典CAN还是CAN FD。经典CAN单帧最多8字节,去掉ISO-TP的PCI(协议控制信息)1字节后,一个单帧最多承载7字节诊断消息;CAN FD单帧可以达到64字节,有效载荷宽裕很多。

如果固件数据块长度超过单帧承载能力,ISO-TP就要把36请求拆成多帧发送:首帧(FF)带着整个诊断消息的总长度,连续帧(CF)按顺序把剩余数据发过去。接收方通过流控帧(FC)告诉发送方“我能收多少、多久收一次”。这就是为什么刷写时经常能看到大量连续帧喷涌而出,那通常就是一条36请求被拆成的多个CAN帧。

所以36服务的数据传输机制,实际上有两层嵌套的分块逻辑:上层是36服务的“块”(由BS管理),下层是ISO-TP的“帧”(由流控管理)。这两层经常被混在一起说,但它们的职责完全不同——36服务的块对应的是业务逻辑上的数据段,ISO-TP的帧对应的是物理总线上的传输单元。

2. 36服务请求与响应的逐字节拆解

2.1 请求报文格式:从SID到数据参数的编排

36服务的请求格式非常简洁,标准定义如下:

字节1:SID,固定为0x36 字节2:blockSequenceCounter(BS),块序号 字节3-末尾:dataParameter[],本块携带的固件数据

举个例子,如果34响应中的最大块长度是256字节,那么第一个36请求会是:

36 01 <256字节数据>

第二个请求:

36 02 <256字节数据>

以此类推。当BS到达0xFF时,下一块回绕为0x01。这里有个隐藏要求:在整个传输过程中,除了最后一个块可以小于协商的块长度,其余每个块的dataParameter长度都应尽量等于协商的最大块长度。如果ECU配置得严格一点,前面某个块长度不满,也可能直接报0x31(requestOutOfRange)。

实际写协议栈的时候,我习惯把BS的初始化和自增逻辑单独封装成一个接口,因为刷写一个1MB的固件可能要发几千次36请求,手写自增代码很容易在0xFF回绕处栽跟头。正确做法是定义一个发送函数,每次调用时先取当前BS发送,发送成功后BS自增,如果大于0xFF就重新赋值为0x01。

2.2 响应格式与正码条件

ECU对36服务请求的正码响应格式也很简单:

字节1:SID + 0x40,即0x76 字节2:blockSequenceCounter,表示下一次期望收到的块序号

也就是说,Tester发36 01,如果ECU正常接收并完成了Flash写入,会回76 02,意思是“下一个请发02”。这个设计其实兼作确认和握手机制:Tester可以拿响应里的BS和本地自增的BS做对比,如果发现ECU回的是76 03而本地发的是76 02的下一个36 02,说明中途出现异常,要么是网络丢帧,要么是ECU侧逻辑跳块了。

需要注意,ECU返回0x76并不代表数据一定已经写入非易失存储。很多Bootloader的实现是先把数据存到RAM缓冲区,等收到37服务或者某个31服务触发“编程校验”时,才真正把整个Buffer刷入Flash。所以正码只代表“这一块我收下了且校验通过”,不代表“已经固化”。

2.3 负码响应:哪些NRC会在36服务中出现

36服务可能遇到的负码不算多,但在工程现场命中率很高,我把它们整理成一张速查表:

NRC名称触发场景
0x13incorrectMessageLengthOrInvalidFormat请求长度与格式不符,例如dataParameter长度超过协商块长度
0x22conditionsNotCorrectECU当前条件不满足传输要求,比如发动机转速过高、KL15掉电、安全锁定状态
0x31requestOutOfRangeBS序号跳变、内存地址或长度超出下载范围
0x33securityAccessDenied未通过27服务安全访问,或安全访问已超时失效
0x36serviceNotSupportedInActiveSession当前诊断会话不允许执行36服务(比如默认会话)
0x37requestSequenceError请求顺序错误,典型场景是没发34直接发36
0x72generalProgrammingFailure一般编程失败,Flash写入异常、Buffer溢出等
0x78requestCorrectlyReceived-ResponsePending请求已收到,需要更多处理时间,稍后会回最终响应

这里最容易被忽略的是0x22。很多ECU对刷写条件做了约束,比如必须满足充电状态、必须挂P挡、必须在一定时间窗口内完成。如果Tester在刷写前没有把这些条件搞定,36服务就会一直撞0x22。这种情况下不是协议栈的问题,而是整车上位策略的问题,需要从诊断之外去排查。

3. 实战:走一遍完整的36服务刷写流程

3.1 握手阶段:会话切换与安全解锁

实际刷写之前,Tester和ECU之间要先建立“可编程”的环境。我把这个阶段的操作拆成几个标准步骤:

第一步切换诊断会话。发送10 02请求进入编程会话(Programming Session),ECU回50 02。这一步的作用是让ECU从正常报文处理模式切换到刷写模式,很多动态控制报文在这个会话下会停止发送,避免刷写过程中出现电驱、刹车等控制器误动作。

第二步做安全访问。发送27 01请求种子,ECU回67 01带上种子值;Tester用厂商算法计算密钥,发送27 02,ECU校验通过后回67 02。如果没有这一步,后面的36服务大概率会收到0x33。安全访问的种子和密钥算法不同厂商差异很大,有的用AES,有的用自定义多项式,但测试时要保证刷写工具和ECU侧算法版本一致,否则就是“种子拿到了,密钥对不上”的尴尬局面。

第三步是检查刷写前置条件。部分ECU要求Tester先发31服务(例程控制)来检查ECU是否满足电压、温度、整车状态等条件。虽然这一步不是UDS标准强制项,但整车厂的刷写规范里基本都会加,建议不要省略。

3.2 传输阶段:34协商与36循环发送

条件就绪后,Tester先发送34服务声明下载意图。一个典型的34请求是:

Tester -> ECU: 34 00 44 00 08 00 00 00 00 04 00 00

拆解一下:0x34是SID;0x00是dataFormatIdentifier(通常为0);0x44表示“4字节地址 + 4字节长度”;后面的地址是0x00080000,长度是0x00040000(256KB)。ECU收到后,如果接受这个下载请求,会回一个74响应:

ECU -> Tester: 74 00 20 01 00

0x20表示“最大块长度用2字节表示”,0x0100即256字节。也就是告诉Tester:你就按每块256字节给我发。

接下来进入36服务的循环发送环节。假设固件是256KB,按256字节一块,总共要发1024块。每块都需要一个独立的36请求:

Tester -> ECU: 36 01 <第1块256字节> ECU -> Tester: 76 02 Tester -> ECU: 36 02 <第2块256字节> ECU -> Tester: 76 03 ... Tester -> ECU: 36 00 FF <这里只假设一个很小长度的示例,实际最后一块长度可能不足256字节>

由于一块256字节的数据在经典CAN上必然走多帧,总线上看到的现象会是:一条36请求被拆成首帧和若干连续帧,发送完后等待ECU的76响应。从总线上抓包看,就是一个一个的“多帧包组 + 单帧响应”组合。

这里有一个经典的分包效率优化点:如果ECU支持CAN FD,36请求可以承载更大的数据块,推荐的典型块长度可以到4096字节甚至更多。块长度变大之后,同样大小的固件可以减少36请求次数,刷写时间大幅缩短。实际取值要结合ECU的RAM缓冲区和Flash写入粒度来定,不是越大越好——ECU要先把整块数据放入RAM缓冲区,如果RAM不够,块太久照样写不了。

3.3 收尾阶段:37退出传输与ECU复位

当最后一个36请求发送完成,ECU回正码后,Tester发送37服务:

Tester -> ECU: 37 00

请求退出数据传输。ECU收到后回77 00,表示整个传输阶段结束。这个动作很重要,因为ECU需要关闭下载通道,释放缓冲区,并进入“可校验”状态。

接下来的校验动作通常在37之后做,不归36管但和它强相关:有的ECU会要求Tester发送31服务请求执行Flash块校验,比如31 01 02 02 00这类例程,校验通过后ECU才算真正完成了编程。校验通过后,Tester一般会发11服务(ECU复位),使ECU退出编程会话并重启到应用程序。如果校验失败,这时候还能重新走34和36流程再刷一遍,不用断电。

4. 刷写链路中的时间参数与超时控制

4.1 影响36服务的关键计时器:P2/P2*/S3

搞懂36服务的时序,比搞懂它的报文格式更值钱。UDS协议里有几个计时器直接决定刷写的成败:

计时器默认值作用
P2Server通常50msECU处理一次请求并回复的最大时间
P2*Server通常5000ms如果处理时间超出P2,ECU先回0x78,之后在P2*内回复最终响应
S3Server通常5000ms会话超时时间,任何服务都不发,超时后返回默认会话

在36服务的刷写过程中,最常遇到的是P2超时。ECU收到一块数据后,需要执行Flash写入,这个操作可能耗时几十毫秒到几百毫秒不等,超过P2很正常。规范的ECU实现会在P2内先回0x78占住总线,让Tester知道“我正在写,别急”,等Flash写完后在P2内回真正的76响应。

这里有个测试工具层面的坑:很多工具默认P2*超时时间很短,比如只有500ms,但刷写ECU时Flash编程时间恰好在500ms边缘。结果就是ECU还在写Flash,Tester已经判定超时,然后重发36请求,两个请求撞在一起,ECU报0x31或0x37,刷写直接失败。所以刷写工具的超时配置一定要开放可调,至少留到5秒以上。

另外要注意S3的作用:诊断会话是有寿命的,如果Tester在两个诊断请求之间间隔超过S3时间,ECU会回到默认会话,安全访问立即失效。此时再发36就会收到0x36(服务不支持当前会话)或者0x33(安全访问被拒绝)。刷写大文件时如果传输比较慢,需要防范这种情况,可以在刷写期间周期性发送一些轻量服务比如3E(TesterPresent)保持会话,但千万别和36请求抢总线时序——最安全的做法是确保按规划的传输周期推进,不要人为加长间隔。

4.2 多帧连续帧间隔与流控帧的关系

36请求在多帧发送时,连续帧之间的时间间隔由ECU下发的流控帧决定。ISO-TP流控帧里有几个参数:BlockSize(BS)、STmin(Separation Time minimum,连续帧的最小间隔值)。

如果STmin设的是0,理论上Tester只要总线空闲就可以连续猛发,但实际会让ECU接收缓冲区瞬间被打满,尤其是在经典CAN上运行速度不太高的场景。如果STmin设得偏大,比如10ms,那么一条36请求如果有40个连续帧,光发送就要400ms,刷写速度明显变慢。

实践中的经验是,Tester侧要充分尊重ECU的流控参数,但也要在工具里提供“延迟注入”能力,因为有些ECU对流控帧STmin的实际处理有Bug,收得太快丢帧,收得太慢又超时。做兼容性测试时,我会把STmin的最小值、常见值(0、1ms、2ms、5ms)都跑一遍,观察哪组参数最稳定。

4.3 大文件刷写时的分包策略与进度管理

处理大文件时,建议在传输开始前对固件做一次规划和计数。比如一个1MB的固件,每块256字节,一共4096块。发送进度可以用“当前块序号/总块数”来计算。由于块序号会回绕,不能单纯看BS大小来算进度,必须在Tester侧维护一个独立的计数器,BS只用来做链路同步,不要拿来做业务进度统计。

分包时还需要处理最后一块。如果固件长度刚好是块长度的整数倍,最后一块仍然是满块;如果不是,最后一块只包含剩余字节。部分ECU要求最后一块即使不满,也必须填满到协商块长度,不足部分用0xFF填充。具体是否需要填充,看ECU的刷写规范和Bootloader实现,不要想当然。我在实际项目中就遇到过一家供应商对最后一块不满长度的情况直接报0x72,补了填充字节之后问题消失。

另外,重传逻辑一定要做细。36服务不是面向连接的可靠传输,底层总线丢帧是难以完全避免的。如果Tester发了36请求但没有收到任何响应,常见的策略是重发同一个BS的36请求一次。但如果重发后收到0x31或0x37,说明ECU内部状态已经前进或出错了,不能盲目继续重发,应该中止刷写流程,通过19服务读故障码或者读取ECU状态来确定下一步。

5. 常见问题排查与踩坑记录

5.1 典型负码与实战场景对照

工作中我整理过一张36服务疑难杂症的对照表,后来调试刷写工具时帮了大忙。

现象可能原因排查方向
发送36立即收到0x33未做安全访问或安全状态超时检查27服务种子密钥流程,检查会话是否还处于编程会话
发送36立即收到0x36当前会话不是允许刷写的会话排查会话切换是否成功,检查ECU是否支持该服务
发送36收到0x37没有先发34,或34状态被重置检查是否漏发34,或ECU刚复位过
发送36收到0x31块长度不匹配、地址越界、BS跳变核对34协商地址和长度、块序号连续性
发送36长时间无响应P2*超时配置太短,或ECU卡死延长P2*时间,抓包看是否有0x78
收到0x72Flash编程失败或Buffer异常读取ECU日志/DTC,检查供电、电压稳定性

最典型的场景是“安全访问已过,但刷写打到一半忽然开始报0x33”。这通常不是安全访问算法问题,而是S3超时导致ECU已经退出编程会话,安全状态被清除。此时回看时间线,多半是两次36请求之间隔了超过5秒。

5.2 四个容易踩的坑

第一个坑是BS回绕误判。块序号到0xFF后回绕到0x01,如果Tester侧把BS当成单调递增的密钥或进度值,就会出错。特别是在日志分析里,看到36 FF之后又出现36 01,不要急着认为是序号跳变,先算一下总块数是否超过255块。

第二个坑是34块长度协商后没有遵守。有些工具在34响应里读到256字节块长,但实现时分包器写死了512字节,结果ECU每次收到前两个36请求正常,第三个开始报0x31。排查时先看34响应中的maxNumberOfBlockLength到底是多少,再看看工具实际每个36请求多长。别用“大概”来判断,直接抓包数长度。

第三个坑是0x78处理不当。有的自定义诊断仪把0x78当作异常负码直接显示“请求失败”,实际上它是“正在处理”。正确的处理是收到0x78后重置内部定时器,等待后续最终响应,而不是立即判定失败并发重传。

第四个坑是核心下载地址和长度格式。34服务的addressAndLengthFormatIdentifier不是随便写的,前半个字节表示地址长度字节数,后半个字节表示长度字段字节数。0x44表示地址和长度都是4字节。如果底层ECU实际上用的是0x24(2字节地址+4字节长度),Tester按4字节地址去组包,地址直接错位,36发出去自然越界。做多车型兼容时务必把这张格式表做成配置项。

5.3 排查工具箱与实战思路

排查36服务问题,最基本的工具是一台能抓CAN/CAN FD报文的分析仪,加上一个能解析UDS/ISO-TP的软件。市面上常用的有CANoe、PCAN-Explorer,也有不少开源的Wireshark配合CAN接口方案。重点不是工具本身,而是抓包时的“对照法”:同时抓取Tester侧和ECU侧的总线报文,对比同一时刻两侧对36请求的处理。

我个人的习惯是每跑一次刷写就存一份完整日志,按时间戳把34、36、37、31、11的服务交互过程画成时间线。如果某个36请求没有收到任何响应,就在时间线上往前找,看上一个服务是否成功,是否出现过0x78,是否有总线错误帧。大多数情况下,问题都能在时间线的前后几百毫秒内找到踪迹。

还有一个小技巧:在测试阶段,把36请求的数据区前几个字节打上一个固定模式,比如每块的前4字节写入该块的偏移值。ECU侧如果支持,可以在写入异常时回读这些标记字节,快速判断出是哪一块数据出错、偏移是不是对得上。这个做法在定位Flash写入错误时特别好用。

6. 安全视角:36服务面临的威胁与防御思路

6.1 为什么36服务是攻击者的重点目标

36服务在诊断协议里算是“高危接口”,因为它的本质是任意数据写入能力。如果攻击者绕过了安全访问,就可以直接向ECU内存中写入数据,这不仅仅是修改个参数的问题,甚至可能覆盖Bootloader区或者植入恶意固件。

常见的攻击入口有两个:一个是物理接口,攻击者直接接到OBD诊断口上,对总线发起诊断请求;另一个是远程攻击链,比如通过车机联网模块、蓝牙接口或无线诊断口拿到总线访问权限后,向ECU发起刷写。后者在现代智能网联汽车上越来越被重视。

所以看待36服务的安全问题,不能光看协议层,还要看整车网络架构的纵深防御。哪怕27服务安全算法再强,如果攻击者能直接在总线上伪造诊断仪身份并且成功解锁,那后面就是一路畅通。这也是为什么很多新平台把刷写钥匙从简单的种子密钥升级成带数字签名、带防重放机制的安全凭证。

6.2 常见攻击思路与防御手段

从攻击链角度看,和36服务密切相关的威胁主要有这几类:

攻击方式攻击行为典型防御手段
未授权刷写跳过27安全访问直接发36强制安全访问、会话状态与安全状态绑定
重放攻击抓取合法36数据流后重放使用加密传输或防重放计数器,每次刷写种子随机化
数据篡改修改36服务中的数据区固件整体签名,ECU写入后校验签名
降级攻击刷写旧版本固件版本号防回滚,一次性OTP记录最低版本
拒绝服务高频发送36请求干扰ECU速率限制、异常诊断请求检测

防御不能只压在ECU端。我在实际做安全方案时,习惯把防御分成三层:第一层是通道防护,如果ECU支持SecOC或安全诊断通道,36服务优先跑在加密通道上;第二层是访问控制,安全访问的密钥算法要够强,种子长度不能太短,重试次数要做限制;第三层是刷写校验,不是ECU回一个76就算数,必须在37之后跑完整的Bootloader校验和固件签名验证。

另外,整车层面的入侵检测系统(IDS)也开始关注36服务流量,比如在非刷写售后模式下检测到高频36请求、在默认会话下出现36请求、或者安全访问失败后紧接着出现36请求,都应该作为异常诊断事件上报。这些规则不需要很复杂,但对识别早期攻击行为很有用。

从我个人做协议栈和刷写诊断的经验来说,36服务最大的难点不在代码量,在于把它放到整个刷写时序里看:什么时候发34、什么时候等0x78、什么时候允许超时重试、什么时候必须无条件中断,这些边界条件才是真正考验一个诊断工程师功底的地方。调试时多留日志、多抓包、多复现异常场景,比背标准条文管用得多。希望这篇实战拆解能让你在下次排查36服务问题时少走几个弯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询