这辆车没有用 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() 中可确认的任务如下。周期来自当前配置,不代表所有硬件都必须采用同一参数。
| 优先级 | 任务 | 周期 | 主要职责 |
|---|---|---|---|
| 0 | Encoder | 10 ms | 读取左右 QEI,计算 RPM 与累计距离 |
| 1 | Tracker | 5 ms | 采集有效循迹通道并更新终点模式 |
| 2 | ICM42688 | 20 ms | 更新 IMU 与姿态;仅启用宏打开时登记 |
| 3 | TrackAuto | 20 ms | 自动启动循迹;仅对应宏打开时登记 |
| 4 | Task_PID | 10 ms | 位置/循迹目标、速度闭环和电机输出 |
| 5 | Buzzer | 10 ms | 非阻塞蜂鸣节拍 |
| 6 | Serial | 50 ms | 输出循迹诊断数据 |
| 7 | LED | 100 ms | 心跳指示 |
| 8 | OLED | 100 ms | 状态显示;OLED 初始化成功后登记 |
| 9 | Key | 20 ms | 按键采样、消抖和模式入口 |
ICM42688 当前启用宏为 0U,TrackAuto 当前也未自动启用。因此“任务表支持”与“本次构建实际登记”必须分开描述。
4. 优先级不等于抢占
优先级只决定同一次扫描中谁先执行。假设低优先级 OLED 任务已经开始运行,编码器任务即使到期,也要等 OLED 返回后才能执行。
错误理解:高优先级任务可以打断低优先级任务实际行为:高优先级任务只会在下一次扫描时排在前面所以调度可靠性的核心不是“把关键任务优先级设高”,而是控制每个任务的最坏执行时间。任何长循环、无超时等待或大段阻塞输出都会增加其他任务的抖动。
5. 系统时基与中断边界
SysTick_Handler() 调用 SysTick_Increasment(),增加全局时基。调度器通过 Sys_GetTick() 读取它。中断只维护时间和必要标志,主要业务仍在普通任务中完成。
ICM42688 的数据就绪中断也采用同一原则:GPIO 中断只设置 Flag_IMU_Ready 并累计次数,真正的 I²C 读取与姿态计算在 IMU 任务中执行。这能减少中断服务程序占用时间。
6. 周期任务怎样跨层协作
以速度控制为例:
- Encoder 任务更新
Encoder_Actual_Val[]。 - Tracker 或位置控制逻辑形成左右目标 RPM。
- Task_PID 读取目标与反馈,调用
SpeedPID_Update()。 - 控制输出经
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.c | GPIO、PWM、QEI、I²C 与原始数据 |
| 芯片配置 | empty.syscfg、DriverLib | 外设实例、引脚复用和寄存器接口 |
新增模块时,应先写稳定的 BSP 接口,再写数据处理,最后通过一个短时任务接入应用层。不要让任务函数同时承担寄存器操作、算法、界面刷新和模式切换。
8. 调度器的已知工程边界
Task_Start()会顺序扫描全部任务,不提供真正的并行执行。Task_GetMaxUsed()记录的是系统 tick 粒度下的最大耗时,无法替代更细粒度的逻辑分析仪或周期计数。Delay()是忙等待;若在普通任务中使用,会阻塞整个调度循环。- 串口格式化发送当前含阻塞发送路径,诊断输出量过大可能影响控制周期。
- 动态周期调整和低功耗宏当前关闭,不应写成正在工作的功能。
9. 接入新任务的标准
一个合格的周期任务应满足:
- 输入、输出和所有权明确。
- 没有无限等待;外设等待必须有超时。
- 单次执行足够短,长流程拆成状态机。
- 周期来自系统需求,而不是随手填写。
- 失败时写入错误状态,不直接让整车卡死。
- 启用宏、初始化、任务登记和应用消费四条链保持一致。
本篇总结
- 主循环反复调用
Task_Start(),到期任务按优先级依次执行并主动返回。 - 非抢占式架构简单透明,但一个阻塞任务会拖慢整张任务表。
- 中断负责时基和事件标志,算法与外设事务尽量留在普通任务中。
- APP 负责整车决策,BSP 负责外设读写;控制和数据处理位于两者之间。
上一篇:01|TI 小车总览
下一篇:03|TB6612 电机驱动