☰
基于STM32的智能灌溉系统:从硬件选型到FreeRTOS软件架构实践
2026/10/13 21:04:00 网站建设 项目流程

简介:本资源是一套基于STM32微控制器的智能灌溉系统完整开发包,面向嵌入式初学者、课程设计学生及农业物联网实践者,解决土壤湿度感知、自动启停灌溉、低功耗控制等典型应用场景问题。压缩包共92个文件,总大小2.82MB,涵盖55个头文件(h)、25个源码文件(c)构成的完整Keil工程,含STM32CubeMX生成的.ioc配置、.mxproject项目文件、MDK-ARM工程(.uvprojx/.uvoptx),以及APP.7z(应用层代码)、图纸.7z(原理图与PCB参考)、程序.7z(固件与驱动)等模块化归档;a.txt提供基础说明,.lst与.s文件辅助调试分析。已有289人学习下载,资源结构清晰分层——Drivers目录封装HAL外设驱动,Src/UserSrc组织核心灌溉逻辑(ADC采样土壤湿度、TIM定时控制电磁阀、GPIO状态指示),配套文档支撑从硬件搭建到固件烧录全流程复现,是理解STM32在农业自动化中落地的典型教学级工程范例。 一直觉得阳台上的绿植是摆设,每周都得掐着日子浇水,出差三天回来,叶子就蔫成咸菜。后来我干脆用开发板做了套智能灌溉系统,才算是彻底解放了双手。这套东西没有多复杂,核心就是一块STM32,配合几个传感器和一个水泵,能根据土壤干湿度自动浇水,还能通过WiFi远程控制。今天就把整条设计链路、代码思路和调试中踩过的坑整理出来,给想从零手搓一套自动浇灌系统的朋友当个参考。无论你是单片机初学者,还是已经做过几个小项目的开发者,这套系统的硬件选型、任务拆分和串口通信方案都有直接复用的价值。

1. 项目定位与需求拆解:不只是自动浇水那么简单

1.1 从"按时浇水"到"按需灌溉"的升级逻辑

大多数家用浇水器是定时工作,每天固定时段喷几分钟,但土壤干湿度受天气、蒸发量影响很大,定时浇灌要么水多了泡根,要么水少了旱着。做这套系统的核心思路是:把"时间触发"改成"条件触发"。即通过采集土壤湿度、环境温湿度和光照度,由主控实时判断是否需要启动水泵,并根据缺水程度动态调整灌水量。

这个逻辑听起来很简单,但落地时需要考虑几个层次:

  • 基础层:土壤湿度太低时启动水泵,达到目标湿度后停止。
  • 进阶层:环境温度过高或光照过强时,即使土壤湿度短期不低,也可以增加补水量;反之阴雨天自动跳过。
  • 远程层:用户能通过手机查看实时数据,手动强制启动或停止水泵,避免异常情况失控。

我最初只做了基础层,后来发现夏天中午阳台温度40多度,土壤湿度掉得极快,基础逻辑完全够用;但冬天连续阴雨,土壤长期湿润,系统又频繁误判。所以,最终版本加入了环境修正因子,用温湿度传感器和光照传感器一起参与决策。

1.2 功能需求清单与系统框架

结合实用场景,我最终确定了一套可裁剪、可扩展的功能清单:

功能模块需求描述实现方式
土壤湿度检测实时采集两路土壤湿度(多点检测)ADC多通道 + DMA + 滤波
环境检测温湿度、光照度辅助决策DHT22 / BH1750
自动灌溉湿度低于阈值时启动水泵,达到目标湿度停止继电器控制直流水泵
远程监控通过WiFi查看数据和手动控制ESP8266 + 串口协议
本地交互显示实时数据,触摸或按键设置阈值OLED + 按键
报警保护水泵空转保护、传感器断线检测电流采样 + 定时器锁

主控采用STM32F103C8T6,也就是常说的"蓝丸"板子。选它的理由很现实:便宜、稳定、资料多,用HAL库开发效率高,而且完全满足这个项目的资源需求——ADC、DMA、定时器、串口、I2C全都有,内置Flash和RAM也够跑FreeRTOS轻量级应用。

2. 硬件选型:每个元器件背后都有自己的理由

2.1 主控选型:STM32F103C8T6为什么是这个项目的主力

很多新人在选主控时容易陷入"性能焦虑",觉得得用F4或者H7。实际上,对于灌溉系统这种低频控制场景,72MHz主频的Cortex-M3内核完全足够了。F103C8T6有64KB Flash、20KB RAM,官方库和第三方例程极其丰富,各种坑几乎都在网上有解决方案,学习成本很低。

我用的是12MHz外部晶振,通过PLL倍频到72MHz。刚开始图省事用内部8MHz RC,后来发现ADC采样和串口波特率会随温度漂移,干脆换回外部晶振,稳定性立刻上一个台阶。具体引脚分配:

  • PA0、PA1:两路土壤湿度传感器ADC输入
  • PB1:DHT22环境温湿度数据脚
  • PB10、PB11:ESP8266串口2通信
  • PA9、PA10:串口1接调试模块
  • PB0:继电器控制脚(低电平有效)
  • PA2:蜂鸣器报警脚

引脚分配的原则是:优先复用有ADC功能的引脚,并且把串口1留作调试口,避免和ESP8266通信冲突。因为调试信息是需要随时看的,不能和业务功能抢资源。

2.2 传感器组合:土壤湿度、环境温湿度、光照的采集方案

土壤湿度传感器我用的是电容式,不推荐那种裸露铜极片的电阻式。电容式的表面有防腐涂层,在潮湿环境中性能衰减慢得多。模块输出电压随湿度升高而降低,范围大概0.8V~3.3V,正好在STM32的ADC量程内。实际测试中,干燥土壤电压约1.2V,饱和后约2.8V,需要根据实际素材校准阈值。

环境温湿度传感器选DHT22(也叫AM2302),精度比DHT11高不少,湿度±2%RH,温度±0.5℃,足以满足灌溉决策需求。DHT22是单总线协议,时序要求严格,时间精度要求微秒级,所以读取时最好关中断,或者用定时器做精确延时。如果项目在RTOS环境下跑,需要注意任务调度导致的时序抖动,可以在读取期间利用临界区保护。

光照传感器我选的是BH1750,走I2C接口,直接输出Lux值。光照主要用来判断天气和昼夜:阴雨天光照低于阈值时就减少浇水量,正午强光下增加浇水频次。这个传感器模块很便宜,属于锦上添花,但加上之后系统才真正算得上"智能"。

2.3 执行器与驱动:水泵、继电器与电源管理的取舍

水泵选型方面,家用浇灌一般用12V直流潜水泵,额定电流在300mA到500mA之间。直接用STM32 GPIO驱动是不行的,必须通过继电器或MOS开关。我选择的是带光耦隔离的5V低电平触发继电器模块,让控制回路与功率回路完全隔离,避免水泵启停产生的反向电动势串回主板。

继电器控制电路需要注意几个点:

  • 继电器线圈要加续流二极管,否则断电瞬间线圈会产生高压反向电动势,直接烧掉驱动三极管。
  • 低电平触发模块的输入端在悬空时是高电平,继电器不吸合,安全性更高。上电瞬间MCU GPIO默认是高阻态,不会误触发。
  • 水泵启动瞬间电流冲击大,建议在电机两端并联一个100nF陶瓷电容和1N4007二极管吸收尖峰。

电源方面,总供电用12V适配器,一路直接给水泵,另一路通过LM2596降压到5V,给主控板、传感器和ESP8266供电。LM2596模块效率高,比7805线性降压发热少很多。唯一要注意的是降压模块的输出纹波,如果纹波过大,会影响DHT22和ADC采样值,所以输出端我加了一个470uF电解电容和0.1uF瓷片电容滤波。

2.4 通信与显示:ESP8266和OLED的搭配

远程控制我用ESP8266-01S,固定串口9600波特率,通过AT指令与STM32交互。这个模块便宜,但功耗较高,峰值电流能到200mA,所以绝对不能直接用LDO给主控的3.3V供电带它,必须单独稳压或使用独立3.3V电源。我用了AMS1117-3.3V专门给ESP8266供电,同时主控板和控制板分开供电,避免复杂干扰。

本地交互显示用0.96寸I2C OLED,128x64分辨率,用来显示湿度、温度、水泵状态。OLED功耗不高,刷新率不需要很快,5Hz刷新足够。显示代码放在一个独立任务里,避免影响控制逻辑实时性。这里还发现了一个小细节:OLED的I2C地址默认是0x3C,但部分模块是0x3D,代码里最好做成宏定义,方便切换。

3. 软件架构:让FreeRTOS把每个任务安排得明明白白

3.1 任务划分与优先级设计

刚开始的时候,我想用裸机while循环,只是ADC采集加串口收发,确实能跑,但是当DHT22、ESP8266、OLED、按键、水泵控制这些外设都堆进来之后,轮询顺序就变成了噩梦:DHT22的延时要求不能被串口阻塞打断,OLED刷新也不能和ADC采样抢总线。所以最终决定用FreeRTOS,把不同功能拆成独立任务,由调度器分配时间片。

任务划分如下:

任务名优先级周期/触发条件功能说明
vTask_adc3100ms周期读取两路土壤湿度并做滤波
vTask_dht2222s周期读取温湿度,刷新参数
vTask_control4500ms周期根据传感器数据做灌溉决策
vTask_comm2事件触发处理ESP8266下发指令,上报状态
vTask_display1200ms周期刷新OLED显示
vTask_watchdog51s周期喂看门狗,检查水泵状态

任务优先级设计原则:控制任务必须最高,因为它直接影响到安全;ADC任务其次,避免数据来不及时;通信任务优先级低,因为WiFi丢包还能重试,但水泵不能误动。DHT22任务虽然周期长,但读取时不能用高优先级任务打断时序,所以我在代码里对DHT22的IO操作加了临界区保护。

3.2 核心数据流:从传感器采样到控制决策

整个系统数据流处理如下:

  1. vTask_adc采集两路土壤湿度原始电压值,通过中值滤波转换成0~100%湿度百分比,存到全局结构体sensor_data.humidity_A、sensor_data.humidity_B。
  2. vTask_dht22采集温湿度,更新sensor_data.temperature和sensor_data.humidity_air。
  3. vTask_control读取最新的sensor_data,根据预设阈值和修正函数计算出是否需要开启水泵。

控制决策伪代码:

void vTask_control(void *arg) { for (;;) { float humi = sensor_data.humidity_A; float air_humi = sensor_data.humidity_air; float temp = sensor_data.temperature; // 环境温度修正:温度高于30度时,阈值下调5个点,更容易启动浇水 float threshold = config.dry_threshold; if (temp > 30.0f) { threshold -= 5.0f; } if (humi < threshold && !pump_status) { pump_on(); } else if (humi > config.wet_threshold && pump_status) { pump_off(); } vTaskDelay(pdMS_TO_TICKS(500)); } }

注意这里的阈值都是可配置的,可以通过串口命令写入EEPROM或Flash固定存下来,掉电不丢失。我用了STM32片内Flash的最后几页模拟EEPROM,存了喷灌阈值、停止阈值、手动模式标志等参数。

4. ADC多通道采集:DMA模式与采样稳定性的实战细节

4.1 为什么选择"DMA+扫描循环采样"而不是单次转换

土壤湿度传感器是模拟信号,需要ADC转换。如果只用单次转换,每次转换前都需要配置通道,CPU一直等待转换完成,效率太低。而且两个通道切换时还容易产生偏差。用"DMA+扫描循环采样"模式,可以让ADC自动循环扫描两个通道,结果通过DMA自动搬运到内存缓冲区,全程无需CPU干预。这样一套数据流对土壤湿度这种缓变信号来说非常适合。

具体配置要点:

  • ADC工作在扫描模式,连续转换模式开启。
  • 右对齐12位分辨率。
  • 规则组配置两个通道,分别为Channel0和Channel1。
  • DMA配置为循环模式,数据宽度Half Word(16位),因为ADC数据寄存器是16位。
  • 触发方式为定时器定时触发,以固定频率采样。

我用的是定时器2的TRGO事件作为ADC触发源,100Hz采样频率,DMA缓冲为2元素数组uint16_t adc_buf[2],每次更新都会自动覆盖。

4.2 代码实现与关键参数配置

下面给出基于STM32CubeMX生成核心配置的核心代码片段:

ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 2; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }

注意ContinuousConvMode设置为DISABLE,因为我们是外部触发模式,每次触发采集一轮,再由DMA搬运。如果开启连续转换,DMA缓冲区会被不断覆盖,反而难以控制数据同步性。采样时间我选了239.5个周期,对高阻抗传感器来说,这个时间能充分让内部采样电容充电,减少转换误差。

4.3 滤波算法:去掉土壤湿度传感器的白噪声

土壤湿度传感器在土壤分布不均时,同一位置的读数会波动。直接拿原始ADC值去判断阈值,很容易导致水泵频繁启停。我在软件里加了两层滤波:

  • 滑动均值滤波:每次采到新值,与前面4个值求平均。
  • 迟滞比较:启动阈值和停止阈值之间设置5%的区间。比如土壤湿度低于30%时启动水泵,高于45%时才停止,中间区间不做动作。这样避免水泵在阈值边界来回抖动。

具体实现代码:

#define FILTER_SIZE 5 static uint16_t history[FILTER_SIZE]; static uint8_t index = 0; uint16_t adc_filter(uint16_t new_value) { history[index] = new_value; index = (index + 1) % FILTER_SIZE; uint32_t sum = 0; for (uint8_t i = 0; i < FILTER_SIZE; i++) { sum += history[i]; } return sum / FILTER_SIZE; }

DMA回调中每收到一轮转换结果,就调用一次滤波函数,并将结果转换成湿度百分比。湿度百分比计算公式是:

humidity_percent = (max_voltage - voltage) / (max_voltage - min_voltage) * 100;

之所以用"高电压对应低湿度"的映射关系,是因为电容式传感器模块输出电压随湿度升高而降低。如果你选用的是另外的传感器,需要根据实际电压特性调整公式方向。

5. 串口通信:如何用不定长数据搞定ESP8266与传感器

5.1 串口接收不定长数据的HAL库实现

串口通信是本项目的一个关键点。ESP8266的AT指令响应是变长字符串,比如 "OK\r\n" 或 "IPD,10:..."。不能用固定的接收缓冲区长度去等,否则可能会截断数据。标准方案是利用HAL库的接收中断配合RTO(接收超时)或者空闲中断。STM32F103没有硬件空闲中断,所以我用的方法是:串口收到第一个字节后开启单字节中断,并在定时器中设置超时判断。一旦在特定间隔内没有下一字节到来,就认为一帧数据发送完毕,然后处理缓冲区中的整帧数据。

代码思路如下:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { if (rx_index < RX_BUFFER_SIZE) { rx_buffer[rx_index++] = rx_byte; } // 每次收到字节都重置超时计数器 timeout_counter = 0; HAL_UART_Receive_IT(&huart2, &rx_byte, 1); } } // 在1ms定时器中断中 timer_1ms_counter++; if (timer_1ms_counter % 10 == 0) { timeout_counter++; if (timeout_counter > 20) { // 20ms没有新字节,判定一帧结束 process_rx_frame(); rx_index = 0; timeout_counter = 0; } }

这种"字节中断+超时收尾"的方式,比HAL_UART_Receive_IT等待固定长度更灵活,也能处理来自ESP8266的各种不定长响应。后来我还踩过坑:ESP8266上电时会自动输出一堆回显字符串,这些字符串会占据接收缓冲区。所以我上电后先清空缓冲区,等3秒再开始发送AT指令。

5.2 ESP8266 AT指令对接与远程控制协议

ESP8266连接WiFi的AT指令流程:

AT+CWMODE=1 # 设置Station模式 AT+CWJAP="SSID","PASSWORD" # 连接WiFi AT+CIPSTART="TCP","你的服务器IP",8080 # 建立TCP连接 AT+CIPMODE=1 # 透传模式

这里我强烈建议直接用透传模式,也就是AT+CIPMODE=1之后,ESP8266会把收到的TCP数据当普通串口数据直接透传过来,STM32无需处理任何IP协议。上层我定义了一个极简TCP控制协议,每帧以#开头,以;结束,中间用逗号分隔字段。比如:

  • #AUTO,30,45;表示设置自动模式,干阈值30%,湿阈值45%
  • #MAN,ON;表示手动开泵
  • #MAN,OFF;表示手动关泵
  • #QRY;表示查询当前数据

STM32收到完整帧后解析,并返回对应数据帧,例如:

#STATUS,23.5,60,12,ON,OK;

字段含义为温度、空气湿度、土壤湿度、水泵状态、命令执行结果。为了简单,我让ESP8266作为TCP服务器,让手机连接ESP8266的AP或同局域网下的IP,省去了配云平台的麻烦。如果你有公网服务器或内网穿透,也可以换成MQTT,但协议复杂度就上来了,后期再扩展也不迟。

5.3 数据校验:防止WiFi乱码污染控制命令

WiFi透传时,偶尔会因干扰或缓冲溢出出现乱码。为了避免乱码帧被误执行,我在每个控制帧末尾附加了一个简单的校验和字节。校验算法采用累加和取低8位。STM32解析时先校验,再执行命令,校验失败直接丢弃,并返回错误标志。

uint8_t get_checksum(const uint8_t *data, uint8_t len) { uint8_t sum = 0; for (uint8_t i = 0; i < len; i++) { sum += data[i]; } return sum; }

控制帧格式变为:

#CMD,field1,field2,...;CHK

比如#MAN,ON;;3A表示校验和0x3A。这样有效过滤掉了大部分因网络抖动造成的无效指令。

6. 水泵控制:安全防护比控制逻辑更重要

6.1 继电器驱动与光电隔离设计

水泵是整套系统的执行核心,也是最容易出问题的部分。继电器模块的输入端与MCU之间要通过光耦隔离,才能确保水泵侧的高压突变不会反向击穿主控GPIO。在接线时,需要确认继电器的JD-VCC跳线帽是否拔掉,如果不拔,光耦两侧共用电源,隔离效果等于没有。这个问题坑过不少人,很多人直接用跳线帽把两边电源连起来,结果一启动水泵,STM32就死机。正确接法是:

  • 保留跳线帽,但让输入侧和驱动侧共地,这并不完全隔离。
  • 更稳的方案是拔掉跳线帽,从VCC引脚单独给继电器驱动芯片供电。

我用的是双电源方案:主控板用5V供电,继电器驱动侧用12V供电(继电器线圈额定电压就是12V),但信号侧仍然共地。这种方式实际测试很稳,即使水泵堵转,也没有干扰主板工作。

6.2 防干烧、定时锁定与掉电恢复策略

灌溉系统中,水泵放在水箱里,如果水箱没水,水泵空转会发热甚至烧毁。所以我在水泵回路上串联了一个电流采样电阻,用ADC采集电流值。正常运行时电流约350mA,空转时约80mA。一旦检测到电流低于200mA且水泵处于开启状态,就立即关闭水泵并触发蜂鸣器报警。

同时加了个"最大运行时间锁定"机制:无论传感器怎么请求,水泵单次连续运行时间不超过5分钟,防止传感器误判导致超时灌溉。代码里用定时器记录启动时间,每次控制任务里检查运行时长。

对于掉电恢复,我在Flash中保存了水泵的状态,重启后先读取当前土壤湿度,如果湿度依然很低但水泵状态显示是关闭,就说明原来尚未浇透,可以继续灌溉;如果湿度正常,则恢复正常自动逻辑。这里要注意的是,掉电期间水泵可能已经停止但湿度还没来得及恢复,所以重启后不要立刻启动水泵,至少要延迟5秒并检测一次水位。

7. 实测与调试:那些实验室不会告诉你的坑

7.1 土壤湿度传感器电极腐蚀问题

用了一个多月后,我发现土壤湿度读数明显漂移,拆开传感器发现电极上长了绿锈。电容式传感器虽然有防护涂层,但在长期高湿度土壤中,引脚和焊盘位置依然会氧化。建议传感器电极部分涂抹硅胶或指甲油做二次密封,而且传感器探头不要完全埋入土中,只让检测区域入土,接线部分留在空气里。

此外,不同土壤的湿度特性差异很大,同一传感器在腐殖土和沙土中输出电压曲线完全不同。所以我的系统里支持两套校正参数,通过按键切换"土壤类型"设置:沙土和黏土分别对应不同的ADC到湿度百分比转换系数。

7.2 水泵启停瞬间的电源波动与看门狗复位

第一次整体调试时,水泵一启动,OLED屏幕就闪一下黑屏,偶尔还会重启。用示波器测到启动瞬间12V电源线上有超过1V的跌落,5V线也掉了400mV。原因是在水泵启动瞬间,电池换气加电瓶供电能力不足?不,是电源模块压降。我换了纹波更小的LM2596模块后在输出端加了一个大电流肖特基二极管防止回流,同时在泵的电源两端并联了1000uF的电解电容,之后问题解决。

另外,系统运行过程中如果发生死锁,会导致灌水失控,所以软件里开了独立看门狗,喂狗任务每隔1秒就喂一次。但调试时,如果程序进入HAL_Delay时间过长,看门狗就会复位。这个坑让我明白了:RTOS任务里不能太长时间使用阻塞延时,否则看门狗冲突。

7.3 串口调试技巧与日志分级

调试串口是这套系统的命脉,我用USB转TTL接到STM32的串口1,没加电平转换直接通3.3V TTL,电脑端用115200波特率。后来加了ESP8266之后,串口2也参与调试,为了方便区分,我在日志输出中加了打印函数,控制台直接打印彩色文本(支持ANSI颜色的串口助手)区分不同模块。实际操作中,日志格式统一为:

[时间][模块][级别] 信息 [5800][ADC ][INFO] ch0=1234, ch1=2345 [6000][CTRL][WARN] soil_humidity=25.3 < threshold, pump ON [6200][UART][ERROR] check_sum failed, frame discarded

这种日志分级会帮迅速定位问题。比如"干湿度偏低但水泵不启动",看日志发现是ADC通道接反,或者校验错误丢弃了指令,而不是盲目改阈值。

另外,串口调试的一个常见奇怪现象:用HAL库的HAL_UART_Transmit发送长字符串时,如果目标串口连上位机时收发引脚接反,会收不到数据。验证串口通不通最简单的方式是用USB转TTL模块的TX/RX短接,发送一个字符能自己收到,说明模块没问题,再排查接线。

8. 从这套系统延伸出去的更多可能

这套基于STM32的灌溉系统做下来,最让我满意的不是它省了多少浇水的体力,而是整套架构可以在不伤筋动骨的前提下叠加新功能。比如,后期我加了一个土壤NPK传感器(虽然贵但真的有用),只需要增加一个I2C设备,并在ADC任务里扩展一路采集,就能根据土壤肥力调整施肥泵。你也可以换成漏水检测传感器,在阳台地面上铺一条水浸检测线,一旦漏水就自动关闭总阀。只要主控的资源还足够,外设都是"新增一个任务+新增一个驱动"的事。

对于刚入门的人来说,建议先不要急着上FreeRTOS,把裸机版跑通,理解ADC、串口、继电器控制的流程,然后再移植RTOS,你会发现任务调度的思路会让代码结构前所未有的清晰。如果你已经在做类似项目,希望这篇内容能帮你少走一些弯路。我在实际使用中体会最深的一点是:智能灌溉的"智能"不在于泵开多少次,而在于数据采得准不准、控制逻辑稳不稳、异常时敢不敢关。把这三点做好,你的系统才算真正能放心托管整个阳台。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询