简介STM32自动浇花系统是一套面向嵌入式初学者与电子爱好者的完整智能硬件实践项目以STM32F103C8T6为核心融合土壤温湿度、光照、空气温湿度传感器与OLED显示、DC-DC电源、水泵控制等模块实现按湿度阈值或定时策略自动灌溉帮助读者理解从传感器采集到执行控制的完整闭环。压缩包共535个文件约27.77MB以86个C源文件与70个头文件构成主要控制逻辑另含工程配置、PCB设计文档、PDF资料、图片及少量脚本与说明文本覆盖硬件设计、软件开发与调试环节。目前已有3779人学习下载。项目涉及STM32CubeMX初始化、HAL/LL库驱动、GPIO驱动OLED、继电器控制水泵及PCB布局与电源管理等知识点适合作为课程设计、毕业设计或DIY智能养护方案的参考便于读者快速搭建并二次开发。1. 从一盆被浇死的绿萝说起STM32自动浇花系统到底要解决什么很多人第一次做自动浇花脑子里想的是“土壤干了就抽水”听起来简单到不需要方案。但真上手就会发现浇多浇少、什么时候浇、断电了怎么办、水泵会不会干烧每一个都是坑。我见过一个同学传感器插在花盆边缘结果中间土还湿着边缘已经干得发白系统一天浇了八次绿萝直接烂根。这套基于STM32的自动浇花系统要解决的不是“能不能自动浇水”而是“怎么浇得准、浇得稳、浇得安全”。它适合正在做嵌入式毕设或课程设计的学生也适合想给阳台花草做个靠谱养护装置的开发者。整套方案围绕土壤湿度采集、阈值判断、水泵驱动、状态显示和异常保护展开硬件成本可控代码结构清晰拿来就能改。2. 需求拆解与技术选型为什么是STM32加电容式湿度传感器2.1 功能需求与非功能需求先把需求分清楚不然后面选型全是拍脑袋。功能需求通常包括实时采集土壤湿度、根据设定阈值自动启停水泵、本地显示当前湿度与工作状态、支持手动强制浇水、缺水时报警。非功能需求往往被忽略但恰恰是答辩时老师最爱问的采样周期多长、湿度控制精度多少、断电后状态是否保存、水泵连续工作保护时间多久。我一般会把非功能需求写成可验证的指标比如“湿度采样周期不大于2秒”“阈值回差不小于5%防止频繁启停”“单次浇水最长不超过60秒”。这些指标直接决定后面代码里定时器和状态机的设计。2.2 主控与传感器选型对比主控选STM32F103C8T6是这类项目的常见做法原因很实际72MHz主频足够跑裸机或轻量RTOS片上ADC多通道定时器资源丰富价格低且资料多。传感器方面电阻式湿度传感器便宜但容易电解腐蚀长期插在土里几个月就报废电容式传感器不直接暴露电极寿命长输出模拟电压与湿度线性度更好。水泵驱动不能用IO口直接推必须加继电器或MOS管模块同时反向并联续流二极管保护。显示用0.96寸OLED比LCD1602省IO口I2C两线搞定。下面这张表是我在方案里常用的选型对照可以直接放进设计文档。模块选型关键参数备注主控STM32F103C8T672MHz64KB Flash20KB RAM最小系统板即可湿度采集电容式土壤湿度传感器输出0-3V模拟量3.3-5V供电需校准水泵驱动5V继电器模块或AO3400 MOS触点容量10A驱动电流1A加续流二极管显示0.96寸OLED SSD1306I2C接口128x643.3V供电供电12V/2A适配器LM2596降压输出5V和3.3V水泵单独走12V注意水泵和主控不要共用一路稳压电机启动瞬间压降会把MCU拉复位这是血泪经验。2.3 系统总体框图与数据流整个系统按“采集—决策—执行—反馈”闭环设计。湿度传感器输出模拟电压STM32的ADC1_IN0通道每2秒采样一次经过滑动平均滤波后换算成百分比。主循环里拿当前湿度与设定阈值比较低于下限且不在浇水冷却期就拉高继电器控制脚启动水泵高于上限或达到单次最长浇水时间立即关闭。OLED每500ms刷新一次显示湿度值、阈值、水泵状态和报警标志。按键用于切换手动/自动模式和调整阈值。数据流不复杂但每个环节都要加保护ADC采样要滤除野值继电器动作要加死区时间水泵关闭后要强制冷却30秒再允许下一次启动。3. 硬件设计与驱动实现从ADC采样到继电器动作的完整链路3.1 最小系统与电源分区硬件部分最容易翻车的是电源。STM32最小系统板用3.3VOLED和传感器也用3.3V但水泵是12V/1A级别的负载。我一般把电源分成三路12V直接给水泵12V经LM2596降到5V给继电器线圈和传感器5V再经AMS1117-3.3降到3.3V给MCU和OLED。三路共地但走线时大电流路径和信号路径分开继电器模块的输入端加0.1uF去耦电容。如果水泵功率更大继电器换成MOS管低边驱动栅极加10k下拉电阻防止上电误动作。这些细节不写进代码但决定了系统能不能稳定跑一整天。3.2 ADC多通道采样与滑动平均滤波湿度采集的核心是把ADC原始值变成可信的百分比。电容式传感器输出0-3VSTM32的ADC参考电压3.3V12位分辨率理论精度0.8mV。但土壤湿度变化慢ADC读数会跳直接拿来比较阈值会导致水泵频繁启停。我通常用长度8的滑动平均滤波再配合中值滤波剔除突变。下面这段代码是ADC采样和滤波的核心逻辑可以直接移植。// ADC1_IN0 土壤湿度采集与滑动平均滤波 #define FILTER_LEN 8 static uint16_t adc_buf[FILTER_LEN] {0}; static uint8_t adc_idx 0; uint16_t get_soil_adc(void) { uint16_t raw 0; // 启动ADC转换并等待完成 HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { raw HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); // 滑动窗口更新 adc_buf[adc_idx] raw; adc_idx (adc_idx 1) % FILTER_LEN; // 求和取平均 uint32_t sum 0; for (uint8_t i 0; i FILTER_LEN; i) { sum adc_buf[i]; } return (uint16_t)(sum / FILTER_LEN); } // 将ADC值换算为湿度百分比需根据校准值修改 // 传感器在空气中ADC约2800浸水中约1200 uint8_t adc_to_percent(uint16_t adc) { const uint16_t adc_dry 2800; const uint16_t adc_wet 1200; if (adc adc_dry) return 0; if (adc adc_wet) return 100; return (uint8_t)((adc_dry - adc) * 100 / (adc_dry - adc_wet)); }这段代码里FILTER_LEN决定平滑程度8个点对应约16秒的窗口湿度变化慢够用。adc_dry和adc_wet必须实测校准把传感器放空气中读一个值插进水里读一个值替换掉常量。注意ADC采样不要放在中断里做裸机主循环轮询即可否则中断嵌套容易出玄学问题。3.3 继电器驱动与水泵保护逻辑水泵控制看着简单但干烧和频繁启停是两大杀手。干烧保护靠湿度下限判断如果湿度已经低于5%且水泵连续工作超过60秒强制关闭并报警说明水箱没水了。频繁启停靠回差和冷却时间解决启动阈值设为30%停止阈值设为45%中间15%的回差避免在阈值附近抖动每次关闭后强制冷却30秒期间即使湿度低于下限也不启动。下面这段状态机代码把保护逻辑串起来。// 水泵控制状态机 typedef enum { PUMP_IDLE 0, PUMP_RUNNING, PUMP_COOLDOWN, PUMP_ERROR } pump_state_t; static pump_state_t pump_state PUMP_IDLE; static uint32_t pump_start_tick 0; static uint32_t pump_stop_tick 0; #define HUMIDITY_LOW 30 // 启动阈值 % #define HUMIDITY_HIGH 45 // 停止阈值 % #define PUMP_MAX_MS 60000 // 单次最长60秒 #define COOLDOWN_MS 30000 // 冷却30秒 void pump_control(uint8_t humidity) { uint32_t now HAL_GetTick(); switch (pump_state) { case PUMP_IDLE: if (humidity HUMIDITY_LOW) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_SET); pump_start_tick now; pump_state PUMP_RUNNING; } break; case PUMP_RUNNING: // 达到停止阈值或超时关闭水泵 if (humidity HUMIDITY_HIGH || (now - pump_start_tick) PUMP_MAX_MS) { HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_RESET); pump_stop_tick now; pump_state PUMP_COOLDOWN; } break; case PUMP_COOLDOWN: if ((now - pump_stop_tick) COOLDOWN_MS) { pump_state PUMP_IDLE; } break; case PUMP_ERROR: HAL_GPIO_WritePin(PUMP_GPIO_Port, PUMP_Pin, GPIO_PIN_RESET); break; } }状态机的好处是每个状态只做一件事逻辑清晰答辩时画个状态转移图就能讲明白。HUMIDITY_LOW和HUMIDITY_HIGH根据实际土壤和植物调整多肉植物可以设低一点蕨类设高一点。PUMP_MAX_MS是干烧保护的最后一道防线即使传感器卡死也不会一直抽水。4. 软件架构与关键模块裸机前后台还是上RTOS4.1 任务划分与调度周期这个项目规模不大裸机前后台架构足够没必要上RTOS增加复杂度。我一般把任务按周期分三档ADC采样和滤波每2秒一次水泵状态机每100ms跑一次OLED刷新和按键扫描每500ms一次。主循环里用HAL_GetTick()做时间片轮询不用阻塞延时。这样即使某个任务偶尔超时也不会影响其他任务。如果非要用RTOSFreeRTOS的任务优先级这样分湿度采集任务优先级2控制任务优先级3显示任务优先级1堆栈各128字。但裸机方案代码量少一半调试也简单毕设够用。4.2 阈值存储与掉电保护阈值和校准参数不能每次上电都重新设通常存在STM32内部Flash的最后一页。写Flash要注意先解锁擦除整页再按半字写入最后上锁。频繁写Flash会缩短寿命所以只在按键确认修改时写一次不要每次循环都写。下面这段代码是Flash读写的最小实现。// 内部Flash保存阈值参数STM32F103最后一页 #define FLASH_SAVE_ADDR 0x0801FC00 #define FLASH_MAGIC 0x5A5A typedef struct { uint16_t magic; uint8_t hum_low; uint8_t hum_high; uint16_t adc_dry; uint16_t adc_wet; } config_t; void config_save(config_t *cfg) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase {0}; uint32_t page_error 0; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress FLASH_SAVE_ADDR; erase.NbPages 1; HAL_FLASHEx_Erase(erase, page_error); uint16_t *p (uint16_t *)cfg; for (uint16_t i 0; i sizeof(config_t) / 2; i) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, FLASH_SAVE_ADDR i * 2, p[i]); } HAL_FLASH_Lock(); } void config_load(config_t *cfg) { memcpy(cfg, (void *)FLASH_SAVE_ADDR, sizeof(config_t)); if (cfg-magic ! FLASH_MAGIC) { // 首次上电加载默认值 cfg-magic FLASH_MAGIC; cfg-hum_low 30; cfg-hum_high 45; cfg-adc_dry 2800; cfg-adc_wet 1200; config_save(cfg); } }FLASH_MAGIC用来判断是否第一次上电避免读到全0xFF的空白页。config_load里如果魔数不对就写默认值并保存。注意Flash写之前一定要擦除擦除单位是页STM32F103C8T6的页大小是1KB地址0x0801FC00是最后一页的起始。写的时候按半字对齐结构体大小最好是2的倍数。4.3 OLED显示与按键交互显示部分用SSD1306驱动I2C地址0x78。界面上排显示当前湿度百分比和阈值范围中排显示水泵状态运行/停止/冷却/报警下排显示模式自动/手动。按键用两个K1切换模式K2在手动模式下强制浇水。按键扫描要加20ms消抖用状态机实现不要用HAL_Delay阻塞。交互逻辑上手动模式优先级最高但干烧保护依然生效这是安全底线。5. 避坑与常见问题排查那些让系统半夜抽水的坑5.1 传感器读数漂移或跳变现象OLED上湿度值一会儿30%一会儿80%水泵跟着频繁启停。原因通常是传感器供电不稳或ADC参考电压波动也可能是传感器插在土里深度不够接触不良。解决先检查传感器供电是否和MCU共用3.3V如果是加一个100uF电解电容在传感器电源脚再检查传感器插入深度金属部分要完全埋入土中但不要碰到花盆壁。软件上把滑动平均窗口从8加到16同时加一个变化率限制单次采样值与前一次相差超过20%就丢弃。5.2 水泵启动导致MCU复位现象水泵一启动OLED黑屏MCU重启串口打印乱码。原因是电机启动瞬间电流冲击12V电源被拉低导致5V和3.3V稳压输出跌落。解决水泵电源和MCU电源分开走线从适配器出来后先经过大电容1000uF再分两路继电器线圈两端加续流二极管如果还不行给MCU的3.3V加一个100uF电容缓冲。我一般还会在软件里加一个“水泵启动后延时100ms再读ADC”的规则避开干扰窗口。5.3 阈值附近频繁启停现象湿度在30%上下波动水泵每隔几秒就开一次关一次继电器咔咔响。原因是只有单阈值判断没有回差。解决设置启动阈值和停止阈值中间留10%到15%的回差。比如低于30%启动高于45%停止。同时加冷却时间水泵关闭后30秒内不允许再次启动。这两个参数配合基本能消除抖动。5.4 干烧保护失效现象水箱没水了水泵还在转发出空转噪音时间长了泵体发热。原因是只靠湿度判断传感器在空气中读数可能仍然高于下限。解决加双重保护。第一重是单次最长运行时间60秒超时强制关闭第二重是湿度低于5%时直接进入错误状态不再启动。另外可以在水箱里加一个水位开关接在MCU的另一个IO口上低电平表示缺水这个成本很低但可靠性高。5.5 Flash参数丢失或乱码现象重新上电后阈值恢复默认或者OLED显示乱码。原因是Flash擦写过程中断电或者结构体对齐问题导致读写地址错位。解决写Flash时先擦除再写写完后回读校验结构体加__attribute__((packed))避免编译器填充上电时如果魔数不对不要直接覆盖先加载默认值到RAM等用户确认后再保存。另外不要在中断里写FlashFlash操作期间CPU会暂停影响其他任务。6. 验收与答辩怎么证明这套浇花系统真的靠谱验收不是把代码烧进去看水泵转不转就完了。我一般会按下面这张表逐项测每项都有明确的通过标准答辩时直接拿数据说话。测试项测试方法通过标准湿度采集精度传感器放空气中、水中、湿土中对比标准值误差不超过5%自动浇水逻辑湿度从50%降到25%观察水泵启动低于30%启动高于45%停止回差与冷却湿度在30%附近波动不出现1分钟内启停超过2次干烧保护传感器放空气中手动强制浇水60秒内自动关闭并报警掉电保存修改阈值后断电再上电阈值与修改前一致连续运行系统通电24小时无复位、无死机、无异常抽水答辩时老师最爱问三个问题为什么选电容式不选电阻式回差和冷却时间怎么定的如果传感器坏了系统会怎样前两个上面已经讲清楚了。第三个问题我的做法是在代码里加一个传感器合理性检查如果ADC读数长时间不变比如连续10次采样值完全相同判定传感器故障关闭水泵并报警。这个逻辑不复杂但能体现你对可靠性的考虑。还有一个技巧答辩现场不要只放PPT把实物带过去现场演示从干燥到浇水的完整过程。提前把土壤弄干一点让老师看到水泵启动、OLED刷新、湿度上升的全过程。如果条件允许准备两套阈值参数一套给多肉一套给绿萝现场切换展示灵活性。最后把关键代码和测试数据打印出来老师翻到哪一页你都能讲出设计意图。从那以后我每次做嵌入式项目都会在验收前强制走一遍“断电—上电—异常注入”的流程因为现场演示翻车的代价太大了。希望这套方案能帮你把自动浇花系统做扎实答辩顺利。本文还有配套的精品资源点击获取