简介:基于STM32F103C8T6的自动浇花系统完整工程文件,适合嵌入式初学者、智能硬件开发者和课程设计参考。整套系统以单片机为控制核心,搭配土壤温湿度传感器、光敏传感器和空气温湿度传感器,实时采集环境数据,并利用OLED显示屏直观展示温度、湿度与光照强度。配合继电器驱动水泵,可按照预设湿度阈值或定时策略自动完成浇水。压缩包内含535个文件,约27.77MB,主要文件类型包括86个C源码、70个H头文件、17张PNG电路图片、示例工程、PDF说明文档和多种工程配置文件,兼具代码、原理图与使用说明,便于直接编译、烧录和学习。目前已有3777人学习下载。资料覆盖STM32CubeMX初始化、HAL库底层驱动、传感器数据读取、控制逻辑编写以及DC-DC电源管理和PCB布局思路,可帮助读者完整经历从硬件搭建到软件调试的过程。整个项目结构清晰、注释完整,适合作为智能浇花领域的入门到进阶实践材料。
1. 从土壤湿度到水泵动作:这个 STM32 自动浇花系统拆开来看是什么
养花的人最怕的不是忘了浇水,而是浇水这件事全凭手感。今天浇多少、明天浇不浇,全看心情,出差三天回来就看见叶子耷拉。把这件事交给单片机做,核心逻辑其实不复杂:土壤湿度传感器实时读数,低于阈值就启动水泵,灌到回滞区间就停。真正难的是把闭环做得稳——传感器不被电极腐蚀骗读数,水泵不会把花盆灌成水池,掉电重启之后还能按你原来的参数继续干活。
这套 STM32 自动浇花系统交付的就是一个能直接复现的完整工程:原理图接线参考、基于 HAL 库的固件源码、串口调参协议,以及一份在普通电子市场就能买齐的元器件清单。新手按着接线图焊完就能跑通,熟手可以直接改状态机逻辑和低功耗策略,把它扩展成温室控制或者远程浇灌的前端节点。
2. 硬件选型与接线:传感器、驱动管和供电树怎么搭才不返工
2.1 传感器选型:电容式耐腐蚀,电阻式便宜但容易极化
市面上常见的土壤湿度传感器分两种,一种是叉形铜箔加比较器,利用土壤中水分改变两极板间电阻来输出信号;另一种是电容式,测的是土壤介电常数变化。电阻式便宜,几块钱一块板,但有个隐蔽问题:直流激励下叉形电极会发生电解,铜箔表面会氧化,几个月之后你会发现干土和湿土的 ADC 读数差距越来越小,这就是电极极化导致的有效检测面积衰减了。
这套资源里我建议优先选电容式传感器,原因就是它没有直流电解过程,探头寿命长得多。买的时候注意区分两类板子:带 LM393 比较器的模组版会直接输出数字量和一路模拟量,板上电位器用来调翻转阈值;不带比较器的裸传感器则只有模拟电压输出,需要自己接 ADC。资源里用的是裸传感器接 STM32 的 ADC,这样读到的是连续湿度值而不是只有“干/湿”两个状态,后续做回滞控制才有余量。如果你手里只有模组版也能用,接它的 A0 模拟输出到 PA1 即可,但要注意板载电位器会影响量程,标定前先把它旋到中间位置。
2.2 驱动方案:继电器和 MOS 管怎么选,GPIO 能不能推得动
水泵驱动有两个常见方案:小型继电器或者逻辑电平 MOS 管。继电器胜在简单和电气隔离,但它的线圈在 5V 下需要 70 到 100mA 电流,STM32 的 GPIO 最大也就能输出 20mA 左右,直接推不动,必须加一个三极管或者 ULN2003 再驱动线圈。而且继电器是机械触点,频繁浇水场景下开关次数一多,触点拉弧烧蚀是个隐患,通断瞬间还会产生噪声干扰 ADC 采样。
这套工程里用的是 AO3400 这颗 N 沟道 MOS 管直接驱动 5V 小水泵。AO3400 的 Vgs(th) 在 1V 出头,3.3V 逻辑电平就能完全导通,导通电阻只有 40 毫欧左右,灌流能力足够应付 1A 以下的直流水泵。要注意的是水泵是感性负载,关断瞬间会产生反向电动势,必须在水泵两端反并联一个续流二极管,资源里选用的是 SS34 肖特基管。如果不加这个二极管,关泵瞬间的反压很容易击穿 MOS 管栅极,翻车概率非常高。
2.3 引脚分配与供电树:每接一步先上电验证一次
下表是这套工程默认的引脚分配,接线时不用改固件也能跑:
| 功能模块 | 引脚 | 接线说明 |
|---|---|---|
| 电容式土壤传感器 | PA1 | 3.3V 供电,模拟输出接 ADC1_IN1 |
| 水泵 MOS 管控制 | PA4 | 逻辑高电平开启水泵 |
| 手动浇水按键 | PA0 | 下拉输入,上升沿触发 |
| OLED 显示屏 | PB6 / PB7 | I2C 接口,3.3V 供电 |
| RTC 晶振 | PC14 / PC15 | 32.768kHz 低速晶振 |
供电树这块要单独说一句。整个系统最忌讳的是让水泵和单片机共用一根细线供电,水泵启动瞬间电流抽走,MCU 电压跌落直接复位,表现为浇一次水就重启一次。常见做法是外部 12V 或 5V 适配器先进来,水泵直接从电源端取电,板子再通过 DC-DC 或者 LDO 降压到 3.3V。12V 水泵和 5V 逻辑之间不需要隔离,但 GND 必须连在一起,否则 MOS 管的驱动回路是断开的,水泵永远不可能工作。接线顺序建议从电源开始,先点亮最小系统板,再逐个接传感器、显示屏和驱动管,每接一步上电验证一次,这样出了问题能快速定位到具体模块。
3. 固件逻辑设计:ADC 采集、滑动滤波与灌溉状态机的实现
3.1 湿度采集代码:连续采样加滑动平均,避免单点读数骗人
土壤湿度传感器输出的模拟量本身不算干净,土壤颗粒松散程度、探头附近局部水分差异都会让单次 ADC 读数跳来跳去。如果直接用单次采样结果做阈值比较,浇水逻辑会非常神经质,甚至会出现在阈值边缘反复启停水泵的现象。这里我一般会在 ADC 层做一层滑动平均滤波,把读数拉稳再往上送。
#define ADC_SAMPLE_NUM 20 uint16_t adc_read_filtered(void) { uint32_t sum = 0; for (uint8_t i = 0; i < ADC_SAMPLE_NUM; i++) { HAL_ADC_Start(&hadc); /* 启动一次 ADC 转换 */ if (HAL_ADC_PollForConversion(&hadc, 10) == HAL_OK) { sum += HAL_ADC_GetValue(&hadc); /* 读取 12 位转换结果 */ } } return (uint16_t)(sum / ADC_SAMPLE_NUM); /* 返回 20 次的平均值 */ }这段代码的逻辑很简单:连续触发 20 次转换并累加,最后求平均。这样处理的好处是把传感器输出里的高频噪声和 ADC 本身的抖动都做了平滑,读数基本稳定在几十个 LSB 的波动范围内。参数这边有两个可以调的点:ADC_SAMPLE_NUM 越大读数越稳,但单次采集耗时越长,在 STM32F103 主频 72MHz 下 20 次转换大约耗时 1 毫秒,对浇水这种秒级响应的场景完全够用;HAL_ADC_PollForConversion 的超时参数 10 指的是 10 毫秒,ADC 转换正常情况下远快于这个时间,设大了只是保险起见,不用刻意调优。如果你用 DMA 方式采集,还可以把 CPU 让出来,不过对于单路传感器加状态机这套逻辑,轮询方式最简单也不容易出错。
3.2 灌溉状态机:自动、手动、定时三种模式如何切换
浇水系统最怕的就是逻辑写成一坨 if-else,后面加需求时改一处崩三处。对照工程源码的做法是把系统运行状态抽象成一个枚举型状态机,自动模式下根据湿度作闭环,手动模式完全交给按键,定时模式则按 RTC 闹钟触发固定时长灌溉。
typedef enum { MODE_AUTO, MODE_MANUAL, MODE_TIMER, MODE_STOP } sys_mode_t; sys_mode_t current_mode = MODE_AUTO; uint16_t hum_raw; uint16_t thr_dry = 2700; /* 干阈值,低于此值开始浇水 */ uint16_t thr_wet = 2100; /* 湿阈值,高于此值停止浇水 */ uint8_t pump_state = 0; void irrigation_task(void) { hum_raw = adc_read_filtered(); switch (current_mode) { case MODE_AUTO: if (hum_raw < thr_dry && pump_state == 0) { pump_on(); /* 土壤偏干,打开水泵 */ } else if (hum_raw > thr_wet && pump_state == 1) { pump_off(); /* 土壤湿度达到湿阈值,关闭水泵 */ } break; case MODE_MANUAL: /* 按键处理放在外部中断里,这里不做动作 */ break; case MODE_TIMER: if (rtc_alarm_flag) /* RTC 闹钟每 N 小时触发一次 */ { pump_on_duration(2000); /* 每次浇水固定 2 秒 */ rtc_alarm_flag = 0; } break; case MODE_STOP: pump_off(); break; } }main 循环里每 500 毫秒调一次 irrigation_task,也就是湿度感知和决策是周期性推进的。这里最关键的是把浇水条件和停止条件拆成了两个阈值,中间留出 600 的带宽,这就是回滞控制。如果没有这一段带宽,传感器读数在阈值附近波动时,系统会反复开机、关机,水泵电机和 MOS 管都会承受很大的电流冲击。thr_dry 和 thr_wet 的默认值只是占位,实际要根据你花盆里的土壤标定出来,下一章会讲具体的标定操作流程。手动模式下你也可以把当前状态屏显出来,按键一次灌 2 秒,适合应急补水。
3.3 参数掉电保存:把阈值写进内部 Flash,重启不用重新调
单片机跑的浇花系统有个尴尬场景:调好的阈值和模式选择,一断电全丢,重启后又回到默认值。这套工程的解决思路是把关键参数存在 STM32 内部 Flash 的最后一页上,配合一个简单的 CRC16 校验,每次启动时读出来校验,不对就用默认值覆盖。下面这段是参数保存的核心代码:
#define PARAM_FLASH_ADDR 0x0800FC00 /* F103C8T6 最后一页 Flash */ #define PARAM_MAGIC_CRC 0x5A5A typedef struct { uint16_t thr_dry; uint16_t thr_wet; uint16_t mode; uint32_t crc; } param_t; param_t g_param; uint16_t crc16_update(uint16_t crc, uint8_t byte) { crc ^= (uint16_t)byte << 8; for (uint8_t i = 0; i < 8; i++) { crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : (crc << 1); } return crc; } void param_save(void) { uint32_t crc = 0; uint8_t *p = (uint8_t *)&g_param; for (uint8_t i = 0; i < sizeof(param_t) - 4; i++) { crc = crc16_update(crc, p[i]); } g_param.crc = crc; HAL_FLASH_Unlock(); FLASH_ErasePage(PARAM_FLASH_ADDR); /* 先擦除整页 */ uint32_t *data = (uint32_t *)&g_param; for (uint8_t i = 0; i < sizeof(param_t) / 4; i++) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, PARAM_FLASH_ADDR + i * 4, data[i]); } HAL_FLASH_Lock(); }这段代码有几个细节值得注意。F103C8T6 的内部 Flash 是 64KB,最后一页从 0x0800FC00 开始,大小 1KB,存这几个结构体绰绰有余。擦除必须以页为单位,所以每次保存前要把整页擦掉,而不是像 RAM 那样直接覆写单个地址。写入用字访问,也就是 4 字节对齐的 FLASH_TYPEPROGRAM_WORD,这是 STM32F1 系例支持的写入方式。CRC 计算只覆盖结构体里的业务字段,不包括 crc 自己,这样读出来校验时能识别数据损坏。掉电保存不需要很频繁,只有串口调参成功后调用一次就行,Flash 擦写寿命在 1 万次以上,正常使用不可能用尽。
4. 参数标定与串口调参:湿度阈值不是拍脑袋定出来的
4.1 标定流程:干土、湿土、饱和水三个状态各采一组数据
很多第一次接触土壤湿度传感器的人,拿到手就按默认阈值跑,结果发现传感器插在干土里读数只有 1600,而默认的 thr_dry 是 2700,等于浇花逻辑一直觉得土壤偏干,水泵转个不停。这不是代码问题,而是不同土壤、不同传感器供电电压下,ADC 读数区间差异非常大。所以上电后第一件事应该是标定,把三个基准状态测出来。
标定步骤很简单,但每一步都要等读数稳定再记录:
| 土壤状态 | 操作 | 等待时间 | 记录值 |
|---|---|---|---|
| 干土 | 传感器插入完全干燥的盆土 | 2 分钟 | 干阈值基准值 |
| 正常润土 | 浇水至土壤湿润但不积水 | 10 分钟 | 维持阈值区间 |
| 饱和土 | 浇水至盆底持续滴水 | 5 分钟 | 湿阈值下限 |
注意,第二步等 10 分钟很关键。刚浇完水时探头附近水分浓度很高,读数会瞬间冲到很低的位置,但土壤里的水在重力作用下会缓慢下渗,需要几分钟才能达到相对稳定的分布状态。如果在这一阶段读数还没稳定就把阈值定下来,后面实际使用时会出现“刚浇完显示很湿,半小时后显示很干”的尴尬情况。实际操作时我会在串口调试助手或者 OLED 屏上实时看 ADC 值,等到数值下降速度明显放缓再记录。
4.2 串口调参协议:用一条指令把阈值写进 Flash
标定出三组数据后,直接把固定值写死在代码里重新编译下载也能用,但太不优雅。这套工程里给了一个简单的串口调参协议,格式是set dry 2700、set wet 2100、set mode auto这种直观的文本指令,主控解析后更新运行参数并落盘保存。解析逻辑放在串口中断接收里:
void uart_rx_process(char *cmd, uint16_t len) { if (strncmp(cmd, "set dry", 7) == 0) { g_param.thr_dry = (uint16_t)atoi(cmd + 8); param_save(); printf("thr_dry updated to %d\r\n", g_param.thr_dry); } else if (strncmp(cmd, "set wet", 7) == 0) { g_param.thr_wet = (uint16_t)atoi(cmd + 8); param_save(); printf("thr_wet updated to %d\r\n", g_param.thr_wet); } else if (strncmp(cmd, "set mode", 8) == 0) { if (strncmp(cmd + 9, "auto", 4) == 0) current_mode = MODE_AUTO; else if (strncmp(cmd + 9, "manual", 6) == 0) current_mode = MODE_MANUAL; else if (strncmp(cmd + 9, "timer", 5) == 0) current_mode = MODE_TIMER; else if (strncmp(cmd + 9, "stop", 4) == 0) current_mode = MODE_STOP; param_save(); } }这段代码的逻辑是标准的文本协议解析:收到完整一行命令后,先比较命令前缀,再用 atoi 把后面的数字字符串转成整数,更新结构体并保存。串口中断里建议只做接收和帧结束判断,解析和处理放到主循环里做,避免在中断里执行 Flash 擦写这种耗时操作。参数方面有一点要注意:cmd + 8的偏移量对应set dry的前 8 个字符,命令格式多一个空格或少一个空格都会解析失败,调的时候尽量用十六进制发送窗口排查,先确认收到的字节数和预期一致。如果你是连显示屏调试,也可以把解析结果实时显示在 OLED 上,不用每次开电脑看串口。
4.3 阈值设多少才合理:回滞带宽和浇水时间上限
标定完成后,thr_dry 和 thr_wet 到底怎么取?我一般用干土基准值和正常湿土基准值之间的中点附近作为 thr_dry,用水饱和状态的读数加一点余量作为 thr_wet。假设标定结果是干土 3200、正常湿土 1800、饱和土 900,那 thr_dry 可以设在 2500 附近,thr_wet 设在 1500 附近,中间留出 1000 的带宽。带宽太小,比如只差 200,土壤湿度缓变时也容易导致水泵频繁启停;带宽太大又会导致一次浇水过度。
另外还应该在代码里加一层兜底限制:水泵单次连续运行时间超过某个上限(比如 60 秒)就强制关闭,防止传感器脱落、探头位置异常导致读数永远低于干阈值的极端情况。这个上限不需要很精确,作用是防止花盆被淹和电机空转过热。很多人会忽略这个保护,直到出差回来发现泵烧了才后悔,属于典型的“参数简单但救命”的配置。
5. 常见问题排查:水泵不转、误浇水、电压跌落的实战复盘
5.1 水泵不动作,IO 电平也正常,问题出在哪
现象:固件里手动调用 pump_on() 后,水泵完全没有反应,用万用表量 MOS 管栅极电压已经到 3.3V。
原因:大概率不是代码问题,而是驱动链路里某个环节断了。最常见的是 MOS 管源极没有和 STM32 的 GND 连在一起,导致栅源电压虽然存在,但水泵回路没有形成;或者是续流二极管接反,直接短路了电源;还有一种可能是水泵额定电压是 12V,而电源适配器输出只有 5V,电压不够启动。
解决:沿着驱动链路逐级量电压。先量电源两端是否真的有标称电压,再量水泵两端电压,开启状态应该有接近电源电压的值;然后把万用表打到二极管档量 MOS 管 D-S 间是否导通;最后确认 GND 线是同一根参考地,不要用两个适配器分别给板子和水泵供电,否则两地之间会有电位差。
5.2 湿度读数上下跳变,浇花逻辑像个精神分裂
现象:OLED 屏上显示的湿度数字频繁波动,比如 1500 跳到 2300 又跳回 1600,水泵在阈值附近反复开关。
原因:这个现象要分成两类看。一类是传感器附近土壤有水分通道,浇完水后水沿探头边沿渗流,读数快速变化,这是传感器安装位置问题;另一类是电源噪声干扰 ADC,12V 水泵启动时电压跌落严重,虽然 MCU 没复位,但 ADC 的 Vref 被拉偏,读数就会随泵的启停周期性波动。
解决:软件上把 ADC_SAMPLE_NUM 从 20 提到 50,能明显稳定短期波动;硬件上确认 STM32 的 VDDA 引脚有没有通过 10uF 加 0.1uF 电容组合做电源滤波。另外传感器尽量竖着插入土壤,插入深度固定,不要靠近盆壁,减少水分沿探头流过的干扰。如果这两步都做完还在跳,就用贯穿本用于对比的模拟值打印到串口,人为浇水观察变化规律,排除是不是传感器本身漂移。
5.3 定时模式不触发,RTC 闹钟像被吃掉了一样
现象:切成定时模式后,到了预设时间水不浇,rtc_alarm_flag 一直等于 0,用串口查询当前时间发现时间走得也不对。
原因:F103C8T6 的 RTC 如果在调试下载过程中被复位,很容易丢失时钟源配置;另外它依赖 PC14/PC15 外接的 32.768kHz 晶振,如果晶体没起振或者负载电容配得不对,RTC 计数根本不走。还有一种情况是中断回调函数没写好,RTC 闹钟中断触发后 HAL_RTC_AlarmIRQHandler 没有正确调用 HAL_RTC_AlarmAEventCallback,导致标志位一直不会被置 1。
解决:先查 RTC 时间有没有正常走动,用串口每秒打印一次日期秒数。如果秒数不动,检查晶振引脚上的负载电容值,常见做法是把 6pF 到 12.5pF 电容装上,别买错成 20pF 甚至更大的。中断回调部分要确认在 stm32f1xx_it.c 里把 RTC_Alarm_IRQHandler 重定向到 HAL 库的处理函数,并把回调函数实现写出来,这块是标准坑,换了 IDE 工程就特别容易漏。
5.4 继电器方案咔哒响一下,整机就复位,找不到原因
现象:有人把工程里默认的 MOS 管方案改成了继电器驱动,结果每次继电器吸合的瞬间,OLED 屏闪一下、MCU 重启,串口日志里能看到 boot 信息。
原因:继电器线圈断电瞬间会产生感性反冲,而且在触点吸合/释放瞬间,线圈电流突变会在电源线上叠加很大的尖峰。如果板子的电源适配器质量一般或者线径太细,这个尖峰直接把 MCU 的 3.3V 拉低到复位阈值以下。从 MOS 管切到继电器不是只换一个器件,驱动逻辑和电源滤波全要跟着变。
解决:三个方向同时堵漏。继电器线圈两端并一个续流二极管(1N4007 就行);继电器电源用独立绕组或独立适配器,不要和 MCU 共用 LDO;在 MCU 电源入口加一个 470uF 电解电容和 0.1uF 瓷片电容,吸收瞬态跌落。如果你只是为了隔离水泵和逻辑地,更推荐直接沿用 MOS 管方案加光耦隔离,体积和稳定性都更好。
6. 进阶技巧:低功耗值守、传感器校准与远程浇水提醒
6.1 低功耗改造:从连续轮询到看门狗加 RTC 定时唤醒
长期挂在阳台上用的浇花系统,如果用一个 5V 适配器一直供电,功耗不是大问题;但如果你想用充电宝或者太阳能电池供电,连续轮询 ADC 的方式就太奢侈了。一个实际可行的手法是让 MCU 大部分时间待在 STOP 模式,由 RTC 闹钟每隔 30 秒唤醒一次,采样一次湿度并判断是否浇水,然后重新入睡。
void enter_stop_mode(void) { /* 配置 RTC 闹钟,间隔 30 秒 */ HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_ALARM_A); /* 进入 STOP 模式,用闹钟事件唤醒 */ HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); } void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) { __HAL_RTC_ALARM_EXTI_CLEAR_FLAG(); irrigation_task(); /* 唤醒后马上执行一次状态判断 */ }用这个方案,整套系统在无浇灌动作时的平均电流可以从几十毫安降到毫安级别,电池供电才有实际意义。要注意的是 STOP 模式会停掉主时钟,但 RTC 用的是低速外部晶振,不受影响。唤醒后如果开启了 HSE,需要重新配置系统时钟,否则外设可能跑在错误频率下,这块很容易被忽略。另外主循环里建议喂独立看门狗,防止出现异常状态卡死之后整机彻底睡死。
6.2 传感器老化修正:两点标定线性校准
电容式传感器用久了之后,即使没有电解腐蚀,也会因为土壤堆积、探头表面有机物附着导致同一个湿度下的 ADC 读数逐渐偏大。这时候重新做一次完整标定很麻烦,我常用的做法是两点校准:只测干土和饱和水两个基准点,然后用线性公式把 ADC 读数映射回一个标准化的湿度百分数。
校准公式是 y = kx + b,其中 x 是原始 ADC 读数,y 是校准后的湿度百分比。干土状态记原始值为 x1,映射为 0%;饱和水状态记原始值为 x2,映射为 100%,则 k = 100 / (x1 - x2),b = -k × x2。代码里维护这两个系数,显示和阈值判断全部走校准后的数值,这样即使传感器漂移,只需要重新测两个基准点就能修正回来,不需要动 thr_dry 和 thr_wet 的绝对值。
6.3 远程浇水提醒:一个串口透传模块就够了
如果想在手机上看到花盆状态,最省事的扩展是加一个 ESP8266 串口透传模块,和 STM32 之间通过 UART 通信,每隔 5 分钟主动上报一次状态包。上报格式用 JSON 文本,形如{"hum":68,"pump":"off","mode":"auto"},ESP8266 那边通过 Wi-Fi 把数据推到自己的手机 App 或者第三方平台。做这一步之前,低功耗值守和定时唤醒一定先跑稳,否则远程功能上线后会发现电耗得特别快,这是之前踩过的硬坑。从那以后我每次拿到一套浇花系统,排查顺序都是先确认阈值标定准确、掉电重启后参数还在,再谈联网和其他智能化,这个习惯救了我好几次差点把花盆淹了的失误。希望帮到你——先把物理层的活干稳,再往上面加看起来高级的功能。
本文还有配套的精品资源,点击获取