ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32自动浇花系统源码:从土壤湿度采集到水泵控制

STM32自动浇花系统源码:从土壤湿度采集到水泵控制

简介:STM32自动浇花系统是一套面向嵌入式初学者与电子爱好者的完整智能硬件实践方案,以STM32F103C8T6为核心,融合土壤温湿度、光照、空气温湿度传感器与OLED显示、DC-DC电源、水泵控制等模块,实现按湿度阈值或定时策略自动灌溉,帮助读者理解从传感器采集到执行控制的完整闭环。资源包共535个文件,以86个C源文件与70个头文件为主体,配合Keil工程文件、PCB设计资料、PDF文档、示例程序及图片素材,压缩包约27.77MB,覆盖固件代码、硬件设计与调试记录。目前已有3779人学习下载,适合希望动手搭建嵌入式项目、掌握STM32CubeMX与HAL库开发流程的读者参考,可据此复现硬件连接、移植控制逻辑并扩展远程配置功能。

1. 从“出差一周花全枯了”说起:STM32自动浇花系统能帮你做什么

出差一周回来,阳台那盆养了三年的绿萝叶子全耷拉了,土干得裂开——这事我经历过不止一次。后来我干脆用STM32搭了一套自动浇花系统,土壤湿度低于阈值就自动开泵,湿度够了就停,手机都不用掏。这份STM32自动浇花系统.zip就是一套完整的工程源码,包含主控逻辑、传感器采集、水泵驱动和状态显示几个核心模块。它适合两类人:一是刚学完STM32外设驱动、想找个完整小项目练手的嵌入式新手;二是家里养了花又经常忘记浇水、想自己动手做一个的开发者。整套代码基于标准库或HAL库组织,外设覆盖ADC、GPIO、定时器,拿来就能编译烧录,改几个阈值参数就能适配你自己的花盆。

2. 系统架构拆解:从土壤湿度采集到水泵控制的完整链路

2.1 硬件模块划分与数据流向

这套系统的硬件结构不复杂,但每个模块都有明确的职责。主控芯片负责调度整个采集-判断-执行循环,土壤湿度传感器把模拟量送到ADC通道,MCU读回来做滤波和阈值比较,判断结果驱动继电器或MOS管控制水泵,同时把当前湿度值和泵的状态刷到OLED或LCD上。如果加了ESP8266之类的WiFi模块,还能把数据推到手机端看。

数据流向是这样的:传感器→ADC→MCU内部处理→GPIO输出→驱动电路→水泵。这条链路里最容易出问题的是ADC采样精度和驱动电路的隔离,后面避坑章节会细说。

常见做法是把整个工程分成三层:硬件抽象层(传感器读取、泵控制、显示驱动)、业务逻辑层(阈值判断、浇水时长控制、防抖)、应用层(主循环调度、状态机)。这样分层的好处是换传感器或换屏幕时只改硬件抽象层,业务逻辑不动。

2.2 目录结构长什么样

拿到源码包解压后,典型的目录结构大致如下(不同版本可能略有差异,但核心文件跑不掉):

STM32_AutoWater/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── adc.h │ │ ├── gpio.h │ │ ├── tim.h │ │ └── oled.h │ └── Src/ │ ├── main.c │ ├── adc.c │ ├── gpio.c │ ├── tim.c │ └── oled.c ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── CMSIS/ ├── Middlewares/ │ └── (可选) WiFi/蓝牙协议栈 ├── MDK-ARM/ 或 STM32CubeIDE/ │ └── 工程文件 └── README.md

Core/Src/main.c是主循环所在地,adc.c管传感器采样,gpio.c管继电器和按键,tim.c管定时中断(用来做周期性采样),oled.c管显示。如果你用的是标准库而不是HAL库,目录会变成User/和Library/的经典结构,逻辑一样。

提示:拿到工程先看main.c里的while(1)循环,那是整个系统的“心脏”,所有调度逻辑都在里面。

2.3 主循环调度逻辑

主循环的骨架大概长这样,我按HAL库的写法给你还原一下核心结构:

/* main.c 主循环核心片段 */ int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_TIM2_Init(); OLED_Init(); uint16_t soil_raw = 0; uint8_t pump_state = 0; uint32_t last_sample_tick = 0; while (1) { /* 每500ms采样一次,避免频繁读取导致ADC抖动 */ if (HAL_GetTick() - last_sample_tick >= 500) { last_sample_tick = HAL_GetTick(); /* 读取ADC通道,获取土壤湿度原始值 */ HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { soil_raw = HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); /* 原始值映射为百分比,0%最干,100%最湿 */ uint8_t soil_percent = map_adc_to_percent(soil_raw); /* 阈值判断:低于30%开泵,高于60%关泵,中间保持 */ if (soil_percent < 30 && pump_state == 0) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_SET); pump_state = 1; } else if (soil_percent > 60 && pump_state == 1) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_RESET); pump_state = 0; } /* 刷新OLED显示当前湿度与泵状态 */ OLED_ShowString(0, 0, "Soil:"); OLED_ShowNum(40, 0, soil_percent, 3); OLED_ShowString(0, 2, pump_state ? "Pump:ON " : "Pump:OFF"); } } }

这段代码有几个关键点。第一,采样周期用HAL_GetTick()做非阻塞延时,而不是HAL_Delay(),这样主循环不会被卡死,后面加按键或通信不会互相干扰。第二,阈值用了30%和60%两个点,形成回差区间,避免湿度在阈值附近波动时泵反复开关——这个在控制里叫滞回,后面避坑章节会专门讲。第三,map_adc_to_percent()需要你根据自己传感器的校准值来写,12位ADC满量程4095,但传感器实际输出范围通常不是0到4095,得实测。

2.4 传感器采样与滤波处理

土壤湿度传感器输出的是模拟电压,STM32的ADC是12位精度,理论上0到4095对应0到3.3V。但实际用的时候,传感器在空气中和泡在水里的输出值并不是0和4095,常见做法是实测两个端点值,然后做线性映射。

/* 将ADC原始值映射为湿度百分比 */ uint8_t map_adc_to_percent(uint16_t raw) { /* 这两个值需要你实测:空气中读数 和 水中读数 */ const uint16_t AIR_VALUE = 3800; /* 传感器在干燥空气中的ADC值 */ const uint16_t WATER_VALUE = 1200; /* 传感器泡在水中的ADC值 */ if (raw >= AIR_VALUE) return 0; if (raw <= WATER_VALUE) return 100; /* 线性映射 */ return (uint8_t)((AIR_VALUE - raw) * 100 / (AIR_VALUE - WATER_VALUE)); }

参数说明:AIR_VALUE和WATER_VALUE必须自己测。把传感器放空气中读一次ADC,再泡水里读一次,把两个值填进去。不同批次的传感器这两个值能差好几百,直接用别人的参数大概率翻车。

滤波方面,最简单的做法是连续采5次去掉最大最小值再取平均,代码里加个数组循环就行。如果ADC读数跳得厉害,可以在硬件上并在传感器输出端加一个0.1uF电容到地,软件硬件一起上才稳。

3. 跑起来:编译、烧录与参数校准的完整步骤

3.1 开发环境准备与工程导入

这套工程大概率是用Keil MDK或者STM32CubeIDE建的。如果你拿到的是MDK工程,需要装好Keil MDK和对应的STM32F1xx器件包;如果是CubeIDE工程,直接File→Import→Existing Projects into Workspace导入即可。

以MDK为例,打开MDK-ARM目录下的.uvprojx文件,检查两件事:一是Options for Target里的器件型号是否和你手上的板子一致(常见是STM32F103C8T6),二是Debug选项卡里选的下载器是不是你用的(ST-Link还是J-Link)。这两项不对,编译能过但烧不进去。

编译前先点Rebuild All,看有没有报错。常见报错是头文件路径没配好,在Options for Target→C/C++→Include Paths里把Core/Inc和Drivers的路径加进去就行。

3.2 烧录与首次上电检查

烧录用ST-Link的话,接线是SWDIO、SWCLK、GND、3.3V四根线。烧录成功后先别急着接水泵,按这个顺序检查:

第一步,只看OLED。上电后屏幕应该显示当前湿度和Pump:OFF。如果屏幕不亮,先查OLED的I2C地址对不对,常见是0x78或0x3C,在oled.c里改。

第二步,手动改变传感器环境。把传感器从空气中放进湿土里,看OLED上的湿度值有没有变化。如果数值不动,用万用表量传感器输出脚电压,确认传感器本身在工作。

第三步,接水泵测试。注意水泵不能直接接STM32的GPIO,必须经过继电器模块或MOS管驱动板。GPIO输出3.3V的电流只有十几毫安,驱动不了水泵,直接接上去要么泵不转,要么把IO口烧了。

# 如果用命令行烧录(st-flash工具) st-flash write build/STM32_AutoWater.bin 0x8000000 # 烧录后复位 st-flash reset

3.3 阈值参数怎么改才合理

代码里默认的30%开泵、60%关泵不是万能值。不同植物对土壤湿度要求不一样,多肉可能20%就够了,蕨类可能要保持在50%以上。改的时候注意两点:

一是开泵阈值和关泵阈值之间至少留20%的间隔,间隔太小泵会频繁启停,继电器咔咔响,寿命也短。二是浇水时长要限制,不能光靠湿度判断。万一传感器坏了或者被拔出来,湿度一直读0%,泵就会一直浇,把花淹死。常见做法是在代码里加一个最大浇水时长,比如泵连续运行超过10秒就强制关闭,并报一个错误状态。

/* 浇水超时保护 */ if (pump_state == 1 && (HAL_GetTick() - pump_start_tick > 10000)) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_RESET); pump_state = 0; fault_flag = 1; /* 标记故障,OLED显示ERR */ }

这个保护逻辑我强烈建议加上,成本几行代码,但能避免“自动浇花变成自动淹花”的惨剧。

4. 避坑与排查:调试自动浇花系统时最容易翻车的几个点

4.1 现象:湿度读数一直在跳,泵跟着反复开关

原因:ADC采样没有滤波,或者传感器供电不稳。土壤湿度传感器的输出阻抗比较高,STM32的ADC采样时间如果设得太短,采到的值就不准。

解决:把ADC的采样时间从默认的1.5周期改成71.5周期或更长,同时在软件里加滑动平均滤波。硬件上在传感器输出和GND之间并一个0.1uF电容。另外阈值判断一定要用回差,别用单点比较。

4.2 现象:水泵不转,但OLED显示Pump:ON

原因:GPIO输出正常但驱动电路没工作。常见是继电器模块的VCC没接,或者继电器是5V的而STM32输出3.3V驱动不了。

解决:先量继电器模块的VCC脚有没有5V。如果是3.3V继电器,检查IN脚有没有收到高电平。如果用的是MOS管驱动,检查MOS管的栅极电阻和下拉电阻有没有焊。最稳妥的方案是用光耦隔离的继电器模块,3.3V能直接驱动。

4.3 现象:烧录后程序不跑,或者跑几秒就死机

原因:堆栈溢出或者中断优先级冲突。如果工程里开了多个中断(定时器、串口、ADC),优先级配置不对会导致中断嵌套出问题。

解决:在CubeMX里检查NVIC配置,把SysTick和定时器中断的优先级设好,别让高优先级中断里做太多事。另外检查启动文件里的堆栈大小,默认可能是0x400,如果用了OLED显存数组,改成0x800。

4.4 现象:传感器泡水几天后读数漂移,甚至完全失效

原因:土壤湿度传感器长期通电会电解腐蚀,这是这类电阻式传感器的通病。

解决:不要给传感器长期供电,改成采样时才供电,用一个GPIO控制传感器的VCC,采完就断电。这样能大幅延长寿命。或者换电容式土壤湿度传感器,虽然贵一点但不腐蚀。

注意:电阻式传感器不适合长期埋在土里通电,这是硬件层面的坑,软件补不了。

5. 二次开发:从能跑到好用,几个改造方向和一个校准技巧

这套系统跑通之后,能改的地方其实很多。我按优先级给你排几个方向。

第一个方向是加WiFi上报。用ESP-01S或ESP8266模块,通过AT指令连上MQTT服务器,把湿度和泵状态推上去。代码改动集中在主循环里加一个串口发送函数,注意别在中断里发,放主循环里做。数据格式用JSON最简单,{"soil":45,"pump":0},服务器端随便用什么都能解析。

第二个方向是加本地按键和菜单。现在阈值是写死在代码里的,改一次要重新烧录。加两个按键和OLED菜单,就能在设备上直接调阈值。实现上用状态机,短按切换菜单项,长按进入编辑,代码量大概两百行。

第三个方向是加数据记录。用STM32内部的Flash模拟EEPROM,每隔一小时存一次湿度值,存满一圈覆盖最老的。这样能回看过去几天的湿度曲线,判断浇水策略是否合理。

最后说一个校准技巧,也是我踩过坑之后养成的习惯:每次换传感器或者换花盆,我都会先跑一遍“干湿标定”。具体做法是在代码里加一个标定模式,上电时按住按键进入,先把传感器放空气中,按一下键记录AIR_VALUE;再泡水里,按键记录WATER_VALUE;两个值自动写进Flash,下次上电直接读。这样换传感器不用改代码重烧,省事很多。

/* 标定模式核心逻辑 */ void calibration_mode(void) { OLED_ShowString(0, 0, "Dry: press KEY1"); while (KEY1_Read() == 0); /* 等待按下 */ AIR_VALUE = read_adc_average(); OLED_ShowString(0, 2, "Wet: press KEY1"); while (KEY1_Read() == 0); WATER_VALUE = read_adc_average(); save_to_flash(AIR_VALUE, WATER_VALUE); OLED_ShowString(0, 4, "Saved!"); HAL_Delay(1000); }

从那以后我每次换传感器都强制走一遍标定流程,再也没出现过“明明土是湿的但显示0%”这种玄学问题。希望这套源码和这些改造思路能帮到你,拿到手先跑通主循环,再按自己的需求往上加功能,比一上来就大改要稳得多。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进