资讯动态

STM32智慧农业温室大棚环境监测与自动控制系统详解

发布时间:2026/10/22 19:00:18 来源:尧图企业网站定制
简介一套基于STM32与物联网技术的智慧农业温室暖棚控制与监测系统源码面向物联网、嵌入式、电子信息等专业的在校学生与开发者尤其适合用于毕业设计、课程设计或项目初期演示。源码已测试运行成功可直接下载学习也可在现有代码基础上二次修改以实现更多功能。压缩包共45个文件大小666KB文件类型涵盖15个C源文件、9个PNG图片、6个JSON配置、5个微信小程序JS文件以及WXML/WXSS前端页面、电路原理图PDF和README说明文档。其中C代码包含主控逻辑、外设驱动与通信模块小程序端可实现数据展示与控制整体覆盖从硬件到App的完整链路。目前已有166人学习下载。通过阅读源码和配套文档可以掌握STM32外设驱动、ESP8266联网、MQTT/OneNET平台交互以及小程序开发等实践技能对搭建完整物联网项目具有较高的参考价值。1. 从一块裸板到一个能自己“看天行事”的暖棚这套 STM32 系统到底在解决什么基于物联网stm32的智慧农业温室暖棚控制与监测系统c源码听起来像是一个嵌入式课程的期末大作业但真正把它跑起来你会发现它其实是在替一个种植园值夜班。市面上很多开发板的“智慧农业Demo”只做了一件事把温湿度传到手机App上然后就结束了。可传上去的数字不会自动开风机、不会自动浇水这种项目放在大棚里基本是摆设。真正能落地的系统核心是“采集—决策—执行—上报”的闭环传感器把数据读进来主控判断该不该动继电器再把状态推到云平台。这套源码要解决的就是这个闭环该怎么用 C 语言组织起来。它适合三类人正在做嵌入式毕设的学生、想低成本改造自家或园区暖棚的开发者以及刚学完点灯、想挑战一个完整项目的新手。接下来我按自己做过这类系统的顺序从选型、采集、控制、联网讲到避坑。2. 智慧温室系统整体方案与选型感知层、执行层和主控怎么匹配2.1 感知层测什么空气温湿度、光照、土壤水分与 CO2 的采集方式温室环境不像工业现场那样追求高精度它要的是“趋势对、响应快、能长期稳定”。真正需要纳入闭环控制的物理量只有四个空气温湿度、光照强度、土壤水分CO2 浓度大部分中小棚可以不加。少而稳好过堆一堆传感器最后全在飘。空气温湿度我用 DHT22单总线协议只占一个 GPIO。同价位的 SHT30 走 I2C精度更好但驱动代码稍长。DHT22 的湿度精度 ±2%RH温度 ±0.5℃对温室控制完全够用。光照部分不用买昂贵的照度计模块一个光敏电阻加 10kΩ 分压电阻接到 ADC 通道读到的电压就能映射出大概的照度趋势用来判断“要不要开补光灯”绰绰有余。土壤水分是这里面最容易踩坑的后面专门讲优先选电容式探头而非电阻式前者输出 0~3V 左右的模拟电压接 ADC 即可。CO2 属于可选扩展项常见做法是接一个红外 NDIR 的模块走 UART 输出 ppm 数据。温室里 CO2 浓度影响光合作用但设备成本高多数小型项目先不接把 UART 口预留出来就行。这里我给一套常见的引脚分配ADC 通道、单总线、I2C、UART 各走各的避免复用冲突信号器件接口引脚备注土壤水分电容式探头ADC1_IN1PA10~3.3V 模拟输出光照光敏电阻分压ADC1_IN0PA0分压点接 PA0空气温湿度DHT22单总线PB5需 4.7kΩ 上拉OLED 显示屏SSD1306 128×64I2C1PB6/PB7地址 0x3C执行机构继电器模块GPIO 输出PA4-PA7低电平或高电平触发物联网模块ESP8266USART2PA2/PA3透传 MQTT 数据注意 PA2/PA3 复用了 ADC 的通道 2/3所以模拟采集只用了 PA0/PA1有意避开。如果你要加雨量、风速这类传感器优先选 I2C 或 UART 接口别再去挤 ADC 通道。2.2 主控选型为什么绕不开 STM32F103C8T6很多新手会纠结要不要上更高主频的片子或者干脆用带 Wi-Fi 的 SoC 一把梭。我的经验是温室控制是典型的“慢逻辑”数据周期是秒级的根本不吃算力。STM32F103C8T6 这个档位的芯片主频 72MHz64KB Flash20KB RAM裸机跑一个状态机加二十几个联动条件都还有富余。选它还有个现实原因生态成熟。标准外设库和 HAL 库的例程一抓一大把遇到问题容易搜到答案。F103C8T6 集成了 3 路 USART、2 个 I2C、2 个 ADC其中 ADC 有 10 个输入通道外设数量对这个场景是“有余量”而非“刚刚够”。如果引脚不够RCT6 同系列是大封装代码不用改如果追求更低成本也有国产同引脚兼容的型号可以直接替换。有人会问为什么不用 ESP32它自带 Wi-Fi 和蓝牙省一个模块。但 ESP32 的 ADC 线性度一般而且把联网和采集耦合在一个芯片上网络异常时会影响本地控制。做暖棚系统我最看重的是“断网不能断控制”所以坚持本地主控加独立网络模块的分工。2.3 执行层规划继电器、交流接触器与电源隔离执行层的设备无非是风机、加热器、卷膜电机、补光灯和水泵。常见的驱动链路是GPIO 输出控制继电器模块继电器再去控制交流接触器接触器最后带动 220V 设备。小功率风机和补光灯可以直接用继电器模块带电加热这类大功率设备必须加交流接触器否则继电器触点会在几次启动后烧黑。我一般把继电器按功能分组并留出安全余量PA4 风机、PA5 加热、PA6 卷膜电机正转、PA7 卷膜电机反转。注意卷膜电机正反转是两个输出代码里必须做联锁防止正反两个继电器同时吸合烧电机。电源规划是这里最关键的单片机用 3.3V继电器线圈用独立 5V 电源不要和主控共用同一个 LDO。继电器吸合瞬间电流会拉低电源电压共用一个电源时最典型的故障就是“一吸合单片机就复位”这个问题几乎每个项目都会遇到一次。3. 把源码跑起来STM32 数据采集链路的工程骨架与关键驱动3.1 工程骨架标准库写法下先做哪几件事我习惯用标准外设库而不是 HAL 库因为这类源码在网络上流传最多新手对着改也容易。工程结构分成几个独立文件sys 管时钟和延时、oled 管显示、dht22 管温湿度、adc_dma 管模拟量采集、control 管控制逻辑、esp8266 管网络上报main 里只做初始化和调度。代码不用上 FreeRTOS裸机状态机足够。/* main.c - 标准外设库裸机工程骨架截选 */ #include stm32f10x.h #include sys.h #include oled.h #include dht22.h #include adc_dma.h #include control.h int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); SysTick_Init(72); /* 1ms 节拍延时、超时、调度都靠它 */ OLED_Init(); /* I2C OLED地址 0x3C */ ADC_DMA_Init(); /* 双通道循环采样DMA 搬运 */ DHT22_Init(); /* 空气温湿度单总线 */ Control_Init(); /* 装载阈值参数、手动自动标记 */ while (1) { Sensor_Collect(); /* 秒级采集内部做周期控制 */ Control_Run(); /* 阈值比较 滞回 联锁 */ Report_Loop(); /* OLED 刷新 串口上报 */ Delay_Ms(200); /* 主循环节拍 200ms */ } }逻辑说明初始化顺序有讲究OLED 要最先起来方便后续调试打印ADC 和 DHT22 都依赖 SysTick 的延时函数所以 SysTick 必须放在最前面。主循环里 Sensor_Collect 和 Control_Run 都各自维护自己的时间戳不是每次循环都重新采集避免 DHT22 读取太频繁导致传感器内部出错。参数说明SysTick_Init(72) 在 72MHz 主频下产生 1ms 中断。主循环 Delay_Ms(200) 是给整个调度一个稳定节拍但 DHT22 的读取间隔要控制在 1 秒以上继电器动作间隔也在秒级所以 200ms 的循环完全够用。这个骨架的好处是后面加传感器只要在初始化里加一行、在 Sensor_Collect 里加一个分支不会动到主流程。3.2 模拟量采集ADCDMA 双通道循环采样与滤波土壤水分和光照都是慢变信号不需要实时中断读取正确做法是 ADC 连续转换DMA 循环搬运主循环里直接取结果。这样 CPU 不用频繁进中断传感器内阻大导致的采样误差也可以通过多取几次平均来消除。/* adc_dma.c - ADC1 规则组双通道 DMA 循环采集 */ #define ADC_CH_NUM 2 #define ADC_SAMPLE_CNT 8 static uint16_t adc_raw_buf[ADC_CH_NUM * ADC_SAMPLE_CNT]; void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; /* 模拟输入 */ GPIO_Init(GPIOA, GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_raw_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_CH_NUM * ADC_SAMPLE_CNT; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; /* 循环模式无需重装 */ DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; /* 扫描多通道 */ ADC_InitStructure.ADC_ContinuousConvMode ENABLE; /* 连续转换 */ ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel ADC_CH_NUM; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)) ; ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)) ; ADC_SoftwareStartConvCmd(ADC1, ENABLE); }逻辑说明DMA 每转换完一个通道就往数组里写一个半字数据8 次采样后换到下一个通道如此循环。主循环里读数组时取每个通道 8 个采样值的平均值并把最大最小各剔除一个剩下的 6 个数再平均能有效滤掉土壤探头接触不良造成的瞬时尖峰。参数说明ADC_SampleTime_239Cycles5 是这代芯片最长的采样时间。土壤电容式探头内阻大如果采样时间设太短内部采样电容还没充满就会读得偏低而且这个偏低值不稳定。光照分压回路内阻小但两个通道共用一套配置更省事。DMA_PeripheralDataSize 和 MemoryDataSize 都用 HalfWord因为 12 位 ADC 结果用 16 位存储就够了。读取函数我通常会写成下面这种带过滤的形式参数 ch_index 传 0 或 1对应 PA0 光照和 PA1 土壤uint16_t Read_ADC_Filter(uint8_t ch_index) { uint32_t sum 0; uint16_t min 0xFFFF, max 0; uint8_t i; for (i 0; i ADC_SAMPLE_CNT; i) { uint16_t v adc_raw_buf[ch_index * ADC_SAMPLE_CNT i]; sum v; if (v min) min v; if (v max) max v; } sum - min max; return (uint16_t)(sum / (ADC_SAMPLE_CNT - 2)); }说明一下这个函数只做滤波不做物理量换算。土壤水分百分比建议放到控制层或应用层去换算因为不同探头的电压范围差别很大纯底层 src 不该写死这些标定参数。3.3 数字量温湿度DHT22 单总线时序与校验DHT22 是单总线数字传感器数据脚既要输出又要输入所以驱动要不停切换 GPIO 方向。它的时序比较苛刻主机先拉低 1ms 以上作为启动信号然后释放总线传感器应答后连续发出 40 个 bit。每个 bit 以低电平开始高电平持续 26~28us 表示 070us 表示 1。读错时序最常见的表现就是偶尔出一个离谱的负温度。/* dht22.c - 单总线读取返回 0 表示成功 */ uint8_t DHT22_Read(int16_t *temp, uint16_t *humi) { uint8_t buf[5] {0}; uint8_t i, j; uint32_t cnt; uint16_t raw; GPIO_DIR_OUT(DHT22_PORT, DHT22_PIN); GPIO_SET_BIT(DHT22_PORT, DHT22_PIN); /* 先拉高 */ Delay_Ms(10); GPIO_CLEAR_BIT(DHT22_PORT, DHT22_PIN); /* 启动信号拉低 */ Delay_Ms(2); /* 拉低至少 1ms */ GPIO_SET_BIT(DHT22_PORT, DHT22_PIN); /* 释放切换输入 */ GPIO_DIR_IN(DHT22_PORT, DHT22_PIN); Delay_Us(50); cnt 0; while (GPIO_READ_BIT(DHT22_PORT, DHT22_PIN) cnt 100) cnt; /* 等应答低 */ cnt 0; while (!GPIO_READ_BIT(DHT22_PORT, DHT22_PIN) cnt 200) cnt; /* 等 80us 高 */ for (i 0; i 5; i) { for (j 0; j 8; j) { cnt 0; while (!GPIO_READ_BIT(DHT22_PORT, DHT22_PIN) cnt 200) cnt; /* 低电平开始 */ cnt 0; while (GPIO_READ_BIT(DHT22_PORT, DHT22_PIN) cnt 120) cnt; /* 高电平脉宽 */ buf[i] 1; if (cnt 50) buf[i] | 1; /* 脉宽 50us 判为 1 */ } } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) return 1; /* 校验和不匹配 */ *humi (buf[0] 8) | buf[1]; raw (buf[2] 8) | buf[3]; if (raw 0x8000) *temp -(int16_t)(raw 0x7FFF); /* 负温度处理 */ else *temp (int16_t)raw; return 0; }逻辑说明DHT22 的校验和是前四个字节相加取低 8 位。温度数据里高字节的最高位是符号位置 1 表示负温度所以要先单独摘出来处理不能直接强转。脉宽判断的 50us 阈值是经验值原厂手册里 0 的高电平脉宽是 26~28us1 是 70us中间取 50us 最稳。参数说明启动信号拉低时间至少 1ms我留 2ms 余量。每次读完建议至少间隔 1 秒再读下一次DHT22 内部采样周期是 2 秒左右频繁去读拿到的还是上次缓存的值。这块最玄学的坑在于读取过程中只要被中断打断时序就会错位解决方法是进入读取函数前关闭中断、读完再开或者像我后面避坑章写的一样用 SysTick 计时代替 GPIO 翻转延时。3.4 OLED 显示只刷变化值别做全屏刷新OLED 用 I2C 接在 PB6/PB7地址 0x3C驱动芯片 SSD1306。这块屏刷新率不高如果每 200ms 全屏写一次肉眼能看到明显闪烁而且 I2C 通信会占用总线影响其他设备。我的做法是单独拉一个 1 秒刷新任务只有数值变化时才更新对应位置的字符串。/* oled.c - 秒级刷新数据只重写变化区域 */ void OLED_ShowData(int16_t t, uint16_t h, uint16_t soil, uint8_t lux) { static int16_t last_t 0x7FFF; static uint16_t last_h 0xFFFF, last_soil 0xFFFF; char line[20]; if (t ! last_t) { snprintf(line, sizeof(line), Temp:%d.%d C, t / 10, t % 10); OLED_ShowString(0, 0, line); last_t t; } if (h ! last_h) { snprintf(line, sizeof(line), Humi:%d.%d %%, h / 10, h % 10); OLED_ShowString(0, 2, line); last_h h; } if (soil ! last_soil) { snprintf(line, sizeof(line), Soil:%d%%, soil); OLED_ShowString(0, 4, line); last_soil soil; } snprintf(line, sizeof(line), Lux:%d, lux); OLED_ShowString(0, 6, line); }逻辑说明OLED 的“页”概念是关键128×64 分成 8 页每页 8 像素高我用 16 像素高的字体所以写一行要占 2 页行坐标用 0、2、4、6。DHT22 返回的温度湿度都是整数放大 10 倍表示比如 250 代表 25.0℃显示时拆成整数和小数两部分。参数说明SSD1306 的 I2C 时钟可以配到 400kHz但如果你在同一根总线上还挂了别的传感器建议降到 200kHz 更稳定。OLED 这种显示器件属于“锦上添花”调试串口才是真正的主力所以我把串口打印和网络上报分开后面讲。4. 控制与联网逻辑阈值滞回、手动优先和 MQTT 上报的最小 C 实现4.1 阈值表把农艺经验翻译成 C 语言判断控制逻辑是这套源码的灵魂也是它和普通数据采集 Demo 拉开差距的地方。写之前先把农艺需求列一张表明确什么条件下开什么设备。每个大棚的作物不同参数一定不能写死在代码里要放在一个配置结构体里方便后面改。触发条件执行动作说明空气温度 28.0℃开风机1、关加热温度过高通风降温空气温度 15.0℃开加热、关风机1低温保护空气湿度 80%开风机2排湿高湿容易引发灰霉病土壤湿度 30%开水泵持续 60 秒定时关断防止烂根光照 3000 Lux开补光灯阴天补光常见的错误是把动作写得“太敏感”比如温度一超过 25℃ 就开风机低于 25℃ 就关风机结果风机在边界来回抖。所以控制函数一定不能直接用 if-else 裸奔要结合下一个节的滞回逻辑。/* control.c - 阈值控制入口200ms 周期调用 */ void Control_Run(void) { if (g_sys.mode MODE_MANUAL) return; /* 手动模式自动联动全部让位 */ /* 温度闭环调用滞回比较 */ g_relay.fan1 Hysteresis_Compare(g_sys.temp, 280, 240, g_relay.fan1); g_relay.heat Hysteresis_Compare(g_sys.temp, 150, 180, g_relay.heat); /* 土壤湿度带定时关断 */ if (g_sys.soil g_cfg.soil_low g_sys.pump_timer 0) { Relay_SetSafe(RELAY_PUMP, ON); g_sys.pump_timer 300; /* 主循环 200ms × 300 60 秒 */ } if (g_sys.pump_timer 0) { g_sys.pump_timer--; if (g_sys.pump_timer 0) Relay_SetSafe(RELAY_PUMP, OFF); } }逻辑说明控制周期 200ms对继电器来说足够快但 300 个周期就是 60 秒所以水泵定时关闭在这里实现非常直观。手动模式在所有判断之前直接 return等于告诉系统“现在不需要你值班了”。这里我把继电器状态存在 g_relay 结构体里滞回函数需要依赖上一次的状态才能决定本次输出。参数说明280 和 240 对应 28.0℃ 开风机、24.0℃ 关风机单位是放大 10 倍的整数改阈值时记得同步对齐单位。soil_low 是土壤湿度百分比我的经验是 30% 以下浇水、35% 停止给电容式探头留 5% 的滞回区不然水泵频繁启动对管路冲击很大。4.2 滞回继电器边界疯狂吸合的元凶暖棚温度不是恒定的风机会让温度缓慢下降降到阈值以下又关闭关闭后温度再升上去这个循环在边界点附近会以分钟甚至几十秒的频率反复吸合。继电器是机械触点频繁动作轻则发热重则触点粘连。滞回的思想很简单开启阈值和关闭阈值故意拉开一个区间。/* control.c - 滞回比较返回 1 开启、0 关闭 */ uint8_t Hysteresis_Compare(int16_t now, int16_t th_on, int16_t th_off, uint8_t last_state) { if (last_state 0 now th_on) return 1; /* 原本关闭超过开启线才开 */ if (last_state 1 now th_off) return 0; /* 原本开启低于关闭线才关 */ return last_state; /* 中间区间状态保持 */ }逻辑说明调用方式注意温度场景和土壤场景相反。温度过高开风机是正逻辑now 大于 th_on 开启土壤湿度是“低于下限浇水”写成 Hysteresis_Compare(soil, 35, 30, pump) 就表示低于 30% 开、高于 35% 停。同一个函数吃两套边界关键是想清楚 on 和 off 的方向。参数说明滞回区间大小直接影响设备寿命。风机类设备我可以接受 4~5℃ 的滞回因为温度变化慢水泵的滞回建议 3~5 个百分点太窄的话土壤湿度传感器本身的噪声就会触发启停。设置原则是“继电器每分钟动作不超过 3 次”这个经验值可以反向约束阈值区间。4.3 手动自动切换与安全联锁一个完整的系统必须允许现场人工干预。大棚里经常会出现传感器失灵或者临时调设备的场景比如阴天突然要额外补光这时候自动控制必须能一键让位。做法是加一个模式标志位物理按键短按切换自动/手动长按控制单个继电器点动。/* relay.c - 安全继电器控制正反转互锁 */ void Relay_SetSafe(uint8_t idx, uint8_t state) { if (idx RELAY_MOTOR_UP state ON) Relay_Set(RELAY_MOTOR_DOWN, OFF); /* 正转前强制停止反转 */ if (idx RELAY_MOTOR_DOWN state ON) Relay_Set(RELAY_MOTOR_UP, OFF); /* 反转前强制停止正转 */ Relay_Set(idx, state); }逻辑说明卷膜电机的两个继电器如果同时吸合直流电机或单相电机都会烧。这段联锁是执行层的最后一道防线即使控制逻辑写错这里也能兜底。手动模式下的按键点动也要走这个函数不能直接操作寄存器。参数说明手动模式我倾向于“临时接管”而不是“锁定”可以加一个超时逻辑进入手动模式超过 30 分钟自动切回自动防止哪天操作完忘了切回来夜里温度骤降而系统无动于衷。按键扫描放主循环里消抖用 20ms 计数。4.4 联网上报串口分工、ESP8266 透传与 MQTT 最小报文联网部分我用 ESP8266 走 USART2串口 1 保留给调试日志。两个串口一个往上、一个往下职责必须分开。ESP8266 的初始化是一串 AT 指令这里有一个关键经验不要每次上电都走完整联网流程如果模块已经连接正常直接发 MQTT 的 PUBLISH 会更快更稳。/* esp8266.c - 上电初始化序列 */ static const char *at_cmds[] { ATRST\r\n, /* 复位模块 */ ATE0\r\n, /* 关闭回显减轻解析负担 */ ATCWMODE1\r\n, /* Station 模式 */ ATCWJAP\wifi_ssid\,\wifi_pass\\r\n, /* 连接路由器 */ ATCIPSTART\TCP\,\192.168.1.100\,1883\r\n, /* 连接 MQTT 服务器 */ ATCIPMODE1\r\n, /* 透传模式 */ ATCIPSEND\r\n, /* 进入数据发送状态 */ }; void ESP_Init_Blocking(void) { uint8_t i; for (i 0; i sizeof(at_cmds) / sizeof(at_cmds[0]); i) { USART2_SendString(at_cmds[i]); if (ESP_WaitReply(OK, 3000) 0) ESP_Reboot(); /* 3 秒内无应答强制复位重来 */ } }逻辑说明AT 指令的应答解析不用做得很复杂串口缓冲里能匹配到“OK”就算通过。CWJAP 连接路由器有时要花 5~10 秒超时时间给 10 秒更稳。CIPMODE1 之后的透传模式里发什么数据就直接进入 TCP 连接但要注意 TCP 断开后模块不会自动退出透传需要发“”返回 AT 模式重新连。参数说明手机 App 或上位机展示的数据我一般 10 秒上报一次状态突变时立即上报这样流量很小服务器压力也不大。MQTT 的心跳包 60 秒发一次服务器 90 秒没收到会认为设备掉线。我建议 MQTT 服务器优先用本地或同机房的地址公网域名解析偶尔会卡住。MQTT 的 CONNECT 报文不用任何第三方库手拼一个最小包就够了这也是 C 源码能跑通的硬核部分/* mqtt.c - 构造 MQTT CONNECT 报文返回总长度 */ uint16_t MQTT_BuildConnect(uint8_t *buf, const char *client_id) { uint16_t len 0; uint16_t id_len (uint16_t)strlen(client_id); uint16_t remain 10 2 id_len; /* 可变头10字节 ID占位 */ buf[len] 0x10; /* CONNECT 固定头 */ buf[len] (uint8_t)(remain 0x7F); /* 剩余长度128 简化编码 */ buf[len] 0x00; buf[len] 0x04; memcpy(buf[len], MQTT, 4); len 4;/* 协议名 */ buf[len] 0x04; /* 协议级别 4 */ buf[len] 0x02; /* Clean Session */ buf[len] 0x00; buf[len] 0x3C; /* KeepAlive 60s */ buf[len] 0x00; buf[len] (uint8_t)id_len; memcpy(buf[len], client_id, id_len); len id_len; return len; }逻辑说明剩余长度是 MQTT 报文最容易算错的位置这里故意只支持小于 128 的长度实际项目里 client_id 没那么长。连接成功后上报一条数据只需要再拼一个 PUBLISH 包固定头 0x30、剩余长度、topic 长度加主题内容、最后是 payload。我曾经在这里翻过车把剩余长度算成整个包长导致服务器直接断开 TCP 连接。参数说明KeepAlive 的 60 秒要和主控的心跳发送周期对齐主控每半分钟发一次 PINGREQ固定头 0xC0、剩余长度 0x00就可以不用真等到 60 秒。这个差值是为了给网络排队留余量防止偶然延迟导致误判掉线。5. 避坑暖棚系统长时间运行最容易翻车的 4 类问题5.1 现象继电器一吸合OLED 闪一下、单片机整个重启这是温室项目最常见的事故现场。现象很典型手动开风机一切正常一旦打开水泵或加热器屏幕乱码程序从头跑。原因继电器线圈是感性负载吸合瞬间电流是稳态的几倍电源被瞬间拉垮MCU 供电跌到复位阈值以下。另一个隐藏原因是继电器模块没有续流二极管线圈断电瞬间产生反向电动势从 GPIO 口反灌进单片机。解决电源分两路继电器线圈用独立 5V 电源主控用 3.3V 单独稳压继电器模块尽量选带光耦隔离的线圈两端反向并联一颗 1N4007 二极管负极接正电源。这一步做完“一吸合就重启”的毛病基本消失。5.2 现象土壤湿度读数一天比一天小最后变成 0有段时间我的监控曲线上土壤湿度从 40% 缓慢下滑到 5%以为是地干了跑过去一看土壤是湿润的。拔出来看探头两个金属电极已经发黑。原因用的是电阻式土壤探头铜电极在直流激励下发生电化学腐蚀同时土壤中的离子极化会让测量值漂移。这个问题不会马上暴露通常使用一个月后才开始明显。解决换成电容式土壤探头它靠介电常数变化感知水分电极不接触土壤中的自由离子寿命长很多倍。另外在代码里不要持续给探头供电采样前 100ms 才上电读完立刻断电能把电解反应降到最低。电容式探头刚插进土和插稳之后读数差不少要等 30 秒左右再取平均。5.3 现象ESP8266 能连上路由器但 MQTT 服务器一直连不上ATCWJAP 返回 OKATCIPSTART 却报 CONNECT FAIL或者偶尔连上但几秒就断开。原因最常见的是服务器地址用了域名DNS 解析超时其次是 MQTT 固定端口被运营商封锁或者服务器只接受加密 TLS 连接而 ESP8266 原厂 AT 固件默认不带 TLS。解决先改成服务器 IP 直连绕过 DNS 问题然后在电脑上把 AT 指令的每一步回复打印出来确认 TCP 连接成功后再发 CONNECT 报文。用串口抓包看 MQTT 报文时重点检查剩余长度字节我曾经把 0x12 写成 0x17服务器一直没反应。如果确认服务器强制 TLS就不要在模块里硬扛增加一个能跑 TLS 的网关把数据转出去或者直接选支持加密的物联网模组。5.4 现象温度偶尔跳成 -999历史曲线出现一个大坑温度曲线整体正常每隔十几个小时突然掉到 -999然后又恢复正常。这个现象在远程监控平台上特别刺眼还会触发错误报警。原因DHT22 读取过程中被中断打断单总线时序超时代码里校验失败后返回了一个表示错误的值很多源码写 -999。另外传感器线超过 2 米且没加粗线径时信号边沿变缓也会导致脉宽测量偏差。解决读取 DHT22 之前关全局中断读完再开保证 40bit 数据不被 SysTick 打断读取失败不要把错误值写入系统状态而是沿用上一次正确值连续失败 5 次才报传感器故障。再加一层软件滤波当前读数与上次读数相差超过 5℃ 就认为是异常直接丢弃。下面这行就是过滤逻辑if (DHT22_Read(t, h) 0) { if (abs(t - g_sys.temp) 50) /* 温度突变 5℃ 才更新 */ g_sys.temp t; }参数说明50 代表 5.0℃温室里 10 秒内温度不可能跳变这么多除非探头被手握住或者被水淋到。这种“宁可丢数据也不要用脏数据”的思路是所有采集代码都应该遵守的底线。6. 进阶验证校准、掉线自愈与半年免维护的实战技巧6.1 两点校准让传感器数据不再是“大概准”DHT22 这类传感器出厂带一点离散偏差同一批两个探头放一起能差 1℃。我的做法是校准后把偏移量写进配置区拿标准温度计和 DHT22 放在同一个环境里稳定 10 分钟读取差值如果差 0.8℃就在代码里设置 temp_offset 80.8℃ 放大 10 倍所有上报和判断都加上这个量。土壤湿度可以做两点线性映射探头完全风干时记 dry_raw泡水饱和时记 wet_raw实际百分比这么算soil_percent (raw - dry_raw) * 100 / (wet_raw - dry_raw);这个公式很简单但效果立竿见影。农业上 5% 的误差在控制上没多大影响关键是长期稳定性。6.2 掉线自愈不给人半夜跑去重启的机会系统跑在暖棚里最怕的不是坏而是“坏了没人知道”。我习惯加三层自愈第一层是独立看门狗主循环死机超过 1 秒自动复位第二层是 MQTT 心跳服务器连续 90 秒没收到心跳就标记设备离线现场 OLED 同步亮出“NET OFF”提示第三层是重连上限ESP8266 重连 5 次失败就脉冲复位模块电源而不是一直卡在 AT 等待里。我现在的习惯是先把本地闭环跑一周确认控制逻辑没问题再开联网上报。网络断了大棚里照样恒温恒湿这才是这套系统该有的底子。这些坑我当年都踩过写出来是希望帮你少走点弯路希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑