☰
STM32与ZigBee智能大棚监测系统源码实战解析
2026/10/18 14:03:15 网站建设 项目流程

简介:一套基于STM32F103C8T6与ZigBee无线通信的智能大棚环境监测系统源码,面向物联网、嵌入式或农业信息化方向的开发者与课程设计人群,解决大棚内温度、光照、土壤湿度远程实时监测与通信异常预警问题。系统采用主机与从机分布式架构,主机端完成多传感器采集与无线组网,从机端负责数据解析、LCD1602显示和断链提示,代码按host/slave、zigbee、sensors等模块划分,便于移植扩展。压缩包共37个文件,以16个C源码和13个头文件为主,配合Makefile、链接脚本、STM32标准外设库启动文件及README等,整体仅79KB,结构清晰、无冗余工程文件。当前已有41人学习下载。资料包含完整可编译的MDK工程源码、传感器驱动与ZigBee协议栈适配层实现,并给出看门狗断链检测、ADC+DMA采样及低功耗供电策略等关键处理思路,适合作为毕业设计、智能农业项目或课程实践的直接参考。

1. 这棵“菜园子电路板”到底能种出什么

北方的冬天,大棚里凌晨三点温度跌破零度,等你早上发现,一垄草莓已经冻得发黑;南方的梅雨季,土壤湿度饱和到根系缺氧,补种都来不及。这些损失,本质上是同一个问题:环境数据靠人盯,盯不住。而我要拆的这份 STM32 智能大棚监测系统源码,就是来干掉“人肉巡检”的。它用 STM32 做主控,配合 ZigBee 无线网络,把温湿度、光照、土壤湿度这些关键参数采上来,超过阈值直接驱动风机、滴灌、补光灯动作,全程不需要你在现场。适合谁?正在做物联网毕业设计的学生、想给自家大棚做低成本改造的种植户,或者刚入门嵌入式想找一个完整闭环参考的开发者。这套源码不是只能看的 demo,它是一套能真正跑起来、能改参数、能扩展节点的大棚环境控制框架。

2. 架构与数据链路:STM32 和 ZigBee 各司其职

2.1 为什么是 STM32 + ZigBee,而不是 WiFi 或蓝牙

动手拆这套源码之前,得先明白它的选型逻辑。很多新手第一反应是:大棚里装 WiFi 不行吗?行,但代价很高。大棚环境的特点是金属骨架多、湿度大、距离远,WiFi 穿两堵墙信号就衰减得厉害,而且大棚区域往往没有稳定的宽带接入。蓝牙就更不用提了,传输距离十几米,一个标准大棚动辄几十米长,根本不现实。

ZigBee 在这套方案里的角色是“低速低功耗的本地通信网”。单个节点的功耗极低,两节五号电池能撑几个月,而且自组网能力很强,某个节点掉线了,数据会自动绕道其他节点传回来。STM32 在这套系统里扮演的是“大脑”,它直接挂载传感器和执行器,同时通过串口连接 ZigBee 模块,把数据往协调器上传。一个协调器可以带几十个终端节点,这意味着你不需要每个大棚都配一套控制器,一个主机管一片区域就够了。

提示:ZigBee 的 2.4G 频段和 WiFi 重叠,但 ZigBee 信道跳频机制能避开部分干扰。实测在一个 WiFi 信号很密的环境里,丢包率依然能控制在 5% 以下。

2.2 数据链路逐级拆解:从传感器读到上位机显示

这套系统的数据流向,我用大白话描述一遍:STM32 读取传感器数据 → 通过串口发给 ZigBee 终端节点 → 无线传输到 ZigBee 协调器 → 协调器通过 USB 转串口连到电脑上位机。反向链路也很重要,上位机下发控制指令 → 协调器 → 终端节点 → STM32 → 驱动继电器。

源码里有个关键文件叫usart.c,它同时处理两路串口:一路用于调试打印,另一路专门用于和 ZigBee 模块通信。这里有个设计细节值得学习:调试串口和通信串口物理隔离,避免调试信息混进无线数据包里,这在定位问题时能节省大量时间。我给这份源码画过一张极简的数据链路图:

传感器(DHT11/光敏/土壤探头) ↓ GPIO/ADC STM32F103 (主控,跑逻辑判断) ↓ USART2 波特率9600 ZigBee终端模块(透传模式) ↓ 2.4G 无线 ZigBee协调器(中心节点) ↓ USB转TTL 上位机软件(实时曲线+报警弹窗)

从源码实现角度看,ZigBee 模块配置在透明传输模式,也就是串口发什么,无线就传什么,对 STM32 来说它就是一个“没有线的串口”。这种设计的巧妙之处在于,你不需要理解 ZigBee 协议栈细节,只需要把它当作一根无限长的串口线。

2.3 源码目录结构与关键文件定位

打开源码包后,首先看 README 和目录结构。这份源码的组织方式很常规,但值得照着学。核心目录大概长这样:

project/ ├── User/ # 主函数、中断入口 ├── Hardware/ # 传感器驱动、继电器驱动 ├── APP/ # 业务逻辑(阈值判断、模式切换) ├── ZigBee/ # 串口协议封装 ├── System/ # 时钟配置、延时函数 └── OBJ/ # 编译产物(hex文件)

Hardware目录下每个传感器一个文件,bsp_dht11.c、bsp_light.c、bsp_soil.c,接口统一命名为xxx_Init()和xxx_ReadData()。我挺喜欢这种命名习惯的,后期如果要换传感器型号,只需要改对应文件内部实现,不用动上层逻辑。APP目录下的control.c是核心,它把多个传感器数据汇总,做阈值判断后决定是否开启电机或水泵。源码包里默认参数是温度上限 35 度、土壤湿度下限 30%,这两个值在control.c顶部宏定义里,后面讲参数修改时会细说。

3. 源码实战拆解:从传感器采集到电机控制

3.1 DHT11 温湿度读取:时序与校验的边界

DHT11 是这套系统里最“亲民”的传感器,也是新手最容易翻车的地方。它的通信协议是单总线,一根数据线既要发指令又要收数据,时序要求微秒级精确。源码里bsp_dht11.c的实现用的是 GPIO 模拟时序,没有依赖定时器捕获,这在 F103 的 72MHz 主频下够用,但对中断优先级要求很高,读数据期间任何中断打断都会导致时序错乱。

核心读取逻辑大概长这样:

uint8_t DHT11_ReadData(DHT11_Data_t *data) { uint8_t buffer[5] = {0, 0, 0, 0, 0}; GPIO_ResetBits(DHT11_PORT, DHT11_PIN); // 主机拉低总线,发起起始信号 Delay_Ms(20); // 至少拉低 18ms GPIO_SetBits(DHT11_PORT, DHT11_PIN); // 释放总线 Delay_Us(30); // 释放后延时 30us if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0) { // 检测响应信号 while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0); // 等待拉高 while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 1); // 等待拉低 for (int i = 0; i < 40; i++) { // 连续读取 40 位数据 while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0); Delay_Us(40); // 40us 后采样判断 0 或 1 if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 1) { buffer[i / 8] |= (0x80 >> (i % 8)); } } } // 校验:buffer[0] + buffer[1] + buffer[2] + buffer[3] 应等于 buffer[4] if ((buffer[0] + buffer[1] + buffer[2] + buffer[3]) == buffer[4]) { >uint16_t ADC_GetAverageValue(uint8_t channel, uint8_t sample_count) { uint32_t sum = 0; for (uint8_t i = 0; i < sample_count; i++) { sum += ADC_ReadSingle(channel); Delay_Ms(10); } return (uint16_t)(sum / sample_count); }

默认采样次数是 10 次,这个值兼顾了实时性和稳定性。如果你发现采集值跳变剧烈,可以改成 30 次,但代价是响应速度变慢,电机启动会更迟钝。ADC 的转换结果和实际物理量之间不是线性关系,源码里预设了一组经验系数:光照强度 = ADC 值 × 0.5,土壤湿度百分比 = 100 - (ADC 值 / 40)。这几个系数是拿常见模块标定出来的,换一个品牌的探头就得重新标定,直接套用会导致湿度显示偏差 10% 以上。

3.3 控制逻辑:阈值判断与继电器动作的联动

control.c是整个系统的“决策大脑”,这套源码写的核心逻辑是典型的三段式:采集→比较→动作。动作执行部分用了一个简单的状态机,避免继电器频繁开关。比如温度超过 35 度就打开风机,降到 32 度才关闭风机,这叫回差控制,目的是防止电机在阈值附近反复启停烧坏继电器。

void Control_Process(void) { // 1. 更新传感器数据 DHT11_ReadData(&dht11_data); light_value = ADC_GetAverageValue(ADC_CHANNEL_LIGHT, 10); soil_value = ADC_GetAverageValue(ADC_CHANNEL_SOIL, 10); // 2. 温度控制:超过上限开风扇,降到下限以下才关 if (dht11_data.temperature_int > TEMP_OPEN_THRESHOLD) { Relay_Control(RELAY_FAN, RELAY_ON); } else if (dht11_data.temperature_int < TEMP_CLOSE_THRESHOLD) { Relay_Control(RELAY_FAN, RELAY_OFF); } // 3. 土壤湿度控制:太干开滴灌,够了就关 if (soil_value < SOIL_OPEN_THRESHOLD) { Relay_Control(RELAY_WATER, RELAY_ON); } else if (soil_value > SOIL_CLOSE_THRESHOLD) { Relay_Control(RELAY_WATER, RELAY_OFF); } }

这段代码最值得学习的点是回差区间:TEMP_OPEN_THRESHOLD和TEMP_CLOSE_THRESHOLD不是同一个值,中间差了 3 度。如果设成同一个值,风扇会在临界点反复抖动,继电器几小时就报废了。这套源码把回差用两个宏分开定义,我觉得比用绝对值判断要直观得多。电机驱动部分用的是继电器模块,STM32 GPIO 输出低电平触发,源码里Relay_Control函数封装了 GPIO 翻转和状态记录,方便上位机查询当前执行器开关状态。

3.4 上位机通信协议:帧格式与解析

无线链路不只是传数据,还要能区分“传感器上报”和“控制指令”。源码里设计了一个极简帧协议:帧头 0xAA + 功能码 + 数据长度 + 数据体 + CRC 校验。这个协议是这套源码里最有移植价值的部分,不管换什么传感器、什么网关,只要帧格式不变,上位机就不用改。

uint8_t Packet_Send(uint8_t func_code, uint8_t *data, uint8_t len) { uint8_t buffer[64]; buffer[0] = 0xAA; // 帧头 buffer[1] = func_code; // 0x01=上报环境数据,0x02=下发控制指令 buffer[2] = len; // 数据体长度 memcpy(&buffer[3], data, len); uint8_t crc = 0; for (int i = 0; i < len + 3; i++) { crc ^= buffer[i]; // 简单异或校验,够用但别用于正式工业场景 } buffer[len + 3] = crc; UART_SendBytes(buffer, len + 4); }

这里的 CRC 用的是单字节异或,工程上如果传输距离远、干扰大,建议换成 CRC16。源码里其他部分不动,只替换校验算法就行。解析端会先找帧头,再按功能码分发数据,这避免了一个很常见的 bug:上位机把上一次的数据残留误当作新数据。我强调一次,这个帧格式设计得虽然简单,但它给出了一个完整的“封包→发送→解析”闭环思维,很多刚学完串口的新手写代码时连帧头都不加,导致数据错位。

4. 换个大棚就要改的六个参数与接线细节

4.1 主控与传感器的引脚映射表

源码包里的stm32f103_gpio.c定义了所有外设的引脚连接。拆包后第一件事,是把你手里的开发板原理图拿出来,对着这个表核对。引脚定义错一位,整个系统就起不来。

外设模块信号线STM32 引脚备注
DHT11DATAPA0开漏模式,需外接 4.7kΩ 上拉
光敏模块AOPA1ADC1 通道1
土壤探头AOPA2ADC1 通道2
继电器1(风机)IN1PB0低电平触发
继电器2(水泵)IN2PB1低电平触发
ZigBee 模块TXPA9USART1 接收
ZigBee 模块RXPA10USART1 发送

这个表里有个细节:DHT11 的 DATA 引脚必须配置为开漏输出并外加上拉电阻,因为 DHT11 是开漏结构,它只能拉低总线,不能主动拉高。如果配置成推挽输出,通信大概率失败。光敏和土壤探头都是模拟输出,所以必须接在 ADC 引脚上,接普通 GPIO 读不到电压。

4.2 阈值宏定义与回差调整

打开control.c顶部,你会看到这样几行:

// 温度控制阈值(单位:摄氏度) #define TEMP_OPEN_THRESHOLD 35 #define TEMP_CLOSE_THRESHOLD 32 // 土壤湿度控制阈值(单位:百分比) #define SOIL_OPEN_THRESHOLD 30 #define SOIL_CLOSE_THRESHOLD 45

这里TEMP_OPEN_THRESHOLD和TEMP_CLOSE_THRESHOLD之间差了 3 度,就是回差区间。改参数时要注意:上限和下限不能相等,至少拉开 2 度,否则会出现在阈值点反复跳变的情况。土壤湿度这块,OPEN是低于 30% 打开滴灌,CLOSE是高于 45% 关闭,这样设置是为了让水充分渗透到根系,而不是表面一湿就停。

4.3 ZigBee 模块的串口配置与组网对码

这套源码的 ZigBee 模块工作在透传模式,出厂默认波特率是 9600。如果模块的波特率不是这个值,就得用 USB 转串口工具配合厂商的配置软件改。源码里 USART2 初始化代码是硬编码的:

/* 初始化 ZigBee 通信串口 */ USART_InitStructure.USART_BaudRate = 9600; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No;

三个关键点:波特率必须和模块配置一致;终端节点必须配置成“透传模式”;协调器和终端必须处于同一个网络 ID(PAN ID)。如果上位机收不到数据,先别怀疑代码,直接用 USB 转 TTL 插到 STM32 的 ZigBee 串口上,用串口助手看有没有数据输出。这一步能隔离出问题出在 STM32 还是无线链路。

4.4 继电器驱动方式与电源隔离

继电器模块是这套系统里最容易出问题的硬件。源码里 PB0、PB1 输出低电平触发,但 STM32 的 GPIO 驱动能力只有几毫安,直接驱动继电器线圈是不可能的。所以需要在 GPIO 和继电器之间加一个 NPN 三极管或 ULN2003 驱动芯片。这里的电源隔离也有学问:继电器的线圈电源和传感器电源最好分开。继电器吸合瞬间会产生很大的电流尖峰,如果和传感器共地,ADC 采样值会在吸合瞬间跳变,看起来就像“土壤湿度突然升高了”。

我一般会这样处理:STM32 的 3.3V 给传感器供电,继电器模块的 VCC 接独立的 5V 电源,两个电源的地线在末端单点相连。即使是同一块开发板上的 VCC 和 5V,也建议在继电器模块的电源引脚上并一个大电容,比如 470uF,用来吸收浪涌电流。源码包的原生设计里可能没有电容,这就属于必加的硬件改动之一。

5. 移植与联调避坑:三个最容易闷头干半天的环节

5.1 现象一:烧录正常但 DHT11 永远返回校验失败

这个坑我估计五个人里能碰到三个。某开发者的板子和源码包一致,线路连了四根线反复检查没问题,但串口打印一直是DHT11 Checksum Error。原因大概率不是源码逻辑,而是 GPIO 模式配置问题。DHT11 的数据引脚在源码里初始化成了推挽输出,但前面说过 DHT11 是开漏结构,需要外接上拉电阻才能正常拉高电平。检查你的板子上的 PA0 引脚附近有没有一个 4.7kΩ 的电阻,如果没有,直接在 PA0 和 3.3V 之间飞线焊一个。这个问题我现在写出来很轻松,当初排查的时候花了一个晚上,把示波器都接上了,最后才发现是硬件设计缺了个电阻。

解决方式也简单:源码里那行GPIO_Mode_Out_PP改成GPIO_Mode_Out_OD,并且确认外接上拉。如果板子上实在没有位置放电阻,程序里也可以不修改,但稳定性会大打折扣,夏天湿度高的时候尤其明显。

5.2 现象二:ZigBee 组网不稳定,数据一会儿有一会儿没

这种“薛定谔的通信”问题,新手最容易怀疑是代码 bug,反复重刷固件。其实根源在 ZigBee 模块的供电。ZigBee 模块对电源噪声很敏感,模块的 TX/RX 在发射瞬间会有约 50mA 的电流脉冲,如果用 STM32 开发板上那个 3.3V 稳压芯片直接供电,电压会被拉低,导致模块不断重启。解决方法是给 ZigBee 模块单独加一个 LDO 稳压芯片,或者至少加一个 100uF 的滤波电容。

提示:检查 ZigBee 模块的供电引脚和 STM32 之间是不是直连的。用手摸模块温度,如果明显发烫,供电电压可能偏高了;如果信号时好时坏,优先加电容而不是换模块。

5.3 现象三:上位机显示的都是 0xFF 或者乱码

串口波特率不对,是最基础但最容易忽略的问题。源码里 ZigBee 用的波特率是 9600,但好多 USB 转 TTL 模块出厂默认是 115200。排查步骤就三步:先把 STM32 的调试串口接出来,用串口助手看有没有正常打印;如果正常,把波特率切到 9600 再看;如果还是乱码,检查两个串口的 GND 是否共地。另外有一种情况是帧协议解析错了,上位机程序里限制了数据体长度,但实际收到的包长度对不上,这就会出现“一帧数据被拆成两条显示”的现象。源码包里帧头是0xAA,拿串口助手的 HEX 模式看原始数据,新到的包应该是AA 01 05 ...这样的格式,如果你看到数据里频繁出现AA AA连续帧头,说明下位机发送逻辑里可能进了重复发送的 bug。

5.4 连线前必做的一步:用万用表量电压再插电

这是我个人的习惯,也是被烧了好几个传感器换来的教训。每次拿到一块新的开发板或者新一批传感器,上电之前先拿万用表量一下引脚电压。特别是土壤探头,有的商家做的模块输出的是 5V 电平,直接接 STM32 的 PA2 引脚会把 ADC 烧掉。这个坑跟源码逻辑没关系,但比任何源码问题都致命。

上电前量两个位置:传感器的 VCC 引脚对地电压,确认是 3.3V 还是 5V;信号输出引脚静态电压,光照模块在光亮环境下应该输出 0~3.3V 之间的值,如果有 5V,说明模块自带电平转换,需要用分压电阻或者确认 STM32 的 ADC 引脚是否容忍 5V 输入。正常流程应该是:不接传感器烧录程序 → 串口助手打印 0xFF 或空值 → 逐个接传感器 → 看数值变化。一次接一个,不要图省事全插上去再找问题。

6. 最后一步:用串口日志逆向验证整套系统的真实行为

整套系统跑起来之后,心里还是不踏实怎么办?我惯用的手法是写一个 20 行的“状态观测函数”,把系统日志全部打到串口 1 上。这不属于源码包自带的功能,但每个合格工程师拿到这套源码后,都会第一时间给自己加这个“后悔药”。所谓后悔药就是,当系统在某个深夜自动打开水泵而你不在现场时,回来的第一件事能查日志还原当时发生了什么。

状态观测函数的核心逻辑就一句话:每次主循环跑完,把当前所有传感器值和控制状态格式化打印出来。这个函数你可以放在control.c里,也可以是独立文件。它不会影响控制逻辑,只在调试时打开,上线后关掉打印就行:

void Debug_PrintStatus(void) { printf("[LOG] Temp=%d.%dC, Humi=%d%%, Light=%d, Soil=%d%%, Fan=%d, Water=%d\r\n", dht11_data.temperature_int, dht11_data.temperature_dec, dht11_data.humidity_int, light_value, soil_value, relay_GetState(RELAY_FAN), relay_GetState(RELAY_WATER)); }

这里的relay_GetState是从源码里Relay_Control函数记录的状态变量里读取的。调试时每两秒打印一行,时间戳是串口助手自动加的。这样当你发现某天晚上土壤湿度数据异常飙升时,回头翻日志看到继电器在对应时间点吸合过,就能判断是传感器误报还是真实环境变化。

日志分析方法也有讲究。不要眯着眼睛盯串口助手,把日志保存成文本文件,用文本编辑器的搜索功能查关键字段,比如查Soil=100出现的频率,如果一小时内出现三次以上,基本可以断定土壤探头电极极化或者接线松动。还有一个验证技巧:人为干扰测试。拿一杯热水靠近温度传感器,看日志里风扇是否在超过 35 度时真的启动;拿一杯水浇到土壤探头附近,看滴灌继电器是否关闭。这套验证流程走完,系统才算真正交接。

从那以后我每次拿到类似的环境监测源码,都会走一遍这个流程——先查日志,再人为制造边界条件,最后才谈自动化控制。比起盯着数据曲线发呆,日志会让你看到每一个决策背后的依据。这套源码的设计让这种验证变得很顺手,串口帧格式和状态记录都留了接口。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询