简介:这是一份基于STM32的盆栽种植自动管理系统设计文档(PDF),主要面向物联网、嵌入式方向的在校学生、毕业设计者及智能家居爱好者,解决传统盆栽养护依赖经验、缺乏实时数据与远程管控的痛点。资源包含1个PDF文件,压缩包约40.96MB,内容涵盖项目背景、需求分析、硬件模块选型、系统整体架构,以及STM32F103RCT6主控芯片与土壤湿度传感器、DHT11温湿度传感器、BH1750光照强度传感器、OLED显示屏、水泵继电器、PWM补光灯、蜂鸣器、ESP8266-WiFi模块的协同设计。文档详细描述了通过腾讯云IoT平台与MQTT协议实现数据上传、微信小程序远程查看与阈值设置的方法,并给出本地按键控制、自动补水、PWM调光、超限报警等功能逻辑与设计思路。已有118人学习下载,适合作为毕业设计参考或嵌入式物联网项目开发的基础资料。
1. 盆栽自动管理为什么值得用STM32自己做一套
出差一周回来,阳台上的薄荷蔫了一半,土壤干得裂开;另一盆绿萝泡在水里,根已经闷烂。这两种死法,几乎是每个养盆栽的人都经历过的。标题里这套基于STM32的盆栽种植自动管理系统,做的就是用一个单片机下位机持续采集土壤湿度、环境温湿度、光照强度,根据预设策略自动浇水、补光,同时把状态通过WiFi推到微信小程序上,让你哪怕人在外地,也能随时确认这盆植物还活着。这套方案最适合三类人:正在做嵌入式或物联网课程设计、毕业设计的学生,家里盆栽不少又不想每天盯着的动手党,以及需要一套低成本可复现方案来验证产品思路的工程师。它能解决的问题就一句话:把“什么时候浇水、浇多少、缺不缺水”从拍脑袋变成可配置的自动逻辑。
2. 系统架构与硬件选型:从传感器到微信小程序的链路
要把一套系统落地,先得确认数据从哪来、命令往哪走。我习惯把整个链路拆成三层:感知层、控制层、交互层。感知层是土壤湿度传感器、温湿度传感器、光强传感器;控制层是STM32主板以及它驱动的水泵、补光灯;交互层是WiFi模组、云端通道和微信小程序。系统跑起来之后,数据流向很清晰:传感器信号进STM32的ADC或I2C接口,单片机做完滤波、标定和阈值判断后,一路驱动继电器控制水泵,另一路把打包好的状态数据通过串口发给WiFi模组,WiFi模组上云,小程序从云端拉取状态、下发指令,指令再原路返回。整个闭环不算复杂,真正决定方案好坏的是每一层怎么选型、怎么分工。
2.1 下位机核心:STM32在系统里承担的角色
先回答一个最常见的纠结:为什么用STM32,而不是Arduino或者ESP32直接干?从功能需求看,这套系统需要三路左右的ADC做传感器采集,一路串口和WiFi模组通信,几个GPIO控制继电器或MOS管,一组定时器做浇水延时和保护判断。STM32F103系列完全够用,而且余量不小。以最常用的STM32F103C8T6为例,64KB Flash、20KB RAM、10个ADC通道、3个串口,裸机程序跑这套逻辑,资源占用大概只有三分之一。Arduino Uno也能跑,但10位ADC在土壤湿度这种慢变信号上虽然够用,一旦要同时采多路信号、还要做滑动平均和多区间判断,F103的12位ADC和72MHz主频带来的余量会更舒服,调试时少很多瓶颈。
那为什么不用ESP32直连?ESP32确实自带WiFi,能省掉一个模组,但它在跑WiFi协议栈时的功耗和发热明显高一些,而且课程设计、毕设评审大概率会更看重下位机的嵌入式设计能力,而不是“买一块WiFi开发板把示例demo改一改”。把通信剥离给ESP8266之后,STM32专注采集、控制、状态机,代码结构上也更好讲清楚。我一般推荐的分工是:STM32承担一切和实时性相关的工作,WiFi模组只做一个透明透传通道。
| 选型 | Flash/RAM | ADC | WiFi | 适合场景 | 注意事项 |
|---|---|---|---|---|---|
| STM32F103C8T6 | 64KB / 20KB | 12位 | 需外挂 | 课程设计、毕设、产品原型 | Flash偏小,不适合塞RTOS |
| STM32F103VET6 | 512KB / 64KB | 12位 | 需外挂 | 后续加RTOS、图形屏幕 | 成本高、引脚多 |
| ESP32系列 | 448KB / 520KB | 12位 | 内置 | 快速原型 | 无线协议栈功耗偏高 |
| Arduino Uno | 32KB / 2KB | 10位 | 需外挂 | 纯学习验证 | 资源紧,堆不了复杂逻辑 |
选型时有一个容易被忽略的点:如果后续想加FreeRTOS来管理采集、通信、控制任务,F103C8T6的64KB Flash会比较紧张,建议一开始选Flash 128KB以上的型号,比如STM32F103RBT6或更大容量的F103VE,给代码留足冗余。不做RTOS、只写裸机轮询的话,C8T6完全够用。这里的取舍不是性能问题,而是你打算把项目做成什么样:一次性通过,还是要长期加功能。
2.2 传感器和执行器选型:土壤湿度、光照、水泵的参考参数
土壤湿度传感器是整套系统里最容易翻车的地方。常见有两种形态:电阻式(铜叉探针)和电容式(镀金或绿色PCB封装)。电阻式靠土壤导电率估计湿度,用久了探针会被电腐蚀,读数还受土壤中肥份离子浓度影响,标定值用几个月就飘了。电容式测的是土壤介电常数变化,金属部分不和土壤直接导电接触,寿命长得多,价格也就贵几块钱。做这个项目,我强烈建议直接用电容式,后面避坑章节还会专门讲它的漂移问题。
环境温湿度传感器,可选DHT11、DHT22、SHT30这三档。DHT11的温度精度±2°C、湿度±5%RH,做浇水决策勉强够用但不够好看;DHT22精度高一些,但是单总线协议,裸机环境下容易被中断干扰导致读取出错,要加很多重试逻辑;SHT30走I2C,精度好、时序简单、不需要精确延时,价格居中,是我在这个项目里最常用的选择。光照传感器直接选数字型的BH1750,I2C接口,读回一个16位整数就是lux值,不用自己做模拟量到光强的换算。
执行器方面,水泵选两三元到十几元的小型直流潜水泵就可以,额定电压3V到6V,工作电流一般在三四百毫安,用一个继电器模块或一颗MOS管加续流二极管就能驱动。需要特别提醒的是,水泵绝不能直接接在STM32的GPIO引脚上,GPIO驱动能力只有20毫安左右,带不动泵,开关瞬间的电流冲击还可能把引脚打坏。补光灯如果要用,选普通白光LED或者红蓝LED灯带,同样用MOS管做开关或PWM调光。液位传感器用来检测水箱缺水,可选浮球开关或非接触式光电液位传感器,前者便宜,后者更可靠而且不会卡浮球。
2.3 上位机链路:为什么选择微信小程序而不是原生App
上位机选微信小程序,理由非常实际。用户不需要安装App,微信里扫码或者搜索就能打开;开发语言是前端那套HTML加JS,对嵌入式背景的人来说门槛不高;个人开发者也能完成小程序的注册和发布,不一定需要企业资质。反过来看原生App,要同时适配安卓和iOS两套系统,光签名打包就够折腾,对这个体量的项目来说明显用力过猛。
通信链路有两种主流做法。第一套是WiFi远程方案:STM32外挂ESP8266模块,ESP8266连家里路由器,通过MQTT协议连到某个云端broker,微信小程序通过WebSocket和同一个broker通信,这才能做到人不在家也能远程浇水。第二套是蓝牙BLE方案:STM32接一个低功耗蓝牙模块,小程序直接和设备配对通信,不经过云端,开发量小,但人离开蓝牙范围就完全失联,适合近距离调试,不适合“自动管理”这个定位。所以后面所有代码和联调说明都按第一套来写:ESP8266充当串口转WiFi的透传通道,STM32发的每一帧数据都不需要关心WiFi内部的协议栈,只需要通过串口按约定格式交给它。
云端这一层,最简单的做法是使用公共的MQTT broker,但公共broker常有连接数限制和掉线问题。只做课程演示问题不大;想长期稳定运行,建议在一台云服务器上自建轻量MQTT服务,配置也不复杂。自建时要特别注意监听端口:小程序端通过带TLS的WebSocket连接,所以除了1883端口,还必须打开WebSocket监听端口,并配置好证书路径。broker的认证最好打开,否则任何知道主题名的人都能往cmd主题发消息控制你的水泵,这听起来夸张,但真的发生在我身边一次,某开发者调了整整两天才发现是有人连错主题误触发了浇水。
3. 下位机端设计与关键代码:STM32的采集、控制与通信
在设计下位机程序之前,先明确任务清单:以1秒为周期采集土壤湿度、环境温湿度、光照强度;每5秒通过串口向ESP8266发一次状态帧;根据土壤湿度阈值决定是否开启水泵;水泵开启后如果达到浇水上限时长或者湿度已经超过上限,立即关闭;收到小程序下发的指令时,执行开启/关闭水泵、调整阈值等操作。下面所有代码都基于HAL库和CubeMX生成的工程骨架,如果你用的是标准库,逻辑完全一致,只是API名称不同。
3.1 土壤湿度采集与ADC采样:从原始值到湿度百分比
电容式土壤湿度传感器的输出通常是一个模拟电压,湿度越高电压越低,也有反过来输出的型号,所以一定要看模块说明书。STM32的ADC采集这个电压,得到12位的原始值,范围0到4095。采集代码里要做两件事:多次采样取平均,以及将原始值映射成0%到100%的湿度百分比。
// adc_soil.c - 基于HAL库的土壤湿度采样 #include "adc_soil.h" #define SOIL_SAMPLE_COUNT 8 // 每次连续采样次数 // 两个标定点:干燥土壤的ADC值和湿润土壤的ADC值 // 需要根据实际传感器和土壤实测得到,不是固定值 #define SOIL_DRY_ADC 3500 #define SOIL_WET_ADC 1400 uint8_t soil_read_percent(void) { uint32_t sum = 0; ADC_ChannelConfTypeDef sConfig = {0}; for (int i = 0; i < SOIL_SAMPLE_COUNT; i++) { sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { sum += HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); } uint32_t avg = sum / SOIL_SAMPLE_COUNT; // 限幅防止除零和越界 if (avg >= SOIL_DRY_ADC) return 0; if (avg <= SOIL_WET_ADC) return 100; // 线性映射:假设ADC值越大代表土壤越干 return (uint8_t)((SOIL_DRY_ADC - avg) * 100 / (SOIL_DRY_ADC - SOIL_WET_ADC)); }这段代码的逻辑是循环8次采集同一通道,每次先配置通道、再启动转换、等待转换完成后读取数值,最后用8次结果求平均,消除传感器输出上的随机噪声。真正要留意的参数是SOIL_DRY_ADC和SOIL_WET_ADC这两个标定点,它们不是芯片手册提供的,而是你在实际花盆里测出来的:把传感器插在完全干燥的土里读一个值,再插在刚浇透水的土里读一个值。如果不做这一步标定,直接套网上抄来的上下限,湿度百分比只是个参考,没法用于精确阈值控制。
采样时间取55.5周期,对土壤湿度这种变化缓慢的信号来说完全足够;如果你把同一通道换成光敏电阻,需要根据响应速度调整这个参数。裸机轮询方式下,8次采样加上配置耗时,整个过程大约1毫秒,不会对1秒周期的主循环造成压力。需要留意的是,如果板子上有多路ADC通道要采,每次切换通道后都要重新调用通道配置函数,否则读到的是上一次通道的残留值。多路采集时还建议把各路传感器接在固定的几个引脚上,不要今天接PA0明天接PA1,代码里通道号和硬件接线上对不上是排错时最耗时的坑之一。
3.2 自动灌溉控制逻辑:阈值判断与状态机
自动灌溉不能只写一个if(湿度 低于阈值)就开泵,必须做成一个小状态机,原因是水泵不能一开一关地振荡,浇水也需要有最短和最长的持续时间限制。我一般定义三个状态:IDLE(空闲)、WATERING(浇水中)、COOLDOWN(冷却等待)。状态机的运转规则如下:IDLE状态下,土壤湿度低于设定的下限阈值且距离上次浇水超过冷却时间,进入WATERING;WATERING状态持续到湿度高于上限阈值或者浇水时长超过最大浇水时间,则返回IDLE并记录这次浇水时间;COOLDOWN是防止频繁误判,浇水结束后短时间内即使传感器读数低落也不重新开泵。
// irrigation.c - 灌溉状态机 #include "irrigation.h" static uint16_t g_low_thr = 45; // 低于此值开始浇水,可从小程序远程修改 static uint16_t g_high_thr = 70; // 高于此值停止浇水 #define MAX_WATER_TIME_SEC 20 // 单次浇水最长20秒,防溢出 #define COOLDOWN_TIME_SEC 60 // 浇水结束后冷却60秒 typedef enum { IRRI_IDLE, IRRI_WATERING, IRRI_COOLDOWN } irri_state_t; static irri_state_t state = IRRI_IDLE; static uint16_t state_tick = 0; // 当前状态已持续秒数 void irrigation_tick(uint8_t soil_percent) { if (state == IRRI_IDLE) { if (soil_percent < g_low_thr && state_tick >= COOLDOWN_TIME_SEC) { state = IRRI_WATERING; state_tick = 0; pump_set(1); } } else if (state == IRRI_WATERING) { if (soil_percent >= g_high_thr) { pump_set(0); state = IRRI_IDLE; state_tick = 0; } else if (state_tick >= MAX_WATER_TIME_SEC) { pump_set(0); state = IRRI_IDLE; state_tick = 0; // 超时兜底:湿度上升太慢,可能漏水、堵转或传感器异常 } } state_tick++; }这个状态机的关键点,在于把“什么时候浇水”和“浇多久”拆开控制。土壤湿度低于下限只是进入浇水的必要条件,不是充分条件;冷却时间保证了浇水结束后不会立刻又被判定为缺水,避免水泵频繁启停。MAX_WATER_TIME_SEC这个超时兜底非常重要,很多项目在这里翻车:如果水泵抽不上水、水管堵塞或者传感器标定错误,湿度始终升不到上限,没有超时保护的话水泵会一直转到水箱烧干。20秒只是一个示例值,具体要根据花盆大小、盆土保水能力和水泵流量调整,建议用“浇透这盆土实际需要的时间乘以1.3到1.5”来估算。
g_low_thr和g_high_thr两个值,我在程序里刻意做成全局变量而不是宏,就是为了让小程序端能远程调整。后面协议部分会讲怎么把收到的阈值更新到这两个变量上。还有一个容易踩的坑:state_tick是以秒为单位计数,所以irrigation_tick这个函数必须每秒被主循环调用一次;如果你把采集周期改成了两秒或者五秒,这里的计数逻辑要同步改,否则冷却时间和最长浇水时间都会失真。我在第一次联调时就遇到浇水30秒才停的问题,最后发现是主循环里加了延时导致tick变慢,不是状态机写错。
3.3 与WiFi模组的串口通信协议:JSON封装与解析
STM32和ESP8266之间走串口,常见两种做法:AT指令透传和直接串口通信。ESP8266刷完AT固件后,先通过AT指令连接路由器、连接云端,然后进入透传模式,此时它对STM32来说就是个透明管道。既然是透明管道,STM32往外发的数据格式就不用受AT指令约束,可以自由定义协议。我习惯的做法是:每5秒发一帧JSON状态数据,同时周期性检查串口接收缓冲区,解析来自小程序的下行指令。
// protocol.c - 使用cJSON库组装与解析协议帧 #include "protocol.h" #include "cJSON.h" void protocol_build_status(char *buf, uint8_t soil, int16_t temp, uint8_t humi, uint16_t lux, uint8_t water_level) { cJSON *root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "type", "status"); cJSON_AddNumberToObject(root, "soil", soil); cJSON_AddNumberToObject(root, "temp", temp); cJSON_AddNumberToObject(root, "humi", humi); cJSON_AddNumberToObject(root, "lux", lux); cJSON_AddNumberToObject(root, "water", water_level); char *str = cJSON_PrintUnformatted(root); strcpy(buf, str); cJSON_free(str); cJSON_Delete(root); } int protocol_parse_cmd(const char *json_str, cmd_t *cmd) { cJSON *root = cJSON_Parse(json_str); if (root == NULL) return -1; cJSON *type = cJSON_GetObjectItem(root, "type"); if (cJSON_IsString(type) && strcmp(type->valuestring, "cmd") == 0) { cJSON *act = cJSON_GetObjectItem(root, "action"); if (cJSON_IsString(act)) { if (strcmp(act->valuestring, "pump_on") == 0) cmd->action = CMD_PUMP_ON; else if (strcmp(act->valuestring, "pump_off") == 0) cmd->action = CMD_PUMP_OFF; else if (strcmp(act->valuestring, "set_threshold") == 0) cmd->action = CMD_SET_THRESHOLD; } cJSON *low = cJSON_GetObjectItem(root, "low"); cJSON *high = cJSON_GetObjectItem(root, "high"); if (cJSON_IsNumber(low)) cmd->low_threshold = low->valueint; if (cJSON_IsNumber(high)) cmd->high_threshold = high->valueint; } cJSON_Delete(root); return 0; }这段代码用了cJSON库,是嵌入式里最常见的JSON方案。组装状态帧时用cJSON_CreateObject创建对象,逐个添加字段,最后用cJSON_PrintUnformatted输出成紧凑字符串。之所以用JSON而不是自定义的十六进制帧格式,是因为数据要经云端转发给小程序,JSON无论调试打印、抓包观察还是跨端解析都直观得多。代价是STM32的Flash会多占十几KB,F103C8T6完全扛得住。
解析下行指令时,先判断type字段是不是cmd,再根据action字段分发动作。把low和high放在同一条指令里,可以让小程序端只发一条消息就完成阈值修改,不用分两条指令处理。性能上,cJSON在72MHz的M3内核上解析这种十几个字段的小JSON,耗时几百微秒,在5秒的通信周期里可以忽略。串口接收这边,我用串口空闲中断加DMA,收到的数据进环形缓冲区,主循环里检查有没有换行符,一旦出现换行就把这一整行拿出来做解析。原因是:JSON帧可能因为网络原因被拆成多个TCP段到达设备,如果每收一个字节就调解析函数,很容易拿到半个JSON,解析必然失败。用行缓冲可以保证每次解析对象都是完整的一帧。最后记得裁掉行尾的\r\n,很多串口工具会在行尾追加回车换行。
3.4 异常处理与低功耗:掉电、缺水、水泵堵转
下位机除了正常逻辑,还要提前处理几种异常。掉电问题最容易被忽视:盆栽系统常年在阳台或室外,供电大概率是USB适配器或充电宝,一旦断电再恢复,程序从头跑,水位、上次浇水时间这些信息全丢,可能导致恢复后立刻误浇一次水。解决办法是把水位状态和关键运行参数存入STM32内部的Flash,上电时先读回来。片内Flash的写入寿命大约一万次,这类参数一天写几次,寿命足够。
水泵堵转和缺水是另一种常见异常:水箱空了、水管弯折或者水泵内部卡住,表现都是浇水时土壤湿度长时间不上升。状态机里加了最大浇水时间超时,但超时后最好再上报一个报警字段,让小程序端显示“浇水超时可能缺水”。缺水检测用浮球开关或者光电液位传感器,放在水箱底部,电平信号直接进GPIO。如果检测到水位过低,无论状态机处于什么状态,都强制停泵并进入故障态,直到水位恢复。这段代码逻辑很直白,就是主循环里先查水位再跑灌溉状态机,水位低时把pump_set强制设成0。
低功耗方面,如果系统靠充电宝供电,可以把MCU时钟从72MHz降到48MHz,关闭不用的外设时钟;传感器可以改成每5秒供电一次,采样完立即断电;串口波特率从115200降到9600也能省一点电。但说实话,盆栽系统长期插着USB电源是常态,对功耗不必追求太激进,重点还是稳定。WiFi模组长时间运行偶尔会卡死,我一般会用GPIO控制ESP8266的复位引脚,每30分钟如果没有收到来自网络的任何数据,就拉低复位脚再拉高,强制重启。这不是最优雅的方案,但在这种体量的设备上,硬件看门狗加周期复位比在协议栈层面做保活更有效。
4. 微信小程序端:设备绑定、数据看板与远程浇水
小程序端做的事情可以拆成三块:建立与云端的连接、展示实时数据、下发控制指令。前面架构里说通信走MQTT,小程序并不会直接和ESP8266建连接,而是由云端broker中转,因此小程序端的核心工作是维护一条WebSocket连接并解析JSON消息。整个界面不用做得太复杂,一个设备列表、一个详情页、一个历史曲线页就够用。
4.1 小程序与云端的连接方式:MQTT与HTTP轮询怎么选
小程序要和设备通信,业界常用两套方案:HTTP轮询和MQTT。HTTP轮询实现最简单:设备每5秒向服务端上报一次数据,小程序每5秒请求一次最新状态。问题是控制指令的实时性差,你在小程序上点了浇水,指令要先经过服务端存库,设备下一个轮询周期才拉到,中间可能延迟5到10秒。而且轮询方式下,设备侧要写HTTP客户端、处理超时重传,复杂度和MQTT差不多,体验却更差。
MQTT方案下,设备端和微信小程序都是broker的客户端,通过发布订阅主题来交换消息。比如设备订阅主题“device/201/cmd”,小程序向这个主题发布指令;设备向“device/201/status”发布状态,小程序订阅这个主题接收状态。推送是即时的,没有轮询延迟。微信小程序本身的网络API不直接支持原生的MQTT over TCP,所以要用基于WebSocket的MQTT实现,实际是一个mqtt.js的移植版本。公共broker要注意:有些免费broker对匿名连接数和设备在线时长有限制,个人项目随便用问题不大;想长期稳定,建议在云服务器自建轻量MQTT服务,打开1883和WebSocket端口,并给设备端配置用户名和密码。
设备绑定这块,最简单的做法是用一个设备ID加预设验证码。小程序首次添加设备时,输入设备ID(比如201),同时输入一个在STM32里写死的6位验证码;验证码验证通过后,云端把两者存下来作为绑定关系。这比做一键配网省事得多。一键配网(比如微信的AirKiss或SmartConfig)体验好,但实现复杂,ESP8266在不同路由器上的兼容性差异很大,个人项目用设备ID加验证码的方式最稳,出问题也好排查。
4.2 界面布局与核心页面:数据卡片、控制按钮、历史曲线
小程序端页面通常分两级:设备列表页和详情页。设备列表页用来绑定和解绑设备,核心字段是设备ID,一个ID对应一套硬件。详情页是核心,上半部分用卡片展示土壤湿度、环境温度、空气湿度、光照强度、水箱水位五个实时值,下半部分放自动浇水阈值滑块、手动浇水按钮、手动补光按钮,再加一段最近浇水记录。历史曲线用Canvas绘制,也可以引入ECharts的微信小程序版本,把近7天土壤湿度趋势画出来。
<!-- pages/detail/detail.wxml - 数据卡片和控制区 --> <view class="card"> <text class="card-title">土壤湿度</text> <text class="card-value">{{soilPercent}}%</text> <slider min="0" max="100" value="{{low}}" bindchange="onLowChange" /> <text>低于 {{low}}% 自动浇水</text> </view> <view class="control-row"> <button type="primary" bindtap="onPumpManual">手动浇水20秒</button> <button type="warn" bindtap="onPumpStop">停止浇水</button> </view>// pages/detail/detail.js - 订阅状态与下发指令 const mqtt = require('../../utils/mqtt.js') Page({ data: { soilPercent: 0, low: 45, temp: 0, humi: 0, lux: 0, water: 1 }, onShow() { this.client = mqtt.connect('wss://your-broker.example.com/mqtt', { clientId: 'wx_' + Date.now(), username: 'mini', password: 'mini123' }) this.client.on('connect', () => { this.client.subscribe('device/201/status') }) this.client.on('message', (topic, payload) => { const msg = JSON.parse(payload.toString()) this.setData({ soilPercent: msg.soil, temp: msg.temp, humi: msg.humi, lux: msg.lux, water: msg.water }) }) }, onPumpManual() { this.client.publish('device/201/cmd', JSON.stringify({ type: 'cmd', action: 'pump_on' })) }, onPumpStop() { this.client.publish('device/201/cmd', JSON.stringify({ type: 'cmd', action: 'pump_off' })) }, onLowChange(e) { this.setData({ low: e.detail.value }) } })这段代码展示了两个关键点。第一,小程序的MQTT客户端通过WebSocket连接broker,所以连接地址是wss前缀而不是mqtt前缀;如果broker没打开WebSocket监听端口,小程序连不上。第二,publish和subscribe的主题必须与STM32端约定一致:设备订阅cmd主题,小程序往cmd主题发指令;设备往status主题发状态,小程序订阅status主题收状态。示例里clientId用时间戳生成,是为了避免多个客户端用相同ID互相踢下线。还有一点要注意,onShow里每次进入页面都新建连接,页面卸载时一定要调用close断开,否则一个用户多次进出会堆积大量僵尸连接,被broker限流或踢出。
4.3 通信协议对齐:上行数据与下行指令的字段约定
两端联调之前,务必把通信协议写成一个文档,字段名、类型、单位、取值范围都固定住。我自己踩过一个坑:下位机用soil表示湿度,小程序却解析soil_humidity,结果数据一直显示不出来,最后逐字段排查才发现是命名不一致。下面给出一份最小可用的字段约定,可以直接照抄。
| 方向 | 字段名 | 类型 | 含义与示例 |
|---|---|---|---|
| 上行 | type | string | 固定为“status” |
| 上行 | soil | int | 土壤湿度百分比,0到100 |
| 上行 | temp | int | 环境温度,单位0.1°C,256表示25.6°C |
| 上行 | humi | int | 空气湿度百分比 |
| 上行 | lux | int | 光照强度,单位lux |
| 上行 | water | int | 水位状态,1正常、0缺水 |
| 下行 | type | string | 固定为“cmd” |
| 下行 | action | string | pump_on / pump_off / set_threshold |
| 下行 | low | int | 阈值修改用,灌溉下限百分比 |
| 下行 | high | int | 阈值修改用,灌溉上限百分比 |
数据单位最容易产生分歧,温度用整数存0.1°C,是为了避免浮点数在JSON传输和不同语言解析之间产生精度损失,小数点在两端手动移位即可。字段名尽量短,因为它们要跑在串口和WebSocket上,字段太长会让单帧数据变大,5秒一帧流量虽然不大,但调试时满屏长JSON看起来也累。如果后续要加历史曲线,还要在状态帧里增加一个时间戳字段,用Unix秒表示,否则云端存储时无法确定设备本地的采集时间,画出来的曲线会因为上报延迟而偏移。
5. 联调与避坑:传感器漂移、WiFi掉线、浇水溢出
到这里,系统在理想情况下已经能跑通了。但真正决定这套系统能不能长期运行的,是各种边界条件。这章列几个实际联调时遇到的问题,每一条都按现象、原因、解决的顺序讲清楚。这些坑我自己基本都踩过一遍,照着排查能省很多时间。
5.1 传感器标定漂移:读数越用越不准
现象:新装的时候土壤湿度标定到干土0%、湿土100%,正常用了半个月以后,同一个花盆在同样湿度下,读数明显偏高或偏低,自动浇水要么过度要么不足。
原因:电容式土壤传感器虽然比电阻式抗腐蚀,但依然受土壤电解质浓度、温度、压实程度影响。标定值是固定的,土壤条件变化后线性映射关系不再成立,误差被放大成百分比偏差。
解决:第一,标定时不要只标干湿两个点,至少取三到五个点做分段线性化,中间点覆盖“摸着微潮”的状态;第二,把自动浇水触发阈值设成一个区间而不是单点,例如下限取40%、上限取65%,留出容差;第三,程序中每隔一段时间用人工实测值做偏移校准,比如用户在小程序端看到湿度显示32%,实际用手感受是45%,就在小程序里填一个偏移量修正。最彻底的做法是每次浇透水后自动记录传感器此时的ADC值,更新标定的湿端参考点,这个做法对土壤环境缓慢变化很有效。
5.2 WiFi模组掉线:设备数据刷新中断
现象:设备刚上电时一切正常,运行几个小时后小程序偶尔收到数据,再后来彻底离线。重启设备又恢复,但过一阵又掉。
原因:ESP8266长时间运行后偶尔会进入异常状态,尤其在路由器信号一般、重传频繁的环境下;另一个常被忽略的原因是MQTT心跳保活不匹配,broker设了60秒保活,设备端因为模组缓冲区阻塞导致心跳发送延迟,被broker判定掉线并踢出连接。
解决:我一般加三层保障。第一层,让WiFi模组固件自带掉线重连能力,AT指令固件本身有重置连接机制,但有时候不彻底;第二层,STM32用GPIO控制ESP8266的复位引脚,配合保活机制,如果连续90秒没收到任何下行数据,就硬重启模组;第三层,在程序里周期向隧道内发送ping帧,cJSON协议里加一条action为ping的消息,小程序端收到后忽略即可,但云端的往返消息能证明链路是通的。
5.3 浇水溢出和夜间误浇水
现象:远程看到设备自动浇水,但花盆托盘里积水淹到地面;或者深夜两三点水泵自动启动,浇了一地。
原因:最大浇水时间设得太大,或者浇水停止条件只依赖湿度阈值,而湿度传感器安装位置离浇水点太远,水已经漫出来传感器读数还没到位。夜间浇水则是自动策略里没做时间约束,半夜蒸发量低,土壤湿度不该降却因为传感器漂移被误判成缺水。
解决:把传感器插在花盆一半高度的位置,不要插在最表层也不要插在盆底积水区;浇水时长上限按“浇透该盆所需时间的1.3倍”来设,并且每浇完一次记录累计浇水量。同时在小程序协议里加一个夜间免打扰时段,比如22点到次日7点之间禁止自动启动水泵,只在极端缺水情况下允许手动远程开启。这个时段参数同样可以由小程序下发到STM32存进Flash,断网重启也不丢。
5.4 小程序数据不同步:状态明明变了界面不变
现象:在手机上点了手动浇水,下位机水泵确实启动了,但小程序界面上的土壤湿度还是几秒前的旧值;或者手动停止后界面依然显示浇水进行中。
原因:指令发出后,设备端状态变化要通过下一帧状态上报才能反映到小程序端,而状态上报周期是5秒;如果用户在弱网环境下点击,指令根本没发出去,界面却已经显示了操作成功。小程序端的本地展示逻辑没有和服务端状态合并,造成了假成功。
解决:点击任何控制按钮后,不立即更新界面状态,而是等收到新一帧状态消息后再刷新,同时在按钮旁显示“指令已发送,等待设备确认”。下发指令时记录一个本地时间戳,如果超过10秒还没等到反映该指令结果的推送,就用Toast提示“控制失败请检查网络”。数据处理上,用设备上报的时间戳做去重,避免重复指令导致水泵反复开关。
5.5 供电不足导致复位:一浇水整个设备重启
现象:水泵一启动,串口日志里看到STM32复位信息,设备重启,小程序显示离线几秒后恢复。
原因:小型潜水泵启动瞬间电流可达稳定运行电流的1.5到2倍,如果USB适配器实际输出能力只有500mA,且水泵和单片机共用同一个5V电源,电压被拉低到4.3V以下,STM32内置稳压器输出波动直接触发掉电复位。这个问题在桌面USB供电时最容易出现,换到阳台电源适配器反而不明显,但其实风险还在。
解决:分路供电才是正解。水泵使用独立5V或6V电源,通过继电器或MOS管开关,与STM32主板只共地不共电源;至少在STM32的5V输入端和3.3V稳压器输出端各并一个100uF的电解电容,再并一个0.1uF瓷片电容滤高频。水泵控制端和电源之间要加续流二极管,防止关断瞬间的反向电动势打坏MOS管。排查方法也很直接:把水泵断开,用万用表观察浇水瞬间电源电压是否跌落超过0.3V,跌了就是电源容量不够。
提示:水泵启动瞬间电流很难用万用表捕捉,最好用示波器看电压跌落曲线;没有示波器时,可以在浇水瞬间观察板载LED是否明显变暗,间接判断供电是否紧张。
6. 进阶技巧:让系统更省心的几个细节
基础链路跑通后,下面这些细节能让整套系统从“能演示”变成“能长期用”,也是我在后续迭代里真正受益的地方。
第一个技巧是记录历史浇水曲线。每次浇水开始和结束都打一条日志,包含时间戳、浇水时长、浇水前后的湿度值,存到云端。一周以后把这7天数据拉出来画成曲线,你会发现每盆植物的需水规律非常固定:绿萝大概三天要浇一次,薄荷两天就要浇一次,而且冬季和夏季的周期完全不同。基于这个规律,可以把自动策略从“湿度阈值触发”升级成“周期预测辅助阈值判断”:如果系统预测今天该浇水但湿度还没降到下限,就把下限临时调高一点,提前进入预浇水,避免每次都是干透了才补救。
第二个技巧是根据环境数据修正浇水时长。现在的停止条件是湿度达到上限或超过最大时长,但夏天温度高、蒸发快,浇水后湿度回落也快;冬天温度低、根系活性弱,浇透一次能撑更久。把温度加进算法里做一个系数:夏季温度高于30°C时,最大浇水时长乘0.8;冬季低于10°C时,浇水时长乘1.2,同时把自动浇水下限临时调低5个百分点。这个系数不需要很精确,能反映季节差异就足够有效。
第三个技巧是做一个离线兜底策略。WiFi断了的时候,系统不能变成傻子。把自动浇水的阈值判断逻辑全部放在STM32本地,而不是依赖云端指令,意味着断网期间浇水照常进行,只是小程序看不到状态。再配合独立看门狗:主循环每500毫秒喂狗一次,一旦程序跑飞或者阻塞,两秒内自动复位恢复,保证设备即使无人看管也能持续工作。我自己的习惯是:每次迭代完程序,先拔掉WiFi模组,让设备在纯离线状态连续跑48小时,确认自动浇水逻辑稳定,再插上WiFi联调远程功能,这样出了问题能快速定位是通讯问题还是控制逻辑问题。
最后再说一个我自己断断续续踩过几次的教训:不要在阳光直射的窗台放传感器,也不要让水泵水管在花盆边缘悬空。环境温度变化和管路弯折会让系统出现一堆“偶发异常”,而这些问题在调试台上永远复现不出来。每次改完程序,先跑离线48小时,再跑联网72小时,两点都稳了再谈功能迭代。这四个技巧做完,这套基于STM32的盆栽自动管理系统才真正站得住:本地能自持、远程能控制、数据能复盘。如果你正准备照着这个方向做,我的建议是把前面五点避坑经验贴在手边,联调时逐条对照;再把历史曲线记录加上,你会很快感受到这套系统比拍脑袋浇水靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取