基于STM32的智能农业系统:从DHT11传感器到继电器控制全解析

发布时间:2026/9/11 23:11:51
基于STM32的智能农业系统:从DHT11传感器到继电器控制全解析 简介这是一份基于STM32开发板的智能农业系统嵌入式源码包主要面向电子/嵌入式方向学生、农业物联网开发者及需要快速搭建环境监测原型的工程师。项目围绕温湿度采集、光照检测、数据展示与自动控制展开具备从底层驱动到应用层逻辑的完整工程结构可直接在开发板上编译调试也适合作为课程设计或竞赛项目的改造基础。压缩包共19个文件其中9个C源文件与9个H头文件构成核心驱动与业务代码涵盖ADC、DHT11温湿度传感、LCD显示、UART串口、I2C EEPROM、外部中断、LED控制等外设模块另附1个Markdown说明和1个TXT文档便于快速了解工程框架和引脚配置。整体包体仅42KB轻量易部署。该资源目前已有435人学习下载说明其在同类资料中具备一定参考价值。通过阅读源码读者能够掌握STM32外设初始化与中断处理思路并理解如何将传感器数据转换为控制指令可显著缩短智能农业项目的起步时间。1. 智能农业系统为什么选STM32从传感器到执行器的一条完整链路做温室大棚环境监测的项目很多人一上来先想到 Arduino 或树莓派。Arduino 上手快但传感器、LCD、继电器和串口调试堆到一起后引脚和中断资源就紧张了树莓派跑 Linux 灵活可野外配电、启动时间和实时性都是负担。STM32 开发板夹在中间反而把这条链路走通了。这套基于 STM32 的智能农业系统源码包用 DHT11 做温湿度采集ADC 通道接光敏或土壤湿度传感器24CXX 保存阈值参数LCD 显示数据UART 输出日志外部中断处理按键和报警GPIO 直接控制继电器和补光灯。每个功能都拆成了独立模块比如 mydht11.c、myadc.c、24cxx.c、bsp_exti.c很适合正在做 stm32 项目或者准备拿 STM32 做毕业设计的人。看懂这些模块怎么组织就能把一套原型改成自己的农业控制系统。2. 数据采集层驱动DHT11温湿度、ADC光照与24CXX存储的读写实现看到源码包里的文件结构可以先按功能分组mydht11.h/mydht11.c 负责温湿度myadc.h/myadc.c 负责 ADC 采集24cxx.h/24cxx.c 和 myiic.h/myiic.c 负责 EEPROM 存储lcd.h/lcd.c 负责显示uart.h/uart.c 负责串口bsp_led.c 和 bsp_exti.c 负责指示灯与外部中断。这样分组的好处是传感器换型号时只需要替换对应的 .c/.h 文件main.c 里调用接口名不变。下面按采集链路从前到后把这几个模块拆开。2.1 DHT11 时序与数据校验mydht11.c 为什么这么写DHT11 是单总线数字传感器数据引脚既要输出又要输入采集流程分三步主机拉低至少 18ms 发起开始信号释放总线等待从机应答再连续读 40 位数据。40 位里湿度整数、湿度小数、温度整数、温度小数各 8 位最后 8 位是校验和前 4 字节相加取低 8 位等于校验和才算一次有效读取。mydht11.c 里一般会把“读位”和“读字节”拆开我也习惯这么做方便单步跟踪是哪一位时序不对。uint8_t DHT11_ReadBit(void) { uint16_t time_out 0; while (DHT11_IN 0 time_out 100) { time_out; } // 等低电平结束 time_out 0; while (DHT11_IN 1 time_out 100) { time_out; } // 计时高电平宽度 return time_out 40 ? 1 : 0; // 高电平持续时间长视为逻辑 1 }这段代码用循环计数的办法测高电平宽度而不是直接 DelayUs。DHT11 的“0”高电平约 26~28us“1”高电平约 70usSTM32 主频高延时函数经过编译优化后误差不稳定改成读引脚状态加计数更稳妥。DHT11_IN 是宏展开后通常是 GPIO_ReadInputDataBit如果你把数据线从 PB0 换到 PB1只改宏和引脚初始化读位逻辑不用动。两个循环后的 time_out 需要根据实际主频校准第一次跑通可以用串口把读到的字节打出来再微调 40 这个阈值。读取完成后要校验。40 位数据在代码里通常放成一个 5 字节数组校验逻辑非常简单uint8_t DHT11_Check(uint8_t *data) { return data[4] (uint8_t)(data[0] data[1] data[2] data[3]); }data[0] 到 data[3] 依次是湿度整数、湿度小数、温度整数、温度小数data[4] 是校验和。如果校验失败这帧数据直接丢弃不要拿去刷新 LCD 或控制继电器。现场常见的错误是 DHT11 数据线太长或者上拉电阻太大导致边沿变缓读位超时这种情况下把 data[0] 和 data[2] 打印出来看是否稳定。数据段位宽含义第 0 字节8bit湿度整数部分第 1 字节8bit湿度小数部分多数模块为 0第 2 字节8bit温度整数部分第 3 字节8bit温度小数部分多数模块为 0第 4 字节8bit校验和前四字节求和取低 8 位2.2 ADC 采集光照与土壤湿度myadc.c 的通道配置与滤波光敏电阻和土壤湿度探头都靠输出电压变化反映物理量STM32 的 ADC 把电压转成数字。源码包里的 myadc.c 通常只配了一个通道但工程里往往还会再扩展一路所以看清楚 ADC 初始化结构体里的参数比背代码重要。void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); 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_8, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); }GPIO_Mode_AIN 让 PB1 进入模拟输入模式数字输入缓冲关闭能减少引脚内部电路对采样结果的影响。ADC_Channel_8 对应 PB1如果你把土壤湿度探头接到 PA1要改成 ADC_Channel_1改通道的同时还要确认使用的 GPIO 属于 ADC1 的输入范围。ADC_SampleTime_239Cycles5 是相对长的采样时间适合土壤湿度这种内阻和分布电容比较大的传感器低阻信号可以缩短到 55.5 或 13.5 个周期但对农业现场的探头来说不加收敛地缩短采样值浮动会变大。uint16_t ADC_ReadAverage(uint8_t times) { uint32_t sum 0; uint8_t i; for (i 0; i times; i) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动单次转换 while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换结束 sum ADC_GetConversionValue(ADC1); } return sum / times; }这里不是取一次值就用而是连续采 8 到 16 次取平均。times 越大数据越平滑但主循环刷新频率会下降我一般先取 8 次看波形如果抖动大再加到 16 次。ADC_GetConversionValue 返回的是 12 位数值范围 0~4095是否换算成实际电压要看你的传感器分压电路。表里的是常见农业传感器接口选型方便做 PCB 或飞线时对照。传感器输出信号STM32 外设典型引脚DHT11单总线数字GPIO 输入PB0光敏电阻 分压0~3.3V 模拟ADCPA1/PB1土壤湿度探头0~3.3V 模拟ADCPA1/PB1继电器模块低电平触发输入GPIO 输出PB8/PB92.3 24CXX 的 I2C 读写myiic.c 软件时序与掉电保存农业系统里的阈值不能每次改一个值都重新编译烧录。更常见的方案是外接一颗 24C02 或 24C16把温度上限、湿度下限、ADC 阈值写进去开机时读回。源码包里的 myiic.c 和 24cxx.c 是软件 I2C软件模拟的好处是引脚可以随便安排不用跟硬件 I2C 外设绑定代价是时序要靠延时扛换系统主频后可能要重新调延时。uint8_t AT24CXX_ReadOneByte(uint8_t addr) { uint8_t temp 0; IIC_Start(); IIC_SendByte(0xA0); // 设备地址 写操作 IIC_WaitAck(); IIC_SendByte(addr); // 指定 EEPROM 内部字节地址 IIC_WaitAck(); IIC_Start(); // 重复起始条件 IIC_SendByte(0xA1); // 设备地址 读操作 IIC_WaitAck(); temp IIC_ReadByte(); IIC_Stop(); return temp; }第一次 IIC_Start 后发送 0xA0是对 24C02 写入设备地址要改成读必须再发一次 Start然后发 0xA1。很多初学者漏掉中间的重复起始条件结果是读回来的数据全是 0xFF。IIC_WaitAck 里应设置超时SDA 或 SCL 忘加上拉电阻时总线会一直处于异常状态没有超时的话程序会卡在等待应答。地址规划建议用表格固定下来避免后加的字段覆盖前面的。参数EEPROM 地址类型默认值湿度下限0x00uint8_t30湿度上限0x01uint8_t60光敏阈值0x02uint16_t2000报警温度上限0x04int8_t35写入时注意24C02 一页只有 8 个字节连续写多个参数可能触发页边界问题要么按页分组要么每个参数单独调用写一字节函数后加 5ms 等待写周期。把阈值参数从 EEPROM 统一载入到全局结构体main.c 里的业务逻辑只读结构体会减少散落的魔数。2.4 UART 输出与日志协议传感器读出来不打印调试效率会很低。源码包里 uart.c 的作用就是提供串口初始化配合 printf 重定向或者自带的发送函数把温度和湿度输出到电脑。打印格式建议带字段名别只发一串裸十六进制。printf(T:%d H:%d ADC:%d ALARM:%d\r\n, temp, humi, adc_value, alarm_flag);T 是 DHT11 读取的温度整数单位摄氏度H 是湿度整数单位百分比ADC 是 12 位采样原始值范围 0~4095ALARM 是当前报警状态0 表示正常1 表示超过阈值。用 \r\n 结尾是因为大部分串口助手按行显示两个都发才能保证每帧独立一行。如果小程序要用无线模块往云端传这种文本日志只适合调试正式协议要换成定长帧、帧头加 CRC否则出现转义和粘包很难查。3. 裸机事件驱动框架EXTI中断、按键与看门狗怎么配合这段源码包里没有 FreeRTOS 文件而是裸机驱动加中断所以任务调度靠主循环轮询和中断标志位完成。很多人在 stm32 项目里最困惑的也是这一层外设驱动都会写但一到 main 里就变成一长串互相等待的顺序代码。把采集、显示、控制拆成标志位触发的任务是成本最低的裸机调度思路。3.1 用标志位把采集、显示、控制拆开SysTick 定时中断可以代替软件延时作为系统的时基。每 1ms 进一次中断计数到 1000 就置位一秒标志主循环检测到标志后执行一轮传感器读取和界面刷新。volatile uint32_t g_tick 0; volatile uint8_t g_flag_1s 0; void SysTick_Handler(void) { static uint32_t cnt 0; g_tick; if (cnt 1000) { cnt 0; g_flag_1s 1; } } int main(void) { SystemInit(); SysTick_Config(SystemCoreClock / 1000); // 1ms 一次中断 while (1) { if (g_flag_1s) { g_flag_1s 0; DHT11_Read_Data(temp, humi); adc_val ADC_ReadAverage(8); Update_Display(); UART_Send_Status(); Relay_Control(temp, humi, adc_val); } IWDG_ReloadCounter(); } }SysTick_Config 的参数是重装载值用 SystemCoreClock / 1000 正好把中断频率设成 1kHz。g_tick 可以提供给按键消抖和报警计时使用只要注意它是 32 位无符号数长时间运行需要处理回绕或者每次只做差值比较。主循环里每秒钟只做一次完整采集和控制剩下的时间都在空转这样做的好处是 DHT11 读取和 ADC 采样不会因为 LCD 刷新慢而互相干扰。3.2 bsp_exti.c 外部中断与按键消抖的配合bsp_exti.c 对应按键和报警触发。外部中断只负责把事件记录下来不要让中断函数里出现延时空等 GPIO 电平。像这种 20ms 的按键防抖放在主循环里做更合适中断响应反而会造成额外抖动。void EXTI15_10_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line13) ! RESET) { g_key_flag 1; // 只置标志不做消抖 EXTI_ClearITPendingBit(EXTI_Line13); } }EXTI_Line13 对应 PC13 或者其他引脚具体以你的板子和源码包配置为准。中断标志位写入全局变量主循环检测到之后读一次实际电平延时 20ms 再读一次两次一致才认为按键有效这就是“标志位 主循环消抖”的常见做法。如果把延时放在中断里DHT11 读位循环会和 EXTI 抢占同一优先级极端情况下数据位被中断打断温湿度读出来就是错值。3.3 独立看门狗 IWDG 的配置与喂狗策略农业设备放在现场最怕程序跑飞后没人按复位。STM32 的独立看门狗由 LSI 时钟驱动不依赖主时钟即使主时钟故障也能触发复位所以它比软件看门狗更适合无人值守环境。void IWDG_Config(void) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64); // 64 分频 IWDG_SetReload(0xFFF); // 12 位重载值 IWDG_ReloadCounter(); IWDG_Enable(); }IWDG 时钟约为 40kHz64 分频后计数频率约 625Hz重载值 0xFFF 对应超时时间约 6.5 秒。预分频和重载值共同决定超时时间不能只看其中一个参数。喂狗位置要放在主循环的正常流程末尾我一般会保证喂狗间隔不超过超时时间的一半比如 6.5 秒超时就在 3 秒左右喂一次。预分频系数计数时钟频率0xFFF 重载超时162500Hz约 1.6s64625Hz约 6.5s128312.5Hz约 13.1s不要在定时器中断里喂狗否则系统就算卡死在某个 while 循环中断依然会周期喂狗看门狗永远不落地起不到保护作用。调试期间可以先禁止看门狗等程序稳定后再打开不然边界问题还没查到板子一直在重启。3.4 中断与主循环共享变量的临界区g_key_flag 这类变量在中断里写主循环里读必须声明成 volatile。volatile 解决的是编译器优化不能保证原子访问。如果你在中断里更新一个结构体主循环同时读取整个结构体就可能读到一个中间状态。uint8_t value; __disable_irq(); value g_shared_value; __enable_irq();这段代码通过临时关中断来保证访问期间不会被中断打断。对单字节变量来说STM32 本身能保证字节读写的原子性不写临界区也没事但共享变量变成多字节或未对齐时建议先拷贝到局部变量再使用。临界区要尽量短长时间关中断会让 DHT11 的单总线时序或者 UART 接收丢数据。4. 控制与交互LCD菜单、继电器动作与stm32串口调试pid参数整定数据采集完之后系统要能让人看到状态让设备按阈值行动。LCD 负责展示继电器负责输出串口负责把控制过程暴露给调试者。这一章讲的是这三块怎么跟前面的传感器数据接起来。4.1 LCD 内容布局与 FONT.H 点阵字库的调用方式源码包里 lcd.c 和 FONT.H 提供基本字符显示。农业界面不需要华丽信息分层清楚更重要。第一行显示温度和湿度第二行显示 ADC 和当前状态第三行显示阈值或菜单选项。调用点阵字库时注意 FONT.H 里通常只放 ASCII 字符中文显示需要单独准备中文字库。LCD_ShowString(0, 0, Temp:); LCD_ShowNum(5, 0, temp, 2); LCD_ShowString(0, 16, Humi:); LCD_ShowNum(5, 16, humi, 2); LCD_ShowString(0, 32, Set:); LCD_ShowNum(5, 32, humidity_low_limit, 2);LCD_ShowString 和 LCD_ShowNum 的参数含义取决于 lcd.c 驱动有些屏以像素坐标计算有些以字符行列计算。FONT.H 里常见的 8x16 点阵行坐标多半按 16 递增列坐标按 8 递增写成 0,16,32 就是在同一列隔行显示。如果你发现字符重叠或偏出屏幕先改成单行显示一个变量确认坐标规则后再铺满。屏型典型接口工程复杂度农业现场适用性16024/8 位并口或 I2C低常用12864并口/SPI中适合中文菜单OLEDI2C/SPI低低功耗场景4.2 继电器控制与阈值回滞从土壤湿度到电磁阀控制水泵或补光灯最直接的做法是 ADC 读到的土壤湿度低于阈值就开高于阈值就关。但现场 ADC 值会抖动阈值卡在临界点时继电器会在几秒内反复吸合关断于是要用回滞区间回避这个问题。#define SOIL_WET_LOW 1800 // 低于此值开始浇水 #define SOIL_WET_HIGH 2200 // 高于此值停止浇水 void Relay_Control(uint16_t soil_adc) { if (soil_adc SOIL_WET_LOW relay_state 0) { relay_state 1; GPIO_SetBits(GPIOB, GPIO_Pin_8); // 打开继电器 } else if (soil_adc SOIL_WET_HIGH relay_state 1) { relay_state 0; GPIO_ResetBits(GPIOB, GPIO_Pin_8); // 关闭继电器 } }SOIL_WET_LOW 和 SOIL_WET_HIGH 之间就是滞回区。土壤湿度在 1800 到 2200 之间时继电器保持原状态不产生频繁通断。relay_state 记录当前状态如果只在函数内部定义静态变量外部要查看状态就麻烦一点建议直接放全局。GPIO 输出电流不能直接驱动继电器线圈中间要加三极管或 ULN2003否则 STM32 引脚很快烧掉。4.3 UART 串口调试 PID 参数整定如果系统要控制风机转速或电磁阀开度开关量不够才需要 PID。stm32 串口调试 PID 的第一步不是调系数而是把过程数据打印出来。控制周期固定比如 100ms 一次每次打印目标值 SP、当前值 PV 和输出值 OUT。printf(SP:%d PV:%d OUT:%d\r\n, setpoint, current_value, pid_output);SP 是目标值PV 是当前传感器采样值OUT 是 PID 计算输出。用串口助手看文本波形能直观看到超调量和收敛时间。Kp 调大响应快但容易震荡Ki 负责消除稳态误差Kd 抑制过冲修改一次只动一个参数记录下响应曲线再改下一个。田间环境变化慢控制周期 500ms 到 1s 也够用过短反而让 PID 输出在阈值附近跳动。如果需要频繁整定把三个系数放在 EEPROM 里用串口下发命令更新比每次改代码烧录更实用。4.4 参数表与 EEPROM 联动菜单修改后怎么保存LCD 菜单里修改阈值修改完按保存键写入 EEPROM。开机时先读读到的值异常就回退默认值。这样用户改完设置断电也不会丢。if (save_flag) { AT24CXX_WriteOneByte(0x00, humidity_low_limit); AT24CXX_WriteOneByte(0x01, humidity_high_limit); save_flag 0; }保存键按下后置 save_flag主循环执行写入。写入地址和前面的参数表对应不能临时换地址。24C02 页写只有 8 字节如果一次要写多个不连续地址建议每个字段单独写并加延时。芯片第一次上电时所有字节都是 0xFF启动时读到 0xFF 要主动恢复默认值否则 LCD 上会显示不合理的阈值进而导致继电器乱动作。整套“阈值参数 回滞控制 EEPROM 保存”的框架换到室内场景就是 stm32 鱼缸的加热棒、补光灯和自动喂食器差别只在传感器和执行器的数量代码组织方式基本不用变。5. stm32标准库新建工程把源码包裁剪成自己的农业项目最后给一个可复现的移植动作。解压 stm32-main.zip 后先不要急着改代码打开工程确认芯片型号。源码包大概率是针对某块具体开发板的 Keil 工程换成自己板子直接编译轻则报找不到器件重则烧录后完全不亮。按 stm32 标准库新建工程的经验我只从原包里复制一组核心文件再逐步加模块。文件作用移植检查点main.c流程调度与 SysTick、延时初始化匹配mydht11.c/.h温湿度数据引脚宏myadc.c/.hADC 采样引脚和 ADC 通道号bsp_exti.c/.h按键/报警中断NVIC 优先级分组24cxx.c myiic.c/.hEEPROMSDA/SCL 上拉lcd.c/.h FONT.H显示接口时序uart.c/.h串口晶振频率和波特率新建工程时把芯片型号和启动文件选对比如 STM32F103ZET6 用 startup_stm32f10x_hd.s并在编译器预处理里定义 STM32F10X_HD否则外设寄存器地址可能对不上。之后先编译最小集也就是 main、uart、led 能点亮跑通再逐个加 LCD 和传感器。#define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOB这段宏集中放在 mydht11.h 顶部换引脚只改三个位置。DHT11_GPIO_PIN 改成 GPIO_Pin_5 时对应的 GPIO 时钟通常是同一个 RCC_APB2Periph_GPIOB不一定要改但如果换到 GPIOADHT11_RCC 必须同步改成 RCC_APB2Periph_GPIOA。ADC 引脚类似从 PB1 换到 PA1 时除了 GPIO 初始化ADC_RegularChannelConfig 里的通道号也要从 ADC_Channel_8 改成 ADC_Channel_1。真正容易踩的坑是系统时钟。同一个标准库工程8MHz 外部晶振和 12MHz 外部晶振要使用不同的 RCC 配置否则外设波特率全部偏移串口打印乱码。DHT11 两次采集之间至少等待 2 秒连读会失败LCD 显示乱码先查型号是 1602 还是 12864不同驱动文件不能混用。按这个顺序把检查做完再编译下载看到 DHT11 出数、LCD 亮字、按键能进菜单就说明这套基于 STM32 的智能农业系统已经跑在自己的开发板上了。本文还有配套的精品资源点击获取