3、UDS网络层(ISO 15765-2):单帧与多帧传输、流控制机制、网络层时间参数、分段与重组

各位好,我是老周。今天我们来聊聊UDS协议栈里最容易被忽视、却又最容易出问题的一层——网络层。

很多人一上来就扎进诊断服务(比如读故障码、写数据),结果在CAN总线上死活收不到响应。查了半天,发现是网络层的单帧多帧没处理好。嗯,我当年也踩过这个坑。

ISO 15765-2(也叫ISO-TP)定义了UDS在CAN总线上的传输规则。说白了,它解决一个核心问题:CAN报文只有8字节,但诊断数据可能几百字节,怎么传?

核心要点:ISO-TP网络层负责将上层诊断服务的数据(最多4095字节)分段封装到CAN帧中,并在接收端重组。它定义了四种帧类型:单帧(SF)、首帧(FF)、连续帧(CF)和流控制帧(FC)。

3.1 单帧传输:小数据的直通车

当诊断数据长度 ≤ 7字节(普通CAN)或 ≤ 6字节(扩展CAN)时,直接用单帧搞定。

单帧的结构很简单:第一个字节的高4位是帧类型标识(0x0),低4位是数据长度。剩下7个字节放实际数据。

// 单帧格式示例(普通CAN,11位ID)
// 字节0: [0x0 | 数据长度]  例如 0x07 表示7字节数据
// 字节1-7: 实际数据

// 发送 "10 03"(诊断会话控制请求)
// 单帧:0x02 0x10 0x03
// 解释:0x02 = 0x0 | 2(2字节数据),后面跟诊断请求

我在项目中遇到过一个问题:有个同事把单帧的数据长度写成了0x00,结果ECU直接忽略了这个请求。记住,长度字段是实际数据字节数,不是CAN报文数据域长度。

3.2 多帧传输:大块数据的搬运工

当数据超过7字节,就得用多帧了。多帧传输分三个阶段:

  1. 发送首帧(FF):告诉接收方“我要发一大包数据,总共X字节”
  2. 接收方回复流控制帧(FC):告诉发送方“准备好了,你可以发了”或“等等,我忙不过来”
  3. 发送方发送连续帧(CF):按流控制帧的要求,一帧一帧地把数据发完

首帧的格式有点特殊:第一个字节高4位是0x1,低4位和第二个字节组成12位的数据总长度(最大4095字节)。

// 首帧示例:发送256字节数据
// 字节0: 0x10 | (0x100 >> 8) = 0x11
// 字节1: 0x00
// 字节2-7: 前6字节数据

// 连续帧示例(第1帧)
// 字节0: 0x20 | 0x01 = 0x21  (SN=1)
// 字节1-7: 后续7字节数据

个人经验:连续帧的序列号(SN)从1开始,到0xF后回绕到0。我见过一个bug:发送方在SN=0xF后直接跳到0x10,结果接收方直接丢弃了后续所有帧。记住,SN只有4位,0xF之后是0x00。

3.3 流控制机制:谁也别把谁撑死

流控制帧(FC)是接收方控制发送节奏的关键。它包含三个参数:

参数 字节位置 说明
流状态(FS) 字节0高4位 0x0=继续发送,0x1=等待,0x2=溢出/中止
块大小(BS) 字节1 允许连续发送的CF帧数,0表示不限
最小间隔时间(STmin) 字节2 相邻CF帧之间的最小间隔(单位ms或us)

你想想看,如果接收方处理速度慢,发送方一股脑把数据全发过来,缓冲区就溢出了。流控制就是干这个的。

我曾经调试过一个项目:ECU回复FC(FS=0x0, BS=5, STmin=10),意思是“一次最多发5帧,每帧间隔至少10ms”。结果发送方没管STmin,连续发完5帧后立刻发第6帧,ECU直接丢帧了。

避坑指南:STmin的值编码方式有坑。0x00-0x7F表示0-127ms,0xF1-0xF9表示100us-900us,0xFA-0xFE保留。我曾经把STmin设为0xF1(100us),结果某个老款ECU根本不认,直接回复溢出。后来改成1ms才稳定。

3.4 网络层时间参数:超时是万恶之源

网络层定义了四个关键时间参数,任何一个超时都会导致传输失败:

参数 默认值 说明
N_As 发送超时 发送CAN帧到总线的时间上限
N_Ar 接收超时 等待接收CAN帧的时间上限
N_Bs 等待FC超时 发送FF后等待FC的时间上限
N_Cr 等待CF超时 发送FC后等待CF的时间上限

这些时间参数在ISO 15765-2里都有推荐值,但实际项目中要根据总线负载调整。我见过一个案例:某Tier1把N_Bs设成100ms,结果在总线负载80%时,FC帧被延迟了120ms才发出,发送方直接超时重传,导致诊断通信完全瘫痪。

我的建议:量产项目中,N_Bs和N_Cr至少设成500ms以上。别问我怎么知道的——问就是血泪教训。

3.5 分段与重组:拆开容易,拼回去难

分段就是把大块数据切成小块,装进连续帧里。重组就是接收方把这些小块按顺序拼回去。

分段的规则很简单:

  • 首帧装前6(或7)字节
  • 连续帧每帧装7(或8)字节
  • 最后一帧可以不满

重组的难点在于:怎么保证顺序不乱?怎么检测丢帧?

连续帧的序列号(SN)就是干这个的。接收方每收到一个CF,检查SN是否连续。如果发现跳号,说明丢帧了,直接丢弃整个多帧传输。

// 重组逻辑伪代码
uint8_t expected_sn = 1;
while (收到CF帧) {
    uint8_t sn = (cf_frame[0] & 0x0F);
    if (sn != expected_sn) {
        // 丢帧了!丢弃整个消息
        abort_transmission();
        break;
    }
    append_data(cf_frame[1..7]);
    expected_sn = (expected_sn + 1) & 0x0F;
}

这里有个细节:首帧不参与SN计数。首帧之后,第一个CF的SN必须是1。我见过一个实现把首帧也算成SN=0,结果和标准不兼容。

小技巧:调试多帧传输时,用CAN工具抓个log,重点看FC帧的BS和STmin,以及CF帧的SN是否连续。80%的多帧问题都能从这里找到线索。

3.6 网络层状态机:一张图看懂全过程

说了这么多,不如一张状态机图来得直观。下面是我手绘的ISO-TP发送方状态机:

ISO-TP 发送方状态机 空闲 (IDLE) 等待FC (WAIT_FC) 发送CF (SEND_CF) 传输完成 (DONE) 发送首帧 (FF) 收到FC (FS=0) 还有数据 数据发完 N_Bs超时 N_Cr超时/丢帧 自动回到空闲 图例 正常流程 异常/超时 状态节点

这张图我画了好几个版本才定稿。你注意看,从“等待FC”到“空闲”有两条虚线,分别对应N_Bs超时和N_Cr超时。实际项目中,这两个超时处理最容易出问题。

3.7 实际项目中的常见问题

最后,我总结几个实际项目中踩过的坑:

  • BS=0的处理:BS=0表示“不限帧数”,但有些ECU实现有bug,设成0反而出问题。我一般设成20-50,既保证效率,又避免溢出。
  • STmin的精度:有些MCU的定时器精度不够,STmin设成1ms实际可能变成2ms。调试时用示波器量一下CF帧之间的间隔。
  • 多帧传输的中断:如果在多帧传输过程中插入了其他诊断请求,有些实现会直接丢弃当前传输。建议用队列管理,别让新请求打断正在进行的多帧传输。
  • CAN ID的过滤:网络层不关心CAN ID,但接收方要根据CAN ID判断是不是诊断报文。我见过一个案例:发送方和接收方的CAN ID配置反了,结果FC帧永远到不了发送方。

重要提醒:ISO 15765-2的协议数据单元(PDU)格式在不同CAN标准(普通CAN vs CAN FD)下有所不同。CAN FD的单帧可以装64字节数据,但流控制机制基本一致。如果你的项目用CAN FD,记得检查数据长度编码。