1. 从一次诊断超时说起:为什么8字节的CAN要传4KB数据
如果你做过汽车诊断开发,大概率遇到过这样的场景:用诊断仪读取ECU的故障码或者刷写固件,明明CAN总线波特率500kbps,理论上每秒能传几十KB,但实际传输速度慢得让人抓狂,一个几十KB的固件刷写要等好几分钟。更让人困惑的是,CAN帧一次最多只能装8个字节,而UDS诊断服务动辄要传输几百字节甚至几KB的数据,这中间的鸿沟到底是怎么填上的?
答案就在ISO 15765-2这个标准里,业内通常叫它CAN TP层(Transport Protocol,传输协议层)。它做的事情说白了就一件:把上层UDS诊断服务的大块数据,切成CAN能装下的小块发出去,到了对面再拼回来。听起来简单,但里面涉及的流控机制、时序参数、帧类型设计,每一个细节都直接影响诊断的稳定性和效率。
这篇文章面向的是做汽车电子诊断开发的工程师,不管你是刚接触UDS的新人,还是已经用过CANoe、周立功CAN分析仪做过诊断测试的老手,我都会从TP层的帧结构讲起,把首帧、连续帧、流控帧、单帧这四种帧类型掰开揉碎,再结合实际的时序参数配置和常见踩坑经验,让你彻底搞明白这8个字节是怎么把4KB数据搬过去的。读完你至少能搞清楚三件事:TP层的四种帧各自解决什么问题、STmin和BS这些参数怎么影响传输效率、以及实际调试中遇到传输中断该怎么排查。
2. ISO 15765-2的四种帧类型:各司其职的分工设计
2.1 单帧:能一趟拉完就绝不跑两趟
单帧(Single Frame,SF)是最简单的情况。当你要传输的数据不超过7个字节时(经典CAN帧8字节,其中1字节被协议占用),直接用一个CAN帧就搞定了。帧的第一个字节的高4位是帧类型标识,0x0表示单帧,低4位表示数据长度。
举个例子,你用UDS服务0x10做会话切换,请求数据就2个字节(0x10 0x03),加上长度信息一共3字节,一个单帧就发出去了。实际报文长这样:
CAN ID: 0x7E0 Data: 02 10 03 00 00 00 00 00第一个字节0x02表示后面有2个字节的有效数据,0x10 0x03就是实际的UDS请求。剩下的用0x00填充,这是CAN帧的惯例,不用太在意。
单帧的优点是零开销、零延迟,发出去就完事。但它的问题也很明显——最多7个字节,稍微大一点的数据就装不下了。你读个DTC故障码可能就三五个字节,单帧够用;但你要读ECU的软件版本号、VIN码,或者做固件刷写,那就必须上多帧传输了。
注意:单帧的长度字段只有4位,最大表示15,但实际有效数据最多7字节(经典CAN)。如果你看到有人用单帧传8字节数据,那肯定是不对的,第8字节会被协议层吃掉。
2.2 首帧:大块数据的开场白
当数据超过7个字节,就需要多帧传输了。首帧(First Frame,FF)是多帧传输的第一条报文,它的任务是告诉接收方:“我要发一大块数据了,总共多少字节,你准备好接。”
首帧的结构比单帧复杂一些。第一个字节的高4位是0x1,表示首帧;低4位和第二个字节一起组成12位的长度字段,最大可以表示4095字节。也就是说,ISO 15765-2在经典CAN上单次最多传输4095字节的数据。对于更大的数据(比如完整的固件包),需要在应用层做分段,TP层一次只负责4095字节以内的传输。
假设你要传输1000字节的数据,首帧长这样:
CAN ID: 0x7E0 Data: 10 3E 8F 01 02 03 04 05第一个字节0x10,高4位1表示首帧,低4位0和第二个字节0x3E组成长度0x03E = 62?不对,这里要仔细算。12位长度的计算方式是:(第一字节低4位 << 8) | 第二字节。所以0x10 0x3E表示长度 = (0 << 8) | 0x3E = 62字节。如果要表示1000字节,应该是0x13 0xE8,因为(3 << 8) | 0xE8 = 0x3E8 = 1000。
首帧的数据区从第三个字节开始,可以携带6个字节的有效数据(8字节减去2字节的长度信息)。这6个字节是整块数据的前6个字节,接收方收到首帧后就知道总长度和开头数据了。
2.3 连续帧:一个接一个把数据搬完
连续帧(Consecutive Frame,CF)是首帧之后的所有数据帧。每个连续帧的第一个字节高4位是0x2,低4位是一个序列号(Sequence Number,SN),从1开始,每发一帧加1,到15之后回绕到0继续。
序列号的作用是让接收方能够检测是否有帧丢失或乱序。如果接收方收到的连续帧序列号不连续,就知道中间丢帧了,可以触发重传或者报错。
连续帧的数据区有7个字节(8减去1字节的序列号),所以传输效率比首帧高一点。继续上面的例子,1000字节的数据,首帧发了6字节,剩下994字节,需要994/7 = 142个连续帧才能发完。
首帧: 10 3E 8F [6字节数据] CF1: 21 [7字节数据] CF2: 22 [7字节数据] ... CF15: 2F [7字节数据] CF16: 20 [7字节数据] // 序列号回绕到0 CF17: 21 [7字节数据] ...这里有个容易踩的坑:序列号回绕。从1到15之后是0,不是16。很多新手写TP层代码时在这里翻车,导致接收方认为序列号错误而中断传输。
2.4 流控帧:接收方的节奏控制器
流控帧(Flow Control,FC)是多帧传输中接收方发给发送方的控制报文,用来告诉发送方:“你发慢点/发快点/先别发,我处理不过来。”
流控帧的第一个字节高4位是0x3,低4位是流状态(Flow Status,FS),有三种值:
- FS=0(Continue To Send,CTS):继续发送,接收方准备好了
- FS=1(Wait,WT):等待,接收方暂时处理不过来,让发送方等一会儿
- FS=2(Overflow,OVFLW):溢出,接收方缓冲区不够,传输中止
流控帧的第二个字节是块大小(Block Size,BS),第三个字节是最小间隔时间(Separation Time Minimum,STmin)。这两个参数是多帧传输效率的核心。
BS表示发送方在收到下一个流控帧之前,可以连续发送多少个连续帧。如果BS=0,表示不限制,发送方可以一直发到数据发完。如果BS=8,发送方发8个连续帧后必须停下来等新的流控帧。
STmin表示两个连续帧之间的最小时间间隔,单位是毫秒。STmin=0表示可以背靠背连续发,STmin=10表示每帧之间至少间隔10ms。这个参数主要是给接收方留出处理时间,防止缓冲区溢出。
流控帧示例: 30 08 0A 00 00 00 00 00 含义: CTS, BS=8, STmin=10ms3. 多帧传输的完整时序:一次1000字节传输的全过程
3.1 发送方视角:从首帧到最后一帧
把上面四种帧串起来,一次完整的多帧传输流程是这样的。假设发送方要传1000字节数据,接收方的流控参数是BS=8、STmin=10ms。
发送方先发首帧,携带总长度和前6字节数据。然后等待接收方的流控帧。收到CTS流控帧后,发送方开始发连续帧,每发8个连续帧就停下来等新的流控帧。接收方每收完8个连续帧,就再发一个流控帧,直到所有数据收完。
整个过程中,发送方需要维护几个状态:当前发送到第几字节、当前序列号是多少、当前块内已发了几帧。接收方则需要维护:已接收多少字节、期望的下一个序列号、当前块内已收了几帧。
这里有个细节值得注意:首帧的数据区是6字节,连续帧是7字节,所以计算总帧数时不能简单用总长度除以7。正确的计算方式是:首帧6字节,剩余长度除以7向上取整就是连续帧数量。1000字节的话,首帧6字节,剩余994字节,994/7=142,刚好整除,所以需要142个连续帧。
3.2 接收方视角:流控帧的发送时机
接收方什么时候发流控帧?有两个时机:收到首帧后必须发第一个流控帧,之后每收满BS个连续帧再发一个。
如果BS=0,接收方只在收到首帧后发一次流控帧,之后不再发送,发送方一直发到数据结束。这种方式效率最高,但对接收方的缓冲区要求也最高,因为发送方不会停下来等。
如果BS=8,接收方每收8个连续帧就发一个流控帧。这种方式给了接收方喘息的机会,但增加了流控帧的开销。1000字节的数据需要142个连续帧,每8个一组,需要18个流控帧(最后一组可能不满8个)。
实际项目中,BS和STmin的取值需要根据ECU的处理能力和总线负载来权衡。BS太小会导致流控帧过多,传输效率下降;BS太大又可能导致接收方缓冲区溢出。STmin太小可能让接收方来不及处理,太大又拖慢整体速度。
3.3 时序参数N_As、N_Ar、N_Bs、N_Br、N_Cs、N_Cr
ISO 15765-2定义了一组网络层时序参数,用来约束各个阶段的最大等待时间。这些参数在CANoe或者诊断仪的网络层配置里经常看到,但很多人不太清楚它们具体管什么。
| 参数 | 含义 | 典型值 | 超时后果 |
|---|---|---|---|
| N_As | 发送方发送一帧的最大时间 | 1000ms | 发送失败 |
| N_Ar | 接收方发送流控帧的最大时间 | 1000ms | 发送方等待超时 |
| N_Bs | 发送方等待流控帧的最大时间 | 1000ms | 传输中止 |
| N_Br | 接收方收到首帧后发流控帧的最大时间 | 1000ms | 发送方超时 |
| N_Cs | 发送方发送连续帧的最大时间 | 1000ms | 接收方超时 |
| N_Cr | 接收方等待连续帧的最大时间 | 1000ms | 接收方超时 |
这些参数的单位都是毫秒,具体取值由OEM在诊断规范里定义。实际调试中,如果N_Bs超时,说明发送方等流控帧等太久了,可能是接收方没发或者总线丢了。如果N_Cr超时,说明接收方等连续帧等太久了,可能是发送方停了或者中间丢帧了。
提示:很多诊断问题最后都归结为时序参数不匹配。比如ECU的N_Bs设了1000ms,但诊断仪因为总线负载高,流控帧发出来已经超过1000ms了,发送方就会报超时。这种情况下要么放宽参数,要么优化总线负载。
4. 流控参数BS和STmin的实战配置:效率与稳定的平衡
4.1 BS=0和BS>0的取舍
BS=0意味着发送方不需要等流控帧,可以一口气把所有连续帧发完。这种方式传输效率最高,因为没有流控帧的开销,也没有等待流控帧的延迟。但它对接收方的要求也最高:接收方必须有足够大的缓冲区来存放整个数据块,而且处理速度要跟得上发送速度。
BS>0则把数据分成多个块,每块之间用流控帧隔开。这种方式给了接收方处理时间,降低了缓冲区要求,但增加了流控帧的开销和块间的等待时间。
实际项目中怎么选?我的经验是:如果数据量不大(比如几百字节),接收方缓冲区够用,直接用BS=0最省事。如果数据量大(比如几KB的固件刷写),或者接收方是处理能力较弱的ECU,那就用BS=8或BS=16,给接收方留出处理时间。
有个实际案例:某项目刷写ECU固件,数据量约2KB,最初用BS=0,结果刷写到一半总是失败。后来抓CAN报文发现,接收方ECU的缓冲区只有256字节,BS=0时发送方发太快,缓冲区溢出导致丢帧。改成BS=16后问题解决,虽然刷写时间从3秒变成了4秒,但稳定性大幅提升。
4.2 STmin的取值逻辑
STmin的单位是毫秒,表示两个连续帧之间的最小间隔。STmin=0表示可以背靠背发送,STmin=10表示每帧之间至少等10ms。
STmin的取值主要考虑两个因素:接收方的处理速度和总线负载。接收方处理一帧数据需要时间,如果发送方发太快,接收方来不及处理就会丢帧。总线负载高的时候,连续帧之间也需要留出间隙,让其他报文有机会发送。
ISO 15765-2规定STmin的有效范围是0x00到0x7F(0到127ms),以及0xF1到0xF9(100到900微秒)。0x80到0xF0是保留值,0xFA到0xFF也是保留值。实际项目中常用的值是0、5、10、20ms。
这里有个容易忽略的点:STmin是接收方在流控帧里告诉发送方的,但发送方不一定完全遵守。有些发送方实现会取STmin和自身最小间隔的较大值。比如接收方说STmin=0,但发送方因为软件架构原因,两帧之间天然有2ms的间隔,那实际间隔就是2ms。
4.3 参数配置不当的典型症状
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 传输中途停止,无后续帧 | BS太小,流控帧丢失 | 检查流控帧是否发出,总线是否有错误帧 |
| 接收方报序列号错误 | 连续帧丢失或乱序 | 检查总线负载,确认STmin是否太小 |
| 传输速度极慢 | STmin太大或BS太小 | 优化流控参数,减少流控帧数量 |
| 首帧后无流控帧 | 接收方未响应或N_Br超时 | 检查接收方状态,确认诊断会话是否激活 |
| 数据内容错乱 | 序列号回绕处理错误 | 检查TP层代码的序列号逻辑 |
5. 实际调试中遇到的坑:从抓包到定位的完整链路
5.1 案例一:序列号回绕导致的传输中断
某次调试中,诊断仪读取ECU的故障快照数据,数据量约500字节。传输到第15个连续帧后突然中断,接收方报序列号错误。
抓包一看,发送方在第15个连续帧(序列号0x2F)之后,下一个帧的序列号是0x20,这是正确的回绕。但接收方的代码里,序列号判断逻辑写的是“期望序列号 = 上一个序列号 + 1”,当上一个序列号是15时,期望序列号变成了16,而实际收到的是0,于是报错。
这个问题出在接收方的TP层实现上。正确的做法是:序列号在0到15之间循环,15之后是0。代码里应该用取模运算,而不是简单加1。
// 错误写法 expected_sn = last_sn + 1; // 正确写法 expected_sn = (last_sn + 1) % 16;这个坑很典型,因为序列号回绕在数据量小的时候不会触发(少于15个连续帧),只有数据量大了才会暴露。所以测试TP层时,一定要用足够大的数据量来覆盖序列号回绕的场景。
5.2 案例二:STmin配置不当导致的随机丢帧
另一个项目里,ECU刷写过程中偶发失败,概率大概10%。抓包发现,失败的时候都是发送方连续发了多个连续帧后,接收方没有响应流控帧,导致发送方N_Bs超时。
进一步分析发现,接收方ECU的CAN控制器缓冲区只有3个报文深度。发送方STmin=0,连续帧背靠背发送,接收方处理不过来,缓冲区溢出丢帧。丢帧后接收方等待期望的序列号,但发送方已经发了后面的帧,双方状态不一致,最终超时。
解决方案是把STmin从0改成5ms,给接收方留出处理时间。改完之后刷写成功率100%,刷写时间只增加了不到1秒。
这个案例说明,STmin不是越小越好。STmin=0虽然理论效率最高,但对接收方的实时性要求也最高。实际项目中,除非确认接收方处理能力足够,否则建议至少设5ms。
5.3 案例三:流控帧的BS字段被忽略
有个项目里,接收方在流控帧里设置了BS=8,但发送方完全忽略了这个字段,一直发到数据结束。结果接收方在收完8个连续帧后开始等新的流控帧,但发送方已经在发第9、第10个连续帧了,接收方全部丢弃,最终传输失败。
这个问题出在发送方的TP层实现上。BS字段是接收方对发送方的约束,发送方必须遵守。如果BS=8,发送方发完8个连续帧后必须停下来,等新的流控帧才能继续。
排查这类问题时,重点看发送方的TP层代码里有没有对BS字段的处理逻辑。有些简易实现为了省事,直接忽略BS,按BS=0处理,这在接收方缓冲区小的时候必然出问题。
5.4 排查TP层问题的一般思路
遇到TP层传输问题,我通常按这个顺序排查:
- 确认物理层正常:CAN总线有没有错误帧,波特率是否匹配,终端电阻是否正确
- 抓包看帧序列:首帧、流控帧、连续帧是否完整,序列号是否连续
- 检查流控参数:BS和STmin是否合理,接收方是否按约定发送流控帧
- 核对时序参数:N_Bs、N_Cr等超时参数是否匹配,有没有超时
- 检查TP层代码:序列号回绕、BS处理、STmin遵守、缓冲区管理
这个顺序是从底层到上层,先排除硬件和总线问题,再查协议层实现。大部分问题在前三步就能定位。
6. 从TP层到UDS:多帧传输如何支撑诊断服务
6.1 UDS服务0x34和0x36的传输需求
UDS诊断服务里,最依赖TP层多帧传输的是0x34(请求下载)和0x36(传输数据)。0x34服务用于请求ECU准备接收数据,请求里包含数据长度和地址信息,通常十几个字节,需要多帧传输。0x36服务用于实际传输数据块,每个块可能几百字节到几KB,更是必须用多帧。
以0x36服务为例,诊断仪发送的数据格式是:服务ID(0x36)+ 块序列号(1字节)+ 数据。如果数据块是1KB,加上2字节头部,总共1026字节,TP层需要传输1026字节。首帧6字节,剩余1020字节,需要146个连续帧。
ECU收到数据后,如果处理成功,返回0x76(正响应);如果失败,返回0x7F 0x36加负响应码。这里有个细节:0x76是正响应,0x7F是负响应。很多新手会混淆,记住0x7F后面跟的是原始服务ID和错误码。
6.2 多帧传输对刷写效率的影响
固件刷写的总时间 = 数据传输时间 + ECU擦写时间。数据传输时间又取决于TP层的效率。假设总线500kbps,每帧CAN报文约130位(含帧间隔),理论上一秒能传约480帧。但实际因为流控帧、STmin、ECU处理时间,有效数据速率会低很多。
以BS=8、STmin=5ms为例,每8个连续帧(56字节数据)需要等待一个流控帧,加上STmin的间隔,实际传输1KB数据大约需要1到2秒。如果STmin=0、BS=0,1KB数据可能只要0.5秒。但后者对ECU要求高,不是所有ECU都能扛住。
实际项目中,刷写时间往往是用户体验的关键指标。4S店技师刷一个ECU,如果等5分钟和等2分钟,感受完全不同。所以TP层参数的优化是刷写效率提升的重要环节。
6.3 诊断会话与TP层的关系
TP层的多帧传输必须在正确的诊断会话下进行。默认会话(0x01)下,很多ECU不支持多帧传输或者限制数据长度。要传大块数据,必须先切换到扩展会话(0x03)或编程会话(0x02)。
切换会话本身是单帧传输,很简单。但切换之后,ECU的TP层参数可能会变。比如默认会话下BS=0,编程会话下BS=8。这个变化诊断仪需要知道,否则可能按旧参数发送导致失败。
实际调试中,建议在切换会话后先发一个小的多帧请求测试一下,确认TP层参数是否变化。如果ECU返回流控帧的BS和STmin跟预期不符,诊断仪需要动态调整。
7. 写TP层代码时容易忽略的几个细节
7.1 缓冲区管理:发送和接收都要留余量
TP层代码最核心的是缓冲区管理。发送方需要缓冲待发送的完整数据,接收方需要缓冲已接收的数据。缓冲区大小至少要是最大传输长度(4095字节),但实际项目中建议留一倍余量。
为什么?因为CAN帧的接收是异步的,可能连续来好几帧,如果缓冲区刚好够,处理稍微慢一点就溢出了。我一般建议接收缓冲区至少8KB,发送缓冲区至少4KB。
另外,缓冲区的读写指针要小心处理。多帧传输时,发送方从缓冲区读数据填到CAN帧里,接收方从CAN帧里读数据写到缓冲区。如果读写指针管理不当,可能出现数据覆盖或者读空。
7.2 超时处理:每个等待状态都要有超时
TP层有多个等待状态:发送方等流控帧、接收方等连续帧、接收方等首帧后的数据。每个等待状态都必须有超时机制,否则一旦对方不响应,就会永久卡死。
超时时间用前面提到的N_Bs、N_Cr等参数。实现上可以用定时器,也可以用计数器。关键是超时后要能恢复到空闲状态,让下一次传输可以正常开始。
有个实际教训:某项目的TP层代码在等流控帧时没有超时处理,结果一次传输失败后,TP层一直卡在等待状态,后续所有诊断请求都无响应。后来加了超时机制,问题解决。
7.3 序列号管理:发送和接收要独立维护
发送方的序列号和接收方的序列号是独立维护的。发送方从1开始递增,接收方从1开始期望。两者不需要同步,但必须遵循同样的回绕规则。
发送方每发一个连续帧,序列号加1,到15后回绕到0。接收方每收一个连续帧,检查序列号是否等于期望值,然后期望值加1,到15后回绕到0。
这里有个细节:首帧没有序列号,第一个连续帧的序列号是1。有些实现从0开始,这是错误的。ISO 15765-2明确规定连续帧序列号从1开始。
7.4 流控帧的发送时机:收到首帧后立即发
接收方收到首帧后,应该立即发送流控帧,不能等。因为发送方在发完首帧后就进入等待流控帧的状态,如果接收方不及时发,发送方会超时。
实际实现中,收到首帧后的处理要尽量快。如果接收方需要在发流控帧前做一些准备工作(比如分配缓冲区),这些工作应该在收到首帧之前就做好,或者足够快。
流控帧的FS字段也要根据实际情况设置。如果接收方缓冲区够用,发CTS;如果暂时不够,发WT让发送方等;如果完全不够,发OVFLW中止传输。WT可以发多次,但每次发送方都会重新等待。
8. 用CAN分析仪验证TP层传输的实操方法
8.1 抓包配置要点
用CAN分析仪(比如周立功或者Vector的硬件)抓TP层报文,配置上有几个要点。首先,波特率必须和总线一致,否则抓不到或者抓到乱码。其次,过滤规则要设置好,只看诊断相关的CAN ID(比如0x7E0到0x7E7和0x7E8到0x7EF),否则总线上的其他报文会淹没诊断报文。
抓包时建议开启时间戳,精确到毫秒。TP层的时序问题往往在毫秒级别,没有精确时间戳很难分析。另外,开启错误帧显示,如果总线上有错误帧,可能是物理层问题。
8.2 分析报文序列的方法
抓到报文后,按时间顺序排列,找到首帧开始的地方。首帧的特征是第一个字节高4位为1。然后看后续的流控帧和连续帧,检查序列号是否连续,流控帧的BS和STmin是否符合预期。
如果传输中断,看最后一个成功接收的帧是什么,下一个期望的帧是什么。如果期望的是流控帧但没收到,说明接收方没发或者丢了。如果期望的是连续帧但没收到,说明发送方停了或者丢了。
分析时可以用CAN分析仪的过滤功能,只看某个CAN ID的报文,这样更清晰。也可以用分析仪的统计功能,看总线上报文的数量和错误率。
8.3 模拟发送和接收的工具
除了被动抓包,还可以用CAN分析仪主动发送报文来模拟TP层传输。比如模拟一个ECU发送多帧响应,看诊断仪能否正确接收。或者模拟诊断仪发送多帧请求,看ECU能否正确响应。
这种主动测试对验证TP层实现很有用。你可以构造各种边界情况:最大长度4095字节、序列号回绕、BS=0和BS>0、STmin=0和STmin>0,看双方是否都能正确处理。
周立功的CAN分析仪配套软件里有报文发送功能,可以手动构造CAN帧。Vector的CANoe更强大,可以写CAPL脚本自动模拟TP层传输。如果预算有限,用周立功的也够用,只是需要手动操作多一些。
9. 关于TP层的一些个人经验
做诊断开发这些年,TP层的问题我踩过不少坑,也总结了一些经验。首先,TP层的实现一定要严格按ISO 15765-2来,不要自作聪明简化。比如忽略BS字段、不做序列号回绕、不处理WT流控帧,这些简化在简单场景下可能没问题,但一旦遇到复杂场景就会暴露。
其次,测试TP层一定要用大数据量。小数据量走单帧,根本测不到多帧的逻辑。我一般建议至少用1KB以上的数据来测试,这样才能覆盖序列号回绕、多块传输、流控帧交互等场景。
第三,时序参数不要照搬别人的配置。每个ECU的处理能力不同,总线负载不同,最优参数也不同。建议先用保守参数(BS=8、STmin=10)跑通,再逐步优化。
第四,抓包分析是排查TP层问题的最有效手段。遇到问题先抓包,看帧序列,大部分问题都能定位。不要凭猜测改代码,那样效率很低。
最后,TP层虽然只是UDS协议栈的一层,但它直接决定了诊断的稳定性和效率。花时间把TP层搞透,对后续做诊断功能开发、刷写优化都有很大帮助。我见过很多诊断问题最后都追溯到TP层,把这一层做扎实,能省掉很多后续的麻烦。