数据链路层:LIN的帧结构与时序秘密

好,我们进入数据链路层。说白了,这一层定义了LIN总线上“一帧数据长什么样”。我当年刚接触LIN时,以为跟CAN差不多,结果被同步间隔字段坑了一把——示波器抓出来全是乱码。

帧结构是LIN通信的基石。每个节点都按这个规矩来收发,谁也不能例外。

LIN帧的整体结构

一帧完整的LIN报文,由主机任务和从机任务共同完成。主机发头,从机响应。帧结构分成两部分:

  • 主机发送部分:同步间隔段、同步段、标识符段
  • 从机响应部分:数据段、校验和段

先看这张图,把这几个段的位置和关系搞清楚。

LIN 帧结构全景 帧头(主机任务发送) 帧响应(从机任务发送) 同步间隔段 同步段 标识符段 数据段 校验和段 主机发送帧头(同步间隔+同步+ID),从机根据ID决定是否响应帧(数据+校验和) 如果ID无对应从机响应,帧就只有帧头,没有帧响应——这叫“无响应帧”

关键点:帧头由主机任务发起,帧响应由被寻址的从机任务填充。主机只发头,从机只响应与自己ID匹配的帧。

同步间隔段(Break)

同步间隔段是帧开始的信号。它告诉所有节点:“准备一下,新帧来了。”

规范要求:同步间隔段至少持续13个显性位时间(低电平)。后续跟着至少1个位的总线空闲(隐性位,高电平),用来做间隔界定符。

为什么是13位?因为正常的字节帧最多10位(1起始+8数据+1停止),13位保证了任何正常字节都不会被误判为同步间隔。嗯,这是协议层的防误触发设计。

我曾经踩过的坑:有次调试一个门控模块,LIN总线偶尔漏帧。查了半天,发现主机发出的同步间隔只有11个位时间。有些从机芯片比较严格,11位不认。后来我把同步间隔设成14位,问题就消失了。

同步间隔的时序要求,记住这张表就够了:

参数 最小值 典型值 最大值
同步间隔长度 13位时间 13~14位时间 无严格上限(建议≤20位)
间隔界定符 1位时间 2位时间 无
同步间隔低电平 显性 显性 显性

同步字段(Sync Byte)

同步字段的值固定为 0x55,也就是二进制 01010101。这可不是随便选的。

0x55 在总线上会产生均匀的上升沿和下降沿——每1位就跳变一次。从机利用这些边沿来测量主机的位时间,校准自己的时钟。说白了,就是从机跟着主机的节奏走。

LIN 2.x 的从机通过同步字段可以容忍最高 ±14% 的时钟偏差。我自己试过,用内部RC振荡器的不校准晶振,偏差在 ±5% 左右,完全没问题。

实用技巧:抓波形时,看到连续的5个方波脉冲(0101交替),那就是同步字段。如果同步字段波形畸变,多半是主从机波特率不匹配,或者总线上容性负载太重。

标识符字段(Protected Identifier)

标识符段是帧头的核心,共8位。它决定了这帧数据的“身份”和“去向”。

这8位的结构如下:

  • ID[5:0]:6位的帧标识,取值范围 0~63
  • P1, P0:2位的奇偶校验位
  • P0 = ID0 ⊕ ID1 ⊕ ID2 ⊕ ID4
  • P1 = ¬(ID1 ⊕ ID3 ⊕ ID4 ⊕ ID5)

你想想看,6位ID共64个可用标识符。LIN 2.x 定义了4种帧类型:无条件帧、事件触发帧、零星帧、诊断帧。通过ID范围来区分。

帧类型 ID范围 用途
无条件帧 0x00 ~ 0x3B 定期广播的信号,一个ID只能对应一个发送节点
事件触发帧 0x00 ~ 0x3B 多个从机可以竞争发送,需搭配冲突解决
零星帧 0x00 ~ 0x3B 按需发送,由主机根据事件触发
诊断帧 0x3C, 0x3D 从机诊断、配置、编程
保留帧 0x3E, 0x3F 未来扩展

我一般把ID设计成跟信号组一一对应。比如车门控制用ID 0x10-0x15,车窗用0x20-0x25。这样调试时一眼就能看出是哪个功能在通信。

避坑指南:注意ID的奇偶校验位是必须的。有些初学者直接写0x15作为ID,但LIN从机芯片需要的是经过校验的Protected ID。如果校验位错了,从机会直接忽略这个帧头。

数据段与校验和段

数据段最多8个字节,这也是LIN协议跟CAN相似的地方。但LIN的数据段长度受LEN字段约束,可以是1~8字节。

校验和段分两种模式:

  • 经典校验和:只覆盖数据段(LIN 1.x兼容)
  • 增强校验和:覆盖数据段 + 标识符段(LIN 2.x默认)

我个人习惯统一用增强校验和。除非项目明确要求兼容LIN 1.x从机。校验和算法很简单:所有字节相加,取反码,丢弃进位。

// 增强校验和计算示例
uint8_t calc_lin_checksum(uint8_t pid, uint8_t *data, uint8_t len) {
    uint16_t sum = 0;
    uint8_t i;

    // 包含标识符段(Protected ID)
    sum = pid;

    // 累加数据段
    for (i = 0; i < len; i++) {
        sum += data[i];
    }

    // 进位回卷
    while (sum > 0xFF) {
        sum = (sum & 0xFF) + (sum >> 8);
    }

    // 取反码
    return (uint8_t)(~sum & 0xFF);
}

嗯,这段代码我在三个项目里直接用。简单可靠,没有坑。

字节场与位时序

每个字节场使用标准的串行UART格式:1个起始位 + 8个数据位 + 1个停止位。LSB先发,MSB后发。这跟RS232一样,所以很多MCU直接用UART外设就能收发LIN。

但是——LIN的位时间不是固定的。它取决于波特率。常用波特率有:

  • 1 kbps(低速,用于车身控制)
  • 2.4 kbps(低速)
  • 9.6 kbps(中速,最常用)
  • 19.2 kbps(高速)

整个帧的传输时间,你按这个公式估算:

帧时间 ≈ 同步间隔(14位) + 间隔界定符(1位) 
         + 同步字节(10位) + 标识符字节(10位) 
         + N×数据字节×10位 + 校验和字节(10位)
         + 帧内响应空间(图中未标,约几位的间隔)

以1字节数据帧@19.2kbps为例:
总位数 ≈ 14+1+10+10+10+10 = 55位
时间 ≈ 55 / 19200 ≈ 2.86 ms

经验之谈:设计调度表时,给每个帧留足时间余量。我一般会在理论时间上再多留20%~30%。因为你测出来的实际波形有上升沿斜率、从机响应延迟,这些都会吃掉时间。

好了,帧结构、同步间隔、同步字段、标识符这几个概念就讲到这里。它们串联起来就是LIN数据链路层的完整骨架。调试时只要盯着这几个段看,大部分问题都能定位。不会?那就拿示波器抓一帧,对着这篇文章的结构对照分析,很快就能上手。