第1章:RTOS任务划分——让药盒的“大脑”有条不紊

各位同学,咱们今天聊聊嵌入式软件架构的核心——RTOS。说白了,就是怎么给智能药盒安排活儿。

我刚开始做嵌入式那会儿,用的还是裸机编程。一个超级循环里塞满各种功能,按键扫描、显示刷新、蜂鸣器控制……全挤在一起。结果呢?按键按下去,蜂鸣器要等显示刷新完才响。这在药盒上可不行——你想想看,老人按了取药键,屏幕要等半秒才亮,这体验多糟糕。

所以,我们需要RTOS。FreeRTOS是个好东西,轻量、免费、生态好。我这些年做过的项目,十有八九都用的它。

1.1 任务划分:把药盒的工作拆解清楚

任务划分是RTOS设计的第一步。我的习惯是:先列出药盒所有功能,再按“实时性要求”和“功能独立性”分组。

智能药盒通常需要这些任务:

  • 按键扫描任务:检测用户按键,去抖,识别短按/长按
  • 显示刷新任务:更新OLED或LCD屏幕内容
  • 蜂鸣器控制任务:播放提示音、报警音
  • 服药提醒任务:检查时间,触发提醒逻辑
  • 通信任务:处理蓝牙/WiFi数据收发
  • 传感器采集任务:读取药盒温湿度、药仓状态

嗯,这里要注意:任务不是越多越好。任务太多,上下文切换开销就大。我见过有人把每个LED灯都单独开一个任务,结果系统响应反而变慢了。

我的经验:任务数量控制在5-8个比较合适。功能紧密相关的可以合并,比如“蜂鸣器控制”和“LED指示”可以放在同一个任务里。

1.2 任务优先级:谁该先跑?

任务优先级设置是个技术活。我一般遵循这个原则:

  • 最高优先级:紧急安全相关任务(比如电池电量过低报警)
  • 高优先级:实时性要求高的任务(按键扫描、服药提醒)
  • 中等优先级:周期性任务(显示刷新、传感器采集)
  • 低优先级:非实时任务(通信处理、日志记录)

举个例子,药盒的“服药提醒任务”优先级必须高于“显示刷新任务”。为什么?因为提醒响了,屏幕可以晚半秒更新,但提醒不能晚。我曾经在一个项目中把显示任务优先级设高了,结果按键响应延迟,用户按了三次都没反应——这教训可深刻了。

避坑指南:优先级不要超过5级。FreeRTOS虽然支持很多级,但级数越多,调度逻辑越复杂,出bug的概率也越大。我一般只用3-4级。

1.3 消息队列:任务间的“快递员”

任务之间怎么通信?消息队列是首选。

你想想看,按键扫描任务检测到用户按了“取药”键,它需要告诉服药提醒任务:“嘿,用户要取药了,你准备一下。” 这时候,消息队列就派上用场了。

消息队列的本质是一个FIFO缓冲区。发送方往里放数据,接收方从里面取数据。双方不需要知道对方的存在,解耦性很好。

看个简单的例子:

// 定义消息结构体
typedef struct {
    uint8_t msgType;    // 消息类型:0-按键事件,1-传感器数据,2-报警事件
    uint16_t msgValue;  // 消息数值
} Msg_t;

// 创建消息队列
QueueHandle_t xMsgQueue;
xMsgQueue = xQueueCreate(10, sizeof(Msg_t));

// 发送消息(在按键扫描任务中)
Msg_t msg = {.msgType = 0, .msgValue = KEY_TAKE_MEDICINE};
xQueueSend(xMsgQueue, &msg, 0);

// 接收消息(在服药提醒任务中)
Msg_t recvMsg;
if (xQueueReceive(xMsgQueue, &recvMsg, portMAX_DELAY) == pdPASS) {
    if (recvMsg.msgType == 0) {
        // 处理按键事件
        handleKeyEvent(recvMsg.msgValue);
    }
}

我个人习惯把消息队列长度设为10-20。太短容易丢消息,太长浪费内存。药盒这种场景,10个就够用了。

1.4 信号量:协调资源的“交通灯”

信号量解决的是“资源竞争”问题。说白了,就是多个任务都想用同一个东西,怎么保证不乱套。

药盒里典型的场景:蜂鸣器只有一个,但“服药提醒任务”和“按键反馈任务”都可能要用它。如果两个任务同时操作蜂鸣器,声音就会乱掉。

这时候用二进制信号量:

// 创建信号量
SemaphoreHandle_t xBuzzerSem;
xBuzzerSem = xSemaphoreCreateBinary();

// 使用蜂鸣器前,先获取信号量
if (xSemaphoreTake(xBuzzerSem, pdMS_TO_TICKS(100)) == pdPASS) {
    // 可以安全操作蜂鸣器了
    buzzerOn();
    vTaskDelay(pdMS_TO_TICKS(200));
    buzzerOff();
    // 用完后释放
    xSemaphoreGive(xBuzzerSem);
} else {
    // 获取失败,说明蜂鸣器正被其他任务使用
    // 可以稍后重试,或者放弃本次操作
}

还有一种叫计数信号量,用于管理多个相同资源。比如药盒有4个药仓,每个药仓可以独立打开。计数信号量初始值设为4,每打开一个药仓就减1,关闭一个就加1。这样就能知道还有几个药仓可用。

关键点:信号量获取一定要设置超时时间。我见过有人用portMAX_DELAY,结果任务一直等,系统卡死。设置个100ms超时,获取不到就做其他事,这才是RTOS的思维。

1.5 实际案例:药盒的典型任务调度

好了,咱们把前面讲的串起来,看看一个完整的药盒任务调度方案:

任务名称 优先级 周期/触发方式 主要功能
按键扫描任务 每20ms触发 检测按键,去抖,发送消息到队列
服药提醒任务 消息触发 接收按键消息,检查时间,触发提醒
显示刷新任务 每100ms触发 更新屏幕内容,显示时间、药量、状态
蜂鸣器控制任务 信号量保护 播放提示音、报警音
传感器采集任务 每1秒触发 读取温湿度、药仓状态
通信任务 事件触发 处理蓝牙/WiFi数据

这个方案我实际用过,效果不错。按键响应在50ms以内,提醒延迟不超过100ms,完全满足药盒的使用需求。

最后说一句:RTOS不是万能药。如果你的药盒功能很简单,裸机编程可能更合适。但一旦功能超过5个,或者有实时性要求,RTOS就是你的好帮手。

下一章咱们聊聊内存管理——FreeRTOS的堆栈配置和内存泄漏排查,这可是很多工程师翻车的地方。