干了这么多年UDS诊断和ECU刷写我到现在还记得第一次被0x36服务折腾到凌晨三点的场景。上位机报了一串NRC 0x73ECU那边就是不理人我当时一度怀疑是不是ECU烧了。后来才发现问题根本不在ECU在我自己写的传输逻辑上发得太急块序列计数器都乱套了。今天就把0x36服务这些年在项目里攒下的经验捋一遍重点聊NRC 0x73、0x72这类让人头疼的负响应以及真正能落地的排查方法。不管你是做协议栈开发、刷写工具上位机还是搞产线EOL的这篇应该都能帮你少走几趟弯路。1. 刷写流程里的角色0x36到底在干什么1.1 一次UDS刷写的完整环节0x36只是中间一环很多新人一上来就盯着0x36猛看但0x36要在整个刷写流程里才有意义。一次完整的UDS刷写通常是这样的链路10 02切换到编程会话27 01 / 27 02安全解锁拿到刷写权限2E F1 3C写入软件指纹/版本号之类的记录31 01 02 02例行控制做擦除或编程准备让Flash进入可写状态34请求下载告诉ECU“我要往哪个地址写多大体积的数据”36传输数据也就是真正把固件内容一块块发进去37请求传输退出告诉ECU“数据发完了你后面自己处理”31 01 02 03检查编程依赖确认整包固件完整11 01复位让新程序跑起来0x36在整个链路里是绝对不能出错的一环因为它是唯一一个反复执行、承载真实数据流的服务。别的服务顶多跑一两次0x36要根据固件大小跑成百上千次刷写速度也主要卡在它身上。也正因为调用次数多它出NRC的概率就比其他服务高得多。我之前见过一份现场日志一次刷写8000多帧0x36中间就在第4520帧附近突然出现一个0x72这种偶发问题排查起来最费劲。1.2 0x36报文结构块序列计数器与数据字段的配合0x36请求帧的格式很简单总共两个核心字段blockSequenceCounter块序列计数器和数据字段。其中计数器占1字节每次请求都要递增用来告诉ECU“这是第几块数据”。正响应是0x76会把接收到的计数原封不动回回来相当于确认“这一块收到而且写好了”。有一个细节特别容易被忽略经典CAN和CAN FD下单个0x36能携带的数据量差别非常大。经典CAN单帧用ISO-TP传输时单帧SF模式下扣掉PCI、SID、计数器真正能放的数据只有4字节左右要是数据多ISO-TP会自动拆成多帧连续传输。而CAN FD下单帧就能放62字节的数据。所以现在刷写基本都是CAN FD否则128KB的固件在经典CAN上要拆几万帧光报文开销就够喝一壶了。blockSequenceCounter对ECU来说不光是序号还是内部写入地址的偏移依据。很多ECU的Flash驱动并不会每次都从左往右重新扫描地址而是直接用“块计数 × 单块长度”去算下一次写入位置。一旦计数器不连续或者跳跃ECU内部计算出的地址就可能是错的这时候报0x73几乎是必然的。我经常跟同事打比方0x36里的blockSequenceCounter就好比快递包裹上的序号快递站是按序号排序拆包的你中间漏了一号后面再来的包裹它就要拒收。1.3 算一笔账128KB固件在CAN FD下要发多少帧假设要刷写128KB的应用程序CAN FD每帧能带62字节有效数据。128 × 1024 131072字节除以62约等于2115帧也就是0x36请求至少发2115次。blockSequenceCounter的范围是0x01到0xFF超过0xFF之后会回绕到0x00然后再从0x01继续。按2115帧算计数器要完整回绕八轮左右。很多上位机就是在回绕这件事上出问题的。有的ECU规范写0xFF之后回到0x00有的实现却要求在0xFF之后重新从0x01开始。你按自己想的发ECU按它自己的规范收两边对不上0x73就出来了。除了计数器每个0x36之间的间隔也有讲究。多数ECU要求等到上一帧0x76正响应或者0x78等待响应之后才能发下一帧有些ECU在刷写大数据块时Flash写入耗时长还会主动给出0x78延后正响应。你要是不理这个等待信号一股脑地发底层缓冲区一满负响应立马飞过来。2. NRC对照表0x73、0x72和其他常见负响应2.1 0x73的触发机制计数器不对还是发太快NRC 0x73在ISO 14229里的标准定义是wrongBlockSequenceCounter直译就是块序列计数器错误。但实际项目中0x73的触发原因至少有这么几类0x34成功之后你发过来的第一个0x36块计数器不是1连续发了几个0x36没有等正响应就直接把计数器递增了导致ECU期望的计数和你发送的计数错位上位机因为某个超时把同一帧发了两次但计数器用的还是同一个值计数器走到0xFF之后回绕规则没对齐有的栈期望回到0x00你直接跳回0x01某些ECU在Flash驱动忙比如还在擦除时会用0x73表达“我现在不接受这个块”最后一点要特别注意。虽说0x73字面上是计数器错误但不少协议栈在底层处于Busy状态、接收缓冲区满时都会笼统地把0x73抛出来。你不要一看0x73就只去查计数有时候真正的原因是发送节奏太快ECU还没处理完上一块数据你的下一帧就已经到了。2.2 0x72的触发机制ECU内部Flash编程失败NRC 0x72的标准定义是generalProgrammingFailure也就是一般编程失败。它和0x73的区别非常关键0x73更多是通信层面或时序层面出了问题而0x72则是ECU真正去写Flash时Flash控制器或者底层驱动返回了错误。常见的触发场景包括没有先做擦除直接往还有旧数据的Flash地址写擦除动作还没真正完成0x36数据就来了写入操作撞在擦除进行中的状态上0x34声明的memoryAddress或memorySize与S19文件里的实际地址不匹配写到不存在的Flash区域写入长度超出了0x34声明的下载范围最后一个块越界Flash驱动没有完成初始化也就是前面漏发了31 01 02 02编程准备硬件层面的问题比如刷写过程中电源电压跌落、主频配置异常、Flash写保护没有解除0x72最讨厌的地方在于它能给上位机的线索非常少。你从CAN日志里只能看到“0x72”三个字节至于里面是驱动哪个步骤失败、哪个地址写不进去ECU根本不会告诉你。所以排查0x72需要把前面的条件全部捋一遍多数时候你会发现不是最后一个动作的问题而是前面某一个前置条件已经埋了雷。2.3 其他高频NRC0x31、0x13、0x22、0x33、0x34在实际刷写过程中0x73和0x72出现概率最高但其他几个NRC也不能忽视它们经常和0x73、0x72混在一起让你误判方向NRC含义常见触发场景排查优先级0x31请求超出范围0x36数据长度超过0x34响应的maxNumberOfBlockLength地址或长度非法高0x13报文长度或格式错误ISO-TP报文长度不对、CAN FD帧长超出预期高0x22条件不正确没进编程会话、没做安全解锁就发0x36中0x33安全访问被拒绝27服务解锁失败密钥比对不过中0x34请求序列错误在0x36过程中插入了其他服务没发0x34就发0x36中0x10一般拒绝服务不被当前会话支持低印象比较深的是一次现场问题工具一直在报0x31大家硬往“请求长度不对”的方向查了半天。最后看日志才发现是0x34正响应里的maxNumberOfBlockLength有的实现按“包含SID和计数器”的整帧长度算有的实现只算数据区长度。两个标准差2字节上位机按数据区长度发62字节ECU按整帧上限收直接抛0x31。这种标准理解的差异在协议栈对接阶段特别常见。3. 从CAN日志锁凶手0x73与0x72的排查步骤3.1 抓包配置先把这些报文和信号完整记下来排查0x36问题的基础是抓包。我用CANoe比较多也会用PCAN或者USBCAN配开源工具做快速抓取。无论用什么工具抓包时至少要确保能同时记录以下内容0x36请求帧里的blockSequenceCounter值0x36请求的数据长度0x76正响应的blockSequenceCounter0x7F负响应的具体NRC码0x34请求和正响应里的memoryAddress、memorySize、maxNumberOfBlockLength0x37请求和正响应的时间点每一帧CAN报文的时间戳精度越高越好时间戳特别重要。你判断“上位机是否在等正响应”完全靠时间戳。如果连续出现多帧0x36它们之间没有插入任何0x76或0x78那发送时序就基本坐实有问题。我之前用tmaster虚拟通道做上位机刷写时遇到过一次0x73就是因为虚拟通道内部在忙时把0x76响应给延迟处理了上位机没等到响应就发了下一帧。那个问题从应用层代码根本看不出来只有把时间戳拉出来才暴露。3.2 排查0x73的四步走计数器、等待机制、回绕规则、发送速率第一步导出所有0x36请求的blockSequenceCounter序列。你在CANoe里加一列显示raw data或者在脚本里把第一个数据字节提取出来然后检查序列是否从1开始、是否严格递增。如果中间出现重复值或者跳号问题基本就在上位机。第二步检查0x36和响应之间的时间间隔。正常情况下每个0x36之后应该有0x76正响应或者至少有个0x78 pending响应。如果你发现三四个0x36连在一起中间没有任何响应则说明上位机没有实现“等待响应再发下一帧”的流控。这不是ECU的问题是上位机逻辑的bug。第三步检查0xFF之后的计数器行为。找到计数器到达0xFF的那一帧看下一帧是0x00还是0x01。参考ECU规范如果规范里写的是0xFF后回0x00而你的上位机发了0x01那就把上位机的回绕逻辑改掉。第四步看发送速率是否超过ECU处理能力。即使计数器完全连续发太快也可能触发0x73。你可以手动在每帧0x36之间加一个延时比如10ms或20ms逐步调大看故障是否消失。如果加上延时就好了说明ECU在Flash写入期间根本处理不了连续请求上位机需要做帧间隔控制。我在实际排查时还会写一个非常小的Python脚本把CANoe导出的ASC或CSV读进来专门算blockSequenceCounter的跳变。这个脚本逻辑不复杂核心就是遍历计数器字段一旦发现相邻差值不是1就把前后两行打印出来你直接定位到具体帧。这个办法用Excel条件格式也能实现但遇到几千帧数据时脚本更快。3.3 排查0x72的六步走前置条件、地址映射、擦写校验0x72的排查逻辑完全不一样它指向的是ECU内部Flash编程失败。我的固定排查路径是这样的第一步回头确认31 01 02 02有没有执行成功。有些ECU的擦除动作是异步的31服务的正响应不代表擦除已经完成可能只是“擦除已启动”。如果上位机在擦除还没结束时就发0x36ECU内部Flash控制器还在忙写操作必然失败。第二步核对0x34请求里的memoryAddress和memorySize。这里最容易出问题的是S19文件地址和0x34声明地址不一致。比如S19文件里有一部分数据落在Bootloader区域而0x34声明的是App区域写到受保护Boot区时Flash控制器直接拒绝。第三步检查每一个0x36块的实际长度是否超过maxNumberOfBlockLength。如果前面几个块都没问题刷到后面某个块突然0x72很可能是最后一个块越过了0x34声明的memorySize边界。第四步确认Flash是否需要按页或按word对齐。有些MCU要求写入地址必须按16字节或32字节对齐如果S19文件里某一行数据没有对齐擦除正常但写入时Flash控制器会报错。第五步排查硬件电源。刷写过程中Flash写入和擦除非常耗电如果供电线损太大电压跌落超过MCU容忍范围Flash控制器会直接报编程失败。这个问题在产线转台测试时特别常见换一根粗一点的电源线就好。第六步用ECU厂商的参考工具刷一遍同样的文件。如果厂商工具能刷成功你的上位机不能那就是你的工具在某个细节上跟ECU驱动不一致如果厂商工具也失败那就要怀疑ECU本身或者硬件环境了。3.4 一个真实案例0x72把我折磨了一整天有一次做ECU升级项目刷写总是在80%左右的固定位置报0x72。最开始怀疑是上位机长度算错了对着协议栈代码调了一天没找到问题。第二天把CAN日志和S19文件放在一起比对才发现真相S19文件里除了常规的App数据还有一个高地址的校准参数区。上位机0x34声明下载范围时只覆盖了App起始到App末尾但数据文件里那一段校准参数区的地址偏移算下来已经超出了0x34声明的memorySize范围。ECU底层Flash驱动发现写入地址超出声明范围直接返回编程失败。解决方式很简单把0x34的memorySize扩大把校准参数区的范围也包进去或者在生成S19文件时就过滤掉不在下载范围内的段。这个事给我的教训是排查0x72一定要把“地址范围”放在第一步。很多看似是Flash编程问题的NRC最后都能追溯到数据文件与下载声明的错位。4. 避坑清单与协议栈源码级提醒4.1 我给你列了一张0x36刷写避坑表这些年把我坑过的、以及帮别人排查过的问题几乎都能归到下面这张表里坑点典型NRC规避方法不等正响应连续发0x360x73严格等0x76或0x78再发下一帧0x34后第一帧计数器不是10x73每个刷写任务开始时重置计数器计数器0xFF后回绕规则不一致0x73按ECU规范明确回绕值0x00还是0x01块长度超过maxNumberOfBlockLength0x31 / 0x13用0x34响应的值动态切割数据块ISO-TP分包和上层计数不同步0x73在应用层和传输层之间加计数器校验漏发31擦除准备0x72刷写前检查前置服务执行状态0x34声明范围与S19文件不匹配0x72解析S19后与0x34参数做比对最后一个块越界0x72计算总长度不允许超过memorySize0x36过程中插入其他会话服务0x34在刷写状态机里屏蔽非0x36高层请求Flash地址未按页对齐0x72生成S19时按MCU对齐要求处理这张表也印证了一个判断刷写NRC问题的根因往往不在最后触发NRC的那一帧而在好几步之前。你只盯触发帧永远找不到源头。4.2 协议栈源码级提醒别只看文档直接翻状态机如果你在移植或维护UDS协议栈源码排查0x36问题时比文档更值得看的是状态机实现。具体看三个点blockSequenceCounter的期望值在哪个状态里更新Flash Busy期间对0x36请求做了什么处理以及0x78 pending与正响应之间的状态切换条件。我见过一个奇葩问题协议栈里收到0x36后先做Flash写操作写成功了才更新blockSequenceCounter的期望值。结果Flash写速度慢上位机已经超时重发了重发帧用的还是旧计数器直接触发0x73。表面看是超时问题实际是状态机不应该把“计数器更新”和“Flash写入完成”强绑在一起应该在接收到合法帧时就先更新计数器再去做耗时操作。另一个源码层面的坑是NRC优先级。很多ECU在收到0x36时会先判断会话模式和访问权限再判断计数器和长度。也就是说即使你的计数器是对的如果没进编程会话ECU返回的可能是0x22而不是0x73。我见过有人拿0x22当0x73查计数器查了半天都不对。排查前先看一眼ECU返回NRC的判定顺序能省很多时间。4.3 硬件在环和产线的坑时间同步和日志留存在产线环境里0x36刷写故障的排查会更难因为产线往往没有完整的CAN日志链路。刷写失败时只有上位机弹出一个NRC码连是哪一帧出的都看不到。我强烈建议产线刷写工位至少做到两点上位机固化刷写全过程的CAN日志文件日志里至少要包含时间戳和blockSequenceCounter。产线和开发环境的另一个差异是时间同步。开发时你可以在受控环境下慢慢抓包产线则要求在几十秒内刷完上位机、CAN卡、ECU三者之间的时间偏差都会被放大。之前有个项目在开发室怎么测都正常一到产线就有概率报0x73最后查到是产线工控机的USB转CAN卡驱动在高负载下延迟不稳定上位机等响应超时定时器误判把超时重发逻辑触发了。后续换了一张独立供电的CAN卡问题就消失了。产线还有一个要注意的地方就是刷写失败后的重试策略。别简单地从头重刷很多时候重刷还是会在同一个地址附近失败。正确做法是保留失败时的CAN日志、当前块序号、数据文件版本形成一个可回溯的刷写记录。我见过太多现场人员拿着手机拍屏幕上的NRC码回去根本没法定位。数据文件版本对不上或者日志根本没记录排查就只能靠猜。最后再分享一个我个人的习惯每次刷写前不管是不是开环测试我都会把0x34的memoryAddress、memorySize、maxNumberOfBlockLength以及S19解析出来的实际地址范围、总字节数统一打印在工具日志第一屏。确认这几个数字对得上再进刷写流程。这个小动作不复杂但能挡掉至少一半以上的0x72和0x73。UDS刷写这个东西90%的NRC都藏在时序和前置条件里ECU真的没你想的那么爱闹脾气更多时候是驱动它的那双手在哪个环节慢了半拍。
