百元成本STM32智能大棚环境监测系统实战 📅 发布时间:2026/9/2 2:14:51 👁 浏览次数: 简介基于STM32F103C8T6与ZigBee无线通信的智能大棚环境监测系统完整源码工程面向物联网、嵌入式及农业智能化方向的开发者与备赛学生。系统由主机端完成温度、光照、土壤湿度采集通过CC2530组网透传从机端经LCD1602实时显示并支持RSSI链路质量监测与断链告警方案包含双时间窗断链检测、ADC-DMA采样、串口环形缓冲区等工程细节适合作为课程设计或竞赛项目蓝本。压缩包共37个文件含16个C源码、13个头文件、Makefile、链接脚本与说明文档代码模块分为传感器驱动、ZigBee协议适配、显示管理与异常处理易于阅读和二次移植。页面显示已有36人学习资源虽小巧但对理解STM32外设、ZigBee组网与前后端数据交互具有较高参考价值。1. 项目概述一个用百元成本搞定的智能大棚环境监测系统先说结论这个STM32智能大棚监测系统是我做过的嵌入式项目里性价比最高、见效最快的一个。硬件成本控制在100元以内代码量不到2000行却能完成温湿度、光照强度、土壤湿度三个维度的实时采集把数据同步显示到本地OLED屏幕和手机端还能根据预设阈值自动控制水泵、风扇和补光灯。整个过程从零开始搭建到跑通大约花了一个周末的时间。这套系统最适合两类人一是正在学STM32但不知道怎么把外设凑成完整项目的开发者二是有小型大棚、阳台种植、菌菇房、花房这类场景需要低成本环境监控的爱好者。它把STM32的GPIO、ADC、定时器、串口、I2C这些外设全部串了起来而且每一块代码都可以单独拆出来复用——这其实是源码最大的价值所在不是整个工程难懂而是每个模块拎出来都能直接抄到别的项目里。系统的工作流程是这样的STM32作为主控通过DHT11采集空气温湿度通过电容式土壤湿度传感器检测土壤含水量通过光敏电阻模块感知光照强度采集到的数据一方面显示在OLED屏上另一方面通过ESP8266模块以MQTT协议上传到云端或局域网服务器手机端可以实时查看。同时主控内部会根据你设定的阈值做判断比如土壤湿度低于30%就自动打开水泵浇灌10秒温度高于35度就启动风扇散热光照不足就点亮补光灯。整套系统是全自动运行不需要人为干预。2. 系统整体设计与方案选型逻辑2.1 为什么选STM32作为主控这个项目的核心主控选的是STM32F103C8T6也就是大家常说的蓝色药丸板。选择这颗芯片的原因很直接第一价格便宜国产替代版本几块钱就能拿到核心板也就十几块第二外设资源够用它有3个USART、2个I2C、2个SPI、12位ADC对于大棚监测这种低复杂度场景绰绰有余第三资料极其丰富无论你用的是标准外设库还是HAL库遇到问题基本上搜一下就有答案。我特别想说明的一点是很多初学者一上来就纠结用标准库还是HAL库。我的实际经验是如果你用的是STM32CubeMX生成工程那就老老实实用HAL库因为代码自动生成入门门槛低如果你想把程序压到极致、追求对底层寄存器的完全掌控那就用标准库或直接操作寄存器。这个项目我选的是HAL库加CubeMX生成的方式原因很简单——大棚监测系统本身逻辑并不复杂没必要在寄存器层面纠结把精力留给传感器时序和通信协议更重要。2.2 传感器与执行机构的选型逻辑传感器选型是这个项目最有意思的部分也是踩坑最多的地方。温湿度我用的DHT11因为它的成本只有两三块钱单总线协议就能搞定虽然精度一般温度±2度湿度±5%但对于大棚环境监测来说够用了。如果你对精度有更高要求可以换DHT22或SHT30但DHT22的时序更严格SHT30是I2C接口代码逻辑会不同。土壤湿度传感器这里要重点说一下市面上有两种电阻式和电容式。电阻式是两个裸露的金属探针靠土壤导电性来判断湿度价格便宜但容易电解腐蚀一般用几个月就废了而且不同土壤的电导率差别很大读数漂移厉害。电容式传感器是检测土壤介电常数变化不会腐蚀寿命长读数也更稳定。这个项目我选的是电容式虽然贵了几块钱但长期稳定性完全值得。光照检测模块用的是光敏电阻加比较器输出的数字模块也可以直接用光敏电阻配合ADC做模拟量采集。用带比较器的数字模块有一个好处可以通过电位器调节亮暗阈值直接输出高低电平代码里只需要读一个GPIO口。但为了演示ADC采集我选择用光敏电阻直接接ADC通道这样可以同时展示模拟量采集的完整流程也为后续做光照强度的连续调节比如PWM调光留了余地。2.3 系统架构总览整个系统的数据流向和处理逻辑可以用一句话概括传感器采集物理量 → 信号调理后进入STM32的ADC或GPIO → 处理器进行数据处理和阈值判断 → 输出控制信号给继电器执行机构同时把数据分发到OLED显示屏和ESP8266串口通信模块。电源部分我使用的是12V电源适配器给整个系统供电12V电压在分压后给继电器供电用AMS1117-3.3稳压给ST-Link的3.3V端子供电给STM32核心板使用。为什么不用USB供电因为继电器线圈吸合瞬间有比较大的电流冲击容易导致电压跌落引起单片机复位。这个问题很多人在实际使用中才会遇到后面排查问题部分我也会提到。3. 硬件搭建与接线实操要点3.1 核心板与传感器接线对照表硬件接线是整个项目最基础的部分但也是出错概率最高的地方。我整理了一张实际使用的接线表直接照着接就行模块引脚连接到STM32F103C8T6DHT11DATAPA0上拉10K电阻到3.3V土壤湿度传感器AOUTPA1ADC1_IN1光敏电阻模块AOUTPA2ADC1_IN2OLED显示屏I2CSDAPB7OLED显示屏I2CSCLPB6ESP8266模块TXDPA3USART2_RXESP8266模块RXDPA2USART2_TX注意要分压继电器水泵IN1PB0继电器风扇IN2PB1继电器补光IN3PB10这里有几个细节要特别提醒DHT11的数据引脚必须接一个4.7K到10K之间的上拉电阻否则读数据会不稳定。ESP8266的RXD引脚电平是3.3V但有些模块的TXD输出是3.3V而STM32的TXD推挽输出也是3.3V理论上可以直接连接但我实际测试中发现ESP8266在某些固件下的TXD线会向上串扰稳妥起见加一个10K串联电阻做保护。3.2 电源设计与抗干扰处理电源是整个系统最容易出问题却又最容易被忽视的部分。我把电源分成了三个层级12V主电源进系统后一组直接给继电器供电一组通过降压模块降为5V给ESP8266和OLED供电另一组再通过AMS1117-3.3降压给STM32供电。之所以要分开放是因为继电器这类感性负载在开关瞬间会产生反向电动势如果和单片机共用电源极易造成复位。每个模块的电源引脚旁边我都加了100nF的去耦电容继电器线圈两端并联了一个1N4007续流二极管。这些是小细节但如果没有它们系统跑几分钟就可能随机死机而且这种问题极其难排查。我用万用表量过继电器吸合瞬间的电流尖峰能达到几百毫安这在没有去耦和续流的情况下对3.3V供电的影响是非常致命的。此外传感器信号线不要和继电器控制线绑在一起走线我一开始图省事用了排线集中走结果继电器一动作ADC数值就跳。分两根线束走线并且让信号线尽量短这个问题就消失了。3.3 成本清单参考分享一个大概的成本明细不同渠道价格有浮动但整体幅度不会太大器件型号/规格参考价格主控板STM32F103C8T6核心板约12元温湿度传感器DHT11模块约3元土壤湿度传感器电容式约12元光敏电阻模块带ADC输出约2元OLED显示屏0.96寸I2C接口约8元WiFi模块ESP8266-01S约6元继电器模块3路光耦隔离约8元电源部分12V适配器降压模块AMS1117约15元其他杜邦线、接线端子、PCB板约15元整套下来不到100元。如果你想降成本主控换成STM32F030系列、WiFi模块去掉改成纯本地显示70元以内也能搞定。但说实话多花30块钱可以保留远程监控能力非常划算。4. 源码结构解析与核心模块实现4.1 工程文件组织方式拿到源码之后先别急着打开就编译花五分钟把工程结构看一遍后面阅读和修改会有条理得多。这个项目的源码基于HAL库工程目录结构是这样的SmartFarm/ ├── Core/ │ ├── Inc/ // 头文件目录 │ │ ├── main.h │ │ ├── dht11.h │ │ ├── sensor.h │ │ ├── relay.h │ │ └── oled.h │ └── Src/ // 源文件目录 │ ├── main.c │ ├── dht11.c │ ├── sensor.c │ ├── relay.c │ └── oled.c ├── Drivers/ // ST官方驱动库 ├── Middlewares/ // 第三方中间件 └── SmartFarm.ioc // CubeMX配置文件这种模块化组织的核心思想是每个外设对应一个.c和.h文件把功能独立封装主函数只负责调用。这个原则听起来简单但很多嵌入式初学者习惯把所有代码都堆在main.c里结果几百行之后自己也找不着北了。我自己的习惯是main.c里只保留系统初始化和主循环调度逻辑具体功能全部封装成函数这样做的好处是调试的时候可以单独屏蔽某个模块快速定位问题。4.2 DHT11温湿度采集实现DHT11使用的是单总线协议时序要求比较严格发送一次读取指令到数据返回大约需要4ms左右。DHT11的数据帧格式是40位8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。校验和的算法很简单就是前四个字节相加的低八位等于校验字节则读取成功。DHT11的驱动在STM32上的核心坑点是时序函数。HAL库的HAL_Delay()函数最小粒度是1毫秒但DHT11的时序要求到了微秒级别所以必须自己实现微秒延时。我使用的是TIM4做微秒延时而不是用SysTick原因在于SysTick已经被HAL库的HAL_GetTick()用掉了再占用会互相干扰。实际代码中的关键部分是主机发送起始信号和读取40位数据// 发送起始信号拉低至少18ms再拉高20-40us DHT11_DATA_HIGH(); delay_us(2); DHT11_DATA_LOW(); delay_us(20000); // 主机拉低至少18ms DHT11_DATA_HIGH(); delay_us(30); // 然后拉高20~40us DHT11_DATA_INPUT(); // 切换为输入模式准备读取应答 // 读取应答DHT11拉低约80us再拉高约80us if (DHT11_READ() 0) { while (DHT11_READ() 0); // 等待拉高结束 while (DHT11_READ() 1); // 等待高电平结束继续读数据 } // 循环读取40位数据 for (int i 0; i 40; i) { while (!DHT11_READ()); // 等待每个bit前的低电平信号50us delay_us(30); // 延时30us后采样 if (DHT11_READ() 1) { // 高电平持续50-70us表示1这里实测时落到40~80us之间 buf | (1 (7 - (i % 8))); } while (DHT11_READ() 1); // 等待当前bit高电平结束 if (i % 8 7) { // 每8位存一个字节 dht11_data[(i / 8)] buf; buf 0; } }这个代码的精髓在于用TIM4实现的microsecond级延时。我一开使用循环自减做延时在开启优化后完全不工作因为编译器把循环优化掉了换成TIM4计数之后彻底解决。实际经验是DHT11对时序容错范围还比较宽不需要太精确但延时函数一定要和编译优化选项无关的东西来实现。4.3 ADC多通道土壤湿度与光照采集STM32F103C8T6内置12位ADC有10个外部通道。我使用了ADC1的IN1和IN2两个通道分别采集土壤湿度和光照。ADC的配置用CubeMX就能完成把PA1和PA2都设置为模拟输入ADC1的扫描模式打开、连续转换关闭触发方式选软件触发。转换完成后用DMA把结果搬内存里这样读取不会阻塞CPU。代码逻辑上采用定期采样取平均值的方式每200ms采样一次连续采10次去掉最大值最小值后取平均。这个做法叫作中位值平均滤波法对于传感器信号的随机噪声和不规则毛刺有很好的抑制作用。// 软件触发ADC转换并读取指定通道的值 uint32_t read_adc_average(ADC_HandleTypeDef *hadc, uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; uint32_t sum 0; uint32_t samples[10] {0}; sConfig.Channel channel; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc, sConfig); for (int i 0; i 10; i) { HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 100); samples[i] HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); HAL_Delay(2); // 留出采样间隔 } // 冒泡法去除最大最小值 for (int i 0; i 9; i) { for (int j 0; j 9 - i; j) { if (samples[j] samples[j 1]) { uint32_t tmp samples[j]; samples[j] samples[j 1]; samples[j 1] tmp; } } } for (int i 1; i 9; i) { sum samples[i]; } return sum / 8; }ADC的原始数值是0~4095需要转换成实际物理量才能用于逻辑判断。土壤湿度的转换公式是湿度百分比 (1.0 - (adc值 / 4095.0)) * 100因为电容式传感器在越干燥时输出电压越高。光照强度的处理更简单也是基于ADC值做一个0-100的百分比映射。这两个数值不需要特别精确但需要能稳定反映环境变化趋势。4.4 继电器控制与阈值判断逻辑控制逻辑放在main函数的主循环里采用状态机的思想而不是简单的if-else叠加。这样做的原因是为了避免多个执行机构之间的逻辑冲突。举个例子温度过高时需要开风扇但如果同时又判定为光照不足需要开补光灯补光灯会散热、风扇会散热这就会造成逻辑上的打架。我的阈值判断策略分两级第一级直接比较当前值和阈值第二级加上滞回控制迟滞区间。所谓滞回控制就是设置两个阈值点比如水泵启动湿度是30%停止湿度是50%。这样避免传感器在临界值附近抖动时继电器频繁开关。如果没有滞回湿度在29%到31%之间波动时水泵会疯狂地开了又关、关了又开继电器寿命会迅速耗尽。// 土壤湿度低于下限且当前不在浇水状态 - 启动水泵 if (soil_humidity HUMIDITY_LOW water_status 0) { HAL_GPIO_WritePin(RELAY_PUMP_GPIO_Port, RELAY_PUMP_Pin, GPIO_PIN_SET); water_status 1; water_start_time HAL_GetTick(); } // 浇灌10秒后或土壤湿度达到上限 - 停止水泵 if (water_status 1 (HAL_GetTick() - water_start_time 10000 || soil_humidity HUMIDITY_HIGH)) { HAL_GPIO_WritePin(RELAY_PUMP_GPIO_Port, RELAY_PUMP_Pin, GPIO_PIN_RESET); water_status 0; }还有一点要注意继电器模块一般带光耦隔离低电平触发有效也就是IN引脚接到低电平时继电器吸合接到高电平时断开。所以代码里控制引脚输出低电平、断开输出高电平接线时一定要看模块说明书别把HIGH和LOW搞反了。4.5 OLED显示与串口输出OLED用的是0.96寸128x64 I2C屏SSD1306驱动芯片。我用的库是移植uclib的接口层只需要实现I2C写字节的函数就可以调用显示字符串、显示数字、画矩形这些基础API。显示布局上把屏幕分成四个区域第一行显示温度第二行显示湿度第三行显示土壤湿度第四行显示光照和WiFi连接状态。刷新频率是每2秒刷新一次太快了会造成闪烁太慢了又感觉不灵敏。串口部分用了两个USARTUSART1连接电脑调试串口波特率115200用于打印调试信息和系统日志USART2连接ESP8266模块同样115200波特率。这里有一个关键的坑ESP8266的固件默认是115200波特率如果你是买了模块自己刷的固件记得先确认波特率。USART2使用了DMA传输和空闲中断接收即使接收端来不及处理数据也不会丢包。串口数据帧的格式是JSON形如{t:25.3,h:62,s:45,l:80}WiFi模块收到JSON帧后按MQTT协议透传到服务器。用JSON的好处是后续接任意平台——无论是巴法云、OneNET还是自建的Node-RED——解析起来都非常方便。5. 调试过程中的典型问题与排查实录5.1 下载失败为什么程序一烧就报错这个问题几乎每个人都遇到过。用ST-Link给STM32下载程序时第一次能烧进去第二次就一直在Flash Download failed不然就是Internal command error。最常见的原因是代码里把SWD引脚PA13/PA14复用了比如把PB0配置成了普通GPIO输出控制继电器然后CubeMX里默认的调试接口没选对。解决办法有两个一是按住复位按键不放在点击下载的瞬间松开复位这样能绕过运行中的程序二是使用ST-Link Utility或STM32CubeProgrammer的Connect under reset模式强制连接。根治办法是在CubeMX的SYS选项里把Debug设置为Serial Wire并且不要随意把PA13/PA14配置成其他功能。5.2 DHT11读不到数据时序为什么这么难调DHT11读不到数据的时候我第一反应是线接错了第二反应是上拉电阻没接第三反应才是代码问题。如果你的DHT11模块是那种带PCB板的三引脚模块板上可能已经有上拉电阻但如果你是买裸传感器自己接的那必须外接一个4.7K到10K的上拉电阻到VCC。排除了硬件之后用逻辑分析仪抓一下时序对比标准时序图和实际抓到的波形基本就能定位问题。另外有一个很值得注意的细节DHT11的精度和采样频率决定了每次读取间隔至少要在1秒以上如果连续读两次第二次极大概率读到0。所以我在代码里做了一个超时保护如果连续5次读取失败就把上次的有效数据保留下来同时输出一条告警日志而不是让系统直接卡死。5.3 ADC采集值乱跳到底怎么处理ADC数值跳动的幅度在50到200之间都属于正常现象。如果只是轻微跳动靠代码里的滤波函数就能解决。但如果数值跳得很离谱比如湿度从100直接跳到2000那就要检查硬件了。我实际遇到过一个特别隐蔽的问题光敏电阻模块和土壤湿度传感器共用一条长杜邦线供电由于导线电阻的影响传感器端的VCC电压已经不足3.0V而STM32的ADC参考电压是3.3V这会导致ADC读数整体偏低。解决办法是给每个模块单独拉一条供电线尽量用短而粗的线。另外ADC引脚旁边加一个1uF的电容到地也有明显的稳波效果。5.4 串口数据乱码竟然是晶振配置的问题这个问题在STM32开发里非常经典。用USB转串口接USART1打印调试信息波特率设置115200但数据一直是乱码。查了半天代码都没问题最后发现是CubeMX里RCC配置里的HSE外部高速晶振的值填错了。开发板上的晶振是8MHz但CubeMX默认选的可能是HSE 25MHz导致系统主频被错误地倍频到最高的64MHz乃至过调理。系统主频不对波特率自然也是错的。这里有个快速验证方法如果串口发AA收到的是0x00或者0x55基本可以确定是主频不对如果收到的数据帧看起来完全乱码并且每次内容都不一样优先怀疑波特率不对。把CubeMX里的HSE值改成8MHz后乱码问题立刻消失。这个坑值得写进教科书因为它和代码逻辑完全无关。5.5 继电器动作就会导致单片机复位怎么排查继电器一吸合屏幕闪一下然后系统重启这是整个项目里最让人崩溃的问题。我当时排查了很久最后定位到是电源问题——继电器线圈吸合瞬间需要较大的电流这个瞬间的电流冲击导致3.3V电压跌落低于STM32的最小工作电压大约2.0V单片机就复位了。方案有三个第一个是继电器使用独立电源供电12V直接给继电器模块供电不要再通过3.3V这路第二个是在继电器的电源端并联一个大电容电解电容220uF左右可以起到缓冲作用能扛住瞬间电流尖峰第三个是给STM32的供电端加上稳压芯片。最终我是把这三条都用上了才彻底解决。这个问题的排查过程也印证了前面说的电源设计一定不能图省事。5.6 常见问题速查表现象可能原因解决方法烧录失败SWD引脚被禁用按住复位再下载或使用Connect under resetDHT11一直返回0上拉电阻缺失或引脚配置错加4.7K-10K上拉检查GPIO输入模式ADC值跳动滤波不够或参考电压不稳加中位值平均滤波检查供电串口乱码HSE晶振值配置错确认CubeMX中HSE值与实际晶振一致继电器动作时复位电源压降继电器独立供电加大电容OLED不显示I2C地址不对扫描I2C地址确认0x3C或0x3DESP8266无响应波特率错或模块供电不足确认固件波特率检查供电电流6. 项目扩展方向与个人心得系统跑通之后我把它放在朋友的一个小型育苗棚里实际运行了一周整体非常稳定。但这只是第一步这个项目还有很多可以深入的方向。如果你想让整套系统更实用我建议从下面几个方向入手。第一个方向是加入实时时钟RTC和EEPROM存储。RTC可以提供时间基准让系统记录历史数据比如每小时存一次温湿度数据到EEPROM或外部Flash方便后续回放分析。第二个方向是把ESP8266升级成ESP32用ESP32的WiFi和蓝牙能力同时做组网通信甚至可以在ESP32上直接跑一个本地Web服务器手机浏览器打开局域网IP就能看到实时数据和控制继电器不需要依赖任何云端平台。第三个方向是加入更多的传感节点比如CO2浓度传感器、土壤PH值传感器把采集维度扩展得更加完整。在源码层面我这里还有两个建议一是给继电器控制代码加上手动/自动切换模式方便调试二是加上定时器中断做状态机的时基保证即使主循环里有阻塞式的延时也不会导致控制逻辑出现明显的时间偏差。最后再分享一个小技巧。调试这个系统的时候我始终保留了USART1作为调试串口所有模块的关键状态都用printf打印出来DHT11是否读到了有效数据、ADC原始值是多少、继电器是开还是关、WiFi是否连接成功、MQTT是否发布成功。这套日志系统在排查问题的时候帮了大忙特别是测试传感器时序和判断继电器逻辑的时候。嵌入式开发很多时候不是程序写不出来而是问题定位不了——一条清晰的日志链路能让你少加两个晚上的班。本文还有配套的精品资源点击获取