简介:基于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陶瓷电容,否则你会体验到「一浇花就重启」的血泪。
| 器件 | 推荐型号/规格 | 关键参数注意点 |
|---|---|---|
| 主控 | STM32F103C8T6 | 64脚,128KB Flash,2路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的通道0(PA0),继电器控制脚接一个GPIO输出(PB1),按键接PB0(外部中断或轮询),OLED或LCD接I2C1(PB6/PB7)。
在CubeMX里需要做的事项:
- PA0设置为Analog模式,这样ADC采样路径没有数字上下拉的干扰
- PB1设置为GPIO_Output,初始电平设为Low,防止上电误触发
- PB0设置为GPIO_Input,开启内部上拉,按键按下接地
- I2C1速率选Standard Mode(100kHz),对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之间留出一个区间,比如A=30、B=60。这样采样到的湿度在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_THRESHOLD=60和PUMP_RUN_SECONDS=25是二选一或互补的——如果土壤本来就接近湿,可能浇2秒湿度就超过了60,走提前停泵分支;如果土壤很干,浇到25秒湿度还没到60,强制停泵保护水泵空转或溢水。SOAK_WAIT_SECONDS=40是强制冷静期,防止水还没渗下去就误判「还很干」。
这里有个需要理解的细节:浇完立刻采样,传感器周围的土含水量极高,读数应该贴近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μA,RTC秒走时加闹钟大约5μA,主循环每次醒来工作1秒左右,平均下来一个月耗电用两节18650电池就能扛。注意:进入STOP前要把ADC外设和传感器电源都关掉,否则外设还在耗电,就白睡了。
第三件是手动接管。做给家人用的时候会发现,他们对「自动判断」有天然的不信任感。加一个硬件开关或按键长按切换手动模式,手动模式下自动逻辑完全失效,只执行「按键浇水/停止」。这个功能几乎不占代码量,但它决定了这套系统能否在家庭环境中被接受。我在主循环里加了一个manual_mode标志,用长按2秒切换,并在OLED上显示当前模式,初版给家人用,后来的日子里几乎没人切换过——自动模式在标定正确时确实比人可靠。
这个系统做下来让我印象最深刻的不是代码或电路,而是「传感器读数的可信度决定了整个系统的智商」。前面讲的标定、滤波、去抖、电源隔离,说到底都是为了一件事:让ADC值真实反映土壤含水量。数据可信了,判断逻辑再简单也不会出错;数据是玄学,算法再复杂也只是在放大噪声。我后来把整套思路复用到养鱼水位监控和育苗温湿度的项目上,发现相同的方法论全部成立。希望这整套从选型、接线到标定、避坑、低功耗的路径能帮到你,也期待你把浇花系统换成其他植物环境后,发现新坑的时候能回来验证一下这些经验是否适用。
本文还有配套的精品资源,点击获取