简介基于STM32F103C8T6的自动浇花系统完整工程包面向嵌入式初学者、电子设计与物联网爱好者解决植物养护中土壤湿度监测与自动补水控制问题。系统集成土壤温湿度、光敏、空气温湿度多类传感器配合OLED显示与水泵继电器控制可依据环境参数自动决策浇水时机。压缩包共535个文件容量27.77MB以C/H源码为主86个c、70个h并包含工程配置、PCB设计图、原理图PDF、编译生成的hex固件及使用文档可覆盖从硬件设计、驱动移植到整机调试的完整过程。目前已有3777人学习下载。通过该项目可掌握STM32CubeMX初始化、HAL库外设驱动、ADC采集传感器数据、OLED显示界面及基于阈值的灌溉策略编写资料中目录结构清晰便于按模块拆解学习是动手实践智能硬件项目的优质参考。1. 一个单片机项目为什么值得认真做完种了三年花之后你会发现最折磨人的不是买苗、换盆、配土而是出远门那几天。北方夏季暴晒下阳台湿度半小时就能从舒适区间掉到植物的生存底线。网上几十块钱的浇花器逻辑基本是「定时定量」或者「缺水就喷一会儿」既不认识你的土也不认识你的植物。我最后一次因为出差毁掉一盆绣球之后决定用手里闲置的STM32开发板自己搭一套自动浇花系统。做一个能读取土壤湿度的闭环比任何定时器都靠谱——土壤说渴了再浇而不是你猜它渴了。这个标题指向的是一个完整的小型闭环系统核心是「土壤湿度感知 → ADC采样 → 阈值判定 → 控制继电器 → 驱动水泵」。它不需要Linux不需要云平台一块几十块钱的STM32最小系统板加几个传感器、一个继电器和一个水泵就能落地。适合刚学完GPIO和中断、想做一个「能拿得出手」的综合练手项目的人也适合有点基础但没做过闭环控制细节的工程师。这类项目真正值钱的地方不在「能浇花」而在「怎么判断该不该浇」和「浇多久停」而这恰好是后面几章要展开的内容。2. 系统拆解与选型从传感器到执行器每一环都别想当然2.1 土壤湿度传感器电阻式与电容式差距比你想的大市面上一块多钱的土壤湿度传感器模块绝大多数是电阻式两根金属探针插入土中利用土壤含水量不同导致导电率不同来输出电压。便宜、电路简单但也有个让很多人翻车的毛病——探针直接接触土壤中的水与电解质施加直流电压时会产生极化现象。连续通电几小时后探针表面会附着离子等效电阻漂移读数会慢慢「假干」。关键点在于不要用连续供电的方式采集这类传感器而是每次采集前先通过GPIO给传感器供电延时几十毫秒等输出电压稳定读完ADC立刻断电。这个「短时供电」的习惯能让传感器寿命从月级别提升到年级别。电容式传感器则好得多它两侧是铜箔中间是绝缘层测的是土壤介电常数变化和导电性无关不容易极化漂移也小。价格贵三五倍但对精度和长期稳定性有要求的人我一般直接推电容式。至于埋入深度两个传感器各买一个在同一个花盆里对比了一个星期电阻式读数波动能到正负15%电容式大概在正负5%以内。这个差距在做阈值判断的时候很关键。2.2 水泵与继电器直接GPIO驱动是新手翻车第一站微型潜水泵的启动电流通常是正常工作电流的3到5倍。一个小型3W水泵正常工作电流大约300mA但刚上电那瞬间能到1A以上。STM32的GPIO能提供多大电流实际工程安全值一般不超过20mA直接驱动轻则复位重则烧管脚。所以「MCU GPIO → 三极管 → 继电器 → 水泵」这条链路基本是标准做法或者用MOSFET模块代替继电器。继电器选型同样有讲究。常见的光耦隔离继电器模块输入侧有个PNP三极管和限流电阻低电平触发和高电平触发都能买到用之前看一眼丝印或者原理图。我一般建议选高电平触发版本因为STM32复位期间所有GPIO默认是浮空或低电平如果是低电平触发系统一上电或者一复位继电器就会误动作水泵直接开始浇花。高电平触发则天然安全程序没跑起来之前GPIO不会输出高水泵不会动。水泵本身也要挑潜水泵分有刷和无刷有刷的便宜但碳刷磨损后有杂质用久了流量下降无刷的安静且寿命长价格高。对一个自动浇花系统来说建议选DC 5V或12V的无刷潜水泵。5V版本可以和控制电路共用一路5V电源前提是电源余量足够12V版本需要单独的电源轨但启动瞬间对MCU的电源干扰会小很多因为物理上分开了。2.3 STM32最小系统与电源稳定不出错的底线STM32型号选择不需要纠结F103C8T6绰绰有余两个ADC、四个定时器、足够的GPIO、一路I2C跑一个浇花逻辑连一半资源都用不到。如果你要接OLED屏显示状态并通过按键切换模式也够用。如果想把数据存下来或者定期上报F103有硬件I2C和UART挂ESP8266或者EEPROM都是成熟的方案。电源是整个系统里最容易出「玄学问题」的地方。一个常见的配置是USB供5V → 给水泵和传感器 → 通过AMS1117-3.3稳压给STM32。问题在于水泵启动瞬间会把5V母线拉低AMS1117的压差可能不够输出电压跟着掉MCU复位或者ADC采样值突然跳变。处理方式有两种水泵用独立电源或者5V电源余量足够大。我后来一类方案是5V 2A适配器供电水泵峰值电流1A、MCU加OLED峰值100mA余量还算健康。如果你只有一个USB口供电务必在5V母线上加一个大电解电容470μF以上并在AMS1117前后各加一个0.1μF陶瓷电容否则你会体验到「一浇花就重启」的血泪。器件推荐型号/规格关键参数注意点主控STM32F103C8T664脚128KB Flash2路ADC土壤湿度传感器电容式如某传感器模块输出0~3.3V模拟量探针不极化继电器模块光耦隔离、高电平触发触点电流至少5A带泄放二极管水泵5V/12V无刷微型潜水泵峰值电流按3A选电源不按额定电流稳压AMS1117-3.3输入电压5V最大输出1A注意功率散热电源5V/2A适配器给水泵和MCU共用余量≥50%3. 把硬件点亮从CubeMX建工程到传感器读数3.1 CubeMX工程配置与初始化参数无论你用什么IDE第一步都是用STM32CubeMX把引脚和时钟初始化生成出来省去手动写寄存器的时间和出错概率。常见配置是外部8MHz晶振 → PLL倍频到64MHz下载方式选SWD留两个引脚给串口做调试输出。土壤湿度传感器的模拟输出接ADC1的通道0PA0继电器控制脚接一个GPIO输出PB1按键接PB0外部中断或轮询OLED或LCD接I2C1PB6/PB7。在CubeMX里需要做的事项PA0设置为Analog模式这样ADC采样路径没有数字上下拉的干扰PB1设置为GPIO_Output初始电平设为Low防止上电误触发PB0设置为GPIO_Input开启内部上拉按键按下接地I2C1速率选Standard Mode100kHz对OLED来说绝对够了生成工程后在main.c的while(1)循环之前加入ADC校准相关调用否则有些型号第一次采样偏差能大到十几个ADC值。HAL库提供了校准函数在MX_ADC1_Init()之后调用/* ADC校准每次上电后都做一次消除内部电容差异导致的采样偏差 */ HAL_ADCEx_Calibration_Start(hadc1);这段代码放在外设初始化之后、主循环之前。参数上没什么可以调的必须调用。调用不调用的差别在F103上大约能差出十几个LSD做土壤湿度这种十几个ADC数值就代表百分之几含水量的场景必须校准。3.2 ADC读取土壤湿度去抖与归一化ADC读取最基础的做法是HAL_ADC_Start()→HAL_ADC_PollForConversion()→HAL_ADC_GetValue()。但单次采样直接用于判断是否浇水会迅速让你怀疑人生浇水瞬间水滴飞溅到传感器周围读数可能从干燥值直接跳到一个更干的值触发误判。我在代码里做了两层处理短时供电去抖 中值滤波。中值滤波在这里的效果比均值滤波好因为土壤湿度读数的噪声是脉冲型的——水滴溅到探针、根系碰到探针都会产生尖峰均值滤波会被尖峰拉走中值滤波能直接把异常点丢掉。/* 传感器短时供电采集每次采集前为传感器供电稳定后采样采集完断电 */ void soil_moisture_power(uint8_t on) { HAL_GPIO_WritePin(SENSOR_PWR_GPIO_Port, SENSOR_PWR_Pin, on ? GPIO_PIN_SET : GPIO_PIN_RESET); } uint32_t adc_read_single(void) { uint32_t val 0; HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { val HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); return val; } uint32_t adc_read_median(uint8_t sample_cnt) { soil_moisture_power(1); HAL_Delay(50); /* 上电后等50ms让传感器输出稳定 */ uint32_t buf[7]; for (uint8_t i 0; i sample_cnt i 7; i) { buf[i] adc_read_single(); HAL_Delay(10); } soil_moisture_power(0); /* 采集完立刻断电缓解极化 */ for (uint8_t i 0; i sample_cnt; i) { for (uint8_t j i 1; j sample_cnt; j) { if (buf[j] buf[i]) { uint32_t tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } } } return buf[sample_cnt / 2]; }sample_cnt通常取5就是采集5次取中值。HAL_Delay(50)是关键参数电阻式传感器模块上电后输出电压需要一段时间建立50ms是安全值电容式传感器通常更快20ms也够。取中值时排序开销可以忽略浇花系统采样频率本来就不高一分钟一次CPU几乎不占。3.3 按键与LCD显示让系统状态可见按键这里有一个需要处理的细节机械按键抖动会带来几十毫秒的电平抖动直接读取可能一次按下变成多次触发。常见的做法是加RC滤波或者软件消抖。软件消抖的做法是每次检测到电平变化后延时20~50ms再确认一次电平确认一致才算有效按键。再配合「松开才触发」的逻辑下降沿触发按键按下置标志松开执行动作可以避免长按误操作。OLED显示让整个系统状态可控可见当前湿度百分比、土壤是否干燥、上一次浇水时间、模式自动/手动。这里有两个实用细节。第一OLED画点刷新整个屏幕会闪烁我用局部刷新湿度数值变才更新数值区域模式切换才更新图标区域。另一个细节是ADC值到湿度百分比的归一化涉及到标定见第四章。/* 湿度百分比换算把ADC值映射到干-湿两个标定点 */ uint8_t moisture_to_percent(uint32_t adc_val, uint32_t dry_val, uint32_t wet_val) { if (adc_val wet_val) return 100; /* 完全泡在水里当作100%含水量 */ if (adc_val dry_val) return 0; /* 空气中值当作0%含水量 */ return (uint8_t)(100UL * (dry_val - adc_val) / (dry_val - wet_val)); }dry_val是传感器在干燥空气中读到的ADC值wet_val是传感器插在水里完全湿润时读到的ADC值。用一个固定百分比乘以ADC区间得到的就是一个相对含水量。这段代码说明了一个细节dry_val和wet_val必须在同一个供电电压和同一次硬件部署下标定换一块电池或者换一个传感器数值就失效。这也是我第五章会展开的「标定」问题。4. 自动浇花的判定逻辑与参数整定湿度阈值、迟滞与定时4.1 阈值判定为什么会让水泵频繁启停最常见的自动浇花逻辑是if (moisture_percent 30) 浇水。这个逻辑能跑但稍微观察一下就会发现问题——土壤从被浇透到变干是一个缓慢的滑坡但如果阈值设得不当或者采样间隔太短会出现「浇完5秒湿度升到60没过多久又掉回29水泵又启动」的振荡。某些情况下水泵频繁启停继电器触点寿命消耗极快无刷水泵倒是能扛但继电器模块的机械触点几千次启停就可能出问题。解决方式之一是引入迟滞hysteresis湿度低于阈值A才开启浇水但要等到湿度高于阈值B才停止浇水。A和B之间留出一个区间比如A30、B60。这样采样到的湿度在30~60之间时系统保持原状态不动作避免靠近单一阈值时反复横跳。4.2 迟滞区间与浇灌时长参数用一次浇透代替多次洒水你还需要回答「浇多久」这个问题。设定合理的浇灌时长比设定精确的湿度阈值更影响效果。浇水时间短了水只渗入表层传感器埋得浅读到湿但根系深处还是干的很快又触发浇水形成表层湿润深层干旱的恶性循环。最好的方案是每次浇水持续足够长时间让水分渗透到花盆底部然后等土壤整体干燥。代码中我定义了一个浇水次数计数和单次浇水时长单次浇完会强制等待一个「静置时间」让水分渗透扩散/* 自动浇水主流程迟滞判定 单次浇透时长 强制静置 */ #define SOIL_DRY_THRESHOLD 30 /* 低于此值启动浇水 */ #define SOIL_WET_THRESHOLD 60 /* 高于此值停止浇水 */ #define PUMP_RUN_SECONDS 25 /* 单次浇水最长持续时间 */ #define SOAK_WAIT_SECONDS 40 /* 浇水后强制静置时间让水分扩散 */ void auto_water_task(void) { if (manual_mode) return; /* 手动模式下不参与自动逻辑 */ uint32_t moist read_smoothed_moisture(); uint8_t percent moisture_to_percent(moist, cal_dry, cal_wet); switch (water_state) { case STATE_IDLE: if (percent SOIL_DRY_THRESHOLD) { water_state STATE_WATERING; pump_on(); pump_timer HAL_GetTick(); } break; case STATE_WATERING: if (percent SOIL_WET_THRESHOLD) { /* 湿度已达标提前停泵 */ pump_off(); water_state STATE_SOAK_WAIT; soak_timer HAL_GetTick(); } else if (HAL_GetTick() - pump_timer PUMP_RUN_SECONDS * 1000) { /* 到达最长浇灌时间强制停泵 */ pump_off(); water_state STATE_SOAK_WAIT; soak_timer HAL_GetTick(); } break; case STATE_SOAK_WAIT: if (HAL_GetTick() - soak_timer SOAK_WAIT_SECONDS * 1000) { water_state STATE_IDLE; } break; } }三个参数之间需要配合SOIL_WET_THRESHOLD60和PUMP_RUN_SECONDS25是二选一或互补的——如果土壤本来就接近湿可能浇2秒湿度就超过了60走提前停泵分支如果土壤很干浇到25秒湿度还没到60强制停泵保护水泵空转或溢水。SOAK_WAIT_SECONDS40是强制冷静期防止水还没渗下去就误判「还很干」。这里有个需要理解的细节浇完立刻采样传感器周围的土含水量极高读数应该贴近100%。但如果你把传感器埋在土层中部浇水后水分往下渗传感器位置要等一会儿才变湿相反如果传感器埋得浅表层干得慢深处已经缺水系统会「觉得不干」。所以传感器的埋深就是系统的判断视界。我一般埋在距表层3-5cm处这个深度既避开表层干湿的剧烈波动又能比根系深处更早反映缺水趋势。4.3 ADC转换误差怎么处理多点采样、中值滤波与标定曲线ADC的12位精度在STM32F103上理论上有4096级但实际上的噪声——电源纹波、传感器模拟输出本身的波动、PCB走线耦合——会让特定的个别ADC码出现偏移。处理思路有三层第一层是用前面提到的中值滤波把粗大误差滤掉。第二层是采集时间应当避开继电器动作的瞬间。继电器吸合瞬间会产生电磁干扰电流突变会通过电源和空间耦合进ADC通道。所以采样应当在泵停止、继电器状态稳定后至少50ms再进行不要在浇水刚停止的瞬间立刻采样。第三层是标定曲线。行业数据表明土壤湿度传感器输出和含水量在中间区间接近线性但在极干和极湿两端有明显饱和。将两端的饱和现象考虑到映射逻辑中采用分段线性插值比单一直线映射更贴近真实值。标定方法拿一个花盆土烘干后测一次读数记为dry_val再把土完全泡水、搅拌均匀、静置30分钟后测一次读数记为wet_val。这两个数据是死的数据不用变。接着可以用一个水杯做中间点验证把传感器插入含水量约50%的土中看读数是否落在中点附近。如果中点偏差超过10%可以采用分段线性校正。我常用的做法是记录两个标定点再配一个中点偏移用三段折线拟合效果远好于「干—湿拉满」的直线。5. 调试与避坑湿度跳变、水土流失、传感器极化、水泵不回水5.1 传感器在水里泡一天读数反而最干现象传感器的读数在刚插入很湿的土壤时是「湿」值比如500第二天再读却变成了「干」值比如2000但土壤看起来明明还很湿。原因这就是电阻式传感器极化问题的典型表现。探针上有直流电流过水中的离子逐渐迁移在探针表面形成了一层极化层等效电阻上升输出信号向「干」的方向漂移。更糟的是这层极化不会因为断电短时间消失设备重启也没用。解决换电容式传感器是根治。如果项目只能用电阻式传感器唯一的缓解方案是所有采集都按第三章方式做短时供电每天累计通电时间控制在秒级同时传感器不要长期泡在积水中花盆底部要有透水层传感器不要放在盆底积水区。.potdash.我见过有人买通排水孔装传感器传感器长期泡在水里读数一星期内彻底失真就是这个极化问题。5.2 继电器吸合瞬间把MCU拉复位现象水泵一启动OLED闪了一下系统重启湿度数据清零有时还会进入一个死循环无法恢复只能拔电。原因继电器线圈吸合瞬间的电流突变和反电动势通过电源线路反馈到MCU同时水泵启动的浪涌电流把5V母线拉到4.4V以下AMS1117输出跟着掉到3.3V以下。MCU欠压复位是结果反电动势是帮凶。解决第一步确认继电器模块已经接好5V和GND不要在MCU板上取电。第二步在继电器线圈两端反向并联一个1N4007二极管阴极接VCC阳极接控制端有效泄放断开瞬间的反电动势。第三步5V母线上加470μF电解电容。第四步是最彻底的水泵电源与控制电源完全分开用12V或24V电压驱动水泵MCU系统单独用5V供电只通过继电器或光耦传递控制信号。我做过的系统里这种「电源隔离」一次解决所有复位问题。5.3 ADC悬空读数漂移不接土壤时显示50%湿度的玄学现象传感器没插到土里屏幕上湿度百分比却不显示0%而是40%~60%之间跳。原因PA0如果是浮空输入引脚电压由内部漏电流决定测出来的ADC值就是随机的。另一个原因是CubeMX里PA0没有设置为Analog模式而是GPIO_Input这种情况下内部上下拉和施密特触发器会影响ADC值。解决在CubeMX里把传感器引脚明确设为Analog确认没有使能内部上下拉。同时硬件上在传感器信号线和GND之间加一个10kΩ下拉电阻确保传感器未接入时读到的是0V而不是悬空噪声。STM32的ADC输入阻抗不算特别高10kΩ的下拉和传感器模块的输出阻抗并联后影响可以忽略。5.4 浇水之后土壤湿度先降后升怎么处理现象浇水停止后第一次采样湿度从60%直接降到20%再过半小时回到55%。看到这个数值波动第一反应是传感器坏了实际上没有。原因水分渗透过程。浇下去的水先在土壤表层聚积然后由于重力向下渗。传感器的周围土壤湿度在浇水后瞬间很高但在水分向下运动期间传感器位置的毛细水量暂时减少读数会短暂回落直到周围土壤重新达到水分平衡。解决这不属于故障不必为它增加逻辑复杂性。正确的手法是在浇水停止后进入STATE_SOAK_WAIT强制等待一段时间期间不采样、不判定直接将读数冻结为上一次稳定值。同时采样频率降为每5分钟一次为水分扩散留出时间。如果等不及 10 分钟就想看结果结果就是把自己吓一跳。5.5 水泵停水不泵水水管那口气回不去了现象水泵正常转但水量极小或不出水。原因微型潜水泵的进水管如果离水面太近或者软管里面有气停机后水管里的水会倒流回容器中下次启动需要重新抽气。潜水泵的标准工作状态是淹没在液体中的自吸状态一旦液面低于进水口或者管路中有气栓就抽不上来。解决把水泵固定在容器底部始终淹没进水口。出水管保持向上走势不要有下垂的U型管段积聚气泡。如果管路确实很长可以在出水管靠近泵的顶端开一个小孔做成「破坏虹吸」结构。这个方法看似简单但能防止停机后水顺着管路自然流走下次启动时不用重新建立水柱。6. 让系统更聪明低功耗值班、掉电阈值保存与手动接管当上述系统稳定运行两周之后你会意识到还有三件真正要紧的事需要处理掉电后的标定数据会丢湿度参数每次上电都要重新设以及夜间浇水时亮着的OLED灯本身就像一个bug。这三个问题的解决办法都不复杂但它们决定了这个系统是否「能脱离电脑独立活着」。第一件是掉电保存。cal_dry、cal_wet、dry_threshold这些标定值和阈值放在RAM里断电即失。用STM32F103内置的Flash做掉电保存是常见做法擦写一个扇区1KB把标定结构体整体写入每次上电先读Flash再跑逻辑。需要注意Flash写之前必须擦除而且擦写寿命大约一万次所以只能在标定完成时写一次不能在主循环里反复写。如果没有外挂EEPROM系统完全可以自己解决这个问题。第二件是低功耗值班。浇水系统的实时性要求极低醒来采样一次、执行一次判断再回去待机平均电流可以做到非常低。用的是WFI等待中断或STOP模式用RTC闹钟定时唤醒醒来做一轮完整逻辑再睡。我实测过一组数据F103在STOP模式下电流约19μARTC秒走时加闹钟大约5μA主循环每次醒来工作1秒左右平均下来一个月耗电用两节18650电池就能扛。注意进入STOP前要把ADC外设和传感器电源都关掉否则外设还在耗电就白睡了。第三件是手动接管。做给家人用的时候会发现他们对「自动判断」有天然的不信任感。加一个硬件开关或按键长按切换手动模式手动模式下自动逻辑完全失效只执行「按键浇水/停止」。这个功能几乎不占代码量但它决定了这套系统能否在家庭环境中被接受。我在主循环里加了一个manual_mode标志用长按2秒切换并在OLED上显示当前模式初版给家人用后来的日子里几乎没人切换过——自动模式在标定正确时确实比人可靠。这个系统做下来让我印象最深刻的不是代码或电路而是「传感器读数的可信度决定了整个系统的智商」。前面讲的标定、滤波、去抖、电源隔离说到底都是为了一件事让ADC值真实反映土壤含水量。数据可信了判断逻辑再简单也不会出错数据是玄学算法再复杂也只是在放大噪声。我后来把整套思路复用到养鱼水位监控和育苗温湿度的项目上发现相同的方法论全部成立。希望这整套从选型、接线到标定、避坑、低功耗的路径能帮到你也期待你把浇花系统换成其他植物环境后发现新坑的时候能回来验证一下这些经验是否适用。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?