第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。
4.2 多帧传输——分拆的艺术
数据超过7个字节怎么办?分帧发送。
多帧传输分三个阶段:
- 首帧(FF)——告诉接收方:我要发大包了,准备接好
- 流控制帧(FC)——接收方回应:我准备好了,你发吧/慢点发/等等
- 连续帧(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 完整的多帧传输流程
咱们走一遍完整流程,加深理解:
- 发送方发首帧(FF),告诉接收方:我要发20字节数据
- 接收方收到FF,分配缓冲区,发FC帧:CTS,BS=5,STmin=10ms
- 发送方收到FC,开始发连续帧。每次最多发5帧(BS=5),然后等下一个FC
- 发送方发CF #1~#5,每帧间隔至少10ms(STmin=10ms)
- 接收方收到5帧后,再发FC:CTS,BS=5,STmin=10ms
- 发送方发CF #6~#10
- ...重复直到发完
- 接收方收齐所有数据,重组,交给上层处理
如果中间接收方处理不过来,可以发WAIT帧。比如:
- 发送方发CF #3后,接收方忙不过来,发WAIT帧
- 发送方停止发送,等待
- 接收方处理完,发CTS帧
- 发送方继续发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、例程控制等。各位有什么问题,欢迎在评论区留言。