简介STM32自动浇花系统是一套面向嵌入式初学者与电子爱好者的完整智能硬件实践项目以STM32F103C8T6为核心结合土壤温湿度、光敏、空气温湿度等传感器与OLED显示、DC-DC电源、水泵控制模块实现按湿度阈值或定时策略自动灌溉帮助读者理解从传感器采集到执行机构驱动的完整闭环。压缩包共535个文件约27.77MB以86个c源文件与70个h头文件为主体配合Keil工程文件、hex固件、PCB设计资料、pdf文档与png图片等覆盖代码、原理图与调试记录。目前已有3779人学习下载。项目涉及STM32CubeMX初始化、HAL库驱动、GPIO控制OLED、继电器驱动水泵及PCB布局与元器件封装等环节适合作为课程设计、毕业设计或DIY智能养护方案的参考便于读者快速搭建硬件平台并移植控制逻辑。1. 从一份 STM32 自动浇花系统源码包说起它到底能解决什么阳台那几盆绿萝出差三天回来叶子全耷拉了——这事我干过不止一次。后来我拿到一份 STM32 自动浇花系统.zip拆开跑了一遍发现它解决的正是这种“人不在、花要死”的场景。核心逻辑不复杂STM32 采集土壤湿度低于阈值就驱动水泵补水高于阈值就停顺带在 OLED 上显示当前湿度百分比。它适合三类人正在找 STM32 综合练手项目的学生、想给家里做个小装置但不想从零画板的开发者、以及需要一套可改可扩的自动浇花系统参考实现的工程师。整包是标准 Keil 工程结构外设驱动、主循环、阈值判断都拆得比较清楚不是那种一个 main.c 写到底的“教学 demo”。下面我按“硬件怎么接、代码怎么读、参数怎么调、坑在哪”的顺序把这份资源拆一遍。2. 硬件链路与工程结构先看清信号从土壤到水泵走了哪条路2.1 传感器选型与 ADC 采集链路自动浇花系统的第一环是土壤湿度检测。这份工程用的是模拟量土壤湿度传感器常见做法是电阻式叉形探头输出 03.3V 模拟电压接到 STM32 的 ADC 通道上。为什么不用数字量模块因为数字量模块只给一个可调电位器决定的开关量你没法在代码里做平滑滤波也没法显示具体湿度百分比调阈值只能拧螺丝很玄学。模拟量方案虽然要多写几行 ADC 采样和换算但阈值可以在代码里改还能做多点平均稳定性高不少。工程里 ADC 的配置大致是规则通道单次转换采样时间设到 55.5 个周期以上因为土壤湿度变化慢不需要高速采样长采样时间反而能降低噪声。下面这段是常见的 ADC 初始化骨架我按工程里的写法整理了一下// ADC1 初始化PA0 作为模拟输入对应土壤湿度传感器 void Soil_ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 时钟 72M/6 12MHz不超过 14MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入必须设 AIN GPIO_Init(GPIOA, GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道不用扫描 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换主循环里手动触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); // ADC 校准别省这一步不然首读偏差能到几十个 LSB ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }逻辑说明这段代码做了三件事——开时钟、配 GPIO 为模拟输入、配 ADC 为单通道单次转换。参数上最需要注意的是RCC_ADCCLKConfig(RCC_PCLK2_Div6)STM32F103 的 ADC 时钟不能超过 14MHz72MHz 主频下分频系数至少选 6。ADC_SampleTime_55Cycles5是采样时间土壤传感器输出阻抗不低采样时间太短会导致读数偏低55.5 周期是稳妥值。校准那两行很多人会漏漏了之后同一块板子换传感器读数能差出 5% 以上。2.2 水泵驱动与继电器/ MOS 管选择STM32 的 GPIO 输出电流最大 20mA 左右带不动水泵必须加驱动级。工程里用的是继电器模块低电平触发。为什么不是 MOS 管继电器模块即插即用隔离性好缺点是寿命有限、切换有声音MOS 管方案更安静、寿命长但要自己算栅极电阻和续流二极管对新手不友好。这份资源定位是快速复现所以选继电器是合理的。接线逻辑STM32 的 PB1 接继电器 IN 脚继电器常开触点串在水泵电源回路里。代码里就是一句 GPIO 置位/复位。但这里有个容易翻车的点继电器模块如果是 5V 供电而 STM32 是 3.3V 电平部分继电器模块的高电平触发阈值是 3.5V 以上3.3V 可能触发不了。常见做法是选低电平触发的继电器模块或者加一个三极管做电平转换。工程里默认低电平触发所以代码里是GPIO_ResetBits开水泵别写反了。2.3 工程目录与关键文件定位解压后目录结构大致是CORE、HARDWARE、SYSTEM、USER四个文件夹。HARDWARE下放adc.c、relay.c、oled.cUSER下是main.c。读代码的顺序建议是先看main.c的主循环搞清楚状态机怎么跑再回头看adc.c里的换算函数最后看oled.c的显示刷新。不要一上来就啃 OLED 驱动那部分和浇花逻辑无关容易陷进去。3. 主循环逻辑与阈值参数湿度百分比是怎么算出来的3.1 ADC 原始值到湿度百分比的换算ADC 读出来是 04095 的原始值要变成“湿度百分比”得做一次映射。这里有个反直觉的地方电阻式土壤传感器的输出电压和湿度是反相关的——越湿输出电圧越低。所以换算时不能直接raw/4095*100得反过来。工程里的做法是先测两个校准点传感器完全干燥时的 ADC 值记为 DRY和泡在水里的 ADC 值记为 WET然后线性映射。// 土壤湿度换算返回 0~100 的百分比 // DRY_VAL 和 WET_VAL 需要根据实际传感器实测后修改 #define DRY_VAL 3800 // 传感器在空气中时的 ADC 读数 #define WET_VAL 1200 // 传感器泡水时的 ADC 读数 u8 Soil_GetPercent(void) { u16 raw 0; u8 i; u32 sum 0; for (i 0; i 10; i) { // 连续采 10 次 ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); sum ADC_GetConversionValue(ADC1); } raw sum / 10; // 取平均滤掉抖动 if (raw DRY_VAL) return 0; if (raw WET_VAL) return 100; return (u8)((DRY_VAL - raw) * 100 / (DRY_VAL - WET_VAL)); }逻辑说明先做 10 次采样取平均这是最省事的软件滤波能压掉大部分随机噪声。参数DRY_VAL和WET_VAL是整份代码里最需要你动手改的地方不同厂家、不同批次的传感器这两个值能差几百个 LSB。我一般会先把传感器晾干读一次串口再泡水里读一次把两个值填回去。如果你不校准直接用默认值显示出来的湿度可能永远卡在 30% 或者 90%然后你就开始怀疑代码其实是传感器个体差异。3.2 阈值判断与水泵启停的迟滞设计主循环里判断逻辑看起来简单湿度低于阈值开水泵高于阈值关水泵。但如果只设一个阈值比如 40%那湿度在 40% 附近抖动时水泵会频繁启停继电器咔咔响寿命掉得很快。工程里用了迟滞区间低于 35% 开高于 55% 关。中间 20% 的区间是“死区”湿度在这区间内水泵保持当前状态。// 主循环中的浇花控制逻辑 #define HUMI_LOW 35 // 低于此值开始浇水 #define HUMI_HIGH 55 // 高于此值停止浇水 void Water_Control(u8 humi) { static u8 pump_on 0; if (humi HUMI_LOW) { pump_on 1; } else if (humi HUMI_HIGH) { pump_on 0; } // 在 LOW 和 HIGH 之间时pump_on 保持上一次的值 if (pump_on) { Relay_On(); // 低电平触发实际是 GPIO_ResetBits } else { Relay_Off(); } }逻辑说明static u8 pump_on保存上一次的状态这就是迟滞的核心。参数HUMI_LOW和HUMI_HIGH的差值建议至少 15%太小了还是会抖。另外浇水时最好加一个最大连续运行时间保护比如水泵连续开 30 秒强制停 10 秒防止传感器故障导致水泵一直抽水把花淹了。这个保护工程里没写但属于我强烈建议你补上的。3.3 OLED 显示刷新与串口调试OLED 部分用的是 I2C 接口的 0.96 寸屏显示内容一般是两行第一行当前湿度百分比第二行水泵状态。刷新频率不用太高主循环里每 500ms 刷一次就够了刷太快反而闪。串口调试更实用把原始 ADC 值、换算后的百分比、水泵状态一起打印出来校准传感器的时候盯着串口看比看 OLED 方便。// 串口打印调试信息波特率 115200 printf(ADC_RAW%d, HUMI%d%%, PUMP%s\r\n, raw, humi, pump_on ? ON : OFF);逻辑说明printf重定向到串口需要实现fputc工程里SYSTEM文件夹下一般有usart.c已经做好了。参数上注意波特率一致115200 是常用值。如果你打印出来是乱码先查波特率再查晶振频率宏定义是不是 8MHz 外部晶振这两个地方错了串口必乱。4. 避坑与排查这份源码跑不起来时先查这五处4.1 现象OLED 不亮但串口有数据原因I2C 地址不对或者接线反了。0.96 寸 OLED 常见地址是 0x78 和 0x7A有些模块背面电阻跳线决定地址。另外 SCL 和 SDA 接反了也会不亮但不会烧只是没反应。解决先用 I2C 扫描代码确认地址再核对模块背面的地址跳线。SCL/SDA 对调一下试试成本最低。4.2 现象水泵一直转湿度显示 0%原因传感器没接好或者 ADC 通道配错。如果 PA0 悬空ADC 读到的值会飘到接近 4095换算后湿度接近 0%触发浇水。另外传感器供电如果是 5V 而 ADC 参考电压是 3.3V输出超过 3.3V 会读满量程也会显示 0%。解决先确认传感器供电和信号线接对用万用表量传感器输出脚电压。如果传感器是 5V 供电加一个分压电阻再进 ADC或者换成 3.3V 供电的传感器。4.3 现象湿度数值跳变超过 10%原因ADC 采样没有做滤波或者传感器探头接触不良。电阻式传感器对接触电阻很敏感探头插在土里如果松动读数就会跳。解决软件上把采样次数从 10 次加到 20 次或者加一阶低通滤波filtered filtered*0.8 new*0.2。硬件上把探头插紧线用杜邦线的话换成长度短一点的减少干扰。4.4 现象继电器吸合瞬间 STM32 复位原因继电器线圈断电时产生反向电动势通过电源线耦合把 MCU 拉复位了。这是血泪经验很多人以为是代码问题其实是电源问题。解决继电器模块的 VCC 和 GND 并一个 100uF 电解电容加 0.1uF 瓷片电容线圈两端加续流二极管如果是裸继电器。另外 STM32 和继电器不要共用一条细杜邦线供电分开走线。4.5 现象编译报错 “cannot open source input file stm32f10x.h”原因Keil 工程里的头文件路径没设对或者你换了一台电脑绝对路径失效。解决在 Keil 的 Options for Target → C/C → Include Paths 里把CORE、HARDWARE、SYSTEM、USER以及标准库的inc目录都加进去。路径用相对路径别用绝对路径不然换电脑就翻车。5. 进阶改造把阈值写进 Flash断电不用重烧5.1 为什么要把阈值存进 Flash现在改阈值要重新编译、重新烧录如果你把装置装到阳台上了想调一下阈值还得把板子拆下来插电脑很麻烦。常见做法是把HUMI_LOW和HUMI_HIGH存到 STM32 内部 Flash 的末尾页上电时读出来运行时可以通过串口命令修改。这样调阈值只需要串口发一条指令不用拆机。5.2 Flash 读写的最小实现STM32F103 的 Flash 页大小是 1KB 或 2KB选最后一页操作别和代码区冲突。下面是一个最小可用的读写封装// 把阈值存到 Flash 最后一页以 STM32F103C8T6 为例最后一页起始 0x0800FC00 #define FLASH_SAVE_ADDR 0x0800FC00 #define FLASH_PAGE_SIZE 1024 void Flash_Save_Threshold(u8 low, u8 high) { u16 buf[2] {low, high}; FLASH_Unlock(); FLASH_ErasePage(FLASH_SAVE_ADDR); // 先擦除整页 FLASH_ProgramHalfWord(FLASH_SAVE_ADDR, buf[0]); FLASH_ProgramHalfWord(FLASH_SAVE_ADDR 2, buf[1]); FLASH_Lock(); } void Flash_Load_Threshold(u8 *low, u8 *high) { *low *(vu16*)FLASH_SAVE_ADDR; *high *(vu16*)(FLASH_SAVE_ADDR 2); // 如果读出来是 0xFF说明从没写过用默认值 if (*low 0xFF || *high 0xFF) { *low 35; *high 55; } }逻辑说明FLASH_ErasePage必须整页擦除擦完所有字节变 0xFF。FLASH_ProgramHalfWord按半字写所以地址每次加 2。参数上注意FLASH_SAVE_ADDR要选在你代码占用空间之外的页如果你工程编译出来超过 63KB就得往前挪一页。读的时候判断 0xFF 是必要的因为新芯片 Flash 擦除后就是 0xFF不判断会读到 255 导致逻辑异常。5.3 串口命令解析与在线调参有了 Flash 读写再加一个简单的串口命令解析比如收到SET 40 60就把低阈值设为 40、高阈值设为 60然后存 Flash。解析逻辑用sscanf最省事// 串口接收缓冲区里收到 SET 40 60 时调用 void Cmd_Parse(char *cmd) { u8 low, high; if (sscanf(cmd, SET %hhu %hhu, low, high) 2) { if (low high high 100) { Flash_Save_Threshold(low, high); printf(OK: LOW%d HIGH%d\r\n, low, high); } else { printf(ERR: invalid range\r\n); } } }逻辑说明%hhu对应unsigned char别写成%d不然会覆盖相邻内存。参数校验里low high是必须的否则迟滞逻辑会失效。调参时我一般会先发SET 35 55确认返回 OK再断电重启看阈值有没有丢验证 Flash 确实写进去了。5.4 验证方法与我的习惯验证 Flash 是否写入成功最直接的办法是断电再上电看串口打印的阈值是不是你设置的值。如果每次上电都回到默认值先查FLASH_SAVE_ADDR是不是和代码区重叠了再查有没有调用FLASH_Unlock。从那以后我每次改 Flash 相关代码都强制走一遍“写→断电→上电→读”的流程不省这一步。希望帮到你。本文还有配套的精品资源点击获取