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

好,咱们今天聊聊网络层。ISO 15765-2,也就是大家常说的CAN TP层。

说实话,很多刚入行的工程师觉得UDS应用层才是核心,网络层嘛,不就是个打包拆包吗?

嗯,我当年也这么想。直到有一次在实车上,一个多帧报文死活收不全,ECU直接进入BusOff状态。排查了三天,最后发现是流控制帧的STmin参数设得太激进,ECU处理不过来。

从那以后,我再也不敢小看网络层了。

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

先说说最简单的场景。你要发送的数据很少,比如就发一个"读取VIN码"的请求,总共才3个字节。

这种情况下,一个CAN帧就搞定了。这就是单帧(Single Frame,SF)。

单帧格式:

  • PCI字节的高4位:固定为0,表示这是单帧
  • PCI字节的低4位:数据长度(0-7)
  • 后续字节:实际数据

举个例子,发送一个"0x22 0xF1 0x90"的请求(按地址读取数据):

CAN ID: 0x7E0
数据: 03 22 F1 90
      ^^
      ||__ 数据长度 = 3字节
      |____ 单帧标识 = 0

这里有个细节我提醒一下。单帧的数据长度最大是7字节,因为CAN帧数据场总共8字节,PCI占1字节,剩下7字节给数据。

那如果数据超过7字节怎么办?别急,这就是多帧传输的用武之地了。

3.2 多帧传输:大块头的拆分艺术

当你要发送的数据超过7字节,比如一个完整的诊断响应可能有几十甚至上百字节,这时候就需要把数据拆成多个CAN帧来发送。

多帧传输涉及三种帧类型:

  • 首帧(First Frame,FF):告诉接收方"我要发大包了,准备接好"
  • 流控制帧(Flow Control,FC):接收方说"我准备好了,你发吧"或者"慢点,我处理不过来"
  • 连续帧(Consecutive Frame,CF):实际的数据帧,一帧一帧发过去

我个人习惯:在调试多帧传输时,我会先用CANoe抓一下总线上的报文流。首帧、流控制、连续帧,这三个阶段的时序必须严格对齐。我曾经遇到过一个项目,ECU的流控制帧总是晚发200ms,导致发送方超时重传,整个诊断链路卡死。

3.3 流控制机制:谁说了算?

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

参数 含义 典型值
FS(Flow Status) 流状态:0=继续发送,1=等待,2=溢出/中止 0x00
BS(Block Size) 块大小:连续发送多少帧后需要等待下一个FC 0x00(无限制)或0x01-0xFF
STmin(Separation Time minimum) 最小间隔时间:连续帧之间的最小时间间隔 0x00-0xF9(毫秒级)或0xF1-0xF9(微秒级)

说白了,这就是一个"你发我收"的握手协议。

接收方说:"你一次发5帧,然后等我确认,帧与帧之间至少隔10ms。"

发送方就得乖乖照做。

避坑指南:我曾经在标定一个ECU时,把STmin设成了0x00(表示无间隔)。结果ECU的CAN控制器根本跟不上,连续丢帧。后来查手册才发现,0x00表示"由发送方决定最小间隔",但很多ECU的实现并不标准。我建议STmin至少设0x10(1ms),稳妥一点。

3.4 分段与重组:拆得开,还得装得上

发送方把大包拆成小帧,接收方收到后要能原样拼回去。这个过程叫分段与重组。

发送方(分段):

  1. 首帧:发送总数据长度(12位)和部分数据
  2. 连续帧:每个帧带一个序列号(4位,从1开始,到15后回绕到0)
  3. 按流控制帧的要求控制发送节奏

接收方(重组):

  1. 收到首帧,分配缓冲区,记录总长度
  2. 按序列号依次接收连续帧
  3. 检查序列号是否连续,有没有丢帧
  4. 收齐所有数据后,拼成完整的诊断报文

这里有个容易踩的坑:序列号回绕。

连续帧的序列号只有4位,范围0-15。如果你要发20个连续帧,序列号会从1走到15,然后回到0,再走到4。

接收方必须能正确处理这种回绕。我见过一个ECU,序列号回绕后直接丢弃后续帧,导致多帧传输永远收不完。

3.5 网络层超时处理:别让总线等太久

网络层定义了多个超时时间,确保通信不会卡死。这些超时时间在ISO 15765-2中有明确规定:

超时名称 缩写 典型值 说明
N_As 发送超时 1000ms 发送方等待帧被成功发送的最大时间
N_Bs 接收准备超时 1000ms 发送方等待流控制帧的最大时间
N_Cr 连续帧接收超时 1000ms 接收方等待下一个连续帧的最大时间
N_Ar 接收响应超时 50ms 接收方发送流控制帧的响应时间

为什么要有这些超时?你想想看,如果发送方发了首帧,接收方一直不给流控制帧,发送方总不能无限等下去吧?

N_Bs就是干这个的。超时了,发送方就认为通信失败,该报错报错,该重试重试。

实际项目中的经验:

我在做TBOX项目时,遇到过N_Ar超时的问题。ECU收到首帧后,需要先处理内部任务,再发流控制帧。但ECU的任务调度优先级没设好,导致流控制帧总是晚于50ms发出。

解决方案有两个:

  • 调整ECU的任务优先级,把CAN接收中断的优先级提高
  • 或者把N_Ar放宽到100ms(但需要和客户确认,因为标准值就是50ms)

最后我们选了第一种方案,毕竟标准不能随便改。

3.6 代码实现:一个简单的多帧发送函数

说了这么多理论,咱们看看代码怎么实现。下面是一个简化的多帧发送函数,用Python写的:

def send_multi_frame(can_bus, can_id, data):
    """
    发送多帧诊断报文
    :param can_bus: CAN总线对象
    :param can_id: 发送ID
    :param data: 要发送的完整数据(bytes)
    """
    total_len = len(data)
    
    if total_len <= 7:
        # 单帧发送
        pci = (0 << 4) | total_len  # 单帧标识 + 长度
        frame_data = bytes([pci]) + data
        can_bus.send(can_id, frame_data)
        return
    
    # 多帧发送
    # 1. 发送首帧
    ff_pci = (1 << 4) | ((total_len >> 8) & 0x0F)  # 首帧标识 + 长度高4位
    ff_data = bytes([ff_pci, total_len & 0xFF]) + data[:6]
    can_bus.send(can_id, ff_data)
    
    # 2. 等待流控制帧
    fc_frame = wait_for_fc(can_bus, timeout=1000)
    if fc_frame is None:
        raise TimeoutError("等待流控制帧超时")
    
    bs = fc_frame[2]   # 块大小
    stmin = fc_frame[3]  # 最小间隔时间
    
    # 3. 发送连续帧
    seq_num = 1
    offset = 6
    frame_count = 0
    
    while offset < total_len:
        # 计算本帧数据长度
        chunk_size = min(7, total_len - offset)
        
        # 构造连续帧
        cf_pci = (2 << 4) | (seq_num & 0x0F)
        cf_data = bytes([cf_pci]) + data[offset:offset+chunk_size]
        can_bus.send(can_id, cf_data)
        
        offset += chunk_size
        seq_num = (seq_num + 1) & 0x0F
        frame_count += 1
        
        # 检查块大小限制
        if bs != 0 and frame_count >= bs:
            # 等待下一个流控制帧
            fc_frame = wait_for_fc(can_bus, timeout=1000)
            if fc_frame is None:
                raise TimeoutError("等待流控制帧超时")
            frame_count = 0
        
        # 等待STmin时间
        time.sleep(stmin / 1000.0)

这段代码我简化了很多,比如没有处理流控制帧的FS=1(等待)和FS=2(溢出)的情况。实际项目中,这些边界情况必须全部覆盖。

一个小技巧:调试多帧传输时,我习惯在代码里加一些日志打印,把每个帧的PCI、序列号、数据长度都打出来。这样一旦出问题,看日志就能定位到是哪一帧丢了或者顺序错了。

3.7 总结

网络层说白了就是解决一个问题:如何在CAN这种小数据包的总线上传输大块数据

单帧搞定小数据,多帧搞定大数据。流控制帧负责协调节奏,超时机制防止死锁。

嗯,内容就这些。下一章咱们聊聊应用层的具体服务,比如诊断会话控制、安全访问这些实战内容。