简介:本资源是一套完整的基于STM32F103C8T6的智能农业大棚毕业设计实现方案,面向电子信息、自动化及物联网方向本科生,解决智慧农业中环境感知与设备协同控制的核心实践问题。压缩包含225个文件,以47个C源文件和51个H头文件构成主程序逻辑(涵盖传感器驱动、PID调节、WiFi通信与OneNET协议栈),辅以31个编译中间文件(.o/.d)和Keil工程配置(.uvprojx/.uvoptx),另含原理图(.schdoc)、说明文档(.pdf/.md)及一键清理脚本(keilkilll.bat),整体大小6.26MB。已有185人学习下载,适合课程设计、毕设开发与嵌入式综合实训。读者可直接部署运行:完整实现DHT11/BH1750/CCS811/土壤模块多传感器融合采集,支持自动调控风扇、水泵、LED灯及蜂鸣器报警,并通过1.8寸TFT屏本地显示+ESP8266上传OneNET云端,工程结构清晰、模块解耦合理,具备良好可扩展性与调试基础。 做毕业设计的人,十个里有八个会碰到“智能xx系统”这种题目,而其中又有一半以上是拿STM32做主控。我见过太多学弟学妹拿到一个“基于STM32单片机智能大棚系统源码+原理图.zip”压缩包,解压以后对着一堆.c文件和原理图发懵,不知道从哪看起,也不知道答辩的时候老师会问什么。这篇文章就按我平时带人的节奏,把这个项目的来龙去脉拆开揉碎讲清楚,顺便把源码和原理图里最容易踩的坑都标出来。不管你是照着改、还是想真正理解之后自己重写一遍,都能省下不少时间。
1. 项目概述与需求拆解
1.1 题目到底想让你做什么
智能大棚系统,乍一看好像是个农业项目,实际上它就是一个典型的嵌入式数据采集与自动控制系统。放在理工科毕设里,核心考察点就三个:传感器数据能不能采上来、执行机构能不能按逻辑动作、系统能不能稳定长时间跑。至于大棚本身是种蔬菜还是种花,不重要,那是包装层面的东西。
拆开需求,至少包含这几块:
- 环境参数采集:空气温度和湿度、土壤湿度、光照强度,这几样是最基本的。
- 自动控制逻辑:根据采集到的数据,决定是否开启灌溉水泵、通风风扇、补光灯,甚至卷帘电机。
- 人机交互:一个显示屏,能看实时数据;几个按键,能修改阈值;报警设备,超限了能响。
- 数据通信(进阶项):有些题目要求把数据传到手机或电脑,这个一般会用ESP8266走Wi-Fi,或者用蓝牙模块。
如果你的题目没有明确要求联网,我建议先别碰无线通信。把本地自动控制做扎实,稳定性做出来,再谈扩展。很多毕设甚至因为加了ESP8266导致系统整体变慢、重启频繁,反而得不偿失。
1.2 从整体架构看项目的真实复杂度
拿到压缩包以后,第一步不是看代码,而是先看目录结构和原理图。一个规范的毕设工程,应该是这样的:
Project/ ├── User/ │ ├── main.c │ ├── stm32f10x_it.c │ └── ... ├── HARDWARE/ │ ├── DHT11/ │ ├── OLED/ │ ├── ADC/ │ └── Relay/ ├── SYSTEM/ │ ├── delay/ │ ├── sys/ │ └── usart/ ├── 原理图/ │ ├── STM32最小系统.pdf │ ├── 传感器采集原理图.pdf │ └── 电源与驱动原理图.pdf └── README.txt如果HARDWARE目录下每个外设对应一个文件夹,说明这是标准的模块化写法,代码结构比较清晰,读懂主函数就能串起整个逻辑。
系统整体数据流大概是这样的:传感器先把物理量变成电信号,ADC等外设把模拟量变成数字量,STM32读取数字量以后做滤波和阈值比较,然后根据比较结果控制GPIO口输出高低电平,GPIO驱动继电器或MOS管,继电器再控制水泵、风扇等强电设备。整个过程是一个典型的闭环控制:采集 → 处理 → 决策 → 执行 → 再采集。
从系统框图来看,项目本质上不复杂。它不属于算法类项目,也不属于多任务系统,就是一个裸机运行的“感知-决策-执行”模式。看代码的时候,抓住一条主线:主循环里先采样数据,再处理数据,最后执行控制,别被中断函数和乱七八糟的库文件带偏。
2. 硬件方案与原理图核心解读
2.1 主控选型与最小系统
这个项目里用的主控,最常见的是STM32F103C8T6,也就是大家常说的“蓝色魔法棒”核心板。C8T6属于F1系列性价比最高的型号之一,72MHz主频,64KB Flash,20KB SRAM,对于这种数据采集型项目来说绰绰有余。而且它的引脚兼容性好,不管你是用最小系统板还是自己画PCB,都很方便。
原理图里如果画了完整的最小系统,通常是这个结构:
- 供电电路:3.3V从AMS1117稳压芯片输出,输入可以是5V或者USB供电。
- 晶振电路:8MHz主晶振加上两个20pF左右的负载电容,另外32.768kHz的RTC晶振看需求,没有也不影响。
- 复位电路:10kΩ上拉电阻加一个0.1μF电容,典型RC复位,保证上电时NRST引脚出现短暂低电平。
- 启动引脚:BOOT0和BOOT1都要拉低,确保从主Flash启动。
- 去耦电容:每个VDD引脚附近放一个100nF,这是新手画PCB最容易漏的地方,漏了以后程序容易出现莫名其妙的现象。
看原理图的时候,有个小技巧:先找电源网络,确认VCC和GND是怎么连接的。所有模块都要供电,电源连错了,后面全是白搭。再找复位和晶振,这两个地方如果画错了,芯片压根跑不起来。
2.2 传感器选型与接口设计
智能大棚系统里,传感器选型是有讲究的,每个传感器的接口方式不一样,驱动难度也不同:
| 传感器 | 测量对象 | 接口类型 | 典型型号 | 驱动难度 |
|---|---|---|---|---|
| 温湿度传感器 | 空气温度/湿度 | 单总线 | DHT11 | 中等,时序严格 |
| 光照强度传感器 | 光照 | I2C | BH1750 | 简单,I2C字节级时序 |
| 土壤湿度传感器 | 土壤含水率 | 模拟量 | LM393模块 | 简单,ADC采集 |
| 空气质量传感器 | 有害气体 | 模拟量/PWM | MQ135 | 简单,但需要预热 |
DHT11是最常用的温湿度传感器,但它有坑。它用的是单总线协议,数据线既当输入又当输出,靠严格的时序来区分0和1,时序偏差几微秒都会导致读出来的数据全是乱码。原理图里DHT11的数据引脚一般会接一个4.7kΩ上拉电阻,这个电阻必须存在,不接的话通信很容易失败。另外DHT11的采样周期是1秒,也就是说两次读取之间至少要隔1秒,不然它会不响应。很多新手第一次驱DHT11失败,就是因为读得太频繁。
光照传感器分为两种:光敏电阻和数字光照传感器。如果你看到原理图里用的是光敏电阻,它通常是和一个10kΩ电阻分压,然后从中间抽头接ADC引脚。这种方式简单,但线性度差,同一光照条件下换一个光敏电阻,读数可能差出一倍。如果用的是BH1750,走I2C接口,ADC资源就省出来了,软件逻辑也稳定。毕设阶段用光敏电阻是可以接受的,但代码里要加入归一化处理,把ADC原始值按照实测范围映射成0%到100%的“光照度”。如果希望更稳定,建议直接换数字的。
土壤湿度传感器模块上,LM393比较器会输出两个信号:AO是模拟输出,直接接STM32的ADC引脚;DO是数字输出,接在普通的GPIO口上,可以通过电位器调节阈值。做毕设的时候,AO接ADC采集是常规做法,因为能读到连续变化的值,方便做比例控制。
2.3 执行机构与驱动电路原理
执行机构通常是继电器模块,继电器再去控制水泵、风扇这类强电设备。这里的核心问题是:STM32的GPIO口输出电流太小,驱动不了继电器线圈。
原理图里常见的驱动方案有三种:
- 三极管驱动:GPIO通过1kΩ基极电阻控制NPN三极管(如S8050),三极管集电极接继电器线圈,线圈并联一个二极管续流。这是最基础也是最有代表性的驱动电路。
- ULN2003达林顿管驱动:一个芯片集成7路达林顿管,专门用来驱动继电器、步进电机这类负载。优点是集成度高,缺点是低电平导通,逻辑上要反向。
- 光耦隔离继电器模块:淘宝上最常见的“继电器模块”,板载光耦和驱动三极管,你只需要用GPIO输出高或低电平控制,模块自己完成隔离和驱动。原理图里一般会把光耦、LED指示灯、续流二极管都画出来。
看原理图的时候,重点关注继电器线圈两端有没有并联续流二极管。线圈是一个感性元件,断电瞬间会产生反向电动势,如果没有二极管泄放,这个高压尖峰轻则干扰单片机,重则击穿驱动管。如果你的原理图里没有这个二极管,那这个设计就是不合格的。
关于电平匹配,还有一种情况需要注意:继电器模块如果用5V供电,而STM32的GPIO输出是3.3V,有些继电器模块的逻辑阈值偏高,3.3V可能驱动不了。这时候要么选低电平触发的继电器模块,要么用5V供电的光耦模块把信号隔离之后再由模块内部三极管放大,要么用MOS管模块,它的栅极阈值通常更低,3.3V驱动很稳。
3. 软件架构与核心代码实现
3.1 开发环境与工程代码组织方式
这个项目用的开发环境不外乎三个:Keil MDK、STM32CubeIDE、IAR。其中Keil MDK在高校里用的最多,网上教程也最多。代码部分如果是老式写法,用的是标准外设库(Standard Peripheral Library);如果是新式写法,用的是HAL库(Hardware Abstraction Layer)。两种库各有优缺点,但毕设阶段,标准库其实更直观——它直接操作寄存器,代码逻辑一眼能看到底。HAL库封裝层次多,新手容易迷失在结构体指针的层层调用里。
不管用什么库,工程代码的核心结构都是一样的:
int main(void) { // 初始化外设:GPIO、ADC、定时器、串口、I2C OLED_Init(); DHT11_Init(); ADC_Init(); Relay_Init(); Key_Init(); // 显示初始页面 OLED_Clear(); OLED_ShowString(0, 0, "Smart Greenhouse"); while(1) { // 1. 采集数据 DHT11_Read_Data(&temperature, &humidity); light_value = ADC_Read(ADC_CHANNEL); soil_value = ADC_Read(ADC_CHANNEL); // 2. 数据滤波与显示 OLED_ShowData(temperature, humidity, light_value, soil_value); // 3. 阈值比较与自动控制 AutoControl(temperature, humidity, light_value, soil_value); delay_ms(1000); } }标准的裸机架构,就是在一个大循环里反复执行采集、显示、控制三件事。这种方式简单可靠,对于这种低速控制场景完全够用。定时器中断一般用于按键消抖、数码管扫描、串口接收这类对时序要求高的任务,不要用中断做传感器读取,尤其是DHT11这种时序敏感的传感器,放中断里极容易把时序打乱。
3.2 DHT11时序驱动深入拆解
DHT11的驱动是这个小项目里最有“含金量”的部分,也是最容易出bug的地方。它的数据线是单总线,通信过程可以分成三个阶段:主机发起请求、传感器响应、传感器发送40位数据。
主机发起请求的时序:
DHT11_Data_GPIO_OUT(); // 数据线配置为输出模式 DHT11_Data_LOW(); // 主机拉低 delay_us(20000); // 至少拉低18ms,通常用20ms DHT11_Data_HIGH(); // 主机拉高 delay_us(30); // 拉高20-40us DHT11_Data_GPIO_IN(); // 数据线配置为输入模式,等待响应这里有几个细节要注意。拉低时间必须在18ms以上,如果低于18ms,传感器可能不响应;但拉高时间要控制在20-40us之间,太长了会被传感器误判。20ms的拉低延时,标准库可以用SysTick实现,也可以写一个简单的软件延时。
传感器响应之后,它会给主机一个80us的低电平和80us的高电平,这个高电平也被叫做“准备信号”。然后才开始发送数据。
每一位数据的读取方式是一样的:先有一个50us的低电平,然后是一个高电平,高电平持续的时间决定了这一位是0还是1。高电平持续26-28us是0,持续70us是1。读取一个bit的代码逻辑是:
// 等待低电平结束 while (DHT11_Data_READ() == 0); // 然后延时40us delay_us(40); // 如果40us后数据线仍为高,则这一位是1,否则是0 if (DHT11_Data_READ() == 1) { data = (data << 1) | 1; } else { data = (data << 1) | 0; }这个方法利用的是0和1在40us处的电平差异,简单高效。但是有个前提:delay_us函数的延时必须准确。如果系统时钟配置不对,或者编译器优化等级太高导致软件延时被优化掉,这个读取方式就会全线崩溃。
DHT11发完40位数据以后,数据线会释放,由上拉电阻保持高电平。40位数据的结构是:8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。校验和等于前四个字节相加的低8位,代码里要检查,校验失败就丢弃本次数据,不要用错误数据去控制设备。
3.3 ADC多通道采集与数据滤波
ADC部分涉及两个技术点:多通道轮询采样和软件滤波。STM32的ADC只有一个转换结果寄存器,但可以通过配置多个通道,在通道之间轮流切换。
用标准库时,多通道扫描加DMA是比较理想的方案。原理是配置ADC工作在扫描模式和连续转换模式,每次转换完自动触发DMA搬运数据到内存数组里。CPU不需要参与每次转换,直接从数组里取最新值就行。配置代码的核心部分是:
DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; // DMA通道初始化,搬运ADC转换结果到adc_value数组 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_value; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 2; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // ... 其余结构体赋值 DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); // ADC1初始化,开启扫描模式 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_Init(ADC1, &ADC_InitStructure);但如果你只是想简单读取光照和土壤湿度这两个模拟量,也可以不用DMA,直接在循环里逐通道读取,在通道之间切换前先延时一小段时间,结果也不会差太多。DMA是性能优化手段,不是功能必需。毕设项目性能余量很大,纯轮询也没问题。
数据滤波是另一个关键点。ADC原始值因为电磁干扰和传感器噪声,跳变会很大。最简单的滤波是“多次采样取平均”,比如连续采10次,去掉最大值和最小值,剩下的取平均。这个中值平均滤波算法,实现简单,效果不错,推荐在所有ADC通道上都做一遍。
unsigned int get_adc_average(ADC_TypeDef* ADCx, uint8_t channel) { unsigned int sum = 0; unsigned char i; for (i = 0; i < 10; i++) { // 切换通道,启动转换,等待结束,读取结果 sum += get_adc_once(ADCx, channel); delay_ms(5); } return sum / 10; }3.4 控制策略与自动控制逻辑设计
自动控制是整个系统的“大脑”,决定了大棚是“看起来智能”还是“真的智能”。最基础的控制逻辑是阈值比较:
- 土壤湿度低于阈值下限,开启水泵;
- 土壤湿度高于阈值上限,关闭水泵;
- 空气温度高于阈值上限,开启风扇;
- 光照强度低于阈值下限,开启补光灯。
这里最常用的是滞回比较,也叫“迟滞比较”。简单说就是开启阈值和关闭阈值不一样,防止设备在阈值附近频繁启停。
举个具体例子:土壤湿度阈值设定为30%到60%。湿度低于30%时启动水泵,高于60%时停止水泵。30%到60%之间的区间,水泵保持上一次的状态。这样就不会出现湿度在29.9%和30.1%之间来回跳、水泵一秒钟开关一次的情况。这种写法在工程上非常常见,比单纯的“大于开、小于关”要可靠得多。
代码实现上,用一个结构体封装控制状态:
typedef struct { unsigned char soil_humidity_low; unsigned char soil_humidity_high; unsigned char temperature_high; unsigned char light_low; } Threshold_TypeDef; void AutoControl(void) { // 土壤湿度控制水泵 if (soil_value < threshold.soil_humidity_low) { Relay_Set(RELAY_PUMP, ON); } else if (soil_value > threshold.soil_humidity_high) { Relay_Set(RELAY_PUMP, OFF); } // 温度控制风扇 if (temperature > threshold.temperature_high) { Relay_Set(RELAY_FAN, ON); } else if (temperature < threshold.temperature_high - 2) { Relay_Set(RELAY_FAN, OFF); } }这种基于阈值的自动控制是毕设的标准答案,也是答辩时老师最容易追问的地方。你需要能说清楚:为什么用滞回比较?滞回区间设多大?如果传感器数据跳变,怎么防止误动作?这三个问题回答好了,控制的分数就稳了。
4. 调试记录与常见问题排查
4.1 传感器读数异常的排查思路
做这种项目,传感器读数不对是百分之百会遇到的,不用怀疑。最常见的几个现象和原因:
现象一:DHT11读出来的数据一直是0,或者永远是同一个值。
这种问题几乎都出在时序上。排查顺序是:第一,数据线上有没有接上拉电阻,4.7kΩ左右;第二,GPIO模式切换是否正常,读之前切输入、写之前切输出;第三,延时函数是否准确,实测一下20us和40us延时是不是符合预期;第四,读取间隔是否大于1秒,DHT11采样周期决定了你不能读太快。
我遇到过一种特殊情况:编译器开-O2优化之后,软件延时函数被优化掉了。解决方案是给延时变量加volatile修饰,或者在函数里加一个空循环体防止内联优化。
现象二:ADC值满量程或者恒为0。
满量程最常见的原因是引脚悬空,ADC读到一个不确定的高电平。检查传感器模块是否接了电源,信号线是否正确接到ADC输入引脚。恒为0的常见原因,是传感器信号线接到了被复用为其他功能的引脚,或者ADC通道配置错误,比如把通道1接到引脚A0上,结果代码里配置的是通道2。先查原理图,再查代码,这个顺序不能反。
现象三:土壤湿度传感器值在空气中很高,插入土壤后变化不大。
土壤湿度传感器测的是两块探针之间的导电性,土壤越湿导电性越好,模拟量输出的电压越低或者越高(取决于模块设计)。如果插入土壤后变化不明显,先把探针插深一点,完全接触土壤;如果还不行,用万能表量传感器模块上的电位器,调整一下比较阈值。需要注意的是,有些传感器模块长期插在土壤里,探针会被腐蚀,影响测量结果,所以实测数据之前建议先校准。
4.2 继电器控制不稳定
继电器控制是强电部分,容易出逻辑和硬件两方面的问题。最典型的故障是继电器吸合一下又断开,频率很快,像在“发抖”。原因通常是:
- 控制逻辑里没有滞回比较,阈值附近反复开关。
- 单片机复位了。继电器吸合的瞬间电流很大,电源电压瞬间跌落,导致STM32低于复位电压,系统重启。
- 继电器模块和单片机共用电源,功率不够。
最后一种情况最恶心,也最难排查。解决办法是给继电器模块单独供电,或者至少保证电源的5V输出电流足够大,用一个12V/2A以上的适配器供电,多做几个滤波电容。如果电路板是自己画的,还要注意继电器线圈的走线尽量粗一点,远离模拟信号线。
4.3 程序卡死与硬件复位问题
程序跑着跑着突然不动了,或者反复重启,这种问题在毕设项目里也不少见。原因排查的优先级如下:
第一,看供电。用示波器或者万用表量一下VDD引脚电压,如果有明显跌落,就是电源问题。电脑USB口供电稳定性一般,如果接了多个模块,建议直接换5V/1A以上的电源适配器。
第二,看中断。检查是不是有中断服务函数里写了死循环,或者中断里调用了延时函数。中断服务函数应该保持精简,只做标志位设置和数据搬运。
第三,看内存是否溢出。如果代码里大量使用局部数组,又没有注意栈空间大小,程序会因为栈溢出而进入HardFault_Handler。排查的时候,给main函数里的局部变量数组改成全局变量,或者减小数组大小,看看问题是否消失。
第四,看是否触发了看门狗。有的工程里开启了独立看门狗(IWDG),主循环跑得太慢,超过喂狗阈值,系统就会不断复位。解决办法是延长喂狗时间,或者把喂狗操作放在定时器中断里。
4.4 原理图常见错误速查表
以下是我审阅毕设原理图时最常见的几个问题,每个都能导致板子完全不能工作,直接对照检查:
| 问题现象 | 原因 | 解决办法 |
|---|---|---|
| 芯片不启动 | 复位电路缺少上拉电阻 | 在NRST引脚接10kΩ上拉到3.3V |
| 芯片发热 | 电源极性接反或短路 | 检查电源部分二极管方向和电容极性 |
| 程序下载失败 | BOOT0/BOOT1悬空 | 两个引脚必须通过电阻拉低 |
| DHT11读不到数据 | 数据线缺少上拉电阻 | 加4.7kΩ上拉到3.3V |
| 继电器乱跳 | 续流二极管缺失 | 线圈两端并联1N4148或1N4007 |
| ADC读数漂移严重 | 模拟参考电压不稳 | 在VREF+引脚多加100nF电容 |
| 屏幕不亮 | I2C地址错误或上拉缺失 | 确认地址0x3C/0x3D,SDA/SCL接4.7kΩ上拉 |
5. 经验总结与进阶建议
5.1 拿到压缩包后,正确的学习路径
如果你是从网上下载的或者是学长学姐给的源码,不要急着烧程序。先把文档和原理图通读一遍,搞清楚哪些引脚接了什么外设,然后打开代码的主函数,从初始化开始逐行读下去。每看到一个外设函数,就回原理图里找到对应的引脚,建立“代码变量 ↔ 硬件引脚 ↔ 物理外设”三者之间的映射关系。
这种“三点对应”的学习方法很有效。比如看到Relay_Set(RELAY_PUMP, ON)这一行,你要能立刻想到:RELAY_PUMP对应PB0,PB0通过一个三极管接到了继电器线圈,继电器触点控制水泵电机。如果你能做到这一点,说明这个项目的硬件和软件你已经吃透了。到时候答辩,老师随便怎么问,你都稳。
改代码的时候,先改宏定义和配置参数,比如阈值、延时时间、引脚号,不要一上来就大段重写。最小改动原则在毕设阶段是保护你的最好策略。改记录也建议写下来,方便复盘。
5.2 从毕设到工程化,还有什么可以升级
如果你不满足于只是“过答辩”,想把项目做得更完整,可以在以下几个方向上升级:
第一,从裸机轮询升级为状态机或FreeRTOS。大棚控制这种低速场景用裸机没问题,但如果以后想往嵌入式开发方向发展,学一下RTOS的任务调度思想,对找工作是加分的。
第二,加上本地数据存储。用AT24C02或者W25Q64保存历史数据,断电不丢失。这样即使不联网,也能积累几天的环境变化记录。
第三,实现无线通信和上位机。用ESP8266将数据上报到云平台,再用手机App远程查看和控制。这一块扩展性强,工作量也大,但做出来以后项目的完整度会明显提升。
第四,PCB画板。把面包板上的杜邦线方案升级成一块完整的PCB,把最小系统、传感器接口、继电器驱动、电源管理都集成在一张板子上。这个经验的含金量很高,而且后面做产品原型也会用得上。
在我个人看来,做这个项目最难受的从来不是代码本身,而是“不知道系统性问题出在哪”。比如电源不稳导致复位、时序不对导致传感器失效、硬件上没有上拉导致读数异常,这些问题要么折磨你几天,要么在你实际跑代码之前就已经埋下了雷。所以拿到一个毕设项目之后,核心思路永远是:先读懂原理图,再分析代码,最后才是动手改。这个顺序走对了,整个项目就能顺畅地做下来;走反了,你就会陷入“改了代码还是不行”的无限循环里。项目本身确实是小项目,但把它真正吃透,你收获的绝对值回票价。
本文还有配套的精品资源,点击获取