4. LIN 2.2与2.1协议差异

调度表,说白了就是 LIN 总线的“交通指挥员”。

它决定了什么时候该发哪条报文,谁先谁后,间隔多久。我刚开始接触 LIN 2.x 时,总觉得调度表不就是个循环列表嘛,有啥好研究的?后来在项目里被坑过几次,才明白这东西的细节真不少。

4.1 调度表的结构

一个调度表,本质上是一个有序的帧时隙(Frame Slot)列表。每个时隙指定了:

  • 帧 ID:要发送的报文标识符
  • 帧类型:无条件帧、事件触发帧、偶发帧或诊断帧
  • 时隙长度:该帧占用的时间(以 Tbit 为单位)

我习惯把调度表想象成一张“节目单”。主节点按节目单的顺序,一条一条往下执行。执行完最后一条,再跳回第一条,如此循环。

关键点:调度表是主节点独有的概念。从节点根本不关心调度表,它们只负责响应主节点发起的帧头。

来看一个典型的调度表定义(LDF 文件片段):

Schedule_tables {
  Main_Schedule {
    LIN_MSG_1     delay 10 ms;
    LIN_MSG_2     delay 15 ms;
    LIN_MSG_3     delay 10 ms;
    LIN_MSG_4     delay 20 ms;
  }
}

这个调度表包含 4 个条目。主节点会先发 LIN_MSG_1 的帧头,等 10ms 后发 LIN_MSG_2,再等 15ms……循环往复。

嗯,这里要注意:delay 是帧与帧之间的间隔,不是帧本身的持续时间。帧本身的长度由波特率和数据字节数决定,系统会自动计算。

4.2 触发方式

调度表的触发方式,我把它分成三类:

触发方式 说明 典型应用
连续循环 调度表从头到尾循环执行,永不停止 常规周期性报文(如车速、转速)
单次执行 调度表执行一遍后自动停止 诊断请求、配置命令
条件触发 满足特定条件时才执行某个调度表 休眠唤醒、故障模式

你想想看,如果所有报文都用连续循环,那诊断报文就得等很久才能插进去。所以实际项目中,我们通常会准备多个调度表,按需切换。

我的习惯:至少准备 3 个调度表——正常运行表、诊断表、休眠表。这样切换起来干净利落。

4.3 动态切换实战

动态切换,就是运行时从一个调度表跳到另一个调度表。这是 LIN 2.x 相比 1.x 的一大进步。

我曾经在一个项目中,遇到过一个诡异的问题:切换调度表后,总线突然丢帧了。查了半天,发现是切换时机不对——在帧传输过程中切换,导致当前帧被截断。

避坑指南:切换调度表的最佳时机是当前帧传输完成之后、下一帧开始之前。我建议在帧完成中断里做切换,而不是在主循环里随意切换。

动态切换的典型流程:

  1. 主节点检测到某个条件(如按键按下、故障发生)
  2. 主节点等待当前帧传输完成
  3. 主节点更新调度表指针,指向新调度表
  4. 新调度表从第一条开始执行

代码实现上,我一般这样写:

// 调度表切换函数
void LIN_ScheduleSwitch(uint8_t newTable) {
    // 等待当前帧完成
    while (LIN_IsFrameInProgress()) {
        // 空等待,或者喂狗
    }
    
    // 更新调度表索引
    currentSchedule = newTable;
    scheduleIndex = 0;
    
    // 触发新调度表的第一帧
    LIN_StartFrame(scheduleTable[currentSchedule][scheduleIndex]);
}

这里有个细节:切换后从第一条开始执行,而不是从上次中断的位置继续。这是 LIN 协议规范的要求,也是为了保证确定性。

4.4 多调度表协同

实际项目中,很少只用一张调度表。我参与的一个车身控制项目,用了 5 张调度表:

  • Normal_Schedule:正常运行,周期 20ms
  • Diagnostic_Schedule:诊断模式,周期 50ms,包含诊断帧
  • Sleep_Schedule:休眠前,只发唤醒帧和休眠帧
  • Bootloader_Schedule:刷写模式,周期 10ms,高优先级
  • Error_Schedule:故障模式,只发故障码

切换逻辑用状态机管理,清晰明了。

经验之谈:调度表切换的响应时间,直接影响系统的实时性。我一般要求切换延迟不超过一个帧周期。如果切换逻辑太复杂,可以考虑用硬件定时器辅助。

4.5 调度表与总线负载

调度表的设计,直接决定了总线负载率。我见过一些新手,把所有报文都塞进一张调度表,结果总线负载飙到 80% 以上。

计算总线负载的公式很简单:

负载率 = (所有帧的总传输时间) / (调度表周期) × 100%

举个例子:

帧 ID 数据长度 传输时间 (20kbps)
0x01 4 字节 2.4 ms
0x02 6 字节 3.2 ms
0x03 8 字节 4.0 ms
0x04 2 字节 1.6 ms

如果调度表周期是 20ms,总传输时间 = 2.4 + 3.2 + 4.0 + 1.6 = 11.2ms,负载率 = 56%。

我个人建议,总线负载率控制在 50% 以下,留出余量给诊断和突发报文。超过 70% 就要小心了,很容易丢帧。

4.6 调度表设计原则

总结几条我踩坑踩出来的原则:

  • 优先级高的帧放前面:比如安全相关的报文,放在调度表开头
  • 周期短的帧多放几次:比如 10ms 的帧,可以在 50ms 的调度表里出现 5 次
  • 诊断帧单独放一张表:避免影响正常通信
  • 切换时保留上下文:切换前保存当前状态,切换后能恢复

小技巧:用 Excel 或脚本工具自动生成调度表。手动写 LDF 文件容易出错,尤其是帧多的时候。我写过一个 Python 脚本,输入帧列表和周期,自动输出调度表定义。

好了,调度表这部分就聊到这儿。记住一句话:调度表是 LIN 总线的灵魂,设计好了,系统跑得稳;设计不好,各种奇怪问题都来了。

调度表核心逻辑 主节点 调度表列表 Normal_Schedule Diagnostic_Schedule Sleep_Schedule Bootloader_Schedule 帧时隙列表 时隙1: 帧ID=0x01, 类型=无条件, 延迟=10ms 时隙2: 帧ID=0x02, 类型=无条件, 延迟=15ms 时隙3: 帧ID=0x03, 类型=事件触发, 延迟=10ms 时隙4: 帧ID=0x04, 类型=诊断, 延迟=20ms LIN 总线 从节点