4、LIN诊断服务实现(上):诊断会话控制(0x10)、安全访问(0x27)、通信控制(0x28)

好,咱们开始啃硬骨头了。诊断服务是LIN节点具备“可维护性”的灵魂。今天先聊三个最基础、也最常用的服务:0x10 诊断会话控制、0x27 安全访问、0x28 通信控制。

这三个服务,说白了就是一把钥匙、一扇门、一个开关。你想想看,车上那么多节点,总不能谁都能随便改数据吧?所以LIN总线上的诊断,第一步就是建立会话,第二步是解锁权限,第三步是控制通信行为。顺序不能乱。

核心逻辑: 会话控制决定“能不能聊”,安全访问决定“能不能动”,通信控制决定“怎么动”。
LIN 诊断服务核心流程(上) 0x10 会话控制 建立通信上下文 0x27 安全访问 解锁种子/密钥 0x28 通信控制 使能/禁止报文 说明: 1. 必须先进入扩展会话或编程会话,才能执行 0x27 和 0x28。 2. 0x27 安全访问通常需要“请求种子 → 发送密钥”两步握手。 3. 0x28 通信控制用于临时关闭或打开特定报文,常用于刷写场景。

4.1 诊断会话控制(0x10)—— 先敲门,再说话

LIN节点上电后,默认处于默认会话(Default Session)。在这个状态下,节点只响应最基本的诊断请求(比如 0x3E 保持激活)。你想做读写数据、刷写固件?对不起,门没开。

0x10 服务的作用,就是让诊断仪告诉节点:“我要切换会话模式了”。常见的会话模式有三种:

子功能(sub-function)会话名称典型用途
0x01默认会话(Default Session)上电后的初始状态,功能受限
0x02编程会话(Programming Session)刷写固件、修改非易失性数据
0x03扩展会话(Extended Session)读写参数、执行例程、解锁安全访问

我个人习惯,在项目初期就把这三个会话的切换逻辑画成状态机。为什么呢?因为LIN节点不像CAN节点那么“强壮”,它的资源有限,状态管理一旦混乱,很容易出现“会话超时”或者“权限不足”的故障码。

避坑指南: 我曾经在一个项目中,把扩展会话的超时时间设成了 5 秒。结果诊断仪在发送 0x27 请求种子时,中间多等了一会儿,节点就自动跳回了默认会话。后来我改成 10 秒,问题解决。记住:LIN 的会话超时时间建议 ≥ 10 秒,给诊断仪留足处理余量。

代码实现上,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 的流程是固定的:

  1. 请求种子(Request Seed): 诊断仪发送 0x27 + 0x01(请求种子),节点返回一个随机种子(比如 4 字节)。
  2. 发送密钥(Send Key): 诊断仪根据种子和约定的算法计算出密钥,发送 0x27 + 0x02 + 密钥。节点验证通过后,解锁对应权限。

这里有个细节:LIN 节点的安全等级通常比 CAN 节点低一些。我见过很多项目直接用简单的异或算法,甚至固定密钥。嗯,这确实不安全,但你要知道,LIN 节点本身算力有限,复杂的加密算法跑不动。所以,平衡安全性和实时性是关键。

注意: 0x27 的种子和密钥长度,在 LIN 诊断规范中推荐为 4 字节。但实际项目中,有些节点用 2 字节,有些用 8 字节。务必在系统需求文档中明确,否则诊断仪和节点对不上,你会排查到崩溃。

我分享一个我踩过的坑:有一次,节点返回种子后,诊断仪计算密钥时把字节序搞反了(大端 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 的控制粒度可以到单个 LIN ID。比如,你可以只禁止 0x01 号报文,而允许 0x02 号报文继续发送。这需要在节点内部维护一个“报文使能掩码表”。
// 伪代码: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 三个服务的协作关系

这三个服务不是孤立的。我画了一张协作流程图,帮你理清它们的关系:

0x10 / 0x27 / 0x28 协作流程 上电 → 默认会话 0x10 → 扩展会话 0x27 → 安全解锁 0x28 → 通信控制 执行诊断操作 关键约束: • 0x27 必须在扩展或编程会话下执行 • 0x28 通常需要安全解锁后才能使用 • 会话超时后,安全解锁状态自动失效 • 通信控制状态在会话切换时重置 • 刷写流程:0x10→0x27→0x28→刷写

从流程图可以看出,这三个服务是串行依赖的。你不能跳过会话控制直接做安全访问,也不能在没解锁的情况下控制通信。这个顺序,是 ISO 14229-1 规定的,也是实际项目中的铁律。

个人经验: 我建议你在设计 LIN 诊断协议栈时,把这三个服务的状态机单独封装成一个模块。这样,其他诊断服务(比如 0x22、0x2E)只需要查询当前会话和安全等级,不用关心底层切换逻辑。代码复用率高,也容易维护。