裸机主循环的极限

裸机程序的结构通常是一个大 while(1),里面按顺序调用若干功能函数。功能少时它非常好用,但随着需求增加会出现三个无法回避的问题:任何一个函数里的延时都会拖慢其他功能;长任务的执行时间决定了其他功能的最坏响应时间;各功能之间靠全局变量通信,时序一旦错位就出现难以复现的偶发故障。

RTOS 解决的核心问题是“谁先跑”:用优先级决定任务的重要性,用阻塞让出 CPU 而不是空转等待,用队列与信号量把共享数据变成有明确规则的传递。
FreeRTOS 配图:多任务调度轮盘与内核圆核
RTOS 解决的是“谁先跑”的问题:优先级决定顺序,阻塞决定让出 CPU 的时机

FreeRTOS 是嵌入式领域最常见的实时内核之一,它的资源占用可以小到几 KB,代码结构清晰,被大量芯片厂商作为默认选择。下面从任务创建开始,把最常用的几个机制讲透。

任务与调度:优先级与状态

一个任务就是一个永不返回的函数,内部通常是一个死循环。FreeRTOS 采用抢占式调度:更高优先级的就绪任务会立刻抢走 CPU;同优先级任务按时间片轮转(可配置)。

#include "FreeRTOS.h"
#include "task.h"

static TaskHandle_t h_sensor, h_upload;

/* 采集任务:最高优先级,周期 10ms */
static void sensor_task(void *arg)
{
    TickType_t last = xTaskGetTickCount();
    for (;;) {
        read_sensor();
        /* vTaskDelayUntil 保证严格周期,不受任务执行时间影响 */
        vTaskDelayUntil(&last, pdMS_TO_TICKS(10));
    }
}

/* 上报任务:低优先级,等数据攒够再走网络 */
static void upload_task(void *arg)
{
    for (;;) {
        if (wait_for_batch(portMAX_DELAY)) {
            do_upload();               /* 阻塞式网络发送,期间高优先级任务照常运行 */
        }
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    hw_init();

    xTaskCreate(sensor_task, "sensor", 256, NULL, 4, &h_sensor);  /* 栈深度单位:字(word) */
    xTaskCreate(upload_task, "upload", 512, NULL, 2, &h_upload);

    vTaskStartScheduler();     /* 启动调度器后不会返回 */
    for (;;) { }
}

两点经验:栈深度参数的单位是“字”而不是字节(Cortex-M 上一个字 4 字节,256 表示约 1KB),分配时要按最坏调用深度估算并留余量;vTaskDelayUntil 用于固定周期任务,vTaskDelay 用于“至少等这么久”的场景,混用会导致周期漂移累积。

任务间通信:队列、信号量与事件组

任务之间共享数据必须用内核提供的机制,直接读写全局变量在抢占式调度下随时可能出错。

#include "queue.h"
#include "semphr.h"

typedef struct { uint32_t ts; float temp; float hum; } sample_t;

static QueueHandle_t   q_sample;      /* 传数据 */
static SemaphoreHandle_t sem_dma;     /* 传“事件发生了” */
static EventGroupHandle_t ev_net;     /* 传“多个条件” */

#define BIT_LINK_UP   (1 << 0)
#define BIT_GOT_IP    (1 << 1)

static void producer(void *arg)
{
    sample_t s;
    for (;;) {
        s.ts = xTaskGetTickCount(); s.temp = read_temp(); s.hum = read_hum();
        /* 队列满时等 10ms,仍写不进就丢弃并计数,绝不无限阻塞 */
        if (xQueueSend(q_sample, &s, pdMS_TO_TICKS(10)) != pdPASS) {
            g_drop_cnt++;
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

static void consumer(void *arg)
{
    EventBits_t bits;
    for (;;) {
        /* 等网络可用:两个位同时满足 */
        bits = xEventGroupWaitBits(ev_net, BIT_LINK_UP | BIT_GOT_IP,
                                   pdFALSE, pdTRUE, portMAX_DELAY);
        if ((bits & (BIT_LINK_UP | BIT_GOT_IP)) != (BIT_LINK_UP | BIT_GOT_IP)) continue;

        sample_t s;
        if (xQueueReceive(q_sample, &s, pdMS_TO_TICKS(200)) == pdPASS) {
            upload_one(&s);
        }
    }
}

选择哪种机制有清晰的标准:传数据用队列(拷贝语义,天然线程安全);通知“某件事发生了”用信号量;需要同时等待多个条件用事件组。队列的一个实用细节是“满了怎么办”:在采集型任务里宁可丢弃并计数,也不要无限阻塞,否则上游设备会产生连锁卡顿。

互斥与临界区:优先级反转怎么来的

多个任务访问同一块资源(如一条 SPI 总线、一个 Flash 区域),需要用互斥量保护。这里有一个容易被忽视的问题:普通二值信号量会导致优先级反转。

static SemaphoreHandle_t m_spi;

static void bus_init(void)
{
    /* 用互斥量而不是二值信号量:它带优先级继承机制 */
    m_spi = xSemaphoreCreateMutex();
}

/* 低优先级任务占用总线,中优先级任务抢走 CPU,高优先级任务被迫等待
   —— 互斥量的优先级继承会把低优先级任务的优先级临时提高,缩短这段阻塞 */
static bool spi_write_safe(const uint8_t *buf, size_t len)
{
    if (xSemaphoreTake(m_spi, pdMS_TO_TICKS(50)) != pdPASS) {
        return false;                     /* 拿不到就返回失败,绝不无限等 */
    }
    bool ok = spi_write(buf, len);
    xSemaphoreGive(m_spi);
    return ok;
}

/* 极短、且不能被打断的操作才用临界区(会关闭中断,时间必须极短) */
static void counter_inc(void)
{
    taskENTER_CRITICAL();
    g_counter++;                          /* 几微秒内完成的原子性操作 */
    taskEXIT_CRITICAL();
}
同步机制的三条准则
  • 保护资源用互斥量二值信号量没有优先级继承,用于“通知”而不是“保护”。
  • 临界区要短taskENTER_CRITICAL 会屏蔽中断,超过几十微秒就会影响实时性。
  • 加超时不要 portMAX_DELAY除“必须等到”的场景外都设置超时,避免一处死锁拖死整机。

时基:延时、定时器与 tick

FreeRTOS 的时间基准来自一个周期性 tick 中断,configTICK_RATE_HZ 决定它的频率;设为 1000 表示 1 个 tick 等于 1ms。所有延时与超时参数都以 tick 为单位,为了可读性建议统一用 pdMS_TO_TICKS() 换算。

/* 软件定时器:适合“延迟执行”或“周期性小任务”,回调在定时器服务任务中执行 */
static TimerHandle_t t_report;

static void report_cb(TimerHandle_t xTimer)
{
    /* 注意:这里运行在定时器服务任务上下文,不能调用阻塞 API */
    g_flags |= FLAG_REPORT_DUE;
}

static void timer_setup(void)
{
    t_report = xTimerCreate("report",
                            pdMS_TO_TICKS(5000),
                            pdTRUE,            /* 周期定时器 */
                            NULL,
                            report_cb);
    xTimerStart(t_report, 0);
}

/* 严格周期任务用 vTaskDelayUntil,避免执行时间造成的漂移 */
static void control_task(void *arg)
{
    TickType_t last = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(20);
    for (;;) {
        do_control();                /* 假设最坏 3ms 完成 */
        vTaskDelayUntil(&last, period);
    }
}

判断“用软件定时器还是独立任务”有个简单标准:回调执行时间短、不阻塞、不需要大栈,就用定时器;反之用专门的任务。把所有逻辑塞进定时器回调,是常见的设计失误。