2. 任务模型建立:识别售货机中的周期性任务与突发性任务
好,咱们进入第二章。这一章很关键,说白了就是给售货机里的各种活儿「排班」。你想想看,一台售货机要同时干多少事?电机要转、支付要等、出货要检测、异常要处理……这些任务如果不能合理分类、排好优先级,系统迟早要出乱子。
我个人习惯,拿到一个嵌入式项目,第一件事不是写代码,而是先把任务模型画出来。哪些任务是周期性的,哪些是突发性的,心里要有数。这就像厨师做菜,得先知道哪些菜要慢炖、哪些要爆炒,才能安排好灶头。
2.1 周期性任务:那些「雷打不动」的活儿
周期性任务,就是每隔固定时间就要执行一次的任务。它们像心跳一样,不能停,也不能乱。
电机控制任务:这是售货机最核心的周期性任务之一。电机转动需要精确的步进控制,通常每 10ms 或 20ms 就要更新一次脉冲信号。我在项目中遇到过,如果电机控制任务的周期抖动超过 5ms,步进电机就会发出刺耳的噪音,甚至丢步。所以这个任务的周期必须稳定,优先级也要给高一些。
支付轮询任务:支付模块(比如扫码器、硬币器)需要定期查询状态。一般每 50ms 轮询一次就够了。为什么不是 10ms?因为支付模块的响应时间通常在 100ms 级别,轮询太快反而浪费 CPU。嗯,这里要注意,轮询间隔不能太长,否则用户扫码后半天没反应,体验就很差了。
我一般这样定义周期性任务:
// 任务周期定义(单位:ms)
#define TASK_MOTOR_PERIOD 10 // 电机控制:10ms
#define TASK_PAY_POLL_PERIOD 50 // 支付轮询:50ms
#define TASK_DISPLAY_REFRESH 100 // 屏幕刷新:100ms
#define TASK_TEMP_MONITOR 500 // 温度监控:500ms
你看,不同任务的周期差异很大。把它们放在一张表里,一目了然:
| 任务名称 | 周期 | 最大执行时间 | 优先级 |
|---|---|---|---|
| 电机控制 | 10ms | 2ms | 高 |
| 支付轮询 | 50ms | 5ms | 中 |
| 屏幕刷新 | 100ms | 10ms | 低 |
| 温度监控 | 500ms | 3ms | 低 |
2.2 突发性任务:那些「说来就来」的活儿
突发性任务,就是平时不干活,一旦触发就必须马上响应。它们像消防员,平时待命,火警一响就得冲出去。
出货反馈任务:当电机转动到位后,出货传感器会触发一个中断。这时候需要立即读取传感器状态,确认货物是否掉落。这个任务必须在 1ms 内响应,否则可能错过传感器信号。我曾经因为出货反馈延迟,导致系统误判「出货失败」,用户付了钱却没拿到货——这可是大事故。
异常处理任务:比如门被打开、硬币器卡币、温度过高……这些异常事件一旦发生,系统需要立即进入安全模式。我建议把异常处理设计成最高优先级,因为它关系到设备安全和用户体验。
突发性任务通常由中断触发,然后在中断服务程序(ISR)中发送信号量或事件标志,唤醒对应的任务。举个例子:
// 出货传感器中断服务程序
void EXTI_IRQHandler(void) {
// 清除中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
// 发送信号量,唤醒出货反馈任务
osSemaphoreRelease(sem_delivery_feedback);
}
// 出货反馈任务
void Task_DeliveryFeedback(void *arg) {
while(1) {
// 等待信号量
osSemaphoreAcquire(sem_delivery_feedback, osWaitForever);
// 读取传感器,确认出货
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == SET) {
// 出货成功
update_inventory();
} else {
// 出货失败,触发异常处理
osEventFlagsSet(evt_abnormal, FLAG_DELIVERY_FAIL);
}
}
}
2.3 任务优先级划分:谁先谁后,心里要有杆秤
任务优先级划分,说白了就是决定「谁先跑,谁后跑」。我见过不少新手,把所有任务都设成同一个优先级,结果系统跑起来后,电机控制被支付轮询打断,电机发出「咔咔咔」的异响——这就是优先级没分好。
我一般遵循这几个原则:
- 实时性要求越高,优先级越高。电机控制 10ms 周期,必须稳定,优先级给最高。
- 突发性任务优先级高于周期性任务。因为突发任务「过了这村就没这店」,比如出货反馈,错过一次就可能导致出货失败。
- 非关键任务优先级放低。比如屏幕刷新、温度监控,偶尔延迟几十毫秒没关系。
来看看我常用的优先级分配方案:
| 优先级 | 任务类型 | 任务举例 |
|---|---|---|
| 最高(0) | 异常处理 | 门禁检测、过热保护 |
| 高(1) | 关键周期性任务 | 电机控制 |
| 中(2) | 突发性任务 | 出货反馈 |
| 低(3) | 普通周期性任务 | 支付轮询、屏幕刷新 |
| 最低(4) | 后台任务 | 日志记录、数据统计 |
2.4 避坑指南:我踩过的那些坑
讲到这里,我想分享几个我亲身踩过的坑,希望能帮你少走弯路。
坑一:优先级反转。我曾经把电机控制设成高优先级,支付轮询设成低优先级,但两者共享一个 I2C 总线。结果电机控制任务在等 I2C 锁的时候,被一个中等优先级的任务抢占了,导致电机控制任务迟迟拿不到锁——这就是经典的优先级反转。解决办法是使用优先级继承协议,或者干脆把共享资源的访问做成互斥的。
坑二:突发任务「饿死」。有一次我把异常处理任务的优先级设得不够高,结果系统在高负载下,异常事件来了却迟迟得不到处理,最后设备过热保护都没触发。从那以后,我规定:所有与安全相关的突发任务,优先级必须是最高的。
坑三:周期任务抖动。电机控制任务虽然优先级高,但如果它的执行时间太长(比如超过 2ms),就会影响其他任务的调度。我后来给每个周期性任务加了「执行时间监控」,一旦发现超时,就记录日志并报警。这样就能及时发现并优化。
嗯,任务模型建立这块,今天就先聊到这儿。下一章咱们会深入讲如何用 FreeRTOS 实现这些任务的调度,包括信号量、消息队列、事件标志组这些同步机制。到时候我会结合代码,一步步带你搭建一个完整的售货机多任务系统。