实战:多任务传感器网关
把前 11 章的任务、队列、信号量、事件组、软件定时器、中断延迟处理与低功耗全部串起来,搭建一个采集→处理→上报的三级流水线网关,给出完整架构、代码骨架与上线 checklist。
把前 11 章的任务、队列、信号量、事件组、软件定时器、中断延迟处理与低功耗全部串起来,搭建一个采集→处理→上报的三级流水线网关,给出完整架构、代码骨架与上线 checklist。
vTaskNotifyGiveFromISR 唤醒采集任务,重活在任务上下文做。按键用 EXTI 中断 + 事件组置位。既保证实时响应,又不在中断里干重活。vTaskDelayUntil 保证严格节拍,唤醒时刻尽量对齐以减少唤醒次数,延长深睡时间。| 对象/任务 | 类型 | 优先级/参数 | 作用 |
|---|---|---|---|
| MgrTask | 任务 | prio 5(最高) | 处理按键/告警等异步事件 |
| SensorTask | 任务 | prio 4 | 被中断唤醒,采原始数据入 rawQ |
| ProcTask | 任务 | prio 3 | 从 rawQ 取,滤波换算入 txQ |
| CommTask | 任务 | prio 2 | 从 txQ 取,上报云端+重试 |
| rawQ / txQ | 队列 | len 8~16 | 两级流水线缓冲 |
| i2cMutex / uartMutex | 互斥量 | — | 保护共享总线 |
| sysEG | 事件组 | 24 位 | 就绪状态 + 异步事件 |
| linkWatchdog | 软件定时器 | 单次 30s | 上报超时→重连 |
| healthTimer | 软件定时器 | 周期 60s | 打印统计/栈水位 |
#include "FreeRTOS.h" #include "task.h" #include "queue.h" #include "semphr.h" #include "event_groups.h" #include "timers.h" /* 事件位定义 */ #define EV_WIFI (1UL<<0) #define EV_TIME (1UL<<1) #define EV_CFG (1UL<<2) #define EV_READY (EV_WIFI|EV_TIME|EV_CFG) #define EV_BTN (1UL<<8) #define EV_ALARM (1UL<<9) typedef struct { uint32_t ts; int16_t raw[4]; } SensorMsg; typedef struct { uint32_t ts; float val[4]; } Packet; QueueHandle_t g_rawQ, g_txQ; SemaphoreHandle_t g_i2cMutex, g_uartMutex; EventGroupHandle_t g_sysEG; TimerHandle_t g_watchdog, g_health; TaskHandle_t g_sensorTask; void app_init(void) { g_rawQ = xQueueCreate(16, sizeof(SensorMsg)); g_txQ = xQueueCreate(8, sizeof(Packet)); g_i2cMutex = xSemaphoreCreateMutex(); g_uartMutex= xSemaphoreCreateMutex(); g_sysEG = xEventGroupCreate(); configASSERT(g_rawQ && g_txQ && g_i2cMutex && g_uartMutex && g_sysEG); g_watchdog = xTimerCreate("wd", pdMS_TO_TICKS(30000), pdFALSE, NULL, LinkTimeoutCb); g_health = xTimerCreate("hp", pdMS_TO_TICKS(60000), pdTRUE, NULL, HealthCb); xTaskCreate(MgrTask, "MGR", 256, NULL, 5, NULL); xTaskCreate(SensorTask, "SEN", 384, NULL, 4, &g_sensorTask); xTaskCreate(ProcTask, "PRC", 384, NULL, 3, NULL); xTaskCreate(CommTask, "COM", 512, NULL, 2, NULL); xTimerStart(g_health, 0); vTaskStartScheduler(); /* 启动调度,不返回 */ }
/* DMA 完成中断:只清标志 + 唤醒采集任务 */ void DMA1_Stream0_IRQHandler(void) { BaseType_t woken = pdFALSE; __HAL_DMA_CLEAR_FLAG(&hdma_i2c, DMA_FLAG_TCIF0); vTaskNotifyGiveFromISR(g_sensorTask, &woken); portYIELD_FROM_ISR(woken); } void SensorTask(void *arg) { TickType_t last = xTaskGetTickCount(); for (;;) { /* 严格 100ms 节拍触发一次采集 */ vTaskDelayUntil(&last, pdMS_TO_TICKS(100)); start_i2c_dma_read(); /* 启动 DMA */ ulTaskNotifyTake(pdTRUE, /* 等 DMA 完成中断 */ pdMS_TO_TICKS(50)); SensorMsg m; m.ts = xTaskGetTickCount(); if (xSemaphoreTake(g_i2cMutex, pdMS_TO_TICKS(20))) { read_raw(m.raw); /* 临界区:独占 I2C */ xSemaphoreGive(g_i2cMutex); /* 入队;满则丢最旧一帧策略视需求而定 */ xQueueSend(g_rawQ, &m, 0); } } }
void ProcTask(void *arg) { SensorMsg m; Packet p; for (;;) { /* 阻塞等原始数据;有数据才醒,利于低功耗 */ if (xQueueReceive(g_rawQ, &m, portMAX_DELAY) == pdTRUE) { p.ts = m.ts; for (int i = 0; i < 4; i++) p.val[i] = filter_and_scale(m.raw[i]); xQueueSend(g_txQ, &p, pdMS_TO_TICKS(10)); } } } void CommTask(void *arg) { Packet p; /* 等三大前置条件全就绪(AND)才开始上报 */ xEventGroupWaitBits(g_sysEG, EV_READY, pdFALSE, pdTRUE, portMAX_DELAY); xTimerStart(g_watchdog, 0); for (;;) { if (xQueueReceive(g_txQ, &p, portMAX_DELAY) == pdTRUE) { if (xSemaphoreTake(g_uartMutex, pdMS_TO_TICKS(100))) { bool ok = upload_with_retry(&p, 3); /* 失败重试 3 次 */ xSemaphoreGive(g_uartMutex); if (ok) xTimerReset(g_watchdog, 0); /* 喂狗 */ } } } }
/* 按键中断:给事件组置位 */ void EXTI0_IRQHandler(void) { BaseType_t woken = pdFALSE; __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); xEventGroupSetBitsFromISR(g_sysEG, EV_BTN, &woken); portYIELD_FROM_ISR(woken); } void MgrTask(void *arg) { for (;;) { EventBits_t b = xEventGroupWaitBits( g_sysEG, EV_BTN | EV_ALARM, pdTRUE, /* 处理后清位 */ pdFALSE, /* OR:任一事件即处理 */ portMAX_DELAY); if (b & EV_BTN) handle_button(); if (b & EV_ALARM) handle_alarm(); } } /* 看门狗回调:长时间没成功上报→触发重连(简短,重活转任务) */ void LinkTimeoutCb(TimerHandle_t t) { (void) t; xEventGroupClearBits(g_sysEG, EV_WIFI); /* 标记掉线 */ request_reconnect(); } /* 健康监控回调:定期打印统计与栈水位 */ void HealthCb(TimerHandle_t t) { (void) t; dump_cpu_usage(); printf("heap free=%u\r\n", (unsigned)xPortGetFreeHeapSize()); }
MgrTask(5) 最高——用户按键与告警要最快响应;SensorTask(4) 次之——采集有硬实时节拍不能被拖;ProcTask(3) 和 CommTask(2) 较低——它们阻塞在队列上,有数据才跑,慢一点由队列缓冲兜底。这种"越靠近硬件/用户、优先级越高,越靠后台、优先级越低"的排布,是数据流水线的通用范式。
本例采集入队用超时 0(满则丢弃当前帧),适合"实时性优先、可容忍偶尔丢点"的监测场景。若要求不丢数据,应给上游设合理阻塞超时并加大队列,或让上游降速;若要求只保留最新,可用队列覆写模式。务必显式设计满队列时的背压策略,不要留默认——这是流水线系统最容易埋雷的地方。
| 类别 | 检查项 |
|---|---|
| 中断 | 要调内核 API 的中断优先级数值 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY;ISR 末尾都有 portYIELD_FROM_ISR |
| 栈 | 每个任务用 uxTaskGetStackHighWaterMark 实测,留 20~30% 余量;开 configCHECK_FOR_STACK_OVERFLOW=2 |
| 堆 | 用 heap_4;xPortGetMinimumEverFreeHeapSize 有足够余量;开 malloc 失败钩子 |
| 互斥 | 共享外设(I2C/UART)全部互斥量保护;Take 都设超时不用永久等待 |
| 队列 | 长度按最坏突发校核;满队列背压策略明确(丢弃/阻塞/覆写) |
| 断言 | 开发/联调期 configASSERT 开启并停机;无断言触发 |
| 低功耗 | 任务尽量阻塞;开 Tickless;唤醒源配全;实测睡眠电流达标 |
| 健壮性 | 上报失败可重试;看门狗(软件+硬件 IWDG)覆盖;断网可恢复 |
| 监控 | 定期打印运行时统计与栈水位;IDLE 占比留有余量(不长期跑满) |
软件定时器看门狗只能发现"上报超时"这类逻辑层面的卡死,无法拯救"整个系统跑飞/死锁"的情况。生产设备必须再加一层硬件独立看门狗(如 STM32 的 IWDG):在最低优先级任务或空闲钩子里定期喂它,一旦系统整体卡死喂狗停止,硬件会强制复位。两层看门狗结合,才是无人值守嵌入式设备的可靠保障。
本章把全课知识串成一个真实网关:任务按"越靠近硬件/用户优先级越高"排布(Mgr 5 > Sensor 4 > Proc 3 > Comm 2);队列把采集→处理→上报连成两级流水线并做缓冲;互斥量保护共享的 I2C/UART 总线;事件组 AND 等待三大前置就绪、OR 等待按键/告警等异步事件;中断延迟处理让 ISR 只清标志+唤醒任务(NotifyGive/SetBitsFromISR + portYIELD_FROM_ISR);软件定时器做链路看门狗(喂狗式 Reset)与周期健康监控;vTaskDelayUntil 保证采集硬节拍;任务尽量阻塞以便进入 Tickless 低功耗。上线前对照 checklist 核查中断优先级、栈/堆余量、互斥覆盖、队列背压、断言、低功耗、健壮性与监控,并务必加一层硬件看门狗兜底。至此你已掌握 FreeRTOS 从内核原理到工程落地的完整链路——愿你的每一个任务都各司其职、准时醒来、从容睡去。