裸机程序设计模式
裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。前面三种方法都无法解决一个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。第4种方法可以解决这个问题,但是实践起来有难度。
状态机模式之所以能解决耗时函数相互影响的问题,关键在于它将程序的执行流程拆解为独立的状态片段,并通过事件驱动切换状态,从而实现“分时处理”。以下是通俗解释:
-
前三种模式的缺陷
- 轮询:像不断打电话问“轮到A/B了吗?”,两个函数必须互相等待对方完全执行完毕。
- 前后台:类似中断处理,但若A/B本身耗时,仍会长时间占用CPU(比如A执行时,B只能干等)。
- 定时器驱动:虽然能分时调度,但若A/B内部需要保持执行进度(比如解析数据包),定时器无法保存中间状态。
-
状态机的解决原理
- 拆解为状态:把A/B两个耗时函数分解成多个小步骤,例如:
每个步骤对应一个独立状态。A步骤1 → 保存进度 → 切换执行B步骤1 → 保存进度 → 切换回A步骤2... - 非阻塞切换:每次只执行当前状态对应的代码片段(比如处理一个数据块),完成后立即释放CPU,通过事件触发下一状态。这类似于“流水线工人轮流处理多道工序”。
- 状态记忆:通过变量记录当前进度(如解析到数据包第几位),下次切换回来时能继续执行。
- 拆解为状态:把A/B两个耗时函数分解成多个小步骤,例如:
-
实践难点举例
- 状态爆炸:若A/B各有10个步骤,组合后可能产生100种状态,需合理抽象(例如用“解析中”“等待响应”等通用状态代替具体步骤)。
- 事件管理:需明确触发状态切换的条件(如定时器到期、传感器信号),类似“红绿灯控制不同方向车流”。
- 调试复杂度:执行流程分散在不同状态中,需用工具跟踪状态跳转路径。
类比理解:想象同时煮面和炒菜。传统方法(轮询/前后台)会先煮完面再炒菜,面可能煮糊;状态机则是:下面→切菜→搅拌面→炒菜→捞面→加调料… 通过快速切换保证两者同步进行且不烧焦。
多任务系统
2. 状态机模式的改进与局限
状态机通过“拆分任务为小步骤”来缓解问题:
- 原理:将A和B拆解为多个小步骤(例如A拆成3步,B拆成3步),每次执行一个步骤后保存进度,下次继续执行后续步骤。例如:
每次执行时只完成一个步骤,然后切换任务。// A的步骤:舀饭 → 喂饭 → 舀菜 → 喂菜(循环) // B的步骤:查看信息 → 打字 → 发送(循环) - 优势:缩短单次执行时间,降低A和B之间的阻塞风险。
- 局限性:
- 拆分难度高:某些任务(如复杂的数据处理)难以拆分为独立步骤,强行拆分可能导致逻辑混乱。
- 状态爆炸:如果A和B各有10个步骤,组合后可能产生100种状态,代码复杂度剧增。
- 时间不可控:若某一步骤意外耗时(如等待硬件响应),仍可能影响其他任务。
类比:妈妈把喂饭拆解为“舀饭、喂饭、舀菜、喂菜”,打字拆解为“看信息、打字、发送”,快速切换步骤。但如果某一步耗时太久(如喂饭时孩子不张嘴),整体流程仍会卡住。
3. 根本矛盾:裸机的单线程本质
无论使用哪种模式,裸机程序最终依赖单核CPU轮流执行代码。即使通过状态机拆分任务,本质仍是“分时复用CPU”:
- A和B的步骤交叉执行:例如先执行A的步骤1 → B的步骤1 → A的步骤2 → B的步骤2,但若总时间超出预期(如所有步骤耗时之和超过一个周期),系统仍会崩溃。
- 无法真正并行:裸机缺乏多任务调度机制,无法像操作系统那样为任务分配独立资源。
类比:妈妈虽然能快速切换步骤,但脑子只能想一件事。如果步骤太多或太复杂,切换速度跟不上,任务仍会积压。
4. 解决方案:从状态机到多任务系统
当状态机也无法满足需求时,需转向多任务系统(如RTOS):
- 核心思想:操作系统为每个任务分配独立“虚拟CPU”,通过快速切换(如毫秒级)模拟并行执行。
- 优势:
- 自动调度:无需手动拆分步骤,任务函数可保持完整逻辑。
- 优先级控制:紧急任务可抢占CPU资源(如喂饭优先于回信息)。
- 代价:需引入任务管理、资源互斥等复杂度。
类比:妈妈请了一个助手,两人分工合作(一个喂饭,一个打字),助手负责协调两人节奏,确保任务看似同时进行。
总结
- 裸机极限:状态机是裸机的终极优化模式,但受限于单线程本质,无法彻底解决耗时任务相互影响的问题。
- 升级方向:当任务复杂度超过人工拆分能力时,需使用多任务系统(RTOS),通过操作系统调度实现“伪并行”。
互斥操作
在基于RTOS的多任务系统中,直接使用全局变量来实现串口互斥访问存在严重问题。以下是通俗解释和代码示例分析:
一、全局变量方案的致命缺陷
假设用全局变量 bool is_busy 作为互斥标志,代码逻辑可能是:
// 任务A和任务B共用的代码逻辑
if (is_busy == false) { // 1.检查是否空闲
is_busy = true; // 2.标记为占用
send_data_via_uart(); // 3.发送数据
is_busy = false; // 4.释放资源
}
问题本质:
-
原子性缺失
操作检查变量 → 修改变量不是原子操作。若任务A刚执行完第1步(检查为false),还未执行第2步时被高优先级任务B抢占,任务B同样会看到is_busy == false,导致两个任务同时操作串口。 -
优先级反转风险
若低优先级任务A占用了串口,高优先级任务B将无限循环等待,导致系统响应异常。 -
阻塞机制缺失
全局变量无法让任务在资源被占用时主动让出CPU,只能通过轮询消耗CPU资源。
二、正确的解决方案:互斥量(Mutex)
示例代码(基于FreeRTOS):
SemaphoreHandle_t uart_mutex = xSemaphoreCreateMutex(); // 创建互斥量
// 任务A和任务B的发送逻辑
void send_task(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(uart_mutex, portMAX_DELAY) == pdTRUE) { // 加锁
send_data_via_uart(); // 安全使用串口
xSemaphoreGive(uart_mutex); // 解锁
}
}
}
核心优势:
- 原子性保障
互斥量的Take和Give是原子操作,不会被任务切换打断。 - 自动阻塞
资源被占用时,任务会进入阻塞状态,释放CPU给其他任务。 - 优先级继承
若低优先级任务占用资源,系统会临时提升其优先级,避免高优先级任务无限等待。
三、替代方案:队列(Queue)
若任务频繁发送数据,可用队列解耦操作:
QueueHandle_t uart_queue = xQueueCreate(10, sizeof(data_t)); // 创建队列
// 发送任务(任务A/B):
xQueueSend(uart_queue, &data, portMAX_DELAY); // 数据入队
// 专用串口发送任务:
void uart_sender_task(void *pvParameters) {
data_t buf;
for (;;) {
if (xQueueReceive(uart_queue, &buf, portMAX_DELAY) == pdTRUE) {
send_data_via_uart(&buf); // 从队列取出数据并发送
}
}
}
优势:
- 所有发送请求由队列统一管理,避免直接操作硬件冲突
- 天然实现“先到先处理”的公平性
四、总结对比
| 方案 | 全局变量 | 互斥量 | 队列 |
|---|---|---|---|
| 线程安全 | ❌ 易冲突 | ✔️ 原子操作 | ✔️ 无需锁 |
| CPU利用率 | ❌ 轮询耗资源 | ✔️ 自动阻塞 | ✔️ 自动阻塞 |
| 适用场景 | 无 | 低频独占操作 | 高频数据发送 |
结论:
在RTOS中,绝对不要用全局变量实现互斥,优先选择互斥量或队列。
同步操作
问题本质
当任务B需要依赖任务A的结果时,如果任务B采用"轮询检查"的方式等待(比如不断循环查询某个变量),会导致以下问题:
- CPU资源浪费:任务B在等待期间持续占用CPU,但实际没有做有意义的工作(类似员工不断打电话问同事“好了吗”)。
- 数据不一致风险:任务A可能尚未完成关键操作,任务B就读取了中间状态的脏数据(比如文件只写入了一半就被读取)。
- 优先级反转:若任务B优先级更高,它会不断抢占CPU,反而让任务A无法尽快完成任务,形成死循环。
错误代码示例(轮询浪费型)
// 全局变量作为标志位
volatile int task_a_done = 0;
void task_a() {
// 执行耗时操作(如写入文件)
write_data_to_file();
task_a_done = 1; // 完成后设置标志
}
void task_b() {
while (1) {
if (task_a_done) { // ❌ 不断循环检查标志
process_file_data(); // 处理文件
break;
}
// 空循环浪费CPU
}
}
正确解决方案
方法1:事件通知(如FreeRTOS信号量)
SemaphoreHandle_t semaphore = xSemaphoreCreateBinary(); // 创建信号量
void task_a() {
write_data_to_file();
xSemaphoreGive(semaphore); // ✅ 完成后发送信号(类似按铃通知)
}
void task_b() {
xSemaphoreTake(semaphore, portMAX_DELAY); // ✅ 阻塞等待信号
process_file_data(); // 收到信号后才开始处理
}
优势:
- 任务B在等待
通俗解释:FreeRTOS信号量如何实现事件通知?
1. 什么是信号量?
信号量就像是一个共享资源的计数器,用来管理多个任务(或中断)对资源的访问或事件的通知。比如:
- 停车场比喻(计数信号量):假设停车场有100个车位(共享资源),每辆车进入时,剩余车位数量(信号量值)减1;离开时加1。当车位为0时,新来的车必须等待(任务阻塞),直到有车离开(信号量释放)。
- 公共电话比喻(二值信号量):电话只能一人使用,状态为“占用”(信号量值0)或“空闲”(信号量值1)。当电话空闲时,你可以使用它;若被占用,你必须等待对方挂断(信号量释放)。
2. 信号量如何用于事件通知?
假设场景:任务A需要等待某个事件(如按键按下)发生后再执行。
- 步骤:
- 初始化信号量:创建一个初始值为0的二值信号量(例如
xSemaphoreCreateBinary())。 - 任务A等待事件:任务A调用
xSemaphoreTake()获取信号量。由于初始值为0,任务A会进入阻塞状态(类似“睡觉”)。 - 事件触发:当按键按下时(比如在中断服务函数中),调用
xSemaphoreGiveFromISR()释放信号量。 - 任务A被唤醒:信号量值变为1,任务A立即获取信号量并开始执行后续操作(如处理按键事件)。
- 初始化信号量:创建一个初始值为0的二值信号量(例如
通俗类比:
- 任务A像在等快递的人,快递(事件)没到时就睡觉(阻塞)。
- 快递员(中断)送快递时按门铃(释放信号量),人醒来签收(处理事件)。
3. 信号量与其他机制的区别
- 与队列的区别:信号量不传递数据,只传递事件发生的标志。比如队列是“传递包裹”,信号量是“按门铃通知”。
- 与任务通知的区别:任务通知更轻量(直接通过任务控制块通信),但信号量更适合多任务竞争的场景。
4. 实际应用场景
- 中断与任务同步:传感器数据到达时,中断释放信号量,任务处理数据。
- 任务间协作:任务B完成任务后释放信号量,通知任务A开始下一步操作。
- 资源互斥:多任务共享打印机时,通过信号量确保同一时间只有一个任务使用。
5. 注意事项
- 优先级继承(仅互斥信号量):若高优先级任务等待低优先级任务释放信号量,系统会临时提升低优先级任务的优先级,避免“优先级反转”问题。
- 避免死锁:确保信号量释放逻辑正确,防止任务永远阻塞。
通过信号量,FreeRTOS可以高效协调任务与事件的关系,就像用“门铃”和“计数器”管理现实中的排队问题一样简单直观。
如果任务之间有依赖关系,比如任务A执行了某个操作之后,需要任务B进行后续的处理。如果代码如下编写的话,任务B大部分时间做的都是无用功。
优先级反转的本质是:高优先级任务需要低优先级任务生产的资源,但中优先级任务通过抢占CPU,导致低优先级任务无法完成资源生产,进而使高优先级任务无限等待。这与共享资源的使用场景密切相关,以下通过更贴切的例子说明:
1. 场景修正(资源生产视角)
- 老板(任务A,高优先级):需要一份由实习生(任务C)生成的报表(资源)才能继续工作。
- 实习生(任务C,低优先级):正在生成报表(需要CPU时间),但尚未完成。
- 普通员工(任务B,中优先级):执行与报表无关的报销单打印任务(占用CPU)。
问题发生过程:
- 任务C开始生产资源:实习生正在生成报表(占用CPU),但生成到一半时……
- 任务B抢占CPU:普通员工(B)因优先级高于实习生(C),直接抢占CPU,开始打印报销单。
- 任务A被阻塞:老板(A)需要等待实习生(C)完成报表,但C的CPU时间被B持续抢占,无法完成资源生产。
- 死循环形成:
- A等待C的报表 → C无法执行(被B抢占) → B无关任务持续运行 → A永远等不到资源。
关键矛盾:
- 资源生产依赖低优先级任务:高优先级任务A需要C生产的资源,但C的CPU时间被中优先级任务B剥夺,导致资源无法产出。
- 系统规则漏洞:CPU调度仅根据优先级分配,未考虑资源生产的依赖性。
2. 为什么老板不能直接“帮”实习生生成报表?
- 资源隔离原则:系统要求任务必须独立完成资源操作,否则会导致数据混乱(如报表生成一半被其他任务修改)。
- 优先级与资源的解耦:高优先级任务可抢占CPU,但无法直接操作其他任务未完成的资源(需等待完整性)。
3. 解决方案:打破中优先级的干扰
方法1:优先级继承(Priority Inheritance)
- 操作:当老板(A)等待实习生(C)时,系统将C的优先级临时提升到与A相同。
- 效果:中优先级任务B无法再抢占C的CPU时间,C快速生成报表,释放资源给A。
- 比喻:老板说:“实习生现在和我一样重要,其他人不许打断他!”
方法2:优先级天花板(Priority Ceiling)
- 操作:规定“生成报表的任务自动获得最高优先级”。
- 效果:实习生(C)一开始生成报表,优先级直接超过B,B无法干扰。
FreeRTOS源码概述
1. 源码结构:像积木一样分层搭建
FreeRTOS的源码像一套积木,分为三个大块:
- 核心积木(内核代码):
tasks.c:管理所有任务(比如老板、员工的工作流程),负责创建、删除、调度任务。queue.c:管“传纸条”,任务之间通过队列发送消息(比如老板让实习生交报表)。list.c:内核的“任务清单”,用链表管理任务、定时器等对象的排队顺序。timers.c:软件定时器,像闹钟一样提醒任务该做什么事。
- 硬件适配积木(移植层):
port.c和portmacro.h:针对不同芯片(如ARM、51单片机)写的适配代码,比如怎么切换任务、处理中断。heap_x.c(如heap_4.c):管内存分配,像“分糖果”一样给任务分内存(x代表不同分配策略)。
- 配置文件:
FreeRTOSConfig.h:像“开关面板”,决定用哪些功能(比如要不要用信号量、任务最多几个优先级)。
2. 核心机制:像公司管理一样分工明确
- 任务管理:每个任务是一个独立员工
- 每个员工(任务)有自己的工作清单(堆栈)和优先级(老板>员工>实习生)。
- 调度器像“值班经理”,根据优先级决定谁先干活(高优先级任务能打断低优先级)。
- 通信机制:任务之间传话用“小纸条”
- 队列(Queue):传数据纸条(比如传感器数据)。
- 信号量(Semaphore):举个牌子表示资源是否可用(比如打印机是否空闲)。
- 事件组(Event Group):挂个公告板,任务贴通知(比如“报表已生成”)。
- 内存管理:分糖果还是固定配额?
heap_1:简单粗暴,一次性分完所有内存,适合不删任务的系统。heap_4:动态分配,像自动回收垃圾,避免内存碎片。
3. 移植与配置:像换手机壳一样适配不同芯片
- 移植步骤:
- 复制对应芯片的
port.c和portmacro.h(比如ARM芯片用ARM_CM3文件夹下的文件)。 - 选一个内存分配策略(如
heap_4.c)。 - 改
FreeRTOSConfig.h:调时钟频率、任务最大优先级数等参数。
- 复制对应芯片的
- 配置示例:
#define configTICK_RATE_HZ 1000 // 系统心跳=1毫秒一次 #define configMAX_PRIORITIES 10 // 最多10个优先级(老板到实习生等级) #define configTOTAL_HEAP_SIZE 4096 // 内存池大小=4KB
4. 代码规范:像写作文一样严格
FreeRTOS的代码遵循MISRA-C安全规范:
- 变量命名:前缀表示类型,比如
c是字符,x是自定义类型(如xTaskHandle)。 - 安全设计:
- 任务切换用汇编实现,保证速度(见
port.c里的汇编代码)。</
- 任务切换用汇编实现,保证速度(见

最低0.47元/天 解锁文章
792

被折叠的 条评论
为什么被折叠?



