裸机系统架构详解:前后台系统

说到裸机系统,我第一个想到的就是「前后台系统」。这名字听着挺玄乎,其实说白了就是:一个死循环当后台,中断服务程序当前台。

我刚开始做嵌入式那会儿,用的就是这种架构。那时候单片机资源紧张,RTOS?想都不敢想。一个8位MCU,2KB的RAM,跑个循环加中断,刚刚好。

超级循环——系统的「心脏」

超级循环,也叫主循环。它的工作方式特别简单:

void main(void)
{
    // 初始化
    System_Init();
    Periph_Init();
    
    // 超级循环
    while(1)
    {
        Task_ScanKey();      // 扫描按键
        Task_UpdateDisplay(); // 更新显示
        Task_ProcessData();   // 处理数据
        Task_CommPoll();      // 通信轮询
    }
}

你看,就是这么个死循环。每个任务按顺序执行,跑完一遍再从头来。我管这叫「轮询式多任务」——虽然它压根儿不算多任务。

核心特点:

  • 任务顺序执行,没有优先级
  • 一个任务卡住,后面的全完蛋
  • 响应时间取决于循环长度
  • 代码简单,容易理解

我在项目中遇到过一个问题:一个LED闪烁任务里加了延时,结果整个系统都跟着「眨眼」了。按键扫描、通信处理,全被拖慢。嗯,这就是超级循环的典型坑。

中断服务程序——系统的「救火队」

超级循环有个致命弱点:它不知道什么时候该处理紧急事件。这时候,中断就派上用场了。

中断服务程序(ISR)就像个「救火队」。平时没事儿,一旦有紧急情况(比如按键按下、数据到达),它立刻冲进去处理。

// 定时器中断 - 每1ms触发一次
void TIM2_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        
        // 处理定时任务
        g_uiTick++;
        if(g_uiTick % 10 == 0)
        {
            g_bScanKey = 1;  // 设置按键扫描标志
        }
    }
}

// 串口接收中断
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t data = USART_ReceiveData(USART1);
        // 将数据存入环形缓冲区
        RingBuf_Write(&g_rxBuf, data);
    }
}

你想想看,中断和主循环配合起来,是不是有点像「前台接待」和「后台办公」?前台(中断)处理紧急事务,后台(主循环)处理常规工作。这就是「前后台系统」名字的由来。

我的经验:中断里千万别做耗时操作。我曾经在中断里调了个printf,结果系统直接崩了。中断服务程序的原则是:快进快出,只做最必要的事。

任务调度机制——没有调度就是最好的调度

裸机系统有任务调度吗?说实话,没有。或者说,它的调度机制就是「顺序执行」。

但这不代表我们没办法实现「伪并行」。常用的技巧有:

  1. 状态机调度:每个任务拆成多个状态,每次循环只执行一个状态
  2. 标志位调度:中断设置标志,主循环检查标志再执行
  3. 时间片轮转:用定时器分配时间片,每个任务跑固定时间
// 状态机调度示例
typedef enum {
    STATE_IDLE,
    STATE_READ_SENSOR,
    STATE_PROCESS_DATA,
    STATE_SEND_RESULT
} TaskState_t;

void Task_Sensor(void)
{
    static TaskState_t state = STATE_IDLE;
    
    switch(state)
    {
        case STATE_IDLE:
            if(g_bStartMeasure)
            {
                state = STATE_READ_SENSOR;
            }
            break;
            
        case STATE_READ_SENSOR:
            ADC_StartConversion();
            state = STATE_PROCESS_DATA;
            break;
            
        case STATE_PROCESS_DATA:
            if(ADC_GetFlagStatus(ADC_FLAG_EOC))
            {
                g_u16Result = ADC_GetConversionValue();
                state = STATE_SEND_RESULT;
            }
            break;
            
        case STATE_SEND_RESULT:
            UART_SendData(g_u16Result);
            state = STATE_IDLE;
            break;
    }
}

说白了,裸机系统的调度就是「程序员手动调度」。你写代码的时候,就得想好每个任务什么时候跑、跑多久。这活儿挺累的,但好处是——你完全掌控了系统的每一纳秒。

资源管理方式——共享资源的「交通规则」

裸机系统里,资源管理是个大问题。主循环和中断都在访问同一个变量,搞不好就出乱子。

我常用的资源管理方式有:

资源类型 管理方式 适用场景
全局变量 volatile + 原子访问 中断与主循环共享
环形缓冲区 读写指针分离 数据流处理
临界区 关中断保护 多中断共享资源
标志位 位操作 + 原子指令 事件通知

注意:关中断的时间一定要短。我见过有人关中断做复杂计算,结果系统响应延迟从微秒级变成了毫秒级。嗯,这锅得程序员背。

举个实际例子:

// 中断和主循环共享的变量
volatile uint32_t g_uiSystemTick;

// 中断中更新
void SysTick_Handler(void)
{
    g_uiSystemTick++;
}

// 主循环中读取
uint32_t GetSystemTick(void)
{
    uint32_t tick;
    
    // 原子读取,防止读取过程中被中断修改
    __disable_irq();
    tick = g_uiSystemTick;
    __enable_irq();
    
    return tick;
}

这里用了volatile关键字,告诉编译器这个变量可能被意外修改。我刚开始做嵌入式时,就因为忘了加volatile,debug了一整天。从那以后,凡是中断和主循环共享的变量,我第一件事就是加volatile。

裸机系统的优缺点总结

做了这么多年嵌入式,我对裸机系统的感受是:

优点:

  • 代码简单,容易调试
  • 资源占用极低(RAM、ROM都省)
  • 实时性可预测(没有调度开销)
  • 适合简单、固定的应用场景

缺点:

  • 任务耦合度高,一个任务出问题影响全局
  • 扩展性差,加个功能可能得重构
  • 紧急任务响应依赖循环周期
  • 复杂逻辑用状态机实现,代码量爆炸

我个人习惯是:如果项目功能少于10个,且实时性要求不高,裸机系统完全够用。但要是功能多了,或者有严格的响应时间要求,那就得考虑RTOS了。

下一章,我会详细讲讲RTOS的架构和调度机制。到时候你就能对比出来,什么时候该用裸机,什么时候该上RTOS。