FEATURED · 精选文章

基于STM32的图书馆环境监测系统设计与Proteus仿真

发布时间 / 2026/9/18 2:11:51
来源 / 创域科博编辑部
栏目 / 资讯中心
基于STM32的图书馆环境监测系统设计与Proteus仿真 说来也巧做了这么多年嵌入式手头攒了不少STM32的小项目但真正让我觉得“值得开源出来给大家抄作业”的这个图书馆环境监测系统算一个。原因很简单它麻雀虽小五脏俱全一个典型的STM32项目该有的东西全都有——传感器采集、数据处理、显示交互、报警输出还附带原理图和Proteus仿真拿来练手、做课程设计、甚至改一改直接当产品原型都能用得上。这个项目的核心场景是图书馆这类需要恒定温湿度、充足光照、防火防烟的室内环境。图书纸张对温度和湿度非常敏感太潮会发霉太干会变脆光照太强会加速纸张老化发黄烟雾更是图书馆的大忌一个火星就可能毁掉整个书库。所以这套系统要做的事情其实很明确实时采集环境数据在本地显示屏上直观呈现一旦超出设定范围就立刻声光报警提醒管理人员介入。如果你是刚接触STM32的初学者这个项目能帮你把GPIO、定时器、ADC、中断、I2C/单总线协议这些基础知识全部串起来如果你是有一定基础的人可以直接拿走这套代码和原理图把阈值逻辑、传感器型号、通信方式换成你自己项目需要的相当省事。我下面就把整个项目的设计思路、原理图要点、代码实现和仿真调试经验全部分享出来。1. 项目整体设计与方案选型1.1 核心需求拆解动手画原理图之前我习惯先把需求一条条列清楚。这个图书馆环境监测系统的需求看起来简单真正拆开其实有五个维度环境数据采集温度、湿度、光照强度、烟雾浓度这四项是图书馆环境监测的底线指标。温湿度用数字传感器最简单光照和烟雾用模拟量采集再加ADC转换。本地实时显示数据采集上来不是给自己看的得让图书馆管理员一眼就能看到当前环境状态。这里需要一个显示屏能显示四项数据加上对应的状态标识。超限报警每项环境参数都要有正常范围超出范围必须能提醒人。声音报警加灯光指示是成本最低也最有效的组合。系统可配置性不同图书馆对环境要求不一样比如古籍书库要求温度18-22℃、湿度50%-60%普通书库要求宽松一些。所以报警阈值最好能通过按键调整而不是写死在代码里。可演示、可仿真这一点很容易被忽略。如果项目只有实物没有仿真别人拿到源码也未必敢直接上手改如果只有仿真没有实物验证又怕实际跑起来翻车。所以这个项目我坚持代码、原理图、仿真三件套齐全方便不同需求的人使用。1.2 主控与传感器选型主控我选了STM32F103C8T6也就是大家常说的“蓝丸”核心板用的那颗芯片。选择它的理由很实在Cortex-M3内核72MHz主频20KB RAM64KB Flash对于这个量级的应用绰绰有余。关键是它的资源太丰富了——3个USART、2个I2C、2个SPI、12位ADC有10个通道哪怕后面想扩展WiFi模块做数据上传、加个SD卡做数据存储也完全不用换主控。传感器选型这部分我对比过好几套方案这里把当初的思考过程也分享出来监测项推荐型号选型理由备选方案温湿度DHT11单总线协议代码简单精度够用±2℃、±5%RHSHT30精度更高但需要I2C光照光敏电阻分压电路成本极低电路简单ADC直接读电压BH1750数字输出需I2C烟雾MQ-2灵敏度高响应快能同时检测可燃气体和烟雾MQ-135侧重空气质量显示OLED 0.96寸I2C显示信息量大功耗低接线只有两根LCD1602字符屏便宜这里有个选型心得想多说一句很多人一上来就追求高精度传感器其实在环境监测这个场景里DHT11的精度完全够用。图书馆的环境监测目的是“发现趋势变化、触发报警”不是做计量认证。温度差个0.5℃、湿度差个3%根本不影响判断。用DHT11省下来的钱和调试时间花在系统稳定性上更值。1.3 系统架构与工作流程整个系统的工作流程用大白话描述就是传感器不停地把环境数据传给STM32STM32处理完以后把数据送到OLED屏幕上显示同时和预设的报警阈值做比较超限就拉响蜂鸣器、点亮LED指示灯。温湿度数据走单总线协议一根线既传数据又传时钟时序要求比较严格。光照和烟雾走ADC模拟量采集光敏电阻和MQ-2模块输出的电压信号经过STM32内部12位ADC转换成数字量。OLED显示屏走I2C总线SCL和SDA两根线搞定。按键用来调整报警阈值长按进入设置模式短按切换参数项再短按调整数值。蜂鸣器和LED作为报警输出超限时按不同频率和节奏报警让管理员一听就知道是哪个参数出了问题——比如温度报警滴一声、湿度报警滴两声这个细节在实际使用中非常实用。这套架构的好处是各模块之间完全解耦出现问题时排查起来很快显示不对查I2C采集不对查传感器和ADC报警不响查GPIO配置思路非常清晰。2. 硬件原理图解析与设计要点2.1 最小系统电路原理图的核心是STM32F103C8T6的最小系统包括电源、晶振、复位电路和BOOT启动配置。电源部分用AMS1117-3.3稳压芯片输入5V输出3.3V给主控和传感器供电。这里我在输入输出两端都加了10uF和100nF的滤波电容一大一小配合使用滤除低频纹波和高频噪声。注意AMS1117的输出电容不能省否则可能引起自激振荡输出电压会不稳定。晶振用的是8MHz无源晶振搭配两个20pF的负载电容。这个电容值不是随便选的8MHz晶振的典型负载电容通常在10-20pF之间20pF是误差比较小的选择。STM32内部PLL把8MHz倍频到72MHz作为系统主频。还有一颗32.768kHz的RTC晶振如果不需要实时时钟功能可以不焊不影响系统运行。复位电路就是一个10k上拉电阻加一个0.1uF电容到地按下复位键时把NRST引脚拉低。BOOT0和BOOT1都通过10k电阻下拉到地确保从主Flash启动。这里有个新手容易犯的错BOOT引脚悬空可能导致启动模式不稳定必须用电阻明确拉高或拉低。2.2 传感器接口电路DHT11的接口电路看起来就三个引脚——VCC、GND、DATA但有一点必须注意DATA引脚需要接一个4.7k-10k的上拉电阻到VCC。DHT11用的是开漏输出没有上拉电阻的话信号根本拉不高。我实际测过把上拉电阻去掉以后DHT11读出来的数据全是0xFF或者随机跳变排查了好久才找到问题。光照检测电路最简方案就是光敏电阻串联一个固定电阻分压中间节点接到STM32的ADC输入引脚。光敏电阻的阻值随光照增强而减小分压点的电压就随之变化。串联电阻的取值取决于光敏电阻的暗阻和亮阻范围我这里用的光敏电阻亮阻约5-10k暗阻约200k以上串联电阻选10k这样在正常室内光照下分压点电压大约在1.5-2.5V之间正好落在ADC的最佳量程范围内。MQ-2烟雾传感器模块自带一个LM393比较器输出数字量和模拟量两种信号。实际使用中我更推荐读取模拟量输出因为数字量的阈值是模块上电位器固定的不灵活模拟量输出接STM32的ADC阈值在软件里随便调想怎么改就怎么改。MQ-2模块上电后需要预热刚上电那几十秒输出值会漂移这是正常的传感器内部加热丝还没稳定。2.3 显示与报警电路OLED显示屏用I2C接口SDA接PB7SCL接PB6这是STM32F103的I2C1外设引脚。OLED模块一般自带4.7k-10k的上拉电阻不需要额外加。供电电压要注意很多0.96寸OLED模块虽然标注支持3.3V-5V但I2C电平逻辑不一样最好统一用3.3V供电避免电平不匹配导致通信异常。蜂鸣器电路我用的是NPN三极管S8050驱动。STM32的GPIO输出能力有限直接驱动蜂鸣器电流不够必须加三极管放大。电路接法蜂鸣器正极接3.3V负极接三极管集电极发射极接地GPIO通过1k电阻接基极。GPIO输出高电平时三极管导通蜂鸣器发声输出低电平时关闭。注意蜂鸣器两端要反向并联一个续流二极管防止关断瞬间的感应电动势击穿三极管。LED指示灯就简单多了GPIO串联一个330欧限流电阻再接到LED正极LED负极接地。3. 软件设计与核心代码实现3.1 软件整体框架软件架构我按照功能模块拆分成四个文件main.c负责主循环和状态调度dht11.c负责温湿度采集adc.c负责光照和烟雾的模拟量读取oled.c负责显示。阈值设置和报警逻辑放main.c里统一管理。这里有一个设计原则值得分享驱动代码和业务逻辑分开。比如dht11.c里面只做一件事——读温湿度返回数据至于读到的温度是25℃还是35℃要不要报警那是main.c的事。这样做的优势在后期维护时特别明显想换SHT30传感器只需要重写dht11.cmain.c的逻辑完全不用动。3.2 DHT11驱动编写与踩坑记录DHT11的单总线时序是这个项目里最容易翻车的地方。读写时序要求微秒级的延时精度用软件延时函数的时候要特别注意编译器的优化级别。我之前试过用delay函数做延时优化级别从-O0调到-O2延时时间直接变了DHT11就死活读不出数据。DHT11的通信流程分三步主机发起开始信号、DHT11响应、DHT11返回40位数据。uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t data[5] {0, 0, 0, 0, 0}; uint8_t i; // 主机拉低总线发起开始信号 DHT11_DQ_OUT_LOW(); delay_ms(20); // 至少18ms才能触发DHT11响应 DHT11_DQ_OUT_HIGH(); delay_us(30); // 拉高30us后释放总线 // 切换为输入模式等待DHT11响应 DHT11_DQ_IN_MODE(); // 检查响应信号先低电平80us再高电平80us if (DHT11_DQ_READ() 1) return 1; // 总线没有被拉低通信失败 delay_us(80); if (DHT11_DQ_READ() 0) return 1; // 总线没有被拉高通信失败 delay_us(80); // 读取40位数据湿度整数部分、湿度小数部分、 // 温度整数部分、温度小数部分、校验和 for (i 0; i 5; i) { data[i] DHT11_ReadByte(); } // 校验前四个字节之和的低8位应等于校验字节 if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) return 2; // 校验失败 *humi data[0]; *temp data[2]; return 0; }读字节函数是DHT11驱动的核心每一位数据的“0”和“1”是通过高电平持续时间区分的uint8_t DHT11_ReadByte(void) { uint8_t i, data 0; for (i 0; i 8; i) { // 等待50us低电平结束 while (DHT11_DQ_READ() 0); // 延时30us后采样如果还是高电平则是逻辑1否则是逻辑0 delay_us(30); if (DHT11_DQ_READ() 1) data (data 1) | 0x01; else data (data 1); // 等待当前位的高电平结束 while (DHT11_DQ_READ() 1); } return data; }这段代码踩过的坑就是那个30us延时。逻辑“0”的高电平脉宽是26-28us逻辑“1”的高电平脉宽是70us采样点选在30us刚好卡在分界线上。延时太短会把“0”误判成“1”延时太长又来不及在“1”结束前采样。建议用逻辑分析仪实测一下你的延时函数实际延时多少再微调这个值。3.3 ADC采集光照与烟雾STM32F103的ADC是12位的参考电压默认接VCC3.3V所以ADC读到的原始值换算成电压的公式是电压 ADC值 × 3300mV / 4095。这个分辨率大约0.8mV对于光照和烟雾检测来说绰绰有余。ADC初始化我用了规则通道扫描模式加软件触发代码比较简洁void ADC_Init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 使能ADC1和GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); // PA0为光敏电阻输入ADC1_IN0PA1为MQ-2输入ADC1_IN1 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 扫描模式 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; // 两个通道 ADC_Init(ADC1, ADC_InitStructure); // 设置通道采样顺序和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); // 校准ADC ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }采样时间我用了55.5个周期环境监测这种慢变信号不需要太快的采样率采样时间长一点反而能让ADC内部采样电容充分充电读数更稳定。读取数据的时候要注意SCAN模式下每个通道转换完会自动把结果存到对应的ADC_DR寄存器里但多通道时必须用ADC_GetConversionValue函数或者DMA方式逐个取走不然数据会乱。我这里是用了两次ADC_SoftwareStartConvCmd触发分别读两个通道的值。3.4 主循环与报警逻辑主循环的逻辑用状态机思路来写每一轮循环做四件事读传感器数据、刷新显示、检查报警、处理按键。每轮循环之间加一个100ms的延时控制刷新频率。int main(void) { uint8_t temp, humi; uint16_t light_adc, smoke_adc; uint8_t temp_threshold 30; uint8_t humi_threshold_low 40; uint8_t humi_threshold_high 70; // 初始化各模块 Delay_Init(); OLED_Init(); DHT11_Init(); ADC_Init(); KEY_Init(); BEEP_Init(); LED_Init(); OLED_Clear(); OLED_ShowString(0, 0, Lib Env Monitor); while (1) { // 读取温湿度失败则显示错误信息 if (DHT11_ReadData(temp, humi) 0) { OLED_ShowTempHumi(temp, humi); } else { OLED_ShowString(2, 0, DHT11 ERR); } // 读取光照和烟雾ADC值 light_adc ADC_GetValue(0); smoke_adc ADC_GetValue(1); OLED_ShowLightSmoke(light_adc, smoke_adc); // 报警判断 if (temp temp_threshold || humi humi_threshold_low || humi humi_threshold_high || smoke_adc SMOKE_THRESHOLD) { BEEP_On(); } else { BEEP_Off(); } // 处理按键调整阈值 KEY_Process(); delay_ms(100); } }报警逻辑这里我做了个简化实际完整的代码里应该区分不同报警源温度超限蜂鸣器响一声歇一下湿度超限响两声烟雾超限连续快速响。这样管理员就算不看屏幕光听声音就知道问题出在哪。这个细节虽然不影响功能但体验感好很多算是加分项。4. Proteus仿真搭建与调试经验4.1 仿真环境的元器件选择Proteus的元件库和实际硬件不完全一样搭建仿真的时候有几个元件选择要特别注意。STM32F103C8T6在Proteus里的模型就是“STM32F103C8T6”DHT11用“DHT11”模型OLED屏我一般用“OLED_I2C”模型也有用LCD1602替代的。MQ-2在Proteus里没有现成模型可以用变阻器来模拟——调节变阻器改变ADC输入电压模拟烟雾浓度的变化。仿真连线有几个容易出问题的地方I2C的SCL和SDA一定要和程序里配置的引脚对应OLED的模型如果选错可能怎么调都不亮DHT11的DATA引脚别忘了加上拉电阻仿真模型同样需要这个上拉才能正常工作。4.2 仿真过程中的典型问题Proteus仿真最坑的一点是时序问题。我第一次跑仿真的时候DHT11死活读不到数据检查了一遍代码没发现问题后来把单片机的时钟频率从默认的4MHz改成了实际的72MHz配置才正常起来。仿真的行为本质上是模型在跑如果模型的主频配置和代码里的SystemInit不一致定时器延时全都会偏差DHT11时序自然对不上。另一个常见问题是ADC读值一直是0。这个大多是因为Proteus的信号源没有正确连接到ADC引脚。变阻器的一端要接VCC另一端接地中间抽头接PA0这个电路在仿真实物里都必须完整少一根线读数就不对。还有OLED不显示的问题。Proteus的OLED模型初始化时序和真实芯片略有差异如果初始化代码执行太快有时候会错过模型的准备状态。我的做法是在OLED_Init前后各加50ms延时仿真和实物都能兼容。4.3 仿真与实物的差异清单仿真能帮你验证逻辑但永远代替不了实物。我把这个项目仿真和实物调试中遇到的差异整理成一张表给后面复现的人做个参考环节仿真表现实物表现DHT11时序逻辑正确但上电瞬间容易读到错误数据需要等待1-2s让传感器稳定后再读ADC读数数值稳定几乎无噪声有轻微波动需要软件滤波连续采样取平均MQ-2用变阻器模拟反应是瞬时的需要预热30-60s响应有滞后OLED显示只要I2C时序对基本一次点亮可能因供电不足导致花屏需要加强滤波最大的差异在ADC部分。仿真里的变阻器输出是理想的稳定电压实物里的光敏电阻和MQ-2输出多少都有噪声。我在实物代码里加了中值滤波连续采样5次去掉最大值和最小值取剩下3次的平均值效果好很多读数波动从±50个ADC值缩小到±10以内。5. 系统调试与问题排查实录5.1 常见问题速查表项目调试过程中遇到的大部分问题都是典型问题我整理了一张速查表按问题现象、可能原因、解决方案三个维度列出来问题现象可能原因解决方案DHT11一直返回校验错误上拉电阻没接延时精度不对总线引脚配置错误检查4.7k-10k上拉用逻辑分析仪验证时序确认GPIO模式为开漏输出ADC读值始终为0引脚没有配置为模拟输入信号源没接好检查GPIO_Mode_AIN用万用表测量引脚电压OLED不显示或花屏I2C地址不对供电电压不稳初始化时序太快确认OLED地址常见0x3C或0x3D加滤波电容初始化前加延时按键没反应GPIO内部上拉没开引脚配置错误开启GPIO_PuPd_UP确认按键接法为按键一端接GND蜂鸣器不响三极管管脚接错GPIO没配置为推挽输出检查三极管B/C/E极接法确认GPIO_Mode_Out_PP仿真中DHT11读不到数据单片机主频配置和代码不一致检查F1C90中晶振频率设置与SystemInit一致5.2 一个印象深刻的调试图DHT11的随机数据说起调试过程中印象最深的问题是在实物上第一次跑的时候DHT11读出的温度和湿度偶尔会出现一次跳变比如温度从26℃突然跳到85℃然后又恢复正常。这种偶发问题最难查因为不是必现的。我用示波器抓了DATA引脚的波形发现DHT11返回的数据位里偶尔会有异常。排查到最后发现是两个原因叠加一是DHT11的供电线太长传感器在采集数据时电流变化引起的压降导致信号电平飘了二是主控读取数据时没有做多次重试。解决方法是把DHT11的供电线剪短在传感器电源引脚旁边加了一个0.1uF去耦电容同时在代码里改成连续读三次、取两次相同结果才采信的逻辑。这个问题后面再也没有复现过。这个经历让我养成了一个习惯凡是传感器项目上电后不要急着读数据先等500ms到2s读数据加一个重试机制偶尔读失败很正常但多次失败才需要处理。这个习惯帮我避免了很多偶发问题。5.3 代码调试的小技巧分享几个这个项目实际调试时特别好用的小技巧第一善用OLED屏做调试输出。不用接串口线、不用开调试器直接把关键变量的值显示在OLED上比什么都直观。我调试ADC的时候就是直接在屏幕上同时显示原始ADC值和滤波后的值肉眼对比就能看出滤波效果。第二给每个功能模块加一个独立的测试函数。比如dht11_test()只读温湿度并不断刷新显示其他功能全部注释掉。这样能把问题快速定位到具体模块而不是在整套系统里大海捞针。第三版本管理从第一天就做起。哪怕是一个人开发的项目也要用Git管理代码。我见过太多人改来改去改回不去了最终只能对着一个坏掉的工程发呆。这个项目开源出来代码提交历史本身就是一份很好的学习资料。6. 项目扩展方向与开源工程文件说明这个项目目前实现的是环境监测和本地报警如果想让系统更实用有几个很自然的扩展方向。第一个方向是加联网功能。STM32F103C8T6可以外接ESP8266模块通过串口AT指令把传感器数据上传到云平台或者局域网服务器管理员用浏览器就能远程查看各书库的环境状态。代码结构上只需要在现有基础上加一个串口发送函数把采集到的数据拼接成JSON字符串发出去就行。第二个方向是加数据存储。给系统挂一个SD卡模块SPI接口每小时记录一次数据生成CSV文件。积累几个月的数据之后可以做趋势分析摸清图书馆不同季节、不同时段的温湿度变化规律提前做好预防措施。第三个方向是换成RS485总线组网。一个图书馆有多个书库和阅览区单点监测远远不够。STM32的USART加一个MAX485收发器就能组成RS485网络每个书库一个节点主机轮询各节点数据集中显示。这个扩展从硬件到软件都有成熟的方案不改动现有的传感器逻辑。最后说一下开源工程包里有什么。我把整个工程整理成了一个压缩包发布包含四个部分完整的MDK5工程源码直接打开就能编译下载、原理图源文件用立创EDA和AD两个版本各存了一份、Proteus仿真文件打开就能跑、以及一份详细的硬件接线说明文档。代码里我写了比较详细的注释关键的DHT11时序部分还画了时序图说明配合这篇博文对照着看应该能少走不少弯路。我在整理这套工程的时候反复打磨了好几遍代码结构和注释就是为了让它能成为一个真正意义上的“教学级开源项目”。嵌入式这个圈子很多时候学到东西靠的就是前人踩坑的经验把自己的项目开源出来分享经验本身就是一种很高效的互相帮助方式。希望这个项目对正在学STM32的朋友有帮助也希望大家拿到手以后能把它改造成更适合自己需求的样子。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻