ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32+UCOS-III的家庭安防系统:RTOS多任务与阿里云MQTT实战

STM32+UCOS-III的家庭安防系统:RTOS多任务与阿里云MQTT实战 简介基于STM32UCOS并接入阿里云物联网平台的家庭安全防控系统是一份适合物联网工程、电子信息或计算机专业学生毕业设计、课程设计及期末大作业的嵌入式项目源码。压缩包内共75个文件主要由39个.h头文件、35个.c源文件和1个MD项目说明构成整体体积仅94KB头文件与源文件覆盖了OLED显示、DHT11温湿度采集、SR04超声波测距、火焰检测、RC522射频识别、OV2640摄像头、Wi-Fi模块等外设驱动MD文档则辅助说明开发环境配置与工程结构。通过完整可编译运行的工程源码读者可以深入理解RTOS任务划分与调度、阿里云物联网平台设备接入流程、传感器数据采集与上传、异常联动报警等核心环节同时掌握STM32外设驱动开发、uC/OS系统移植、云平台通信调试等关键技能源码按模块化组织方便按需查阅和二次开发。目前已有134人学习下载对需要参考真实项目做改造或提升实践能力的开发者具有扎实的借鉴价值。1. 一个跑在RTOS上的家庭安防这套STM32工程到底在做什么以前写单片机程序我习惯在一个while(1)里塞满DHT11_Read()、OLED_ShowString()、RC522_Check()加上几个标志位和延时也能跑。但一旦把火焰检测、门禁刷卡、温湿度采集、WiFi上报全堆在一起裸机轮询就会出问题——DHT11读时序时被其他中断打断读出来的数据偶尔是错的OLED刷新慢卡住了后面的人体红外扫描WiFi模块AT命令超时整个循环就堵住了。这套基于STM32UCOS阿里云物联网平台的家庭安全防控系统解决的正是这类“多个外设要同时工作”的并发问题。工程源码里HARDWARE目录下按外设拆成了RC522读卡、DHT11温湿度、SR04超声波、FIRE火焰、OLED显示、WiFi-E8通信等模块APP层用UCOS-III的任务来调度它们。适合正在做嵌入式课程设计、毕业设计的人也适合想从裸机转RTOS开发的从业者。它的价值不只是“能跑”而是给你一套任务划分的现成范本什么外设该独立成任务共享资源怎么加锁阿里云平台的上行下行数据怎么和本地逻辑联动。拿到手之后你改的是传感器类型不变的是这套并发框架。2. UCOS-III任务划分与STM32外设驱动从裸机到多任务的改造思路2.1 看工程结构HARDWARE目录下的每一块都是任务展开工程源码先看到的是HARDWARE下的外设驱动然后是APP下的app.c和os_app_hooks.c等内核配置文件。从开发者的角度看HARDWARE里每个独立功能模块就是将来UCOS任务的最小单元。RC522负责门禁卡识别DHT11负责温湿度采集SR04负责距离检测放在家庭安防里常用来做人体接近感应或水浸液位FIRE火焰传感器走ADC采集OLED负责本地显示WiFi-E8负责上云。这些模块如果都在一个裸机循环里跑最直观的问题就是“时间片分配不均”。DHT11一次完整读取约需要20msRC522读卡需要几十毫秒WiFi发送数据包的AT交互可能花200ms以上。用UCOS的思维重新想就应该把每类传感器封装成一个任务每个任务只关心自己的数据源自身状态变化通过邮箱或消息队列通知其他任务。工程里其实已经有这种雏形key和exit这类输入模块通常作为事件源beep和LED作为输出设备中间用信号量同步。我一般拿到这种工程不会先逐行读驱动而是先打开app.c看main_task或者start_task里创建了哪些任务每个任务分配了多少栈。这是一个快速理解系统骨架的切入点。工程中典型的任务会有显示任务、数据采集任务、报警处理任务、WiFi通信任务这正好对应家庭安防需要“感知、决策、执行、上报”四个阶段。2.2 任务优先级与栈分配为什么DHT11和RC522不能放同一个优先级UCOS-III是抢占式内核任务优先级数值越小优先级越高。实际上UCOS-III允许同优先级任务时间片轮转但工程里通常还是让每个任务有独立优先级。我的经验是不要把所有传感器任务都设成同一个优先级否则一个阻塞就会拖累另一个。比如DHT11读取时需要关中断或进入临界区来保证时序如果RC522也在这个优先级上被调度到RC522阻塞的这段时间恰好DHT11需要精确延时就会导致时序漂移。栈的大小也要看任务内部调用了多少函数。工程里涉及printf、sprintf、AT指令处理的任务栈至少要给512字节以上否则很容易溢出。一个合理的安排是系统空闲任务栈最小通信任务栈最大因为阿里云SDK或AT指令解析函数内部可能有较深调用。以下是一个参考配置表具体数值需要在调试时用OS_TaskStkChk核对。任务名优先级栈大小单位4字节职责start_task5256创建其他任务后自删dht11_task8128定时读取温湿度并通过消息队列发给显示任务rc522_task9128轮询刷卡状态匹配后触发门禁事件fire_task10128读取ADC火焰传感器超阈值发报警事件oled_task11256接收队列数据并刷新屏幕wifi_task12512维护阿里云MQTT连接上报属性接收命令关键不是具体数值而是你理解“为什么WiFi任务栈要这么大”因为它内部可能拼接JSON字符串sprintf一个完整的属性上报报文要占用缓冲区加上AT指令响应缓冲256字节根本不够。如果板子内存充裕宁可多给不要吝啬。2.3 关键外设驱动示例DHT11温湿度读取与OLED显示任务2.3.1 DHT11时序代码与UCOS延时陷阱DHT11是单总线协议读取一次数据需要主机先拉低总线至少18ms然后切换输入模式等待DHT11响应。这段时序对指令周期敏感必须在关中断或临界区内完成。UCOS-III提供了两种进入临界区的方法OS_CRITICAL_ENTER()和CPU_CRITICAL_ENTER()。前者如果不用锁调度器而是关中断那么临界区内就不能调用UCOS的任何延时API。DHT11读取恰好需要微秒级延时所以不能用OSTimeDly只能用空循环或delay_us。下面是一段兼容UCOS-III的DHT11读取代码注意在读取8个字节之前关闭调度或中断读取完成后再恢复。static void DHT11_SendStartSignal(void) { GPIO_SetBits(DHT11_PORT, DHT11_PIN); delay_ms(5); GPIO_ResetBits(DHT11_PORT, DHT11_PIN); delay_ms(25); /* 主机拉低 18ms触发DHT11响应 */ GPIO_SetBits(DHT11_PORT, DHT11_PIN); delay_us(30); /* 释放总线等待DHT11拉低响应 */ }逻辑说明开始信号的作用是让DHT11从空闲状态进入采集状态。整个流程里delay_ms可以在临界区外调用因为它依赖系统节拍中断但delay_us是软件延时在临界区内执行时不会被任务调度打断。如果你把这段代码放在一个高优先级任务里而没有关调度那么DHT11采样过程中系统节拍中断会周期性触发导致软件延时不准确读出的数据里偶发0xFF或校验错误。实际读取位时序时我习惯用下面的方式先等总线拉低然后延时40us判断电平高低。uint8_t DHT11_ReadByte(void) { uint8_t i, data 0; for (i 0; i 8; i) { while (DHT11_DATA_IN() 0); /* 等待50us低电平结束 */ delay_us(40); /* 高电平持续40us后判断 */ if (DHT11_DATA_IN() 1) { data | 0x80 i; while (DHT11_DATA_IN() 1); /* 等待高电平结束 */ } } return data; }参数说明循环里判断的是每个bit的高电平持续时间DHT11用高电平长短表示0或1。标准是26~28us为070us为1。这里延时40us作为中线超过视为1。注意while等待低电平/高电平结束时如果DHT11损坏或线路接触不良等待会死循环。工程安全做法是在循环里加上超时计数例如递减一个变量到0就退出。这属于驱动健壮性问题一般调试时发现任务卡死优先查这里。2.3.2 OLED显示任务如何接收传感器数据OLED不独占任务也可以但放在任务里能避免显示刷新阻塞其他逻辑。我把DHT11读取结果放到一个全局结构体里用消息队列发给OLED任务。比较省事的做法是定义一个TemperatureAndHumidity结构体DHT11任务定时发送OLED任务阻塞接收后格式化输出。typedef struct { int8_t temperature; uint8_t humidity; } ThData; void oled_task(void *p_arg) { OS_ERR err; ThData th; while (1) { OSQPend(th_queue, 200, OS_OPT_PEND_BLOCKING, th, err); if (err OS_ERR_Q_EMPTY || err OS_ERR_TIMEOUT) { continue; } OLED_ShowString(0, 0, (uint8_t *)TEMP:); OLED_ShowNum(40, 0, th.temperature, 2, 12); OLED_ShowString(0, 16, (uint8_t *)HUMI:); OLED_ShowNum(40, 16, th.humidity, 2, 12); } }这段代码里OSQPend的最后一个参数是阻塞超时时间单位是系统时钟节拍。给200个节拍表示就算没收到数据任务也会每2秒左右醒来一次避免完全挂死。如果改成OS_OPT_PEND_NON_BLOCKING队列没有人发数据任务会立即返回空错误白占CPU。参数说明队列消息类型用指针传递时要确保指针指向的内存生命周期覆盖到接收方处理完不能使用任务栈上的局部变量。3. 阿里云物联网接入MQTT连接、属性上报与命令下发3.1 设备认证与Topic规划三元组怎么用阿里云物联网平台不直接支持裸TCP接入必须走MQTT协议。在平台上创建产品后每个设备会分配ProductKey、DeviceName、DeviceSecret这就是三元组。连接时需要先把DeviceSecret通过HMAC-SHA1算法算出MQTT密码用户名是DeviceNameclientId需要拼接。工程里的WiFi-E8模块如果支持AT指令通常会自带MQTT接入功能在AT指令里填入三元组即可。如果不支持就需要MCU通过串口收发MQTT报文那代码量和复杂度会高一个量级。设备属性上报前要在阿里云平台的产品定义里添加功能模型。比如“温度”属性标识符是Temperature类型是int32“湿度”是Humidity。平台会为每个设备自动生成Topic属性上报/sys/{productKey}/{deviceName}/thing/event/property/post属性下发/sys/{productKey}/{deviceName}/thing/service/property/set工程里一般只关注这两条。上报用的Topic可以在平台设备详情页看到也可以根据格式自己拼。我见过不少同学直接把平台给的测试三元组写死在源码里这在开发阶段没问题但一旦刷机下载给其他人别人连不上因为平台端设备已被绑定。3.2 WiFi-E8模块与AT指令对接工程内HARDWARE目录下是wifi-e8这类模块通常用AT指令驱动。初始化顺序一般是复位模块、配置模式、连接路由器、建立MQTT连接。每一步都要等待模块返回OK或MQTTCONN: OK。如果模块没有返回任务不能继续往下走要加超时重发。下面是典型的AT交互序列用串口发送字符串并等待模块应答。ATRST ATCWMODE1 ATCWJAPyour_ssid,your_password ATMQTTUSERCFG0,1,clientId,userName,passWord,0,0, ATMQTTCONN0,productKey.iot-as-mqtt.cn-shanghai.aliyuncs.com,1883,1逻辑说明前两条命令负责复位和设置Station模式。ATCWJAP连接家庭路由器之后模块才能访问公网。ATMQTTUSERCFG里第一个参数是MQTT连接索引第二参数scheme类型为1表示TCP后面依次是clientId、MQTT用户名和密码。最后的ATMQTTCONN中的域名是阿里云上海节点的MQTT接入地址不同地域节点域名不同。实际工程里用户名和密码需要用三元组动态计算不能直接写死否则换一台设备就失效。MCU端处理这类AT指令最好也独立成任务。接收串口数据用中断加环形缓冲解析时按行匹配匹配到关键关键字再置事件标志。不要在主循环里用串口阻塞读因为AT响应时间波动很大可能几十毫秒到几百毫秒。3.3 数据上报JSON构造温湿度、火焰、人体红外上报数据格式由阿里云物模型决定默认是JSON。我在工程里用sprintf拼接字符串再通过AT指令发送。一个完整的属性上报JSON如下static char json_buf[128]; sprintf(json_buf, {\Temperature\:%d,\Humidity\:%d,\FireAlarm\:%d,\SomeoneDetected\:%d}, temp, humi, fire_alarm, human_detect); sprintf(at_cmd, ATMQTTPUB0,\/sys/%s/%s/thing/event/property/post\,\%s\,1,0, PRODUCT_KEY, DEVICE_NAME, json_buf);注意这里有个典型坑MQTT发布AT指令里JSON字符串里的双引号必须转义。不同模块对转义的处理不同有的模块要求把写成\有的模块直接原样发送。工程里如果发现平台上收不到数据先用串口助手看发送出来的报文大概率是JSON格式不对或者Topic拼错。上报频率也是需要控制的。阿里云对单设备QoS0的消息没有严格限制但家用路由器上行带宽有限。我一般把上报任务做成“变化上报”或“定时上报”相结合温度变化超过0.5度才发或者每30秒强制发一次。这样既能保证云端看到最新数据又不会让WiFi模块长时间占用STA状态导致其他外设响应慢。火焰报警和人体红外这类事件型数据必须立即上报不能等定时周期。服务端命令下发这一端处理方式比较统一订阅属性设置的Topic收到平台下发的数据后解析JSON更新对应的全局标志。比如平台下发一个“布防/撤防”命令MCU解析到Command:arm后把代表布防状态的变量置1后续读到门磁或火焰传感器才触发报警。void wifi_data_handler(char *payload) { if (payload NULL) return; cJSON *json cJSON_Parse(payload); cJSON *cmd cJSON_GetObjectItem(json, Command); if (cJSON_IsString(cmd)) { if (strcmp(cmd-valuestring, arm) 0) { system_arm_state 1; } else if (strcmp(cmd-valuestring, disarm) 0) { system_arm_state 0; } } cJSON_Delete(json); }逻辑说明解析下发的命令Payload这里用了cJSON库。UCOS-III工程里引入cJSON时注意设置堆大小因为cJSON_Parse会动态分配内存如果FreeRTOS和UCOS的堆管理没有统一可能出现内存碎片。工程里也可以直接用字符串比对只是遇到复杂命令不灵活。参数说明system_arm_state是全局布防状态在联动处理时要用它作为判断条件如果撤防状态下即使检测到人体红外也不上报也不响铃否则自己家人来回走动就会误报。4. 功能模块联调门禁RC522、火焰检测与本地报警4.1 RC522读卡流程与UCOS互斥锁RC522是13.56MHz RFID读卡芯片通常通过SPI或模拟SPI连接STM32。读卡流程分三步复位RC522、设置天线、寻卡并读取卡号。因为SPI总线上可能还挂了其他传感器比如有些板子把FLASH也放在SPI1上所以对RC522的访问要对SPI总线加锁。在UCOS-III下SPI总线是共享资源任务A在读取卡片的同时任务B在擦写FLASH片选信号会在中途被切换导致两个设备都读写入错误数据。解决办法是用互斥信号量OSMutexPend和OSMutexPost包裹整个SPI交易过程。注意互斥信号量不支持在中断服务函数里释放SPI中断里如果调用OSMutexPost会触发断言。所以RC522任务内部不要使用中断方式读写统一用轮询或DMA加信号量同步。RC522读卡号的关键是得到UID下面是伪代码OSMutexPend(spi_mutex, 0, OS_OPT_PEND_BLOCKING, err); RC522_Reset(); RC522_AntennaOn(); status RC522_Request(PICC_REQIDL, g_uid); if (status MI_OK) { status RC522_Anticoll(g_uid); /* 取5字节2字节校验 4字节卡号 */ } OSMutexPost(spi_mutex, OS_OPT_POST_FIFO, err);RC522_Anticoll得到的5字节数组中第一个字节是厂商代码中间4字节是卡号最后一个字节是校验。门禁逻辑里通常把4字节卡号转成字符串与预存的授权列表比对。比对时要处理大小端因为RC522输出的卡号是高字节在前而大多数人存的卡号是十六进制字符串例如A1:B2:C3:D4。注意一个问题不要把卡号比对放在RC522任务里做耗时操作。如果授权列表很大比如有200张卡用线性查找没问题如果扩展成指纹或人脸识别就应该把比对放到更低优先级任务用邮箱把卡号转发过去避免阻塞下一张卡片的识别。4.2 火焰检测阈值与蜂鸣器联动火焰传感器输出两种信号数字输出DO和模拟输出AO。工程里FIRE模块接在adc_dac上说明走的是ADC模拟采集。火焰越近红外强度越高AO电压越低。所以判断逻辑是“电压低于阈值”视为有火。我用一个ATK_MD0280模块或直接读取ADC值做一次滑动滤波再和阈值比较。阈值不能固定死因为环境光和传感器摆放位置都会影响基准值。建议上电时先空采几秒得到一个baseline然后用baseline - 50作为火焰触发阈值这样能适应白天晚上的光照差异。uint16_t adc_value ADC_ReadValue(FIRE_CH); if (adc_value fire_threshold system_arm_state 1) { beep_alarm_on(); upload_fire_alarm(1); }参数说明fire_threshold在初始化时赋值为读取的基线值减去50具体差值取决于传感器灵敏度和分压电阻。如果发现日光灯下频繁误报就把差值从50改成80或者提高采样频率做二次确认。工程里可以加一个防抖计数器连续3次采样都低于阈值才报警避免瞬态噪声干扰。蜂鸣器响铃逻辑不要直接放在ADC中断里。中断里只置标志位然后通过二值信号量唤醒报警任务。报警任务里再决定鸣叫频率和时长。因为蜂鸣器如果只是简单GPIO翻转放在任务里反而好维护出故障时可以随时关掉。否则在中断里翻转蜂鸣器遇到火焰报警和门禁报警同时触发时状态机很容易乱。4.3 看门狗与掉电保存系统稳定性设计家庭安防设备要求7x24小时运行裸机程序跑飞了可能卡死UCOS下如果某个任务进入死循环调度器就完蛋。所以工程里加了WATCHDOG模块。常规做法是单独建一个喂狗任务优先级设为最低每500ms喂一次。这样一旦任何高优先级任务卡住喂狗任务无法运行系统复位。void watchdog_task(void *p_arg) { OS_ERR err; while (1) { IWDG_ReloadCounter(); OSTimeDly(200, OS_OPT_TIME_DLY, err); } }喂狗周期设置为200ms而看门狗溢出时间设为1秒留足余量。如果某个任务的阻塞时间会超过200ms比如WiFi模块断线重连可能阻塞3秒那么喂狗任务还能继续跑系统不会复位但应用逻辑已经卡死。所以更稳妥的做法是喂狗前检查关键任务的心跳计数器比如WiFi通信任务每循环一次就把wifi_heartbeat喂狗任务检测这个值是否在增加没增加就主动软复位而不是无脑喂狗。掉电保存主要是把布防状态、门禁卡报警记录的标志位存在FLASH或RTC后备寄存器里。工程里FLASH模块可以用来存放用户配置。最省事的是用STM32内部Flash最后一个扇区在写之前先擦除。注意擦除时不能在Flash上有正在执行的代码最好把擦写操作放到RAM中执行或者临时切换到其他任务运行。在UCOS下如果多个任务同时访问内部Flash要用互斥锁。5. 调试技巧与源码二次开发用UCOS的调试手段查问题5.1 查看任务栈使用率和CPU占用UCOS-III自带OS_TaskStkChk()函数可以统计每个任务栈的使用峰值。我习惯在调试阶段写一个状态打印任务每5秒打印一次所有任务栈的使用率格式如下CPU_TS ts; OS_TCB *p_tcb; OS_TaskStkChk(task_oled_tcb, p_tcb, free_bytes, used_bytes, err); printf(OLED stack used: %d, free: %d\r\n, used_bytes, free_bytes);参数说明free_bytes是剩余栈字节数used_bytes是当前已使用量。打印出来后如果某个任务used_bytes超过80%总栈就加大该任务的栈大小。把栈定得刚好够用就行太大浪费RAM太小运行到深层调用就溢出溢出后第一个表现往往是该任务内的变量被改乱。UCOS还有一个钩子函数OS_AppTaskReturnHook任务返回或异常时可以在这里抓现场。RTT调试是另一个实用技巧。用SEGGER RTT代替串口打印不占用串口和中断打印速度也快。在os_app_hooks.c里初始化RTT然后在任务里用SEGGER_RTT_printf输出。我能看到每个任务的调度顺序和阻塞位置比单纯接逻辑分析仪方便。5.2 阿里云掉线重连与常驻异常排查表现可能原因排查方法平台收不到数据MQTT Topic拼错用串口打印AT指令实际内容上报频率过高被平台限流使用了QoS1并重复发送降频或用QoS0客户端频繁掉线心跳间隔大于平台保活时间设置MQTT keepalive为60秒DHT11任务卡死读取时序被中断打断进入临界区或增加超时退出OLED显示乱码任务栈溢出用OS_TaskStkChk检查栈使用刷卡偶尔无效SPI被其他外设抢占对SPI总线加互斥信号量重连逻辑我一般放在WiFi任务里当检测到MQTT连接断开标志后先执行ATMQTTDISCONN再重新配置连接参数并重连。重连之间要延时3~5秒否则模块频繁发起TCP连接会耗尽路由器连接表。还有一个容易忽略的点ATK_MD0280这类带功放的模块初始化时要确保供电稳定WiFi射频开启瞬间电流很大如果板子电源纹波超过3%DHT11和RC522会同时出现随机错误。排查时可以用示波器看3.3V在WiFi发送瞬间的跌落幅度。二次开发时如果想把DHT11换成SHT30最省事的路径是保留原任务接口只重写DHT11_Read()函数内部的I2C或单总线时序上层用于上报和显示的数据结构不用改。同样把FIRE火焰传感器换成烟雾传感器只需在fire_task里把ADC通道改一下阈值重新标定。整个UCOS的骨架不需要动这也是使用RTOS做这类项目最大的收益。本文还有配套的精品资源点击获取
返回列表