
去年协助客户排查一台纯电动车的Bootloader刷写失败现象很经典0x34请求下载正常0x36传输数据一上来ECU就回7F 31服务不支持紧接着连接断开。查到最后问题出在传输层的多帧重组上——连续帧的SN序列号在某个时刻发生了跳变ECU接收端直接丢弃了整个ISO-TP报文。这类问题如果不把CAN/CAN FD诊断通信里的单帧与多帧机制吃透光靠肉眼盯报文列表真的很难定位。这篇内容围绕UDS诊断协议里最容易被忽视、又最影响实战的传输层机制展开。适合刚入门的诊断工程师、BMS/VCU/ECU软件开发者以及做自动化测试的兄弟们。你不需要提前掌握太深的总线知识只要写过CAN报文看得懂十六进制就能跟着把单帧、首帧、连续帧、流控帧这套逻辑完整串起来。1. 先搞清楚单帧与多帧到底在解决什么问题1.1 两条总线的“小信封”与“大信封”经典CAN的数据场只有8个字节CAN FD把上限提到了64个字节。这个变化在普通通信里可能感受不明显但在UDS诊断场景下差异非常大。一条UDS诊断请求往往包含服务ID、DID、数据三个部分。比如0x2E WriteDataByIdentifier写一个DID2E加两个字节的DID再加数据稍微长一点的数据就突破8字节。而在Bootloader刷写过程中0x36 TransferData单条数据往往达到几百甚至上千字节别说8字节64字节也装不下。所以ISO 15765-2出现了大家习惯叫它ISO-TP。它的作用就是在CAN/CAN FD的数据帧之上做一层“分包与重组”把上层超过总线帧长度限制的诊断数据拆成多个CAN帧发送接收端再拼回去。单帧和多帧指的就是这个传输层里的两种工作形态。1.2 单帧、多帧的真实含义单帧Single FrameSF比较好理解一个CAN帧的数据场里放得下完整的UDS消息就用一个帧解决。比如0x22读一个DID请求和正响应通常都在几个字节内完成经典CAN一个8字节帧就搞定了。多帧Multi Frame就是拆包。ISO-TP把一个较大的诊断数据拆成一个首帧First FrameFF 若干个连续帧Consecutive FrameCF再配合接收方返回的流控帧Flow ControlFC来控制节奏。我用一个生活化的类比单帧就像一句话能说完的事情直接说。多帧是写一封信信封上写清楚总共有几页然后一页一页递过去对方每隔几页回一个“收到继续”。这个“总共有几页”就是首帧里的12位长度字段“每隔几页回一次”就是流控帧里的块大小BS“每两页之间的停顿”就是STmin。很多刚接触UDS的人会被单帧多帧搞晕其实是把数据链路层和传输层混在一起看了。数据链路层的CAN帧每次就发8字节或者64字节传输层的ISO-TP决定这8字节或64字节里有多少是真正的“内容”、多少是“控制信息”。2. 一个诊断帧的“壳”PCI字节与CAN/CAN FD帧格式2.1 经典CAN与CAN FD的数据场差异动手解析之前先看清总线帧的差异。经典CAN数据场固定8字节CAN FD从0到64字节可变由DLC字段决定。CAN FD还多了一个BRS位表示是否有位率切换诊断数据段可以跑到更高的速率。另一个差异是CRCCAN FD的CRC覆盖更长的数据但这部分对ISO-TP解析没有直接影响抓包工具会自动处理。实际诊断开发中CAN FD的DLC和ISO-TP的SF_DL是两个不同层面的长度。CAN FD的DLC是64不代表SF_DL就是64。SF_DL表示的是这一帧里真正给上层UDS协议用的字节数需要减去ISO-TP的协议开销。2.2 ISO-TP首字节0x0x、0x1x、0x2x、0x3xISO-TP的每一帧第一个字节叫PCI字节高半字节是帧类型低半字节是附加信息。看这个字节就知道当前是哪种帧。单帧0x00到0x0F低4位是SF_DL表示单帧里的用户数据长度。首帧0x10到0x1F低4位与第二字节合起来组成12位的FF_DL表示整个多帧报文的总长度。连续帧0x20到0x2F低4位是SN序列号。流控帧0x30到0x3F低4位是FS流控状态。抓包时看到0x02开头的就是连续帧0x30开头的是流控帧这个判断几乎不会错。容易错的是单帧和首帧因为首帧的0x10和单帧的0x00之间只差一个高半字节如果只看低4位就容易被误导。2.3 扩展单帧与“单帧解码长度”的误区有个搜索热词“aac单帧解码长度是多少”很多刚接触数据解析的工程师会搜到这个然后产生困惑。这里要说明白AAC音频里的“单帧”是音频编码帧一个AAC帧通常包含1024个采样点和UDS诊断里的单帧完全是两个世界。UDS诊断里的单帧长度要看ISO-TP首字节的低4位和CAN总线类型。在经典CAN里单帧最多装7字节用户数据因为8字节数据场减1字节PCI。在CAN FD里单帧最多可以是63字节但前提是使用扩展单帧格式当数据长度超过7字节时首字节低4位填0第二个字节作为扩展长度字段。实际开发中大部分诊断数据在12字节以内单帧在CAN FD上非常常见。3. 单帧SF机制详解一个CAN帧装下整个对话3.1 单帧的组成与最大长度单帧的结构分三段CAN ID、PCI字节、用户数据。以0x7E0为物理请求ID为例发送一个0x22读DID F190的请求完整CAN帧数据是02 22 F1 90。其中0x02是PCI低4位的2表示后面有2个用户数据字节0x22是服务IDF1 90是DID。这里有个关键点SF_DL只算UDS层的数据长度不含PCI本身。很多人在手工写脚本时把SF_DL多算一个字节导致接收端的ISO-TP层解析出的长度与实际数据不匹配整个报文被当作错误帧丢弃。经典CAN下SF_DL最大7CAN FD扩展单帧最大63。实际设计UDS服务时能用单帧完成的交互设计越简单越好。单帧没有流控过程一端发一端收出错概率极低。3.2 典型场景0x22与0x2E的短数据交互0x22 ReadDataByIdentifier是最典型的单帧场景。请求固定4字节正响应通常也不长。比如DID F190存储一个4字节的车辆里程值正响应格式是06 62 F1 90 00 00 1A 2B。PCI的0x06表示用户数据6字节62是0x22的正响应SIDF1 90是DID后面4字节是里程值。0x2E WriteDataByIdentifier如果写入数据较短也能单帧完成。比如写入一个DID格式是2E F1 90 01 02 03 04共7字节用户数据经典CAN刚好一个单帧装下PCI是0x07。如果写的数据超过7字节经典CAN就必须走多帧了。3.3 单帧解析中的常见误判单帧解析最常见的问题是把PCI当作数据的一部分直接送给上层应用。比如收到报文07 2E F1 90 01 02 03 04如果直接把从第0字节开始的内容全交给UDS层上层就会收到一个错误的0x07作为服务ID。正确做法是先解析PCI识别为单帧按SF_DL截取后面的数据。另一个误判是地址模式。ISO-TP有普通寻址和扩展寻址扩展寻址会在PCI之前多一个目标地址字节。抓包时如果遇到了单字节的地址标识解析PCI时要先跳过这个地址字节否则整个长度和内容都会错位。4. 多帧FF/CF/FC机制详解接力赛怎么跑4.1 首帧FF12位长度字段的含义当UDS消息超过一个CAN帧的承载能力时发送方先发首帧。首帧的PCI格式是第一个字节的高4位是1低4位是FF_DL的高4位第二个字节是FF_DL的低8位合起来12位最大4095。这就是ISO-TP单条消息最大长度4095字节的由来。首帧之后紧跟的一帧连续帧序号为1。所以抓包时看到第一个连续帧SN1说明这是首帧之后的第一个数据块。有些工具显示FF的SN为0这是内部实现抓包时要留意。首帧里能带多少实际数据经典CAN下8字节数据场减去2字节PCI能带6字节。CAN FD下64字节数据场减去2字节PCI能带62字节。这就是为什么同样一条大报文CAN FD下首帧携带的数据是经典CAN的10倍。4.2 流控帧FCFS、BS、STmin三个参数接收方收到首帧后并不是立刻开始收连续帧而是先回一个流控帧告诉发送方“我准备好了你按这个节奏发”。流控帧一共3个字节格式是PCI BS STmin。PCI低4位是FS流控状态0表示CTS继续发送1表示WAIT等待2表示OVFLW溢出/中止。BS是Block Size表示允许发送方在收到下一个流控帧之前最多发多少个连续帧。0是个特殊值表示不限制直到发完。STmin表示两个连续帧之间的最小间隔时间。这三个参数理解起来有点绕我实际调试时这么记BS控制“一次发几个”STmin控制“发完一个等多久”FS控制“能不能发”。BS0的意思是“你一口气全发完我扛得住”STmin0的意思是“你发完一帧立刻发下一帧不用故意等”。4.3 连续帧CF与SN编号的接力规则连续帧的PCI格式是高4位是2低4位是SN序列号。SN是0到F循环递增首帧之后的第一个连续帧SN1第二个SN2一直到F下一个回到0。SN的本质是接收端的重组依据。接收方收到FF后知道总长度收到第一个CF后把SN1的数据填到缓冲区偏移6的位置收到SN2的填到下一个位置以此类推。如果SN出现跳变比如上一个收到5下一个收到7ISO-TP层就会判定为错误整个多帧报文直接丢弃。这里有个实战细节SN跳变不代表一定是丢帧也可能是接收端在启动时把SN初始化为0而发送端还在按上一个报文的序列继续数。常见于连续执行多次多帧诊断时发送端和接收端的状态没有同步。4.4 多帧传输完整时序与超时参数一次完整的UDS多帧交互时序是这样的发送方先发FF等待接收方回复FC。接收方检查缓冲区后回FCFS0带上BS和STmin。发送方收到FC后按BS和STmin发若干个CF如果没有发完且BS不为0就再等下一个FC循环直到所有数据发完。整个过程中任何一步超时都会导致传输失败。ISO 15765-2定义了N_As、N_Ar、N_Bs、N_Cr等超时参数。N_Bs是发送方等待流控帧的超时时间一般设置为1000msN_Cr是接收方等待连续帧的超时时间也是1000ms量级。调试时如果发现发送方发完FF后一直等FC不到先看接收方有没有进到接收流程再看N_Bs设置是不是被配成了几十毫秒。这类问题在自动化测试台上很容易复现。5. 实战拆解19字节DID写入的完整抓包5.1 模拟场景与报文构造我构了一个很常见的场景通过0x2E向ECU写入DID F190数据内容16字节总用户数据是2E F1 90加16字节共19字节。经典CAN下19字节超过了单帧7字节的上限必须走多帧。总线参数按标准UDS设置请求ID 0x7E0响应ID 0x7E8。发送方构造流程如下FF0x10 0x13高4位1是首帧低12位0x01319表示总长度19字节携带前6字节数据。FC接收方收到FF后回复0x30 0x00 0x0A表示CTSBS0不限制STmin10ms。CF10x21开头SN1携带7字节数据。CF20x22开头SN2携带剩余6字节数据。5.2 经典CAN下的FFCF拆包过程下面直接把抓包数据写出来方便大家对照。第一条报文ID 0x7E0数据10 13 2E F1 90 01 02 03。0x10 0x13是首帧PCI后面6字节是第一批数据包括服务ID、DID和前3个数据字节。第二条报文ID 0x7E8数据30 00 0A。流控帧CTSBS0STmin10ms。第三条报文ID 0x7E0数据21 04 05 06 07 08 09 0A。0x21表示连续帧SN1携带7字节数据。第四条报文ID 0x7E0数据22 0B 0C 0D 0E 0F 10。0x22表示连续帧SN2携带6字节数据。到这里19字节收齐。接收端重组时先从FF_DL得知总长19把FF的6字节放到偏移0到5CF1的7字节放到偏移6到12CF2的6字节放到偏移13到18拼出完整的UDS数据2E F1 90 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10。5.3 CAN FD下的对比少了多少帧同样的19字节数据如果用CAN FD传输单帧就可以装下。CAN FD数据场64字节首字节是PCI如果用户数据19字节SF_DL19一个CAN FD帧就完成了不需要FF、FC、CF这套流程。这就是CAN FD对Bootloader刷写效率提升最直观的地方。一个64字节的CAN FD帧可以承载原来8个经典CAN帧才能装完的数据而ISO-TP的控制开销依然只有1到2个字节。实际刷写过程中大块数据从经典CAN切换到CAN FD后用时往往能缩短一半以上。5.4 抓包后的解码脚本平时排查问题我会写个小脚本快速把多帧拼起来。这个Python函数可以把一组ISO-TP帧还原成完整数据思路很简单def iso_tp_unpack(frames): frames: list of bytes每个元素是一帧CAN数据场 返回完整UDS数据或抛出异常 if not frames: raise ValueError(empty frames) pci frames[0][0] frame_type pci 0xF0 if frame_type 0x00: # 单帧 length pci 0x0F if length 0 and len(frames[0]) 2: # CAN FD扩展单帧第二字节为长度 length frames[0][1] return frames[0][2:2 length] return frames[0][1:1 length] if frame_type 0x10: # 首帧 total ((pci 0x0F) 8) | frames[0][1] data bytearray(frames[0][2:]) # 连续帧按SN顺序拼接 for frame in frames[1:]: p frame[0] if (p 0xF0) ! 0x20: raise ValueError(expected consecutive frame) data.extend(frame[1:]) if len(data) total: raise ValueError(incomplete multi-frame message) return bytes(data[:total]) raise ValueError(not a single or first frame)实际使用时要处理CAN FD下连续帧一帧携带63字节的情况这个函数里没有额外判断但思路是通用的看PCI识别帧类型用FF_DL确认总长度再按连续帧到达顺序拼接。6. 常见问题与排查技巧实录6.1 长度字段算错接收端直接丢帧多帧传输最隐蔽的问题发生在长度字段。FF_DL表示的是整个ISO-TP消息的用户数据长度不包含PCI本身。有人会把FF里的实际数据字节数当总长比如FF带了6字节数据就写FF_DL6结果ECU按6字节重组只取了前6字节后面的数据全部丢失。建议构造报文时先数清UDS数据总长再决定用单帧还是多帧。长度单位是字节不是帧数这个一定要盯死。6.2 SN跳变导致多帧卡死连续帧SN跳变的场景我遇到过好几次。最典型的是发送方在每次开始新的多帧时SN没有重置为1而是延续了上一次的序号。接收方重组时发现SN不连续直接丢弃。排查方法是把发送方的状态机打出来看每次多帧开始时SN的初值。正常流程是每次进多帧发送FF发完后第一个CF的SN必须是1不管上一次多帧结束时的SN是多少。6.3 经典CAN与CAN FD混用时的DLC陷阱如果整车网络里既有经典CAN节点又有CAN FD节点ISO-TP报文可能会经过网关转发。网关在做CAN FD转经典CAN时会把64字节数据场截断成8字节如果原报文是CAN FD单帧数据长度超过7字节转完就必然丢数据。遇到这类问题先确认诊断链路全程的总线类型再检查网关的路由配置。不要在CAN FD总线上发送长度超过7字节的单帧给一个经典CAN节点这是最容易踩的坑。6.4 STmin设太小ECU时报超时STmin的作用是控制连续帧的发送节奏。经典CAN下如果发得很快接收方的CAN控制器忙不过来会产生硬件层面的丢帧。流控帧里STmin0代表不限制有些ECU实现不好就真的会被连续的CF冲垮。我一般建议经典CAN下把STmin设为10ms到20msCAN FD下可以设置更小的微秒级值。如果测试中发现多帧传输成功率不稳定先把STmin调大排除发送节奏问题再回到协议栈层面排查。6.5 我常用的排查手段排查UDS通信问题抓包是基本功。我习惯在CANoe或PCAN里加上ISO-TP解包窗口先看物理层的CAN帧有没有错误帧再看ISO-TP层的重组状态。如果看到某个多帧报文始终停在“等待CF”状态就去翻N_Cr超时计数器基本能定位到接收端漏收帧。另外日志一定要带时间戳。很多多帧问题是时序问题没有精确到毫秒的时间戳很难判断STmin是否生效。抓包工具里把时间分辨率调高能省大量排查时间。最后再分享一个自己的习惯每次拿到一个新ECU我都会先做一次“多帧压力测试”用0x22读一个超长DID连续发100次统计成功率。这一轮就能暴露出SN管理、STmin配置、流控时序上的大部分潜在问题。很多车主在售后碰到的偶发诊断失败其实源头就是这些平时不起眼的传输层细节。