第1章:RTOS任务划分——让药盒的“大脑”有条不紊
各位同学,咱们今天聊聊嵌入式软件架构的核心——RTOS。说白了,就是怎么给智能药盒安排活儿。
我刚开始做嵌入式那会儿,用的还是裸机编程。一个超级循环里塞满各种功能,按键扫描、显示刷新、蜂鸣器控制……全挤在一起。结果呢?按键按下去,蜂鸣器要等显示刷新完才响。这在药盒上可不行——你想想看,老人按了取药键,屏幕要等半秒才亮,这体验多糟糕。
所以,我们需要RTOS。FreeRTOS是个好东西,轻量、免费、生态好。我这些年做过的项目,十有八九都用的它。
1.1 任务划分:把药盒的工作拆解清楚
任务划分是RTOS设计的第一步。我的习惯是:先列出药盒所有功能,再按“实时性要求”和“功能独立性”分组。
智能药盒通常需要这些任务:
- 按键扫描任务:检测用户按键,去抖,识别短按/长按
- 显示刷新任务:更新OLED或LCD屏幕内容
- 蜂鸣器控制任务:播放提示音、报警音
- 服药提醒任务:检查时间,触发提醒逻辑
- 通信任务:处理蓝牙/WiFi数据收发
- 传感器采集任务:读取药盒温湿度、药仓状态
嗯,这里要注意:任务不是越多越好。任务太多,上下文切换开销就大。我见过有人把每个LED灯都单独开一个任务,结果系统响应反而变慢了。
1.2 任务优先级:谁该先跑?
任务优先级设置是个技术活。我一般遵循这个原则:
- 最高优先级:紧急安全相关任务(比如电池电量过低报警)
- 高优先级:实时性要求高的任务(按键扫描、服药提醒)
- 中等优先级:周期性任务(显示刷新、传感器采集)
- 低优先级:非实时任务(通信处理、日志记录)
举个例子,药盒的“服药提醒任务”优先级必须高于“显示刷新任务”。为什么?因为提醒响了,屏幕可以晚半秒更新,但提醒不能晚。我曾经在一个项目中把显示任务优先级设高了,结果按键响应延迟,用户按了三次都没反应——这教训可深刻了。
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。这样就能知道还有几个药仓可用。
1.5 实际案例:药盒的典型任务调度
好了,咱们把前面讲的串起来,看看一个完整的药盒任务调度方案:
| 任务名称 | 优先级 | 周期/触发方式 | 主要功能 |
|---|---|---|---|
| 按键扫描任务 | 高 | 每20ms触发 | 检测按键,去抖,发送消息到队列 |
| 服药提醒任务 | 高 | 消息触发 | 接收按键消息,检查时间,触发提醒 |
| 显示刷新任务 | 中 | 每100ms触发 | 更新屏幕内容,显示时间、药量、状态 |
| 蜂鸣器控制任务 | 中 | 信号量保护 | 播放提示音、报警音 |
| 传感器采集任务 | 低 | 每1秒触发 | 读取温湿度、药仓状态 |
| 通信任务 | 低 | 事件触发 | 处理蓝牙/WiFi数据 |
这个方案我实际用过,效果不错。按键响应在50ms以内,提醒延迟不超过100ms,完全满足药盒的使用需求。
最后说一句:RTOS不是万能药。如果你的药盒功能很简单,裸机编程可能更合适。但一旦功能超过5个,或者有实时性要求,RTOS就是你的好帮手。
下一章咱们聊聊内存管理——FreeRTOS的堆栈配置和内存泄漏排查,这可是很多工程师翻车的地方。