管理入口

请输入管理密码。此密码门槛仅用于前端原型,不提供真正的仓库权限保护。

0%
TI小车实战

02|非抢占式调度器:整车软件如何按节拍运行

拆解 MSPM0G35XX TI 小车的合作式任务表、执行顺序、周期预算、APP/BSP 分层和共享状态边界。

这辆车没有用 RTOS。Task.c 实现一个非抢占式任务调度器,Task_App.c 把编码器、循迹、控制、交互与诊断注册成周期任务。本篇先解释它如何工作,再说明每个模块怎样接入。

文档属性内容
文档编号TI-CAR-02
核心对象Task_Add()、Task_Start()、Task_Init()
架构类型主循环 + 系统时基 + 合作式周期任务
证据状态已由当前代码确认

1. 从 main() 看运行骨架

主函数只做三件事:

SYSCFG_DL_init();
Task_Init();
while (1) {
Task_Start(Sys_GetTick);
}

SYSCFG_DL_init() 建立外设配置,Task_Init() 初始化设备并登记任务,主循环随后不断扫描任务表。业务逻辑没有散落在 while (1) 中,而是由任务函数按周期执行。

2. 调度器保存哪些信息

每个 task_t 记录任务名称、上次唤醒时刻、执行间隔、优先级、状态、创建顺序、函数指针和参数。任务加入后按优先级排序;数值越小,越先被扫描。

当满足以下条件时,调度器直接调用任务函数:

当前时刻 - 上次唤醒时刻 >= 任务周期
并且任务状态 == ACTIVE

函数返回后,调度器更新 LastWakeUp,再检查下一项。这里没有线程栈、时间片或任务切换上下文,因而它是非抢占式调度。

3. 当前任务表

Task_Init() 中可确认的任务如下。周期来自当前配置,不代表所有硬件都必须采用同一参数。

优先级任务周期主要职责
0Encoder10 ms读取左右 QEI,计算 RPM 与累计距离
1Tracker5 ms采集有效循迹通道并更新终点模式
2ICM4268820 ms更新 IMU 与姿态;仅启用宏打开时登记
3TrackAuto20 ms自动启动循迹;仅对应宏打开时登记
4Task_PID10 ms位置/循迹目标、速度闭环和电机输出
5Buzzer10 ms非阻塞蜂鸣节拍
6Serial50 ms输出循迹诊断数据
7LED100 ms心跳指示
8OLED100 ms状态显示;OLED 初始化成功后登记
9Key20 ms按键采样、消抖和模式入口

ICM42688 当前启用宏为 0U,TrackAuto 当前也未自动启用。因此“任务表支持”与“本次构建实际登记”必须分开描述。

4. 优先级不等于抢占

优先级只决定同一次扫描中谁先执行。假设低优先级 OLED 任务已经开始运行,编码器任务即使到期,也要等 OLED 返回后才能执行。

错误理解:高优先级任务可以打断低优先级任务
实际行为:高优先级任务只会在下一次扫描时排在前面

所以调度可靠性的核心不是“把关键任务优先级设高”,而是控制每个任务的最坏执行时间。任何长循环、无超时等待或大段阻塞输出都会增加其他任务的抖动。

5. 系统时基与中断边界

SysTick_Handler() 调用 SysTick_Increasment(),增加全局时基。调度器通过 Sys_GetTick() 读取它。中断只维护时间和必要标志,主要业务仍在普通任务中完成。

ICM42688 的数据就绪中断也采用同一原则:GPIO 中断只设置 Flag_IMU_Ready 并累计次数,真正的 I²C 读取与姿态计算在 IMU 任务中执行。这能减少中断服务程序占用时间。

6. 周期任务怎样跨层协作

以速度控制为例:

  1. Encoder 任务更新 Encoder_Actual_Val[]。
  2. Tracker 或位置控制逻辑形成左右目标 RPM。
  3. Task_PID 读取目标与反馈,调用 SpeedPID_Update()。
  4. 控制输出经 Load_Motor_PWM() 进入电机 BSP。

模块通过明确的数据对象和接口协作,而不是互相调用所有细节。Encoder 任务不知道车辆处于哪个比赛阶段;TB6612 驱动也不知道 PWM 来自循迹还是位置控制。

7. APP/BSP 分层

层级代表文件关注点
应用编排Task_App.c、TrackMode_App.c任务何时开始、处于哪个阶段、何时停止
控制与规划PositionControl_App.c、SpeedTarget_App.c、PID.c目标、误差、滤波、限幅与控制量
板级驱动Motor.c、Tracker.c、ICM42688.cGPIO、PWM、QEI、I²C 与原始数据
芯片配置empty.syscfg、DriverLib外设实例、引脚复用和寄存器接口

新增模块时,应先写稳定的 BSP 接口,再写数据处理,最后通过一个短时任务接入应用层。不要让任务函数同时承担寄存器操作、算法、界面刷新和模式切换。

8. 调度器的已知工程边界

  • Task_Start() 会顺序扫描全部任务,不提供真正的并行执行。
  • Task_GetMaxUsed() 记录的是系统 tick 粒度下的最大耗时,无法替代更细粒度的逻辑分析仪或周期计数。
  • Delay() 是忙等待;若在普通任务中使用,会阻塞整个调度循环。
  • 串口格式化发送当前含阻塞发送路径,诊断输出量过大可能影响控制周期。
  • 动态周期调整和低功耗宏当前关闭,不应写成正在工作的功能。

9. 接入新任务的标准

一个合格的周期任务应满足:

  1. 输入、输出和所有权明确。
  2. 没有无限等待;外设等待必须有超时。
  3. 单次执行足够短,长流程拆成状态机。
  4. 周期来自系统需求,而不是随手填写。
  5. 失败时写入错误状态,不直接让整车卡死。
  6. 启用宏、初始化、任务登记和应用消费四条链保持一致。

本篇总结

  • 主循环反复调用 Task_Start(),到期任务按优先级依次执行并主动返回。
  • 非抢占式架构简单透明,但一个阻塞任务会拖慢整张任务表。
  • 中断负责时基和事件标志,算法与外设事务尽量留在普通任务中。
  • APP 负责整车决策,BSP 负责外设读写;控制和数据处理位于两者之间。

上一篇:01|TI 小车总览

下一篇:03|TB6612 电机驱动

输入标题、正文关键词或标签,查找文章。