免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32+FreeRTOS智能安防系统实战:从传感器采集到云平台报警链路设计

STM32+FreeRTOS智能安防系统实战:从传感器采集到云平台报警链路设计 简介基于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 内核本身就占 13KB四个任务各自的栈按 512B1KB 估算要 24KBMQTT 收发缓冲区各留 1KBJSON 打包缓冲区再给 12KB剩下 20KB RAM 其实已经比较紧张。因此设计重点不是外设越全越好而是每个引脚和每块内存都提前划定用途。外设资源连接模块在系统中的职责USART1调试串口 CH340日志输出与本地命令USART2ESP8266 Wi-Fi 模组MQTT 数据通道GPIO EXTIPIR 红外、门磁、烟雾数字输出快速中断输入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 晶振8MHzPLL 倍频到 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 总线上时钟 72MHzUSART3 挂在 APB1 总线上当 APB1 预分频为 2 时外设时钟只有 36MHz。这个差异直接影响波特率寄存器 USART_BRR 的分频计算相同波特率下两个外设写进 BRR 的数值完全不同千万不要把 USART1 的初始化参数直接复制给 USART3。另一个高频错误是中断服务函数名。USART1 对应 USART1_IRQHandlerUSART3 对应 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 在 APB2USART2、USART3、SPI2、TIM2~TIM4 在 APB1。系统初始化里 SystemInit 会把 8MHz HSE 通过 PLL 倍频到 72MHz再按 RCC_CFGR 分成 PCLK136MHz、PCLK272MHz串口外设时钟以这两个总线为准。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 必须设为 0Cortex-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明文是 1883Client 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 以上版本通常支持 ATMQTTUSERCFG 和 ATMQTTCONN 这两个扩展指令。调试阶段用串口工具手动敲指令太慢我用 Python 脚本自动跑一遍完整流程同时验证每一条命令的 OK 返回值import serial import time ser serial.Serial(COM5, 115200, timeout1) def send(cmd, wait1.5, ok_keywordOK): ser.write((cmd \r\n).encode()) time.sleep(wait) resp ser.read(ser.in_waiting or 64).decode(errorsignore) print(f {cmd}\n {resp.strip()}) return ok_keyword in resp send(AT, wait1) send(ATE0, wait0.5) # 1. 设置 Station 模式并连接家庭 Wi-Fi send(ATCWMODE1, wait0.5) send(ATCWJAPYOUR_SSID,YOUR_PASSWORD, wait8) # 2. 配置 MQTT 用户参数 send(ATMQTTUSERCFG0,1,client_id,username,password,0,0,, wait1) # 3. 建立 MQTT 连接最后参数 1 表示自动重连 send(ATMQTTCONN0,broker_host,6002,1, wait2)ATMQTTUSERCFG 的参数从左到右是连接 ID、协议类型1 表示 MQTT、Client ID、用户名、密码、证书相关和路径。ATMQTTCONN 里的 broker 地址和端口以云平台接入文档为准不要凭记忆写死。每次发送 AT 指令后都要读返回并检查 OK连续两次失败就该让 MCU 重新触发 CWJAP 而不是盲目重试 MQTTCONN。4.4 STM32F103 端 MQTT 发布与订阅的代码封装MCU 端不把整个 MQTT 报文展开而是直接调 ESP8266 的 AT 指令封装成两个函数。发布函数构造 ATMQTTPUB数据长度必须和 JSON 实际字节数一致void MQTT_Publish(const char *topic, const char *json) { char cmd[96]; int len strlen(json); sprintf(cmd, ATMQTTPUB0,\%s\,%d,1,0, topic, len); ESP8266_SendCmd(cmd); // 带 OK/ERROR 判断 ESP8266_SendRaw(json); // 发送 JSON 数据体 ESP8266_WaitResp(OK, 1000); }ATMQTTPUB 的第四个参数是 QoS1 表示至少一次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。用这根线加串口时间戳一起看调度器被卡在哪比单靠调试器断点更容易复现偶发现象。本文还有配套的精品资源点击获取
返回列表