4. LIN总线调度表:调度表的概念、调度表的配置、时隙分配、调度表的执行流程
好,咱们今天聊聊LIN总线里一个特别核心的东西——调度表。
说实话,很多刚接触LIN的朋友,总觉得它就是个低速的CAN。其实不然。LIN的通信方式,跟CAN那种“谁抢到谁发”完全不一样。LIN是主从结构,什么时候发,发什么,全由主节点说了算。而这个“说了算”的规则,就写在调度表里。
4.1 调度表的概念
调度表是什么?说白了,它就是一张时间表。
主节点按照这张表,一个格子一个格子地往下走。每个格子里规定了:
- 该发哪个帧(哪个从节点响应)
- 帧之间的间隔时间是多少
我打个比方。你想想看,这就像学校里的课程表。周一第一节语文,第二节数学,第三节英语。到了时间就打铃,老师就进教室。调度表就是LIN总线的“课程表”。
核心要点:调度表决定了LIN总线上所有报文的发送顺序和时间。没有调度表,LIN总线就是一盘散沙。
我在项目中遇到过一件事。有个同事把调度表理解成了“建议性的”,觉得从节点可以自己决定什么时候发。结果呢?总线上一堆报文冲突,数据全乱了。嗯,这里要注意:调度表是强制性的,主节点必须严格执行。
4.2 调度表的配置
调度表的配置,通常在LDF文件里完成。LDF就是LIN描述文件,相当于LIN总线的“宪法”。
一个典型的调度表配置,长这样:
Schedule_tables {
// 主调度表,名字叫NormalSchedule
NormalSchedule {
// 时隙1:发ID为0x10的帧,间隔10ms
Collision_resolution = 0;
Frame 0x10 time 10 ms;
// 时隙2:发ID为0x20的帧,间隔20ms
Frame 0x20 time 20 ms;
// 时隙3:发ID为0x30的帧,间隔5ms
Frame 0x30 time 5 ms;
}
}
我个人习惯,在配置调度表时,会特别注意以下几点:
- 帧ID不能重复:同一个调度表里,同一个帧ID只能出现一次。否则从节点会懵掉。
- 时间间隔要合理:不能太短,否则从节点来不及处理;也不能太长,否则总线利用率太低。
- 调度表可以多个:比如一个正常模式调度表,一个休眠模式调度表,一个诊断模式调度表。主节点可以切换。
小技巧:我建议你在LDF里给调度表起名字时,用“NormalSchedule”、“SleepSchedule”、“DiagSchedule”这种一眼就能看懂的命名。别用“Sched1”、“Sched2”,三个月后你自己都看不懂。
4.3 时隙分配
时隙分配,就是给每个帧分配一个“时间窗口”。
这个窗口包括两部分:
- 帧传输时间:从主节点发同步间隔场开始,到从节点发完数据结束。这个时间跟波特率和数据长度有关。
- 帧间间隔:两个帧之间的空闲时间。这个时间由调度表里的“time”参数决定。
举个例子。假设波特率是20kbps,一个标准LIN帧(8字节数据)大概需要:
- 同步间隔场:13位
- 同步场:1字节
- 标识符场:1字节
- 数据场:8字节
- 校验和场:1字节
- 应答间隔:1位
算下来大概1.2ms左右。如果你在调度表里配了10ms的间隔,那实际一个时隙就是1.2ms + 10ms = 11.2ms。
避坑指南:我曾经在项目里犯过一个低级错误。我把所有帧的间隔都配成了5ms,觉得这样总线利用率高。结果发现,有些从节点处理不过来,数据老是丢。后来才意识到,每个从节点的处理能力不一样。你想想看,一个8位单片机,处理完一个帧可能需要几毫秒。你5ms就发下一个,它根本来不及。所以,时隙分配一定要考虑从节点的处理时间。
4.4 调度表的执行流程
调度表的执行流程,其实就是一个循环。主节点不停地重复执行调度表里的内容。
具体流程如下:
- 启动调度表:主节点上电后,选择并启动一个调度表。通常是NormalSchedule。
- 读取第一个时隙:主节点看调度表里第一个时隙配的是什么帧。
- 发送帧头:主节点发送同步间隔场、同步场、标识符场。这就是帧头。
- 等待从节点响应:主节点等待从节点发送数据场和校验和场。如果从节点没响应,主节点会超时处理。
- 等待间隔时间:帧传输完成后,主节点等待调度表里配的间隔时间。
- 进入下一个时隙:间隔时间到了,主节点读取下一个时隙,重复步骤3-5。
- 循环:调度表执行到最后一条后,主节点回到第一条,重新开始。
这个流程,我用代码实现过很多次。核心逻辑大概是这样:
void LIN_Master_Schedule_Handler(void)
{
uint8_t current_slot = 0;
uint8_t frame_id;
uint16_t slot_time;
while(1) // 主循环
{
// 从调度表里读取当前时隙
frame_id = ScheduleTable[current_slot].FrameID;
slot_time = ScheduleTable[current_slot].SlotTime;
// 发送帧头
LIN_SendHeader(frame_id);
// 等待从节点响应(带超时)
if(LIN_WaitResponse() == LIN_OK)
{
// 数据接收成功,处理数据
LIN_ProcessData(frame_id);
}
else
{
// 超时或错误处理
LIN_ErrorHandler(frame_id);
}
// 等待间隔时间
LIN_Delay_ms(slot_time);
// 进入下一个时隙
current_slot++;
if(current_slot >= SCHEDULE_TABLE_SIZE)
{
current_slot = 0; // 循环
}
}
}
这段代码看起来简单,但实际项目里要考虑的东西很多。比如:
- 如果某个从节点一直不响应,要不要跳过?我一般会设置一个最大重试次数。
- 调度表执行过程中,能不能动态切换?可以,但要注意切换时机,最好在帧间间隔里切换。
- 多个调度表之间怎么切换?我习惯用一个状态机来管理。
关键点:调度表的执行是严格时序的。主节点不能因为某个从节点没响应就停下来等,必须按照调度表的时间往前走。否则整个总线的时间基准就乱了。
我记得有一次调试,发现总线上的数据总是偶尔跳变。查了半天,发现是主节点在等待从节点响应时,用了阻塞式等待。结果某个从节点响应慢了,后面的所有帧都跟着延迟。整个调度表的时间全乱了。后来改成非阻塞式等待,配合超时处理,问题就解决了。
嗯,调度表这块内容,说难不难,说简单也不简单。关键是要理解它的核心思想:时序驱动,主控调度。你把这个想明白了,LIN总线驱动开发就入门了一半。
下一章,咱们聊聊LIN总线的帧类型。到时候我会讲讲无条件帧、事件触发帧、诊断帧的区别,还有我在项目里是怎么选型的。