UDS诊断必懂ISO-TP:CAN网络层如何拆帧重组4KB数据?
接触UDS诊断的人应该都有这种经历读VIN、读DTC、会话切换请求短、响应短一帧CAN报文解决战斗。可一旦进入Bootloader刷写或者读取DTC扩展快照一条UDS响应根本塞不进8字节的CAN数据场。这时候如果你还按单帧思路抓包看到的全是零碎的数据根本拼不成一条完整的诊断消息。UDS网络层TP层ISO 15765-2解决的就是这个问题它在CAN之上定义了一套分段、重组、流控与超时机制把物理层的8字节帧拼成一条逻辑上完整的诊断报文。这篇文章从协议结构、传输流程、实际参数和踩坑经验几个角度把这套机制拆开讲透适合正在做UDS诊断开发、诊断测试或Bootloader刷写的小伙伴参考。1. 为什么8字节的CAN帧装不下诊断数据1.1 CAN单帧的物理上限经典CANCAN 2.0的数据场最大就是8字节DLC范围0到8协议写死的没有任何弹性。标准帧11位ID加8字节数据挂上总线大约110多位500k波特率下传一帧约0.2ms。这个8字节基本决定了任何超出7字节的应用层消息都必须想办法跨帧传输。CAN FD虽然把数据场扩到了64字节但现状是大量量产ECU、OBD接口、存量诊断工具依然是经典CAN。所以做诊断开发时默认还是要按一帧8字节去设计不能指望对方一定有CAN FD。物理层决定了每次最多递一张小纸条而我要传的是整本手册中间就差着这个网络层协议。1.2 UDS诊断服务里大块数据是刚需有人可能觉得诊断嘛不都是问一句答一句数据能有多大实际翻一下UDS的服务列表大块数据是日常不是特例0x22 ReadDataByIdentifier读VIN、读内部标定、读冻结帧响应经常超过7字节。VIN本身17字节单帧根本放不下。0x19 ReadDTCInformation读DTC快照、扩展数据记录、事件记录动辄几十上百字节。0x2E WriteDataByIdentifier写标定数据数据量可能很大和读一样需要承载大块。0x31 RoutineControl例程执行结果可能返回大数组比如计算校验值时给出一整段计算结果。0x34 RequestDownload 0x36 TransferData 0x37 RequestTransferExit刷写流程的重头戏。每个Block通常1KB到4KB整个升级包几十上百KB全靠0x36一包一包搬。如果TP层不存在这些服务在CAN上基本没法用。你没法把一条0x76正响应硬塞进8字节除非每个ECU各自搞一套分包方法那UDS就失去了跨ECU互通的意义。所以ISO 15765-2的出现本质上是给UDS在CAN上铺了一条能走大件的路。1.3 没有网络层协议会怎样有人会问CAN本身不是有ID和仲裁吗把数据拆成多帧顺序发接收端拼回去不就行了问题远没这么简单怎么告诉接收端这是一包的开始、总共有多少字节没有起点标记接收端拿到一堆帧根本无法判断归属。帧丢了怎么办总线拥塞、CAN错误帧、FIFO溢出都是常态没有序号接收端连丢帧都发现不了。发送端快速连发接收端缓冲区溢出谁来叫停接收端重组期间上层应用需要等待等多久算超时TP层就是回答这些问题的。ISO 15765-2定义了一套信封机制消息开头告诉对方这包多大后续每帧带序号接收端用流控帧请求暂停、继续、甚至终止传输。这套规则不关心你传的是UDS请求还是刷写数据对网络层之上的所有用户数据一视同仁。打个比方8字节CAN像一张明信片4KB诊断数据像一本书。TP层规定好了封面写页数、每页标页码、收件人每隔几页喊一声继续、丢页了重传这就是它存在的全部理由。2. TP层四类帧SF、FF、CF、FC的角色分配2.1 第一眼识别PCI类型域所有ISO-TP帧的第一个字节高4位定义了帧类型0x0是SF单帧0x1是FF首帧0x2是CF连续帧0x3是FC流控帧。低4位的含义根据帧类型不同而不同SF里是数据长度FF里是总长度的最高4位CF里是序列号FC里是流控状态。抓包时不需要完全解析协议栈看到首位字节是0x1、0x2、0x3开头就能立刻判断当前TP传输处于什么阶段。这是排查TP故障的第一步后面章节会反复用到。2.2 SF与FF小报文与首帧SF单帧用于长度不超过7字节的消息。Byte0 0x0 实际数据长度Byte1到Byte7是用户数据。比如最常见的读VIN请求22 F1 90只有3字节单帧就能发出Byte0填0x03。FF首帧用于长度在8到4095字节的消息。Byte0 0x1 数据总长度的高4位Byte1 数据总长度的低8位Byte2到Byte7是第一批用户数据。接收端看到FF就知道后面一定会跟着一串CF并且要为总长度预留缓冲区。FF是大消息开始的信号也是接收端分配内存的依据。新手最容易错的地方SF明明数据场有8字节却把8字节全当数据发把PCI也塞进去了对端拿Byte0一看是0x08根本不是一个合法的SF。标准里SF_DL的范围是0到7因为PCI占了一个字节这件事值得写在代码注释最显眼的位置。2.3 CF与FC数据流与节流阀CF连续帧负责携带后续数据。Byte0 0x2 序列号SNSN是4位从1开始到0xF后回绕到0循环递增直到整个消息传完。Byte1到Byte7是用户数据。接收端通过检查SN是否连续来判断有没有丢帧一旦发现跳号这包数据通常就要作废重来。FC流控帧是接收端对发送端的指令。Byte0 0x3 流控状态FSByte1 BS块大小Byte2 STmin最小帧间隔。FS0表示Continue To Send继续发FS1表示Wait先别发我还没准备好FS2表示Overflow缓冲区溢出传输直接终止。BS告诉发送端一次最多连续发多少帧CF发完等我下一次FC再继续。STmin告诉发送端相邻两个CF之间至少要隔多少时间。把FC比作红绿灯很形象BS是绿灯一次放行几辆车STmin是每辆车之间最少拉开的距离FS就是红黄绿三种信号。2.4 12位的长度陷阱为什么是4095而不是4096FF_DL一共12位最大值0xFFF就是4095。所以单条TP消息最多携带4095字节用户数据这是ISO 15765-2的标准上限。标题里说的4KB按1000进制算是4096字节比4095还多1字节严格来讲单条TP消息装不下这是协议设计里很容易被忽略的边界。实战中怎么处理如果ECU的maxNumberOfBlockLength给的是4095那刷写块就按4095切如果应用层非要一次性传4096就必须拆成两条TP消息或者把块大小定义成4095剩下1字节并入下一块。我在Bootloader项目里见过卡在这个问题上的测试应用层按4096切块TP层解析FF_DL时发现总长度超了12位接收端直接判定长度非法整个刷写流程莫名其妙中断。这个上限问题应该放在协议设计的第一页任何块切割工具都要提前做边界检测。3. 一次4KB数据的TP传输从拆帧到重组3.1 先看应用层怎么用TP以刷写为例。诊断仪发0x34 RequestDownload带上内存地址和大小ECU回0x74正响应响应里带一个maxNumberOfBlockLength告诉诊断仪后续0x36每个数据块的最大长度。诊断仪按这个长度把升级包切成块每块发一个0x36 TransferDataECU收完一整块回0x76正响应然后再发下一块。TP层的工作就是把0x36的一整块数据安全地搬过去。上层完全感知不到SF/CF这些概念。所以在协议栈设计里TP层应该是独立模块上层传下来的完整报文它负责拆收到的完整报文它负责拼。应用层代码里不应该出现帧号首帧这类术语这是分层清晰的重要标志。3.2 拆帧计算的真实账目假设要传4095字节用户数据经典CAN标准寻址FF先带走6字节剩余4089字节CF每帧最多7字节4089除以7等于584.14向上取整需要585个CF总帧数 1 585 586帧。注意这586帧里最后一帧可能只有1字节数据但PCI开销一分不少。哪怕总数据只多1字节也可能多出一整帧的代价所以切块工具必须精确计算边界。代码实现时向上取整要写成这样num_cf (remaining_len 6) / 7; // remaining_len total_len - 6整数除法向上取整这里有个移植陷阱不同编译器对负数和除法的处理可能有差异如果remaining_len是0比如总共就是6字节这个公式会因为多算出一个CF而发一帧空数据。所以调用前要加判断只在总长度大于6时才走这个分支。3.3 传输耗时估算500kbps下的数学底线经典CAN标准帧带8字节数据总线上大约111位不含位填充。500kbps下一帧理论耗时约0.22ms考虑位填充和CAN控制器处理按0.24ms/帧估算是比较稳的。586帧单程就是约140ms。实际项目里还不止发完FF要等ECU回FC这一来一回至少1ms以上如果BS16每16个CF要停下来等下一个FCECU处理完一个0x36块还要回0x76应用层响应如果STmin1ms585个CF之间会额外增加约585ms的时间。所以BS和STmin决定的不只是稳定性还直接影响刷写的总耗时。最快情况下小几百毫秒传完一块最保守时到秒级。设计刷写超时时间时务必按最保守的参数留出余量否则偶发一次ECU处理延迟就能触发发送端的N_Bs超时整块数据被判定失败。3.4 收发两端的状态机接收端的状态流转收到SF直接按长度提取数据一次完成。收到FF记录总长度分配缓冲区立刻回FCFSCTSBS和STmin由接收端的能力决定。按BS数量接收CF校验SN连续性和累计长度收满后向上层交付完整报文。处理不过来就回FCFS1WAIT发现缓冲区不够或者长度异常就回FCFS2OVERFLOW。发送端的状态流转上层调用发送接口长度小于等于7就直接发SF长度大于等于8发FF随后进入等待FC状态启动N_Bs计时。收到FCCTS按BS限制连续发CF每帧之间遵守STminBS用完继续等下一个FC。收到FCWAIT停止发送N_Bs继续计时等待对方转CTS。收到FCOVERFLOW立即终止本次发送通知上层错误。直到最后一个CF发完本次TP传输结束。常见的失败场景我在项目里见过很多次ECU回FCWAIT之后迟迟不转CTS诊断仪的N_Bs超时上层判定传输失败但诊断会话还挂着。抓包时看到FC状态一直是1且持续时间超过N_Bs问题基本出在接收端应用处理太慢或者消息队列被高优先级报文卡死。4. 地址模式与CAN IDTP数据怎么找到自己的ECU4.1 物理寻址与功能寻址物理寻址是点对点的诊断仪发请求用一个CAN ID目标ECU回响应用自己的CAN ID地址一一对应一次只问一个ECU。功能寻址是广播式的诊断仪发请求用一个功能寻址ID总线上所有支持该ID的ECU都会执行请求并各自用自己的物理响应ID回复。功能寻址典型场景是10 03进入扩展会话这类需要整条总线都执行的命令。注意功能寻址在TP层并没有什么特殊帧类型它只是CAN ID不同。底层复杂度在于一个功能请求会引发多个ECU同时回复诊断仪必须靠每个ECU的物理响应ID来区分信息来源抓包时经常看到同一个请求后面跟着一串来自不同ID的响应。4.2 典型ID规划ISO 15765-4定义了一套经典的OBD诊断地址11位标准帧功能请求ID0x7DF物理请求ID段0x7E0到0x7E7物理响应ID段0x7E8到0x7EF与物理请求ID一一对应差值固定为8。这套规划的优势是一眼能对上看到0x7E0发那0x7E8必然是它的ECU响应。开发时如果项目用的是标准11位ID直接套这套地址很省心。29位扩展ID的场景则复杂得多常见于J1939风格的DoCAN或者OEM私有网络。29位ID里通常前几个字节表示诊断标识后两个字节表示源地址和目标地址但各OEM规划五花八门。我的建议是不要背任何经验值直接查项目网络设计文档。29位ID背错一个字节报文发到别的ECU上排查起来非常痛苦。4.3 扩展寻址一个ID应对多个ECU当网络里ECU数量超过物理请求ID可用范围或者OEM不想为每个ECU单独分配CAN ID时就会用扩展寻址。核心思想CAN ID只管这是一条诊断报文或者这条给某个网关ECU真正的目标地址放在数据场第一个字节也就是TA地址字节。每个TP帧都会因为这个地址字节额外少掉1字节用户空间。扩展寻址下的容量账要重新算SFByte0是PCIByte1是TA用户数据最多6字节FFByte0和Byte1是PCIByte2是TA用户数据第一批最多5字节CFByte0是PCIByte1是TA用户数据每帧最多6字节FCByte0到Byte2是PCIByte3是TA。总长度上限还是4095但逐帧容量变小总帧数显著增加。我做过的某个网关项目里多个ECU共用0x7E0用TA0x10到0x1F区分抓包时第一眼必须先看TA不然根本不知道这是发给谁的请求或者来自谁的响应。做协议栈时一定要把正常寻址和扩展寻址的收发接口分开实现别混在一个解析函数里用flag硬切。原因很简单帧容量不同长度字段解析位置不同混在一起早晚出边界Bug。5. TP传输故障与抓包排查三个最常见的坑5.1 坑一SF长度被当成8字节很多自写诊断栈的Bug出在PCI解析上。SF的Byte0低4位是数据长度范围0到7不是8。FF的Byte0低4位是总长度的高4位不是固定0。抓包看到一帧数据开头是0x08不少人会误判成长度8的单帧但标准ISO-TP里0x08不是一个合法的SF长度值。正确用法请求22 F1 90是3字节SF的Byte0应该是0x03后面跟22 F1 90再补填充字节。解析函数一律用低4位去提取千万不要对Byte0做完整字节的比较判断。5.2 坑二BS0被当成不发帧FC里的BS表示块大小BS0是标准明确规定的无限制——发送端可以连续把所有CF发完不需要再等新的FC。不少新人把BS0理解成一次都不发于是把BS设成1性能骤降。也有人反过来以为BS设得越大越稳结果接收端缓冲区被连续冲刺的CF冲爆。正确的记忆方式BS决定的是连续帧的批大小0是无限批不是零批。如果希望发送端每发几帧就停下来等FCBS就设成那个几如果希望一鼓作气发完BS就是0。5.3 坑三STmin设置过小导致丢帧STmin控制连续CF之间的最小间隔。0x00表示尽量快无最小间隔要求理论上接收端应该能扛住。但实际ECU的CAN接收中断经常被高优先级报文或Flash操作打断两帧连续挤进接收FIFO很容易溢出。收到FCBS0STmin0x00时诊断仪会把几百个CF一口气发完。ECU处理不过来抓包就会看到CF序号跳变上一帧SN5下一帧直接SN7。遇到这种优先把STmin调到1ms或者把BS降到16/32重新实测间隔。不要用CAN分析仪的连续发送功能顶着0间隔发包那不是真实总线场景跑出来的数据没有参考价值。5.4 抓包示例一条17字节的0x22响应假设诊断仪发22 F1 90请求读VINECU响应17字节完整抓包如下# 方向 CAN ID D0 D1 D2 D3 D4 D5 D6 D7 说明 1 诊断仪 0x7E0 03 22 F1 90 xx xx xx xx 请求SF长度3 2 ECU 0x7E8 11 11 62 F1 90 31 32 33 FF总长0x01117数据62 F1 90 3字节VIN 3 诊断仪 0x7E0 30 00 00 xx xx xx xx xx 诊断仪回FCBS0STmin0 4 ECU 0x7E8 21 34 35 36 37 38 39 30 CFSN17字节数据 5 ECU 0x7E8 22 31 32 33 34 35 xx xx CFSN2剩余5字节数据第2行的FF第0字节0x11高4位是1低4位是总长度的高4位1第1字节0x11是总长度的低8位17。两字节拼一起就是0x01117。第3行是诊断仪对ECU发送方做的流控因为此时发送方是ECU接收方是诊断仪流控方向是反过来的。遇到传输卡死就按这个链路查先找FF确认总长再看FC的BS/STmin然后数CF数量核对SN连续性最后用时间戳验证帧间隔是否符合STmin。只要有一个环节对不上问题就在那里。6. 工程调优BS、STmin与CAN FD时代的传输策略6.1 参数组合速查表场景BSSTmin效果点对点刷写总线空闲00x00或微秒档最快4095字节约140ms总线繁忙怕阻塞通信160x011ms每16帧停顿给其他报文让路接收ECU处理慢80x055ms最保守几乎不丢帧调试阶段先跑通320x022ms折中便于观察时序STmin的微秒档位在标准里用0xF1到0xF9表示常见定义是100us到900us逐档递增其中0xF1是100us0xF2是200us以此类推。也有OEM协议文档采用125us起步的编码具体以标准原文和项目规范为准。我自己的经验是除非硬件验证过收端能力否则刷写场景别一上来就上0x00先用0x011ms跑通再往下压。6.2 实际项目里的建议针对Bootloader刷写我推荐一组经过多个项目验证的起步配置BS0STmin用微秒档0xF1到0xF3之间或者直接0x01。原因很明显BS0免去频繁等FC的时间STmin控制住基本节奏整体性能好。如果发现刷写偶尔失败优先把STmin从0x00调到0x01。别看只差1msECU端Flash写入期间会禁止中断0间隔的连续帧在那一刻进不了FIFO丢帧率立刻上升。反过来如果ECU的接收硬件有双缓冲或DMA0间隔也能稳定运行那性能收益会很大586帧省掉几百毫秒间隔时间刷写几个MB的包能快出一大截。还有一点所有时间参数N_Bs、N_Cr、N_As这些不要死记标准默认值。OEM的规范经常改N_Bs可能不是1000ms而是1500ms。开发前把超时参数做成可配置项和OEM联调时双方对一下规范不然会出现一边等到1500ms另一边1000ms就把传输掐断的尴尬局面。6.3 CAN FD来了TP还是老一套CAN FD把数据场从8字节扩到64字节ISO 15765-2这套TP机制照样在用但每帧的用户容量变了FF最多能带62字节用户数据CF最多能带63字节用户数据。同样传4095字节经典CAN需要586帧CAN FD只需要大约66帧1个FF加65个CF速度快接近一个数量级总线占用时间大幅缩短。刷写一个几百KB的升级包从分钟级降到秒级这就是新车型普遍上CAN FD最直接的理由。但底层机制完全没变SF/FF/CF/FC、SN回绕、BS/STmin、超时参数一个都不少。做协议栈时把帧数据域大小抽成一个配置参数同时支持8字节和64字节比写死一份经典CAN逻辑划算得多。最后留个经验我调试TP层最怕的不是协议理解不了而是参数配得太激进。诊断仪的发送接口一个个全速往总线砸帧效率看起来很高但ECU端只要一个中断被Flash操作卡住前面的努力全白费N_Cr一堆超时最后只能把STmin调大降速。所以我的习惯是第一次联调永远从保守参数起步BS16、STmin1ms跑通了再逐步往下压每改一档抓一包用时间戳说话而不是凭感觉。协议是死的总线是活的8字节的CAN想传4KB诊断数据靠的是一套好规则再加上对现场时序的敬畏。