基于STM32的图书馆环境监测系统设计与仿真
发布时间:2026/9/14 15:49:52 锦皓数字建站

1. 项目概述为什么要给图书馆做一套环境监测系统做嵌入式这些年手上过过不少板子从51到MSP430再到STM32真正让我觉得既有教学价值又有实际意义的开源小项目图书馆环境监测系统算是一个很典型的存在。你可能要问图书馆这种地方看起来安安静静需要监测什么环境参数如果你去过那种老式大型图书馆应该会有体会书库深处闷热潮湿古籍区空气干得发脆阅览室人多的时候二氧化碳浓度飙高、人昏昏欲睡。温度、湿度、光照、空气质量这四个参数直接决定读者的舒适度也影响馆藏图书的保存寿命。书页对温湿度极其敏感湿度过大容易发霉过于干燥则纸张变脆、发黄。光照方面紫外线会让书脊褪色而自然光直射的阅览区在不同时段照度差异巨大靠人工开关窗帘效率太低。这套STM32环境监测系统的价值就在这用一块常见的STM32主控配合温湿度传感器、光照传感器再加个OLED显示屏和LED指示就能搭出一个实时的环境监测节点。它能做三件事实时采集环境数据、在本地屏幕直观展示、超阈值自动触发声光报警或风扇联动。整套项目包含完整的可编译代码、可打样的原理图、可直接跑的仿真工程非常适合正在学STM32的学生、想接触传感器应用开发的工程师以及需要快速搭建环境监测原型机的创客。我选择在Proteus里做仿真验证同时给出真实硬件运行方案。这两条路我都实际跑通过仿真和实物之间的差异、坑点后面会单独拿出一整节来说。2. 整体设计与硬件方案选型2.1 为什么选STM32F103C8T6做主控提起环境监测很多人第一反应是用Arduino甚至用ESP8266直接连云端。但我最终把主控定在STM32F103C8T6理由很现实第一STM32F103C8T6是目前生态最成熟、资料最全的入门级Cortex-M3芯片。72MHz主频、64KB Flash、20KB RAM、丰富的外设接口跑这类多传感器采集显示的任务绰绰有余。它就像嵌入式界的捷达皮实耐用网上随便一搜都是参考设计踩坑了也容易找到答案。第二如果整套东西用Arduino写传感器库一键装好、代码几十行搞定学习价值反而被稀释了。用STM32裸机HAL库从时序到I2C协议全部手写一遍你才能真正理解传感器是怎么说话的。对学习者来说这种麻烦恰恰是最珍贵的学习机会。第三从成本角度看F103C8T6核心板在市场上的价格很低加上几个传感器模块整体物料成本可以控制在几十元以内。对于学校实验室、个人学习者来说这个门槛几乎不存在。2.2 传感器选型DHT11和BH1750的组合逻辑环境参数怎么选、用什么传感器是这个项目决策的核心环节。我最终确定了两颗传感器温湿度使用DHT11光照使用BH1750。这个组合不是随手选的背后是成本和调试成本的权衡。DHT11的测温范围0到50摄氏度、湿度20%到90%RH精度虽然一般正负2摄氏度、正负5%RH但胜在单总线协议简单、模块化程度高、价格低廉。图书馆这种室内环境温湿度波动本来就不剧烈DHT11的精度完全够用。你用SHT30确实能拿到更高精度但多出来的成本和学习曲线对这个项目来说并不划算。光照用BH1750则是因为它直接输出数字量内部集成16位ADC量程从1到65535勒克斯覆盖从深夜书库到白天窗边的全部场景。最方便的是它走I2C接口两根线就能挂到STM32上不占用太多GPIO。而且它内置了光敏二极管和运算放大器不需要像光敏电阻那样自己做分压电路和校准曲线对新手极其友好。有人会问要不要再加个空气质量传感器比如SGP30或CCS811我在扩展版里确实留了I2C接口也写好了预留驱动但主版本没有纳入。原因有二一是空气质量传感器普遍需要较长的预热时间而且在Proteus里几乎没有仿真模型不利于做纯仿真学习二是很多东西一旦加了项目的核心教学主线就容易被稀释。先做好温湿度加光照再自己动手扩展气体检测节奏更合理。2.3 系统架构与工作链路整个系统的数据链路其实很清晰传感器采集物理量转换成电信号主控通过协议读取数值做数据处理和阈值判断最后把结果送到显示和报警模块。具体到这套设计STM32F103C8T6作为核心控制器通过GPIO模拟单总线协议读取DHT11的温湿度数据通过I2C总线读取BH1750的光照强度。数据经过简单的滤波和越限判断后驱动一块0.96寸的I2C接口OLED显示屏完成本地展示。一旦温湿度超出设定区间蜂鸣器会发出报警提示同时板载LED状态灯变换闪烁模式。如果有需要还可以通过GPIO控制一个继电器模块外接风扇或除湿机做自动联动。整机供电采用USB 5V输入板载AMS1117-3.3稳压芯片降到3.3V给主控和传感器供电。功耗控制上OLED和传感器都支持休眠模式后续如果需要电池供电可以在代码里加入待机模式这个我在扩展建议部分会提到。3. 原理图设计与硬件细节3.1 最小系统电路一套能跑起来的STM32系统最小电路包含四部分电源、复位、时钟、下载调试接口。这些在很多开发板上都集成好了但如果想按自己的需求打样理解这部分尤为重要。电源部分我用AMS1117-3.3把USB的5V降到3.3V输入输出各加一个10uF电解电容和一个100nF瓷片电容滤波。AMS1117的最大输出电流是1A对这套负载来说绰绰有余。注意一点DHT11模块有的版本自带3.3V稳压有的没有接线前要看清楚模块上的丝印说明。复位电路用一个10K上拉电阻加一个100nF电容到地按键按下时把NRST引脚拉低实现手动复位。时钟部分用了8MHz无源晶振配上两个20pF负载电容。这里有个细节STM32F103的OSC_IN和OSC_OUT引脚对走线长度和电容容差比较敏感如果画PCB时走线过长可能会引起起振不稳尽量让晶振靠近MCU。下载调试接口我同时引出了SWD四线和UART1的Boot下载引脚方便用ST-Link下载调试也方便用串口ISP做备选方案。实际使用时SWD的SWDIO和SWCLK各加了一个10K上拉电阻能有效避免下载器连接不稳定。GPIO分配上我精心规划过引脚占用PA0到PA3接四个独立按键用于设定温湿度阈值PA5、PA6、PA7接HC-SR04超声波模块预留的扩展功能后面做库区人员检测时用PB0、PB1接DHT11和继电器PB6、PB7接I2C总线的SCL和SDA分别连OLED和BH1750PB8、PB9备用一组I2C2给后续扩展传感器。蜂鸣器用PB5驱动LED指示灯用PC13板载和PB3、PB4外接。3.2 DHT11与BH1750的接入电路DHT11的接法很简单VCC接3.3VGND接地DATA引脚通过一个4.7K上拉电阻连接到主控的PB0。DHT11用的是单总线协议空闲时为高电平主机拉低总线发起通信。上拉电阻不能省否则数据线上的高电平驱动能力不够会导致读到的数据全是0xFF。BH1750的接入稍微注意一下它的VCC可以接3.3V到5V但I2C的SDA和SCL引脚如果要和3.3V的STM32直接相连最好确认模块上是否自带电平转换。大多数市售BH1750模块的VCC接3.3V时I2C引脚输出也是3.3V电平可以直接连接但有些模块为了兼容5V系统做了上拉此时建议在STM32侧串一个100到220欧姆的电阻做保护稳妥不会烧引脚。ADDR引脚接地时I2C地址是0x23接VCC时地址变成0x5C画原理图时要标注清楚方便后续写驱动时对照。OLED显示屏同样挂在I2C1总线上和BH1750共用SCL和SDA。这里有个总线负载问题I2C总线上的设备越多上拉电阻就需要越小。我在画完原理图后把上拉电阻从默认的10K改成了4.7K就是为了照顾两个I2C设备同时挂载时的信号完整性。实测下来100KHz的标准模式通信非常稳定。3.3 报警与联动电路设计报警部分不能直接用MCU的GPIO驱动蜂鸣器因为GPIO的灌电流和拉电流能力有限。我用了一个NPN三极管S8050做开关管PB5通过1K限流电阻连接到三极管基极蜂鸣器接在VCC和集电极之间发射极直接接地。当PB5输出高电平时三极管导通蜂鸣器得电发出声音输出低电平时蜂鸣器关闭。这种低边驱动方式的优点是控制逻辑简单GPIO输出高电平即触发而且三极管导通时的饱和压降很小蜂鸣器能获得接近满额的驱动电压。如果你用的是有源蜂鸣器自带振荡源给电就响不需要外部提供频率如果用的是无源蜂鸣器代码里需要通过定时器输出一个2到4KHz的PWM信号才能发声。我在原理图里画的是有源蜂鸣器代码也更简单新手不容易卡壳。继电器联动电路类似用一个NPN三极管加一个续流二极管来实现。继电器线圈是个大电感断电瞬间会产生很高的反向电动势如果不加续流二极管这个反压可能直接击穿三极管。D11N4007就承担了这个续流作用这是绝大多数新手画继电器电路时最容易漏掉的地方。3.4 我自己打样时遇到的硬件坑原理图画好、板子打回来之后有几个坑是实际调试中才暴露出来的写在这里供大家参考。第一个坑是DHT11的引脚顺序。不同厂家生产的DHT11模块引脚排列居然不完全一致。我最早拿到的一批模块丝印上的正反面标反了插上去以后数据始终读不到。后来养成一个习惯不管什么模块拿到手先用万用表二极管档测一下VCC和GND之间的导通性或者先看模块背面的文字标注再接线不要完全依赖丝印。第二个坑是去耦电容的放置。原理图上我虽然画了100nF去耦电容但第一次画PCB布局时电容放在了板子边缘离MCU电源引脚很远结果MCU在继电器吸合的瞬间偶尔会复位。后来把电容挪到MCU电源引脚旁边并且每个VDD引脚就近放一个100nF问题就消失了。对于高频数字电路来说去耦电容离芯片电源引脚越近越好这是PCB布局的硬规则。第三个坑是OLED的I2C地址。买的0.96寸OLED模块大部分地址是0x787位地址0x3C但也有少量模块是0x7A7位地址0x3D。如果屏幕一直不亮先用I2C扫描程序确认一下实际地址再修改代码里的宏定义不要死磕一件事。4. 代码架构与核心实现4.1 工程结构与模块划分代码我基于STM32CubeMX生成HAL库工程开发环境用Keil MDK5。整个工程按功能模块划分得比较清晰方便大家按需取用Library_Monitor/ ├── Core/ │ ├── Inc/ │ └── Src/ ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── BSP/ │ ├── dht11.c/h │ ├── bh1750.c/h │ ├── oled.c/h │ └── bsp_key.c/h └── User/ ├── main.c └── user_task.c/hBSP层的四个驱动模块是重点。dht11负责单总线时序读写bh1750封装了I2C读写接口和光照数据转换oled提供显示和绘图APIbsp_key做按键扫描和消抖处理。main.c里的逻辑只关心数据流和业务状态机不直接操作寄存器这样代码的可读性和可移植性都好很多。4.2 DHT11驱动用手写时序理解单总线协议DHT11的驱动是整个项目里最值得细看的代码因为它涉及严格的时间要求。单总线通信的流程分成两步主机发起起始信号然后读取40位数据。主机先把总线拉低至少18ms然后释放并延时20到40us这是起始信号。DHT11收到后会拉低80us作为响应再把总线拉高80us之后开始逐位发送数据。每位的发送方式是先拉低50us然后拉高高电平持续26到28us表示0持续70us表示1。驱动代码里最核心的部分是读取一位数据的函数static uint8_t DHT11_ReadBit(void) { uint8_t retry 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET) { if (retry 100) return 0xFF; // 超时保护 delay_us(1); } delay_us(40); // 跳过高电平前段的0判定区间 if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if (retry 100) return 0xFF; delay_us(1); } return 1; } else { return 0; } }这段代码的逻辑是先等数据线变高DHT11拉高开始发数据位然后延时40us后再采样。如果此刻引脚仍然是高电平说明这一位是1高电平持续70us40us后还没结束如果已经变低说明是0高电平只持续26us40us后已经结束。这种方法用延时替代了更精确的输入捕获定时器虽然牺牲了一点精度但在主频72MHz下配合空循环延时实际测试读出的温湿度数据相当稳定。40位数据的组成为16位湿度整数小数、16位温度整数小数、8位校验和。校验和的计算方式是前四个字节相加取低8位如果和校验字节一致说明这帧数据有效。我在代码里做了严格的校验判断宁可这次采样丢弃也不能把脏数据送进显示层。4.3 BH1750驱动I2C协议与光强换算BH1750的驱动相对简单关键是搞清楚它的测量模式和换算公式。芯片上电后默认是连续H-分辨率模式分辨率1勒克斯测量时间约120ms。如果你需要更快的响应速度可以切换到连续L-分辨率模式4勒克斯分辨率测量时间16ms。图书馆环境变化不快我选了H-分辨率模式数据更平滑。读取流程分两步先发送测量命令等待测量完成再连续读取2字节数据。I2C通信的代码我直接用HAL库的接口封装uint8_t BH1750_ReadLight(float *lux) { uint8_t buf[2]; uint8_t cmd 0x10; // 连续H-分辨率模式 if (HAL_I2C_Master_Transmit(hi2c1, BH1750_ADDR 1, cmd, 1, 100) ! HAL_OK) return 1; HAL_Delay(180); // 等待测量完成留足余量 if (HAL_I2C_Master_Receive(hi2c1, BH1750_ADDR 1, buf, 2, 100) ! HAL_OK) return 1; uint16_t raw (buf[0] 8) | buf[1]; *lux (float)raw / 1.2f; // H-分辨率模式除1.2 return 0; }BH1750地址左移一位是因为HAL库的I2C接口需要8位地址7位地址加读写位这和传感器数据手册上写的0x237位地址存在差异新手刚接触时容易在这里绕晕。换算公式里的1.2是芯片手册给定的分辨率系数在H-分辨率模式下1勒克斯对应1.2个LSB。实测正午窗边能读到两万以上勒克斯深夜关灯时读到个位数量程覆盖非常理想。4.4 主循环与业务逻辑状态机思路main函数里初始化完外设后进入while(1)主循环。我采用了一个简单的状态机来管理采集、显示、报警三件事避免所有功能堆在一个循环里互相阻塞typedef enum { STATE_IDLE, STATE_SENSOR_READ, STATE_DISPLAY_UPDATE, STATE_ALARM_CHECK, STATE_KEY_SCAN } SystemState;循环里每隔2秒触发一次完整的采集周期因为DHT11的采样频率建议不低于1秒间隔采集完成后通过标志位通知其他状态执行相应动作。显示更新和按键扫描使用短周期轮询保证OLED刷新不卡顿、按键响应不迟钝。报警判断的逻辑是温湿度或光照的实测值超出设定区间后蜂鸣器触发。为了避免临界值附近反复鸣叫导致刺耳我加了5%的回差滞回控制实测效果很理想。显示屏的UI设计分两个页面首页显示温度、湿度、光照三个数值第二页显示当前设定的阈值范围。短按KEY1切换页面KEY2和KEY3调整阈值大小。UI切换时只重绘变化的区域而不是全屏刷新OLED的刷新率会明显提高。4.5 按键消抖与参数设置按键处理看起来简单但做不好用户体验很差。我用了10ms级定时扫描加状态机消抖的方案检测到按键按下后连续采样3次每次间隔10ms如果结果一致才确认有效按键事件。这个方案比简单的HAL_Delay(20)消抖更稳不会因为主循环堵塞而让按键卡键。阈值设定采用了长按加速的逻辑短按一次当前阈值加减1个单位长按超过1秒后每200ms自动加或减10个单位。这样设定温度上限从25度调到28度短按三次即可而大幅调整时也不会累手。5. Proteus仿真搭建与联调验证5.1 仿真工程的元件选型与连线Proteus仿真是这套项目的一个亮点也是很多初学者最需要参照的部分。很多人以为Proteus只能跑跑LED流水灯实际上它支持DHT11和BH1750的仿真模型只要库里有就能完美跑出数据波形。我使用的Proteus版本是8.9以上新建工程后从元件库中依次添加STM32F103C8T6、DHT11、BH1750、LM016L用LCD1602代替OLED做显示展示Proteus对OLED的仿真支持不稳定、BUTTON、BUZZER、RESISTOR、LED。元件找不到时用关键字搜索即可比如DHT11在Temperature and Humidity Sensors分类下。连线和实物电路一致DHT11的DATA接PB0BH1750的SCL接PB6、SDA接PB7LCD的RS、EN、D4到D7分别接PC0到PC3蜂鸣器接PB5。电源和地网络要仔细布置Proteus对电源网络自动以三角形符号标注千万不要遗漏。5.2 仿真模型驱动的避坑指南Proteus仿真最让人头痛的一点是DHT11的时序要求极其严格而Proteus的虚拟时间片和真实硬件存在差异。我最早在仿真里跑实物代码DHT11数据死活读不出来折腾了很久。后来把延时函数从空循环Delay改为基于SysTick的精确微秒延时仿真和实物都通了。BH1750在Proteus的模型响应速度也比较慢实测下来读取命令发出后需要等待至少200ms才能读到有效数据。我在驱动里把延时从180ms提升到250ms仿真稳定性明显提升实物上也没有副作用。LCD1602在仿真里替代OLED时需要注意对比度调节。V0脚对地接一个10K电位器调节到合适位置才能清晰显示。很多仿真实物能跑、但屏幕上白花花一片基本就是对比度没调好。5.3 仿真联调结果记录仿真联调通过后我对系统做了几个典型场景的模拟测试第一个场景是模拟正常阅读环境。DHT11的仿真模型可以通过滑块实时调整温湿度我把温度调到25度、湿度调到55%RH光照强度通过BH1750模型的滑动条调到500勒克斯此时系统显示数据正常蜂鸣器不响LED为绿色常亮。第二个场景是模拟库房高温预警。我把温度滑块推到32度超过阈值30度后LCD界面显示异常提示蜂鸣器以1Hz频率鸣叫LED变为红色闪烁。继电器输出引脚电平翻转仿真里外接的风扇模型可以用直流电机模型代替开始转动。第三个场景是模拟夜晚闭馆状态。光照调低到10勒克斯以下系统进入低照度模式OLED自动切换为夜间配色方案黄字黑底。这个功能是偏体验向的但图书馆管理员反馈夜里巡检时很实用。仿真工程里也包含了STM32的虚拟串口调试。我通过VSM Studio的虚拟终端观察了DHT11每帧数据的校验结果确认了数据稳定性。6. 常见问题与排查技巧实录6.1 DHT11读取数据一直为0或0xFF这个问题几乎每个做过DHT11的人都会遇到。排查思路从软件到硬件逐步排除第一确认GPIO模式。DHT11的DATA引脚必须配置为开漏输出且带上拉。如果配置成推挽输出单片机引脚输出高电平时的驱动能力和DHT11内部的下拉发生冲突通信时序就会错乱。第二确认延时函数精度。DHT11的时序在微秒级别用HAL_Delay只能延时毫秒完全不符合要求。必须用SysTick实现微秒级延时或者用定时器的输入捕获功能来测量高电平宽度后者精度更高。第三检查上拉电阻。如果DATA引脚外部没有4.7K上拉到VCC或者上拉电阻选得太大信号边沿会变得平缓导致单片机误判电平。第四降低采样频率。DHT11的官方手册说采集间隔建议大于1秒连续快速读取会导致传感器不响应。我实测采样间隔低于500ms时偶发读取出错间隔拉到2秒后稳定性非常好。6.2 I2C设备无响应或读到错误值I2C总线上的设备总是不响应先用逻辑分析仪或示波器看波形没有仪器的话可以从以下几个方面排查总线地址是否写对。BH1750的7位地址是0x23但HAL库要求8位地址需要左移一位成为0x46。OLED是0x3C或0x3D7位左移后是0x78或0x7A。地址写错是最低级但最常见的错误。上拉电阻是否合适。I2C总线需要上拉电阻一般4.7K到10K都行。如果总线上挂载设备多、通信距离长选用更小的上拉电阻比如2.2K有助于提升信号质量。测量等待时间是否充足。BH1750在H-分辨率模式下测量时间约120ms如果立即读数据可能读到上一帧的旧值。我在代码里加了180ms延时实际使用时可以根据情况调整。6.3 OLED不显示或者显示乱码OLED不显示的原因排在第一位的是地址配置错误第二位是供电不足。OLED模块全亮时的电流约20到30mA一般3.3V稳压芯片能扛住。但如果你的USB口供电能力弱或者线缆压降大OLED上电瞬间可能导致整个系统掉电复位。显示乱码则多半是I2C速率不匹配。OLED模块的标准I2C速率是100K到400K如果你把I2C时钟配置成1MHz以上部分屏驱芯片跟不上升级协议导致乱码。解决办法是把I2C时钟调整为100KHz或者重刷初始化的配置序列。6.4 Proteus仿真不运行或运行卡死Proteus仿真卡死大概率是模型冲突或时序死循环。DHT11模型如果长时间不响应代码里的超时保护会卡在while(1)里出不来。我建议在读取函数里加入严格的重试计数超过一定次数直接返回错误码主循环继续执行其他逻辑这是写嵌入式代码时应有的容错思维。还有一个常见问题仿真工程里STM32的晶振频率必须和代码里配置的一致。如果你代码用CubeMX配置了72MHz主频但Proteus里设置的是8MHz外部晶振没有配置PLL系统时钟就会异常所有延时和外设时序都会乱套。我建的仿真工程里在HSE_VALUE上做了处理让仿真环境能正确匹配并运行。7. 项目扩展方向与后续规划这套系统目前是一个完整的单点监测节点但如果把视野放大它其实是一个更大的物联网系统的神经末梢。我从做完板子之后陆陆续续给它规划了三个扩展方向。第一个方向是联网化。给F103C8T6外挂一个ESP8266模块通过串口AT指令把温湿度数据上报到本地MQTT服务器再接个简单的前端页面就能做成一个多节点的图书馆环境监控网络。管理员在手机上就能看到每个楼层、每个库房的实时环境参数报警信息也能推送到微信或钉钉。这个扩展的代码我已经在调了核心就是给现有的采集任务增加一个网络上报的分支。第二个方向是数据存储。用F103的SPI接口外接一个MicroSD卡模块定时把环境数据写入CSV文件。这样就能做历史数据回放、温湿度变化曲线分析甚至预测哪些区域在什么季节容易发生霉变。对图书馆这样的场景来说长期数据积累比实时数据更有管理价值。第三个方向是低功耗改造。如果要做无线节点就得考虑电池供电。F103的待机模式电流可以做到微安级别配合RTC定时唤醒每隔10分钟采集一次并发送数据两节18650电池撑几个月没有问题。当然DHT11和BH1750在休眠模式下的功耗特性也需要一并考虑。我个人在实际操作中最深的一个体会是这类项目千万不要一味追求参数高配和功能堆叠。把最简单可靠的电路吃透把时序和协议搞清楚比多挂几个传感器有用得多。这套图书馆环境监测系统从零到一走下来通信协议、传感器驱动、状态机设计、软硬件联调的功夫全都练到了很多人卡了好久的学完STM32不知道做什么其实就差这样一个能把知识串起来的完整闭环。代码、原理图和仿真工程我都在网盘里共享了需要的可以直接拿去参考有问题也欢迎随时交流。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。