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,持续多少毫秒。
我在项目中遇到过一种情况:工程师把槽位持续时间算错了,导致帧与帧之间产生了重叠。结果呢?从节点还没来得及回复上一帧,下一帧的帧头就来了,整个网络乱成一锅粥。
配置调度表时,我建议你关注以下几点:
- 帧持续时间:这个时间要大于等于帧的最大传输时间。LIN帧最大是10个字节(2个同步+1个ID+8个数据+1个校验),按20kbps算,大约需要5ms。我一般会留20%的余量,设成6ms。
- 槽位顺序:把紧急的帧放在前面。比如安全气囊的帧,肯定比车窗升降的帧优先级高。
- 空闲槽位:在调度表末尾留几个空闲槽位,用于诊断或临时任务。
下面是一个典型的调度表配置示例:
// 调度表配置结构体
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 调度表执行
调度表的执行,其实就是主节点的一个定时中断服务程序。
主节点内部维护一个指针,指向当前槽位。定时器每触发一次,指针就往前走一步。如果走到表尾,就回到表头重新开始。
执行流程大致如下:
- 定时器中断触发
- 读取当前槽位的帧ID
- 发送帧头
- 等待从节点回复(或超时)
- 更新指针到下一个槽位
- 设置下一次中断的时间
嗯,这里要注意:等待从节点回复的时间不能超过槽位时间。如果从节点没回复,主节点也要在槽位时间内放弃等待,继续执行下一个槽位。否则整个调度表就会卡住。
我曾经在一个项目中,从节点因为电源不稳定偶尔丢帧。主节点傻傻地等它回复,结果后面的帧全部延迟了。后来我加了一个超时机制:如果从节点在槽位时间的80%内没回复,直接跳过。
// 调度表执行伪代码
void Schedule_Execute(void) {
ScheduleSlot_t *slot = ¤tSchedule[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的诊断功能,那可是排查故障的利器。