简介: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两线搞定。下面这张表是我在方案里常用的选型对照,可以直接放进设计文档。
| 模块 | 选型 | 关键参数 | 备注 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | 72MHz,64KB Flash,20KB RAM | 最小系统板即可 |
| 湿度采集 | 电容式土壤湿度传感器 | 输出0-3V模拟量,3.3-5V供电 | 需校准 |
| 水泵驱动 | 5V继电器模块或AO3400 MOS | 触点容量10A,驱动电流>1A | 加续流二极管 |
| 显示 | 0.96寸OLED SSD1306 | I2C接口,128x64 | 3.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.3V,OLED和传感器也用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-3V,STM32的ADC参考电压3.3V,12位分辨率,理论精度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. 软件架构与关键模块:裸机前后台还是上RTOS
4.1 任务划分与调度周期
这个项目规模不大,裸机前后台架构足够,没必要上RTOS增加复杂度。我一般把任务按周期分三档:ADC采样和滤波每2秒一次,水泵状态机每100ms跑一次,OLED刷新和按键扫描每500ms一次。主循环里用HAL_GetTick()做时间片轮询,不用阻塞延时。这样即使某个任务偶尔超时,也不会影响其他任务。如果非要用RTOS,FreeRTOS的任务优先级这样分:湿度采集任务优先级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,等用户确认后再保存。另外不要在中断里写Flash,Flash操作期间CPU会暂停,影响其他任务。
6. 验收与答辩:怎么证明这套浇花系统真的靠谱
验收不是把代码烧进去看水泵转不转就完了。我一般会按下面这张表逐项测,每项都有明确的通过标准,答辩时直接拿数据说话。
| 测试项 | 测试方法 | 通过标准 |
|---|---|---|
| 湿度采集精度 | 传感器放空气中、水中、湿土中,对比标准值 | 误差不超过5% |
| 自动浇水逻辑 | 湿度从50%降到25%,观察水泵启动 | 低于30%启动,高于45%停止 |
| 回差与冷却 | 湿度在30%附近波动 | 不出现1分钟内启停超过2次 |
| 干烧保护 | 传感器放空气中,手动强制浇水 | 60秒内自动关闭并报警 |
| 掉电保存 | 修改阈值后断电再上电 | 阈值与修改前一致 |
| 连续运行 | 系统通电24小时 | 无复位、无死机、无异常抽水 |
答辩时老师最爱问三个问题:为什么选电容式不选电阻式?回差和冷却时间怎么定的?如果传感器坏了系统会怎样?前两个上面已经讲清楚了。第三个问题,我的做法是在代码里加一个传感器合理性检查:如果ADC读数长时间不变(比如连续10次采样值完全相同),判定传感器故障,关闭水泵并报警。这个逻辑不复杂,但能体现你对可靠性的考虑。
还有一个技巧:答辩现场不要只放PPT,把实物带过去,现场演示从干燥到浇水的完整过程。提前把土壤弄干一点,让老师看到水泵启动、OLED刷新、湿度上升的全过程。如果条件允许,准备两套阈值参数,一套给多肉,一套给绿萝,现场切换展示灵活性。最后,把关键代码和测试数据打印出来,老师翻到哪一页你都能讲出设计意图。
从那以后我每次做嵌入式项目,都会在验收前强制走一遍“断电—上电—异常注入”的流程,因为现场演示翻车的代价太大了。希望这套方案能帮你把自动浇花系统做扎实,答辩顺利。
本文还有配套的精品资源,点击获取