在嵌入式开发中,良好的架构设计是项目可维护性和可扩展性的基础。没有操作系统的情况下,如何组织代码结构显得尤为重要。本文将介绍几种常见的裸机架构模式,帮助你选择适合自己项目的方案。
常见的裸机架构模式
1. 超级循环(Super Loop)
超级循环是最简单也最直接的架构模式。主循环依次调用各任务处理函数,顺序执行所有业务逻辑。
代码示例:
int main(void) {
System_Init();
while (1) {
Task_KeyScan(); // 按键扫描
Task_Display(); // 显示屏刷新
Task_Sensor(); // 传感器采集
Task_Communication(); // 通信处理
// 可选:加入延时控制循环频率
delay_ms(10);
}
}
优点:
- 代码结构简单,易于理解和实现
- 无额外资源消耗,无需调度器
- 中断响应确定性好
缺点:
- 任务耦合严重,修改一个任务可能影响其他任务
- 实时性差,所有任务必须等待循环执行
- 任务数量和复杂度受循环时间限制
适用场景: 任务简单、实时性要求不高的简单产品,如玩具、小家电控制板。
2. 前后台系统(Foreground-Background)
将紧急任务放在中断前台处理,非紧急任务在主循环后台执行。这是最常用的裸机架构模式。
代码示例:
// 前台:中断服务程序
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
// 紧急事件处理,如按键按下、外部报警
g_key_pressed = 1;
g_system_error = ERROR_NONE;
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
// 定时器中断,标记时间基准
g_timer_flag = 1;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
// 后台:主循环
int main(void) {
System_Init();
while (1) {
if (g_timer_flag) {
g_timer_flag = 0;
Task_10ms(); // 10ms周期任务
}
if (g_key_pressed) {
g_key_pressed = 0;
Task_KeyProcess(); // 按键处理
}
Task_Display(); // 显示屏刷新
Task_Communication(); // 通信处理
}
}
优点:
- 紧急事件由中断快速响应,保证实时性
- 结构清晰,任务分层明确
- 资源消耗小
缺点:
- 中断中不宜执行耗时操作
- 后台任务仍受循环执行影响
- 中断优先级管理需要谨慎设计
适用场景: 大多数中低端嵌入式产品,如家用电器、工业控制模块。
3. 事件驱动架构(Event-Driven)
基于事件队列的架构模式,实现任务间解耦。通过事件标志或消息队列将事件与处理分离。
代码示例:
// 事件定义
typedef enum {
EVENT_KEY_PRESS,
EVENT_UART_RECV,
EVENT_TIMER,
EVENT_SENSOR_DATA_READY,
EVENT_MAX
} Event_Type;
typedef struct {
Event_Type type;
uint32_t param;
} Event;
// 事件队列
static Event g_event_queue[MAX_EVENT_QUEUE];
static uint8_t g_event_head = 0;
static uint8_t g_event_tail = 0;
static void Event_Push(Event_Type type, uint32_t param) {
uint8_t next = (g_event_head + 1) % MAX_EVENT_QUEUE;
if (next != g_event_tail) {
g_event_queue[g_event_head].type = type;
g_event_queue[g_event_head].param = param;
g_event_head = next;
}
}
static Event* Event_Pop(void) {
if (g_event_head != g_event_tail) {
uint8_t current = g_event_tail;
g_event_tail = (g_event_tail + 1) % MAX_EVENT_QUEUE;
return &g_event_queue[current];
}
return NULL;
}
// 事件处理表
typedef void (*EventHandler)(uint32_t param);
static EventHandler g_handlers[EVENT_MAX];
void Event_Init(void) {
g_handlers[EVENT_KEY_PRESS] = On_KeyPress;
g_handlers[EVENT_UART_RECV] = On_UartRecv;
g_handlers[EVENT_TIMER] = On_Timer;
g_handlers[EVENT_SENSOR_DATA_READY] = On_SensorReady;
}
int main(void) {
System_Init();
Event_Init();
while (1) {
Event *evt = Event_Pop();
if (evt && g_handlers[evt->type]) {
g_handlers[evt->type](evt->param);
}
}
}
// 中断中产生事件
void UART1_IRQHandler(void) {
if (USART_GetITStatus(UART1, USART_IT_RXNE) != RESET) {
uint8_t data = USART_ReceiveData(UART1);
Event_Push(EVENT_UART_RECV, data);
}
}
优点:
- 任务间松耦合,易于维护和扩展
- 新增功能只需添加事件处理函数
- 便于模块化测试
缺点:
- 需要额外的内存管理
- 事件处理延迟不确定
- 系统复杂度随事件增多而增加
适用场景: 功能较多、需要灵活扩展的中等复杂度产品。
4. 时间片轮询(Time-Triggered Polling)
基于系统滴答定时器的时间片调度,将CPU时间划分为固定长度的时间片,每个任务占用一个时间片。
代码示例:
#define TASK_COUNT 4
#define TICK_MS 10
#define TASK0_PERIOD 1 // 10ms
#define TASK1_PERIOD 2 // 20ms
#define TASK2_PERIOD 5 // 50ms
#define TASK3_PERIOD 10 // 100ms
typedef struct {
void (*func)(void);
uint16_t period; // 周期(Tick数)
uint16_t elapsed; // 已用时间
} TaskDef;
static TaskDef g_tasks[TASK_COUNT] = {
{Task_LED, TASK0_PERIOD, 0},
{Task_KeyScan, TASK1_PERIOD, 0},
{Task_Display, TASK2_PERIOD, 0},
{Task_Communication, TASK3_PERIOD, 0}
};
void Scheduler_Init(void) {
Systick_Config(TICK_MS); // 配置SysTick为10ms中断
}
void Scheduler_Update(void) {
for (int i = 0; i < TASK_COUNT; i++) {
g_tasks[i].elapsed++;
if (g_tasks[i].elapsed >= g_tasks[i].period) {
g_tasks[i].elapsed = 0;
g_tasks[i].func();
}
}
}
void SysTick_Handler(void) {
Scheduler_Update();
}
int main(void) {
System_Init();
Scheduler_Init();
while (1) {
// 主循环可以处理非实时任务
Task_PowerManagement();
}
}
优点:
- 任务调度确定性好,实时性有保障
- CPU时间公平分配,避免单个任务饥饿
- 易于添加和删除任务
缺点:
- 时间片大小选择需要权衡
- 实时性受任务执行时间影响
- 不适合异步事件处理
适用场景: 多任务定时执行、实时性要求较高的产品。
架构选择指南
| 架构 | 复杂度 | 实时性 | 扩展性 | 适用场景 |
|---|---|---|---|---|
| 超级循环 | 低 | 差 | 差 | 简单产品、任务极少 |
| 前后台 | 中 | 良 | 中 | 大多数中低端产品 |
| 事件驱动 | 中高 | 中 | 良 | 功能较多的复杂产品 |
| 时间片轮询 | 中 | 优 | 中 | 多任务实时系统 |
选择建议:
- 任务简单(3个以内):直接用超级循环,简单高效
- 有紧急事件处理:选择前后台系统
- 任务较多、需要解耦:事件驱动架构
- 多任务实时系统:时间片轮询调度
最佳实践
1. 模块化设计
将系统拆分为独立的功能模块,每个模块:
- 有明确的职责边界
- 通过接口与其他模块交互
- 可以独立编译和测试
// 模块头文件示例
#ifndef __BSP_LED_H__
#define __BSP_LED_H__
void LED_Init(void);
void LED_On(uint8_t led);
void LED_Off(uint8_t led);
void LED_Toggle(uint8_t led);
#endif
2. 分层思想
+-------------------+
| Application | 应用层:业务逻辑
+-------------------+
| Middleware | 中间层:协议、算法
+-------------------+
| Driver | 驱动层:外设控制
+-------------------+
| Hardware | 硬件层:MCU、电路
+-------------------+
3. 状态机管理
在模块内部使用状态机管理复杂逻辑,详见另一篇文章《状态机在嵌入式开发中的应用》。
4. 配置分离
将可配置参数集中管理,便于产品定制和维护:
// config.h
#ifndef __CONFIG_H__
#define __CONFIG_H__
#define LED_BLINK_INTERVAL_MS 500
#define KEY_SCAN_INTERVAL_MS 20
#define UART_BAUDRATE 115200
#define SYSTEM_CLOCK 72000000
#endif
总结
裸机架构设计没有标准答案,需要根据具体项目需求选择合适的模式。关键原则是:
- 够用就好:不要过度设计,简单项目用简单架构
- 预留扩展:考虑未来可能的需求变化
- 保持一致:同一项目内保持架构风格统一
- 注重可维护性:代码是写给人看的,易于维护最重要
在实际项目中,常常会混合使用多种架构模式,根据各模块的特点选择最合适的方式。