Chapter 09 · ARM 嵌入式 C 开发
FreeRTOS 实时操作系统
从裸机到 RTOS 的思维转变,掌握任务状态机、调度算法、IPC 原语和内存管理,构建健壮的多任务系统。
从裸机到 RTOS 的思维转变,掌握任务状态机、调度算法、IPC 原语和内存管理,构建健壮的多任务系统。
#include "FreeRTOS.h" #include "task.h" #include "queue.h" #include "semphr.h" #include "timers.h" /* ─── 任务句柄和队列句柄 ──────────────────────────── */ static TaskHandle_t hTask_Sensor = NULL; static TaskHandle_t hTask_Display = NULL; static QueueHandle_t qSensorData = NULL; static SemaphoreHandle_t xMutex_OLED = NULL; /* ─── 传输数据结构 ───────────────────────────────── */ typedef struct { float temperature; float humidity; uint32_t timestamp; } SensorData_t; /* ─── 传感器采集任务(生产者)───────────────────── */ void Task_Sensor(void *pvParams) { (void)pvParams; SensorData_t data; for (;;) { /* 采集传感器数据(假设已实现)*/ data.temperature = DHT22_ReadTemp(); data.humidity = DHT22_ReadHumi(); data.timestamp = HAL_GetTick(); /* 发送到队列(阻塞最多 100ms,若队列满则丢弃)*/ if (xQueueSend(qSensorData, &data, pdMS_TO_TICKS(100)) != pdPASS) { /* 队列满,可记录日志或统计丢帧 */ } vTaskDelay(pdMS_TO_TICKS(2000)); /* 每2秒采集一次 */ } } /* ─── 显示任务(消费者)────────────────────────── */ void Task_Display(void *pvParams) { (void)pvParams; SensorData_t data; char buf[32]; for (;;) { /* 等待队列数据(永久阻塞,不占 CPU)*/ if (xQueueReceive(qSensorData, &data, portMAX_DELAY) == pdPASS) { /* 获取 OLED 互斥锁(防止多任务同时刷屏)*/ if (xSemaphoreTake(xMutex_OLED, pdMS_TO_TICKS(50)) == pdPASS) { SSD1306_Clear(); snprintf(buf, sizeof(buf), "T:%.1fC H:%.1f%%", data.temperature, data.humidity); SSD1306_PrintString(0, 0, buf); SSD1306_Refresh(); xSemaphoreGive(xMutex_OLED); } } } } /* ─── 主函数:创建所有任务后启动调度器 ─────────── */ int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); /* 创建队列(深度=5,每条消息 sizeof(SensorData_t) 字节)*/ qSensorData = xQueueCreate(5, sizeof(SensorData_t)); xMutex_OLED = xSemaphoreCreateMutex(); /* 创建任务(名称、栈字数、参数、优先级、句柄)*/ xTaskCreate(Task_Sensor, "Sensor", 256, NULL, 3, &hTask_Sensor); xTaskCreate(Task_Display, "Display", 256, NULL, 2, &hTask_Display); /* 启动调度器(永不返回)*/ vTaskStartScheduler(); while (1); /* 永远不会到这里 */ }
任务通知是 FreeRTOS v8.2+ 提供的轻量级替代方案,比信号量快 45%,无需额外内核对象:
/* 从 ISR 通知任务(最高效的 ISR→Task 同步方式)*/ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; if (GPIO_Pin == GPIO_PIN_0) { vTaskNotifyGiveFromISR(hTask_Sensor, &xHigherPriorityTaskWoken); /* 若被唤醒的任务优先级更高,请求上下文切换 */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } /* 任务中等待通知 */ void Task_WaitForButton(void *pvParams) { for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); /* 清零并等待 */ /* 按键事件处理 */ HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12); } }
/** * IWDG(独立看门狗):由 LSI(32kHz) 时钟驱动,即使主时钟故障也能工作 * 超时后强制系统复位,用于检测程序跑飞 * 喂狗操作:定期调用 HAL_IWDG_Refresh() */ IWDG_HandleTypeDef hiwdg; void IWDG_Init(void) { hiwdg.Instance = IWDG; hiwdg.Init.Prescaler = IWDG_PRESCALER_64; /* 32kHz/64 = 500Hz */ hiwdg.Init.Reload = 500; /* 500/500Hz = 1秒超时 */ HAL_IWDG_Init(&hiwdg); } /* FreeRTOS 喂狗任务:最低优先级,正常运行时能定期执行 */ void Task_WDG(void *pvParams) { for (;;) { HAL_IWDG_Refresh(&hiwdg); vTaskDelay(pdMS_TO_TICKS(500)); /* 每500ms喂一次,超时1秒 */ } }
xQueueCreate(len, size) — 创建队列xQueueSend(q, &data, timeout) — 发送xQueueReceive(q, &data, timeout) — 接收xSemaphoreCreateBinary() — 二值信号量xSemaphoreCreateMutex() — 互斥量xSemaphoreGive/Take(sem) — 释放/获取xEventGroupCreate() — 事件组xEventGroupSetBits(eg, bits) — 设置事件xTaskCreate(func, name, stack, param, pri, &hdl)vTaskDelete(handle) — 删除任务vTaskDelay(ticks) — 相对延时vTaskDelayUntil(&prev, period) — 绝对延时uxTaskGetStackHighWaterMark(h) — 栈余量vTaskSuspend/Resume(handle) — 挂起/恢复vTaskPrioritySet(h, priority) — 改优先级xTaskGetTickCount() — 获取系统节拍在 FreeRTOSConfig.h 中开启 configUSE_TRACE_FACILITY=1 和 configGENERATE_RUN_TIME_STATS=1,然后调用 vTaskGetRunTimeStats() 可以打印每个任务的 CPU 占用百分比。这是定位某任务占用 CPU 过多(导致其他任务饥饿)的最直接工具。另外,uxTaskGetStackHighWaterMark() 返回任务栈的最小剩余量,若为 0 则表示曾经溢出。
① 栈太小:任务函数调用层次深或有大型局部变量,导致栈溢出 → HardFault。建议先开启 configCHECK_FOR_STACK_OVERFLOW=2 捕获溢出。
② ISR 中调用非 FromISR 版本 API:如 xQueueSend() 而非 xQueueSendFromISR(),会导致调度器死锁。
③ Mutex 在 ISR 中使用:互斥量不能在 ISR 中 Take/Give,ISR 应使用二值信号量。
④ configMAX_SYSCALL_INTERRUPT_PRIORITY 设置错误:中断优先级高于此值的 ISR 不能调用 FreeRTOS API,否则破坏内核状态。