建议先阅读:
【LIN】1.LIN通信实战:帧收发全流程代码实现
【LIN】2.LIN总线通信机制深度解析:主从架构、五种帧类型与动态调度策略

我将分为三个大步骤,每个步骤都基于上一步的成果,最终形成一个完整的LIN通信系统。我们第一步只实现最核心、最简单的功能


整体框架思路:三步走战略

  1. 第一步:基础通信建立 - 实现主从节点间最基础的"一问一答"(无条件帧)。主节点发送命令,从节点执行并回复状态。
  2. 第二步:事件与异常处理 - 在基础通信上,添加"事件触发帧"机制,让从节点能主动报告异常(如防夹),优化带宽。
  3. 第三步:高级功能扩展 - 加入诊断、动态调度等高级功能,形成一个健壮、高效的完整系统。

第一步:基础通信建立 - 主从问询与控制

场景目标:主节点(BCM)周期性地询问开关状态,并根据状态控制车窗升降,最后再询问车窗是否执行成功。

核心概念:这一步我们只使用无条件帧。实现两种模式:

  1. 主机发Header,从机响应 (模式:问答)
  2. 主机发完整帧 (模式:命令)
1.1 软件框架设计(核心思想)

我们要构建两个独立的程序:一个给主节点(BCM),一个给从节点(车窗控制器)。它们通过LIN总线交谈。

  • 主节点的工作流程(循环执行)

    主节点调度循环
    发送帧头: 询问开关状态
    接收从机1的响应数据
    解析数据
    是否需要关窗?
    发送完整帧: 关窗命令
    等待下一次循环
    发送帧头: 询问车窗状态
    接收从机2的响应数据
  • 从节点的工作流程(中断驱动)

    • 一直安静地等待。
    • 一旦听到总线上的PID是属于自己的,就立即行动:要么准备数据并回复,要么接收命令并执行。
1.2 代码实现 - 共用的基础与定义 (lin_core.h/c)

首先,我们创建一些共用的定义和函数,供主从节点一起使用。

lin_core.h

#ifndef LIN_CORE_H
#define LIN_CORE_H

#include "stm32f0xx.h" // 根据你的MCU型号修改

// 1. 定义PID (受保护标识符)
#define PID_MASTER_QUERY_SWITCH    0x21 // 主机询问开关状态
#define PID_MASTER_CTRL_WINDOW     0x22 // 主机发送控制命令
#define PID_SLAVE_REPORT_STATUS    0x23 // 从机报告车窗状态

// 2. 定义命令字
#define CMD_WINDOW_UP              0x01 // 上升
#define CMD_WINDOW_DOWN            0x02 // 下降
#define CMD_WINDOW_STOP            0x00 // 停止

// 3. 声明共用函数
void LIN_UART_Init(void);
uint8_t LIN_CalculatePID(uint8_t id);
uint8_t LIN_CalculateChecksum(uint8_t pid, const uint8_t* data, uint8_t length);

#endif

lin_core.c

#include "lin_core.h"
#include <string.h>

// 初始化UART为LIN模式
void LIN_UART_Init(void) {
    // ... (这里直接使用您提供的 LIN_USART_Init() 函数代码,它是完美的)
    // 包括GPIO、USART、LIN模式、中断的配置
}

// 计算受保护标识符(PID)
uint8_t LIN_CalculatePID(uint8_t id) {
    uint8_t pid = id & 0x3F;
    uint8_t p0 = ((pid >> 0) & 0x01) ^ ((pid >> 1) & 0x01) ^ ((pid >> 2) & 0x01) ^ ((pid >> 4) & 0x01);
    uint8_t p1 = ~(((pid >> 1) & 0x01) ^ ((pid >> 3) & 0x01) ^ ((pid >> 4) & 0x01) ^ ((pid >> 5) & 0x01)) & 0x01;
    return (pid) | (p0 << 6) | (p1 << 7);
}

// 计算校验和(增强型)
uint8_t LIN_CalculateChecksum(uint8_t pid, const uint8_t* data, uint8_t length) {
    uint16_t sum = pid; // 增强校验和包含PID
    for (uint8_t i = 0; i < length; i++) {
        sum += data[i];
        if (sum > 0xFF)
            sum -= 0xFF;
    }
    return (uint8_t)(0xFF - sum); // 取补码
}
1.3 代码实现 - 主节点 (master_node.c)

主节点的核心是调度器(Scheduler),它像一个指挥官,定时决定下一步要做什么。

#include "lin_core.h"
#include "master_node.h"

// 主节点发送帧头(只发Break+Sync+PID,等待从机响应)
void LIN_Master_SendHeader(uint8_t pid) {
    // 1. 发送Break
    USART_SendBreak(USART1);
    while (USART_GetFlagStatus(USART1, USART_FLAG_LBD) == RESET);
    USART_ClearFlag(USART1, USART_FLAG_LBD);

    // 2. 发送同步字节 0x55
    USART_SendData(USART1, 0x55);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

    // 3. 计算并发送PID
    uint8_t protected_pid = LIN_CalculatePID(pid);
    USART_SendData(USART1, protected_pid);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

    // 4. 主机切换为接收模式,准备接收从机的响应(数据+校验和)
    // 具体硬件操作取决于你的USART,可能是设置某个寄存器位
    // USART_DirectionMode_Rx(USART1);
}

// 主节点发送完整帧(发Break+Sync+PID+Data+Checksum,用于下发命令)
void LIN_Master_SendFrame(uint8_t pid, const uint8_t* data, uint8_t data_len) {
    // 1. 发送Break
    USART_SendBreak(USART1);
    while (USART_GetFlagStatus(USART1, USART_FLAG_LBD) == RESET);
    USART_ClearFlag(USART1, USART_FLAG_LBD);

    // 2. 发送同步字节 0x55
    USART_SendData(USART1, 0x55);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

    // 3. 计算并发送PID
    uint8_t protected_pid = LIN_CalculatePID(pid);
    USART_SendData(USART1, protected_pid);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

    // 4. 发送数据
    for (uint8_t i = 0; i < data_len; i++) {
        USART_SendData(USART1, data[i]);
        while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    }

    // 5. 计算并发送校验和
    uint8_t checksum = LIN_CalculateChecksum(protected_pid, data, data_len);
    USART_SendData(USART1, checksum);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}

// 主调度器 - 在主循环中定时调用
void LIN_Master_Scheduler(void) {
    static uint32_t last_time = 0;
    if (HAL_GetTick() - last_time < 100) return; // 每100ms运行一次
    last_time = HAL_GetTick();

    // 1. 询问开关状态 (模式:主机问,从机答)
    LIN_Master_SendHeader(PID_MASTER_QUERY_SWITCH);
    // 此时主机会进入接收状态,等待中断接收数据
    // 假设我们在一个全局变量 `switch_state` 中收到了数据

    // 2. 根据开关状态下发命令 (模式:主机发命令)
    if (switch_state == CMD_WINDOW_UP) {
        uint8_t cmd_data[1] = {CMD_WINDOW_UP};
        LIN_Master_SendFrame(PID_MASTER_CTRL_WINDOW, cmd_data, 1);
    }

    // 3. 询问车窗当前状态,确认执行结果 (模式:主机问,从机答)
    LIN_Master_SendHeader(PID_SLAVE_REPORT_STATUS);
    // 接收数据,更新状态显示...
}
1.4 代码实现 - 从节点 (slave_node.c)

从节点的核心是中断服务程序(ISR),它像一个随时待命的士兵,听到呼叫就行动。

#include "lin_core.h"
#include "slave_node.h"

// 定义全局变量用于LIN通信
volatile uint8_t lin_rx_buffer[10];
volatile uint8_t lin_rx_index = 0;
volatile uint8_t lin_expected_pid = 0;
volatile uint8_t lin_current_pid = 0;

// USART中断服务函数
void USART1_IRQHandler(void) {
    // A. Break检测中断
    if (USART_GetITStatus(USART1, USART_IT_LBD) != RESET) {
        USART_ClearITPendingBit(USART1, USART_IT_LBD);
        lin_rx_index = 0; // 重置接收缓冲区
        lin_current_pid = 0;
        // 进入等待Sync状态
    }

    // B. 接收到数据中断 (RXNE)
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        uint8_t received_data = USART_ReceiveData(USART1);

        // 状态机解析
        if (lin_rx_index == 0 && received_data == 0x55) {
            // 第一个字节是0x55,说明是Sync字段
            lin_rx_index = 1;
        } 
        else if (lin_rx_index == 1) {
            // 第二个字节是PID
            lin_current_pid = received_data & 0x3F; // 提取原始ID
            lin_rx_index = 2;

            // 判断这个PID是不是给我的?如果是,我要做什么?
            if (lin_current_pid == PID_MASTER_QUERY_SWITCH) {
                // 主机在问开关状态!准备响应。
                // 但此时先继续接收,主机可能发的是完整帧?
                // 对于Header,我们接收完PID就结束了,准备响应数据
                lin_expected_data_length = 0; // Header没有后续数据
                LIN_Slave_PrepareResponse(); // 见下方函数
            } 
            else if (lin_current_pid == PID_MASTER_CTRL_WINDOW) {
                // 主机要控制车窗!这是一个完整帧,后面会有数据。
                lin_expected_data_length = 1; // 我们期望1字节数据
                // 继续接收...
            }
            else if (lin_current_pid == PID_SLAVE_REPORT_STATUS) {
                // 主机在问状态!准备响应。
                lin_expected_data_length = 0;
                LIN_Slave_PrepareResponse();
            }
        } 
        else if (lin_rx_index >= 2 && lin_current_pid == PID_MASTER_CTRL_WINDOW) {
            // 接收控制命令的数据字节
            if (lin_rx_index == 2) { // 第一个数据字节
                uint8_t command = received_data;
                Execute_Window_Command(command); // 执行升降命令
            } 
            else if (lin_rx_index == 3) {
                // 收到校验和,可以验证(这里略过)
                // 验证成功,命令执行完毕
            }
            lin_rx_index++;
        }
        // ... 可以处理更多数据
    }

    // C. 空闲中断(一帧接收完成)
    if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
        USART_ReceiveData(USART1); // 清除中断
        // 可以在这里做最终处理,比如校验和验证
        lin_rx_index = 0;
        lin_current_pid = 0;
    }
}

// 从节点准备响应数据并发送
void LIN_Slave_PrepareResponse(void) {
    uint8_t response_data[2];
    uint8_t data_length = 0;

    switch (lin_current_pid) {
        case PID_MASTER_QUERY_SWITCH:
            response_data[0] = Read_Switch_GPIO(); // 读取硬件引脚状态
            data_length = 1;
            break;

        case PID_SLAVE_REPORT_STATUS:
            response_data[0] = Get_Window_Position(); // 获取电机编码器位置
            response_data[1] = Get_Window_Status();   // 获取状态(正常/错误)
            data_length = 2;
            break;

        default:
            return; // 不是我的PID,不响应
    }

    // 发送数据
    for (uint8_t i = 0; i < data_length; i++) {
        USART_SendData(USART1, response_data[i]);
        while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    }

    // 计算并发送校验和
    uint8_t protected_pid = LIN_CalculatePID(lin_current_pid);
    uint8_t checksum = LIN_CalculateChecksum(protected_pid, response_data, data_length);
    USART_SendData(USART1, checksum);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}

// 硬件相关函数(需要您根据实际硬件实现)
uint8_t Read_Switch_GPIO(void) { /* ... */ }
void Execute_Window_Command(uint8_t cmd) { /* ... */ }
uint8_t Get_Window_Position(void) { /* ... */ }
uint8_t Get_Window_Status(void) { /* ... */ }

第一步总结

到此为止,我们完成了LIN通信最核心的基础建设:

  1. 物理层:配置好了UART硬件,可以发送/接收Break、Sync、数据。
  2. 协议层:实现了PID计算、校验和计算,保证了数据的可靠性。
  3. 应用层
    • 主节点:通过一个调度循环,定时发起三种通信:
      • 问开关 -> (收开关状态) -> 发命令 -> 问结果 -> (收结果)
    • 从节点:通过中断驱动,监听总线:
      • 听到问开关问结果,就准备数据并回复
      • 听到发命令,就接收数据并执行

这个简单的“一问一答”机制,就是LIN总线最本质的工作方式。后续所有高级功能(事件触发、诊断等)都是在这个坚实的基础上搭建起来的。


既然第一步的基础通信已经搭建完成,我们现在开始第二步:引入事件触发帧

这一步的目标是解决一个关键问题:如何高效地处理多个从节点可能发生的、非周期性的异常事件(比如防夹功能触发),而不必像第一步那样不停地轮询所有节点,浪费宝贵的总线带宽。

第二步:事件与异常处理 - 事件触发帧

2.1 核心思想与场景痛点

场景回顾
在第一步,主节点(BCM)通过周期性(如每100ms)轮询车窗状态(PID_SLAVE_REPORT_STATUS)来了解情况。这很可靠,但效率低下。

  • 痛点:99%的时间里,车窗状态都是“正常”,轮询得到的都是“无变化”的响应,这浪费了总线时间。
  • 新需求:当车窗在升降过程中遇到障碍物(防夹触发),这是一个需要立即报告的重要事件。我们希望在事件发生时才报告,无事发生时保持安静。

事件触发帧的解决方案

  1. 公共呼叫:主节点发送一个特殊的公共PID(例如PID_EVENT_TRIGGERED)。
  2. 有事应答:所有配置为监听这个PID的从节点(车窗、天窗等),只有在自己有事件要报告时,才会在响应时隙内回复数据。
  3. 冲突解决:如果奇迹般地有多个从节点同时响应导致数据冲突,主节点能检测到冲突(通过校验和错误),然后退回到第一步的轮询模式,逐个询问,精确定位所有事件源。

这个过程如下图所示:

主节点发送事件触发帧头
PID=0x40
从节点有无事件?
从节点1: 状态无变化
从节点2: 防夹事件发生!
保持沉默
发送响应数据
e.g. 0x01, 75
主节点成功接收
解析事件数据
总线无冲突
流程结束
若多个从节点同时响应
总线数据冲突
主节点检测到校验和错误
主节点退回到轮询模式
逐个查询PID_0x23, PID_0x33...
精确定位所有事件源
2.2 代码实现 - 修改共用定义 (lin_core.h)

首先,我们需要定义事件触发帧相关的PID和事件类型。

// lin_core.h (在原有定义上添加)

// 1. 定义事件触发帧的公共PID
#define PID_EVENT_TRIGGERED       0x40 // 事件触发帧的公共PID

// 2. 定义事件类型(示例)
#define EVENT_TYPE_ANTI_PINCH     0x01  // 防夹事件
#define EVENT_TYPE_MOTOR_STALL    0x02  // 电机堵转
#define EVENT_TYPE_OVERHEAT       0x03  // 过热保护

// 3. 声明一个函数,用于判断PID是否是事件触发帧
#define IS_EVENT_TRIGGERED_PID(pid) (((pid) & 0x3F) == PID_EVENT_TRIGGERED)
2.3 代码实现 - 增强主节点 (master_node.c)

主节点需要做三件事:

  1. 在调度表中加入发送事件触发帧的调用。
  2. 能够处理事件触发帧的响应。
  3. 实现冲突检测和回退机制。
// master_node.c (在原有代码上添加和修改)

// 新增全局变量
volatile uint8_t event_frame_has_conflict = 0; // 事件帧冲突标志
volatile uint8_t received_event_data[8];       // 存储收到的事件数据

// 主调度器 - 增强版
void LIN_Master_Scheduler(void) {
    static uint32_t last_time = 0;
    static uint8_t polling_mode = 0; // 0:正常模式, 1:冲突后退回轮询模式
    if (HAL_GetTick() - last_time < 100) return;
    last_time = HAL_GetTick();

    // --- 如果处于冲突回退的轮询模式 ---
    if (polling_mode) {
        static uint8_t poll_index = 0;
        uint8_t poll_pids[] = {PID_SLAVE_REPORT_STATUS, PID_SUNROOF_STATUS}; // 要轮询的PID列表

        LIN_Master_SendHeader(poll_pids[poll_index]);
        // ... 接收处理数据 ...

        poll_index++;
        if (poll_index >= sizeof(poll_pids)) {
            poll_index = 0;
            polling_mode = 0; // 轮询一遍后,恢复正常模式
            event_frame_has_conflict = 0;
        }
        return; // 退出本次调度,优先完成轮询
    }
    // --- 正常调度模式 ---
    
    // 1. 首先尝试用事件触发帧询问所有节点
    LIN_Master_SendHeader(PID_EVENT_TRIGGERED);
    // 等待中断接收响应...

    // 2. 检查上一次事件帧是否有冲突
    if (event_frame_has_conflict) {
        polling_mode = 1; // 设置标志,下次调度进入轮询模式
        event_frame_has_conflict = 0;
        return;
    }

    // 3. 如果没有冲突,并且收到了事件数据,就处理它
    if (ReceivedEventDataIsValid()) {
        ProcessEventData(received_event_data);
        // 处理完事件后,可以额外发一次命令或查询
        // 例如,如果收到防夹事件,立即发送停止命令
        if (received_event_data[0] == EVENT_TYPE_ANTI_PINCH) {
            uint8_t stop_cmd[1] = {CMD_WINDOW_STOP};
            LIN_Master_SendFrame(PID_MASTER_CTRL_WINDOW, stop_cmd, 1);
        }
    }

    // 4. 原有的无条件帧调度(可以降低频率,因为事件帧承担了大部分工作)
    static uint32_t last_unconditional_time = 0;
    if (HAL_GetTick() - last_unconditional_time > 500) { // 改为500ms轮询一次
        last_unconditional_time = HAL_GetTick();
        LIN_Master_SendHeader(PID_MASTER_QUERY_SWITCH);
        // ... 后续操作不变 ...
    }
}

// 在USART中断服务程序中(或数据接收回调中),添加冲突检测
// 假设数据接收完成,并进行校验和验证
void LIN_OnResponseReceived(uint8_t pid, uint8_t* data, uint8_t length, uint8_t checksum_ok) {
    uint8_t raw_pid = pid & 0x3F;

    if (raw_pid == PID_EVENT_TRIGGERED) {
        if (!checksum_ok) {
            // 校验和错误!很可能发生了冲突。
            event_frame_has_conflict = 1;
        } else {
            // 校验成功,保存事件数据
            memcpy(received_event_data, data, (length < 8) ? length : 8);
        }
    } 
    else if (raw_pid == PID_MASTER_QUERY_SWITCH) {
        switch_state = data[0]; // 保存开关状态
    }
    else if (raw_pid == PID_SLAVE_REPORT_STATUS) {
        window_position = data[0];
        window_status = data[1];
    }
}
2.4 代码实现 - 增强从节点 (slave_node.c)

从节点需要做两件事:

  1. 能够判断自己是否需要响应公共的事件触发帧。
  2. 在需要响应时,准备特定格式的事件数据。
// slave_node.c (在原有代码上添加和修改)

// 新增全局变量和函数
volatile uint8_t node_has_event = 0;    // 本节点是否有事件待报告
volatile uint8_t current_event_type;    // 当前事件类型
volatile uint8_t current_event_data[4]; // 当前事件相关数据

// 在防夹检测函数(或其他事件检测函数)中设置事件标志
void Check_Anti_Pinch(void) {
    if (/* 检测到障碍物 */) {
        node_has_event = 1;
        current_event_type = EVENT_TYPE_ANTI_PINCH;
        current_event_data[0] = Get_Window_Position(); // 记录事件发生时的位置
        current_event_data[1] = Read_Motor_Current();  // 记录事件发生时的电流
        // ... 触发紧急停止 ...
        Execute_Window_Command(CMD_WINDOW_STOP);
    }
}

// 修改LIN从节点准备响应函数
void LIN_Slave_PrepareResponse(void) {
    uint8_t response_data[4]; // 事件数据可能需要多个字节
    uint8_t data_length = 0;

    switch (lin_current_pid) {
        case PID_MASTER_QUERY_SWITCH:
            response_data[0] = Read_Switch_GPIO();
            data_length = 1;
            break;

        case PID_SLAVE_REPORT_STATUS:
            response_data[0] = Get_Window_Position();
            response_data[1] = Get_Window_Status();
            data_length = 2;
            break;

        // --- 新增:处理事件触发帧 ---
        case PID_EVENT_TRIGGERED:
            if (node_has_event) {
                // 只有有事件时才响应
                response_data[0] = current_event_type; // 事件类型
                response_data[1] = current_event_data[0]; // 数据1
                response_data[2] = current_event_data[1]; // 数据2
                data_length = 3;
                node_has_event = 0; // 清除事件标志,已上报
            } else {
                // 没有事件,不响应,直接返回
                return;
            }
            break;

        default:
            return;
    }

    // ... 发送数据和校验和(保持不变)...
}

第二步总结

通过以上代码,我们成功地在第一步的基础之上引入了事件触发帧机制:

  1. 效率提升:主节点不再需要高频轮询所有状态。它只需定期发送一个PID_EVENT_TRIGGERED,绝大多数情况下总线非常安静,节省了带宽。
  2. 及时响应:从节点一旦检测到紧急事件(如防夹),可以设置标志位,并在下一次主节点发起事件询问时立即上报,响应及时。
  3. 鲁棒性:我们设计了冲突检测和回退机制。万一多个节点同时事件上报导致冲突,系统能自动、可靠地退回到传统的轮询模式,确保所有事件源都能被定位,不会丢失信息。

现在LIN网络变得更加智能和高效了! 它不再是机械地轮流问答,而是具备了“有情况才报告”的事件驱动能力。


太好了!我们继续第三步,这也是最后一步:高级功能扩展 - 诊断与动态调度

这一步的目标是让LIN网络具备可维护性终极效率。我们通过引入诊断帧和优化调度策略来实现。

第三步:高级功能扩展 - 诊断与动态调度

3.1 核心思想与场景痛点

新需求一:可维护性
当车窗防夹功能触发后,维修技师不仅要知道“发生了防夹”,还需要知道详细信息:发生在哪个位置?电机电流多大?历史记录是怎样的?第一步和第二步的普通通信帧无法承载这些复杂的诊断信息,我们需要一个专用的、可靠的“后台通道”。

解决方案:诊断帧

  • 使用LIN协议保留的专用PID0x3C, 0x3D)。
  • 遵循行业标准(如UDS),使用固定的8字节数据格式,进行点对点的深度通信。
  • 主节点(BCM)充当网关,将来自更高级网络(如CAN总线)的诊断指令翻译成LIN诊断帧,反之亦然。

新需求二:终极效率
在第二步中,我们用事件触发帧避免了无用的轮询。但我们还可以更聪明:只有当数据确实可能变化时,才去查询它。这就是“偶发帧”的概念,它本质上是一种智能调度策略。

解决方案:动态调度(偶发帧策略)

  • 主节点监视数据变化的可能性(如:有按键按下吗?有事件上报吗?)。
  • 如果可能变化,就临时提高相关查询帧的调度频率。
  • 如果数据稳定,就降低甚至暂停相关查询帧的调度,极大节省带宽。
3.2 代码实现 - 诊断帧(共用定义 lin_core.h)

首先定义诊断专用的PID和诊断服务ID。

// lin_core.h (在原有定义上添加)

// 1. 定义诊断帧的保留PID (LIN协议规定)
#define PID_DIAGNOSTIC_REQUEST     0x3C // 主节点发送诊断请求
#define PID_DIAGNOSTIC_RESPONSE    0x3D // 从节点回复诊断响应

// 2. 定义示例诊断服务ID (UDS标准的一部分)
#define DID_READ_ANTI_PINCH_EVENT  0xF190 // 自定义: 读取防夹事件数据
#define SID_READ_DATA_BY_ID        0x22   // UDS服务: 按标识符读数据
#define SID_READ_DATA_BY_ID_RESP   0x62   // UDS正响应: 0x22 + 0x40

// 3. 增强PID检查宏
#define IS_DIAG_PID(pid) (((pid) & 0x3F) == PID_DIAGNOSTIC_REQUEST || \
                         ((pid) & 0x3F) == PID_DIAGNOSTIC_RESPONSE)
#define IS_VALID_USER_PID(pid) (!IS_DIAG_PID(pid)) // 用户PID不能是诊断PID
3.3 代码实现 - 诊断帧(主节点 master_node.c)

主节点需要实现诊断网关功能,在LIN诊断和CAN诊断之间转换。

// master_node.c (新增诊断网关功能)

// 假设这个函数被调用当BCM从CAN总线收到诊断请求
void CAN_Received_Diagnostic_Request(uint8_t *can_data, uint8_t len) {
    // 1. 解析CAN诊断消息,判断目标是哪个LIN从节点及服务
    // 例如,CAN数据可能包含目标地址和服务ID

    // 2. 构建LIN诊断请求帧 (8字节)
    uint8_t lin_diag_request[8] = {0};
    lin_diag_request[0] = 0x06;       // 后续有效数据长度: 6字节
    lin_diag_request[1] = SID_READ_DATA_BY_ID; // 服务ID: 0x22
    lin_diag_request[2] = (DID_READ_ANTI_PINCH_EVENT >> 8) & 0xFF; // 数据标识符高字节
    lin_diag_request[3] = DID_READ_ANTI_PINCH_EVENT & 0xFF;        // 数据标识符低字节
    // lin_diag_request[4-7] 可以是0或其它参数

    // 3. 发送LIN诊断请求帧 (完整帧)
    LIN_Master_SendFrame(PID_DIAGNOSTIC_REQUEST, lin_diag_request, 8);

    // 4. 紧接着,发送诊断响应帧的Header,请求从节点回复
    LIN_Master_SendHeader(PID_DIAGNOSTIC_RESPONSE);
    // 现在主节点会等待接收从节点的诊断响应数据
}

// 在数据接收处理函数中,添加对诊断响应的处理
void LIN_OnResponseReceived(uint8_t pid, uint8_t* data, uint8_t length, uint8_t checksum_ok) {
    uint8_t raw_pid = pid & 0x3F;

    // ... 处理其他PID ...

    if (raw_pid == PID_DIAGNOSTIC_RESPONSE) {
        if (checksum_ok && length >= 3) {
            // 解析LIN诊断响应,转换成CAN格式
            uint8_t can_diagnostic_data[8] = {0};
            // 示例: 直接转发前8字节(实际需要根据UDS格式处理)
            uint8_t bytes_to_copy = (length < 8) ? length : 8;
            memcpy(can_diagnostic_data, data, bytes_to_copy);

            // 通过CAN总线发送给诊断仪
            CAN_Send_Diagnostic_Response(can_diagnostic_data, bytes_to_copy);
        }
    }
}
3.4 代码实现 - 诊断帧(从节点 slave_node.c)

从节点需要能够解析和执行诊断命令,并返回详细的诊断数据。

// slave_node.c (新增诊断请求处理功能)

// 在LIN_Slave_PrepareResponse函数中添加诊断帧的响应
void LIN_Slave_PrepareResponse(void) {
    uint8_t response_data[8] = {0}; // 诊断响应固定为8字节
    uint8_t data_length = 0;

    switch (lin_current_pid) {
        // ... 处理其他普通PID ...

        // --- 新增:处理诊断响应帧 ---
        case PID_DIAGNOSTIC_RESPONSE:
            // 准备诊断响应数据
            data_length = 8; // 诊断帧固定长度
            LIN_Prepare_Diagnostic_Response(response_data);
            break;

        default:
            return;
    }

    // ... 发送数据和校验和 ...
}

// 新函数:准备诊断响应数据
void LIN_Prepare_Diagnostic_Response(uint8_t *response_data) {
    // 这里我们假设之前接收并解析了一个诊断请求,存储在某个结构体中
    // 根据请求的服务ID来组织响应数据

    // 示例:响应“读数据”服务
    response_data[0] = 0x05; // 后续有效数据长度: 5字节
    response_data[1] = SID_READ_DATA_BY_ID_RESP; // 响应SID: 0x62
    response_data[2] = (DID_READ_ANTI_PINCH_EVENT >> 8) & 0xFF; // 回显DID
    response_data[3] = DID_READ_ANTI_PINCH_EVENT & 0xFF;

    // 填入真正的诊断数据(例如,上次防夹事件的数据)
    response_data[4] = current_event_type;     // 事件类型
    response_data[5] = current_event_data[0];  // 事件位置
    response_data[6] = current_event_data[1];  // 事件电流
    response_data[7] = 0x00;                   // 保留或其它数据
}

// 在中断中,还需要处理接收到的诊断请求帧(PID_DIAGNOSTIC_REQUEST)
// 这部分需要在数据接收状态机中添加,用于接收和存储主机发来的诊断指令
// 代码较长,概念是:当收到PID=0x3C时,期待接收8字节数据,并解析存储。
3.5 代码实现 - 动态调度(主节点 master_node.c)

最后,我们实现智能的动态调度策略,这是“偶发帧”概念的软件实现。

// master_node.c (实现智能调度器)

// 增强调度表条目结构体
typedef struct {
    uint8_t pid;
    uint32_t default_interval_ms;
    uint32_t min_interval_ms;
    uint8_t is_active;           // 该帧是否应被调度
    uint32_t last_sent_time;
    uint8_t sporadic_trigger;    // 触发该帧变为活跃的条件
} LIN_ScheduleTableEntry_t;

LIN_ScheduleTableEntry_t schedule_table[] = {
    // PID           默认间隔  最小间隔  活跃?  最后发送时间  触发条件
    {PID_MASTER_QUERY_SWITCH,  100,   50,   1, 0, 0}, // 总是活跃
    {PID_SLAVE_REPORT_STATUS, 1000,  100,   0, 0, TRIGGER_ON_WINDOW_MOVE}, // 初始不活跃
    {PID_EVENT_TRIGGERED,      200,  200,   1, 0, 0},
    // ... 可以添加更多条目 ...
};
#define TRIGGER_ON_WINDOW_MOVE 0x01

// 智能调度器 - 替代原来的简单调度器
void LIN_Master_Advanced_Scheduler(void) {
    uint32_t current_time = HAL_GetTick();

    for (int i = 0; i < sizeof(schedule_table)/sizeof(schedule_table[0]); i++) {
        LIN_ScheduleTableEntry_t *entry = &schedule_table[i];

        // 检查触发条件,动态激活非活跃帧
        if (!entry->is_active && entry->sporadic_trigger) {
            if (entry->sporadic_trigger & TRIGGER_ON_WINDOW_MOVE) {
                if (switch_state != CMD_WINDOW_STOP) { // 如果开关按下,车窗应该正在动
                    entry->is_active = 1;
                    entry->last_sent_time = 0; // 让它可以立即发送
                }
            }
        }

        // 调度活跃的帧
        if (entry->is_active) {
            if (current_time - entry->last_sent_time >= entry->default_interval_ms) {
                // 发送Header或Frame
                if (entry->pid == PID_MASTER_CTRL_WINDOW) {
                    // 发送命令帧
                    uint8_t cmd_data[1] = {switch_state};
                    LIN_Master_SendFrame(entry->pid, cmd_data, 1);
                } else {
                    // 发送Header,等待响应
                    LIN_Master_SendHeader(entry->pid);
                }
                entry->last_sent_time = current_time;

                // 如果是状态查询帧,检查是否可以停用它
                if (entry->pid == PID_SLAVE_REPORT_STATUS) {
                    static uint8_t last_position = 0;
                    static uint8_t no_change_count = 0;
                    if (window_position == last_position) {
                        no_change_count++;
                        if (no_change_count > 3) { // 连续3次无变化
                            entry->is_active = 0; // 停用调度
                            no_change_count = 0;
                        }
                    } else {
                        no_change_count = 0;
                        last_position = window_position;
                    }
                }
            }
        }
    }
}
3.6 代码实现 - PID验证(共用 lin_core.c)

为确保协议兼容性,添加PID合法性验证。

// lin_core.c (新增函数)

// 发送前的PID检查函数
uint8_t LIN_Validate_PID_Usage(uint8_t pid) {
    uint8_t raw_pid = pid & 0x3F;

    // 检查是否误用了保留给诊断的PID
    if (IS_DIAG_PID(raw_pid)) {
        // 日志记录错误
        Log_Error(ERROR_RESERVED_PID_MISUSE);
        return 0;
    }

    // 检查PID值是否在有效范围内 (0-63, 但60-61被保留)
    if (raw_pid > 0x3F || (raw_pid >= 0x3C && raw_pid <= 0x3D)) {
        Log_Error(ERROR_INVALID_PID_RANGE);
        return 0;
    }

    return 1; // PID有效
}

// 增强的发送函数,带检查
uint8_t LIN_Master_SendFrame_Checked(uint8_t pid, const uint8_t* data, uint8_t data_len) {
    if (!LIN_Validate_PID_Usage(pid)) {
        return 0; // 发送失败
    }
    LIN_Master_SendFrame(pid, data, data_len);
    return 1; // 发送成功
}

第三步总结

至此,我们完成了LIN总线框架的最后一步升级,实现了诊断智能调度两大高级功能:

  1. 诊断帧 (Diagnostic Frames)

    • 我们使用了协议规定的保留PID0x3C, 0x3D)。
    • 实现了基于UDS标准的诊断服务,可以访问从节点的深层数据(如详细的防夹事件记录)。
    • 主节点充当了网关,桥接了LIN总线与更高级的网络(如CAN)。
  2. 动态调度 (偶发帧策略 Sporadic Frame Strategy)

    • 实现了一个智能调度表,可以根据条件(如开关按下)动态激活或停用对某些PID的查询。
    • 极大地优化了带宽使用,避免了在数据稳定时进行无用的轮询。
  3. 保留帧与兼容性 (Reserved Frames)

    • 通过PID验证函数,确保了用户不会误用协议保留的PID,保证了系统的标准性和兼容性。

您的LIN总线通信系统现在已经是一个功能完整、高效且专业的实现了! 它具备了:

  • 基础通信(无条件帧)
  • 高效事件处理(事件触发帧)
  • 专业诊断能力(诊断帧)
  • 带宽优化(动态调度/偶发帧策略)
  • 协议兼容性(保留帧处理)
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐