UDS 36服务数据传输机制详解:刷写流程与NRC排查要点

发布时间:2026/9/25 4:16:06
UDS 36服务数据传输机制详解:刷写流程与NRC排查要点
搞UDS诊断这些年我发现一个很有意思的现象新手拿到诊断规范最先想调通的往往是22读数据、2E写数据和19读故障码因为它们的报文短、逻辑直白在测试工具上发一帧就出结果特别有成就感。可真到了刷写Flash Programming项目上36服务TransferData数据传输才是那只绕不开的拦路虎。一整个刷写流程里36服务是调用次数最多、单次耗时最长、也是最容易出“幺蛾子”的环节。这篇文章我想把36服务的数据传输机制从头到尾扒一遍报文结构、块序列计数器、与34/37服务的时序配合、超时重发处理、NRC排查思路再聊聊这个服务在安全层面为什么是重点攻击对象。内容同时面向三类人正在写诊断上位机或者刷写工具的软件工程师需要排查刷写失败问题做ECU端UDS协议栈开发、需要把34/36/37状态机做严谨的嵌入式工程师以及对整车诊断安全、防篡改有兴趣的测试人员。不同基础的朋友可以按需跳读但建议你至少把第3章块序列计数器和第6章故障排查看完这两个地方是我在实际项目中踩坑最多、也是很多UDS资料里讲得最浅的部分。1. 36服务到底解决了什么问题把它放进刷写链路里看1.1 刷写的“三段式”里36服务是搬砖主力标准UDS刷写流程可以粗略拆成三个阶段前置准备、数据搬运、后置验证。前置准备切换诊断会话比如10 03进入扩展会话、安全访问27服务、擦除Flash的例程控制31服务等目的是把ECU调整到“可以收数据”的状态。数据搬运请求下载34服务确认传输范围和能力传输数据36服务把镜像数据一块块送进去请求退出传输37服务告诉ECU“数据发完了”。后置验证通过例程控制做完整性校验、程序依赖检查再通过19服务读取相关故障码确认刷写状态。36服务承担的就是第二个阶段里最枯燥但最关键的“搬砖”工作。可以打个比方34服务是跟仓库管理员说“等会儿有一批货要进来总共多大体积、放在哪个区域”36服务是那辆反复来回运货的卡车一趟一趟把货搬进仓库37服务是搬完后通知管理员封仓、核对数量。刷写几百KB甚至几MB的固件上位机要循环调用几十次、几百次36服务每次都带一小块数据。1.2 36服务与22/2E、19、31的本质区别很多刚接触UDS的朋友容易混淆36服务和2E服务。2E写数据是按DID数据标识符往某个“配置项”里写通常用于保存VIN码、配置参数这类结构化数据长度不会太大。36服务则是面向连续内存空间的流式写入它更接近“把某一个镜像按地址连续铺进Flash”的语义协议栈内部往往直接对应Flash驱动接口。19服务读故障码、31服务例程控制本身不承担大批量数据传输任务。特别是在产线刷写和OTA刷写场景里可能出现这样的组合31服务负责擦除、校验、执行跳转36服务负责传输两者交替使用。正因为36服务在整个刷写链路里处于“高频执行”的位置它的稳定性和协议栈设计的严谨程度直接决定了刷写成功率。2. 36服务报文格式逐字节拆解请求、正响应与负响应全景图2.1 请求报文结构36服务的请求报文格式非常简洁就三段字节序号名称说明1SID固定为0x362blockSequenceCounter块序列计数器合法范围12553NtransferRequestParameter实际要写入的数据块内容这里没有任何子功能参数所以36服务不走sub-function那套NRC逻辑。核心字段就是第二个字节块序列计数器简称BSC以及第三个字节开始的数据块。数据块的最大长度没有统一硬性规定而是由ECU能力决定。很多ECU在34服务正响应里会返回一个maxNumberOfBlockLength告诉测试仪“你每次调用36服务最多发多少字节”。常见的单块长度有64字节、256字节、512字节、1024字节也有能做到4095字节的。一个需要注意的细节是虽然ISO 14229-1允许较大的数据块但实际ECU往往会根据自身RAM缓冲、Flash页大小来限制。你在上位机里把块大小设成4096如果ECU最大只支持1024对方就可能回NRC 0x13消息长度错误或者直接忽略多余数据。所以不要想当然地发大块数据一定要以34服务正响应里的maxNumberOfBlockLength为准。2.2 正响应与负响应全景36服务正响应格式如下字节序号名称值1SID0x760x36 0x402blockSequenceCounter回显请求里的块序列计数器正响应里回显BSC是为了让测试仪确认“我发的那一块被正确接受了”。如果是负响应格式就是常见的0x7F 0x36 NRC。我给一个实际工作中最常用的NRC速查表NRC含义常见触发场景0x13消息长度错误/格式无效数据块长度超出ECU支持范围或请求参数缺失0x22条件不正确不在编程会话、安全访问未通过、外部条件不满足0x31请求超出范围数据内容与34服务声明的地址/长度不一致或BSC为非法值0x33安全访问被拒绝没有先通过27服务鉴权0x72一般编程失败Flash写入失败、硬件异常、编程电压异常等0x73块序列计数器错误收到的BSC不是ECU期望的下一个序号0x74请求顺序错误没有先执行34服务或传输状态机未初始化0x78响应待处理ECU需要更多时间处理请稍后重发完全相同的请求这里我想特别提一句0x78不是一个失败响应它是“先别急我正在干活你待会儿把同一个请求再发一遍”的通知。很多测试仪对这个NRC处理得不好要么直接判超时退出要么错误地递增BSC后再重发结果把本来能成功的刷写搞失败。后面第6章我会用真实场景展开讲这个问题。2.3 关于0x00块序列计数器的实现差异协议标准里BSC的取值范围是12550被当作非法值。不同协议栈对“非法值0”的负响应选择不完全一样有的返回0x13因为消息格式不对有的返回0x31因为请求超出范围有的返回0x73因为计数器不是期望值。这属于实现差异测试仪端最好把这几类NRC都当成“这一块数据没有被接受”来处理不要写死只认某一种。3. 块序列计数器从1循环到FF为什么偏偏不能碰03.1 计数器背后的可靠传输思想UDS诊断请求在传输层上不是一个“连接”测试仪发完一帧ECU处理完再回一帧响应两边并没有像TCP那样维护一个持续握手的会话状态。ISO-TP做了报文分片和重组但它只保证一帧完整报文能收发应用层并不知道上一个请求到底有没有被成功处理。这种情况下怎么防止报文重复、丢失、乱序导致的数据写错位置答案就是块序列计数器。你在发第一块数据时BSC要从1开始也有OEM规定从0x20开始但0依然是禁区。每成功发送一块下一次就加1。到0xFF之后会回卷到0x01而不是0。ECU端维护一个期望值当前传了一块期望的就是上一块加1。如果收到的BSC不是期望值ECU直接回NRC 0x73并把这一块数据丢掉不做写入。可以这么理解TCP用序列号保证字节流不乱序36服务的BSC就是一个简化版的应用层序列号。它看似简单却是整个数据传输可靠性的最后一道保险。3.2 为什么不能用0做回卷值很多刚写协议栈的人会不自觉地把计数器写成“0到255循环”发完0xFF下一个发0x00。这事在项目里会出大问题的。0x00在标准语境里被保留通常用来表示“传输状态未初始化”或“当前不在活动传输会话中”。如果ECU端收到0x00很容易判定为非法请求即便某些实现允许0x00进到数据处理逻辑也会跟测试仪端的初始状态判断产生歧义。最稳妥的写法就是回卷时从0xFF转到0x01整个传输过程中永远不出现0x00。我见过有同事在调试时把计数器写成unsigned char自增溢出之后变成0结果刷写了256块之后必现失败查了很久才反应过来是回卷值的问题。3.3 超过255块怎么办一个很实际的问题如果镜像很大会不会出现“块数超过255”会。假设每块传输4KB一个1MB的镜像需要256块正好超过255触发一次回卷。如果每块1KB一个512KB的镜像就有512块回卷两次。所以“BSC只有8位够不够用”这种担心是多余的协议设计里就允许回卷关键在于测试仪和ECU都必须严格按“0xFF后接0x01”处理。做上位机的同学这里最容易犯的错就是我上面说的“自增溢出变成0”或者用了带符号类型在0x80之后变成负数导致后续所有判断全错。3.4 计数器对重放和重试的影响计数器另一个作用是防止同一个数据块被意外重复提交。假设测试仪发送块5成功ECU也写入成功但响应帧在总线上丢了。测试仪超时后如果直接重发“块5”ECU一看期望值是6会回NRC 0x73。这个机制在正常场景下是保护但同时也意味着测试仪不能简单用“超时重发同一条请求”的策略来处理36服务。正确做法是记录已确认成功的BSC超时后要么根据业务语义决定是否重发当前块要么重新发起34服务把传输状态机复位到已知状态。后面第6章我会给一个更完整的排查思路。4. 34到36再到37完整时序与一次真实刷写请求过程剖析4.1 一次典型刷写的通信序列我直接给一个精简但真实的通信序列方便你对照总线日志理解测试仪发送10 03ECU回复50 03切换到扩展会话。测试仪发送27 01ECU返回67 01 种子测试仪计算密钥后发送27 02 密钥ECU返回67 02安全访问通过。测试仪发送31 01 xx yy擦除例程ECU可能先回7F 31 78擦除完成后再回71 01 xx yy。测试仪发送34 00 44 4字节起始地址 4字节长度ECU返回74 02 maxNumberOfBlockLength。测试仪依据maxNumberOfBlockLength把镜像切块循环发送36 01 data、36 02 data……每收到76 01、76 02后发下一块。所有数据发送完毕测试仪发送37ECU返回77。测试仪发送31 01 xx zz做完整性校验例程ECU返回71 01 xx zz。这里36服务和31服务的关系值得注意。有的刷写流程会先做擦除再做传输有的会在传输过程中穿插“页写入确认”之类的例程还有的ECU在做完36服务后必须马上做某类检验程序否则不会真正把最后一块数据落盘。所以不要死板认定36服务只能出现在一个固定位置要以OEM刷写规范为准。4.2 34服务正响应里的maxNumberOfBlockLength我在实际项目里反复强调一点上位机拿到34正响应后第一件事就是解析maxNumberOfBlockLength用它来约束后续36服务的块大小。常见实现里这个字段占2字节比如ECU回74 02 10 00就表示最大块长度是0x1000也就是4096字节。如果上位机忽略这个值按自己写死的块大小发数据很容易触发NRC 0x13或者0x31。更麻烦的是有些ECU在34正响应里返回的最大块长度是动态的比如擦除模式下可用RAM变小能收的数据块就变小。所以每次刷写都重新解析不要缓存上一次的值。4.3 一个容易被忽视的边界最后一包整个镜像长度不一定是块大小的整数倍所以最后一包36服务携带的数据长度会小于maxNumberOfBlockLength。这是完全合法的ECU应当能正确处理短数据块。但这里有两个常见坑有的上位机在拼包时如果最后一块长度不足会补0xFF凑整。补进去的0xFF会被ECU当成有效数据写入Flash导致镜像校验和不匹配。刷完看起来成功但功能异常或者校验失败。有的ECU协议栈实现不严谨对短块也要求长度必须等于声明的块大小否则回0x13。这种情况只能通过和ECU开发确认或者看规范里对短数据的处理约定。4.4 请求下载的地址与长度要精确匹配34服务请求里包含memoryAddress和memorySize。测试仪在链路日志里经常只关注34服务成功了却忘了核对地址范围和实际镜像大小。比如34服务声明“从0x10000开始共1MB”但36服务实际传输过程中数据总量超过了1MBECU就会收尾时发现长度不对通常在37服务之后校验失败或者传输中途回0x31。我的习惯是上位机侧在启动刷写前把镜像文件长度和34服务里的memorySize做一次严格比较。二者不一致就果断中止不要心存侥幸继续往下走。这一条能省掉大量定位时间。5. 传输参数选择与超时控制刷写性能与稳定性的平衡点5.1 单块大小怎么定单块36服务的数据量是刷写性能的第一决定因素。块越大总帧数越少交互次数越少吞吐率越高。但块太大会带来几个问题ECU的RAM缓冲可能放不下触发长度错误。单个请求在CAN总线上占用时间长中间只要有一帧连续帧丢失整个ISO-TP报文就重组失败ECU等待超时测试仪也得整体重发。总线高负载持续太久可能影响其他网络节点。所以实际项目中我通常不会把单块数据压到ECU最大长度而是留20%左右的余量。如果一个ECU最大支持4096字节我会优先测试4096但发现连续帧偶尔丢失时会降到2048。性能不是唯一指标稳定性才是。特别在OTA远程刷写场景下网络波动更大块大小反而要保守一些。5.2 ISO-TP流控参数对36服务的影响在CAN上跑UDS36服务的超长请求会被ISO-TP拆成多帧。经典CAN每帧有效载荷只有8字节其中连续帧的头字节要被PCI占用实际每帧只能传7字节数据。如果单块4KB大约需要580多帧连续帧这个过程中流控参数BSBlockSize和STmin直接决定ECU能不能及时接收。BS表示发送方连续发送多少个连续帧后要等待接收方一个流控帧。BS越大发送方越激进。STmin表示连续帧之间的最小间隔单位通常是毫秒。ECU端如果Flash接口跟不上但BS设得很大、STmin设得很小总线侧就会丢帧。反过来STmin设太大刷写时间线性增长用户不可接受。这个参数没有万能值必须结合具体ECU的接收缓冲大小、处理速度和总线波特率来调。我之前调过一个项目STmin从0x0A1ms改成0x131.9ms后刷写时间只增加了几秒但连续帧丢失率从千分之几直接降到零。5.3 P2与P2*的配合UDS标准里有P2和P2两个时间概念P2是服务器正常响应时间P2是服务器处理长时间程序时的扩展响应时间。刷写过程中ECU写Flash可能超过P2时间这时ECU应该先回一个0x78让测试仪知道自己没有死。测试仪收到0x78后需要等P2*时间然后把和刚才完全相同的36服务请求重新发一遍。这个“完全相同”包含了BSC和数据块。有些工具实现不好在重发时把BSC加了1ECU马上回0x73或者把块内容换了ECU写入错误数据。这是刷写工具开发里非常典型的错误。5.4 关于总线时间预算如果要做产线刷写节拍估算可以按单块大小、总线波特率做个粗算。以经典CAN 500kbps为例一帧连续帧从开始到结束大约130到150微秒。传一块4KB数据按每帧7字节算大约585帧光数据帧就要80毫秒左右。再加上34/36/37交互、0x78等待、Flash擦写时间单块整体耗时往往在100到300毫秒之间。这个数值能帮你判断刷写总耗时的合理性也能在你看到“刷写时间异常变长”时快速猜出是丢帧重发还是ECU处理变慢。6. 排查NRC 0x73、0x74、0x78三次真实刷写故障还原6.1 故障一响应丢失后的盲目重发误触0x73先说一个我印象很深的案例。某次在台架上测ECU刷写第50块数据发送后总线日志里只有请求帧没有ECU响应一直到超时。上位机此时进入重试逻辑把超时的那一包重新发了一遍。结果ECU立刻回7F 36 73。排查链路是这样的先看总线日志确认第一次请求已经完整到达ECUISO-TP层级没有丢帧、重组成功。再做响应超时分析发现ECU实际上已经把第50块写进Flash只是响应帧因为某种原因没发回来。上位机重发同一个BSC时ECU内部状态机已经把期望计数器更新成了51自然会回0x73。这个故障的根子不在ECU在于上位机的重试策略太粗暴。之后我们把逻辑改成36服务超时后不立即重发同一BSC而是先通过读取会话状态或受控例程确认ECU是否处于活动传输状态如果无法确定就直接终止本次传输重新发起34服务重建传输会话再从已知确认点继续。6.2 故障二没做34服务就直接发36一直到0x74另一个案例是刷写工具经过一次异常中断后测试仪没有重新走“切换会话-安全访问-请求下载”流程而是直接从上次断点开始发36服务。ECU马上回7F 36 74。0x74的含义是请求顺序错误。ECU端34/36/37是一个严格状态机只有先收到34服务ECU才会初始化传输缓冲区和BSC期望值没初始化就收到36服务只能回0x74。排查时先看上位机是不是在异常退出后没有执行完整的重连流程。很多工具会把“安全访问状态”和“传输状态”混在一起缓存异常退出后保留了旧的会话状态自以为还在合法传输阶段但ECU早已因为会话超时或主动重置回到了默认状态。这类问题最好的防御方式是每次开始刷写都强制走完整的前置流程不要缓存认证状态和传输状态。6.3 故障三Flash写入慢0x78反复出现导致误判超时还有一次在产线上刷写成功率忽高忽低。总线日志显示ECU经常回7F 36 78上位机也回发了原请求但问题出在回发时机上位机把P2*设置得太短还没等ECU准备好就重发ECU还在忙又回一次0x78如此往复几次后上位机判定超时。排查链路先用示波器抓ECU从收到36请求到回0x78的时间再抓从回0x78到ECU“空闲”的时间。发现Flash擦写过程在特定地址区间耗时波动很大最坏情况超过了上位机设置的P2*。解决方法是把P2*放宽到ECU最坏执行时间的1.5倍以上同时让重发逻辑保证“原帧重发”。这个案例也说明0x78本身不是错误而是ECU在告诉我“再给我一点时间”。测试仪应该把它当成正常状态流转不要算进失败次数。7. 攻击者眼中的36服务伪造写入与重放风险以及防御手段7.1 36服务为什么是攻击面从安全角度看36服务是UDS里“杀伤力最强”的服务之一。它配合34服务可以让任何能够发送CAN报文的节点往ECU内存里写入任意数据。如果这段数据落在可执行区域攻击者直接把恶意固件刷进去如果落在标定区域可以篡改排放参数、限速参数、电池管理策略。攻击方式不复杂在OBD口挂一个CAN设备模拟诊断仪先尝试进入扩展会话再尝试安全访问。如果ECU用的是固定种子、简单密钥算法或者出厂时安全访问没有正确锁死攻击者就能轻松拿到写入权限。即使当前ECU有安全访问保护攻击者也可以记录合法刷写工具发出的36服务请求在稍后重放造成重复写入、镜像损坏。7.2 常见防御手段强制安全访问所有写操作必须通过27服务完成鉴权且密钥不能硬编码在固件里应该由HSM硬件安全模块保护。内存区域隔离将可编程的内存范围在白名单中严格控制刷写驱动不允许覆盖Bootloader保留区。就算攻击者拿到了写入权也不能把关键启动代码干掉。固件签名验证ECU在收到完整镜像后通过37服务或31服务触发验签流程证书链和公钥存放在不可篡改区域。没有合法签名的镜像即使被写入也不能运行。通信认证与新鲜度在诊断通信上增加MAC/CMAC校验和新鲜度计数器抵御伪造和重放。虽然UDS标准本身没有强制但整车安全架构里对刷写类服务的防护越来越必要。网关过滤网关处限制哪些节点能发起34/36/37服务从OBD口直接访问内部动力域ECU的路由往往要经过授权和过滤。7.3 块序列计数器在安全上的局限有人会觉得BSC本身能防重放因为重放的块计数器会对不上。其实这个理解有偏差。BSC只在一次传输会话内部有效攻击者只要完整重放“34服务一串36服务37服务”整段序列BSC是自洽的ECU无法单靠计数器判断这是不是历史重放。所以BSC更多是可靠性设计不是安全设计。真正要防重放得靠新鲜度计数器、时间戳或者随机数挑战机制。7.4 协议栈自身的健壮性还有一类隐患来自UDS协议栈本身缓冲区溢出。很多小型ECU的协议栈是从老代码改过来的对36服务的数据长度没有做严格上限校验。攻击者发一个超大长度的36请求塞满缓冲区就可能造成内存踩踏。这是模糊测试Fuzz Testing最喜欢打的地方。做ECU端协议栈开发时必须在进入36服务数据处理前先检查总长度是否超过接收缓冲区容量超过就直接回0x13绝对不要把数据拷进定长数组。8. 我建议的36服务实现清单与长期经验沉淀8.1 测试仪/上位机侧清单解析34服务正响应里的maxNumberOfBlockLength并以此设定36服务单块大小。维护独立的BSC变量从1开始每收到正响应后加10xFF之后回卷到0x01。超时重发36服务时必须重发完全相同的请求内容不能自动修改BSC。处理0x78收到后等待P2*再原样重发一次重发次数设置上限。刷写镜像长度与34服务memorySize不一致时提前中止。使用无符号整型保存BSC和块长度避免符号扩展导致的判断错误。8.2 ECU端协议栈清单34/36/37用明确的状态机管理不要用散落的全局变量拼逻辑。36服务处理前先校验BSC是否等于期望值不等直接回0x73且不写入。对数据长度做严格上限检查超过缓冲区或maxNumberOfBlockLength就回0x13。支持最后一包短数据不要强行补位。在写Flash期间如果超过P2时间先回0x78等测试仪重发。对可编程内存范围做白名单限制防止越界写入。8.3 测试与验证建议刷写功能测试别只测“快乐路径”。我建议至少把下面几种异常场景加进测试用例里传输中途断电、总线断开后重连、连续帧丢失或乱序、0x78响应连续出现多次、镜像长度非块大小整数倍、几万次刷写压力测试。协议栈对异常输入的处理能力往往比正常流程更能反映真实质量。这些年我调过的刷写项目最终出问题的地方很少在“协议没学会”上多半是细节处理不严谨BSC回卷错了、0x78处理不到位、最后一包补了填充字节、超时重发时改了请求内容。好消息是这些坑都有固定的解法而且都可以通过测试用例提前拦住。36服务本身不难难的是把它放到一整套完整的刷写流程里和34、37、27、31服务配合得天衣无缝。希望你读完这篇之后再去查刷写问题能比从前更快找到那个“不起眼”的根因。