自己一直在折腾嵌入式项目,手头也有不少STM32开发板,但真正把一整套需求、原理图、代码、仿真串起来的项目,还是这个图书馆环境监测系统让我收获最大。一方面是它贴近真实场景,另一方面它把传感器采集、数据处理、控制执行、人机交互全走了一遍,非常适合作为STM32的综合练手项目,也能直接用在实际的实验室、办公室、书库环境里。
这篇文章我会把整套方案从头到尾拆开讲:为什么这么设计、原理图怎么画、代码怎么写、仿真怎么做,以及我实际调试过程中踩过的一些坑。代码、原理图、仿真工程文件我都做了整理,属于直接可以拿走的开源方案,你在自己板子上改改引脚就能跑起来。
1. 项目思路与系统架构
1.1 为什么是图书馆环境监测
图书馆、档案室这类场所,对环境参数有比较严格的要求。温度和湿度过高,纸质书刊容易发霉变形;光照过强,书籍封面会褪色老化;空气质量差,尤其是粉尘和异味超标,直接影响阅读体验。传统做法是靠空调和除湿机手动开关,但人不可能二十四小时盯着温湿度计,更不可能根据实时数据精准控制设备。
环境监测系统解决的就是这个“被动管理”的问题。用STM32持续采集环境数据,超过阈值自动联动风扇、除湿机、补光灯等设备,把环境控制在合理区间。同时把实时数据展示在屏幕上,方便管理人员查看。
这个项目选择STM32F103C8T6作为主控,主要看中三点:价格便宜量又足,一颗不到十块钱的芯片,72MHz主频、64KB Flash、20KB RAM,跑这些传感器绰绰有余;外设资源丰富,I2C、SPI、USART、ADC全部齐活;生态成熟,网上资料多,遇到问题基本都能搜到解决方案。
1.2 功能需求拆解
做项目第一步不是画板子,而是想清楚要做什么。我把系统拆成四个维度:
- 环境数据采集:温湿度(DHT11)、光照强度(BH1750)、空气质量(MQ-2)
- 数据处理与控制:STM32读取传感器数据,经过滤波和阈值判断,控制继电器驱动外围设备
- 人机交互:OLED屏幕实时显示环境参数,按键切换显示页面
- 告警提示:数据异常时,蜂鸣器响,同时在屏幕上以醒目标识提示
选传感器的时候有人可能会问,DHT11精度不够高,为啥不用DHT22或者SHT30?我的考虑是,图书馆环境监测不需要实验室级别的精度,能反映趋势和大致范围就够了。DHT11胜在便宜、接线简单、单总线协议也不复杂。最适合入门练手。如果你对精度有要求,代码里我做了接口抽象,换成DHT22也就是改一个驱动文件的事情。BH1750光照传感器走I2C,测到的是真实的勒克斯值,这个对判断室内采光是否达标很有帮助,不用自己调公式。
1.3 系统整体工作流程
整个系统的运行逻辑非常清晰,就是一个典型的“感知-决策-执行”闭环:
STM32上电后先初始化各个外设和传感器,然后进入主循环。主循环里先读取DHT11温湿度、BH1750光照值、MQ-2烟雾浓度;接着对这些原始数据进行滑动滤波;再和预设的阈值进行比较,如果超过阈值就通过继电器开启排气扇、除湿机等设备;最后把所有数据打包,一并在OLED上刷新显示。
这里要特别说下,为什么不用中断而是用轮询?传感器读取本身就是毫秒级别的操作,而且各个传感器之间的读取频率也不一致,DHT11建议读取间隔至少1秒,BH1750连续测量模式下大概120ms更新一次,如果用中断去处理反而把逻辑搞复杂了。主循环轮询配合简单的定时标志位,完全够用,代码也更容易理解和维护。
2. 硬件与原理图设计
2.1 最小系统电路细节
STM32F103C8T6的最小系统看起来简单,BOOT0、复位、晶振、电源,但每个部分都有讲究。晶振我用了8MHz无源晶振,配合两个20pF负载电容。这个地方很多人直接用芯片内部的RC振荡器省掉晶振,我的建议是如果你要用USART通信,最好还是把外部晶振加上,内部RC的精度在波特率要求高的时候会出问题。
复位电路用一个10k上拉电阻加一个0.1uF电容,再串联一个按键到地。按键按下时NRST引脚被拉低,芯片复位。这个电路已经是行业标准做法,没必要改动。BOOT0引脚通过10k电阻接地,让芯片从Flash启动,这是正常运行的状态。如果你想用串口下载程序,把BOOT0拉高再复位,程序下载完再拉回来。
电源部分我单独说,因为踩过坑。系统需要5V和3.3V两路电压,5V给继电器,3.3V给主控和传感器。我用AMS1117-3.3把USB的5V转成3.3V,在输入端和输出端分别加了10uF和0.1uF的去耦电容。去耦电容不是摆设,数字电路高频开关时会产生电流毛刺,没有电容滤波,传感器采集的数据会出现莫名其妙跳动,尤其是ADC采样时特别明显。
2.2 传感器接口设计
DHT11有三根引脚,VCC、DATA、GND。DATA引脚要接一个4.7k上拉电阻到VCC。单总线协议默认空闲是高电平,主机拉低总线发起通信,上拉电阻的作用就是保证总线空闲时能稳定在高电平。这个电阻不能省,否则通信很容易超时。
BH1750是标准I2C接口,SDA和SCL都要接上拉电阻,一般在4.7k到10k之间。STM32的I2C外设和GPIO模拟I2C都可以,但我最终选择的是软件模拟I2C。原因很实际,STM32硬件I2C确实能用,但配置比较繁琐,时序要求严格,网上吐槽的人也多。软件模拟I2C用两个GPIO口翻转电平实现时序,只要代码逻辑没问题,稳定性其实相当好,而且换任意引脚都能用,硬件布线灵活度大。
MQ-2烟雾传感器输出的是模拟量,接到STM32的PA0引脚,用ADC1的通道0采集。MQ-2上电后需要预热,首次读数会偏高,软件里我做了30秒的预热延时,这段时间数据显示“Sensor Warming Up”而不是直接刷新数值,避免误报警。
2.3 电源与继电器控制
继电器控制是整个硬件里最容易出问题的地方。最开始我用STM32的GPIO直接接继电器模块,结果发现复位现象频繁,后来才意识到是驱动电流不够。GPIO最大输出电流也就20mA左右,而普通5V继电器线圈的吸合电流可能到70-80mA,直接用GPIO带不动。
我的方案是用一个S8050三极管做开关驱动继电器,GPIO输出高电平经过1k电阻到三极管基极,三极管导通,继电器线圈通电吸合。线圈两端要并联一个1N4007续流二极管,方向是反向并联,就是继电器线圈断电瞬间会产生反向电动势,如果不加二极管,这个高压尖峰很可能击穿三极管。
如果你用的是继电器模块,模块上一般自带光耦隔离和驱动电路,就不用自己搭三极管电路了。但要注意,模块的JD-VCC和VCC跳线帽,在使用时如果是单电源供电,跳线帽要插上;如果用隔离双电源,就要拿掉跳线帽,分别供电。
2.4 PCB布局与抗干扰心得
原理图画完之后,Layout是我花时间最多的地方。有一个原则我印象非常深刻,就是“先电源,再信号,最后地”。先把电源走线规划好,电源线的宽度至少要能承载电流需求,信号线再密集也不怕,但电源线细了就会发热、压降大。
传感器接口我全部做成了排针,方便外接模块,没有直接焊死在板子上。这样模块坏了可以单独换,调试时也能方便地拔下来单独测试模块。板上我预留了SWD下载接口,四根线:SWDIO、SWCLK、VCC、GND,用ST-Link就能下载调试。另外还预留了USART1的串口引脚排针,方便外接USB转TTL模块打印日志。
PCB走线有一个细节值得提醒:继电器的控制信号线要离信号采集的模拟线远一点,继电器吸合和释放的瞬间会产生电磁干扰,如果走线太近,ADC采集的数值会有波动。我第一版板子就吃了这个亏,风扇一启动,光照数值跳几个点。
3. 代码实现与驱动逻辑
3.1 开发环境与工程配置
代码我是基于标准外设库写的,没有用HAL库。不是说HAL不好,而是标准库更贴近底层寄存器操作,能够帮助你理解芯片工作原理。比如你调用GPIO_WriteBit,你清楚知道这是在操作ODR寄存器;你用HAL_GPIO_WritePin,实际也是封装了同样的操作,但中间多了很多句柄和检查逻辑。
工程结构我是分层来建的:
Libraries/ ├── sys/ // 延时、时钟配置 ├── usart/ // 串口打印调试 ├── i2c_soft/ // 软件模拟I2C ├── sensor/ // DHT11、BH1750、MQ-2 驱动 ├── display/ // OLED 显示驱动 └── main.c // 主逻辑这种方式最大的好处是职责清晰,换传感器时只需要改sensor目录下的对应文件,主逻辑不用动。比如你想把DHT11换成DHT22,新建一个dht22.c,保持和dht11.c相同的接口函数,调用方完全不用改。
时钟配置上,我使用外部8MHz晶振,通过PLL倍频到72MHz。系统时钟SysTick配置为1ms中断一次,所有延时函数都基于这个时基。这里有个小技巧,DHT11的时序要求微秒级延时,1ms的SysTick不够用,我在systick的基础上又封装了一个微秒延时函数,用DWT计数器实现,精度可以达到几微秒级别,非常实用。
3.2 DHT11温湿度读取详解
DHT11的单总线协议是整个项目里最需要耐心的部分。读一个完整数据需要40位,分别是湿度整数部分、湿度小数部分、温度整数部分、温度小数部分、校验和。每一位的读取,靠的是高低电平持续的时间来区分0和1。
读取流程是:主机先拉低总线18ms以上,然后释放并延时20-40us,之后读取从机的响应信号。从机响应信号是80us的低电平和80us的高电平,这个响应信号能收到,说明通信链路是通的。紧接着就是40位数据,每一位都以50us低电平开始,然后是高电平,高电平持续26-28us表示“0”,持续70us左右表示“1”。
代码里最关键的是判断0和1的阈值,我这个延时读取的逻辑实测是稳定的:
uint8_t dht11_read_byte(void) { uint8_t i, data = 0; for(i = 0; i < 8; i++) { while(DHT11_DATA_IN() == RESET); // 等待50us低电平结束 delay_us(30); // 延时30us后采样 if(DHT11_DATA_IN() == SET) { data |= (1 << (7 - i)); while(DHT11_DATA_IN() == SET); // 等待高电平结束 } } return data; }原理是,当50us低电平结束后,数据线进入高电平阶段。如果是“0”,高电平只维持约26-28us;如果是“1”,高电平维持约70us。我在高电平开始后延时30us再采样,此时如果是“0”早就结束了,读到的是低电平;如果是“1”还在高电平阶段,读到的是高电平。用这个办法区分0和1,容错性比单纯测量脉冲宽度好很多。
注意DHT11的读取间隔必须有1秒以上,连续快速读取会导致传感器不响应。我在主循环里用一个计数器做标志,不到1秒不发起新的读取。
3.3 BH1750光照与MQ-2气体采集
BH1750的操作流程比较固定:发送上电命令(0x01),然后发送连续高分辨率测量模式命令(0x10),等待约120ms后读取两个字节的数据,合并成16位数值,再除以1.2就是实际的勒克斯值。
软件模拟I2C我这里用一个具体的启动信号示例:
void i2c_start(void) { I2C_SDA_SET(); I2C_SCL_SET(); delay_us(5); I2C_SDA_CLR(); delay_us(5); I2C_SCL_CLR(); }启动信号就是SCL高电平时SDA从高到低跳变,这个时序必须严格遵守。所有I2C通信的第一步都是这个启动信号,如果从设备没响应,优先检查这个波形是不是对了。没有逻辑分析仪的话,可以用示波器观察SDA和SCL脚的边沿。
MQ-2读取就更直接了,通过ADC采集输出电压换算浓度。MQ-2的浓度和输出电压不是线性关系,严格来说要用对数坐标拟合曲线,但做环境监测只要知道“大致超标了没有”,我用一个简单的百分比来表示:
smoke_percent = (float)adc_value / 4096.0f * 100.0f;如果这个值超过设定阈值(比如60%),就认为空气质量不佳,启动排气扇。如果你要更精确的ppm数值,需要查MQ-2手册的灵敏度特性曲线,做标定,这个后续再展开。
3.4 主控逻辑与联动策略
主循环代码我写成了一个清晰的状态机流程,每轮循环做四件事:采集、处理、控制、显示。采集部分要注意给各个传感器合理的等待时间,不能一上来就疯狂采,尤其是DHT11。
联动控制我设定了三组阈值条件:
| 参数 | 正常范围 | 异常条件 | 执行动作 |
|---|---|---|---|
| 温度 | 18-26℃ | 超过28℃ | 开启排气扇 |
| 湿度 | 45%-65% | 超过70% | 开启除湿机 |
| 光照 | 200-600 lux | 低于150 lux | 开启补光灯 |
| 烟雾 | 低于50% | 超过60% | 开启排气扇并蜂鸣器报警 |
这些阈值都是可以通过按键调节的,我在代码里定义了一个全局结构体保存阈值,按键修改后存入Flash。因为STM32F103的Flash最后一个扇区有1KB也可以拿来存储用户数据,直接用内部Flash模拟EEPROM,省掉一颗外部EEPROM芯片。
显示逻辑上,OLED只有128x64像素,一屏显示不了太多信息,我分了三页:第一页显示温湿度和时间,第二页显示光照和空气质量,第三页显示系统状态和各设备开关状态。按键控制翻页,长按可以进入参数设置模式。OLED的显存刷新是整个屏幕重新刷新,但如果每帧都全屏刷新,I2C的负载会比较大,所以我实现了局部刷新,只有数值变化才更新对应区域,这样显示过程流畅很多。
4. 仿真调试与问题排查
4.1 Proteus仿真环境搭建
Proteus仿真是这个项目开源包里很重要的部分,不用买硬件就能把整体逻辑跑起来。做仿真的话,有几个问题要提前注意。
首先,Proteus里默认的STM32F103C8T6模型是存在的,但是要确认你的Proteus版本支持,太老的版本没有这个芯片模型,建议用Proteus 8.9以上版本。其次,DHT11和BH1750在Proteus元件库里是有仿真模型的,但MQ-2没有现成的,我用一个电位器代替,通过调节电位器电压来模拟MQ-2输出的变化,效果是一样的。
仿真中最常遇到的坑是晶振频率。Proteus仿真时芯片运行频率由模型参数决定,如果你代码里配置的是外部8MHz晶振,但仿真模型默认用的是内部RC,串口波特率就会不准,OLED显示也会异常。解决方法是,在仿真模型的属性里显式设置晶振频率为8MHz,或者把代码里时钟初始化改成HSE模式和这个保持一致。
4.2 Wokwi在线仿真备选方案
如果不想折腾Proteus安装包,Wokwi这个在线仿真平台也是很好的选择。直接在浏览器打开,选STM32F103C8T6板卡模型,把代码贴进去就能跑,对新手特别友好。
Wokwi有个优势是提供了面包板视图,可以直观地看到杜邦线怎么连接,对还不熟悉硬件连接的人来说,比看原理图更直观。而且Wokwi内置了DHT22模型,和DHT11的时序基本兼容,仿真跑起来毫无压力。我用Wokwi做过一次快速原型验证,从创建工程到OLED显示数据,不到半小时就跑通了,适合在板子还没到手时就先把逻辑验证好。
不过Wokwi的仿真速度和真实硬件还是有差距,I2C时序的模拟偏理想,真实硬件上DHT11可能偶尔读不到数据,需要加超时重试机制,这个问题我在下一节专门说。
4.3 常见问题速查表
我在实际调试中遇到的问题不少,整理成表格,方便你对照排查:
| 现象 | 排查方向 | 解决方法 |
|---|---|---|
| DHT11读数一直是0 | 引脚定义写错 | 确认GPIO配置,检查上拉电阻 |
| DHT11偶发读取失败 | 时序延时不准 | 延时函数改用DWT微秒延时,避免被中断干扰 |
| OLED屏幕不亮 | I2C地址或接线错误 | BH1750地址0x23,OLED地址0x3C,区分清楚 |
| OLED花屏 | 电源纹波太大 | 在OLED电源引脚加0.1uF电容 |
| MQ-2数值漂移大 | 没有预热或电源不稳 | 上电预热30秒,采样做多次平均 |
| 继电器不吸合 | 驱动电流不足 | 改用三极管或模块驱动,不要GPIO直接接 |
| 整个系统经常复位 | 继电器反向电动势 | 确认续流二极管是否接上 |
| 仿真中OLED无显示 | 仿真时钟频率设置不对 | 在芯片属性里设置外部晶振频率为8MHz |
DHT11偶发读取失败这个问题我多说两句。我早期的代码里,每读取一次如果失败就立即重试,结果发现重试频率越高,失败概率越大,因为DHT11需要时间恢复。后来改成首次失败后延时50ms再重试,最多重试三次,如果三次都失败就保留上一次的数据,不再死循环等待,系统的鲁棒性一下子好了很多。
温度采集还有一个容易忽略的坑,DHT11芯片本身在工作时会轻微发热,如果传感器紧贴着主控板或电源模块,读数会偏高1-2度。所以传感器最好通过排线引出,离主控板远一点,模拟真实环境下的温度。
5. 扩展思路与开源资源说明
5.1 代码和原理图的设计要点回顾
开源包里包含的东西,我按模块做了划分,每个都有明确的文档说明,帮助你快速定位:
- 原理图:PDF和嘉立创EDA源文件两种格式,方便直接打板
- 仿真工程:Proteus完整工程,打开就能跑
- 源码:Keil5工程,标准外设库版本,代码注释比较完整
- 使用文档:接线说明、烧录步骤、串口调试命令说明
代码的整体风格我尽量保持了统一,每个传感器驱动文件都提供init、read、get_value这样的标准接口,模块之间的耦合降到最低。比如你不想用OLED显示,直接删掉display相关的调用的即可,不会影响传感器读取和控制逻辑。
5.2 后续可以怎么扩展
这个系统做出来是基础版,其实后续可以加的东西很多。最直接的是加ESP8266模块,通过MQTT协议把数据上传到服务器,手机端实时查看环境参数和告警信息,这样就变成了一个物联网项目。我试过在原来基础上加ESP8266,用USART2和它通信,大概多写两百行代码就能实现。
如果想要更低的功耗,可以做休眠唤醒机制,传感器定时唤醒采集一次,其他时间进入Stop模式。STM32L系列或者STM32F103的睡眠模式功耗可以降到很低,用锂电池加太阳能板就能实现真正无线的环境监测节点。
如果对测量精度有更高要求,可以换成SHT30温湿度传感器,I2C接口,精度比DHT11高一个量级。或者用BME280,温湿度气压三合一,体积也更小。原理图里传感器接口留的是标准排针,换传感器只需要改驱动代码即可。
我做这个项目最大的体会是:嵌入式系统设计里,硬件和软件永远是一体的。原理图上多一点去耦电容,代码上可能就少加十行滤波;布局时信号线走得好,调试时就能少抓几个小时的波形。这个图书馆环境监测系统,功能不算复杂,但环节齐全,从硬件到软件再到调试工具链,走一遍下来,对STM32项目开发的整个链路就心里有数了。
如果你在实际复现过程中遇到问题,欢迎在评论区把你卡住的现象发出来,最好带上你的接线照片和关键代码片段,这样我能更快帮你定位问题。