1. 粮仓环境的真实痛点:监测什么、为什么重要
前几年帮老家一个粮库做过一次巡检系统改造,当时陪我下仓的老保管员说了句话让我印象特别深:“这个仓要是半夜闷热返潮,一仓粮能毁掉一半,等第二天早上发现,神仙来了也救不回。”粮食存储这事儿,最怕的不是老鼠,是看不见的温湿度和突发火情。很多团队做STM32毕设或者农业物联网项目时,习惯把粮仓环境监测当成“温湿度采集 + OLED显示”的简化任务,但真正跑过现场的人都知道,粮仓环境安防远远不止读两个数据那么简单。
这套系统的定位很明确:用STM32做一套适合粮仓场景的本地环境监测与安防终端,采集温度、湿度、烟雾浓度、火焰信号和人体入侵信号,超限时声光告警,同时驱动排风扇等执行设备。代码、原理图、Proteus仿真全部开源,适合正在找STM32项目练手的嵌入式学习者,也适合想做农业物联网课程设计或者小规模粮仓改造的工程师参考。项目本身不复杂,但把“环境监测”和“安防”两套逻辑完整地揉进一个STM32F103C8T6里,涉及到的单总线时序、ADC采集、多路告警调度和仿真联调,都值得掰开揉碎讲一遍。
1.1 温度和湿度是粮食安全的头号杀手
粮食在存储过程中最核心的生命活动是呼吸作用。温度每升高10℃,粮食呼吸强度大概会翻倍,呼吸消耗干物质不说,还会释放水分和热量,让粮堆局部“出汗”。当温度掉到25℃以上、相对湿度超过70%的时候,霉菌和储粮害虫的繁殖速度会进入指数区间。换句话说,粮仓里最需要盯的两个指标就是温度和湿度,这俩数据一旦越线,霉变和虫害几乎不可避免。
传统做法是保管员每天定时拿手持温湿度计逐仓巡查,记录成纸质表格。这里有两个致命问题:一是夜间和非工作时间出现温湿度异常根本没人知道,等第二天开门已经晚了;二是大仓不同部位温湿度差异很大,单点手持测量覆盖不全。这套系统在每个监控节点实时采集温湿度,超过阈值立刻告警,等于把“人盯数据”变成了“系统盯数据”。
1.2 烟雾、火焰、非法入侵:安防的另一半
粮仓的安防需求很容易被忽略。粮粉在特定浓度下是易燃易爆的,电气线路老化、人员违规吸烟都有可能引发火情;同时粮仓一般位置偏远,夜间防盗压力也不小。单纯做“环境监测”不做“安防”,只能算完成了一半。
所以这套系统在温湿度采集之外,增加了三个安防维度:
- 烟雾浓度检测:通过MQ-2气体传感器实时监测烟雾,发生火情时能在早期发现。
- 火焰检测:红外火焰传感器识别火光,作为烟雾告警的交叉验证,减少误报。
- 人体入侵检测:被动式红外热释电传感器(HC-SR501)感知闯入者,夜间告警提醒值守人员。
四路信号在STM32里统一调度,用不同的告警策略区分“环境异常”和“安防事件”,这样不会一有风吹草动就乱响,也不会漏报真正的问题。这个需求拆解的过程,恰恰是很多教程没讲透的部分——拿到原理图先别急着画板子,先搞清楚系统到底要感知哪些物理量。
2. 系统架构与硬件选型:这套方案为什么这么配
硬件选型是项目能不能顺利跑起来的第一道坎。很多初学者一上来就想上顶级芯片,结果外围电路复杂、调试困难,项目烂尾。这套系统选件的基本原则是:在满足性能的前提下,优先选资料多、成本低、容易买到的器件。
2.1 主控选型:STM32F103C8T6凭什么够用
主控选了STM32F103C8T6,这是一颗基于ARM Cortex-M3内核的芯片,主频72MHz,64KB Flash,20KB SRAM。有人会问:粮仓监测是不是得用更高端的芯片来保证可靠性?实测下来的结论是没必要。
从资源占用角度算一笔账:
| 资源 | 用途 | 占用情况 |
|---|---|---|
| GPIO | DHT11、LCD1602/OLED、按键、LED | 约15个引脚 |
| ADC | MQ-2烟雾传感器模拟输出 | 1个通道 |
| 定时器 | 系统节拍、延时延时 | 1-2个 |
| 串口 | 调试日志输出 | 1个 |
把这些全部算进去,Flash和SRAM的占用率都不超过一半,余量足够后续扩展。而且这颗芯片可以说是STM32生态里资料最丰富、Proteus仿真支持最成熟的型号之一,遇到问题搜解决方案一搜一大把,非常适合作为教学和工程验证的载体。
2.2 传感器与执行器选型清单
整个系统的硬件清单如下:
| 模块 | 型号/方案 | 作用 | 选型理由 |
|---|---|---|---|
| 温湿度采集 | DHT11 | 采集温度和相对湿度 | 单总线协议,接线简单,够用 |
| 烟雾检测 | MQ-2 | 检测烟雾/可燃气浓度 | 模拟+数字双输出,灵敏度可调 |
| 火焰检测 | 红外火焰传感器模块 | 识别火光 | 数字电平输出,接口简单 |
| 人体感应 | HC-SR501 | 感知人体红外辐射 | 被动式热释电,误报率低 |
| 显示 | LCD1602(或OLED SSD1306) | 本地显示温湿度与告警状态 | 16x2字符显示,调试直观 |
| 告警 | 有源蜂鸣器 + LED | 声光告警 | 有源蜂鸣器单片机直接驱动不了,需要三极管 |
| 输出控制 | 继电器模块 | 驱动排风扇 | 隔离大负载,安全可靠 |
| 电源 | USB 5V + AMS1117-3.3V | 系统供电 | 稳压简单,成本极低 |
这里重点解释一下DHT11和DHT22的选择。DHT11的精度是±2℃、±5%RH,在粮仓这种场景下够不够?坦白说,做精细化粮库监测,这个精度偏粗,更推荐DHT22(±0.5℃、±2%RH)。但DHT11的优势在于单总线时序简单、代码量小、仿真模型成熟,作为项目学习和技术验证完全够用。如果需要高精度方案,软件层只需要替换驱动函数,上层逻辑不用动——这也是把传感器驱动和业务逻辑分开写的价值所在。
2.3 整体工作流程
系统上电后先自检初始化,LCD显示启动界面,然后进入主循环:
- 定时器产生2秒节拍,触发温湿度采集(DHT11的数据手册要求两次读取间隔大于1秒,太频繁会读取失败)。
- 周期性读取烟雾浓度ADC值,检测火焰传感器和人体感应模块的电平状态。
- 将采集数据刷新到LCD。
- 在告警判断函数里做阈值比较和消抖处理。
- 超限时触发声光告警,并通过继电器启动排风扇/打开应急照明。
这套流程的逻辑非常清晰,后面代码部分会详细展开。
3. 原理图设计:每个模块背后的设计逻辑
原理图是这套系统最容易被“抄完就忘”的部分。很多人拿到原理图直接用,但不知道为什么这里要加上拉电阻、那里要加RC滤波。把这些设计逻辑讲清楚,才是真正吃透项目。
3.1 电源与最小系统:稳定是底线
电源部分采用了USB 5V输入 + AMS1117-3.3V稳压的方案。为什么不用ST-Link直接给3.3V?因为MQ-2的加热回路需要5V供电,DHT11和LCD1602的典型工作电压也是5V,所以系统必须同时存在5V和3.3V两路电源。
AMS1117-3.3的输出端要加10uF和100nF去耦电容,一个负责低频稳压,一个负责高频噪声滤除。STM32F103C8T6的每个VDD引脚旁边也建议就近放一个100nF去耦电容,这是很多初学者容易漏掉的小细节,不加也能跑,但ADC采样精度和系统稳定性会打折扣。
最小系统部分有几个关键字——8MHz外部晶振配两个22pF负载电容、复位电路用10k上拉电阻加100nF电容、BOOT0和BOOT1通过10k下拉电阻接地保证从Flash启动、SWD下载口引出PA13/PA14。这里特别提醒一点:SWD接口一定要把GND也引出来,否则下载器经常连不上目标板。
3.2 传感器接口电路:上拉、分压、隔离
DHT11的输出引脚是漏极开路结构,数据线上需要接一个4.7k-10k的上拉电阻到VCC。上拉电阻太小会让总线驱动能力不足,太大会导致上升沿变缓,实测4.7k最稳。
MQ-2模块的AO引脚输出的是模拟电压,直接接到STM32的PA1引脚即可。但注意MQ-2的AO输出范围是0-5V,而STM32的ADC输入范围是0-3.3V——市售MQ-2模块上往往自带一个可调电位器和比较器,模块的DO输出是TTL电平可以直接接,但AO输出直接接ADC引脚是有风险的。稳妥的做法是用两个电阻做分压(比如10k和20k串联,将5V分压到3.3V以内),或者直接购买3.3V兼容版的MQ-2模块。
HC-SR501的输出是3.3V兼容的数字电平,可以直接接GPIO。这个模块上有两个可调电位器,分别调节感应距离(0-7米)和延时时间(5秒-300秒),用在粮仓安防场景建议把延时调到最小、灵敏度调到中间档。
3.3 告警与驱动电路:GPIO怎么带动大负载
STM32的GPIO最大输出电流只有约20mA,直接驱动蜂鸣器和继电器的线圈肯定不够,必须加驱动电路。蜂鸣器用S8050 NPN三极管驱动,GPIO通过1k基极电阻接三极管基极,蜂鸣器接在集电极和VCC之间,发射极接地。GPIO输出高电平时三极管导通,蜂鸣器响起。
继电器驱动用的是ULN2003达林顿管阵列,这个芯片内部自带续流二极管,可以直接驱动继电器线圈。这里有一个反复被问的问题:为什么不用三极管?因为继电器线圈是感性负载,断电瞬间会产生很高的反向电动势,如果不加续流二极管会击穿驱动管。ULN2003内部集成了这个保护,设计上省心很多。如果是单路继电器,用一个NPN三极管加一个1N4007续流二极管也完全可以。
4. 代码实现:驱动、调度与告警逻辑
整套代码是在Keil MDK5环境下,基于STM32标准外设库完成的。工程结构分为四层:硬件驱动层(DHT11、ADC、LCD)、业务逻辑层(采集任务、告警任务)、调度层(主循环+定时器标志位)、配置层(时钟、GPIO、中断初始化)。
4.1 工程结构与代码布局
建议的工程文件组织方式:
| 文件/文件夹 | 职责 |
|---|---|
| core/ | 启动文件、系统时钟配置 |
| user/ | main.c、stm32f10x_it.c 中断服务函数 |
| hardware/ | dht11.c、adc.c、lcd1602.c、alarm.c、key.c |
| system/ | 系统初始化、延时函数 |
把硬件驱动和业务逻辑分开,最大的好处是换传感器或者改逻辑时不用动底层驱动。比如从DHT11换成DHT22,只需要重写dht11.c里的读写时序,上层调用函数接口不变。
4.2 DHT11单总线时序的软件实现
DHT11的时序是整个代码里最讲究的部分。单总线协议没有时钟线,靠严格的延时和电平翻转来传输数据,容错窗口通常在10-30us级别,所以延时函数的精度直接影响通信成败。
读取一次温湿度的完整流程:
- 主机把总线拉低至少18ms,发出起始信号。
- 主机释放总线,并延时20-40us,等待DHT11响应。
- DHT11拉低总线80us作为应答,然后拉高80us表示即将发送数据。
- 随后DHT11依次发送40位数据:8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。
- 每位数据的解码规则是:先有50us低电平,紧接着高电平。高电平持续26-28us表示逻辑0,高电平持续70us表示逻辑1。
核心读取代码:
uint8_t DHT11_ReadByte(void) { uint8_t i, data = 0; for (i = 0; i < 8; i++) { // 等待50us低电平结束 while (DHT11_PIN == 0); // 延时约30us后采样电平 Delay_us(30); if (DHT11_PIN == 1) { data = (data << 1) | 0x01; } else { data = (data << 1) & 0xFE; } // 等待高电平结束 while (DHT11_PIN == 1); } return data; }读取完整数据的流程:
uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i; // 主机拉低至少18ms,发送起始信号 DHT11_PIN_OUT(); DHT11_PIN_LOW(); Delay_ms(20); DHT11_PIN_HIGH(); Delay_us(30); DHT11_PIN_IN(); // 检查DHT11应答信号 if (DHT11_PIN == 1) return 1; // 无应答 while (DHT11_PIN == 0); // 等待应答低电平结束 while (DHT11_PIN == 1); // 等待高电平结束 // 连续读取5字节(含校验) for (i = 0; i < 5; i++) { buf[i] = DHT11_ReadByte(); } // 校验和验证 if (buf[4] != (buf[0] + buf[1] + buf[2] + buf[3])) return 2; // 校验失败 *humi = buf[0]; *temp = buf[2]; return 0; }这个代码里有几个细节值得注意:一是Delay_us(30)必须放在判断之前,放在判断之后读到的高电平就包含了70us和26us两种可能,无法区分0和1;二是主循环调用DHT11的间隔不能小于1秒,否则传感器无法完成内部测量,会一直返回0。这也是为什么把采集放到2秒定时器节拍里。
4.3 ADC采样与烟雾浓度换算
MQ-2的AO输出接到STM32的PA1引脚,对应的ADC通道是ADC1_IN1。配置ADC1工作在连续转换模式,采样时间为55.5周期,确保阻抗匹配。读取代码:
uint16_t ADC_ReadChannel(uint8_t channel) { ADC1->SQR3 = channel; // 选择转换通道 ADC1->CR2 |= (uint32_t)1 << 22; // 启动转换 while (!(ADC1->SR & (1 << 1))); // 等待转换完成 return ADC1->DR; // 返回12位ADC值 }烟雾浓度的判断不能只看单次ADC值。MQ-2在刚上电的几十秒内读数会飘,因为内部加热丝需要时间达到工作温度。我的做法是在开机后等待MQ-2预热完成(约60秒),启动时保存一个基线值,然后用当前读数相对基线的偏差作为判断依据,这样可以抵消不同环境下的浓度基准差异。
uint8_t SMOKE_GetLevel(uint16_t baseline, uint16_t current) { // 偏差超过200个ADC码值认为有烟雾 if (current > baseline + 200) return 1; return 0; }4.4 告警消抖与状态机设计
告警逻辑最容易犯的错误是一读到超限就立刻触发告警。传感器瞬间波动、电磁干扰、甚至灰尘飘过探测口都会造成毛刺,直接触发的后果就是蜂鸣器乱叫,值守人员很快就会失去信任。所以告警函数里做了一次数值消抖:连续3次采样都超限才确认告警。
void Alarm_Task(void) { static uint8_t confirm_count = 0; uint8_t danger = 0; // 条件判断 if (temp > TEMP_ALARM_THRESHOLD || humi > HUMI_ALARM_THRESHOLD || smoke_adc > smoke_baseline + SMOKE_DEVIATION || fire_detected || pir_detected) { confirm_count++; if (confirm_count >= 3) { danger = 1; } } else { confirm_count = 0; } if (danger) { Alarm_On(); // 蜂鸣器响、LED闪烁、继电器动作 } else { Alarm_Off(); } }同时,不同的告警源应该有不同的表现方式。温湿度超限属于“环境预警”,蜂鸣器慢速间歇鸣响;烟雾和火焰属于“火情告警”,蜂鸣器快速长鸣;人体入侵属于“安防事件”,蜂鸣器短促三声循环。这样值守人员光听声音就能判断现场出了什么类型的问题,不用每次跑过去看屏幕。
5. Proteus仿真:从工程到跑通的完整过程
Proteus仿真最大的价值在于可以不焊接一块板子就把代码逻辑验证一遍。但这个环节恰恰是无数人卡壳的地方——很多人连仿真图都搭不起来。下面把从我这边实测跑通的完整流程拆开讲。
5.1 仿真环境的搭建与元件准备
Proteus版本建议用8.9以上,对STM32F103C8T6的支持比较完善。新建工程后,在元件搜索栏里依次添加:
| 搜索关键字 | 元件 | 说明 |
|---|---|---|
| STM32F103C8T6 | MCU主控 | 在“ST Microcontrollers”分类下 |
| DHT11 | 温湿度传感器 | 旧版本Proteus没有,需要更新库文件 |
| LM016L | LCD1602液晶屏 | 标准字符液晶模型 |
| MQ-2 | 气体传感器 | 部分版本用MQ-2模型或电压源替代 |
| BUZZER | 有源蜂鸣器 | 需要配合三极管驱动 |
| LED-RED / LED-GREEN | 指示灯 | 告警状态显示 |
| RESISTOR / CAPACITOR | 电阻电容 | 按原理图参数设置 |
| BUTTON | 按键 | 阈值设置/复位 |
| POT-HG | 电位器 | LCD对比度调节 |
如果搜索不到DHT11,有一个替代思路:用两个电压源模拟DHT11输出,或者用Proteus自带的DHT55传感器模型来验证逻辑。但这样没法验证时序代码对不对,所以还是建议安装带完整元件库的版本,或者从Proteus官网更新库文件。
5.2 编译生成hex并加载到Proteus
代码在Keil里编译生成hex文件的方法:
- 在Keil工程中点击魔术棒(Options for Target)。
- 在Output选项卡勾选“Create HEX File”。
- 编译工程,在输出目录下生成.hex文件。
然后在Proteus中,双击STM32F103C8T6元件,在弹出的属性对话框里,将Program File路径指向刚生成的hex文件。这里要注意两个坑:
- Crystal Frequency设置:必须设置为8MHz,与工程代码里的SystemInit配置的外部晶振一致。如果设为默认值0,仿真时钟直接乱了,延时函数会出错。
- 电源引脚:Proteus的STM32模型虽然会自动供电,但如果仿真中使用了PA0等模拟输入引脚,记得在VSSA/VDDA引脚上正确接电源,否则ADC读到的数据是乱的。
加载完成后点击运行。正常的现象是:LCD显示当前温度和湿度,调节仿真中的DHT11湿度值或温度值,LCD数据跟着变化;按下对应按键触发超限,蜂鸣器和LED开始告警。
5.3 仿真中踩到的三个坑
第一个坑是DHT11仿真时序与实物不一致。Proteus的DHT11模型对延时的敏感度比实物更高,实物上30us的容错窗口在仿真里可能只有10us。解决方法是微调代码里的延时参数,把Delay_us(30)改成Delay_us(50)左右,前提是你用的是带仿真模型的DHT11。如果怎么调都读不到数据,还有一个土办法:把DHT11换成两个可变电阻模拟温湿度变化,用ADC读取来代替单总线读取,先把整体流程跑通,再回来搞时序。
第二个坑是LCD1602显示乱码或全屏方块。最常见原因是仿真里LCD的对比度调节引脚(Vo)直接接地了,没有接电位器。加上一个10k电位器,把Vo电压调到1V左右(LCD工作电压的10%-20%),显示就正常了。
第三个坑是蜂鸣器不响。Proteus里的蜂鸣器分有源和无源两种,有源蜂鸣器只要通电就会发声,无源需要PWM才能响。仿真里如果上电不响,先检查型号是不是SOUNDER(有源/无源模型不同),再看驱动管是否型号匹配。这里有一个小技巧:Proteus仿真的蜂鸣器很多时候是“逻辑上已经响起、但电脑声卡没输出”——看仿真里的电流表和驱动管状态就能确认。
6. 从仿真到实物:部署经验与扩展方向
仿真跑通只是完成了一半,实物搭建和现场部署才是真正检验系统可靠性的地方。这个环节有很多仿真里根本不会暴露的问题,非常值得单独拿出来说。
6.1 实物搭建中容易被忽略的细节
DHT11在实物调试里的头号问题是IO口方向切换的时序。STM32的GPIO配置为输入和输出之间切换需要大约几个时钟周期,标准外设库里的GPIO_Init调用开销比较大。一个可行的做法是把DHT11的数据线接到一个独立IO,用寄存器操作快速切换方向:
#define DHT11_PIN_OUT() { GPIOB->CRH &= 0xFFFF0FFF; GPIOB->CRH |= 0x00003000; } #define DHT11_PIN_IN() { GPIOB->CRH &= 0xFFFF0FFF; GPIOB->CRH |= 0x00004000; }这样配置方向只需要两条寄存器指令,比调用库函数快几十倍,实测在48MHz主频下能稳定读取DHT11。
第二个是供电问题。MQ-2的加热丝最大工作电流在150mA左右,如果和STM32共用一组USB供电,电源线上的纹波会影响到ADC采样。建议的做法是:MQ-2单独从5V端取电,并在模块电源引脚就近加一个100uF电解电容和一个104瓷片电容,把加热丝的瞬态电流波动吸收掉。
第三个是继电器带负载的问题。继电器触点驱动排风扇这种感性负载时,触点闭合断开的瞬间会产生电弧,长期使用会造成触点烧蚀。在继电器输出端并联一个RC吸收电路(典型值100Ω + 104电容串联),能有效延长继电器寿命。
6.2 粮仓现场的实用建议
如果这套系统真的要用在粮仓环境里,有几个点比代码和原理图更关键:
传感器的布点位置不能只放一个点。粮仓的不同高度和位置温湿度差异很大,靠近门窗的位置湿度高、粮堆中心温度高。至少要在仓内东南西北四个角落各装一个节点,再在粮堆表层和距表层50cm深处各埋一个测温点,才能形成有效的监测网络。这套单节点方案可以作为每个点位的基础单元,后续通过RS485总线把多个节点组网,把数据汇总到上位机。
传感器的防护很关键。粮仓内粉尘浓度高,DHT11的感湿元件如果直接裸露在环境里,用不了几个月就会被粉尘覆盖,测量值严重偏移。建议给传感器加一个透气防尘罩,或者把传感器放在一个开有小孔的塑料盒子里。MQ-2也面临同样的问题,它的防爆网罩结构本身能防尘,但网罩上的粉尘也需要定期清理。
告警阈值不能拍脑袋定。不同的粮食品种对温湿度要求不同,小麦的安全水分是12.5%左右,稻谷是14%左右,对应的仓储环境湿度阈值最好参考《粮油储藏技术规范》来设置。我预留了通过按键调节阈值的功能,就是为了让使用者根据实际存储品种灵活调整。
6.3 后续扩展方向
这套系统在扩展性上留了很多接口。软件层面,主循环的调度结构可以很容易地加入新传感器——只需要在硬件驱动层增加一个文件,在Alarm_Task里增加一个判断条件。硬件层面,如果要做远程监控,可以在串口1上挂一个ESP8266模块,把温湿度和告警状态通过MQTT协议上报到云平台,实现手机远程查看。要做低功耗的话,STM32F103C8T6可以进入待机模式,配合RTC定时唤醒,用外部中断唤醒做入侵检测,一块18650电池可以支持几个月。
如果要组网做多节点方案,建议把通信链路从单总线换成RS485,DHT11的数据通过传感器节点上的小MCU预处理后,再通过RS485总线上传。这样既解决了长距离布线的抗干扰问题,也让每个节点具备本地独立判断能力,不会因为总线上某个节点故障而影响整个网络。
我在实际做的过程中最大的感受是:仿真和实物之间的差距,就是理论学习和工程实践的差距。仿真帮你验证了逻辑,但实物会告诉你什么是电源纹波、什么是时序容错、什么是接触不良。这套系统从画原理图到最终在粮库角落稳定运行,前前后后调试了将近两周,踩的坑远比这篇文章里写得多。但正是因为这些坑,整个系统的每一行代码、每一颗电阻的取值,才能讲得清楚来龙去脉。这也是我决定把它们整理成开源项目的初衷——不要让后人重复踩同样的坑。