4. LIN帧结构与通信机制
好,咱们开始啃硬骨头了。诊断服务是LIN节点具备“可维护性”的灵魂。今天先聊三个最基础、也最常用的服务:0x10 诊断会话控制、0x27 安全访问、0x28 通信控制。
这三个服务,说白了就是一把钥匙、一扇门、一个开关。你想想看,车上那么多节点,总不能谁都能随便改数据吧?所以LIN总线上的诊断,第一步就是建立会话,第二步是解锁权限,第三步是控制通信行为。顺序不能乱。
4.1 诊断会话控制(0x10)—— 先敲门,再说话
LIN节点上电后,默认处于默认会话(Default Session)。在这个状态下,节点只响应最基本的诊断请求(比如 0x3E 保持激活)。你想做读写数据、刷写固件?对不起,门没开。
0x10 服务的作用,就是让诊断仪告诉节点:“我要切换会话模式了”。常见的会话模式有三种:
| 子功能(sub-function) | 会话名称 | 典型用途 |
|---|---|---|
| 0x01 | 默认会话(Default Session) | 上电后的初始状态,功能受限 |
| 0x02 | 编程会话(Programming Session) | 刷写固件、修改非易失性数据 |
| 0x03 | 扩展会话(Extended Session) | 读写参数、执行例程、解锁安全访问 |
我个人习惯,在项目初期就把这三个会话的切换逻辑画成状态机。为什么呢?因为LIN节点不像CAN节点那么“强壮”,它的资源有限,状态管理一旦混乱,很容易出现“会话超时”或者“权限不足”的故障码。
代码实现上,0x10 的响应很简单:如果支持请求的会话类型,就回复肯定响应(0x50 + sub-function + 附加的会话参数);如果不支持,就回复否定响应(0x7F + 0x10 + NRC)。
// 伪代码:0x10 会话控制处理
uint8_t Diag_Handle_10(uint8_t subFunction) {
switch (subFunction) {
case 0x01: // 默认会话
gActiveSession = SESSION_DEFAULT;
gSessionTimer = 0;
return 0x50; // 肯定响应
case 0x02: // 编程会话
if (IsProgrammingAllowed()) {
gActiveSession = SESSION_PROGRAMMING;
gSessionTimer = 0;
return 0x50;
} else {
return 0x7F; // 否定响应,NRC = 0x22(条件不满足)
}
case 0x03: // 扩展会话
gActiveSession = SESSION_EXTENDED;
gSessionTimer = 0;
return 0x50;
default:
return 0x7F; // NRC = 0x12(子功能不支持)
}
}
4.2 安全访问(0x27)—— 解锁权限,防止误操作
会话切换完了,你进入了扩展会话。但别高兴太早,很多关键操作(比如写 EEPROM、执行 Bootloader 跳转)还需要一把“钥匙”。这把钥匙就是 0x27 安全访问服务。
0x27 的流程是固定的:
- 请求种子(Request Seed): 诊断仪发送 0x27 + 0x01(请求种子),节点返回一个随机种子(比如 4 字节)。
- 发送密钥(Send Key): 诊断仪根据种子和约定的算法计算出密钥,发送 0x27 + 0x02 + 密钥。节点验证通过后,解锁对应权限。
这里有个细节:LIN 节点的安全等级通常比 CAN 节点低一些。我见过很多项目直接用简单的异或算法,甚至固定密钥。嗯,这确实不安全,但你要知道,LIN 节点本身算力有限,复杂的加密算法跑不动。所以,平衡安全性和实时性是关键。
我分享一个我踩过的坑:有一次,节点返回种子后,诊断仪计算密钥时把字节序搞反了(大端 vs 小端)。结果节点一直回复 NRC 0x35(无效密钥)。排查了两天才发现,是种子字节序不一致。从那以后,我在设计文档里都会专门画一张“种子/密钥字节序图”。
// 伪代码:0x27 安全访问处理(简化版)
uint8_t gSecuritySeed[4];
uint8_t gSecurityLevel = 0;
uint8_t Diag_Handle_27(uint8_t subFunction, uint8_t* data, uint8_t len) {
if (subFunction == 0x01) { // 请求种子
GenerateRandomSeed(gSecuritySeed, 4);
gSecurityLevel = 1; // 标记已请求种子
return 0x67; // 肯定响应,后面跟种子数据
}
else if (subFunction == 0x02) { // 发送密钥
if (gSecurityLevel != 1) {
return 0x7F; // NRC = 0x24(请求顺序错误)
}
uint8_t expectedKey[4];
CalculateKey(gSecuritySeed, expectedKey);
if (memcmp(data, expectedKey, 4) == 0) {
gSecurityLevel = 2; // 解锁成功
return 0x67;
} else {
gSecurityLevel = 0;
return 0x7F; // NRC = 0x35(无效密钥)
}
}
else {
return 0x7F; // NRC = 0x12
}
}
4.3 通信控制(0x28)—— 让节点“闭嘴”或“说话”
0x28 通信控制服务,说白了就是让诊断仪可以临时控制 LIN 节点的报文收发行为。这个服务在刷写场景中特别有用——你想让节点停止发送应用报文,专心接收刷写数据。
0x28 的子功能主要有:
| 子功能 | 含义 | 典型应用 |
|---|---|---|
| 0x00 | 使能所有报文 | 恢复正常通信 |
| 0x01 | 禁止所有报文 | 刷写前让节点静默 |
| 0x02 | 使能指定报文 | 只允许特定 ID 的报文通过 |
| 0x03 | 禁止指定报文 | 屏蔽某个干扰报文 |
你可能会问:“LIN 节点本来报文就不多,有必要这么精细控制吗?” 有。我记得在一个项目中,LIN 节点同时负责车窗控制和门锁控制。刷写固件时,如果不禁止车窗报文,电机可能会误动作,夹到手。所以,0x28 不仅是技术需求,更是安全需求。
// 伪代码:0x28 通信控制处理
uint8_t gLinMsgEnableMask[64]; // 每个 bit 对应一个 LIN ID
uint8_t Diag_Handle_28(uint8_t subFunction, uint8_t controlType, uint8_t linId) {
switch (subFunction) {
case 0x00: // 使能所有
memset(gLinMsgEnableMask, 0xFF, sizeof(gLinMsgEnableMask));
return 0x68;
case 0x01: // 禁止所有
memset(gLinMsgEnableMask, 0x00, sizeof(gLinMsgEnableMask));
return 0x68;
case 0x02: // 使能指定
if (linId < 64) {
gLinMsgEnableMask[linId / 8] |= (1 << (linId % 8));
return 0x68;
}
return 0x7F; // NRC = 0x31(请求超出范围)
case 0x03: // 禁止指定
if (linId < 64) {
gLinMsgEnableMask[linId / 8] &= ~(1 << (linId % 8));
return 0x68;
}
return 0x7F;
default:
return 0x7F; // NRC = 0x12
}
}
4.4 三个服务的协作关系
这三个服务不是孤立的。我画了一张协作流程图,帮你理清它们的关系:
从流程图可以看出,这三个服务是串行依赖的。你不能跳过会话控制直接做安全访问,也不能在没解锁的情况下控制通信。这个顺序,是 ISO 14229-1 规定的,也是实际项目中的铁律。