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
小技巧:周期性任务的周期最好取整,并且相互之间是整数倍关系。比如 10ms、50ms、100ms,这样调度器更容易管理,也方便做时间片轮转。

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 实现这些任务的调度,包括信号量、消息队列、事件标志组这些同步机制。到时候我会结合代码,一步步带你搭建一个完整的售货机多任务系统。