拓冰建站拓冰建站
首页 / 资讯中心 / 正文

RTOS任务划分与通信方式详解:队列/信号量/互斥量/事件组实战

上一篇把二级指针的用法理了一遍回到更基础的东西。这篇聊聊 RTOS 项目里另一个高频翻车点任务划分和任务间通信。搞嵌入式开发的迟早会碰到 RTOS。项目复杂度一上来多路传感器采集、屏幕刷新、协议解析、电机控制要同时跑裸机的 while(1) 就开始力不从心。上 RTOS 是顺理成章的事。但我这些年 review 过不少项目代码发现一个普遍问题RTOS 用上了任务也创建了一堆可整个系统的任务划分和通信方式一塌糊涂。优先级随便给队列和信号量乱用任务之间耦合得死死的改一个地方牵一发动全身。说白了很多人只是把 RTOS 当成一个能跑多个 while(1)的工具没真正理解任务该怎么拆、通信该怎么选。这篇就把这两件事掰开讲清楚。任务到底该怎么拆新手最常犯的错就是按功能模块建任务——传感器一个任务、LCD 一个任务、按键一个任务、LED 一个任务、蜂鸣器一个任务……最后搞出十几个任务栈空间炸了不说调度开销也大得离谱。任务划分的核心依据不是功能模块而是实时性需求和执行周期。举个具体例子。一个工业控制器项目有这些功能ADC 采集10ms 周期PID 运算10ms 周期屏幕刷新100ms 周期按键扫描20ms 周期Modbus 通信事件驱动LED 状态指示500ms 周期按功能拆6 个任务。但仔细看ADC 采集和 PID 运算周期一样、时序上强关联完全可以合并成一个任务。LED 指示和屏幕刷新都是低优先级的显示类操作也能合并。合理的划分是这样6 个功能4 个任务每个任务职责边界清晰。记住几条原则。实时性相同的功能合到一起。周期一致、优先级一致的功能没必要分开分开只会增加通信开销。强耦合的功能合到一起。ADC 采完立刻做 PID数据在局部变量里直接传递比跨任务发消息高效得多。任务数量尽量精简。每个任务都要吃栈空间都会参与调度。在 Cortex-M0 这种小核上5 个任务和 15 个任务的调度开销差距是肉眼可见的。优先级要反映真实的紧迫程度。电机控制、安全检测这类硬实时任务给高优先级UI 刷新这种慢半拍用户也感知不到的给低优先级。任务间通信有哪些武器很多人搞不清什么场景该用什么最常见的错误就两种。该用队列的地方用了全局变量——传感器采集任务把数据往全局数组里写显示任务直接去读没有任何同步保护数据撕裂、读到半截值查起来要命。该用信号量的地方用了队列——明明只是通知一下数据准备好了非要往队列里塞个没意义的值浪费内存还增加复杂度。消息队列任务间传数据的主力凡是涉及到一个任务产生数据、另一个任务消费数据的场景优先考虑队列。队列的好处很直接生产者和消费者完全解耦速度不一致也没关系。ADC 任务 10ms 采一次显示任务 100ms 刷新一次中间有队列做缓冲各跑各的节奏。队列深度不是越大越好。如果消费速度长期跟不上生产速度队列再深也迟早满。深度一般给到正常突发量的 2 到 3 倍就够了。传指针还是传值小数据几个字节的结构体直接传值拷贝进队列简单安全。大数据比如一帧图像传指针但你必须保证数据在消费者取走之前不会被修改或释放。/* 推荐的队列消息结构体设计 */ typedef struct { uint8_t msg_id; /* 消息类型标识 */ uint8_t src_task; /* 来源任务 */ uint16_t data_len; /* 数据长度 */ union { uint32_t value; /* 小数据直接传值 */ void *ptr; /* 大数据传指针 */ } payload; } task_msg_t;信号量轻量级的拍一下肩膀信号量不传数据只做通知和同步。最经典的场景ISR 通知任务去干活。二值信号量就像一个开关只有 0 和 1。适合通知一次、处理一次的场景。注意二值信号量会丢通知如果 ISR 连续 post 了 3 次但任务只 wait 了 1 次那另外 2 次通知就丢了。计数信号量可以累加适合资源计数的场景。互斥量共享资源的保护伞互斥量解决的是互斥访问问题同一时刻只允许一个任务访问某个共享资源。互斥量相比信号量有一个关键特性优先级继承。如果低优先级任务持有锁高优先级任务在等锁RTOS 会临时把低优先级任务提升到高优先级让它尽快执行完释放锁避免优先级反转问题。核心纪律持有锁的时间尽可能短。拿到锁就干活干完立刻释放中间不要有任何阻塞操作。事件标志组多条件联合触发的利器一个任务可以同时等待多个条件全部满足AND或任一满足OR时才继续执行。典型场景系统初始化阶段主任务要等所有子模块都初始化完成后才启动业务逻辑。四种通信方式怎么选把上面四种串起来看选型其实有规律可循。要传数据用队列只通知不传数据用信号量要保护共享资源、防止优先级反转用互斥量要等多个条件组合用事件标志组。很多人卡在队列还是信号量上判断标准就一条这次通信要不要把数据带过去。要带数据队列只说一声事来了信号量。把这条记死能少踩一半坑。几条血泪教训永远不要在 ISR 里做阻塞操作。ISR 中不能调用会阻塞的 API只能用带 FromISR 后缀的非阻塞版本。在 ISR 里等一个信号量整个系统就卡死了。警惕死锁。项目规范里必须约定加锁顺序所有任务按同样的顺序获取多把锁。A 任务先拿锁 1 再拿锁 2B 任务却先拿锁 2 再拿锁 1互相等对方手里的锁死锁就来了。给通信操作加超时。不要用永久等待加超时后记录日志、做异常处理至少系统还能运转。一个任务永久卡在一个队列上你看不出来但它已经死了。监控任务栈使用情况。大多数 RTOS 都提供栈水位查询的 API调试阶段一定要用起来。栈溢出是最阴险的 bug往往跑到某个不确定的时刻才崩查起来要命。别把通信做成蜘蛛网。好的设计应该有清晰的层次和流向。每个任务知道该跟谁通信、不该跟谁通信而不是所有任务都能往所有队列里塞东西。写在最后回头看这些问题的本质——任务怎么拆分、通信方式怎么选、如何避免耦合——你会发现它们的本质并不是 RTOS 的 API 怎么调用而是软件架构该怎么设计。API 文档谁都能翻但为什么有的人写出来的 RTOS 项目结构清晰、稳定运行有的人写出来的代码改一处崩三处差别就在于对设计的理解深度。RTOS 给了你多任务的能力但没告诉你怎么拆任务。这件事得靠架构思维来补。有用的话点个在看让更多还在跟 RTOS 任务划分较劲的嵌入式工程师看到。嵌入式 RTOS FreeRTOS 任务划分 消息队列 信号量 互斥量
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门