在嵌入式开发中,良好的架构设计是项目可维护性和可扩展性的基础。没有操作系统的情况下,如何组织代码结构显得尤为重要。本文将介绍几种常见的裸机架构模式,帮助你选择适合自己项目的方案。

常见的裸机架构模式

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

总结

裸机架构设计没有标准答案,需要根据具体项目需求选择合适的模式。关键原则是:

  1. 够用就好:不要过度设计,简单项目用简单架构
  2. 预留扩展:考虑未来可能的需求变化
  3. 保持一致:同一项目内保持架构风格统一
  4. 注重可维护性:代码是写给人看的,易于维护最重要

在实际项目中,常常会混合使用多种架构模式,根据各模块的特点选择最合适的方式。