资讯动态

ISO 15765-2 TP层详解:UDS刷写中的CAN分包与流控调优

发布时间:2026/10/13 4:23:57 来源:尧图企业网站定制
干汽车诊断这一行的几乎天天要跟 CAN 总线、UDS 和 ISO 15765-2 打交道。CAN 单帧最多就是 8 个数据字节而你需要传的东西却经常是几百字节起步甚至一个刷写块就是 4KB。这个矛盾怎么解决答案就是 ISO 15765-2 定义的网络层也就是俗称的 TP 层。UDSISO 14229-1应用层只需要说“我要读哪个 DID”“我要下载多大的数据”至于底层怎么把 4KB 拆成几百帧、怎么排序、怎么流控、怎么重组全部由这一层负责。这篇文章不打算照抄标准原文我直接把这层协议的数据帧格式、分包重组过程、流控参数调优以及这几年调测诊断栈时踩过的真实坑都讲透适合做 ECU 诊断协议栈、刷写工具或 OTA 方案的工程师参考。1. 8 字节帧撞上 KB 级数据UDS 应用层为什么不能自己拆包很多人第一次接触诊断协议时会有个疑问既然 CAN 一帧才 8 字节那我传输大数据时自己把数据切开、一帧一帧发出去不就行了为什么还要单独搞一个 TP 层1.1 先看看哪些诊断服务会吐出大块数据先说清楚一个事实UDS 并不总是“一次小请求 一次小响应”的小事务。实际开发中以下场景的数据量动辄上百字节甚至更多0x22 按 DID 读取数据读 VIN 码 17 字节读软件件本号、标定校验值、冻结帧快照经常几十到几百字节。如果按 CAN 单帧 8 字节算一个 300 字节的快照就要拆 40 多帧。0x23 按地址读内存读 RAM 或 Flash 指定区域单次最大可申请读 4095 字节。0x2E 按 DID 写数据写入长配置、证书、密钥材料数据块往往超过单帧容量。0x34 0x36 下载 传输数据这就是刷写固件的核心流程一块典型的数据块大小就贴着 TP 层上限标题里的“4KB”就是这个场景来的。0x27 安全访问现在很多 ECU 的种子/密钥已经做到 16 字节甚至更长虽然多数情况下一个 SF 能塞下但密钥交换逻辑复杂时也会突破单帧极限。其中刷写是最典型的。实际 OTA 项目中一个应用镜像动辄几十 KB 到几 MBECU 的 Bootloader 通常不会一次性接收整包而是按块接收、边收边写 Flash。块大小为什么常选 4KB因为不少 MCU 的 Flash 擦除页就是 4KB 或 8KB把下载块和擦除页对齐写入逻辑最简单缓冲区也最好规划。于是“8 字节的 CAN 怎么传 4KB 的诊断数据”就变成了每个刷写工程师都要回答的问题。1.2 分层设计UDS 只看到“一条完整字节流”如果让应用层自己拆包第一个问题就是接收方不知道总长度。你发 500 帧过去接收方怎么知道什么时候算收完中途丢了一帧又怎么发现第二个问题是流控没人管。ECU 的缓冲区可能只有 2KB发送端如果一股脑把 64KB 都灌进总线接收方必然溢出。第三个问题是互通性。制造诊断仪、刷写工具的厂家很多每家的 ECU 协议栈也各不相同如果大家都自己发明一套拆包规则工具和 ECU 根本没法组合使用。所以标准的分层思路是这样的UDS 应用层把一整段数据交给 TP 层并声明长度TP 层负责把它切成适合 CAN 帧的单片在接收端再还原成完整字节流真正关心“这一段数据分了几帧、序号是多少、中间有没有等待”的人才去看网络层。这跟寄快递很像你只需要写收件地址包裹怎么分拨、怎么中转、怎么拼接物流公司内部消化你拿到手的是一个完整箱子。2. SF、FF、CF、FC 四件套TP 层通信的基本语法TP 层所有功能都靠四种帧完成每帧的第一个字节叫 PCIProtocol Control Information高 4 位表示帧类型低 4 位随着类型不同表达不同的含义。这四种帧的分工可以用一个表格说清楚帧类型PCI 高 4 位低 4 位含义经典 CAN 帧内的数据载荷作用Single FrameSF0x0后续数据长度1~77 字节一条完整消息 ≤7 字节时一帧发完First FrameFF0x1总长度高 4 位配合第 2 字节6 字节起始载荷宣告总长度并携带消息开头Consecutive FrameCF0x2序号 SN1~15 循环7 字节携带后续载荷Flow ControlFC0x3流控状态 FS无业务数据控制发送节奏或中止传输每种帧在普通寻址、经典 8 字节 CAN 帧下的 PCI 占位和实际可用字节数是写协议栈时必须背下来的基础。下面逐个拆。2.1 PCI 字节每个帧头的类型位是怎么拼出来的PCI 是整个 TP 层的灵魂。整个网络层没有额外的地址字段除扩展寻址外所有语义都压在这个字节上。SF 的 PCI 是0x0nn 表示本条消息的数据长度n 最大 7普通 CAN 数据域 8 字节减去 1 字节 PCI正好剩 7 字节。FF 的 PCI 是0x1nn 是 12 位总长度的最高 4 位第二个字节是长度的低 8 位。CF 的 PCI 是0x2nn 是序号。FC 的 PCI 是0x3nn 是流控状态。十六进制看起来抽象实际抓包时一眼就能认出来报文第一个字节以0x0开头就是单帧以0x1开头就是首帧以0x2开头就是连续帧以0x3开头就是流控帧。我做总线分析时习惯先在过滤条件里按 PCI 高 4 位分类看一条长消息的帧结构是否完整比逐个看字节快得多。2.2 SF 与 FF一帧搞定 vs 先报长度SF 是“小消息快车道”。比如发一个 2 字节的 UDS 请求22 F1 90TP 层直接封装成02 22 F1 90 xx xx xx xx一个 CAN 帧就发完了。这里02就是 PCI高 4 位为 0低 4 位表示后面有 2 个数据字节其余位置是填充字节接收方忽略。FF 则用于超过 7 字节的消息。它的第二个字节加上第一个字节的低 4 位组成一个 12 位长度字段最大值0xFFF也就是4095字节。这正是标题里“4KB”的真实上限。很多人习惯把“4KB”当成 4096但经典 TP 的长度字段只有 12 位编不了 4096。如果你真的传 4096len 8 0x10和类型 0x1 或完之后变成0x20接收方读到的就是一个 CF 帧整个传输直接乱套。所以实际刷写块的规格要么是 4095要么干脆用 2048、1024 这类更安全的值。这个“差一字节”的坑我下面专门讲。2.3 CF 的 SN 序号为什么从 1 绕到 15首帧之后的所有后续数据都通过 CF 传输。CF 的 PCI 低 4 位是序号 SN范围 1~15每发一帧加 1到 15 之后下一个是 1而不是 16 或 0。SN 0 保留给 FF所以连续帧序号永远不会出现 0。序号的校验是接收端发现丢帧的关键。比如接收方连续收到 SN 12、13、14、16中间跳过了 15说明 15 号帧在总线上丢了或损坏了这时接收方应当中止本次接收而不是继续把残缺数据往上送。实际总线上一帧 15 个序号之后立刻回绕如果接收方的校验逻辑没把“15 到 1”这种情况排除很容易误判成乱序。2.4 FC 载荷FS、BS、STmin 就藏在三字节里流控帧只有 3 个字节有实际意义PCI(FS)、BS、STmin后面 5 个字节全是填充。FS 为 0 表示 ContinueToSend允许发送方继续FS 为 1 表示 Wait请等待FS 为 2 表示 Overflow缓冲区装不下了中止本次传输。BS 是指一个“块”内最多允许发送多少个 CFSTmin 是相邻两个 CF 之间的最小发送间隔这两个参数的工程意义在第 4 章展开。3. 手算一遍 4095 字节的拆与装586 帧的前因后果光看格式不够我建议你把一次完整的 4095 字节传输手算一遍。算完你对整个 TP 层的工作方式和性能瓶颈会有非常直观的理解。3.1 发送端分包流程与 C 语言实现普通寻址、经典 8 字节 CAN 帧、总长度 4095 字节分包过程如下首帧 FF2 字节 PCI 6 字节数据剩余4095 - 6 4089字节连续帧 CF每帧 1 字节 PCI 7 字节数据CF 帧数4089 / 7 584.14向上取整得到585 个 CF其中前 584 个各带 7 字节最后一个只带 1 字节总帧数1 个 FF 585 个 CF 586 帧。发送端的 C 语言实现大概长这样核心是“先发 FF等 FC再按 SN 循环发 CF”#define PCI_TYPE_SF 0x00u #define PCI_TYPE_FF 0x10u #define PCI_TYPE_CF 0x20u #define PCI_TYPE_FC 0x30u #define SF_MAX_DATA 7u /* 普通寻址 经典 CAN 8 字节帧 */ #define FF_MAX_DATA 6u #define CF_MAX_DATA 7u static uint8_t tx_buf[8]; void tp_send(uint8_t *msg, uint16_t len) { uint16_t offset; uint8_t sn 1u; if (len SF_MAX_DATA) { tx_buf[0] (uint8_t)(PCI_TYPE_SF | len); (void)memcpy(tx_buf[1], msg, len); can_send(tx_buf, 8); return; } /* FF长度 12 位高 4 位放进 PCI 低 4 位 */ tx_buf[0] (uint8_t)(PCI_TYPE_FF | ((len 8) 0x0Fu)); tx_buf[1] (uint8_t)(len 0xFFu); (void)memcpy(tx_buf[2], msg, FF_MAX_DATA); can_send(tx_buf, 8); /* 等待 FC这里省略实际接收等待逻辑 */ offset FF_MAX_DATA; while (offset len) { uint8_t n (uint8_t)((len - offset) CF_MAX_DATA ? CF_MAX_DATA : (len - offset)); tx_buf[0] (uint8_t)(PCI_TYPE_CF | sn); (void)memcpy(tx_buf[1], msg[offset], n); can_send(tx_buf, 8); offset n; sn (uint8_t)((sn % 15u) 1u); /* 1..15 循环 */ /* 此处应按照接收方 FC 给出的 STmin 做延时 */ } }注意最后那个 CF它可能只有 1 个数据字节但这一帧仍然要发DLC 也可以小于 8。接收方因为有 FF 里声明的总长度知道什么时候该收完。协议规定连续帧至少要带 1 字节数据别发 0 字节的 CF。3.2 接收端重组与 SN 校验接收端相对简单本质是个状态机空闲状态下读到PCI 0x1n的 FF记录总长度分配缓冲区如果缓冲区不够回 FC OVERFLOW如果够回 FC CTS附上 BS 和 STmin。然后进入“等 CF”状态按序号把每个 CF 的 7 字节或最后一帧更少拷贝到缓冲区直到收到的字节数等于 FF 宣告的总长度就把整条消息交给 UDS 层。SN 校验在接收端非常关键。一个健壮的做法是记录“下一次期望的 SN”收到 CF 后比较不一致就立即中止并清理缓冲区。尤其注意 15 之后的回绕期望序号 15 时下一个期望应该是 1而不是 16 或 0。我见过不少协议栈在这上面栽跟头原因就是回绕判断写成了sn expected_sn 1遇到 15 之后直接报错。3.3 500 kbps 下到底要传多久STmin 对总时长的影响算完帧数顺便把时间也算一遍。经典 CAN 标准帧8 字节数据一帧在总线上占用的比特数大约是1SOF 11ID 1RTR 6控制 64数据 16CRC 及分隔 2ACK 7EOF 3IFS 111 比特。在 500 kbps 下一帧约111 / 500000 222 微秒。586 帧如果完全不隔纯总线占用约586 × 0.222 ≈ 130 毫秒。但实际传输不可能全靠总线空转接收方 FC 会给出 STmin。假设 STmin 10 ms那么 585 个“帧间隔”就是585 × 10 ms 5.85 秒再加 130 毫秒的总线时间传一个 4095 字节块接近 6 秒。如果 STmin 压到 1 ms总耗时约 0.72 秒。这就是为什么刷写速度受 STmin 影响巨大——总线本身明明有接近 50% 的带宽富余但协议节奏卡住了。这里顺便说一个很多人忽略的点TP 层并不是 100% 高效。4095 字节数据最终需要 586 个 CAN 帧每帧 DLC 都是 8总共在数据域占用了 4688 字节的容量加上帧头、CRC 等总线开销真正有用的数据占比也就一半左右。所以评估一条诊断刷写时间线时别只拿“数据量 ÷ 波特率”算要用“帧数 × 单帧总线时间 帧间约束”来算这样算出来的时间才和实测对得上。4. BS 和 STmin 的工程调优刷写快慢全靠这两个参数很多人把 TP 层当“拆包工具”却忽略了流控参数其实是刷写性能的分水岭。同一个 ECU、同一条总线上BS 和 STmin 配得好4KB 块能跑进 1 秒配不好直接掉到 6 秒甚至触发 OVERFLOW 中止。4.1 BlockSize 的节奏控制与 BS0 的例外BS 是接收方在 FC 里下发的表示“你最多连续发这么多 CF然后必须停下来等我下一个 FC”。比如 BS 16发送方发完 16 个 CF 就要暂停等待接收方再次下发 FC CTS才能继续。BS 0 是特例表示不限块数发送方可以一直发到消息结束中间不再需要 FC。为什么需要 BS因为接收方的缓冲和消费能力是有限的。典型场景是刷写ECU 的 TP 接收缓冲区可能只有 2KB但 Flash 写入是按页的驱动在写一页时要占用内部总线来不及同时消费后续的 CF。如果 BS 设成 0TCP 层数据会一直灌入缓冲区很快溢出。设一个合适的 BS比如 8 或 16接收方每收一小批就停下利用发送方等待 FC 的时间把缓冲区里的数据写入 Flash、腾出空间再放行下一批。这实际上是一个简单的“生产者-消费者”握手只不过握手信息塞在了 FC 帧里。BS 的值不是越大越好。BS 太大会让某一段突发数据超过接收方瞬时处理能力太小则每个块之间都要等一次 FC协议开销变大。我一般先按“接收缓冲区容量 ÷ 单 CF 数据长度”再留一点余量来定比如缓冲区 256 字节、CF 数据 7 字节那 BS 取 32 左右比较安全。4.2 STmin 毫秒/微秒编码与收发时序违约STmin 是三字节流控参数里的第三个字节表示发送方相邻两个 CF 之间的最小间隔时间。编码规则是0x00 ~ 0x7F对应 0~127 ms0xF1 ~ 0xF9对应 100~900 微秒主要用于 CAN FD 高速场景其余值保留。实际工程里最常见的坑是发送方不遵守 STmin。有些上位机工具为了“快”把 STmin 当成参考值发完一帧立刻发下一帧结果 ECU 缓冲区被冲爆直接回一个 FS 2 的 OVERFLOW整个 4KB 块作废。调这类问题第一件事就是抓总线看两个 CF 之间的实际时间间隔拿示波器或总线分析工具的“带时间戳”模式量一量很多情况下问题不在 ECU而在发送工具不老实。STmin 选多少合适没有通解。保守起见先用 10 ms 或 20 ms跑通后再根据接收方处理和响应情况逐渐往下压。注意STmin 不是越小越好除了缓冲区压力收方 CPU 每收一帧都产生一次接收中断和协议栈处理STmin 低于接收方处理一帧的耗时必然丢帧。量产 ECU 的通信矩阵里通常会明确写死这个值第三方工具必须遵守。4.3 WAIT 与 OVERFLOW 的实战用法FS 1WAIT在实际协议栈里用得不多但很能体现实现水平。它的语义是“我暂时忙不过来但没打算中止你先等着”。比如接收方正在擦除 Flash 扇区可能要几十毫秒这段时间如果让它把 FC CTS 发出来但没有及时消费后续 CF就会出问题。正确做法是回一个 WAIT发送方收到后冻结发送接收方忙完后再回一个 CTS 恢复。这里有个实现细节很多协议栈约定 WAIT 只回一次如果发送方连续多次收到 WAIT就不再傻等而是按 N_Bs 超时处理并上报错误避免两边互相等待导致死锁。ISO 标准没有强制规定最多几次 WAIT但实际项目中“只允许一次 WAIT”是比较稳妥的约定。FS 2OVERFLOW则是明确的“我装不下了这次传输作废”。收到 OVERFLOW 后发送方必须中止整个消息的传输已发的所有数据都作废。要不要自动重传取决于上层策略。刷写场景里我更倾向于让上层应用感知到这个失败由刷写策略决定是重试当前块还是终止刷写流程而不是让 TP 层无限自动重传否则很容易在 ECU 处于半写状态时反复尝试把 Flash 搞坏。5. 寻址模式、CAN FD 扩展与实战排障经验最后说三个实战中逃不掉的话题寻址方式对有效载荷的影响、CAN FD 带来的协议变化以及我这些年整理的一套排查清单。5.1 扩展寻址多占一字节载荷立即缩水普通寻址下CAN ID 本身就区分了收发双方8 字节数据域全部可用于 TP 数据。扩展寻址则是在数据域第一个字节放一个“目标地址/源地址”用来在共享 ID 的多个逻辑节点之间再做区分。代价很直接每帧可用数据少 1 字节。扩展寻址下SF 数据只有 6 字节FF 携带的起始数据只有 5 字节CF 每帧只有 6 字节。还传 4095 字节帧数就变成 1 个 FF 682 个 CF(4095-5)/6向上取整比普通寻址多了 97 帧传输时间直接增加约 17%。所以设计通信矩阵时能用普通寻址就别开扩展寻址如果必须扩展要把这个开销计入性能预算。这个坑在调第三方诊断工具时尤其常见。配置工具时选了普通寻址ECU 却按扩展寻址解析结果所有长消息都解析失败短消息因为 SF 恰好把地址字节当作数据还可能“侥幸”通过。排查时先确认通信矩阵里寻址模式到底怎么写再去看工具配置。5.2 CAN FD 时代 TP 层的变化20 位长度和微秒 STminISO 15765-2 在 2016 版本里补充了 CAN FD 支持主要变化有三点FF 长度字段扩到 20 位PCI 占 3 个字节长度最大值从 4095 提升到约 100 万字节4KB 不再是天花板。CF 单帧载荷变大64 字节 CAN FD 帧下CF 每帧可携带 63 字节数据FF 也能带 62 字节起始数据。还是 4095 字节1 个 FF 65 个 CF 就够了对比经典 CAN 的 586 帧帧数直接少一个数量级。STmin 增加微秒档位0xF1 ~ 0xF9对应 100~900 µs适配 FD 的高波特率。所以现在做新项目的朋友如果从零开始我强烈建议直接在 CAN FD 或者 CAN XL 的方向上评估。CAN FD 的 64 字节帧在物理层上已经非常接近以太网单帧的有效载荷配合 20 位 TP 长度字段刷写一个大块镜像需要的握手次数大幅下降端到端时间可以做到经典 CAN 的几分之一。但要注意CAN FD 网络里如果混有经典 CAN 节点仲裁段仍然用经典 CAN 的波特率只有数据段才切到高速率TP 层报文填充长度、DLC 检查和混跑兼容都需要在通信矩阵阶段就定清楚否则两头认为的帧格式不一样那才是噩梦。5.3 现场调试最常踩的坑与排查清单我总结了一份精简的排查清单每次遇到 TP 层问题都能照着过一遍现象大概率原因处理方向发了 FF 后一直收不到 FC直到 N_Bs 超时接收方缓冲区不足/上层忙或 FC 的 CAN ID 被过滤掉了先看总线分析工具里接收方是否回了任何帧再看 FC ID 配置中途卡住直到 N_Cr 超时某个 CF 在总线上丢失或错误帧看总线错误帧计数检查波特率和终端电阻收到 OVERFLOW接收方缓冲被占满BS/STmin 加速太狠调小 BS、调大 STmin或增大接收缓冲区首帧解析乱码传输长度等于 4096 被截断成 4095/0或扩展寻址的地址字节被当数据核对长度字段 12 位上限核对寻址模式偶发丢帧但总线无错误帧发送方没遵守 STmin或协议栈中断处理不及时用分析工具测量相邻 CF 间隔时间对比 STmin 声明值这里挑两个典型场景展开一下。第一个是“4096 魔术数字”。我参与过的刷写项目里不止一次看到有人把下载块大小配置成 4096理由很简单4KB。结果 FF 的 12 位长度字段没法表达 4096长度高 4 位0x10和类型位0x1或起来变成0x20接收方把首帧当成 CF 帧SN 还是 0直接按协议错误丢弃。这种问题最坑的地方在于它不是每次都失败而是和具体的长度值强相关偶尔工具又因为某种巧合“能跑通”导致排查要花很久。我的建议是凡是和安全裕量相关的参数尽量选距离边界远一点的值比如 1024、2048实在要顶格就明确按 4095 算。第二个是“STmin 形同虚设”。有一次在台架上测试刷写ECU 总是在数据传输到一半时报 OVERFLOW且每次失败点还不一样。抓总线发现工具发出的相邻 CF 间隔只有 STmin 声明值的一半进一步看是工具使用的是“尽力而为”模式应用层发出两个帧之间差了 5 ms但 TP 层代码没有真正把 STmin 落到发送循环里而是依赖 OS 调度的天然间隔。把这些“天然间隔”当成 STmin等于没设。后来把发送循环改成硬实时校验帧间隔严格按照 STmin 执行OVERFLOW 再没出现过。教训就一句话协议栈的发送路径上STmin 必须是硬约束不是软建议。做了一个项目之后我的习惯是遇到 TP 层问题先不看代码先用总线分析工具把 FF、FC、CF 按 ID 过滤出来把总长度、BS、STmin 和通信矩阵一对比再配合超时表N_Bs、N_Cr 这类判断是节奏问题还是丢帧问题基本能定位八成故障。TP 层这东西原理并不复杂难就难在每一个字节、每一个时间参数都要较真较过真之后UDS 刷写和诊断里的大多数疑难杂症你都会有很清晰的排查路径。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑