1. 课程导论:为什么需要架构设计?从裸机到事件驱动再到RTOS的演进之路
做嵌入式开发这些年,我见过太多项目从「能跑就行」变成「改不动了」。
你想想看,一个产品刚立项时,功能就那么几个。工程师三下五除二,在 main 函数里写个死循环,轮询一下按键,查一下传感器,完事。产品跑起来了,老板很开心。
但三个月后呢?需求翻了一倍。要加蓝牙,要加 OTA,要低功耗。原来的代码就像一团乱麻——改一个地方,崩三个地方。这时候你才意识到:架构设计不是锦上添花,是救命稻草。
1.1 裸机编程:最直接的「能跑就行」
裸机编程,说白了就是没有操作系统。一个 while(1) 循环,把所有的活儿都堆在里面。
// 典型的裸机轮询架构
int main(void) {
system_init();
while (1) {
scan_key(); // 扫描按键
read_sensor(); // 读取传感器
update_display();// 更新屏幕
delay_ms(10); // 延时
}
}
这种写法有什么问题?
- 实时性差:按键扫描必须等传感器读完才能执行。如果传感器读取花了 50ms,按键响应就慢了 50ms。
- 功耗高:CPU 一直在跑,没法进入休眠。我在一个智能门锁项目里就吃过这个亏——裸机轮询,电池撑不过两周。
- 扩展性差:加一个新功能,就得往主循环里塞一段代码。塞多了,循环周期越来越长,系统越来越卡。
1.2 中断与前后台系统:第一次「架构觉醒」
为了解决实时性问题,前辈们想出了一个办法:用中断处理紧急事件,主循环处理后台任务。这就是前后台系统(Foreground/Background)。
// 前后台系统示意
void EXTI_IRQHandler(void) {
// 前台:中断服务,处理紧急事件
key_pressed = 1;
key_value = read_gpio();
}
int main(void) {
while (1) {
// 后台:主循环,处理非紧急任务
if (key_pressed) {
handle_key(key_value);
key_pressed = 0;
}
update_display();
system_sleep(); // 没事做就睡觉
}
}
这个架构比裸机轮询进步了一大截:
- 紧急事件(按键、通信)由中断实时响应
- 主循环可以休眠,降低功耗
- 任务之间通过标志位通信,结构清晰了一些
但问题也来了:中断多了怎么办? 我在一个多传感器采集项目里,用了 6 个中断。每个中断里都要处理数据,中断嵌套一多,栈溢出、数据竞争、优先级反转……调试起来简直要命。
1.3 事件驱动架构:让系统「按需工作」
前后台系统虽然解决了实时响应,但任务之间的协作还是靠全局标志位。代码一多,标志位满天飞,你根本搞不清哪个标志位被谁改了。
这时候,事件驱动架构登场了。
核心思想很简单:系统不主动轮询,而是等待事件发生。事件来了,查一下事件表,找到对应的处理函数,执行完继续睡觉。
// 事件驱动架构示意
typedef enum {
EVT_KEY_PRESS,
EVT_SENSOR_READY,
EVT_TIMER_TICK,
EVT_UART_RX
} event_id_t;
typedef struct {
event_id_t id;
uint32_t data;
} event_t;
void event_dispatcher(event_t *evt) {
switch (evt->id) {
case EVT_KEY_PRESS:
handle_key(evt->data);
break;
case EVT_SENSOR_READY:
process_sensor_data(evt->data);
break;
// ... 其他事件
}
}
int main(void) {
event_t evt;
while (1) {
if (event_queue_dequeue(&evt)) {
event_dispatcher(&evt);
} else {
system_sleep(); // 没事件就睡
}
}
}
事件驱动的好处很明显:
- 解耦:事件产生者和消费者互不关心对方是谁
- 低功耗:没有事件时 CPU 深度休眠
- 可扩展:加一个新功能,只需要注册一个新事件
但事件驱动也不是万能的。我在一个需要同时处理 20 个任务的网关项目里,事件队列被塞得满满当当。高优先级的事件(比如网络断连)要等低优先级事件处理完才能执行——优先级反转问题暴露了。
1.4 RTOS:当事件驱动不够用时
事件驱动架构的瓶颈在于:所有事件共用一个线程。一个事件处理慢了,后面的都得排队。
RTOS(实时操作系统)解决了这个问题。它让你可以把不同的任务放到不同的线程里,每个线程有自己的栈、自己的优先级。高优先级的任务可以抢占低优先级的任务。
// RTOS 多任务示意(以 FreeRTOS 为例)
void vKeyTask(void *pvParameters) {
while (1) {
if (xQueueReceive(key_queue, &key_val, portMAX_DELAY)) {
handle_key(key_val);
}
}
}
void vSensorTask(void *pvParameters) {
while (1) {
vTaskDelay(pdMS_TO_TICKS(100)); // 每 100ms 采集一次
read_sensor();
xQueueSend(sensor_queue, &data, 0);
}
}
int main(void) {
xTaskCreate(vKeyTask, "Key", 256, NULL, 3, NULL);
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 2, NULL);
vTaskStartScheduler();
}
RTOS 带来的核心能力:
- 任务隔离:一个任务崩溃不会拖垮整个系统
- 优先级管理:紧急任务可以立即执行
- 同步机制:队列、信号量、互斥锁,让任务协作更安全
1.5 三种架构的对比与选择
| 维度 | 裸机轮询 | 事件驱动 | RTOS |
|---|---|---|---|
| 实时性 | 差(依赖循环周期) | 中(依赖事件队列处理速度) | 好(抢占式调度) |
| 功耗 | 高(CPU 持续运行) | 低(无事件时休眠) | 中(需考虑 Tickless 模式) |
| 代码复杂度 | 低 | 中 | 高 |
| RAM 开销 | 极低 | 低(事件队列) | 高(每个任务独立栈) |
| 任务数量 | ≤3 | 3~10 | ≥5 |
| 典型场景 | 简单控制、玩具 | 传感器节点、遥控器 | 网关、工业控制器 |
我个人习惯这样选:
- 任务 ≤ 3 个,逻辑简单 → 裸机轮询(别折腾)
- 任务 3~10 个,需要低功耗 → 事件驱动(性价比最高)
- 任务 ≥ 5 个,有实时性要求 → RTOS(值得投入)
1.6 本章知识结构
下面这张图总结了从裸机到 RTOS 的演进路径,以及每种架构的核心特征:
看到这张图,你应该能理解:架构的演进不是技术的堆砌,而是对问题复杂度的回应。裸机解决「能跑」的问题,事件驱动解决「低功耗」的问题,RTOS 解决「多任务协作」的问题。
接下来的课程,我会带你深入每一种架构的细节。我们会从事件驱动开始,一步步构建一个完整的低功耗嵌入式系统。你会在实战中理解:为什么架构设计不是「纸上谈兵」,而是决定产品成败的关键。