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字节,就得用多帧了。多帧传输分三个阶段:
- 发送首帧(FF):告诉接收方“我要发一大包数据,总共X字节”
- 接收方回复流控制帧(FC):告诉发送方“准备好了,你可以发了”或“等等,我忙不过来”
- 发送方发送连续帧(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发送方状态机:
这张图我画了好几个版本才定稿。你注意看,从“等待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,记得检查数据长度编码。