4. 前后台架构与时间片轮询

前后台架构是嵌入式裸机程序中最经典、也最容易被低估的一种实时软件组织方式。它的基本思想非常朴素:后台是一个永不退出的超级循环(super loop),前台是各类中断服务程序(ISR)。后台负责“常规的、可等待的”工作,前台负责“必须立刻响应的”事件。理解这一架构的关键,不在于循环本身,而在于如何划分前台与后台的职责边界,以及后台如何在没有操作系统调度器的情况下,让多个任务看起来在同时推进。

超级循环的基本形态

最原始的超级循环只做一件事:顺序执行若干函数,然后回到开头。例如:

int main(void)
{
    System_Init();
    while (1) {
        Task_KeyScan();      // 按键扫描
        Task_Display();      // 刷新显示
        Task_CommPoll();     // 串口协议轮询
        Task_LedBlink();     // LED 闪烁
    }
}

这种写法的问题在于:每个任务的执行时间不同,且互相阻塞。如果 Task_Display() 需要 8 ms,那么按键扫描的响应间隔就被拉长到至少 8 ms,实时性完全取决于最慢的任务。因此,真正可用的前后台架构必须引入时间片轮询,把“顺序执行”升级为“按时间节拍分时执行”。

中断服务程序的分工原则

在前后台架构中,ISR 的职责应当被严格限制为三件事:清除中断标志、搬运关键数据、置位事件标志。任何耗时操作——浮点运算、字符串格式化、Flash 写入、长延时——都不应放在 ISR 中。原因很直接:ISR 执行期间,同级或更低优先级的中断被屏蔽,ISR 越长,系统的中断响应抖动越大。

一个典型的串口接收 ISR 应当这样写:

void USART1_IRQHandler(void)
{
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t ch = USART1->DR;          // 读走数据,自动清标志
        RingBuf_Put(&rx_buf, ch);         // 仅做入队,不做解析
        g_rx_event = 1;                   // 置位事件标志
    }
}

真正的协议解析、命令执行、应答组包,全部交给后台循环在检测到 g_rx_event 后处理。这样 ISR 的执行时间可以控制在微秒级,而解析逻辑无论多复杂,都不会影响中断响应。

时间片轮询的实现

时间片轮询的核心是:用一个硬件定时器产生固定周期的“节拍”,后台循环在每个节拍到来时,按预设的时间片分配执行各个任务。常见做法是维护一个软件定时器数组,每个任务记录“上次执行时刻”和“执行周期”,后台循环只判断当前节拍是否到达该任务的执行点。

#define TICK_MS 1
volatile uint32_t g_tick = 0;

void SysTick_Handler(void)   // 1 ms 节拍
{
    g_tick++;
}

typedef struct {
    uint32_t period;   // 执行周期,单位 ms
    uint32_t last;     // 上次执行时的 g_tick
    void (*fn)(void);
} Task_t;

Task_t tasks[] = {
    { 5,  0, Task_KeyScan  },   // 每 5 ms
    { 20, 0, Task_Display  },   // 每 20 ms
    { 10, 0, Task_CommPoll },   // 每 10 ms
    { 500,0, Task_LedBlink },   // 每 500 ms
};

void Scheduler_Run(void)
{
    for (int i = 0; i < sizeof(tasks)/sizeof(tasks[0]); i++) {
        if ((g_tick - tasks[i].last) >= tasks[i].period) {
            tasks[i].last = g_tick;
            tasks[i].fn();
        }
    }
}

这里有一个工程细节值得注意:g_tick - tasks[i].last 使用无符号减法,即使 g_tick 溢出回绕,差值仍然正确。这是嵌入式时间比较的标准写法,比直接比较 g_tick >= last + period 更安全。

时间片轮询的调度效果,可以用下面的信号链来理解:定时器节拍驱动 tick 累加,后台循环在每个节拍检查各任务的到期条件,到期任务被依次执行,未到期任务跳过。整个系统的实时性由“最坏情况下所有到期任务的总执行时间”决定,而不是由单个最慢任务决定。

前后台架构:中断置位事件,后台按时间片轮询执行 前台(中断服务程序 ISR) SysTick 1ms g_tick++ USART1 RX 入队 + 置事件 EXTI 按键 置按键标志 原则:短、快、只置标志 不做解析 / 不延时 事件标志 / 数据入队 后台(超级循环 + 时间片轮询) while(1) { Scheduler_Run(); } Task_KeyScan 周期 5 ms Task_CommPoll 周期 10 ms Task_Display 周期 20 ms Task_LedBlink 周期 500 ms 每个节拍检查到期条件,到期任务执行,未到期跳过

适用场景与局限性

前后台架构最适合任务数量少、实时性要求中等、任务之间耦合度低的场景。例如:家电控制板、简单工控模块、传感器采集节点、教学实验平台。这类系统的共同特征是:最坏响应时间在毫秒级即可接受,任务之间不需要复杂的同步与互斥,代码规模通常在几千行以内。

它的局限性同样明确。第一,任务执行时间不可抢占:如果某个任务因为等待外设而阻塞 30 ms,其他所有任务都被推迟,系统的实时性被最坏任务拖累。第二,优先级无法表达:时间片轮询对所有任务一视同仁,紧急任务和普通任务只能靠缩短周期来“抢”执行机会,但这会加剧 CPU 占用。第三,任务间共享数据需要额外保护:后台任务与 ISR 共享的变量必须用 volatile 修饰,多字节变量的读写要考虑原子性,必要时用临界区保护。第四,可扩展性差:当任务增加到十几个、周期要求各不相同、还存在任务间依赖时,手工维护时间片表会迅速变得难以调试。

使用时间片轮询时,务必保证每个任务的单次执行时间远小于其周期。经验上,所有任务单次执行时间之和不应超过最小周期的 50%,否则节拍会被“吃掉”,出现任务漏执行或周期抖动。若发现某个任务经常超时,应优先拆分该任务,而不是简单缩短其他任务的周期。

从工程实践角度看,前后台架构并不是“过时”的方案,而是复杂度与实时性之间的一种明确取舍。当系统规模尚小、实时性要求不苛刻时,它代码直观、调试简单、无内核开销,是非常合理的选择。真正需要警惕的是:在任务数量和实时性要求增长后,仍然强行用超级循环硬撑,导致系统行为不可预测。此时应当考虑引入 RTOS,把时间片轮询升级为基于优先级的抢占式调度。