
干车载诊断这行的估计都跟UDS刷写打过交道。你在CANoe里把固件往ECU里灌表面上就是发几个诊断服务顺不顺另说报错才是常态。而刷写里最核心的三个服务就是34RequestDownload、36TransferData和37RequestTransferExit。它们不是你背几个报文就能用明白的尤其是真到了现场配合Trace报文一层层扒你才会理解那些地址、块长度、序列计数器到底在干嘛。这篇文章我打算直接按实战来写。先讲清楚这三个服务在整个刷写流程里的定位和参数心法再带你从头到尾在CANoe里把一次刷写跑下来最后把Trace报文逐帧拆开看并整理几个现场最容易踩的坑。无论你是刚接触UDS的新人还是已经在用CANoe但刷写老出问题的工程师按照这个思路去排查基本都能定位到原因。1. 刷写项目里的34、36、37先把服务流程串起来再看报文1.1 一次完整UDS刷写到底做了什么在啃34、36、37之前我建议你先在心里把一次完整的UDS刷写流程画出来。很多人一上来就盯着34服务结果连前面的10服务、27服务都没走通ECU回个负响应就开始懵。实际上一次能成功烧录的刷写流程通常长这样用10服务切换到编程会话10 02这是刷写的前提。用27服务做安全解锁拿种子算密钥过了安全校验才能动Flash。用31服务做例程控制把目标内存区域擦除。用34服务告诉ECU我要下载数据了起始地址在哪、数据总长是多少。用36服务把固件按块传过去ECU每收到一块会回一个肯定响应。用37服务告诉ECU所有数据都传完了你去做校验、编程收尾动作。最后还用10服务切回默认会话或者用11服务复位ECU。这套流程里34、36、37是真正承担数据传输任务的核心三兄弟。34是开场白36是主体搬运工37是收官。剩下的都是给它们铺路和收尾的。1.2 34、36、37各自扮演什么角色举个例子34服务像是你给快递网点打电话下单告诉对方“我有个包裹要发收件地址是某某区某某路几号包裹体积多大”。36服务则是快递员一车一车地送货每送一车门卫签收一次。37服务就是最后一车送完后你告诉网点“货全到了你清点一下没问题就可以签总单”。把这个类比映射到UDS上34服务请求里的内存地址就是“收件地址”内存大小就是“包裹体积”ECU回的正响应里会带一个“最大允许的单车载货量”也就是maxNumberOfBlockLength36服务里每一次带的数据块都不能超过这个上限37服务则用于触发ECU做整体的完整性和有效性检查。为什么这个流程设计得这么“啰嗦”因为ECU的Flash写入是有风险和时序要求的。它必须知道你要写多少、写到哪才能判断自己的剩余存储空间和地址边界是否够用。它必须限制每一包数据大小才能在自己的RAM缓冲区里做搬运。而块序列计数器则是为了让ECU能检查你是不是传漏了某一包。这些机制组合起来才保证了刷写过程可以中断、可以续传、可以定位错误。1.3 为什么选CANoe来干这个活用普通CAN卡加串口助手也没法完全不能刷写但调试效率完全两回事。CANoe在这个场景下的不可替代性在于它能同时做到三层可视化底层总线报文、网络层ISO-TP分帧、应用层UDS服务解码。你导入了CDD或ODX诊断描述文件之后Trace窗口里甚至能直接显示这条报文对应的是哪个服务参数怎么解析的不用再拿协议规范一字节一字节去对。而且刷写过程中ECU回什么负响应、超时在哪一帧、ISO-TP哪一帧没续上在CANoe的Trace里都是一目了然的。加上CAPL脚本可以把整个刷写流程自动化重复测试和回归验证的效率高很多。所以CANoe不光是分析工具也是我调试刷写流程的主战场。2. 三个核心服务拆解参数、边界与应对策略2.1 34服务下载请求的正确打开方式34服务的请求格式是SID0x34 dataFormatIdentifier addressAndLengthFormatIdentifier memoryAddress memorySize。其中dataFormatIdentifier通常填0x00表示数据不压缩、不加密。如果你遇到某些ECU要求压缩或者加密这个字节才会用到非0值常见场景基本用不到。addressAndLengthFormatIdentifier是很多人第一个犯迷糊的地方。它的高四位表示内存地址占几个字节低四位表示内存大小占几个字节。比如0x42意思就是内存地址占4字节、内存大小占2字节。再比如0x44就是地址和大小都占4字节。网上流传的很多例子里写0x24那其实是地址占2字节、大小占4字节千万别用错。我举一个实际的请求例子。假设ECU的Flash地址从0x00002000开始固件大小是0x1234字节地址长度用4字节、大小长度用2字节那么地址和长度格式标识符就是0x42请求报文是34 00 42 00 00 20 00 12 34注意这里的字节序UDS协议里所有多字节参数都是大端模式也就是先发高位字节。后面ISO-TP分帧也是按照这个顺序去切的。34服务的正响应格式是响应SID0x74 lengthFormatIdentifier maxNumberOfBlockLength。其中lengthFormatIdentifier常见为0x20表示后面的maxNumberOfBlockLength占2字节。比如ECU回。74 20 00 80这表示maxNumberOfBlockLength 0x0080 128字节。这个值就是在告诉你后面发36服务时每一包里最多能塞多少数据。它是ECU能力的一种“上限声明”你必须严格遵守。我遇到过不少工程师拿到34正响应后根本不看这个值习惯性用512甚至1024的块长度去发36服务结果ECU直接回0x73或者0x31。正确的做法是先从响应里解析出这个值再决定后续36服务的分块大小。按协议规定36服务中transferData的实际长度不应超过这个maxNumberOfBlockLength。2.2 36服务块序列计数器和数据块长度的门道36服务的请求格式是SID0x36 blockSequenceCounter transferData。blockSequenceCounter也叫BSC是块序列计数器从0x01开始每成功发送一块就加1。它的作用是让ECU可以验证你传过来的数据是不是连续的有没有丢块。如果传着传着ECU发现BSC跳号了就会认为通信异常拒绝后续数据。BSC的递增规则是8位的所以当它从0x01一直加到0xFF后下一块会变成0x00然后再继续递增。不过这里有个细节部分ECU的实现是到0xFF后重新回到0x01而不是回到0x00。这就需要你在开发前跟ECU供应商确认或者通过实验来看它到底怎么处理。如果两者不匹配刷写会在跑到第255块之后突然失败。每一块的大小怎么定原则很简单不能超过34正响应里返回的maxNumberOfBlockLength。同时还要考虑你的总线能力和ECU接收缓冲。假如你用的是经典CAN每帧CAN报文最多只有8字节数据而一个36服务的数据块往往是128字节甚至更多这就要依赖ISO-TP层做分帧了。传输层的分帧会占据大量总线时间所以块长度也不是越大越好因为一旦某一个帧丢失整个36数据块都会被ECU丢弃重传代价很大。36服务的正响应格式是0x76 blockSequenceCounter也就是把请求里的块序列计数器原样回显。你看到ECU回了0x76 03就说明它正确处理了BSC0x03的那一块。如果ECU回的是负响应比如0x7F 36 73那就要结合具体NRC去排查了。2.3 37服务传输退出的隐藏细节37服务只有SID一个参数请求就是一条62 37正响应是0x77。看起来很简单但它背后的ECU动作一点不简单ECU收到37后通常会开始做数据完整性校验、地址边界检查、写入Flash、甚至触发应用启动。所以37服务发出去之后ECU的响应时间可能比34和36都长在CANoe里你要特别注意P2/P2*超时参数设置别一超时就把请求判定为失败。37服务最常见的失败是ECU回0x22表示conditionsNotCorrect或者0x72表示generalProgrammingFailure。0x22通常意味着前置条件没满足最典型的就是数据没有完全传完就发了37。比如你的固件本来有4660字节但36服务只传了4608字节剩下52字节没传你就急着发37ECU做校验时发现字节数和34服务里声明的memorySize对不上自然就会拒绝。0x72的出现则更复杂一些可能是校验和错误、可能是写入Flash失败、也可能是ECU在编程过程中检测到电压异常或者安全状态变化。遇到这种NRC单靠37服务本身已经无法定位必须回头检查你传输的数据内容、编程会话状态、ECU供电稳定性等外围因素。这里有一个特别容易踩的坑不要以为37服务收到正响应就万事大吉。有些ECU在37正响应之后还会做一段时间的内部编程如果此时你直接切会话或者复位可能把正在写入的Flash搞坏。所以刷完37之后建议先观察一段时间等ECU状态稳定了再做后续动作。2.4 别忘了刷写前后的配套服务虽然这篇文章的主角是34、36、37但刷写失败经常是前面的配套服务没做好。比如10 02进编程会话如果ECU不支持从当前状态直接跳转你可能需要先从默认会话切到扩展会话再切到编程会话。再比如27服务安全解锁密钥算法错了后面34服务连地址都还没报ECU可能就回0x7F 34 33安全密钥过期或者0x7F 34 72。31服务擦除也很关键。很多ECU要求先擦除再下载如果不擦或者擦除范围不对34服务可以正常通过但36服务写到一半就会报错。所以我给你的建议是排查刷写问题先别怀疑34、36、37本身而是把前面的会话切换、安全解锁、擦除例程全部确认一遍。这跟盖房子先打地基一个道理。3. CANoe实战从工程配置到Trace报文逐帧解析3.1 环境准备与CANoe诊断通道配置要在CANoe里复现一次刷写你至少需要一个CANoe授权带Diagnostics选项、一个支持CAN/CAN FD的硬件接口、一份ECU的诊断描述文件CDD或者ODX以及一个带目标ECU相关的DBC或CANoe工程。拿到这些之后先在CANoe的Simulation Setup里添加CAN/CAN FD通道并把对应的DBC挂到通道上。然后在通道属性里配置Diagnostics/ISO TP导入CDD文件。这一步很关键如果通道上没有挂诊断描述文件后面Diagnostic Console里就看不到服务定义Trace窗口也不会自动解码出服务名你只能手工拿原始字节对协议。诊断通道配置好之后可以用诊断控制台Diagnostic Console做一次手动刷写验证。左侧选择ECU节点右侧服务列表里就能看到根据CDD解析出来的全部诊断服务。选择34服务在参数栏里填好dataFormatIdentifier、addressAndLengthFormatIdentifier、memoryAddress和memorySize点击发送就能看到CANoe帮你组好的报文以及ECU回的正响应。这个手动操作非常直观适合第一次摸流程的人。3.2 用CAPL把刷写流程自动化起来手动操作适合调试单个服务但完整刷写流程几十甚至上百个步骤还是得靠CAPL脚本。CAPL的写法不复杂核心是用diagRequest对象定义服务、用diagSendRequest发送请求、用on diagResponse回调处理响应。这里我贴一个简化的框架实际使用时你需要根据自己CDD里的服务名和ECU节点的命名进行调整。/* 简化版CAPL刷写流程框架实际使用时需根据CDD服务定义调整 */ variables { diagRequest ECU.10_RequestSession reqSession; diagRequest ECU.27_RequestSeed reqSeed; diagRequest ECU.27_SendKey reqKey; diagRequest ECU.31_RoutineControl reqErase; diagRequest ECU.34_RequestDownload req34; diagRequest ECU.36_TransferData req36; diagRequest ECU.37_RequestTransferExit req37; byte fileData[0x4000]; long fileSize 0; long sentSize 0; byte blockSeq 0x01; long blockLen 0x80; } on key u { // 从文件读取固件到fileData缓冲区再进入编程会话 fileRead(D:\\project\\app.bin, fileData, 0); fileSize 0x1234; // 示意大小实际按文件字节数取 diagSendRequest(reqSession); } on diagResponse ECU.10_RequestSession { // 进入编程会话成功后开始安全解锁流程 if (diagGetLastResponseCode() 0) diagSendRequest(reqSeed); } on diagResponse ECU.27_SendKey { // 解锁成功后先做擦除 if (diagGetLastResponseCode() 0) diagSendRequest(reqErase); } on diagResponse ECU.31_RoutineControl { // 擦除完成后发送34服务声明下载意图 if (diagGetLastResponseCode() 0) diagSendRequest(req34); } on diagResponse ECU.34_RequestDownload { // 解析34正响应中的maxNumberOfBlockLength按它切分数据 diagGetParameter(req34, maxNumberOfBlockLength, blockLen); sentSize 0; blockSeq 0x01; // 这里把req36的第一个数据块参数填好后发送 } on diagResponse ECU.36_TransferData { // 每收到一个正响应就更新数据指针和BSC继续下一块 sentSize blockLen; blockSeq; if (sentSize fileSize) { // 继续发送下一块 } else { // 数据全部发送完毕请求传输退出 diagSendRequest(req37); } } on diagResponse ECU.37_RequestTransferExit { // 刷写完成按需复位ECU或者读取状态 }你看这段代码其实把前面讲的流程都串起来了。实际项目里你还可以在这基础上增加错误处理、超时重发、逐帧打印日志等逻辑。不过要提醒一句diagRequest对象在CAPL里通常需要根据CDD生成不同版本CANoe的API参数访问方式略有区别用的时候多参考自带帮助。3.3 Trace报文逐帧分析一眼看懂各自信号的帧接下来我们看Trace。我模拟一个实际的经典CAN刷写片段下面表格中是进入编程会话后从34服务开始到37服务结束的核心报文。时间通道ID方向数据Hex说明0.050000CAN10x7E0Tx10 09 34 00 42 00 00 2034服务首帧FF总长度9字节0.050045CAN10x7E0Tx21 00 12 3434服务连续帧CFSN10.050210CAN10x7E8Rx05 74 20 00 8034正响应maxNumberOfBlockLength0x00800.060000CAN10x7E0Tx10 82 36 01 01 02 03 0436服务首帧FF总长度130字节BSC0x010.060045CAN10x7E0Tx21 05 06 07 08 09 0A 0B36服务连续帧CFSN10.060090CAN10x7E0Tx22 0C 0D 0E 0F 10 11 1236服务连续帧CFSN2………………0.068120CAN10x7E8Rx02 76 0136正响应回显BSC0x010.070000CAN10x7E0Tx02 3737服务请求0.070350CAN10x7E8Rx02 7737正响应先看第一组34服务的报文。CANoe Trace里看到的原始数据是经过ISO-TP层封装的0x10 09 表示首帧0x09是后续全部UDS数据的长度0x21是连续帧高四位0x2表示这是连续帧低四位1是帧序号。把这两个CAN帧的有效数据拼起来就是34 00 42 00 00 20 00 12 34跟前面我们设计的请求完全一致。这里要特别注意ISO-TP的帧序号CF SN是4位的从1开始0x0F之后下一帧回到0x00循环使用。这个4位帧序号和36服务里的8位块序列计数器BSC是两码事。很多人抓Trace时看到一堆“21、22、23”就以为是BSC在变其实那只是网络层在拆包BSC是UDS层里的概念。再来看36服务的分帧。130字节的UDS数据用经典CAN传输首帧只能带6字节剩下124字节都要靠连续帧一帧7字节地搬所以总共需要18个连续帧。你在Trace里会看到0x21、0x22、0x23一直到0x2F然后再到0x20、0x21……这种循环。如果E CU在中途少回了一个肯定响应你排查的时候优先去数的就是这个4位帧序号的连续性。36正响应只有3个字节0x02 76 01。0x02是单帧标识76是正响应SID01是刚才那一块的BSC回显。看到这个就说明ECU成功接收了BSC0x01的那个数据块。最后是37服务。协议层面它真的只有两个字节但ECU内部要做的事情很多所以Trace里从0.070000发出请求到0.070350才收到正响应这个350ms的间隔比前面的34、36肯定响应都长。如果你的P2超时时间设置得太短比如150ms这350ms就会被误判为超时。这是一个非常常见但容易被忽略的坑。3.4 帧开销计算经典CAN和CAN FD的差别有多大刷写性能的差异在Trace里一眼就能看出来。同样是128字节数据块经典CAN下130字节UDS数据需要19个CAN帧1个FF 18个CF加上36正响应30帧。如果整个固件是0x12344660字节按128字节一块来分一共要发37个36服务光36服务的数据帧就是37×19703帧。这个数量在总线上是很可观的。如果换成CAN FD把DLC配置成64字节ISO-TP每帧连续帧可以携带62字节那么130字节的36服务只需要3个CAN FD帧1个FF 2个CF。37个36服务加起来才111帧左右总帧数直接砍到原来的六分之一还低。所以你会发现现在新平台的ECU刷写基本都推CAN FD不只是因为带宽大更是因为传输效率高带来的刷写时间显著缩短。不过要注意CAN FD的单帧数据长了如果某帧在总线上出现错误或者丢帧重传的代价也会放大。所以刷写块长并不是盲目拉大就好。我的习惯是先看34正响应里ECU自己声明的maxNumberOfBlockLength再结合总线的错误帧率和ECU的P2时间选择一个折中的块长。经典CAN我常选128或256CAN FD我会选512到1024每个项目实际验证后最终确定。4. 刷写现场高频问题排查与避坑记录4.1 34服务收到0x31怎么办0x31requestOutOfRange是34服务最常见的负响应原因基本集中在两个地方请求参数超出了ECU允许的地址范围或者地址和长度格式标识符不被ECU支持。排查思路是先把Trace里的请求报文完整调出来看这个请求是“34 00 42 00 00 20 00 12 34”还是别的样子。然后把memoryAddress和memorySize和你手里的ECU内存映射表对一遍确认起始地址在可编程Flash范围内终点地址没有越过Flash末尾。如果地址没问题再检查格式标识符0x42是不是ECU支持的组合有些老ECU只认地址和大小都占4字节的0x44你发0x42过去它会直接拒收。还有一种情况就是34服务之前必须先进编程会话和安全解锁状态不对的时候ECU也会回0x31而不是0x72。这时候不要把NRC当成了参数问题要先看会话状态代码和27服务解锁的结果。4.2 36服务传一半中断BSC错乱和数据量不符36服务在刷写中指最容易出事。常见的现象是传到某一包之后ECU突然回负响应0x73或者干脆没有任何响应CANoe里显示请求超时。遇到0x73时优先检查两件事这一包的BSC是否严格等于上一块的BSC加1这一包携带的数据长度是否小于等于34正响应里的maxNumberOfBlockLength。BSC跳号了ECU会认为有丢包数据长度超了ECU会认为消息格式不对。这两点在Trace里一眼就能确认因为Trace上每一帧36请求的第二个UDS字节就是BSC。如果ECU完全没回响应那问题往往不在UDS层而在网络层或者硬件层。常见原因包括ISO-TP连续帧之间时间间隔超过ECU接收窗口或者ECU在刷写过程中被复位。这时先检查Trace里有没有大量的错误帧再看整个刷写过程中ECU有没有重新上电的报文最后确认供电电压有没有跌落。4.3 Trace窗口常见疑难没有解码、看不到ID、ID Name空白关于Trace窗口回复群里也有人问过“为什么ID Name列是空白的”。这个问题九成是数据库没有正确关联。你在Simulation Setup里挂了DBC但Trace窗口左上角选择了别的通道或者Trace的显示配置里把Name列隐藏了都会导致ID Name空白。处理办法分两步第一步右键Trace窗口的列头在列配置里确保勾选了Name、Type、Dir、ID、Data等列第二步在Simulation Setup里确认当前Trace通道选中的是绑定了DBC和CDD的那个CAN通道。如果两件事都做到位了Trace窗口不仅会有ID名称还会把诊断服务解码成可读的服务名比如显示“RequestDownload”而不是让你自己数34、36、37的原始字节。还有一个类似的坑就是Trace窗口能看到原始CAN报文但看不到诊断层的服务名这时候你需要检查诊断通道是否激活了诊断解码功能。CDD导入只是让服务‘可识别’如果你没有把该通道的诊断层插件打开Trace还是无法知道0x7E0上跑的哪些字节是哪个服务。这个在CANoe的设置面板里多试几次配一次就熟练了。4.4 关于刷写速度和块长度匹配的个人经验最后聊一个在项目里很实际的问题块长度到底怎么选。我在不同项目里验证过的经验是块长度跟ECU的RAM缓冲区、Flash编程时间、总线波特率三者强相关。经典CAN环境下块长选128是相对稳妥的选择因为每一帧只有8字节128字节的数据块已经要接近19帧的CAN报文负载了再大的话总线上等待时间太长很容易碰到P2超时。CAN FD环境下块长可以适当加大到512甚至1024但前提是ECU的maxNumberBlockLength允许并且它的接收缓冲区足够大。曾经有个项目ECU自动把响应里的maxNumberBlockLength设为0x10004096我把块长设成4096后每传一块ECU都要处理几百毫秒P2*都快不够用了后来改成1024反而稳定很多。所以不要贪婪稳定的刷写一定是在“块长度”“总线负载”“ECU处理能力”三者之间找平衡。从实践来看把34、36、37三个服务理解透再配上一套好用的CANoe分析环境刷写问题的大头基本就解决了。我做项目这么多年每次刷写成功后都会习惯性地把Trace完整导出来存一份带日期的日志文件备注好ECU版本、CDD版本和固件版本。下次现场出了问题拿出来一比对很多时候原因就已经写在这个Trace里了。这个习惯说不上多高级但确实帮我躲过了不少返工。