深入解析ISO 15765-2:从CAN帧到UDS多帧传输的TP层机制

发布时间:2026/10/13 3:45:25
深入解析ISO 15765-2:从CAN帧到UDS多帧传输的TP层机制
搞过车载诊断或者ECU软件开发的朋友估计都经历过这个场景诊断仪发一条UDS请求比如0x22按ID读数据应用层逻辑很简单填好SID和DID丢下去就行了。可问题在于底层是CAN总线经典CAN一个数据帧最多8字节有效数据。而UDS服务动辄几十上百字节Bootloader刷写的时候一条传输数据轻轻松松几KB甚至几MB。8字节的“小车”怎么拉走几KB的“大货”答案就是ISO 15765-2也就是业内常说的TP层。这篇文章的定位很明确把ISO 15765-2的核心机制讲透。不管你是写MCU端诊断协议栈的底层工程师是做诊断仪上位机的开发还是天天拿诊断仪在台架上点来点去的测试工程师TP层都是绕不开的基本功。我会从帧格式、多帧传输流程、流控参数、寻址模式一直讲到实际调试中的坑尽量做到看完就能用。1. 从8字节到4KB一条诊断报文的“运输困境”1.1 经典CAN帧到底有多“小”经典CANClassical CAN的数据场长度受DLCData Length Code限制DLC最大值是8所以一个标准CAN数据帧最多只能携带8字节用户数据。别小看这8字节它里面包含了ID、控制位、CRC、ACK等一大堆开销。真正干活的“货仓”就是那8个字节多一个都塞不下。CAN FD出来之后数据场最多扩展到64字节确实缓解了不少压力但也没根本性解决问题——64字节依然装不下一条动辄几百字节的完整诊断消息更别提固件刷写时几MB的映像文件了。所以不论经典CAN还是CAN FD只要走的是CAN总线就天然需要一层“分包-重组”机制。这里顺便说清楚一个容易混淆的概念ISO 15765-2定义的传输协议负责把一条长报文拆成多个CAN帧发送再在接收端重组。它处于UDS应用层ISO 14229-1和CAN数据链路层之间处于OSI模型里传输层/网络层的位置所以行业内习惯直接叫它TP层。1.2 UDS诊断请求为什么动辄很大有人会问诊断请求不都是短消息吗像0x10会话控制、0x11 ECU复位这种确实很短几个字节就完了。但UDS远不止这些。举几个真实场景读取VIN码0x22服务读F190响应的数据是17字节ASCII字符加上服务ID和DID一共20字节。20字节在单帧只能装7字节的现实下必须走多帧。读取DTC快照0x19服务读冻结帧数据一个快照包含环境数据、时间戳、里程等多组信息动不动就上百字节。例程控制0x31服务做某些标定例程比如下载一段查表数据或者上传一段采集结果数据量很容易到几百字节甚至KB级。Bootloader刷写0x34请求下载、0x36传输数据、0x37请求退出传输这是TP层压力最大的场景。一段固件可能几百KB分包后几百上千帧连续往外发。还有些OBD诊断场景比如读取排放相关的监控数据会一次请求几十上百条PID响应数据同样轻松超过单帧容量。1.3 “4KB”这个数字从哪来很多初次接触ISO 15765-2的人会记住一个数字一条TP消息最大长度是4095字节。这个4KB级别的上限正好对应经典CAN多帧传输规格的上限。为什么是4095因为首帧FF里的长度字段占12位。算一下2的12次方减1正好等于4095。也就是说不管底层CAN帧多么小通过TP层分包理论上一条应用层消息最大可以传接近4KB。不过这里有个很容易忽略的细节4KB如果严格按1024乘以4等于4096字节算已经超过了4095所以4KB恰好卡在TP层单条消息的边界上。实际工程中应用层很少把一条TP消息塞到接近上限。以刷写固件为例0x36请求里通常是一个块加一块数据块大小往往设置在256字节到1024字节之间。这样设计不只是为了配合TP层更是为了出错重传时不必整条大消息重来控制粒度更细。明白了这个背景自然知道接下来要解决的核心问题怎么用8字节的CAN帧把最长4095字节的消息安全可靠地传过去。答案就是ISO 15765-2定义的那四种帧。2. ISO 15765-2帧格式四种帧和它们的PCI2.1 四种帧类型总览TP层协议控制信息PCIProtocol Control Information放在CAN数据场的前面用来标识当前帧是哪种类型、携带什么控制参数。经典CAN下ISO 15765-2定义了四种帧每种帧通过PCI的最高四位区分。帧类型PCI高4位简称作用单帧0x0SF整条消息不超过7字节时一帧搞定首帧0x1FF多帧传输的第一帧声明总长度并携带部分数据连续帧0x2CF多帧传输的数据帧按序号连续发送流控帧0x3FC接收方向发送方反馈允许发送、等待或终止其中SF和CF的PCI是1字节FF的PCI是2字节FC的PCI是3字节。下面逐个拆解。2.2 单帧和首帧短消息与大消息的入口**单帧SF**适用于整条应用层消息不超过7字节的情况。注意是“应用层消息总长度”不超过7字节不是CAN数据场等于7字节。SF的PCI格式是0x0N低4位的N表示后续用户数据的长度。以一条典型的0x11复位请求为例CAN数据场是0x02 0x11 0x01PCI为0x02低4位为2表示后面有2字节数据。注意这里的0x02代表协议长度它只表示TP层载荷的长度也就是UDS消息自身的长度。一条0x10 0x01的会话控制请求总共就2字节所以SF真正需要的CAN帧DLC是1加2等于3。**首帧FF**用于总长度超过7字节的消息。它的PCI占用2字节Byte0 0x1X Byte1 LL高4位的0x1标识这是首帧低4位的X是总长度的高4位Byte1的LL是总长度的低8位。两个拼起来是12位长度值范围0到4095。所以一条总长度4000字节的消息FF的Byte0高4位是1低4位是4000的高4位这里4000的十六进制是0xFA0高4位是0低8位是0xA0所以Byte0是0x10Byte1是0xA0。FF携带的数据量等于8字节CAN数据场减去2字节PCI最多6字节。也就是说总长度4000字节的消息FF先带走6字节剩下的3994字节全部由CF接力。2.3 连续帧与流控帧数据流和水龙头**连续帧CF**的PCI是1字节格式为0x2N其中低4位是序号SN。CF序号从1开始每发送一个CF递增1递增到15之后下一个回到0之后继续循环。每个CF最多携带7字节数据。整个多帧传输中CF是真正的数据主力一个接一个地把剩余数据传完。**流控帧FC**是整个协议里最特殊的因为它不带用户数据只做“流量控制”。它的PCI固定为3字节Byte0 0x3S Byte1 BS Byte2 STminByte0低4位的S是流控状态FS三种取值0继续发送CTS表示接收方准备好接收连续帧。1等待WT表示接收方暂时处理不了稍后会再发FC。2溢出OVFLW表示接收方缓冲区装不下整条消息本次传输中止。Byte1的BS是块大小告诉发送方最多可以连续发多少个CF发满这个数必须停下来等新的FC。BS为0是特殊含义表示不限制可以一直发到消息结束。Byte2的STmin是连续帧最小间隔时间单位毫秒常用值0x0A代表10ms0x14代表20ms。它解决的是接收方处理不过来、同时防止总线上消息刷屏的问题。STmin由接收方决定发送方必须严格遵守。2.4 一个具体例子读VIN的响应把上面几种帧连起来看一个实际例子。假设ECU响应0x22服务返回VIN码17字节和响应头3字节总共20字节的消息响应消息0x62 0xF1 0x90 17字节VIN 总长度20字节总长度20字节显然超过SF的7字节上限所以走多帧FFPCI为0x10 0x14后跟最多6字节响应数据。这里0x14是十进制20正好是总长度。然后接收方回FCFS为0继续BS设为0不限制STmin设为0或某个小值。发送方连续发送CF剩余14字节数据需要2个CF每个最多7字节。接收侧累计收到的字节数达到20知道消息收齐了。3. 多帧传输的完整流程与流控细节3.1 从首帧到流控一次完整的握手多帧传输不是发送方把数据一股脑往外扔而是有握手、有节流、有确认的。标准流程是这样的发送方先发一帧FF把总长度和首批数据送出去。接收方收到FF后根据自己能用的缓冲区和处理能力回一帧FC。如果一切正常FC里的FS为0BS和STmin按实际情况填写。发送方收到FS等于0的FC后开始连续发送CF。如果接收方暂时没准备好比如内存正在被其他任务占用它会回FS等于1的FC相当于说“先等一下”。发送方收到等待指示后不能继续发CF只能等下一个FC。这个机制在ECU开发早期特别有用因为ECU内部往往同时跑着CAN收发、数据处理、诊断任务缓冲区不是随时都够。如果接收方发现消息总长度超过了缓冲区容量直接回FS等于2的FC整个传输就此终止。这种情况一般属于协议栈设计缺陷或者参数配置错误正常量产ECU里很少出现。3.2 连续帧序号SN的循环规则CF序号看似简单但这里是初学者踩坑最多的位置。SN一共4位从1开始。第一个CF的SN是1第二个是2一直递增到15下一个变成0然后继续0、1、2……循环。注意两个关键点SN从1开始不是从0开始。FF不带序号标记第一个CF就是1。从15回到0而不是回到1。不少人在实现接收方校验时写错导致长消息传到最后几帧总是报序号错误。以4000字节消息为例需要571个CF。SN会经历15到0的循环多次接收方必须按循环序号规则校验。严格实现里接收方会记录期望的下一帧SN每收到一个CF就比对一次不匹配直接丢弃或报错。3.3 BS与STmin两个流控节流阀怎么调理解了四种帧之后TP层调试中真正需要花心思调的就是BS和STmin。BS决定了发送方连续发多少个CF后必须停下来等FC。如果把BS设为0发送方会一口气把所有CF全部发完。对于一段较长的固件刷写流程来说BS设为0可以减少收发双方反复握手的次数提高总吞吐量但前提是接收方的缓冲和处理速度跟得上。如果BS设为1就变成每发一个CF就要等一次FC吞吐量会急剧下降。实际项目中很多ECU对连续帧的处理速度并不慢完全顶得住整条消息连续到达所以BS设0比较常见。STmin则是一个“时间间隔”接收方用它告诉发送方每个CF之间至少隔多少毫秒。它和BS解决的是不同层面的问题。BS控制的是“发多少个歇一次”STmin控制的是“每次发送之间隔多久”。两者的本质都是一种降速手段防止接收方来不及处理。实际调试时如果发现接收方丢帧优先排查STmin是不是被设成了0且接收缓冲区不够大或者ECU的中断处理阻塞。反过来讲如果消息传得慢也要看看是不是某个ECU把STmin设得过大比如0x64代表100ms一条4000字节消息571个CF每个间隔100ms光等待时间就接近57秒这在台架测试时能明显感觉到卡顿。3.4 超时与异常终止几个N_参数必须知道ISO 15765-2定义了一组超时参数实际调试中打交道最多的有三个N_Bs发送方发出FF后等待FC的超时上限。N_Cr接收方发出FC后等待下一个CF的超时上限。如果接收方在FC发出后N_Cr时间内没收到CF会认为发送方异常。N_As / N_Ar发送方应用层请求到报文真正上总线的最大时间以及接收方收到报文到应用层获取数据的最大时间。典型经验值里N_Bs和N_Cr通常配置为1000ms。量产项目里为了快速诊断断线有的会收紧到500ms甚至更短但过短容易导致正常操作被误判为超时。这里要结合总线上最坏情况计算比如STmin为10ms时100个CF需要1秒才能传完如果N_Cr设成500ms就必然误判。所以这两个参数的设置必须和BS、STmin联动考虑。多帧传输中途还有一个特殊场景接收方如果发现已经接收到的字节数不等于总长度或者FC给出了溢出状态传输都会被中止。中止后发送方应用层通常会收到一个错误指示而不是UDS层的否定响应。这一点和UDS应用层的0x7F响应机制完全不同别把两层搞混了。3.5 4KB数据一次传输要多久用一条4000字节的消息来算一笔账看看TP层到底要发多少帧。FF带6字节剩余3994字节。每个CF最多带7字节需要多少个CF3994除以7等于570.6向上取整就是571个CF加上1个FF总共572帧。计算总耗时可以分两部分。CAN总线本身的传输时间取决于波特率。以500kbps为例一个经典CAN标准帧大约占用130到150个位时间换算下来每帧约260到300微秒。572帧的纯总线时间大概是150到170毫秒。这是PS全速发送、没有STmin的理想值。如果STmin是10ms每两个CF之间的等待时间是10ms571个间隔就是5.71秒传输立刻变成秒级。这就是为什么Bootloader刷写场景下STmin通常设0或1ms否则一次大块传输根本没法看。把4KB消息在单次TP传输中耗时控制在200毫秒以内对于多数刷写场景是够用的。但考虑到一整份几百KB的固件要拆成几百个TP消息每个消息还需要协议开销和块间的处理时间总刷写时间就很容易拉到几分钟。这是TP层设计本身的合理代价也是应用层要把块大小、流控参数一起优化的原因。4. 寻址模式与CAN ID规划4.1 物理寻址和功能寻址的区别TP层只管数据可靠传输但它不管“这帧数据是发给谁的”。这个任务由CAN ID结合寻址模式完成。UDS诊断里有两种寻址方式物理寻址是点对点的。诊断仪给某一个ECU发请求CAN ID里明确指定了目标ECU的地址只有这个ECU会响应。典型场景是读取某ECU的故障码、单独复位某ECU。功能寻址是一对多的。诊断仪用一个功能地址广播总线上所有支持该功能的ECU都会收到请求并处理。典型场景是广播进入扩展会话或者广播读取所有ECU的VIN码。功能寻址下多个ECU同时响应会让总线上瞬间多出很多TP消息测试时要考虑到总线负载。4.2 常见CAN ID搭配规则ISO 15765-2标准本身并不强制规定具体的CAN ID数值但行业实践已经形成了相对固定的约定。最常见的11位标准CAN ID组合方向CAN ID说明物理请求0x7E0诊断仪发给ECU的对象地址物理响应0x7E8目标ECU回复给诊断仪功能请求0x7DF诊断仪广播给所有ECU29位扩展ID也很常见比如0x18DAxxF1其中包含了目标地址和源地址。具体取值由整车厂定义不同平台的诊断规范会给出明确的CAN ID分配表。需要注意的是CAN ID在TP层寻址中扮演的是地址角色因此同一个ECU的物理请求ID和功能请求ID往往是不同的。协议栈实现时要做地址过滤只接收发给自己的帧同时丢弃功能寻址中不需要响应的场景。4.3 扩展寻址一个ID下挂多个ECU有些项目为了节省CAN ID资源会使用扩展寻址模式。标准寻址下CAN ID本身就代表了源和目标而扩展寻址则在CAN数据场的开头额外加一个寻址字节N_AE用来区分同一个CAN ID下的多个ECU。扩展寻址对数据容量的影响很直接单帧可携带的数据从7字节变成6字节首帧可携带的数据从6字节变成5字节连续帧从7字节变成6字节。因为每一帧都要腾出1字节给N_AE。做诊断仪上位机时如果发现同一协议栈支持标准寻址和扩展寻址需要特别留意CAN ID和N_AE的配置逻辑。同一个物理请求ID下如果N_AE不同发给的ECU也不同。5. 协议栈实现的几个关键设计与避坑心得5.1 发送侧状态机怎么设计实现TP层发送侧本质上是一个状态机。先说状态划分建议原样抄下面这套IDLE: 空闲等待应用层发送请求 SEND_FF: 已发送首帧等待流控帧 SEND_CF: 已收到允许发送的FC正在发送连续帧 WAIT_FC: 已发送BS个CF等待新的FC状态切换逻辑要处理几个边界在SEND_FF状态收到FCFS等于0就进入SEND_CF。FS等于1继续等FS等于2直接回到IDLE并上报应用层错误。在SEND_CF状态每发完一个CF就减计数器。发送CF数量达到BS后进入WAIT_FC。如果BS是0则不进入WAIT_FC一直发到消息结束。SN序号从1开始每发一个CF加1。到达15后下一个变成0这个循环逻辑必须放在发送子程序里处理。这里有个容易错的地方BS是0的时候发送方在SEND_CF状态发完所有CF后直接回IDLE不需要等FC。BS不是0的时候最后一个CF如果恰好发满BS发送方要停下来等FC即便数据已经发完了也不能直接结束。有些协议栈实现漏了这一步导致总线上出现多余的FC交互。5.2 接收侧重组缓冲区怎么管理接收侧相对简单但缓冲区管理是重灾区。收到FF后从PCI里解析出总长度然后分配对应大小的缓冲区。之后每收到一个CF就往缓冲区里填数据。关键点在于校验累积长度。不要用CAN帧的DLC判断消息是否结束而是根据收到数据的累计字节数是否等于FF里声明的大小来判断。如果所有CF的总数据量刚好等于总长度消息完整可以提交给上层。如果总长度到了但数据还没发完或者数据发完了但总长度没到都属于异常。缓冲区不足时接收侧有两种选择发FS等于1的等待FC或者发FS等于2的溢出FC。工程上推荐优先使用FS等于1等待因为缓冲区在后续某个时刻可能腾出来直接溢出会让发送方整个消息白传效率太低。5.3 参数选型与兼容性建议BS和STmin的取值没有“最优解”取决于项目需求。我推荐这么设对于诊断仪侧BS设为0STmin设为0到10ms之间适应绝大多数ECU。对于ECU侧如果接收中断服务程序处理快BS设0STmin设0。如果处理较慢优先把STmin调大而不是把BS调小。因为调小BS会显著增加FC交互次数总线上多出很多帧反而增加负载。超时参数方面N_Bs和N_Cr建议先按1000ms设置确保最坏情况不漏判。如果实测出现长时间无响应再把N_Cr收紧到500ms同时排查具体是哪一层的延迟。还有一种兼容性问题某些老式ECU的FC帧没有严格填写BS和STmin可能出现BS为0但STmin为0xFF这种不规范组合。协议栈需要容忍这类异常值——至少不能因为STmin是0xFF就直接拒绝。更稳妥的做法是设置一个上限钳制比如STmin超过100ms时按100ms处理。5.4 我踩过的几个TP层的坑写TP层代码和调试的过程中遇到的坑多数不在大原理上而在细节里。分享几个印象深刻的**第一个坑是SN回绕。**某个项目测试长帧固件刷写传到最后几帧总出现序号错误。排查发现发送代码在SN到15后错误地重设为1没有变成0。这个错误在小数据量测试时完全暴露不出来因为几百字节的消息根本走不到15个CF。只有真正的长消息才能触发这种边界问题。写单元测试时一定要构造一条超过15个CF的消息专门验证SN从15到0的回绕逻辑。**第二个坑是流控帧的超时重发。**接收方因为缓冲区暂时不足回了一个FS等于1的FC但发送方在N_Bs超时后直接放弃了整个传输。实际协议里FS等于1的FC只是临时等待不在超时判定范围内。正确做法是发送方收到FS等于1后重新启动等待定时器继续等待新的FC。项目里有一版代码把FS等于1和超时混在一起处理结果导致ECU偶发诊断失败。**第三个坑是DLC固定为8的填值问题。**一些CAN控制器驱动在发送数据时如果消息实际长度不足8字节会用随机值补满。接收方如果按DLC判断数据长度可能把填充字节当成有效数据。TP层的长度信息在PCI里已经足够明确接收方必须严格按照累计数据量判断消息边界。调试时建议把填充字节统一成0xAA或0x00抓包时更好分辨也方便写断言。6. 常见问题速查与排查思路现象可能原因排查思路多帧传输发完FF后一直收不到FCN_Bs超时接收方缓冲区不足未及时回复抓包确认FC是否出现检查接收方协议栈状态发送方发到一定数量CF后停住不动BS配置和接收方预期不一致核对BS值确认发送方发满BS后等待FC的逻辑接收方报连续帧序号不连续SN回绕逻辑错误丢帧导致序号跳变打印每帧SN检查15到0的循环检查总线丢帧传输速度远低于理论值STmin设得过大BS设得过小计算每个CF间隔对比STmin配置短消息正常长消息失败FF长度字段解析错误缓冲区分配不足重点检查12位长度字段的拼接逻辑响应消息总是比预期多几个字节接收侧把填充字节当有效数据验证累计字节数判断逻辑不要依赖DLC功能寻址请求后多ECU响应总线上帧太多功能寻址使用场景不合理确认是否必须功能寻址必要时改物理寻址逐个访问排查TP层问题时最有效的工具是总线抓包。把每一个CAN数据的8字节完整打出来手工对着PCI走一遍流程通常几分钟就能定位问题。养成一个好习惯先看FF里的长度值再看FC里的BS和STmin然后核对CF的SN序列。这个顺序能覆盖绝大多数TP层故障。还有一个排查技巧在协议栈里加一个计数器统计发送CF总数和接收CF总数。如果两边不一致说明丢帧或者发错。这个计数器在量产版本里可以保留留着做现场诊断很有用。7. 项目里的几点体会做了几个诊断协议栈项目之后我最深的体会是TP层本身机制不复杂难点全在参数配合和边界处理。很多问题不是原理不懂而是小细节在不同ECU上的表现不一样。比如同一个诊断仪在甲ECU上BS设0跑得很顺在乙ECU上就丢帧同一个ECU在工程模式下的STmin要求和量产模式可能还不一样。协议的灵活性给开发者留了很多空间但也意味着不能拿一套参数打天下。调试时最好把所有TP层参数做成可配置项或者至少留一个运行时调试接口。我习惯把FF的长度、FC的BS、STmin、上一次超时状态都放在一个结构体里通过诊断仪读出来。这样现场出问题时不用反复刷固件直接看数据就能判断是发送方异常还是接收方异常。另外写长消息测试用例时别只测几百字节一定要测一条接近4095字节的消息。很多SN回绕、缓冲区边界问题只有在这种长度下才现出原形。ISO 15765-2这套协议这么多年没有被淘汰说明它的设计还是很扎实的。用8字节的CAN传4KB的诊断数据听起来像魔术拆开看就是一个清晰的分包逻辑加一套严谨的流控机制。把这个基础打牢后面做CAN FD支持、刷写流程优化、多ECU并行诊断都会顺手很多。