
简介基于STM32微控制器与UCOS实时操作系统面向阿里云物联网平台的家庭安全防控系统完整源码包专为电子、计算机、物联网等专业的毕业设计、课程设计及项目实训准备要求读者具备一定C语言和单片机开发基础。压缩包共75个文件以35个C源文件、39个H头文件为核心另附1份Markdown项目说明文档整体仅94KB源码紧凑、注释规范便于直接导入编译工程进行学习。项目采集层覆盖RC522射频识别、DHT11温湿度、SR04超声波测距、OV2640摄像头、火焰传感、OLED显示等硬件驱动并利用UCOS进行多任务划分配合定时器、中断、看门狗、FLASH存储、WiFi通信等底层资源实现本地控制和阿里云数据双向交互。整个工程按HARDWARE、APP等目录组织各功能模块解耦清晰可以根据需要裁剪或添加传感器。目前已有134人学习浏览适合作为理解RTOS调度、嵌入式外设驱动和物联网云平台接入的实战参照既可用于验收答辩也可作为二次开发安防报警或智能家居系统的起点。1. 这套系统解决的不只是“有人闯入就报警”家庭安全防控系统的第一反应往往是“检测到人体就推送告警”但真正做过 STM32 物联网项目的人都知道传感器误报率、上报时延和云端断连恢复这几个问题任何一个都能让整套系统变成摆设。这个项目标题里的三个关键词——STM32 负责底层采集与控制、UCOS 提供多任务调度、阿里云物联网平台承担设备接入与消息推送——组合在一起才构成一个完整的、可远程监管的家庭安全防护方案本地有实时响应云端有历史记录手机端能收到主动推送。这篇文章面向的是已经跑过流水灯、能独立完成串口打印的开发者目标是在拿到源码包之后不只看懂每个文件是干什么的还能说出“为什么这个任务要开 512 字节栈”“为什么温湿度上报周期是 5 秒而不是 1 秒”“断网重连的退避时间为什么是 30 秒起步”这类设计决策。文中给出的代码片段和参数都按这套系统的典型实现来写不依赖仓库里的原文件你可以在自己的工程里直接对照修改。2. 基于 UCOS 的任务划分先想清楚有哪些事要做再谈优先级2.1 为什么不用裸机 while 循环而是引入 UCOS裸机开发常驻一个主循环检测火焰传感器、读取温湿度、处理按键、刷新 OLED 都按顺序轮流执行。当代码膨胀到 3000 行以上、外设超过四个时最头疼的问题不是功能做不出来而是某个传感器在阻塞等待应答时其他事件全部被卡住。比如 DHT22 的读时序要求主机拉低总线至少 18ms这十几毫秒里如果恰好有人体红外触发系统根本无法及时置位报警标志。引入 UCOS 之后每个独立功能变成独立任务由内核按优先级调度。这个家庭安全防控系统里典型的任务划分是最高优先级给告警处理包括蜂鸣器驱动和继电器动作中间给传感器采集和 OLED 刷新最低给云平台通信。这样的优先级顺序是故意的——告警属于毫秒级响应事件云通信属于可以容忍几百毫秒延迟的事情如果反着分本地告警就会被网络阻塞拖垮。2.2 一个可复用的任务优先级表和栈分配参数以下是一个参考的分任务表取自同类项目最常见的配置你可以按自己的外设资源增删。任务优先级数值越小优先级越高UCOS-III 中数值范围是 1 到 OS_CFG_PRIO_MAX - 1。任务名优先级栈大小字节周期/触发方式职责Task_Alarm3256信号量触发驱动蜂鸣器与继电器标记本地报警状态Task_Sensor5512200ms 周期读取火焰、人体红外、温湿度传感器Task_Display7256500ms 周期刷新 OLED 显示当前温度、湿度和设备状态Task_Cloud910245s 周期事件触发组装 JSON、发布 MQTT、处理下行命令Task_KeyScan1112820ms 周期扫描按键处理设防/撤防切换栈大小不是随手写的。Task_Cloud 里调用了阿里云 C-SDK 的 MQTT 发布接口这个函数内部有较深的调用栈加上格式化 JSON 时申请的临时缓冲区1024 字节是安全值。而 Task_KeyScan 只做 GPIO 读取和延时消抖128 字节已经够用。如果你在调试时发现任务运行一段时间后卡死优先怀疑栈溢出——UCOS-III 会在任务创建时自动填充特殊字节查看任务信息里的StkFree值就能确认。2.3 告警任务与传感器任务之间用信号量还是消息队列告警事件有两个来源人体红外传感器触发、火焰传感器触发。前者代表有人闯入后者代表火灾风险。两者都需要抢占当前流程去处理但处理动作不同——人体触发可能只是短暂经过火焰触发必须立即切断燃气阀门继电器。代码里常见的做法是用两个二值信号量分别表示两种事件传感器任务在检测到对应电平变化时调用OSSemPost告警任务阻塞在OSSemPend上。下面的代码展示了告警任务和火焰检测的协作关系OS_SEM sem_alert_fire; OS_SEM sem_alert_pir; void Task_Alarm(void *p_arg) { OS_ERR err; while (1) { /* 同时等待两类告警信号量谁先来就处理谁 */ OSSemPend(sem_alert_fire, 0, OS_OPT_PEND_BLOCKING, 0, err); if (err OS_ERR_NONE) { /* 火焰告警拉高继电器引脚切断燃气同时鸣叫 */ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); /* 置位标志位供云平台任务下次上报使用 */ g_alarm_flags | ALARM_FLAG_FIRE; OSTimeDlyHMSM(0, 0, 3, 0, OS_OPT_TIME_HMSM_STRICT, err); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); } } }这里不用消息队列的原因在于告警事件本身不含数据载荷只需要一个“发生了”的布尔信号。如果用消息队列传递一个无意义的结构体只是在浪费 RAM 和 CPU 周期。等到需要把“火焰传感器具体数值”或“是哪一路 PIR 触发的”传给云端时再从信号量升级为消息队列不迟。提示告警任务里不要直接调用 HAL_Delay。OSTimeDlyHMSM会让出 CPU 给低优先级任务而 HAL_Delay 是忙等待两者在 UCOS 环境下的行为有本质区别。3. 传感器采集与告警判定采样周期和滤波参数才是防误报的核心3.1 温湿度采集的失败重试和超时保护DHT22 是单总线设备STM32 通过一个 GPIO 口模拟时序读取数据。它的典型问题是总线被干扰时读到的数据校验和不通过或者设备无响应时主机陷入死循环。裸机代码里最常见的问题是 GPIO 配置不当导致读时序失败后程序卡在等待电平跳变的 while 循环里。UCOS 环境下的解决思路是给读操作加超时计数超时后返回错误码由传感器任务决定是否丢弃本次数据。一个可靠实现的伪代码逻辑是uint8_t DHT22_Read(float *temp, float *humi) { uint8_t data[5] {0}; /* 主机拉低 18ms 作为起始信号 */ DHT22_Start(); /* 等待从机响应超时 200us */ uint32_t timeout 200; while (HAL_GPIO_ReadPin(DHT22_GPIO, DHT22_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 1; } /* 读取 40 位数据每一位先拉低 50us然后高电平持续时间决定 0 或 1 */ for (int i 0; i 40; i) { timeout 50; while (HAL_GPIO_ReadPin(DHT22_GPIO, DHT22_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 2; } delay_us(30); data[i / 8] (data[i / 8] 1) | (HAL_GPIO_ReadPin(DHT22_GPIO, DHT22_PIN) GPIO_PIN_SET); } /* 校验最后一位等于前四个字节之和的低 8 位 */ if (data[4] ! ((data[0] data[1] data[2] data[3]) 0xFF)) return 3; *humi ((data[0] 8) | data[1]) / 10.0f; *temp ((data[2] 8) | data[3]) / 10.0f; return 0; }参数说明超时值用的是循环次数而非具体微秒数是因为不同主频下同样的循环消耗时间不同。如果你在 72MHz 主频下调试发现偶尔进入超时分支先确认delay_us函数是否被正确配置为微秒级精确延时很多时候问题出在延时函数本身被编译优化掉了。3.2 火焰传感器和 PIR 的滤波逻辑连续触发 N 次才确认告警模拟量输出的火焰传感器模块在环境光线剧烈变化时输出值会抖动。一个简单有效的滤波是“连续采样滑动窗口”每次采集到的 ADC 值放入环形缓冲区只有连续 5 次都低于阈值才确认火焰存在。这个阈值通过板载电位器调节不同环境下差异很大所以不能把阈值写死在代码里。这个项目里通常的做法是在 OLED 上显示“当前 ADC 原始值”和“当前阈值”通过按键进入配置模式调节阈值。以下是滑动窗口滤波的核心代码#define FLAME_WINDOW_SIZE 5 uint16_t flame_buf[FLAME_WINDOW_SIZE]; uint8_t flame_idx 0; uint8_t Flame_Confirmed(uint16_t threshold) { uint8_t count 0; AD_Value Read_Flame_ADC(); flame_buf[flame_idx] AD_Value; flame_idx (flame_idx 1) % FLAME_WINDOW_SIZE; for (int i 0; i FLAME_WINDOW_SIZE; i) { if (flame_buf[i] threshold) count; } return (count 3) ? 1 : 0; }注意这里没有用经典的滑动平均值滤波而是用“低于阈值的次数占比”。原因是火焰传感器的 AD 值在安全状态时是一个稳定的高值在火焰靠近时才会下降。如果直接做平均一次突刺就可能拉低平均值导致误判。用计数方式只有持续半个采样周期以上出现低值才能触发告警天然滤掉了瞬间干扰。3.3 G-Sensor 被闯入检测和本地声光告警的配合有些方案会在门或窗户上安装加速度传感器检测震动来判断“有人撬门”。G-Sensor比如 ADXL345通过 I2C 接口连接 STM32当检测到超过阈值的加速度突变时产生中断信号。这里的核心参数是阈值的选取——门被正常打开时会产生约 0.5g 的加速度变化被强行撬动时通常超过 1.5g而楼上装修的震动可能只有 0.2g。检测逻辑是等待硬件中断然后读取 X/Y/Z 轴数据计算合成加速度幅度void Gsensor_Int_Handler(void) { OS_ERR err; /* 中断服务函数里只做信号量通知具体读取放在任务中 */ OSSemPost(sem_alert_vibrate, OS_OPT_POST_1, err); } void Task_Vibrate(void *p_arg) { OS_ERR err; while (1) { OSSemPend(sem_alert_vibrate, 0, OS_OPT_PEND_BLOCKING, 0, err); /* 读取三轴数据并计算合成幅度 */ Read_ADXL345_XYZ(x, y, z); g_accel_mag sqrt(x*x y*y z*z); if (g_accel_mag 1.5f) { g_alarm_flags | ALARM_FLAG_VIBRATE; } } }4. 阿里云物联网平台接入三元组、Topic 和物模型数据解析4.1 设备上线前的准备在阿里云控制台创建产品和设备在阿里云物联网平台控制台你需要先创建产品选择“直连设备”节点类型数据格式选“Alink JSON”。创建完成后在产品下添加设备获取设备证书三元组ProductKey、DeviceName、DeviceSecret。这三个字符串是设备连接云端的唯一凭证在 STM32 固件中必须烧录到 Flash 的指定区域不要直接硬编码在公开的源码文件里。接入地址的格式是${ProductKey}.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883。如果设备所在地域不是上海要换成对应地域的接入域名。在 STM32 上使用 MQTT 协议连接最常用的有两种方式直接移植 Eclipse Paho MQTT 嵌入式 C 库或者使用阿里云官方提供的 Link SDK 裁剪版。前者更加轻量代码量少RAM 占用大约 10KB后者自带物模型解析、OTA 和 NTP 时间同步但需要按阿里云要求的 SDK 版本和编译器配合。4.2 基于 Paho 实现的三元组认证与发布订阅以下代码展示如何在 STM32 上用改写过底层网络接口的 Paho 库连接阿里云。注意阿里云 MQTT 的用户名和密码不是自定义的而是由三元组派生出的固定格式字符串。// 连接参数构造这是阿里云平台独有的认证方式 void aliyun_mqtt_init(Client *c, Network *n) { char username[64] {0}; char password[64] {0}; char client_id[128] {0}; // username 固定为设备证书组合deviceNameproductKey sprintf(username, %s%s, DEVICE_NAME, PRODUCT_KEY); // 若使能一机一密password 为 base64(HMAC-SHA1(deviceSecret, content)) // content 为 clientId client_id deviceName device_name productKey product_key // 未使能一机一密时 password 固定为 deviceSecret sprintf(password, %s, DEVICE_SECRET); // clientId 需要带 securemode 后缀固定为 3TLS 直连模式 sprintf(client_id, %s|securemode3,signmethodhmacsha1,timestamp0|, DEVICE_NAME); MQTTClientInit(c, n, 1024, 1024); MQTTConnect(c, username, password, client_id); }参数说明securemode3表示 TCP 直连 MQTT 端口1883不用 TLS前提是云平台允许该产品使用非加密连接。如果你用的是 8883 端口走 TLSsecuremode改为 2。HMAC-SHA1 签名串的拼接顺序有严格要求clientId后面跟的参数值必须和 client_id 里带的保持一致否则云端签名校验失败返回错误码 5。4.3 属性上报和事件上报的物模型 Topic阿里云的物模型定义了属性Property、事件Event和服务Service三类数据。本系统中最常用的是属性上报Topic 结构为/sys/${productKey}/${deviceName}/thing/event/property/post上报数据需要包含物模型标识符和值。如果产品中已经定义了“温度”属性标识符为Temperature那么消息格式为{ id: 123, version: 1.0, params: { Temperature: 26.5, Humidity: 58.3, FireAlarm: 0, PirAlarm: 1 }, method: thing.event.property.post }设备端上报之后云平台回复确认消息订阅这个回复 Topic 可以判断上报是否成功void parse_cloud_reply(char *json_str) { cJSON *root cJSON_Parse(json_str); cJSON *code cJSON_GetObjectItem(root, code); if (code ! NULL code-valueint 200) { /* 上报成功清除待发送标志 */ g_report_pending 0; } cJSON_Delete(root); }4.4 断线重连的参数与本地缓存策略家庭网络中路由器重启、WiFi 模块不稳定都会导致设备断开连接。重连逻辑的两个关键参数是重连尝试间隔和重连次数上限。常见策略是第一次断线立即重连失败后等待 10 秒再试连续失败则每次把等待时间翻倍直到最多 300 秒封顶也就是指数退避。这个策略能避免设备在路由器未恢复时高频重复拨号导致死循环。在断线期间传感器数据不能丢弃需要在本地维护一个 FIFO 队列#define CACHE_EVENT_MAX 20 typedef struct { float temperature; float humidity; uint8_t alarmFlags; uint32_t timestamp; } cache_event_t; cache_event_t event_cache[CACHE_EVENT_MAX]; uint8_t cache_head 0, cache_tail 0; void cache_push(cache_event_t *evt) { if ((cache_tail 1) % CACHE_EVENT_MAX cache_head) { /* 队列满丢弃最旧数据保留最新告警 */ cache_head (cache_head 1) % CACHE_EVENT_MAX; } event_cache[cache_tail] *evt; cache_tail (cache_tail 1) % CACHE_EVENT_MAX; }重连成功后按顺序补发缓存的数据。注意补发时仍然要携带原始时间戳timestamp云端才能正确记录事件发生时间而不是上报时间。5. 进阶实测方法上报时延如何测量云端联动规则怎么配置跑通基础通信之后系统的实际可用性取决于两个指标从传感器触发到云端收到消息的端到端时延以及本地告警与云端告警的一致性。测量时延的最直接方法是在 STM32 的传感器中断服务里置位一个 GPIO 引脚延时计数同时在云端日志里找到该条消息的接收时间两者差值就是链路时延。常见结果参考本地局域网内 Wi-Fi 模块 MQTT 上报的时延在 200-500ms 之间如果超过 1 秒优先排查无线信号强度和 MQTT 发布时是否需要等待 QoS1 确认。阿里云物联网平台的消息日志里会记录每一条上行消息的完整链路耗时包含设备发送、云端接收、规则引擎处理三部分在产品详情页的“日志服务”里可以按时间筛选排查。时延异常的往往不是网络而是 STM32 端 JSON 序列化太慢。如果你用的是 cJSON 库频繁调用cJSON_CreateObject和cJSON_CreateNumber会产生大量动态内存碎片影响后续任务的栈分配。解决方法是复用一个已经分配好的 cJSON 对象每次更新字段值而不是重新创建整个 JSON 树。云端联动规则的做法是在阿里云物联网平台的“规则引擎”中新建一条规则SQL 语句筛选属性上报中出现PirAlarm1的数据转发到 HTTP 服务端。这个 HTTP 服务可以是运行在一台 Linux 服务器上的 Flask 脚本接收请求后调用手机 App 的推送 APIServerChan、PushPlus 均可。下面是规则引擎的参考配置SELECT deviceName() as deviceName, timestamp() as ts, items.PirAlarm.value as pir, items.FireAlarm.value as fire FROM /sys/${productKey}/${deviceName}/thing/event/property/post WHERE items.PirAlarm.value 1 OR items.FireAlarm.value 1这条 SQL 比直接转发全部数据节省了云端到业务服务器的带宽而且只在有告警时才触发 HTTP 转发。若你需要在设备离线超过 N 分钟时也发通知需要再配置一条设备生命周期事件规则监听thing.disconnect事件加上timestamp() - lastSeenTime 300000的条件表达式。最后一个实际调试技巧综合看本地日志和云端日志定位“设备显示在线但不推送”的问题。如果设备在线、属性上报也成功但手机收不到告警问题通常出在规则引擎的 SQL 过滤条件与物模型上报的数据格式不匹配。阿里云对 Alink JSON 的字段名大小写敏感物模型标识符定义为PirAlarm和实际上报pirAlarm会导致规则引擎查询结果为 null。建议先在规则引擎的“运行日志”里看转发的原始消息是否为 null确定问题在云端还是设备端而不是反复重置设备配置。本文还有配套的精品资源点击获取