搞车载诊断的朋友,十有八九都撞上过这个场景:读内存、刷固件、采集数据,一条 UDS 诊断消息轻轻松松就超过 8 个字节,可 CAN 一帧的 payload 天生就只有 8 字节。你第一反应可能是“拆开多帧发不就行了”,但拆包这件事远没有看起来简单——帧与帧之间的顺序怎么定、收方怎么知道总长度、丢了帧要不要重发、收发两端节奏不同步怎么办,这些规矩如果各做各的,整车诊断就全乱套了。
这套规矩就是 ISO 15765-2,也就是业内常说的 TP 层(Transport Protocol Layer),也叫网络层。UDS 跑在它上面,CAN 跑在它下面,它夹在中间专门干“拆包、编号、流控、重组”的脏活累活。这篇文章我就从原理到实战,把“8 字节 CAN 帧怎么传 4KB 诊断数据”这件事彻底拆开讲透。无论你是刚入门的嵌入式工程师,还是被 PT 刷写问题折磨过的老手,这篇都值得花十分钟读完,后面直接能用到项目里。
1. 为什么 CAN 只能带 8 字节,UDS 却要面对 4KB 数据
1.1 CAN 帧载荷的天花板
先看客观约束。这里说的 8 字节,指的是 Classical CAN(CAN 2.0)单帧数据场的上限,DLC 最大就是 8。CAN FD 把上限提到了 64 字节,但那是另一个故事,本文默认讨论的是最主流的 Classical CAN 诊断场景,也就是绝大多数量产车现在还在跑的方式。
一个标准 CAN 数据帧,不管数据场是 0 字节还是 8 字节,总线上的物理开销是固定的:帧起始、仲裁段、控制段、CRC、ACK、EOF、帧间隔。以 11 位标准帧为例,8 字节数据时整帧在总线上占 111 个位时间,算下来在 500 kbps 下大约 222 微秒一帧;29 位扩展帧再多 18 个位,大约 258 微秒一帧。这个数字后面算耗时要用,先记住。
问题在于,诊断一层的需求完全不受这 8 字节限制。UDS 里随便一个服务都可能要求大块数据:
0x22按标识符读取数据,读一段 VIN、配置信息可能就几百字节;0x23按地址读内存,读取量动辄一两 KB;0x34请求下载 +0x36传输数据,刷写 Bootloader 或应用程序时,单条0x36请求可以携带几千字节的固件块。
一刀切的矛盾就出现了:CAN 底层一次只能拿 8 字节,UDS 上层却想一次性塞 4KB。
1.2 协议栈里的“三明治”:UDS、TP、CAN 各干哪层活
要理解 ISO 15765-2,先看它在协议栈里的位置。按 OSI 模型粗略对应:
- 应用层:ISO 14229,也就是 UDS,定义
0x22、0x23、0x34这些服务和它们的正负响应格式。UDS 只管“我要读内存地址 X 的长度 Y”,完全不管数据怎么上车。 - 网络层/传输层:ISO 15765-2,就是 TP 层。它负责把上层一大坨数据切成多个 CAN 帧,再在接收端拼回去,同时负责流控和时序。
- 数据链路层:ISO 11898,CAN 控制器干的活,处理位填充、CRC、仲裁这些物理细节。
UDS 发一条消息时,并不感知 CAN 帧边界;TP 层提供的服务就是“你给我一大块数据,我保证完整送到对端”。所以从 UDS 视角看,这条“虚拟通道”能传的最大报文长度是 4095 字节——约等于 4KB。
这里有个经常被问的点:为什么偏偏是 4095,不是 4096,也不是更大?因为 TP 层首帧里用 12 个 bit 来声明总长度,0xFFF就是 4095,这是协议硬编码的天花板。你后面会看到,所有“4KB”的说法都来源于这个 12 位长度字段。
1.3 为什么简单拆包不行
有人会问:既然 CAN 一帧 8 字节,那我把 4095 字节按 7 字节一份,分成 585 份,直接连续发出去不就完了?理论上能发,但接收端根本没法用:
- 不知道总长度。接收方收到一堆帧,不知道这条消息什么时候结束,也不知道该准备多大的缓冲区。要么缓冲区开小了装不下,要么开大了浪费内存。
- 不知道帧的顺序。CAN 总线是有仲裁和优先级的,高优先级帧随时可能插进来;发送端自己的队列也可能把帧重排。接收端必须能识别“这是第几片”,才能按序拼装。
- 没有流控。发送方连发几百帧,接收方 CPU 可能正在忙别的,CAN 控制器 FIFO 一满,后面的帧直接被硬件丢弃,没有任何机制让发送方“等一下”。
- 丢了帧没法发现。少了一帧,拼出来的数据就是错的;如果没有序号和长度校验,应用层拿到错数据还浑然不觉。
ISO 15765-2 解决这四件事的手段,就是定义四种帧类型。这也是 TP 层全部奥义的所在。
2. ISO 15765-2 四种帧:看懂 PCI 字节就懂了 TP 层
TP 层所有帧都长在同一个 CAN 帧里,区别只在于数据场开头的 PCI(Protocol Control Information,协议控制信息)字节怎么解释。数据场最多 8 字节,PCI 占掉一部分,剩下的才是真正承载数据的 payload。四种帧分别是:单帧(SF)、首帧(FF)、连续帧(CF)、流控帧(FC)。
2.1 Single Frame:小消息一把梭
单帧是最简单的:整条消息就 7 个字节以内,一个 CAN 帧直接带走。
PCI 格式是0x0N,高 4 位固定为0000表示这是单帧,低 4 位N表示这条消息携带的实际数据字节数(0~7)。比如发一条0x10 0x01的诊断会话控制请求,数据就 2 字节,PCI 写0x02,整帧就是:
CAN ID 0x7E0 02 10 01 00 00 00 00 00PCI(02)后面的10 01是 UDS 数据,后面 5 个字节是 padding。单帧不需要接收方回任何东西,发完即完,所以它是效率最高的一种传输方式。平时刷写协议里的大量短请求、短响应,全是单帧。
2.2 First Frame:先报总长度,再带第一批数据
当 UDS 消息超过 7 字节,就得走多帧流程,第一步由发送方发一个首帧 FF。
首帧的 PCI 占 2 个字节。第一个字节高 4 位固定0001表示首帧,低 4 位是 12 位总长度字段的高 4 位;第二个字节是总长度字段的低 8 位。合起来0x1M 0xLL,MLL就是整条消息的字节数,范围 0~4095。
为什么首帧要占 2 字节 PCI?因为必须用那多出来的 8 个 bit 把总长度声明出来,接收端才能决定缓冲区大小、才知道要等多少数据。首帧剩下的 6 个字节也不能浪费,会先带上消息开头的 6 字节数据,比如响应报文最前面的服务 ID 和参数。
举个例子,一条总长度为0xFFF(4095 字节)的响应,首帧数据场长这样:
CAN ID 0x7E8 10 FF 63 22 D1 D2 D3 D410 FF表示总长度 4095,后面63 22 D1 D2 D3 D4是前 6 字节数据(63是0x23服务的正响应,22是地址/长度格式标识,D1~D4是头 4 个数据字节)。
到这一步,发送方把话撂下了:“我要发 4095 字节,这是开头 6 字节,你准备好了吗?”
2.3 Consecutive Frame:按 4 位序号背货
接收方收到首帧后,如果决定接收,会回一个流控帧(下一节讲)。发送方收到允许发送的流控帧后,就开始连续发连续帧 CF。
连续帧的 PCI 只占 1 字节:高 4 位固定0010,低 4 位是序号 SN,范围 0~15。第一个连续帧的 SN 从 1 开始,之后每个连续帧加 1,到 15 之后回绕到 0,再继续 1、2、3……这是协议里很容易被忽略的细节:SN 是 4 位计数器,不是一直累加的。校验接收端必须按1,2,...,15,0,1,2...的循环来判断丢帧。
每个连续帧最多带 7 字节数据。对于 4095 字节的消息,首帧已经带走 6 字节,剩下 4089 字节全部由 CF 扛:
4089 = 584 × 7 + 1也就是 584 个满载的连续帧,再加最后一个只带 1 字节的连续帧,总共 585 个连续帧。加上首帧,整条 4KB 消息一共 586 个 CAN 帧。
连续帧的 PCI 示例:
0x21 D5 D6 D7 D8 D9 D10 D11 // 第 1 个 CF,7 字节 0x22 D12 D13 D14 D15 D16 D17 D18 // 第 2 个 CF ... 0x2F ... // 第 15 个 CF,SN=15 0x20 ... // 第 16 个 CF,SN 回绕到 0 0x21 ... // 第 17 个 CF,SN 回到 1,继续最后一个连续帧可能不满 7 字节,剩下的字节填充成 0x00 或按 OEM 规范填充,接收端靠首帧声明的总长度自行截断。
2.4 Flow Control:接收方手里的闸门
流控帧 FC 是这四种帧里唯一由接收方发出的帧,用来回答首帧:“你发吧,但按我的节奏来”。FC 的 PCI 占 3 个字节:
- 第一个字节高 4 位固定
0011,低 4 位是流控状态 FS; - 第二个字节是块大小 BS;
- 第三个字节是最小间隔时间 STmin。
FS 有三种取值:
| FS 值 | 含义 | 说明 |
|---|---|---|
| 0 | ContinueToSend | 允许继续发送 |
| 1 | Wait | 接收方忙,让发送方等待 |
| 2 | Overflow | 接收方缓冲区溢出,中止传输 |
BS 表示“允许连续发送多少个 CF”:0表示不限制,连续帧随便发;1~255表示发完这么多个连续帧后,必须停下来等下一个 FC。STmin 表示“相邻两个连续帧之间的最小发送间隔”,单位通常是 0.1ms,比如0x01表示 100 微秒,0x0A表示 1ms,0xFF表示 127ms;另外0xF1~0xF9是 100~900 微秒的微调档位,这是 2016 版之后的用法,老项目里少见。
FC 帧数据场只有前 3 字节是 PCI,剩下 5 个字节是填充。一个典型的“放行”流控帧长这样:
CAN ID 0x7E0 30 00 00 00 00 00 00 0030表示 FS=0(放行),00表示 BS=0(不限制块数),00表示 STmin=0(不要求间隔)。这是刷写场景里最常见的配置,因为快。
2.5 四种帧速查表
把四种帧整理成一张表,项目里查起来最方便:
| 帧类型 | PCI 格式 | 数据场字节 | Payload 能力 | 谁发 |
|---|---|---|---|---|
| 单帧 SF | 0x0N(N=数据长度) | 1 | 7 字节 | 发送方 |
| 首帧 FF | 0x1M LL(12 位总长度) | 2 | 6 字节 | 发送方 |
| 连续帧 CF | 0x2S(S=序号) | 1 | 7 字节 | 发送方 |
| 流控帧 FC | 0x3F BS STmin | 3 | 5 字节(一般填充) | 接收方 |
需要特别强调 FC 的方向:FC 的源地址是接收方,不是发送方。比如 ECU 发一条 4KB 响应给诊断仪,首帧和连续帧的 CAN ID 是0x7E8,而回给 ECU 的流控帧 CAN ID 是0x7E0。不少人在抓包的时候只盯响应 ID,找不到 FC 就以为对方没回,其实是方向看反了。
3. 实操拆解:一条 4KB 读内存响应的完整旅程
讲完帧格式,我们把一个典型的读内存场景从头到尾走一遍。场景:诊断仪向 ECU 发送0x23(按地址读内存)请求,ECU 返回 4095 字节数据,正好顶到 TP 层报文长度上限。
3.1 请求侧:一条 8 字节的单帧就搞定
0x23请求本身很短,可以塞进单帧。假设地址格式标识0x22表示“2 字节地址 + 2 字节长度”,地址是0x0001,长度是0x0FFD(4093 字节),请求数据就是:
23 22 00 01 0F FD6 字节,不超过 7 字节上限,所以走单帧。完整 CAN 帧:
CAN ID 0x7E0: 06 23 22 00 01 0F FD 0006是 PCI,表示后面有 6 字节数据;最后一个00是填充。发送这一个帧,请求侧完成。
注意响应总长度怎么来的:0x23的响应格式是63+ 地址格式标识 + 数据,也就是63 22占 2 字节,加上 4093 字节数据,正好 4095 字节。想读 4094 字节都不行,因为响应会变成 4096 字节,超过 12 位长度字段上限,TP 层根本无法表示。
3.2 响应侧:4095 字节怎么切成 586 帧
ECU 侧拿到 4095 字节的响应后,TP 层做切割。前面算过了:
- 首帧 FF 带 6 字节;
- 剩余 4089 字节,每个连续帧带 7 字节;
4089 ÷ 7 = 584 余 1,所以是 584 个满载连续帧加 1 个含 1 字节的末帧,共 585 个连续帧;- 总帧数 = 1 首帧 + 585 连续帧 = 586 帧。
发送顺序严格固定:FF 必须先发,然后等待接收方的 FC,收到 FC 之后才能发 CF。CF 之间按 BS 和 STmin 的约束发送。
3.3 字节级时序:从 FF 到最后一个 CF 长什么样
我们假设 BS=0、STmin=0,接收方一上来就全量放行。抓包看到的帧序列大致如下:
| 序号 | CAN ID | 方向 | 数据场 | 说明 |
|---|---|---|---|---|
| 1 | 0x7E0 | 请求 | 06 23 22 00 01 0F FD 00 | 单帧请求 |
| 2 | 0x7E8 | 响应 | 10 FF 63 22 D1 D2 D3 D4 | 首帧,总长 4095,带 6 字节 |
| 3 | 0x7E0 | 流控 | 30 00 00 00 00 00 00 00 | FC:放行,BS=0,STmin=0 |
| 4 | 0x7E8 | 响应 | 21 D5 D6 D7 D8 D9 D10 D11 | CF1,SN=1 |
| 5 | 0x7E8 | 响应 | 22 D12 D13 ... D18 | CF2,SN=2 |
| ... | ... | ... | ... | ... |
| 587 | 0x7E8 | 响应 | 29 D4089 | 最后一个 CF,SN=9,只带 1 字节 |
最后那个 CF 的 SN 为什么是9?585 个连续帧按 16 个序号循环,585 = 36 × 16 + 9,余 9,所以最后一个循环里的第 9 个序号就是 SN=9。这是纯算出来的,实际抓包时你可以数帧校验。
末尾那个只带 1 字节数据的 CF,CAN 数据场按协议帧格式仍有 8 字节物理空间,剩下 6 字节是填充。有的实现会直接把 DLC 设成 2 减少总线占用,有的控制器不支持非 8 DLC 就只能填满,两种做法都合规。
3.4 500 kbps 下耗时到底多少
算一笔账。11 位标准帧、8 字节数据时,一帧上总线约 222 微秒。586 帧全部发完:
586 × 222 µs ≈ 130 ms这是纯总线传输时间,没算任何 STmin 间隔。也就是说,在 500 kbps、BS=0、STmin=0 的理想情况下,一条 4KB 响应从首帧到末帧大约需要 130 毫秒。加上请求帧、ECU 内部处理、CAN 控制器 FIFO 排队,整条事务跑到 150~200 毫秒都算正常。
如果把 STmin 设成 1ms,那 585 个连续帧之间要多等 1ms,总耗时立刻变成130 + 585 ≈ 715 ms,直接慢了五倍多。这就是为什么刷写项目里对 STmin 极其敏感——它直接决定一个 Bootloader 刷几十分钟还是几个小时。
如果总线是 250 kbps,每帧约 444 微秒,586 帧就是约 260 毫秒。低速总线 + 大数据量 + 严格 STmin,这个组合在实车上能把超时逼出来。
4. 流控与定时参数:快慢与成败都在这几个数
TP 层能不能稳、能不能快,全看 BS、STmin 和几个 N_* 超时参数。这一节把它们的选型和坑点讲清楚。
4.1 BS 与 STmin 怎么选
BS(块大小)和 STmin(最小间隔)是接收方在 FC 里发号施令的两个旋钮,它们决定了发送方的“油门”。
BS=0 的场景:接收方一次放行全部连续帧,发送方只要满足 STmin 就能一直发到结束。这是刷写、大容量读取的首选,因为帧间没有额外握手,吞吐率最高。代价是接收方要有足够大的缓冲区或足够快的处理速度,否则 FIFO 溢出丢帧。
BS=n 的场景:接收方要求发送方每发 n 个连续帧就停下来等下一个 FC,相当于把一条长消息切成若干个小块,每块都握手一次。这样接收方可以一块一块地消化数据,缓冲区不用开太大。代价是每个块之间多一个 FC 往返,整条消息耗时增加。
举一个具体对比:4095 字节、585 个连续帧,500 kbps 总线,单帧 0.222ms。
- BS=0、STmin=0:
1 次 FC + 585 CF,约 130ms; - BS=16、STmin=1ms:需要
585/16 ≈ 37个块,每个块之间多一个 FC 往返(约 0.222ms)加 16 个 CF 之间的 1ms 间隔,总耗时大约130 + 585×1 + 37×0.5 ≈ 734ms。
同样的数据,一个 130ms 搞定,一个 734ms。实际车载项目中,诊断仪侧一般倾向于 BS=0、STmin 尽量小;ECU 作为接收方(比如刷写时接收固件)如果内存紧张,才会用 BS 分块。但很多 OEM 规范会对 STmin 设下限,比如要求不小于 1ms,目的是防止某些能力弱的 ECU 被连续帧冲垮。
4.2 N_* 超时参数矩阵
ISO 15765-2 定义了一组超时参数,每个都对应一个“等不到下一帧就报错”的窗口。这些参数在诊断仪和 ECU 两端都配了默认值,但实际项目里 OEM 经常会收紧。常用的几个:
| 参数 | 含义 | ISO 默认值 | 常见 OEM 值 |
|---|---|---|---|
| N_Bs | 接收方收到 FF 后发出 FC 的最大时间 | 1000 ms | 200~500 ms |
| N_Br | 发送方收到 FC 后发出第一个 CF 的最大时间 | 1000 ms | 1000 ms |
| N_Cs | 发送方两个相邻 CF 之间的最大间隔 | 1000 ms | 500~1000 ms |
| N_Cr | 接收方两个相邻 CF 之间的最大间隔 | 1000 ms | 500~1000 ms |
| N_Ar | 收到一帧后到发出下一帧的最大时间 | 1000 ms | 1000 ms |
诊断仪做服务超时处理时,一般以 N_Bs 或 N_Cr 为准;ECU 侧则以 N_Br、N_Cs 为准。调试时遇到“响应超时”,第一件事就是看抓包里 FC 到底回没回、FC 到第一个 CF 之间隔了多久。很多所谓“TP 层超时”,本质是某一端的 N_* 参数配得太紧,或者另一端响应太慢。
4.3 扩展寻址与 CAN FD 的变体
前面所有容量计算都假设“正常寻址”:请求和响应使用各自的物理 CAN ID(如0x7E0/0x7E8),数据场里不携带地址字节。
但某些项目里,同一个 CAN ID 下要挂多个逻辑节点,就得用“扩展寻址”:在 PCI 之后塞一个目标地址字节(N_TA),也叫做“扩展地址”。这一个字节直接吃掉数据空间,各帧 payload 变成:
| 帧类型 | 正常寻址 | 扩展寻址 |
|---|---|---|
| SF | 7 字节 | 6 字节 |
| FF 首批数据 | 6 字节 | 5 字节 |
| CF | 7 字节 | 6 字节 |
| FC | 3 字节 PCI + 5 填充 | 3 字节 PCI + 1 地址 + 4 填充 |
所以同样的 4KB 数据,走扩展寻址的连续帧变多,总耗时上升。诊断开发时,地址模式先跟客户确认清楚,不然抓包看着 ID 都正常,就是数据对不上。
CAN FD 则是把数据场从 8 字节放大到 64 字节。ISO 15765-2 在 2016 版里支持了 CAN FD 上的 TP:首帧可以带 62 字节数据,连续帧可以带 64 字节。同样 4095 字节的消息,在 CAN FD 下只需要1 个 FF + 63 个 CF ≈ 64 帧,比 Classical CAN 的 586 帧少一个数量级。但帧类型、PCI 语义、流控逻辑跟 Classical CAN 完全一致,先把本文这套搞懂,CAN FD 只是把容量数字换一换。
5. 实战踩坑与排查清单
TP 层的坑我在项目里踩过不少,也帮别人排查过不少。以下几种现象出现频率最高,直接按“现象 → 根因 → 解法”整理成表,能省一大半调试时间。
5.1 典型现象与根因对照表
| 现象 | 可能根因 | 解决办法 |
|---|---|---|
| 诊断仪发完 FF 后一直等不到响应 | 接收方缓冲区太小,直接回了 Overflow;或 N_Bs 超时 | 查抓包看有没有 FC;确认接收方缓冲配置是否大于首帧声明长度 |
| 发了几个 CF 后发送方停了 | BS 被设成非 0,发送方在等下一个 FC,而接收方忘了发 | 对抓包里的 FC 块序号;确认 BS 值双方理解一致 |
| 接收端报“丢帧” | SN 回绕处理错误:把 0 当成了非法序号 | 按1~15,0循环校验,不要把 SN=0 当错误 |
| 最后数据多出几个字节 | 接收端没用首帧总长度截断,连同 padding 一起交给应用层 | 重组完成后按 FF 声明的 4095 截断 |
| 刷写速度异常慢 | STmin 被设得过大,或每块都要等 FC | 在 FC 里把 STmin 设小、BS 设 0 |
| 高负载时 TP 频繁超时 | 总线上高优先级报文抢占,CF 间隔超过 N_Cr | 调大 N_Cr/N_Cs,或降低诊断报文 CAN ID 优先级 |
5.2 抓包怎么看
看 TP 层诊断,在 CANoe、PCAN、周立功这些工具里过滤出诊断 CAN ID,然后按下面步骤读:
- 找到以
0x1x开头的帧,这就是 FF,先把0x1M LL拼成总长度。比如10 FF就是 4095。 - 确认紧跟着 FF 的、来自对端的
0x3x帧就是 FC,读它的 FS、BS、STmin。 - 数一数
0x2x的连续帧个数,对照公式ceil((总长 - 6) / 7)检查有没有缺帧。 - 顺着
0x21→0x2F→0x20→0x21检查 SN 循环,任何跳跃都代表丢帧或重组错误。 - 最后一个 CF 之后,比对已收字节数是否等于 FF 声明的总长度。
实际操作中最容易看走眼的是 FC 方向。FF/CF 从 ECU 来,FC 从诊断仪来,两个 CAN ID 不一样,别只盯着一个 ID 找。
5.3 工程落地建议
最后说几个工程落地的经验,都是从教训里换来的:
缓冲区按天花板开。接收路径的缓冲区直接按 4095 字节规划,别按“当前业务只要 1KB”去省。业务需求一变,缓冲区没跟着变,Overflow 就来了。多帧重组缓冲区最好带长度校验,收到的字节数一旦超过 FF 声明长度,当场报错。
填充字节要统一。各帧的 padding 用什么值,OEM 规范经常有明确要求(有的要0x00,有的要0xAA)。发送方和接收方对这种字节的态度必须一致:发送方按规范填,接收方重组时一律忽略。最怕的是接收方对 padding 做严格比对,双方规范写得不一样,白白丢一帧。
SN 回绕写进代码评审清单。4 位 SN 从 15 回绕到 0,这个逻辑太容易写错了。写if (sn == 0) sn = 0x20这种代码时,一定要让同事评审——我见过不止一次因为这里写错,导致大数据量传输必现丢帧。
功能寻址别传大消息。0x7DF 功能寻址适合发短命令,因为多个 ECU 会同时回响应,TP 层无法处理多路响应交错。大块数据传输必须走物理寻址,这是规范隐含的要求,也是很多“刷写时总线乱成一锅粥”问题的根源。
超时参数先按 OEM 规范来,再留余量。自己搭测试台时,把 N_* 都设成规范值的 1.5 到 2 倍去测;如果这个余量下还频繁超时,问题基本不在超时配置,而在总线负载或对端处理速度。
我在实际项目里排查过一个问题:某控制器刷写时偶发“传输中止”,抓了很久的包,最后发现是诊断仪在收到 FF 后,因为应用层忙于处理上一帧,回 FC 超过了 N_Bs,ECU 直接放弃传输。问题根本不在 TP 层协议本身,而在于诊断仪实现里把 N_Bs 配得太紧,又没给应用层留够处理时间。这类问题用抓包软件一眼就能定位:FF 和 FC 之间的时间戳差摆在那里,对照超时配置就知道是谁的锅。
ISO 15765-2 作为 UDS 的基础传输通道,原理并不复杂,就是“一报长度、二回闸门、三段编号发货、四端拼装验收”。真正让它在量产项目里显得难缠的,永远是那些边边角角的参数配置和实现细节。把四种帧的 PCI 读熟,把 BS、STmin、N_* 这几个数装进脑子里,再复杂的刷写问题,抓包一看基本都能有个七成判断。