资讯动态

STM32自动浇花系统:土壤湿度闭环控制与工程实践

发布时间:2026/10/15 13:35:54 来源:尧图企业网站定制
简介STM32自动浇花系统.zip 是一份面向嵌入式学习者与智能硬件爱好者的完整项目资料包基于STM32F103C8T6实现土壤温湿度、光照及空气温湿度监测并自动控制水泵浇水。资源共535个文件大小约27.77MB涵盖C语言源码86个.c、70个.h、原理图/PCB设计png、pdf、工程配置文件uvprojx、uvoptx等以及说明文档txt、docx便于对照硬件设计理解代码逻辑。已有3777人学习下载。项目不仅包含完整的STM32工程源码还提供OLED显示、DC-DC电源、传感器接口等模块设计资料以及水泵控制策略示例适合从零搭建类似系统或进行课程设计参考。通过该资料可掌握STM32外设驱动、硬件布局和完整开发流程。1. STM32自动浇花系统让土壤湿度说话而不是让定时器替植物做主拿到“STM32自动浇花系统.zip”这个压缩包多数人第一反应是“做个定时浇水器”这个思路恰恰是本项目最容易翻车的地方。花不是按钟点渴的夏天正午盆土已经干透定时器还没到点连续阴天土壤还很湿定时器照样补一遍水几天下来根就泡烂了。这套系统的价值在于把“土壤湿度”变成反馈量用STM32不断读传感器湿度低于下限就开泵浇水达到上限就停泵形成闭环控制。它适合三类人一是养绿植总担心出差浇水的阳台党二是想练手ADC、GPIO、状态机的嵌入式初学者三是有土壤样本保水需求的实验室场景。下面按我实际做这类系统的习惯从选型、接线、代码到排查一步步拆开讲。2. 选型与接线先把控制策略和硬件关系盘清楚再动手2.1 为什么湿度闭环控制比定时浇灌可靠定时浇水本质是开环控制系统只知道自己转了多少圈不知道盆土是干是湿。植物蒸腾速率受光照、温度、风力影响同一天里不同时段差异很大室外和室内的盆土蒸发速度也完全不是一个量级。固定周期浇水的结果必然是“晴天浇不够、阴天浇过头”。而闭环控制把土壤湿度传感器当作系统输入每次浇水前先问“你现在渴不渴”渴了就补不渴就继续等。工程上这个系统不需要测出绝对湿度值只需要一个可重复的相对量干燥土壤对应一个ADC读数浸水饱和对应另一个读数中间用线性映射换算成0100%的湿度百分比再跟两个阈值做比较。低于下限开泵高于上限停泵中间留一段迟滞区避免在临界点反复启停水泵。这就是自动浇花系统的核心控制思路其余OLED显示、按键设置、掉电保存都是围绕它铺开的。土壤湿度传感器对MCU来说其实是个“黑匣子”你只看到电压变化不知道土壤内部发生了什么。想让它可靠必须先把干湿两个参照点校准出来。这一点很多移植这个项目的开发者会跳过结果读数一直在飘后面我会专门花一节讲校准。2.2 传感器、继电器、水泵怎么选电容式传感器和带光耦的继电器先看一张选型对比表这是我做第一版时踩完坑后总结出来的部件常见选项特性与坑建议土壤湿度传感器电阻式金属探针直接插土靠土壤导电率估湿度探针通直流电会电解用一两个月后读数开始飘胜在便宜不推荐土壤湿度传感器电容式PCB敷铜做电极测介电常数电极不直接和土壤发生电解反应寿命长模块稍贵第一选择执行控制继电器模块机械触点驱动能力大但线圈有反电动势需确认触发电平和光耦推荐带光耦执行控制MOS管无机械寿命但驱动电平、散热和感性负载吸收都要自己做进阶再换水泵直流离心泵/隔膜泵12V额定电流约0.51A启动瞬间是额定电流的两倍以上先上12V隔膜泵我一般第一版会选用“电容式传感器 带光耦继电器 12V隔膜泵”。电容式传感器省掉每月拔探针打磨的维护带光耦的继电器模块把MCU控制侧和12V功率侧隔开抗干扰能力好12V隔膜泵流量可控短时间干转也不至于立刻烧掉。传感器模块的AO模拟输出直接接STM32的ADC引脚DO数字输出不用阈值判断全在单片机里自己做。2.3 供电与接线12V与3.3V必须共地ADC不能超量程接线关系是这样的12V电源一路接继电器模块的JD-Vcc和COM公共端另一路接一个降压模块降到3.3V给STM32开发板、OLED屏幕、湿度传感器供电。继电器模块的逻辑侧Vcc接3.3VIN引脚接MCU的GPIOOUT触点一端接12V正极另一端接水泵正极水泵负极接12V地。有几个细节在这里就要定好。第一12V电源的地和3.3V的地必须连在一起否则继电器线圈回路形不成通路水泵根本不会动作。第二很多湿度传感器模块标称3.35V供电但如果用5V供电AO口在干燥时可能输出到4V以上而STM32的ADC输入范围是03.3V超量程会损坏引脚。我在这里一律给传感器供3.3VAO直接进ADC满量程分辨率虽低一点但校准后完全够用。第三继电器模块的触发极性要确认常见模块有高电平触发和低电平触发两种版本也有的模块带跳线可切换接线前先看丝印。MCU上电瞬间GPIO多为低电平如果碰到低电平触发的继电器又不做处理就会出现插电瞬间水泵自己转了一下这种尴尬事。2.4 CubeMX初始化ADC采样周期、GPIO默认电平、I2C速率用CubeMX建工程时芯片选F103系列最常用的型号时钟源用外部晶振还是内部RC都行这个系统对时间精度要求不高。关键配置是四个点ADC1选一个通道比如PA0配置为模拟输入。采样时间要拉到最长239.5周期因为土壤湿度传感器输出阻抗偏高采样时间短了读数会明显抖动。水泵控制引脚设为输出模式初始电平必须对应“继电器断开”的状态。GPIO在复位后默认是低电平如果继电器是低电平触发需要在CubeMX里把初始电平设成高或者外部电路加一个合适的上下拉。按键引脚配置为上拉输入内部上拉打开按键另一端接地按下时读到低电平。如果接OLEDI2C时钟设100kHz就够了SSD1306虽然支持400kHz但长线连接时100kHz抗干扰更强。水泵控制GPIO的默认电平是很多移植项目忽略的坑CubeMX生成的代码在HAL_GPIO_Init之前引脚还处于复位状态。也就是说如果硬件电路没做任何处理从芯片上电到GPIO初始化完成这段窗口期继电器可能先导通一次。常见做法是选择高电平触发的继电器或者把GPIO默认输出电平配置成“断开状态”的电平再晚一点让控制逻辑接管。3. 最小可跑系统从ADC读土壤湿度到自动浇水的完整控制循环这个系统的最小代码量很小核心就三件事读传感器、换算百分比、跑浇水状态机。下面按我习惯的文件划分把每个环节单独拆开代码可以直接照着移植。3.1 读取土壤湿度滤波函数与百分比映射先写一个读ADC原始值的函数。注意HAL_ADC_Start之后要PollForConversion确认转换完成再取值不能直接拿GetValue。// soil_sensor.c 土壤湿度采集 uint16_t soil_read_raw(void) { uint16_t raw 0; HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 20) HAL_OK) { raw HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); return raw; }这段代码每次采集一个ADC原始值排超时保护。20ms的等待足够F103完成一次转换转换完成后立即Stop避免ADC持续采样占用功耗。原始值直接拿来用容易被毛刺干扰土壤中的根系、气泡、石块都会让读数瞬时跳变。我在采集层加一个去极值平均滤波连续采5次排序后丢掉最大和最小取中间3个的平均。// soil_sensor.c 滤波采集 #define FILTER_SAMPLES 5 uint16_t soil_read_filtered(void) { uint16_t buf[FILTER_SAMPLES]; for (int i 0; i FILTER_SAMPLES; i) { buf[i] soil_read_raw(); HAL_Delay(50); // 间隔50ms两次采样之间让电容充分恢复 } // 冒泡排序去掉最大最小值 for (int i 0; i FILTER_SAMPLES - 1; i) { for (int j i 1; j FILTER_SAMPLES; j) { if (buf[i] buf[j]) { uint16_t t buf[i]; buf[i] buf[j]; buf[j] t; } } } uint32_t sum 0; for (int i 1; i FILTER_SAMPLES - 1; i) { sum buf[i]; } return (uint16_t)(sum / (FILTER_SAMPLES - 2)); }50ms间隔是给采样电容和传感器调理电路一个恢复时间连续快速采多个点反而会互相影响。5次采样过滤掉一次峰值和一次低谷后取平均能压掉大部分毛刺代价是每次滤波约250ms对1秒级控制周期来说完全可接受。有了比较干净的原始值下一步做百分比映射。不同模块的电压方向不一样我用的常见电容式模块是“越湿电压越低”下面的公式按这个方向写// soil_sensor.c 湿度百分比换算 #define CAL_DRY 3300 // 干燥土壤时的ADC读数务必按自己模块校准 #define CAL_WET 1100 // 浸水饱和时的ADC读数务必按自己模块校准 uint8_t soil_humidity_percent(uint16_t raw) { if (raw CAL_DRY) return 0; if (raw CAL_WET) return 100; return (uint8_t)((uint32_t)(CAL_DRY - raw) * 100 / (CAL_DRY - CAL_WET)); }公式的含义很直接把干燥到饱和这段ADC区间线性映射到0100%。F103的ADC是12位满量程4095CAL_DRY和CAL_WET必须在实际土壤中测量得到不能照抄。校准方法很简单传感器插在干透的盆土里读20次取平均得到CAL_DRY再插进浸水后的土里读20次取平均得到CAL_WET。如果发现模块电压方向和公式相反把两个值对调或者把公式反过来即可这个问题我后面专门讲。3.2 控制核心迟滞比较器与超时保护的状态机控制部分不要用“湿度小就开、湿度大就关”的单点比较否则会在阈值附近反复抽水。我用迟滞区间低于下限才浇浇到高于上限才停中间这段不动作。// control.c 浇灌状态机 typedef enum { SOIL_STATE_WAIT, // 待机等待土壤变干 SOIL_STATE_WATERING // 浇水正在供水 } soil_state_t; #define SOIL_HYST_LOW 30 // 湿度低于30%启动浇水 #define SOIL_HYST_HIGH 55 // 湿度高于55%停止浇水 #define SOIL_WATER_MAX_SEC 30 // 单次浇水最长30秒 soil_state_t g_state SOIL_STATE_WAIT; volatile uint32_t g_water_start_ms 0; void watering_loop(void) { uint16_t raw soil_read_filtered(); uint8_t pct soil_humidity_percent(raw); if (g_state SOIL_STATE_WAIT) { if (pct SOIL_HYST_LOW) { pump_on(); g_water_start_ms HAL_GetTick(); g_state SOIL_STATE_WATERING; } } else if (g_state SOIL_STATE_WATERING) { if (pct SOIL_HYST_HIGH || (HAL_GetTick() - g_water_start_ms) SOIL_WATER_MAX_SEC * 1000U) { pump_off(); g_state SOIL_STATE_WAIT; } } }SOIL_WATER_MAX_SEC是硬兜底逻辑哪怕传感器坏了、读数一直停在低位水泵也只转30秒避免水漫金盆。上限阈值一般设在5565%让土壤浇透后有余量慢慢蒸发不会刚停又立刻启动。水泵驱动函数本身是两个GPIO操作但要注意继电器触发极性// control.c 水泵控制 void pump_on(void) { HAL_GPIO_WritePin(PUMP_CTRL_GPIO_Port, PUMP_CTRL_Pin, GPIO_PIN_SET); } void pump_off(void) { HAL_GPIO_WritePin(PUMP_CTRL_GPIO_Port, PUMP_CTRL_Pin, GPIO_PIN_RESET); }如果继电器模块是低电平触发把SET和RESET互换或者改模块上的跳线。我在代码里统一用“写1代表水泵转”的逻辑硬件上通过跳线去迁就它这样主程序看着最直观。3.3 按键调阈值与OLED显示非阻塞的人机交互浇水阈值不能写死在代码里得留两个按键让使用者现场调。按键扫描放在主循环里做非阻塞检测检测下降沿触发按下一次阈值加1或减1。// ui.c 按键与显示 void ui_button_scan(void) { static uint8_t last_up 1; static uint8_t last_dn 1; uint8_t up HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin); uint8_t dn HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port, KEY_DOWN_Pin); // 按下沿有效 if (up 0 last_up 1) { if (g_high_th 90) g_high_th 1; } if (dn 0 last_dn 1) { if (g_high_th 20) g_high_th - 1; } last_up up; last_dn dn; }这里对阈值操作的是全局变量g_high_thOLED显示和浇水状态机都读取同一个变量。长按快速连加的功能可以后续加核心原则是按键扫描里不能用HAL_Delay否则按下的瞬间整个控制循环就卡住了。OLED显示用常见的SSD1306库I2C刷新一次屏幕大概几毫秒不会阻塞主逻辑。我一般把屏幕分成两行第一行显示当前湿度百分比第二行显示阈值区间例如“30~55%”。刷新周期可以放到500ms一次避免屏幕闪烁也减少I2C占用。3.4 手动浇水模式与低水位保护自动浇水之外还要留一个手动模式长按确认键强制浇水一段时间方便换盆、施肥后手动补水。手动模式同样要受超时保护约束防止忘关。// control.c 手动浇水 #define MANUAL_WATER_MS 5000 // 手动模式单次最多5秒 void manual_watering_if_needed(uint8_t button_pressed) { if (button_pressed) { pump_on(); g_water_start_ms HAL_GetTick(); g_state SOIL_STATE_WATERING; g_manual_active 1; } if (g_manual_active (HAL_GetTick() - g_water_start_ms) MANUAL_WATER_MS) { pump_off(); g_manual_active 0; g_state SOIL_STATE_WAIT; } }低水位保护在水泵类项目里是必须的水箱没水时继续抽隔膜泵虽然耐干转但长期空抽发热照样会坏。我习惯在水箱里放一个浮球液位开关输出信号接MCU上拉输入水位正常时为高电平水位过低变成低电平。控制循环里先判断这个信号无效就直接禁止开泵。4. 移植与调试避坑湿度跳变、水泵复位、阈值丢失逐个排查4.1 湿度值跳变浇水后读数方向还反着来——先校准再谈滤波现象是串口打印的ADC数值像心电图一样乱跳更气人的是有时候浇完水读数不降反升感觉传感器在跟你作对。我先说第一个问题。ADC值抖动大多是采样时间太短或者线材受干扰F103的采样时间在CubeMX里没拉到最长传感器输出阻抗又偏高采样电容充不满读数自然飘。解决方法是把采样时间选到239.5周期并把传感器AO线尽量缩短远离12V动力线。第二个问题则是典型的校准方向不对不同型号模块的电压方向本来就不一样有的越湿电压越低有的越湿电压越高。如果代码里按“越高越干”写而模块实际是“越高越湿”浇完水读数必然往反方向跑。排查思路很简单先别滤波直接打印原始ADC值。传感器插在干燥土里和浸水土里各读20次看哪个值大哪个值小然后根据真实方向调整映射公式的分子。这个步骤省不得属于系统移植的第一步。4.2 水泵启动的瞬间MCU复位——电源与续流都没做好现象很典型继电器吸合那一刻OLED屏幕闪一下程序从头开始跑串口打印全部重来。原因是水泵启动电流是额定电流的两倍以上瞬时把12V电源拉垮DC-DC降压模块的输出也跟着掉MCU当场掉电复位。另外一个隐蔽原因是继电器线圈在断开瞬间会产生反电动势如果模块上没有续流二极管这个尖峰顺着供电线串进3.3V侧也会让MCU重启动。解决分三步走。第一12V电源线用粗导线单独走水泵的供电不要和MCU供电共用小细线第二继电器模块确认带光耦隔离继电器线圈两端并联1N4007续流二极管电机两端并联一个100nF陶瓷电容吸收尖峰第三如果电源适配器本身余量不足直接换电流大一档的适配器这个钱不能省。4.3 调好的阈值断电就丢——把配置写进Flash才靠得住现象是每次断电再上电之前用按键调好的上下限又回到默认值30和55。原因是阈值一直存在RAM变量里掉电即消失。解决办法是把配置存进STM32内部Flash的最后一页。F103C8T6有64KB Flash每页1KB最后一页起始地址是0x0800FC00程序体积不大时这页不会被占用。写入前要先解锁Flash、擦除整页再按4字节Word写入读取时用一个固定魔数判断是有效配置还是空页// config.c 掉电保存阈值 #define FLASH_SAVE_ADDR 0x0800FC00 // F103C8T6 64KB的最后一页 #define CONFIG_MAGIC 0xA5A5A5A5 void config_save(uint16_t low_th, uint16_t high_th) { HAL_FLASH_Unlock(); FLASH_ErasePage(FLASH_SAVE_ADDR); // 必须先擦除整页 HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SAVE_ADDR, (uint32_t)high_th 16 | low_th); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SAVE_ADDR 4, CONFIG_MAGIC); HAL_FLASH_Lock(); } void config_load(uint16_t *low_th, uint16_t *high_th) { uint32_t *p (uint32_t *)FLASH_SAVE_ADDR; if (p[1] CONFIG_MAGIC) { *low_th p[0] 0xFFFF; *high_th p[0] 16; } else { *low_th 30; *high_th 55; } }写入逻辑是“先擦后写”擦除失败时不要继续写读取端遇到空页全0xFFFFFFFF或魔数不对都回退到默认值。要注意Flash有擦写寿命这个函数只在按键修改阈值后调一次不要在每轮浇水循环里写。如果程序代码体积已经很大去链接脚本里把Flash终点缩到0x0800FC00防止程序区覆盖配置页。4.4 显示值与控制值对不上——全链路统一用百分比别重复映射现象是OLED明明显示湿度58%可程序在水泵该停的时候没停或者该开的时候没开看起来逻辑“精神分裂”。原因是显示模块和控制模块各做了一次ADC到百分比的换算两边可能用了不同的阈值判断变量或者在控制函数里直接拿ADC原始值和百分比阈值比较。这类问题的排查成本很高因为我总怀疑是传感器坏了实际是数据流没理清。解决方法是全系统只保留一个湿度来源在采集层把最终的百分比结果存到全局变量g_soil_pctOLED显示和控制状态机都只读它任何地方都不允许再做第二次换算。这样就算数值有问题只需要盯一个变量就能定位。// 全链路只保留一个百分比数据 volatile uint8_t g_soil_pct 0; void soil_500ms_task(void) { g_soil_pct soil_humidity_percent(soil_read_filtered()); } // OLED显示直接用 g_soil_pct // 控制循环也直接用 g_soil_pct4.5 继电器上电就吸合或不动作——先查触发电平和跳帽现象分两种一种是刚插上电源水泵自己转了一两秒才停另一种是湿度已经低于阈值继电器一点反应都没有。上电就转的根因是MCU复位期间GPIO处于低电平而模块恰好是低电平触发。复位期间引脚电平受内部状态和外部上下拉影响如果电路板上没接下拉电阻这个窗口期就可能误触发。我习惯选高电平触发的继电器或者把模块跳线切到高电平触发从硬件上保证“写1才吸合、复位默认断开”。继电器不动作则反过来先确认触发极性跟代码是否一致再用万用表量继电器模块IN引脚的电压区分是GPIO没输出还是模块供电不对。很多低电平触发模块的Vcc逻辑侧没供电IN电平再对也没反应。5. 进阶方向Flash掉电保存、PWM滴灌与雨量传感器接入基础版能自动浇水以后我会再加三个低成本升级每个都不复杂但对实际体验提升很大。第一个是把校准值也纳入掉电保存。前面存的只是上下限实际上CAL_DRY和CAL_WET也会随土壤类型变化换盆换土后要重新校准。把这两个值连同阈值一起放进Flash配置页再留一个“长按进入校准模式”的菜单就能做到完全不改代码、不重新烧录就适配新土壤。配置页结构可以从4.3的两字节扩张成结构体但要注意Flash按4字节对齐写入读回时做版本号和魔数双重校验。第二个是把继电器换成N沟道MOS管用PWM控制蠕动泵做滴灌。继电器只有通和断两个状态浇水总是猛地浇透改成PWM后可以先用低占空比慢慢渗水再提高流量。CubeMX里把TIM3通道配成PWM输出主频72MHz、预分频72、自动重装值999得到1kHz PWM代码里一行就能调速__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 300); // 30%占空比低速滴灌占空比先给30%等土壤湿度接近上限再提到60%让水有时间下渗而不是从盆边流走。PWM驱动感性负载时MOS管栅极要串电阻电机两端留续流二极管这些不能省。第三个是接雨量传感器和水位开关做联锁。雨量传感器检测到下雨时自动浇水直接暂停水箱浮球开关检测到低水位时强制所有水泵操作失效并蜂鸣提醒。控制循环里加一个前置判断就够if (HAL_GPIO_ReadPin(RAIN_GPIO_Port, RAIN_Pin) GPIO_PIN_SET || HAL_GPIO_ReadPin(WATER_LOW_GPIO_Port, WATER_LOW_Pin) GPIO_PIN_SET) { pump_off(); g_state SOIL_STATE_WAIT; return; }这套系统做完之后我最大的感受是嵌入式开发的坑多数不在代码而在“你以为已经明白边界条件”。我自己最早做定时版就吃过大亏出差一周回来盆底积水已经发臭。现在养成了一个习惯每次改完逻辑先用串口或OLED把湿度、状态、阈值的当前值打出来观察两个完整的浇水周期再离人传感器失灵时看数据能少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑