去年年底在一家Tier1的台架上调UDS刷写,遇到一个特别磨人的问题:0x36传输数据跑了大半,突然连续返回NRC 0x73,重启上位机再刷又是同样的位置挂掉。一开始我怀疑是ECU的Flash驱动有缺陷,后来把总线抓包文件和上位机日志逐帧比对才发现,问题根本不在ECU侧,而是上位机的超时重传逻辑出了bug——重传时块序列计数器没有回到出错前的位置。
这个坑让我整整折腾了一天。后来我把0x36服务相关的NRC(尤其是0x73、0x72)彻底梳理了一遍,又翻了不少协议栈源码和诊断规范,发现刷写阶段的大多数问题其实都有规律可循,排查思路也完全可以系统化。这篇文章就把这些经验整理出来,围绕0x36服务的报文格式、NRC触发机制、常见根因和排查方法展开,适合ECU诊断开发、刷写工具开发、诊断测试工程师以及刚接触UDS协议栈的朋友参考。
1. 刷写链路中的0x36服务:先看清它在整盘棋里的位置
1.1 从0x34到0x37的完整时序
很多人一上来就盯着0x36的NRC看,却忽略了0x36在整个刷写流程中的位置。实际上,0x36(TransferData,数据传输)只是刷写链路中的一环,它的前后都有严格的前置条件和后续动作,任何一个环节没走对,0x36都可能报错。
一次典型的UDS刷写流程是这样的:首先通过10 02进入编程会话,然后通过27 01/02完成安全解锁,随后执行31 01例程控制做编程预条件检查和Flash擦除,接着发0x34(RequestDownload,请求下载)告诉ECU“我要往哪个地址写多少数据”,ECU回正响应后,上位机才开始用0x36逐块搬运数据,全部搬完后发0x37(RequestTransferExit,请求退出传输)结束下载,最后再来一轮例程控制做编程依赖检查,最后11 01复位ECU。
这个顺序不是随便定的。0x34的作用是让ECU做好接收数据的准备,包括校验地址范围、分配缓冲区、初始化Flash编程状态;0x36则是真正的“搬砖”环节,把固件数据一块一块送进ECU;0x37告诉ECU“数据发完了,你校验一下整体完整性”。如果0x34没成功,或者安全等级不够,0x36就会直接报错——这也是很多人排查0x36故障时容易忽略的盲区。
1.2 0x36的报文格式与块序列计数器的设计意图
0x36的请求帧格式很简洁:第一个字节是SID 0x36,第二个字节是块序列计数器(blockSequenceCounter),从第三个字节开始才是真正的传输数据。
响应帧则是0x76加相同的块序列计数器,表示ECU确认收到了这一块数据。
请求:36 | blockSequenceCounter | data[0...n] 响应:76 | blockSequenceCounter这个块序列计数器是理解0x73的关键。它从0x01开始,每成功接收一块数据就加1,到0xFF后回绕到0x00继续递增。计数器存在的意义是让ECU能识别数据块的顺序——因为底层传输可能有重传、丢帧等异常情况,ECU需要通过计数器判断当前收到的数据块是不是自己期望的那一块,防止数据错位写入Flash。
这里有一个容易踩的坑:0x34的正响应SID是0x74,而NRC 0x74也是“请求序列错误”。在抓包日志里看到0x74,先确认它是正响应的SID还是负响应码,别一上来就以为ECU报了请求序列错误。这种细节在协议栈源码调试和日志分析中很容易把人带偏。
2. 0x73错误块序列计数:最常见的刷写中断元凶
2.1 ECU侧对块序列计数器的校验逻辑
按ISO 14229-1的规定,ECU内部会维护一个期望接收的块序列计数器值。每收到一个合法的0x36请求,ECU会把请求中的计数器和自己期望的值比较,一致则处理数据、计数器加1、返回正响应;不一致则返回NRC 0x73(wrongBlockSequenceCounter)。
这里有个值得注意的细节:ECU收到一个计数器不匹配的0x36请求后,它内部的期望计数器是否改变,标准没有强制规定,由实现决定。我实际接触过的ECU中,有些在收到错误的计数器后保持原状态,上位机只要重新发送正确的块序号就能继续;有些则会中止当前的下载会话,后续所有0x36都会返回0x73或0x74,必须重新执行0x34甚至重新解锁才能恢复。所以遇到0x73,先查清楚你的ECU属于哪种行为模式,否则排查方向很容易跑偏。
另外,0x73和超时是两个层面的问题。如果底层传输丢帧导致ECU的TP层根本没收到完整的0x36请求,上位机侧表现为超时而不是收到0x73;只有当请求完整到达ECU应用层,但计数器和期望值不一致时,才会看到0x73。区分这两者,能帮你快速判断问题出在传输层还是应用层。
2.2 导致0x73的四类根因场景
根据我这些年的经验,0x73的根因可以归为四类:
第一类是上位机重传逻辑bug。这是最常见的。典型表现是:上位机发送块N后超时(实际上ECU可能已经收到并处理了,只是正响应帧丢了),上位机决定重传,但重传时计数器已经递增到N+1,于是ECU期望N却收到N+1,直接报0x73。更麻烦的是有些自研刷写工具在超时后会“重新开始传输”,但计数器没有重置为0x01,导致错位一直延续。
第二类是分包大小不一致。应用层定义一个数据块是4096字节,底层CAN FD单帧只能传64字节,需要把一个大块拆成多个CAN帧经TP层传输。如果上位机的TP层分段逻辑有bug,或者ECU侧接收缓冲区大小不匹配,可能出现上一块的尾部数据残留在TP层缓冲中,被当成下一块的头部数据,导致ECU应用层拿到的数据块边界和上位机不一致,计数器自然就对不上了。
第三类是传输层丢帧或乱序。CAN总线负载过高、错误帧重试、总线关闭等情况都可能导致TP层丢帧。ECU的TP层收不到完整消息就不会向应用层提交,上位机发了一个块但ECU完全没收到,双方计数器就不同步了。这种场景下,上位机看到的往往是超时之后又收到0x73,很迷惑。
第四类是软件工程问题——多线程竞争。自研刷写工具中,发送线程和维护计数器的线程不是同一个,或者多个任务共享一个全局计数器变量,没有加锁保护,就可能在某个瞬间把错误的计数器值发出去。这类问题随机性强,复现困难,但一旦出现就是疑难杂症。
2.3 定位0x73的排查清单与实测经验
面对0x73,我建议按以下清单逐项排查:
- 导出完整的抓包文件,把出错前后各20帧的0x36请求和响应全部列出来,核对计数器的递增序列。
- 检查ECU返回0x73后,自己的期望计数器值是多少——可以通过重新执行0x34后再试,观察是否恢复正常来间接判断。
- 核对0x34正响应中的maxNumberOfBlockLength,确认你的实际块大小是否在ECU允许的范围内。
- 检查传输层是否有重传、错误帧、总线关闭等异常记录。
- 如果是自研上位机,重点审计超时重传分支和计数器管理的代码。
实测下来,0x73有六成以上是上位机逻辑问题,三成是传输层问题,真正ECU侧自己出问题的不到一成。所以遇到0x73,先别急着怀疑ECU的协议栈实现,先从自己这一侧找原因,往往更快。
3. 0x72一般编程失败:看似笼统,实则信息量很大
3.1 0x72的触发层次:从应用层到驱动层
NRC 0x72(generalProgrammingFailure)翻译过来是“一般编程失败”,单看这个名字非常笼统,好像什么都没说。但如果结合0x72出现的位置和上下文,它其实能告诉我们很多信息。
0x72通常出现在两种场景中:一种是0x36传输数据的过程中,ECU尝试把数据写入Flash时失败;另一种是0x31例程控制执行擦除操作时失败。和0x73不同的是,0x73说明ECU认为你的计数器不对,属于时序问题;0x72则说明时序没问题、数据块序号也对,但ECU在执行编程操作时,物理层面或者驱动层面出了岔子。
所以看到0x72,第一反应不应该是“ECU在说什么”,而是“ECU在哪个环节失败的”。这个环节可以粗略分为应用层状态检查失败、驱动层擦写操作失败、数据完整性校验失败三个层次,排查方向完全不同。
3.2 Flash操作失败、地址校验失败、数据完整性校验失败
按我的经验,0x72背后最常见的具体原因有几种:
一是Flash擦写操作失败。Bootloader里的Flash驱动依赖底层库函数,如果驱动代码本身有bug,或者擦写时电压不稳定、Flash器件异常,就会返回失败。这类问题通常在固定地址或固定数据块附近复现,因为某些Flash扇区本身就有坏块或擦写寿命耗尽。
二是地址校验失败。0x34请求下载时指定的内存地址范围,和0x36实际写入数据跨越的地址范围不一致。比如0x34里给的memorySize比固件文件实际大小少了一个块,最后一个0x36的数据就超出了ECU允许写入的Flash区域,ECU在做地址边界检查时直接返回0x72。这类问题定位起来相对容易:把0x34的参数和固件文件大小对着算一遍就能发现。
三是数据完整性校验失败。有些ECU在0x36传输过程中就会对已收到的数据做校验和或CRC累计计算,如果发现累计值不对,立即返回0x72。这种机制下,0x72出现的位置往往是最后一个0x36块,但具体原因可能是上位机读取固件文件时读错了字节、传输过程中数据被破坏,或者数据块顺序错乱导致累计校验值对不上。
四是一次性写入缓冲区溢出。某些ECU的Flash编程驱动要求一次性写入的数据长度不能超过内部缓冲区大小,如果上位机某个块的data字段长度超过了缓冲区,也会触发0x72。
3.3 利用DID和例程控制进一步定位
0x72的排查不能只靠看NRC本身,还得借助其他诊断服务来获取更多上下文信息。
首先,通过读取DID(比如0xF1 00当前会话状态、0xF1 80软件版本号等)确认ECU当前是否还在编程会话、安全等级是否维持解锁状态。有些ECU在0x36传输过程中如果长时间没有请求交互,安全状态会自动降级,后续0x36就会报0x72或0x33。
其次,重新执行例程控制检查编程预条件。部分ECU实现中,0x31 01 FF 00(检查编程预条件)的结果会带一个子错误码或状态字节,能精确告诉你是电压问题、温度问题、点火信号问题还是其他外部条件不满足。我见过一个案例,ECU在0x72之后通过读取一个OEM自定义DID,直接读出了“目标扇区擦除超时”的具体错误,问题瞬间定位。
再有就是读取DTC。很多ECU在Flash编程失败时会记录诊断故障码,通过19 02读取相关DTC,能确认是否是Flash硬件故障导致的0x72。
如果以上手段都用了还是定位不了,就用最小复现法:把传输块大小调小,换一块已知没问题的Flash区域写入,逐步逼近问题边界。这个方法虽然朴素,但在驱动类问题上非常有效。
4. 其他高频NRC:0x74、0x22、0x31、0x70同样值得关注
4.1 0x74请求序列错误:顺序错了全盘皆输
0x74(requestSequenceError)和0x73不一样,它不是说“你这块序号不对”,而是说“你压根就不该在这个时候发0x36”。最常见的触发场景有三种。
第一种是跳过了0x34直接发0x36。ECU根本还没建立下载会话,你上来就传数据,它只能回0x74。这种情况通常是上位机逻辑漏了0x34,或者0x34发送失败后没有正确处理、继续往下走了。
第二种是安全解锁没完成就发0x36。部分ECU在0x34之后、0x36之前还会校验安全等级,如果当前处于锁定状态,直接返回0x74或0x33。注意这两个NRC在诊断规范中都可能出现在这个场景,具体返回哪个取决于ECU的安全等级检查在哪个环节实现。
第三种是0x36和0x37之间插入了不该有的服务,或者0x37之后又发0x36。有些ECU对下载会话的边界管理非常严格,0x37结束后的第一个0x36立刻返回0x74。
演示一下典型错误时序:
10 02 -> 50 02 27 01 11 22 33 -> 67 01 44 55 66 34 ... -> 74 ... # 正响应SID是74 36 01 ... -> 7F 36 74 # 还没解锁?报序列错误注意这里的0x74既是0x34正响应的SID,也是NRC。在代码里做日志解析时,一定要区分帧的上下文,否则会把正响应误判成负响应。这个坑我在自研解析脚本时踩过,特此提醒。
4.2 0x22条件不满足与0x31请求超出范围
0x22(conditionsNotCorrect)和0x74有点像,都是说“当前条件不对”,但0x22更偏向外部的环境条件,而不是协议时序条件。
刷写场景中,0x22常见于ECU检测到外部条件不满足时拒绝0x36。比如某些OEM要求刷写时点火信号必须处于ON状态、蓄电池电压必须在一定范围内、挡位必须在P挡等。如果这些硬线信号条件不满足,ECU在收到0x36后会以0x22拒绝。
0x31(requestOutOfRange)则在参数层面做文章。0x36请求中如果出现非法的计数器值(比如0x00)、数据长度超过0x34协商的块长度、或者传输的数据量超过了0x34声明的memorySize,都可能触发0x31。这个NRC的出现通常意味着上位机的参数计算有误,属于逻辑bug,排查时对照0x34的请求参数逐一核对即可。
4.3 0x70上传下载未接受
0x70(uploadDownloadNotAccepted)在0x36阶段相对少见,但偶尔会遇到。它表示ECU当前不接受上传或下载操作,常常出现在0x34阶段,但如果ECU在下载会话过程中状态被重置(比如发生了软件复位),后续的0x36可能返回0x70而不是0x74。
如果遇到0x70,优先检查ECU是否还在编程会话中——通过发送10 02重新进入会话,并重新执行安全解锁和0x34,一般就能恢复。另外需要确认ECU是否处于某种保护状态,比如下载会话被某个例程锁定、或者软件擦除后进入了一种“只能写固定区域”的特殊模式。这种特殊模式在部分Bootloader实现中存在,需要参考具体OEM的诊断规范确认。
还有0x33(securityAccessDenied)也值得顺带提一下。如果安全解锁后过了一段时间再发0x36,部分ECU的安全状态会超时锁定,这时0x36会返回0x33,需要重新执行27 01/02解锁。刷写工具的超时设置如果比ECU的安全锁定时间还长,就很容易踩到这个。
5. 实战排查方法论:从现象到根因的完整链路
5.1 必备的排查工具与日志记录习惯
刷写问题排查,工具链和日志习惯决定了效率。我个人常用的工具组合分三层。
第一层是总线抓包工具。CANoe/CANalyzer是行业主流,功能强大但价格不菲;预算有限时,PCAN-Explorer、周立功CANTest、同星TSMaster都是不错的替代。抓包时要开启时间戳,精确到微秒级,并且同时记录CAN错误帧和总线统计数据,这对判断传输层问题至关重要。
第二层是诊断交互日志。如果上位机用的是UDS诊断库(比如python的udsoncan、C++的Scapy或自研协议栈),要把每次请求和响应的完整报文、NRC码、块序列计数器、时间戳都记录下来。我习惯把日志格式固定成CSV或JSON,方便用脚本做计数器连续性分析。
第三层是应用层业务日志。记录上位机当前的刷写阶段、固件文件偏移、期望的块序号、重传计数等业务状态。排查0x73时,这三层日志放在一起对照,基本能把问题缩小到具体模块。
我自己保存日志的规范是:文件名带日期、ECU软件版本、测试用例编号,比如2025-01-15_ECU_v1.2_TC07_0x73.csv。这个习惯救过我很多次——有次隔了两周客户反馈0x72,我翻出当时的抓包文件一比对,发现和之前修复过的bug一模一样,只是换了台ECU复现。没有基线数据,排查真的得从零开始。
5.2 两个典型故障案例拆解
分享两个我实际处理过的案例,能代表0x73和0x72常见的排查路径。
案例一:0x73反复出现在第128块附近。抓包发现,第127块的0x36请求发了两次——第一次ECU处理完毕但正响应丢失,上位机超时后重发,ECU收到重复的块序号后返回0x73。但问题不止于此,上位机重发后并没有把当前块的计数器恢复到127,而是接着递增到128发送,ECU期望128却收到了129,于是连续报错。修复方案很简单:超时重传时,先从日志中还原最后一次ECU确认过的计数器值,从这个值继续发。这个修复看似容易,但在多线程架构的上位机里,要保证计数器全局唯一且线程安全,还是需要动点脑筋的。
案例二:0x72在最后一个数据块出现,而且每次都是最后一个块。排查时把0x34的参数打出来一看,发现memorySize=固件实际大小-4096,也就是少了一个块的大小。上位机传完最后一个合法块后,还有4096字节的数据“无处安放”,ECU在地址边界检查时直接报0x72。根因是0x34参数计算时用了旧的固件文件大小,而实际发送的是新固件,两者差了刚好一个块。定位这个问题的关键就是核对0x34参数和固件文件实际大小,这个核对步骤应该做成刷写工具的自动校验,而不是靠人工排查。
5.3 一套可以复用的排查顺序
最后给出一套我在项目中沉淀下来的排查顺序,适用于0x36相关的绝大多数NRC问题。
第一步,确认上下文状态。检查当前会话是否编程会话(10 02成功)、安全等级是否解锁、0x34是否成功。这三个前置条件任何一个不满足,后续0x36的任何NRC都不能直接当作0x36本身的问题来分析。
第二步,抓包看完整时序。从10 02到NRC出现的完整交互序列,逐帧列出0x36请求的块序号和ECU响应。这一步能排除掉大量低级问题。
第三步,核对0x34参数。把0x34里的memoryAddress、memorySize、addressAndLengthFormatIdentifier解析出来,和固件文件的实际大小、起始地址做比对。这一步专治0x72和0x31。
第四步,检查传输层。看总线统计里有没有错误帧、重试记录、总线关闭,确认TP层是否丢帧。这一步专治0x73和超时问题。
第五步,检查ECU侧状态信息。读DID、读DTC、重新执行例程控制检查,获取ECU内部的错误上下文。
第六步,用最小复现用例验证。如果以上都查不出问题,就写一个最简脚本,只做“0x34+单个0x36块”,逐步增加复杂度,直到复现问题。这个过程往往能把疑似ECU的问题转化成确定的上位机问题。
最后说一个我自己的体会:0x36的NRC排查,本质上是一种“分层归因”的过程。先从协议时序层找问题,再从参数计算层找问题,然后从传输层找问题,最后才怀疑ECU实现。严格按照这个顺序,大多数刷写故障都能在半小时内定位到根因,而不是靠猜和试。希望这篇文章能帮你少踩几个我当年踩过的坑。