4. LIN调度表:调度表概念、调度表配置、调度表执行、调度表切换与冲突处理

好,咱们今天聊聊LIN总线里一个非常核心的概念——调度表。

说实话,我刚接触LIN的时候,觉得这东西不就是个定时器列表嘛,有啥好讲的?后来在项目里踩了坑才发现,调度表的设计直接决定了整个网络的实时性和稳定性。你想想看,LIN网络里所有节点都是被动听话的,谁什么时候说话,全凭主节点手里的那张调度表说了算。

4.1 调度表概念

调度表,英文叫Schedule Table。说白了,它就是一张时间表。

主节点按照这张表,一个接一个地发送帧头。每个帧头对应一个具体的从节点任务。比如0号槽位发0x01的ID,1号槽位发0x02的ID,以此类推。整个网络就像一台精密的钟表,滴答滴答往前走。

我个人习惯把调度表理解成「剧本」。主节点是导演,从节点是演员。导演按照剧本喊「Action」,演员们按剧本演戏。剧本里写了每个场景(帧ID)什么时候开始,持续多久。

调度表有几个关键属性:

  • 槽位(Slot):调度表的最小执行单元。每个槽位包含一个帧ID和该帧的持续时间。
  • 周期(Cycle):调度表从头到尾执行一遍的时间。
  • 跳转(Jump):执行完当前槽位后,可以跳转到另一个调度表的某个槽位继续执行。

重要概念:调度表不是一成不变的。它可以在运行时动态切换。这一点在车身控制中非常常见,比如车门解锁后,调度表要从「休眠模式」切换到「激活模式」。

4.2 调度表配置

配置调度表,说白了就是填一张Excel表格。每个槽位填什么ID,持续多少毫秒。

我在项目中遇到过一种情况:工程师把槽位持续时间算错了,导致帧与帧之间产生了重叠。结果呢?从节点还没来得及回复上一帧,下一帧的帧头就来了,整个网络乱成一锅粥。

配置调度表时,我建议你关注以下几点:

  1. 帧持续时间:这个时间要大于等于帧的最大传输时间。LIN帧最大是10个字节(2个同步+1个ID+8个数据+1个校验),按20kbps算,大约需要5ms。我一般会留20%的余量,设成6ms。
  2. 槽位顺序:把紧急的帧放在前面。比如安全气囊的帧,肯定比车窗升降的帧优先级高。
  3. 空闲槽位:在调度表末尾留几个空闲槽位,用于诊断或临时任务。

下面是一个典型的调度表配置示例:

// 调度表配置结构体
typedef struct {
    uint8_t  frameId;      // 帧ID
    uint16_t slotTimeMs;   // 槽位时间(ms)
    uint8_t  nextTable;    // 下一调度表索引(0xFF表示结束)
} ScheduleSlot_t;

// 示例:车门控制调度表
ScheduleSlot_t doorSchedule[] = {
    {0x01, 10, 0xFF},  // 驾驶员门锁状态
    {0x02, 10, 0xFF},  // 乘客门锁状态
    {0x03, 15, 0xFF},  // 车窗位置反馈
    {0x04, 10, 0xFF},  // 后视镜状态
    {0xFF, 50, 0x01}   // 空闲槽位,跳转到表1
};

我的小技巧:配置调度表时,先用Excel模拟一遍。把每个槽位的起始时间、结束时间画成甘特图,一眼就能看出有没有重叠或空隙。

4.3 调度表执行

调度表的执行,其实就是主节点的一个定时中断服务程序。

主节点内部维护一个指针,指向当前槽位。定时器每触发一次,指针就往前走一步。如果走到表尾,就回到表头重新开始。

执行流程大致如下:

  1. 定时器中断触发
  2. 读取当前槽位的帧ID
  3. 发送帧头
  4. 等待从节点回复(或超时)
  5. 更新指针到下一个槽位
  6. 设置下一次中断的时间

嗯,这里要注意:等待从节点回复的时间不能超过槽位时间。如果从节点没回复,主节点也要在槽位时间内放弃等待,继续执行下一个槽位。否则整个调度表就会卡住。

我曾经在一个项目中,从节点因为电源不稳定偶尔丢帧。主节点傻傻地等它回复,结果后面的帧全部延迟了。后来我加了一个超时机制:如果从节点在槽位时间的80%内没回复,直接跳过。

// 调度表执行伪代码
void Schedule_Execute(void) {
    ScheduleSlot_t *slot = &currentSchedule[currentSlot];
    
    // 发送帧头
    LIN_SendHeader(slot->frameId);
    
    // 等待回复,最多等待 slotTimeMs * 0.8
    uint32_t timeout = (slot->slotTimeMs * 80) / 100;
    while (timeout--) {
        if (LIN_ResponseReceived()) {
            break;
        }
        delay(1);
    }
    
    // 移动到下一个槽位
    currentSlot++;
    if (currentSchedule[currentSlot].frameId == 0xFF) {
        // 跳转到其他调度表
        currentSchedule = &scheduleTable[currentSchedule[currentSlot].nextTable];
        currentSlot = 0;
    }
}

4.4 调度表切换与冲突处理

调度表切换,是LIN网络里最容易出问题的地方。

为什么要切换调度表?举个例子:

  • 车辆熄火时,用「休眠调度表」,只发几个心跳帧
  • 车辆启动时,切换到「激活调度表」,所有节点全速工作
  • 诊断模式下,切换到「诊断调度表」,只发诊断帧

切换时机很关键。我建议在调度表执行完一个完整周期后再切换。为什么?因为如果在槽位中间切换,有的从节点可能正在回复上一帧,突然收到新的帧头,会直接懵逼。

避坑指南:我曾经在项目里犯过一个低级错误——在槽位执行到一半时切换调度表。结果从节点收到了一个「半截子」帧头,校验失败,直接进入错误状态。整个网络花了3个周期才恢复过来。

冲突处理,主要考虑以下几种情况:

冲突类型 原因 处理方法
帧ID冲突 两个调度表使用了相同的帧ID 确保同一时刻只有一个调度表激活
时间冲突 切换后槽位时间与当前时间不匹配 在切换点重新同步定时器
状态冲突 从节点状态与调度表不匹配 切换前广播状态变更通知

我个人习惯的做法是:在调度表末尾设置一个「切换槽位」。这个槽位不发送任何帧,只做一件事——检查是否有切换请求。如果有,就执行切换;如果没有,就继续循环当前调度表。

// 调度表切换示例
void Schedule_Switch(uint8_t newTableIndex) {
    // 标记切换请求,不立即执行
    scheduleSwitchRequest = newTableIndex;
}

// 在调度表末尾的切换槽位中执行
void Schedule_CheckSwitch(void) {
    if (scheduleSwitchRequest != 0xFF) {
        // 等待当前帧回复完成
        while (LIN_Busy());
        
        // 执行切换
        currentSchedule = &scheduleTable[scheduleSwitchRequest];
        currentSlot = 0;
        scheduleSwitchRequest = 0xFF;
        
        // 重新同步定时器
        Timer_Reset();
    }
}

最后说一句:调度表的设计没有银弹。不同的应用场景,调度策略完全不同。比如车窗控制,调度表可以慢一点,100ms一个周期都行。但安全气囊的调度表,必须10ms以内一个周期。

你想想看,如果安全气囊的调度表被车窗的调度表堵住了,那后果...嗯,你懂的。

好了,调度表的内容就讲到这里。下一章咱们聊聊LIN的诊断功能,那可是排查故障的利器。