简介:STM32自动浇花系统是一份完整项目资料,面向嵌入式初学者、电子爱好者和需要课程设计参考的学生,解决如何根据土壤温湿度、环境光照和空气温湿度自动控制水泵浇水的问题。包内共有535个文件,压缩后约27.77MB,以C/H源码、Keil工程文件、图片、PDF/DOCX说明文档为主,还包含HEX固件及PCB设计相关文件;C/H文件用于主控逻辑,PDF/DOCX介绍设计与使用说明,PNG图片可查看接线与实物效果,便于对照电路、程序和文档进行学习。目前已有3773人学习下载。该项目以STM32F103C8T6为核心,完整展示了土壤温湿度、光敏、空气温湿度传感器采集、OLED显示、阈值触发浇水、DC-DC供电及继电器控制等环节,既适合通过源码理解模块化嵌入式编程,也可照文档复现整机,作为毕业设计、电子竞赛或智能家居小项目的参考模板。 先说个背景:家里养了几盆绿植,前阵子夏天连续出差,回来后发现阳台那盆薄荷已经干到叶子发脆了。淘宝上那种几十块的自动浇水器我也试过,要么是定时傻浇,要么是湿度探针泡几天就不准。后来干脆自己动手做了一套基于STM32的自动浇花系统,从土壤湿度检测、水泵控制到状态切换全部自己写代码,目前已经稳定跑了两个多月,出门再久也不用担心花被干死。这篇文章就把整个项目从方案选型、硬件连接、代码逻辑到调试踩坑完整记录下来。这套东西非常适合刚学完STM32基础想拿一个完整项目练手的同学,也适合直接改造成基于STM32的毕业设计,或者自建一个阳台种植的自动灌溉小系统。
1. 系统整体设计与思路拆解
1.1 为什么选STM32而不是Arduino或ESP8266
做自动浇花,很多人第一反应是Arduino或者ESP8266,毕竟网上现成代码一大把。我当初也纠结过,但最后选了STM32F103C8T6这块最经典的单片机,原因很现实。
首先是控制精度和扩展余量。Arduino Uno的ADC是10位,ESP8266的ADC不仅只有10位,而且输入电压范围只有0到1V,要加电阻分压才能测传感器,麻烦还容易不准。STM32F103的ADC是12位,直接量程0到3.3V,土壤湿度传感器输出的模拟量接上去就是硬件级匹配,采样分辨率4096级,完全够用。其次是外设接口丰富,后面如果想加OLED显示、加RTC定时、加ESP8266联网上报,F103的I2C、SPI、USART都够分配,不会像Arduino一样一个项目用着用着发现引脚不够了。
成本也是一个考虑。一块STM32F103C8T6最小系统板现在也就是十来块钱,跟一块Arduino Nano价格差不多,但性能强了不止一个档次。对于做毕业设计或者想深入学习嵌入式开发的人来说,用STM32完成这个项目,既有一整套完整的嵌入式开发流程(时钟配置、外设初始化、中断管理、状态机设计),又不会像用Arduino那样把底层全封装完了,最后什么都学不到。
1.2 控制策略:湿度阈值加迟滞区间
自动浇花的核心问题不是“怎么浇水”,而是“什么时候浇、浇多久”。网上很多简单方案就是定时器,每天早上浇30秒,完全不看土壤实际情况。这种方案的缺点是显而易见的:阴天下雨土壤还是湿的它也浇,大太阳暴晒一天土早干了它也只能等到第二天早上。所以我的方案改成用土壤湿度传感器实时检测含水率,用“低阈值启动、高阈值停止”的迟滞区间来控制水泵。
为什么不直接用一个阈值?因为那样会出现临界点附近频繁启停的问题。比如设定土壤干了就浇,浇到刚好达到目标值就停,那么传感器的读数会在这个值附近来回波动,水泵就会不断开机关机,继电器频繁吸合,用不了几天就烧了。迟滞控制的思路很简单:设两个阈值,湿度的下限值比如2800(传感器读数,数值越大表示土壤越干),低于这个值就启动浇水;当湿度上升到上限值比如2200(表示足够湿润),就停止浇水。中间这个600的间隔就是迟滞区,在这个区间内水泵维持现状,不会因为噪声波动而反复切换。
我还额外加了两个限制条件:单次浇水最长持续60秒,超过直接强制停止,防止探头故障时水泵一直抽水把花盆淹了;浇水结束后进入一个短暂的冷却状态,等传感器读数稳定后再进入下一个检测循环。这套控制逻辑用状态机实现,逻辑清晰,可读性也好,后面如果要加定时模式或者手机远程控制,只需要在状态机里加状态就行。
2. 硬件选型与电路连接细节
2.1 核心器件清单与选型理由
整个系统用到的硬件不多,但每一件我都踩过不合适的坑,这里直接给出我现在在用的方案。
| 器件 | 型号/规格 | 干什么用 | 选型理由 |
|---|---|---|---|
| 主控 | STM32F103C8T6最小系统板 | 核心控制 | 性价比高、资料多、外设够用 |
| 土壤湿度传感器 | 电容式土壤湿度传感器模块 | 监测土壤含水量 | 输出0-3.3V模拟量,寿命比电阻式长很多 |
| 水泵 | 3-6V微型潜水水泵 | 把水箱里的水抽到花盆 | 功耗低,12V版需要额外的电源 |
| 继电器模块 | 5V单路低电平触发继电器 | 控制水泵通断 | 实现功率隔离,主控不直接驱动水泵 |
| 电源 | 5V/2A USB电源适配器 | 给主控和传感器供电 | 稳定、通用,手机充电头就能用 |
| 显示(可选) | OLED 0.96寸 I2C | 显示湿度和工作状态 | 方便调试,不接也能跑 |
重点说下土壤湿度传感器的选型。市面上最常见的是那种两插针的电阻式探头,利用土壤导电率来测湿度,二三十块钱好几个,但我劝你别用。一个是铜探针在土壤和水分作用下会电解腐蚀,用不了几个星期读数就开始漂移,而且土壤里通直流电本身就会加速金属离子析出,时间长了反而伤害植物。电容式探头表面有绝缘涂层,不直接接触土壤导电体,既不腐蚀也能长期稳定工作,价格贵一点但物有所值。
水泵和继电器的搭配也要注意。微型水泵的启动电流比正常工作电流大不少,不能用单片机GPIO直接驱动(GPIO最大也就输出20mA左右),所以必须通过继电器模块做中间切换。继电器模块本身带三极管驱动电路,输入侧一个GPIO信号就能控制,输出侧是独立干接点,水泵的电源从外接电源走,这样主控电路和水泵功率电路就隔离开了,安全性和稳定性都有保障。
2.2 接线方式与电路设计保护细节
接线图这里就不画了,我直接把每一根线说清楚,照着接就行。
STM32F103C8T6的PA1作为ADC输入引脚接电容式土壤湿度传感器的AO模拟输出口,传感器VCC接3.3V,GND接GND。注意传感器的模拟输出端不用接任何电阻,直接进PA1,因为STM32的ADC输入阻抗足够高。水泵控制线接PB0(我用的是低电平触发继电器模块,注意不同模块触发逻辑不一样,有的高电平触发,这个一定要看模块丝印),继电器模块的JD-VCC和VCC、GND引脚接法按照模块说明书来,信号输入IN接PB0。
电源部分需要稍微用心。整个系统我采用5V单电源方案:5V的USB电源进来之后,一路直接给继电器模块供电(继电器线圈是5V的),另一路经过AMS1117-3.3稳压降到3.3V给STM32和传感器供电。水泵的电源我从5V电源适配器的输出端直接分一路接继电器输出侧的公共端COM,继电器的常开端NO接水泵正极,水泵负极接5V电源负极。
这里有个非常容易踩的坑:不要把水泵的地和单片机的GND混成一条回路。水泵启动瞬间电流大,会在共用GND上产生压降波动,直接干扰ADC读数。我的做法是两组GND在电源输入端汇合,但传感器、单片机那一路GND单独走一条线,水泵走另一条线,这样能明显减小ADC采样的噪声。
还有一个硬件保护点:继电器线圈在断电瞬间会产生反向感应电动势,模块上一般已经焊了续流二极管,如果买到的模块没有,自己一定要在继电器线圈两端反向并联一个1N4007二极管(负极接电源正极),否则容易打坏MCU引脚。另外所有接线端子都建议做成可插拔的,不要直接焊死,这样后期换传感器、拆模块方便很多。
3. 软件实现:环境搭建与核心代码解析
3.1 开发环境准备:CubMX加HAL库搭工程
软件部分我选择了ST官方主推的HAL库,用STM32CubeMX生成初始化代码,再在Keil MDK里写应用逻辑。之所以不用标准库,是因为标准库虽然启动快、代码执行效率稍高,但外设初始化的代码写起来繁琐,而且ST已经宣布新芯片逐步转向HAL库开发,现在新学STM32直接上手HAL库,以后做其他项目也能无缝衔接。
环境搭建有几个容易出问题的地方,我简单说下我的操作顺序。先装Keil MDK 5,再去Keil官网的Pack Installer里装STMicroelectronics STM32F1系列芯片包。如果之前装过Keil4或者C51版本,安装的时候注意选默认路径,不要混装到同一个目录,否则会有头文件路径冲突的诡异问题。STM32CubeMX生成代码的时候,芯片型号选STM32F103C8Tx,SYS里的Debug选项选Serial Wire,这个很重要——默认是No Debug,不改成Serial Wire的话即使程序能跑,下次用ST-Link下载就会报错。
我在CubeMX里的配置大概是这样的:PA1设置为ADC1通道1、采样时间拉到最大(810.5 Cycles),因为土壤湿度传感器的输出阻抗偏高,采样时间太短会采不准;PB0设置为GPIO输出,初始电平设为高(对应继电器低电平触发,默认不吸合状态);系统时钟用HSE外部晶振,主频拉到72MHz。如果你使用的是默认内部RC振荡器,延时函数的时间精度会差不少,后面程序里要用HAL_GetTick做超时判断,所以最好直接上外部晶振。
3.2 湿度采集:ADC加DMA多次采样滑动平均
土壤湿度传感器输出的模拟电压本身是连续的,但ADC转换结果会有波动。尤其是水泵启动和继电器切换的瞬间,电源波动会在读数上叠加周期性噪声。解决思路是多次采样取平均,靠算法把噪声滤掉。
我用的方案是ADC单通道加DMA循环采集,DMA存满10个样本后触发中断,在主循环里做滑动平均。下面这段就是核心代码,基于CubeMX生成的HAL库基础上改的。
// 全局变量定义 uint16_t adc_buf[10] = {0}; // DMA存放ADC数据缓冲区 volatile uint8_t adc_data_ready = 0; // 数据采集完成标志 uint32_t humidity_raw = 0; // 滤波后的湿度原始值 // 主函数初始化部分,启动ADC DMA void humidity_init(void) { // adc1和dma的句柄由CubeMX生成 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 10); } // DMA传输完成回调函数,HAL库在ADC一轮采集完成后自动调用 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_data_ready = 1; } } // 在主循环里调用,拿到稳定的湿度读数 uint32_t humidity_get_average(void) { uint32_t sum = 0; uint8_t i; if (adc_data_ready) { for (i = 0; i < 10; i++) { sum += adc_buf[i]; } humidity_raw = sum / 10; adc_data_ready = 0; // 重新启动DMA,保证下一轮采集无缝衔接 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 10); } return humidity_raw; }这里有个细节很多人容易忽略:HAL_ADC_Start_DMA在第一次调用之后,DMA是连续循环采样的,不需要每次转换都重新启动;但如果你的CubeMX配置里DMA模式不是Circular而是Normal,那每采集完一轮就停了,必须在回调里重新启动。为了稳妥起见,我在回调里每次都重新调用了一次,实测没有任何副作用。
3.3 自动浇水状态机与按键手动模式
自动浇花的控制逻辑不能用一堆if嵌套写,否则状态一多就乱成一团。我用了经典的状态机思路:系统只有三个状态,空闲、浇水、冷却,每个状态只处理自己的事,状态之间的切换条件很明确。顺便加了一个按键,长按1秒可以在自动模式和强制浇水模式之间切换,这个在调试和测试水泵好坏的时候非常有用。
// 状态机定义 typedef enum { STATE_IDLE = 0, // 空闲状态,自动检测湿度 STATE_WATERING, // 浇水状态,水泵开启 STATE_COOLDOWN // 冷却状态,等传感器稳定 } sys_state_t; sys_state_t state = STATE_IDLE; uint32_t state_tick = 0; // 记录进入当前状态的时间点 uint32_t pump_work_time = 0; // 记录单次浇水已经持续的时间 // 两个湿度阈值,具体数值要根据传感器标定值调整 uint32_t humid_dry = 2800; // 空气湿度读数低到2800,土壤已干 uint32_t humid_wet = 2200; // 浇水后湿度上升到2200以下,说明足够湿 void watering_task(void) { uint32_t cur_tick = HAL_GetTick(); uint32_t humidity = humidity_get_average(); switch (state) { case STATE_IDLE: // 土壤湿度达到干燥阈值,启动水泵 if (humidity > humid_dry) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_RESET); state = STATE_WATERING; state_tick = cur_tick; } break; case STATE_WATERING: // 湿度已充足,停止浇水 if (humidity < humid_wet) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_SET); state = STATE_COOLDOWN; state_tick = cur_tick; } // 单次浇水超过60秒,强制停止并进入冷却 else if ((cur_tick - state_tick) > 60000) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_SET); state = STATE_COOLDOWN; state_tick = cur_tick; } break; case STATE_COOLDOWN: // 冷却5秒,等传感器读数稳定再继续检测 if ((cur_tick - state_tick) > 5000) { state = STATE_IDLE; state_tick = cur_tick; } break; default: state = STATE_IDLE; break; } }代码里dry阈值取2800、wet阈值取2200,但这两不是固定的,取决于你的土壤类型、探头埋深和传感器模块的基准电压。我的建议是第一次运行前先把传感器放在空气里读一个值(大约是3900到4000),插到充分浇透的土壤里读一个值(大约1200到1500),然后按“干土值低于空气值300到500,湿土值高于水面值200到400”的原则来标定。程序运行阶段用串口打印湿度原始值,边观察边调整阈值最靠谱。
按键逻辑我放在定时器中断里做扫描,每10毫秒读一次按键引脚,连续读到100次高电平才认为是一次有效长按,这个防抖思路可以杜绝机械按键的抖动误触发。长按之后系统切换到手动模式,水泵强制开启15秒后自动关闭并返回自动模式,这样调试水泵时不需要每次都抓一根杜邦线去短接继电器。
4. 常见问题与排查技巧实录
4.1 烧录报错:Error: No STM32 Target Found!
这个报错我相信玩过STM32的都见过。我第一次用ST-Link给最小系统板下载程序时就遇到了,当时还以为是芯片坏了,折腾了好久才发现是接线问题。排查顺序很关键:先看ST-Link和STM32板子之间SWDIO、SWCLK、GND三根线是否接对,然后看目标板有没有独立供电(有的ST-Link能从SWD接口给目标板供电,但有些不能,要单独接USB线的5V),最后看芯片的BOOT0引脚是否被拉低到GND(BOOT0悬空状态会导致芯片进入系统内存引导模式,不执行Flash里的用户程序,ST-Link连接时也可能报target not found)。
还有一类情况是程序里把SWD引脚重映射成普通GPIO了。我在调试OLED显示的时候就把PB3/PB4当成了普通IO用,而这两个引脚默认就是SWD的JTAG引脚,程序一旦把JTAG关闭,ST-Link就再也连不上芯片了。解决办法是下载程序前按住复位键,点下载松开复位,利用复位瞬间擦除Flash,或者直接用串口ISP方式擦除芯片。我现在习惯在程序初始化阶段保留SWD功能,用不到JTAG的时候不要调用GPIO_PinRemapConfig里面的JTAG-Disable重映射函数。
另外ST-Link Utility这个工具也很好用,可以直接把编译出来的bin文件烧进芯片,还能看Flash内容。如果只是烧录不想每次打开Keil,可以单独用它,界面里Target菜单下面的Connect选项改成Under Reset模式,这样对已经禁用SWD的芯片也能强连恢复。
4.2 Keil5编译工程与延时函数卡死问题
很多同学第一次用Keil5,装完发现不能打开STM32工程,或者编译各种头文件找不到。大部分原因是芯片支持包(DFP)没装,或者装了和工程版本不匹配的包。我推荐直接在Pack Installer里面装最新稳定版,不要在工程文件里用老版本包覆盖,否则经常出现缺核心头文件的诡异问题。
还有C51和STM32一起装的情况。Keil5本身是可以同时支持8051和ARM的,但必须分别安装两个CDK包,安装目录建议一个装C盘Keil_v5,一个装D盘Keil_C51,不要在同一个目录里反复覆盖,不然操作系统会把两个工具的注册信息搞混,编译时不是报缺C51编译器就是报缺ARM编译器。
程序里HAL_Delay卡死这个问题也很典型。常见原因是CubeMX初始化的时候配置了系统滴答定时器(SysTick),但在某个外设中断里又调用HAL_Delay,而HAL_Delay需要依赖SysTick中断不断更新tick计数器,如果SysTick中断优先级被设置得比其他中断低,且这个中断里是一个死循环等待延迟结束,就形成了死锁。我在调试继电器切换的时候踩过这个坑,后来统一把SysTick中断优先级设置为最高,并保证不在中断回调函数里调用HAL_Delay,只在主循环里用,卡死问题就再没出现过。
4.3 湿度读数不稳定与校准技巧
湿度传感器的读数不稳定的因素很多,最常见的是供电噪声。传感器模块上最好靠近VCC和GND引脚加一个0.1uF的陶瓷电容做去耦;数据线不要和继电器、水泵的供电线捆扎在一起走线,否则合闸瞬间的尖峰会直接耦合到ADC引脚上。
还有一个我自己摸索出来的校准技巧:把探头放在花盆里埋好位置之后,浇一次透水,记录下充分湿润状态下的ADC值;再让它在太阳下暴晒两天,记录下接近干枯状态的ADC值。然后把这两个值分别加减10%作为启动和停止阈值,实测下来比拍脑袋设值靠谱得多。每次换土或者换花盆都要重新标定,因为不同土壤的持水特性差异很大,同一个阈值可能在这个盆里频繁浇水,在另一个盆里又长期不浇。
我在实际使用中发现,把传感器探头竖着插入土里比水平放置读数更稳定,因为竖插时探头接触的土壤层更厚,局部的土壤水分差异对读数影响更小。探头也不要插到底挨着花盆底部,那里容易积水,建议安装在花盆高度1/3到1/2的位置,大概在植物根系的中间层次,这样测出来才是根系真正能够感知到的水分状态。
整套系统跑下来的体会是,STM32自动浇花这个项目的难度曲线对初学者非常友好:硬件电路不复杂,核心逻辑就是ADC采样加状态机,代码量也不大,但麻雀虽小五脏俱全——GPIO、ADC、DMA、定时器、中断、串口调试都用上了,完整做完一遍,相当于把STM32从入门到实战最核心的那部分知识全部过了一遍。而且这个项目做出来是真能用的,不是学完就扔的玩具,夏天出门一周回来,阳台上那几盆绿植还在精神地活着,这种成就感比跑通一个流水灯或者OLED显示一个大大的“Hello World”强太多了。
本文还有配套的精品资源,点击获取