第4章:UDS网络层(ISO 15765-2)——单帧与多帧传输、流控制机制

各位同学,咱们今天聊聊网络层。说实话,很多做UDS诊断的工程师,应用层协议背得滚瓜烂熟,但一遇到网络层就懵了。尤其是多帧传输那点事,我当年刚入行时也栽过跟头。

ISO 15765-2,说白了就是解决一个核心问题:诊断数据包太大,CAN报文装不下怎么办?CAN一帧最多8字节数据,而UDS诊断动辄几十上百字节。嗯,这时候就需要网络层来分拆和重组了。

4.1 单帧传输(SF)

先讲最简单的。单帧,就是一条CAN报文能装下的数据。

单帧的格式长这样:

| 字节0 | 字节1~N |
| PCI   | 数据     |

其中PCI(协议控制信息)的第一个半字节固定为0,表示这是单帧。剩下的7位表示数据长度。

举个例子:

0x03 0x22 0xF1 0x90

0x03表示:单帧,后面跟着3个字节数据。0x22 0xF1 0x90就是真正的诊断请求——读取DID F190。

我的小经验:单帧的数据长度最大是多少?7个字节。因为PCI占1个字节,CAN数据场总共8字节,剩下7字节给数据。但注意,如果使用CAN FD,这个限制会放宽。

4.2 多帧传输——分拆的艺术

数据超过7个字节怎么办?分帧发送。

多帧传输分三个阶段:

  1. 首帧(FF)——告诉接收方:我要发大包了,准备接好
  2. 流控制帧(FC)——接收方回应:我准备好了,你发吧/慢点发/等等
  3. 连续帧(CF)——发送方把剩下的数据一帧一帧发过去

首帧的PCI格式:第一个半字节是1,表示首帧。剩下的11位表示总数据长度。

| 字节0       | 字节1~N |
| 0x1 + 长度高3位 | 长度低8位 + 数据 |

举个例子,要发送20字节数据:

首帧:0x14 0x10 [数据1~6]
// 0x14 = 0x1 | (20 >> 8) = 0x1 | 0x0 = 0x10? 不对,我算一下
// 20 = 0x14,高3位是0,低8位是0x14
// 所以首帧PCI = 0x10 0x14? 不对,标准写法是:
// 字节0 = 0x10 | (len >> 8) = 0x10 | 0 = 0x10
// 字节1 = len & 0xFF = 0x14
// 所以首帧:0x10 0x14 [数据1~6]

关键点:首帧只带6个字节数据。因为PCI占了2个字节,CAN数据场8字节,剩下6字节给数据。

4.3 流控制机制(FC)——谁说了算?

发送方发完首帧,不能直接发连续帧。得等接收方说「我准备好了」。

流控制帧的PCI格式:

| 字节0       | 字节1    | 字节2    |
| 0x3 + 流状态 | 块大小(BS) | 最小间隔(STmin) |

流状态有三种:

流状态 含义
CTS(继续发送) 0 来吧,我准备好了
WAIT(等待) 1 等等,我还没准备好
OVFLW(溢出) 2 我缓冲区不够,别发了

块大小(BS):告诉发送方,你一次最多发几帧。0表示不限量,随便发。

最小间隔(STmin):两帧之间最少隔多少毫秒。0x00表示不限制,0xF1~0xF9表示100μs~900μs,0x01~0x7F表示1ms~127ms。

我曾经踩过的坑:有一次调试,ECU总是丢帧。查了半天,发现是STmin设得太小。发送方发得太快,接收方来不及处理。后来我把STmin设为0x0A(10ms),问题就解决了。所以,STmin不是随便设的,得看接收方的处理能力。

4.4 连续帧(CF)——一鼓作气

收到FC帧且状态为CTS后,发送方开始发连续帧。

连续帧的PCI格式:

| 字节0       | 字节1~7 |
| 0x2 + 序列号 | 数据     |

序列号从1开始,每帧加1,到15后回0(模16计数)。

举个例子:

CF #1: 0x21 [数据7~13]
CF #2: 0x22 [数据14~20]
CF #3: 0x23 [数据21~27]
...
CF #15: 0x2F [数据...]
CF #16: 0x20 [数据...]  // 序列号回0

我个人的习惯:在实现接收端时,我会用一个变量记录期望的序列号。每次收到CF帧,先检查序列号对不对。如果不对,说明丢帧了,直接丢弃整个多帧消息。别想着补帧,太复杂,容易出bug。

4.5 等待帧(WF)——别急,让我缓缓

有时候接收方处理不过来,会发WAIT帧。格式和FC帧一样,只是流状态设为1。

发送方收到WAIT帧后,不能发连续帧,得继续等。接收方什么时候准备好了,再发一个CTS帧。

但注意,WAIT帧不能无限发。网络层有定时器管着。

4.6 网络层定时参数——时间就是生命

ISO 15765-2定义了几个关键定时器。我当年背这些参数背得头疼,后来发现,记住几个关键的就行。

定时器 描述 典型值 超时后果
N_As 发送方发送帧的时间 0~1000ms 发送失败
N_Bs 发送方等待FC帧的时间 0~1000ms 发送方超时,放弃发送
N_Cs 发送方发送连续帧的时间 0~1000ms 发送失败
N_Br 接收方发送FC帧的时间 0~1000ms 接收方超时,放弃接收
N_Cr 接收方等待连续帧的时间 0~1000ms 接收方超时,放弃接收

你想想看,这些定时器为什么重要?

举个例子:发送方发了首帧,然后等FC帧。如果N_Bs设了500ms,接收方500ms内没回FC,发送方就认为通信失败,直接放弃。这时候如果接收方后来才回FC,发送方已经不认了。

实际项目中的经验:我建议把N_Bs和N_Cr设得稍微大一点,比如1000ms。有些ECU处理慢,500ms不够用。但也不能太大,否则诊断仪等得不耐烦,用户体验差。

4.7 完整的多帧传输流程

咱们走一遍完整流程,加深理解:

  1. 发送方发首帧(FF),告诉接收方:我要发20字节数据
  2. 接收方收到FF,分配缓冲区,发FC帧:CTS,BS=5,STmin=10ms
  3. 发送方收到FC,开始发连续帧。每次最多发5帧(BS=5),然后等下一个FC
  4. 发送方发CF #1~#5,每帧间隔至少10ms(STmin=10ms)
  5. 接收方收到5帧后,再发FC:CTS,BS=5,STmin=10ms
  6. 发送方发CF #6~#10
  7. ...重复直到发完
  8. 接收方收齐所有数据,重组,交给上层处理

如果中间接收方处理不过来,可以发WAIT帧。比如:

  1. 发送方发CF #3后,接收方忙不过来,发WAIT帧
  2. 发送方停止发送,等待
  3. 接收方处理完,发CTS帧
  4. 发送方继续发CF #4

注意:WAIT帧不能滥用。我曾经见过一个ECU,每次收到首帧都先发WAIT,等100ms再发CTS。虽然能工作,但诊断速度慢得让人抓狂。后来发现是代码里写了个固定的延时,去掉后速度快了3倍。

4.8 代码实现要点

最后,给各位分享一些实现上的要点:

  • 状态机是核心:发送方和接收方各自维护一个状态机。发送方有IDLE、WAIT_FC、SEND_CF、WAIT_FC_AFTER_BS等状态。接收方有IDLE、WAIT_CF、RECEIVING等状态。
  • 缓冲区管理:多帧传输需要缓冲区。我建议用环形缓冲区,大小至少能装下最大诊断消息(通常4096字节)。
  • 定时器实现:每个多帧会话都需要一个定时器。用硬件定时器或者操作系统定时器都行,关键是精度要够(至少1ms)。
  • 错误处理:超时、序列号错误、溢出等,都要有对应的处理逻辑。我一般会记录错误码,方便调试。

好了,网络层的内容就讲到这里。下一章咱们聊聊应用层——UDS服务的基础。到时候我会讲一些实际项目中常用的服务,比如读取DID、写入DID、例程控制等。各位有什么问题,欢迎在评论区留言。