
第一次在真实总线上抓UDS诊断报文的时候我被一个现象卡了很久读VIN用的0x22服务请求只有3个字节响应却是20个字节。CAN帧的数据场最多8个字节ECU到底是怎么把这20个字节完整交给诊断仪的答案就是ISO 15765-2也就是大家常说的ISO-TP传输层。它一句话就能讲清把超长诊断消息切块先发一个首帧告诉接收方“总长度是多少”再用若干连续帧逐段搬运中间靠流控帧协商“你一次能发多少、帧间隔多久”。搞懂这三类帧UDS诊断的多帧传输就全通了。这篇文章我打算用一套读VIN的完整报文从首帧、流控到连续帧走一遍全过程再把时序参数的含义、排障时踩过的坑一起讲清楚。适合刚接触UDS诊断的嵌入式工程师也适合正在自研诊断刷写工具的人参考。1. 单帧装不下数据UDS多帧传输到底解决了什么问题1.1 CAN帧的数据场只有8字节经典CAN帧最常用的数据场长度是8字节这是控制器和总线在仲裁、填充位、收发器负载等条件下权衡出来的结果。8字节对控制类报文足够用但对诊断消息来说非常局促——UDS的一个响应经常要携带VIN码、DTC状态、内存数据甚至固件数据。0x22读VIN至少返回17字节0x19读DTC信息可能返回几十个字节0x36传输数据时更是动辄几百上千字节。诊断协议必须有一个方案能跨越多帧传输同时让应用层不用关心底层的拆分过程。1.2 UDS应用层为什么不管拆包UDS在OSI模型里属于应用层它定义的是服务语义比如0x22后面跟两个字节的数据标识符响应里先回0x62再回数据内容。至于这个响应在CAN上怎么发出去UDS规范本身不负责。ISO 15765-2恰好补上了这个空档发送方把应用层消息分段成若干CAN帧接收方根据帧头里的协议控制信息把分段重新拼成完整消息再提交给上层的UDS。这种分层设计让UDS服务实现者完全感知不到多帧的存在——数据量小就发单帧数据量大就自动走多帧应用层看到的始终是完整消息。1.3 “发送方”和“接收方”是相对消息方向而言的多帧传输由三件事组成发送方先发首帧FF宣告总长度接收方收到后回一个流控帧FC表示“你可以继续发了”发送方再按协商好的节奏发连续帧CF。这里最容易被误导的是“接收方”的指向。读VIN的场景里ECU是响应消息的发送方诊断仪才是接收方所以流控帧由诊断仪发出目的地址是ECU的响应ID。反过来做0x36下载时诊断仪是发送方ECU收到首帧后回流控帧流控帧的CAN ID就是ECU的物理响应地址。把这一条搞反看抓包时会把方向标错排查问题会越查越乱。2. 首帧、流控帧、连续帧的报文结构逐字段拆解2.1 第一个字节的PCI就能判断帧类型ISO-TP所有帧的第一个字节都叫PCI字节高4位直接决定帧类型。看到0x0开头是单帧0x1开头是首帧0x2开头是连续帧0x3开头是流控帧。我在初学时就靠这个口诀快速扫描抓包文件。帧类型PCI高4位PCI占字节数一帧可携带的诊断数据作用单帧 SF0x01最多7字节短消息一次发完首帧 FF0x126字节宣布总长度并携带开头数据连续帧 CF0x21最多7字节携带后续数据按序号排列流控帧 FC0x33—控制发送节奏PCI低4位在不同帧里的含义完全不同单帧里表示数据长度首帧里和第二个字节拼总长度连续帧里是序号流控帧里是流状态FS。下面逐类说。2.2 首帧那12位长度千万别读错首帧的PCI字节是0x1X这里的X不能当成长度的高4位直接参与计算。正确做法是把第一个字节的低4位和第二个字节拼成一个12位数值length ((byte0 0x0F) 8) | byte1。抓包经常看到的 10 14表示总长度是0x014即20字节而不是0x114即276字节。这个错我见过不止一次后果是接收方按照错误的总长等待后续帧永远等不齐最后超时。12位长度上限是4095字节这也是单次ISO-TP消息的最大长度。想传输更大的数据块比如刷写固件应用层必须自己把数据拆成多个4095字节以内的块。2.3 流控帧的FS、BS、STmin流控帧只用了三个参数。第一个字节0x3XX是流控状态FS0表示继续发送1表示请等待2表示过载中止。第二个字节BS叫块大小含义是“每发多少个连续帧后我要再回一个流控帧”如果BS为0则代表“你后续不用等我流控了一口气把剩下的连续帧发完”。第三个字节STmin叫最小间隔时间表示发送方相邻两个连续帧之间至少间隔多久。FSWait在实际总线中不太常见因为绝大多数接收方要么能处理就直接CTS处理不了就直接Overflow用Wait拖着的实现容易把发送方挂在半路。但协议栈如果收到Wait正确的做法是原地暂停而不是放弃当前传输等下一个FC再恢复。STmin值含义0x00无间隔要求发送方可不加延时0x01~0x7F1ms~127ms0xF1~0xF9100us~900us步进100us0x80~0xF0、0xFA~0xFF保留2.4 连续帧的序号从F回到0连续帧的PCI字节是0x2NN是4位序列号。首帧发完后第一个连续帧的序号固定是1之后每发一帧加1加到F后回到0继续不需要接收方额外发流控来重置序号。以4095字节上限计算首帧带走6字节剩余4089字节按每帧7字节拆分约585个连续帧序列号会绕很多圈。所以判断序号是否连续时逻辑应该是(期望序号 (上一帧序号 1) 0x0F)而不是简单地要求新序号大于旧序号。很多小工具就是在这个回绕点上翻车的。3. 一次读VIN的完整报文走读3.1 报文现场还原下面是一段我在调试T-Box时抓到的读VIN报文请求是0x22 F1 90响应内容包含服务ID 0x62、数据标识符0xF1 0x90和17字节VIN码总长20字节跨首帧加两个连续帧发送。序号 方向 CAN ID DLC DATA 0x123 Tester→ECU 0x7E0 8 03 22 F1 90 AA AA AA AA 0x124 ECU→Tester 0x7E8 8 10 14 62 F1 90 4C 53 56 0x125 Tester→ECU 0x7E0 8 30 00 32 00 00 00 00 00 0x126 ECU→Tester 0x7E8 8 21 41 4D 34 31 38 37 43 0x127 ECU→Tester 0x7E8 8 22 32 31 38 34 38 35 35第0x123行是诊断仪发出来的单帧请求0x03表示后面有3个有效数据字节即22 F1 90剩余AA是填充字节接收方不会把它们当数据。第0x124行是ECU回的首帧0x10 0x14解析出的总长度是20字节紧跟其后的6个字节是响应数据的开头62 F1 90 4C 53 56其中62是0x22的肯定响应服务ID。第0x125行是流控帧0x30 00 32表示FS0继续发送、BS0没有块限制、STmin50ms。这一帧是整段报文的关键。第0x126行和第0x127行是两个连续帧序号分别是1和2各自携带7字节数据。把首帧的6字节和两个连续帧的14字节拼起来正好20字节VIN读取完成。3.2 流控帧到底是谁发的这段报文里最容易看错的是第0x125行流控帧出现在CAN ID 0x7E0上方向是Tester→ECU。很多刚从单帧思路转过来的同学看到0x30就以为是ECU在报错其实这是诊断仪在自己的请求地址上发送流控帧行使的是接收方权力。ECU收到后才会把剩余连续帧发出来。判断方向时永远先问一句当前消息是谁发给谁的谁在接收这条多帧消息谁就有资格发FC。3.3 当BS不是0时时序就多了几个来回很多ECU的响应流控都习惯用BS0因为响应数据长度不大没必要分段限制。但接收方如果需要周期性处理数据可以设置BS2。假设ECU要回复一个34字节的响应完整的交互时序应该是这样的步方向帧数据说明1ECU→Tester10 22 D1 D2 D3 D4 D5 D6FF总长34字节2Tester→ECU30 02 0AFCBS2STmin10ms3ECU→Tester21 D7 D8 D9 D10 D11 D12 D13CFSN14ECU→Tester22 D14 D15 D16 D17 D18 D19 D20CFSN25Tester→ECU30 02 0A第二个FC允许再发2帧6ECU→Tester23 D21 D22 D23 D24 D25 D26 D27CFSN37ECU→Tester24 D28 D29 D30 D31 D32 D33 D34CFSN4注意第5步到来之前发送方发完第4步后必须停下来等待不能自作主张继续发CF。第二段连续帧的序号从3继续而不是被重置回1。3.4 连续帧之间允许插入其他CAN报文还有一个经常被抓包工具误导的地方连续帧之间的“连续”指的是序列号连续不是时间上背靠背。总线上有大量高优先级报文时ECU发出的CF2可能延迟几百微秒甚至几毫秒才上总线中间会插入别的CAN ID报文。接收方的重组逻辑只要按序号取数据就行不能因为CF之间混入了其他报文就判断为异常。这也是我建议排查时先看原始CAN帧层面再去看ISO-TP解码层的原因。4. STmin、BS和超时参数背后的工程逻辑4.1 STmin不是越小越好也不是越大越稳STmin由接收方在FC里给出发送方必须遵守。取值0x00意味着接收方不要求帧间间隔发送方可以连续把CF发出去吞吐最高但前提是接收方的CAN控制器和上层软件能跟得上。经典CAN上8字节一帧在500kbps下本身就要占大约两百微秒所以100us级的STmin0xF1~0xF9在经典CAN上没有实际意义更多是用在带宽更高的总线上。反过来如果接收方是慢速MCUSTmin取50ms甚至100ms能显著降低丢帧概率代价是整条响应耗时变长。实际项目里0x3250ms是一个比较稳妥的中档值很多OEM的响应流控都愿意用它。4.2 块大小BS是在拿时间换空间BS的本质是接收方给发送方的一个“额度”。若接收缓冲区能装下全部消息抛0x00最省事若缓冲区有限就抛一个BS发满额度后强制对方暂停接收方利用间隙处理数据、腾出缓冲再发下一个FC。代价是总线上多出一来一回的额外帧和等待时间。Bootloader刷写场景经常用BS配合STmin因为ECU在擦除Flash或写Flash期间CPU忙不过来一次性收太多帧只能丢。4.3 超时参数是分角色看的ISO 15765-2定义了一组以N_开头的超时名字工程上最关心四条N_Br是发送方发出FF后等待FC的时间N_Cr是接收方发出FC后等待CF补齐的时间N_Bs是接收方收到FF后准备发FC的时间N_Cs是发送方收到FC后准备下一帧的时间。具体毫秒值各OEM有不同规范常见默认在1000ms左右。我习惯用一个参考配置超时角色监控的事件常见默认值N_Br发送方发完FF后等FC1000msN_Bs接收方收到FF后准备发FC1000msN_Cr接收方发完FC后等CF补齐1000msN_Cs发送方收到FC后准备下个CF小于STmin配置值记不住字母没关系原理就是双方都要有超时保护协议才不会因为某一帧丢失而永久卡死。实际项目一律以OEM诊断规范为准。4.4 为什么总长上限是4095字节4095由12位长度字段决定这是ISO 15765-2的硬限制。要传更大块数据应用层必须拆成多个消息比如0x340x36刷写就是一次只传一段每段控制在4095以内。这个问题在Bootloader软件里几乎每天都要处理看到上位机把4MB固件切成4095字节的块不要觉得奇怪根本原因就是ISO-TP单次消息装不下。5. 实测多帧传输最容易踩的五个坑5.1 首帧长度解析错误有个同事自研诊断仪读VIN总是超时。抓包看ECU明明回了 10 14 和一串连续帧工具却一直说消息未完成。代码里把长度写成了0x114按276字节等后续帧自然等不到。改成((byte0 0x0F) 8) | byte1之后就好了。这种错很容易出现在从别处抄来的解析代码里而且8字节报文从界面上看很难一眼发现差别只能靠抓包软件自带的ISO-TP解码来对照。5.2 把BS0理解成“不需要流控”导致死等反过来也有案例。某个ECU代码收到FC后判断BS0以为“不需要流控”就是“两边都不用管了”结果每发一个CF都等接收方再发FC而接收方发的又是BS0认为发送方会自己发完两边都等对方直到N_Br和N_Cr超时。BS0的确切含义是“不需要额外来回复用FC控制节奏”发送方收到后应连续发完剩余CF只是每帧之间仍要遵守STmin。5.3 序列号到F之后不认0读DTC快照这类服务响应经常上百字节CF数量多到序号会回绕。有些接收栈只实现了if(SN expectedSN)这种写法在SN从F变0的那一刻判定为乱序把前面拼好的数据全丢了。正确逻辑是每次维护expectedSN (expectedSN 1) 0x0F只要收到的SN等于它就认为连续。5.4 收到Wait或Overflow时处理不当FC的FS值不只是0。收到Wait要暂停而不是放弃收到Overflow应该中止当前传输并向上层报错由应用层决定是否重发。很多初版协议栈只处理了FS0收到其他值直接当错误帧丢掉结果是发送方继续发CF接收方缓冲区已经溢出整个重组过程乱掉。诊断仪侧发送多帧时尤其要处理这两种情况因为ECU侧的资源限制比PC侧严格得多。5.5 只看解码层不看CAN原始帧CANoe、CANalyzer这类工具会把ISO-TP自动解出来Trace界面直接显示“xxx bytes reassembled”。方便是好但排查问题容易偷懒。我自己的习惯是先切到原始CAN帧视图把FF/FC/CF的ID、DLC、数据列全都摆出来确认每一帧都在、顺序对、时间戳符合预期再回到解码层看结果。有两次排查“ISO-TP重组失败”最终都是因为CF帧被高优先级报文挤掉或CAN收发器重试导致的物理层丢帧解码层看不出这些细节。6. 跑通之后这套机制在日常工具链里怎么落地6.1 抓包工具怎么配合使用Linux环境我常用can-utils的candump看原始CAN帧再用isotpsend和isotprecv模拟ISO-TP单侧收发调试速度很快。Windows下PCAN-View加Wireshark的USB-CAN extcap插件也能拿到原始帧再手动解码。如果是用Vector硬件CAPL里可以直接发送指定BS和STmin的FC帧用来验证ECU对不同流控参数的行为。6.2 接收侧状态机最少要有这几个状态无论是ECU还是诊断仪实现ISO-TP接收侧最少要有IDLE空闲等FF/SF、RECV_FF收到FF后解析长度并回FC、RECV_CF按SN收连续帧直到长度满足三个状态。发FC这件事必须落在RECV_FF状态里自动完成RECV_CF里要同时做BS计数和总长度检查。发送侧则对应WAIT_FC等首次FC、SEND_CF按BS和STmin发、WAIT_FC_BS一块发完后等下一个FC三个状态。把状态机画清楚比在业务代码里到处处理帧拼接要可靠得多。6.3 几个可以直接拿走的健壮性建议最后给几条我自己沿用很久的经验。第一接收缓冲区按最大消息长度预留如果协议栈只能处理单帧对长度超过7字节的FF要主动报错而不是截断。第二重组过程中收到同一个CAN ID的新SF或FF说明上层重新发起了一次消息应该丢弃旧的半包上下文。第三诊断仪做发送方时收到FC后先把BS和STmin解析保存再启动发送循环不要在循环里反复解析。第四CAN控制器发送队列可能缓冲多帧如果先用软件模拟ISO-TP再发给CAN驱动STmin要由软件在写驱动前就睡眠控制好否则背靠背发出去STmin就名存实亡了。多帧传输看起来就那么几类帧真正的麻烦都藏在参数解释、方向判断和序号回绕这些细节里。把上面几条吃透再回头看抓包文件诊断多帧基本不会被卡住。