简介:基于STM32F103和FreeRTOS,集成OneNET云平台与ESP8266 Wi-Fi模块的嵌入式安防系统项目源码,适合嵌入式开发者、物联网学习者以及准备电子竞赛或课程设计的学生。项目覆盖从外设驱动到云平台通信的完整链路,包含ADC、定时器、LCD显示等外设程序,以及MQTT协议封装和FreeRTOS任务调度逻辑,可帮助读者掌握传感采集、任务划分、远程上下行数据交互等实际工程方法。
压缩包共273个文件,大小8.66MB,以C语言源码(65个c文件、71个h文件)和Keil工程配置(uvprojx、uvoptx、dep等)为主,同时包含编译生成的hex、axf、map、lst和调试相关文件,另有若干图片、文档与README说明,目录结构符合典型STM32工程组织方式。
目前已有466人学习使用,适合需要参考完整项目来快速搭建和验证同类安防系统的进阶开发者。
1. 这套智能家居安防系统值得动手复现的地方在哪
下班前在手机 App 上点一下“布防”,家里没人也能在火焰、烟雾、红外触发后的几秒内做出反应:本地蜂鸣器响起,云平台把报警信息推给业主。这套基于 STM32F103 加 FreeRTOS 加云平台的智能家居安防系统,核心不是把外围传感器堆上去,而是把三件事组织起来:STM32F103C8T6 负责采集红外、烟雾、门磁和火焰信号,FreeRTOS 保证报警链路不被慢速传感器和网络等待拖死,云平台承担远程状态同步与命令下发。对想从裸机切换到实时操作系统的开发者,或者正在做安防类产品原型、需要评估 MCU 资源怎么分配的工程师,把这条链路的细节拆开,收获比单纯跑例程大得多。
2. STM32F103 最小系统与工程搭建:先把外设和时钟整明白
2.1 为什么 STM32F103C8T6 够用:资源边界与外围分配
STM32F103C8T6 是 64KB Flash、20KB RAM、主频 72MHz 的 Cortex-M3 芯片,在智能家居网关、安防控制器这类中低端节点上非常常见。云平台接入走 MQTT over TCP 时,协议栈和缓冲会吃掉不少内存:FreeRTOS 内核本身就占 1~3KB,四个任务各自的栈按 512B~1KB 估算要 2~4KB,MQTT 收发缓冲区各留 1KB,JSON 打包缓冲区再给 1~2KB,剩下 20KB RAM 其实已经比较紧张。因此设计重点不是外设越全越好,而是每个引脚和每块内存都提前划定用途。
| 外设资源 | 连接模块 | 在系统中的职责 |
|---|---|---|
| USART1 | 调试串口 CH340 | 日志输出与本地命令 |
| USART2 | ESP8266 Wi-Fi 模组 | MQTT 数据通道 |
| GPIO + EXTI | PIR 红外、门磁、烟雾数字输出 | 快速中断输入 |
| TIM2_CH1/CH2 | 火焰、气流传感器频率输出 | 多路输入捕获测频率 |
| ADC1 单通道 | 烟雾传感器模拟输出 | 浓度量化采集 |
| GPIO 输出 | 蜂鸣器、继电器 | 声光报警与门锁控制 |
传感器模拟量如果走 ADC,要注意 STM32F103 的 VDDA 引脚必须接模拟电源,不能只接数字 3.3V,否则采样值会漂得没法用。火焰和气流类传感器若是频率输出型,用定时器做多路输入捕获比 ADC 更稳,后面给的示例里会用 TIM2 两个通道同时抓频率。
2.2 STM32F103 最小系统电路要点:晶振、复位、BOOT 与滤波
大多数入门板子能跑半年不崩,靠的是几个便宜元件没省。8MHz 晶体配两个 22pF 负载电容,NRST 引脚接 100nF 电容到地再加 10kΩ 上拉,BOOT0 通过 10kΩ 下拉到地,BOOT1 直接悬空或同样下拉。电源侧参照 ST 手册在 VDD 与地之间并 4.7μF 和 100nF,模拟电源 VDDA 单独用磁珠或 10Ω 电阻隔离后接 3.3V。
| 元件 | 典型值 | 作用 |
|---|---|---|
| HSE 晶振 | 8MHz | PLL 倍频到 72MHz |
| 负载电容 | 22pF×2 | 起振稳定 |
| NRST 电容/电阻 | 100nF + 10kΩ | 上电复位可靠 |
| BOOT0 下拉 | 10kΩ | 固定从 Flash 启动 |
| 电源去耦 | 4.7μF + 100nF | 抑制高频纹波 |
这里有个反直觉点:BOOT0 引脚内部本来有下拉,但很多量产板还是会在外部再加一个 10kΩ 下拉,原因是烧录器或杜邦线拔插时引脚悬空会偶发进 Bootloader,导致程序“跑飞”到系统存储器。调试阶段如果遇到下载后程序不运行,第一个检查对象就是 BOOT0 电平和复位脚电压。
2.3 工程搭建:基于 Keil 和 IAR 的标准外设库还是 HAL
旧项目的 FreeRTOS 移植模板绝大多数基于 STM32F10x 标准外设库 V3.5.0,资料全、网上可直接抄的例程多;STM32CubeMX 生成的 HAL 工程配置快,但 HAL 默认占用 SysTick,和 FreeRTOS 的时基存在冲突,需要额外做时基重映射。我的建议是:这个标题对应的安防系统里,如果希望把重心放在任务划分和云平台协议上,就选标准外设库;如果打算长期维护和换型号,HAL 更合适。
下面是标准外设库下 USART1 的初始化,调试日志全靠它:
void USART1_Config(void) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_9; // TX gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_10; // RX gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); usart.USART_BaudRate = 115200; usart.USART_WordLength = USART_WordLength_8b; usart.USART_StopBits = USART_StopBits_1; usart.USART_Parity = USART_Parity_No; usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None; usart.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, &usart); USART_Cmd(USART1, ENABLE); }代码里把 USART1 的时钟使能放在 APB2 总线上,GPIOA 的 Pin9 配成复用推挽输出,Pin10 配成浮空输入。USART1 的 TX 引脚不能漏配复用推挽,否则输出电平会被拉死,串口助手收不到任何数据。如果想把 printf 重定向到串口,Keil 下勾选 MicroLIB,重写 fputc 即可;IAR 则要改 __write 函数,两者不能复用同一套代码。
2.4 串口 1 和串口 3 的使用差异:APB 总线、中断向量与波特率
做调试串口和 Wi-Fi 串口时很容易在 USART1 和 USART3 之间踩坑。USART1 挂在 APB2 总线上,时钟 72MHz;USART3 挂在 APB1 总线上,当 APB1 预分频为 2 时外设时钟只有 36MHz。这个差异直接影响波特率寄存器 USART_BRR 的分频计算,相同波特率下两个外设写进 BRR 的数值完全不同,千万不要把 USART1 的初始化参数直接复制给 USART3。
另一个高频错误是中断服务函数名。USART1 对应 USART1_IRQHandler,USART3 对应 USART3_IRQHandler,它们分别在启动文件的向量表中注册。如果你的代码里写了 USART3_IRQHandler 但启动文件里没有这个向量,链接器会静默把它归到默认中断,串口中断永远不会触发。USART3 的初始化还要注意默认引脚映射是 PB10 和 PB11,如果不小心使能了重映射,TX 跑到 PD8/PD9,板子上压根没引出:
void USART3_Config(void) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_USART3, DISABLE); // 保持 PB10/PB11 gpio.GPIO_Pin = GPIO_Pin_10; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); gpio.GPIO_Pin = GPIO_Pin_11; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &gpio); usart.USART_BaudRate = 115200; usart.USART_WordLength = USART_WordLength_8b; usart.USART_StopBits = USART_StopBits_1; usart.USART_Parity = USART_Parity_No; usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None; usart.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART3, &usart); USART_Cmd(USART3, ENABLE); }RCC_APB1PeriphClockCmd 和 RCC_APB2PeriphClockCmd 是两套独立的时钟使能函数,APB1 外设写错到 APB2 会编译报错。判断方法是看外设的中断向量和 RCC 域名:USART1、SPI1、TIM1 在 APB2,USART2、USART3、SPI2、TIM2~TIM4 在 APB1。系统初始化里 SystemInit 会把 8MHz HSE 通过 PLL 倍频到 72MHz,再按 RCC_CFGR 分成 PCLK1=36MHz、PCLK2=72MHz,串口外设时钟以这两个总线为准。
3. FreeRTOS 移植与任务设计:实时内核不是把裸机 while 拆成几个函数
3.1 移植方式与 FreeRTOSConfig.h 关键配置
FreeRTOS 在 Cortex-M3 上的移植非常成熟,官方源码的 portable 目录里直接有 ARM_CM3 端口。工程里需要添加 tasks.c、queue.c、list.c、heap_4.c,以及 portable 目录下与编译器和内核对应的 port.c、portmacro.h 等文件。heap 选择方面,我一般用 heap_4,它支持合并相邻空闲内存,能有效减少长时间运行产生的碎片,正好适合安防系统这种长期不重启、任务频繁创建退出的场景。
#define configUSE_PREEMPTION 1 #define configTOTAL_HEAP_SIZE (10 * 1024) #define configMAX_PRIORITIES 7 #define configMINIMAL_STACK_SIZE 128 #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_TIMERS 1 #define configUSE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_16_BIT_TICKS 0 #define configCPU_CLOCK_HZ ((uint32_t)72000000) #define configTICK_RATE_HZ ((TickType_t)1000)configTOTAL_HEAP_SIZE 给到 10KB 是基于 C8T6 的 20KB RAM 倒推的:任务栈、队列和软件定时器都从这个堆里分配,留一半给全局变量、协议缓冲和栈底。configCHECK_FOR_STACK_OVERFLOW 设为 2 时,内核会在任务切换时主动检查栈指针是否越界,编译期和运行期的开销都比模式 1 大一些,但排查任务栈溢出更有效。configUSE_16_BIT_TICKS 必须设为 0,Cortex-M3 是 32 位内核,用 16 位 tick 会缩短 vTaskDelay 的最大延时上限。
3.2 用队列在传感器任务与云平台任务之间传递数据
裸机写法里传感器值常常是一个全局结构体,多个中断和主循环同时改写,读的时候会读到中间状态。FreeRTOS 队列解决的是生产者和消费者之间的同步与数据拷贝问题。安防系统里我把采集任务和上报任务拆开,中间放一个长度 10 的队列:
typedef struct { uint8_t ir; // 红外触发状态 uint8_t smokeDigital; // 烟雾数字输出 uint16_t flameFreq; // 火焰传感器频率 } SensorData_t; QueueHandle_t xSensorQueue; void vSensorTask(void *arg) { SensorData_t data; for (;;) { data.ir = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4); data.smokeDigital = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3); data.flameFreq = TIM_GetCapture1(TIM2); xQueueSend(xSensorQueue, &data, pdMS_TO_TICKS(20)); vTaskDelay(pdMS_TO_TICKS(50)); } } void vCloudTask(void *arg) { SensorData_t data; for (;;) { if (xQueueReceive(xSensorQueue, &data, pdMS_TO_TICKS(200)) == pdPASS) { // 把 data 封包成 JSON,走 MQTT 发布 } } }vSensorTask 以 50ms 为周期采集一次,xQueueSend 的阻塞时间 20ms 意味着队列满时最多等 20ms 就返回,不会让采集任务死在写队列上。vCloudTask 等待数据的最长时间是 200ms,这个值根据网络中继策略调整:Wi-Fi 信号差时上报任务卡在网络发送上,队列会把数据暂存起来,采集任务不受影响。队列深度的选择要考虑最坏情况,如果云端断线 10 秒,50ms 一条数据就有 200 条积压,长度 10 的队列肯定不够,所以断线时云任务要主动清空队列或做本地压缩上报。
3.3 互斥量保护串口打印,二值信号量响应外部报警中断
多个任务同时 printf 会互相穿插,日志完全没法看。常见做法是包一个带互斥量的日志函数:
SemaphoreHandle_t xLogMutex; void Log_Print(const char *msg) { xSemaphoreTake(xLogMutex, portMAX_DELAY); printf("%s\r\n", msg); xSemaphoreGive(xLogMutex); }串口打印本身耗时较长,用互斥量比用临界区要合理,临界区会把所有中断都关掉,影响云平台串口接收和定时器输入捕获。互斥量在这里还要注意优先级继承:如果高优先级任务等锁,低优先级任务暂时被提升到高优先级,尽快释放锁,避免高优先级任务被中优先级任务饿死。
红外和门磁这类报警输入用外部中断更及时。中断服务函数里不能调用 xSemaphoreGive,要用专门的中断版本:
SemaphoreHandle_t xAlarmSem; void EXTI4_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(xAlarmSem, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); EXTI_ClearITPendingBit(EXTI_Line4); }xSemaphoreGiveFromISR 的第二个参数是一个返回值标志,如果唤醒的高优先级任务确实需要切换,portYIELD_FROM_ISR 会触发一次 PendSV,中断退出后立即完成上下文切换,报警任务能在一个 tick 内被调度。这里不要手动清中断过多,EXTI_ClearITPendingBit 是最后一步,避免清标志的瞬间又进来一次边沿中断造成重复触发。
3.4 堆栈溢出检测:栈高水位与 hook 函数的配合
任务栈给大了浪费 RAM,给小了现场翻车,所以要在调试阶段把每个任务的真实栈用量打出来:
void Log_TaskStackWaterMark(TaskHandle_t task, const char *name) { uint32_t freeWords = uxTaskGetStackHighWaterMark(task); printf("%s stack free: %u words (%.2f%% used)\r\n", name, freeWords, (1.0f - (float)freeWords / 128.0f) * 100.0f); }uxTaskGetStackHighWaterMark 返回的是从这个任务启动以来剩余栈空间的最小值,单位是字,不是字节,Cortex-M3 上 1 字为 4 字节。调用它不会清零记录值,所以可以周期性地在每个任务里调用一次,观察峰值内存消耗。配合 configCHECK_FOR_STACK_OVERFLOW 设为 2,系统运行几十个小时后如果还稳定,说明栈分配基本合理。
常见错误是任务函数里定义大型局部数组,比如 char buf[512],栈空间瞬间被吃掉一大块。FreeRTOS 任务栈分配按字对齐,100 字的任务栈在 Keil 里默认 8 字节对齐,实际可用空间和理论值有细微差异,栈高水位统计能把这些差异暴露出来。
3.5 移植中最高频的 3 个报错与现象
第一,Keil 报.\obj\freertos.hex: error: Q0147e: failed to create directory .\obj\freertos。这个通常不是代码问题,而是工程里设置了 Obj 输出目录,但该目录层级不存在,Keil 创建失败。解决办法是在工程设置 Output 页面把目录改成已存在路径,或提前手工建好 obj 目录。
第二,HAL 库工程里 FreeRTOS 跑不起来,卡死在 SysTick_Handler。HAL_Delay 和 FreeRTOS 都要抢占 SysTick,需要在 CubeMX 里把 HAL 时基改为 TIM6 或 TIM7,把 SysTick 完整让给 FreeRTOS。标准外设库没有这个冲突,但中断向量表里 SysTick_Handler 要指向 FreeRTOS 的 xPortSysTickHandler。
第三,任务切换后频繁进 HardFault。先查启动文件里 PendSV_Handler、SVC_Handler 是否被原厂库的占位中断函数占用,再查 FreeRTOSConfig.h 中 configKERNEL_INTERRUPT_PRIORITY 和 configMAX_SYSCALL_INTERRUPT_PRIORITY 是否与工程里的 NVIC 优先级分组匹配。Cortex-M3 上 FreeRTOS 要求中断优先级用高 4 位,常见组合是内核中断优先级 255,可调用系统服务的最高中断优先级不高于 5。
4. 云平台接入方案:STM32F103 怎么跟云端安全地说话
4.1 接入通道选型:ESP8266、NB-IoT 还是以太网
智能家居安防节点通常没有有线网络条件,常见接入通道是 MCU 通过串口外挂通信模组。ESP8266 成本最低、资料最多,适合 Wi-Fi 覆盖家庭;NB-IoT 模组适合经常断电断网的偏远场景,但月卡成本高;有线以太网最稳,适合带底座的安防网关。
| 通道方案 | 成本 | 功耗 | 实时性 | 典型场景 |
|---|---|---|---|---|
| ESP8266 AT 固件 | 低 | 较高 | 秒级 | 家庭 Wi-Fi 智能家居 |
| NB-IoT 模组 | 中 | 低 | 秒级 | 独立门磁、烟雾报警 |
| W5500 以太网 | 中 | 低 | 毫秒级 | 带网口的安防网关 |
| 4G Cat.1 | 高 | 高 | 秒级 | 移动布防、室外监控 |
对这个标题的系统,ESP8266 是默认首选。它有三类工作模式:透传模式只负责把串口数据变成 TCP 流,MQTT 协议栈要放在 MCU 上实现;AT 固件自带 MQTT 指令,MCU 只拼命令串,开发量小但 AT 指令占用 ROM 和 RAM;非固件 SDK 模式则相当于在 ESP8266 上独立跑协议,STM32F103 只做业务控制。我一般用第二种,AT 加 MQTT 指令的方式最适合在 20KB RAM 的 MCU 上实现云平台接入。
4.2 MQTT 协议流程与设备接入参数
MQTT 是发布订阅协议,设备作为客户端连接 broker,上报数据时发布到某个主题,接收命令时订阅另一个主题。STM32F103 上实现 MQTT 客户端只需要一个 TCP socket,控制报文头只有几个字节,比 HTTP 的文本头节省太多流量和内存。
以 OneNET 这类云平台为例,接入前先要准备好三样东西:产品 ID、设备 ID、设备密钥。它们在平台控制台创建产品和设备时生成,MCU 固件里通常写死并用宏定义隔离。MQTT 连接参数如下表,具体字段以云平台接入文档为准:
| 参数 | 示例 | 说明 |
|---|---|---|
| Broker 地址 | 平台分配 | 物联网接入服务器域名或 IP |
| 端口 | 平台分配 | MQTT 端口,TLS 是 8883,明文是 1883 |
| Client ID | 产品ID_设备ID | 全局唯一 |
| Username | 产品ID | 认证用户名 |
| Password | 设备密钥或 token | 连接鉴权 |
上报数据的负载用 JSON 最简单,安防事件可以这样组织:
{ "type": "alarm", "dev": "livingroom", "ts": 1700000000, "ir": 1, "smoke": 0, "flameFreq": 42 }平台收到后按规则存储,App 端通过平台的数据推送接口实时拿到报警。命令下发方向正好相反,平台给设备下发布防或继电器控制指令,设备订阅对应主题后解析 JSON,执行本地动作并回一条 ACK。
4.3 ESP8266 用 AT 指令接入云平台:完整连接序列
ESP8266 的 AT 固件版本较多,2.0 以上版本通常支持 AT+MQTTUSERCFG 和 AT+MQTTCONN 这两个扩展指令。调试阶段用串口工具手动敲指令太慢,我用 Python 脚本自动跑一遍完整流程,同时验证每一条命令的 OK 返回值:
import serial import time ser = serial.Serial("COM5", 115200, timeout=1) def send(cmd, wait=1.5, ok_keyword="OK"): ser.write((cmd + "\r\n").encode()) time.sleep(wait) resp = ser.read(ser.in_waiting or 64).decode(errors="ignore") print(f"> {cmd}\n< {resp.strip()}") return ok_keyword in resp send("AT", wait=1) send("ATE0", wait=0.5) # 1. 设置 Station 模式并连接家庭 Wi-Fi send("AT+CWMODE=1", wait=0.5) send('AT+CWJAP="YOUR_SSID","YOUR_PASSWORD"', wait=8) # 2. 配置 MQTT 用户参数 send('AT+MQTTUSERCFG=0,1,"client_id","username","password",0,0,""', wait=1) # 3. 建立 MQTT 连接,最后参数 1 表示自动重连 send('AT+MQTTCONN=0,"broker_host",6002,1', wait=2)AT+MQTTUSERCFG 的参数从左到右是连接 ID、协议类型(1 表示 MQTT)、Client ID、用户名、密码、证书相关和路径。AT+MQTTCONN 里的 broker 地址和端口以云平台接入文档为准,不要凭记忆写死。每次发送 AT 指令后都要读返回并检查 OK,连续两次失败就该让 MCU 重新触发 CWJAP 而不是盲目重试 MQTTCONN。
4.4 STM32F103 端 MQTT 发布与订阅的代码封装
MCU 端不把整个 MQTT 报文展开,而是直接调 ESP8266 的 AT 指令,封装成两个函数。发布函数构造 AT+MQTTPUB,数据长度必须和 JSON 实际字节数一致:
void MQTT_Publish(const char *topic, const char *json) { char cmd[96]; int len = strlen(json); sprintf(cmd, "AT+MQTTPUB=0,\"%s\",%d,1,0", topic, len); ESP8266_SendCmd(cmd); // 带 OK/ERROR 判断 ESP8266_SendRaw(json); // 发送 JSON 数据体 ESP8266_WaitResp("OK", 1000); }AT+MQTTPUB 的第四个参数是 QoS,1 表示至少一次,broker 收到后会返回 PUBACK,设备端要能容忍重复消息;如果平台主题只支持 QoS0,就把这个参数改成 0,省掉应答等待。第五个参数 retain 表示是否保留遗嘱消息,安防场景一般不用。订阅命令则把平台下发的主题和回调绑定,在串口 2 的中断里解析 +MQTTSUB 开头的事件,取出 JSON 后交给布防状态机处理。
调试阶段最容易犯的错误是 JSON 里字符串转义符处理不当,导致平台解析不了。建议在 STM32F103 侧用编译期字符串常量维护一个示例帧,先拿网络调试助手模拟 broker,确认 MCU 发出去的报文能被完整接收,再切到真实云平台。
5. 布防状态机、断线重连与任务级调试技巧
把报警逻辑放进 FreeRTOS 任务后,不能再用 if 嵌套堆需求,我习惯用一个小状态机区分撤防、布防延时、布防和报警四个状态。布防延时期间红外触发只记录不报警,给业主离开门留时间;报警状态触发后,连续确认 10 次采集值都有效才置位声光输出,避免猫或窗帘晃动造成误报。状态转换的结果直接推入云平台任务,用一条带事件类型的结构体走同一个队列上报。
断线重连要做指数退避。第一次失败隔 2 秒重连,第二次 4 秒,最多 30 秒封顶,每次 AT 连接重置后清空 Wi-Fi 协商缓存。重连期间不要丢最新数据,把报警事件先写进一个环形缓冲区,连接恢复后按时间戳补报。云平台侧如果没有数据续传能力,至少要保证最新一条事件不丢,这对安防语义足够。
调试 FreeRTOS 任务卡死和优先级问题时,别只盯着逻辑分析仪,先打开任务追踪:
portBASE_TYPE uxTaskGetSystemState(void); void vTaskList(char *pcWriteBuffer);开启 vTaskList 要求在 FreeRTOSConfig.h 里同时定义 configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS 为 1,然后把输出缓冲打印出来,能看到每个任务的状态、优先级和剩余栈。状态列出现 B 表示阻塞在队列或信号量,R 表示就绪;如果云平台任务一直处于 R 且占满 CPU,多半是它在轮询等待串口接收,应该把等待改成信号量或事件组。
更直观的周期验证方法是在任务循环末尾翻转一个空闲 GPIO,示波器挂上去看方波周期是否符合 vTaskDelay 设定。如果发现方波周期被拉长数百微秒以上,基本可以认定有更高优先级任务或中断在长时间占用 CPU。用这根线加串口时间戳一起看调度器被卡在哪,比单靠调试器断点更容易复现偶发现象。
本文还有配套的精品资源,点击获取