在正式学习 FreeRTOS 之前,先要弄清楚一个问题:为什么单片机程序需要操作系统?很多 STM32 初学项目都是裸机程序,也就是没有操作系统,程序主要依靠 main 函数里的死循环和中断来完成任务。但随着功能越来越多,比如按键、串口、传感器、显示屏、电机控制同时存在时,裸机程序会变得越来越难维护。

这一节主要理解三种软件结构:轮询系统、前后台系统、多任务系统。

1. 轮询系统

轮询系统是最基础的裸机程序结构。程序初始化外设后,进入一个 while(1) 死循环,在循环中按照固定顺序执行各个功能函数。

简单例子如下:

int main(void)
{
    Board_Init();

    while (1)
    {
        Led_Process();
        Key_Process();
        Uart_Process();
        Sensor_Process();
    }
}

这种方式的特点是简单、直观,适合 LED 闪烁、串口打印、简单传感器读取这类小项目。

但是它的问题也很明显:所有函数必须排队执行。如果 Sensor_Process() 执行时间很长,后面的程序就会被拖慢。如果某个外部事件突然发生,比如按键按下或者串口收到数据,程序也不能立刻处理,只能等主循环运行到对应函数时才处理。

所以轮询系统适合顺序执行的简单任务,不适合实时性要求高的场景。

2. 前后台系统

前后台系统是在轮询系统基础上加入中断机制。

这里的“前台”一般指中断服务函数,负责快速响应外部事件;“后台”指 main 函数里的死循环,负责处理主要业务逻辑。

比如按键中断来了以后,不在中断里做复杂处理,只设置一个标志位:

volatile uint8_t key_event = 0;

void EXTI0_IRQHandler(void)
{
    key_event = 1;
    Clear_EXTI_Flag();
}

int main(void)
{
    Board_Init();

    while (1)
    {
        if (key_event)
        {
            key_event = 0;
            Key_Event_Process();
        }

        Led_Process();
        Uart_Process();
    }
}

这种方式比纯轮询好很多,因为外部事件可以被中断及时捕获,不容易丢失。例如按键按下、串口接收完成、定时器到达,都可以先在中断中记录下来。

但是前后台系统仍然有一个问题:事件虽然能被及时“发现”,但真正的处理过程仍然在主循环中排队完成。如果后台某个函数执行时间很长,其他事件的处理还是会被延迟。

因此,前后台系统适合中小型裸机项目。它比轮询系统实时性更好,但本质上仍然是单一主循环结构。

3. 多任务系统

多任务系统就是 FreeRTOS 这类实时操作系统要解决的问题。

在多任务系统中,不再把所有功能都塞进一个 while(1) 里面,而是把不同功能拆成多个相对独立的任务。比如 LED 是一个任务,按键是一个任务,串口是一个任务,传感器采集是一个任务。

简化代码如下:

void LedTask(void *argument)
{
    while (1)
    {
        Led_Toggle();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void KeyTask(void *argument)
{
    while (1)
    {
        Key_Scan();
        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

int main(void)
{
    Board_Init();

    xTaskCreate(LedTask, "led", 128, NULL, 1, NULL);
    xTaskCreate(KeyTask, "key", 128, NULL, 2, NULL);

    vTaskStartScheduler();

    while (1)
    {
    }
}

这里有几个关键点:

xTaskCreate() 用来创建任务。

vTaskStartScheduler() 用来启动任务调度器。

任务函数内部通常都是 while(1),因为任务本身就是长期运行的小程序。

vTaskDelay() 不是普通的死等延时,它会让当前任务进入阻塞状态,把 CPU 让给其他任务。

这就是 RTOS 和普通裸机延时最大的区别。裸机里如果写 delay_ms(500),CPU 基本就在原地等着;而 FreeRTOS 里使用 vTaskDelay(),当前任务暂停,系统可以去运行其他任务。

4. 中断和任务如何配合

在多任务系统中,中断仍然存在,但是中断一般只做很短的事情,比如接收数据、清除标志位、通知任务,不建议在中断里写复杂逻辑。

例如串口接收中断可以通知串口任务:

TaskHandle_t UartTaskHandle;

void USART1_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    if (Uart_Rx_Done())
    {
        vTaskNotifyGiveFromISR(UartTaskHandle, &xHigherPriorityTaskWoken);
    }

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void UartTask(void *argument)
{
    while (1)
    {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        Uart_Data_Process();
    }
}

这个思路非常重要:中断负责快速响应,任务负责具体处理。这样既保证了响应速度,也避免中断函数太长影响系统稳定性。

5. 三种模型对比

模型 事件响应 事件处理 特点
轮询系统 主循环发现 主循环处理 简单,但实时性差
前后台系统 中断发现 主循环处理 响应更快,适合中小项目
多任务系统 中断或任务触发 对应任务处理 结构清晰,实时性更好

6. 我的理解

轮询系统像是一个人按顺序做所有事情,必须一件做完再做下一件。

前后台系统像是有人提醒你“有新事情来了”,但最终还是要你按顺序处理。

多任务系统则像是把事情分给多个“任务角色”,每个任务负责自己的功能,调度器根据优先级和状态决定谁先运行。

对于 STM32 裸机项目来说,如果功能少,用轮询或者前后台系统就够了。但如果项目中有多个模块同时运行,比如串口通信、传感器采集、按键控制、屏幕刷新、电机控制等,就很适合引入 FreeRTOS。它可以让程序结构更清晰,也能提高实时响应能力。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐