☰
UDS诊断报文多帧传输:ISO-TP首帧、流控帧与连续帧全解析
2026/9/29 17:00:29 网站建设 项目流程

第一次在真实总线上抓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

流控帧只用了三个参数。第一个字节0x3X,X是流控状态FS:0表示继续发送,1表示请等待,2表示过载中止。第二个字节BS叫块大小,含义是“每发多少个连续帧后我要再回一个流控帧”,如果BS为0则代表“你后续不用等我流控了,一口气把剩下的连续帧发完”。第三个字节STmin叫最小间隔时间,表示发送方相邻两个连续帧之间至少间隔多久。

FS=Wait在实际总线中不太常见,因为绝大多数接收方要么能处理就直接CTS,处理不了就直接Overflow,用Wait拖着的实现容易把发送方挂在半路。但协议栈如果收到Wait,正确的做法是原地暂停而不是放弃当前传输,等下一个FC再恢复。

STmin值含义
0x00无间隔要求,发送方可不加延时
0x01~0x7F1ms~127ms
0xF1~0xF9100us~900us,步进100us
0x80~0xF0、0xFA~0xFF保留

2.4 连续帧的序号从F回到0

连续帧的PCI字节是0x2N,N是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表示FS=0继续发送、BS=0没有块限制、STmin=50ms。这一帧是整段报文的关键。第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的响应流控都习惯用BS=0,因为响应数据长度不大,没必要分段限制。但接收方如果需要周期性处理数据,可以设置BS=2。假设ECU要回复一个34字节的响应,完整的交互时序应该是这样的:

步方向帧数据说明
1ECU→Tester10 22 D1 D2 D3 D4 D5 D6FF,总长34字节
2Tester→ECU30 02 0AFC,BS=2,STmin=10ms
3ECU→Tester21 D7 D8 D9 D10 D11 D12 D13CF,SN=1
4ECU→Tester22 D14 D15 D16 D17 D18 D19 D20CF,SN=2
5Tester→ECU30 02 0A第二个FC,允许再发2帧
6ECU→Tester23 D21 D22 D23 D24 D25 D26 D27CF,SN=3
7ECU→Tester24 D28 D29 D30 D31 D32 D33 D34CF,SN=4

注意第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级的STmin(0xF1~0xF9)在经典CAN上没有实际意义,更多是用在带宽更高的总线上。反过来,如果接收方是慢速MCU,STmin取50ms甚至100ms能显著降低丢帧概率,代价是整条响应耗时变长。实际项目里0x32(50ms)是一个比较稳妥的中档值,很多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后等FC1000ms
N_Bs接收方收到FF后准备发FC1000ms
N_Cr接收方发完FC后等CF补齐1000ms
N_Cs发送方收到FC后准备下个CF小于STmin配置值

记不住字母没关系,原理就是双方都要有超时保护,协议才不会因为某一帧丢失而永久卡死。实际项目一律以OEM诊断规范为准。

4.4 为什么总长上限是4095字节

4095由12位长度字段决定,这是ISO 15765-2的硬限制。要传更大块数据,应用层必须拆成多个消息,比如0x34+0x36刷写就是一次只传一段,每段控制在4095以内。这个问题在Bootloader软件里几乎每天都要处理,看到上位机把4MB固件切成4095字节的块不要觉得奇怪,根本原因就是ISO-TP单次消息装不下。

5. 实测多帧传输最容易踩的五个坑

5.1 首帧长度解析错误

有个同事自研诊断仪,读VIN总是超时。抓包看ECU明明回了 10 14 和一串连续帧,工具却一直说消息未完成。代码里把长度写成了0x114,按276字节等后续帧,自然等不到。改成((byte0 & 0x0F) << 8) | byte1之后就好了。这种错很容易出现在从别处抄来的解析代码里,而且8字节报文从界面上看很难一眼发现差别,只能靠抓包软件自带的ISO-TP解码来对照。

5.2 把BS=0理解成“不需要流控”导致死等

反过来也有案例。某个ECU代码收到FC后判断BS==0,以为“不需要流控”就是“两边都不用管了”,结果每发一个CF都等接收方再发FC,而接收方发的又是BS=0,认为发送方会自己发完,两边都等对方,直到N_Br和N_Cr超时。BS=0的确切含义是“不需要额外来回复用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应该中止当前传输并向上层报错,由应用层决定是否重发。很多初版协议栈只处理了FS==0,收到其他值直接当错误帧丢掉,结果是发送方继续发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就名存实亡了。多帧传输看起来就那么几类帧,真正的麻烦都藏在参数解释、方向判断和序号回绕这些细节里。把上面几条吃透,再回头看抓包文件,诊断多帧基本不会被卡住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询