拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32环境质量监测系统设计:传感器采集到Proteus仿真全流程

1. 项目整体设计与思路拆解做这个环境质量监测系统的起因其实挺朴素家里和办公室空气质量不透明尤其冬天门窗紧闭或者雾霾天你不知道室内湿度是不是太低、VOC有害气体浓度有没有超标。市面上成品监测仪少则两三百功能又不透明干脆自己用一块STM32F103C8T6搭一个。这个项目本质上是把传感器采集、数据处理、显示报警、仿真验证这一条完整链路走通。整个系统围绕三个核心模块展开STM32F103C8T6做主控DHT11负责温湿度采集MQ135负责VOC和有害气体浓度感知再用0.96寸OLED屏实时显示同时驱动蜂鸣器和LED做超限报警。原理图是用嘉立创EDA画的仿真用Proteus完成代码基于标准库编写整个工程包含源码、原理图和仿真文件全部开源放出。适合谁参考如果你刚学完STM32基础外设想找一个“外设覆盖广但不复杂”的整合型项目练手这个项目非常合适。它把GPIO、定时器、ADC、I2C这几个最常用的外设全部串起来了还要处理传感器时序、数据滤波、阈值报警这些工程实践绕不开的问题。比单独点个灯、读个按键有含金量又比跑系统、上协议栈容易上手。1.1 系统功能定位不止测数据更要看得懂数据很多新手做环境监测就容易做成“传感器读数值 屏幕显示数值”的裸奔模式。我在规划这个项目时特意加上了一层“评价逻辑”也就是把原始测量值转化为分级描述。温度按人体舒适度分为偏冷、舒适、偏热三个档位湿度按30%、60%两个边界划分干燥、舒适、潮湿空气质量则根据MQ135输出电压折算的浓度等级驱动力三档报警逻辑这样一来用户不需要去查“这数值算正常吗”的表格看一眼屏幕就知道当前环境状态。这也是一个完整项目应该具备的交互思维代码里体现为状态判断和显示切换的逻辑分层。1.2 硬件方案选型围绕成本、精度和调试成本做平衡传感器选型是整个项目里最值得琢磨的一件事。DHT11的精度确实一般温度误差±2℃湿度误差±5%但这玩意有两个不可替代的优势便宜、协议简单。单总线协议逻辑清晰耗时短对刚接触时序编程的人非常友好。如果换成SHT30或DHT22精度上去了但I2C时序和寄存器操作复杂度也上去了对新手不友好间接提高了项目的门槛。MQ135选它是因为对甲醛、甲苯、烟雾等常见家庭污染源都有响应输出电压随浓度变化用STM32的ADC采样就能读数。缺点是不能给出精确的ppm数值标定也麻烦但对这个项目而言看的是相对浓度变化和超限报警完全够用。OLED屏选SSD1306方案的0.96寸I2C接口只需要两根线驱动代码成熟嘉立创上几块钱一片。1.3 主控选型为什么锁定STM32F103C8T6而不是其他F103C8T6这颗芯片在老项目里的地位不用多说但确实有几个实打实的原因让我每次做原型都优先选它。资源够用内部Flash 64KB、SRAM 20KB跑这个工程还剩一大半余量后续想加个无线模块或者多接两个传感器都扛得住3.3V供电、5V兼容的GPIO结构和DHT11、MQ135这类传感器对接不用来回做电平转换Proteus有完整的C8T6仿真模型可以先在软件里整机联调再动手焊板子省掉一大半低级错误最小系统板在各大平台都便宜坏了不心疼2. 硬件电路设计原理图核心细节解析原理图我全程在嘉立创EDA里画的这个工具在线就能用元件库全生成的原理图转PCB也方便。很多人画这类项目原理图容易“能连上就行”但有几个细节会在后期调试时直接决定你是十分钟搞定问题还是折腾一晚上值得展开说说。2.1 最小系统电路三大必须注意的细节首先说电源。STM32F103C8T6的供电范围是2.0V到3.6V所以板子上必须有3.3V稳压芯片。我习惯用AMS1117-3.3输入接USB的5V输出端配上10uF和100nF双电容组合滤波。这里有个小细节输入输出端的电容要尽量靠近芯片引脚走线要短否则容易出现上电瞬间电压跌落导致单片机复位失败。再说晶振。F103C8T6内部有RC振荡器可以省掉外部晶振直接跑但内部振荡器精度有限对UART通信这类有时序要求的应用不够稳。我在原理图上保留了8MHz外部晶振加两个20pF负载电容的完整电路。画PCB时晶振下方不要铺地以减少寄生电容对起振的影响。实测用内部RC的时候串口偶发乱码换成外部晶振后一切正常。最后是复位电路。复位引脚接10k电阻上拉到3.3V再接一个100nF电容到地形成上电复位延迟。同时并联一个按键方便手动复位。这里有个排坑点如果复位引脚悬空或者上拉电阻阻值取太大会引入噪声导致芯片随机复位。有次调试程序时芯片频繁重启查了半天才发现是复位引脚虚焊。2.2 传感器接口电路DHT11和MQ135的连接细节DHT11是单总线器件数据引脚需要接一个4.7k上拉电阻到3.3V。这个上拉电阻不是可选项是必须项因为单总线协议要求总线空闲时保持高电平。有些封装好的DHT11模块板上已经集成了上拉电阻可以直接连如果是裸传感器一定要在原理图里补上。数据引脚我接的是PA6选这个引脚没有特别原因纯粹是为了让走线顺一点你也可以换任意GPIO代码里对应改一下就行。MQ135模块的模拟输出接PA1对应ADC1的通道1。模块有四个引脚VCC、GND、AO模拟输出、DO数字输出我这里只用AO因为DO的阈值是模块上电位器固定死的不够灵活。VCC可以直接接5V因为模块内部有调理电路AO输出电压范围在0到4V左右注意STM32的ADC输入范围是0到3.3V所以中间串了一个10k电阻加稳压管保护电路避免电压过高损坏ADC引脚。如果没有稳压管用两个电阻分压也可以但要注意分压比会压缩有效采样范围。2.3 OLED、按键与报警电路设计OLED的SDA接PB7、SCL接PB6对应I2C1外设地址默认0x787位地址0x3C。这里有个比较隐蔽的坑SSD1306的I2C地址可以通过模块背面的电阻选择是0x78还是0x7A代码里设置错了屏幕怎么都不亮。所以画原理图之前先确认模块地址或者代码里写个自动扫描函数。按键电路我用了PA0、PA1、PA2三个引脚每个按键一端接GPIO另一端接地GPIO内部配置为带上拉输入。这样按下时读到低电平松开时读到高电平不需要外部上拉电阻。三个按键的功能分别定义为切换显示页面、确认设置、退出设置。蜂鸣器用NPN三极管S8050驱动PB8输出高电平时三极管导通蜂鸣器发声。为什么用三极管而不是直接驱动因为STM32的GPIO最大输出电流约25mA而蜂鸣器工作电流一般在30mA以上直接驱动容易让引脚过热甚至烧毁。三极管相当于一个电流放大器用小电流控制大电流的通断。2.4 电源与去耦最容易翻车的地方这一段是我画过多次原理图后最想强调的。初学者画图时最爱犯的毛病是忘了在每个芯片电源引脚附近放去耦电容。单片机在工作时电流是脉冲式的电源走线上的寄生电感会阻碍电流突变导致芯片电源引脚上的电压出现尖峰毛刺轻则工作不稳定重则程序跑飞。每个芯片的VCC和GND之间放一个100nF陶瓷电容位置尽量贴近引脚整板电源入口放一个10uF钽电容或电解电容吸收低频纹波STM32的VDDA引脚一定要单独接3.3V并加滤波模拟部分和数字部分电源在入口处用一个磁珠隔离效果更好我这张原理图里MQ135模块的供电单独走一路由PA3控制的MOS管开关这样可以在传感器预热阶段单独控制通断。空气传感器类的加热元件上电瞬间电流很大直接拉低系统电压会导致单片机复位加这个开关后系统启动先让MCU跑起来延时500ms再打开传感器电源稳定得多。3. 嵌入式软件设计代码架构与关键模块实现这一部分是整个项目里东西最多的。代码我基于标准外设库Standard Peripheral Library编写不是用HAL库。原因很简单标准库的逻辑是把寄存器操作封装成函数每一步干了什么清清楚楚非常适合学原理。HAL库封装层次太深遇到bug调试时层层跳转对新手不够友好。如果你后续要用CubeMX生成工程代码移植也不难主要就是改一下底层延时和引脚定义。3.1 工程框架与文件组织整个工程分为以下几个模块每个模块的功能边界很清晰main.c主循环、状态机调度、阈值判断dht11.cDHT11时序读写、数据校验mq135.cADC采样、多点滤波、浓度折算oled.cSSD1306初始化、字符和图形显示key.c按键扫描支持短按和长按buzzer.c蜂鸣器驱动和报警模式控制我写这种综合项目铁律就是“每个文件只做一件事”。DHT11的读取时序里有大量微妙级的延时节拍把它和显示逻辑混在一起调试起来会非常痛苦。模块化了以后某一块出问题直接定位到对应文件也不用来回翻几千行的main.c。3.2 DHT11单总线时序边读边防卡死DHT11的数据格式是40位8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。读取过程分三步主机拉低总线至少18ms发起起始信号释放总线等待从机响应然后依次读取40位数据。这里有一个非常关键的时序细节每一位数据的读取是靠高电平持续时间来区分的。DHT11先拉低50us表示数据位开始然后拉高高电平持续26-28us表示“0”持续70us表示“1”。所以代码里的做法是等待引脚变高后延时40us再读电平读到的就是该位的值。这个40us的判断阈值是整个时序读取的核心取太小会把“1”误判成“0”取太大又可能错过整个位数据。我用定时器微秒延时来实现这个时序同时加了超时保护。因为DHT11是单总线器件一旦传感器没接好或者损坏总线上没有响应程序就会卡死在等待电平变化的循环里。我写的dht11_read函数里每次等待电平状态变化时都设一个时限比如200us超时直接返回错误码。uint8_t DHT11_ReadByte(void) { uint8_t i, byte 0; for (i 0; i 8; i) { while (DHT11_DQ_IN() 0); // 等待低电平结束 delay_us(40); // 延时到数据位中间 if (DHT11_DQ_IN() 1) // 此时读到的电平决定数据位 { byte | (0x80 i); // 如果是高则为1 while (DHT11_DQ_IN() 1); // 等待高电平结束 } } return byte; }这个写法里最关键的就是那个40us延时。我最初按数据手册上写的“延时50us”来写结果读取出来的数据偶发乱跳后来用示波器抓波形发现DHT11的高电平脉宽在标准范围内但50us的采样点太靠近高电平结束边界了。改成40us之后读取稳定率大幅提升一百次读取里基本不会出现校验错误。3.3 MQ135采样ADC读取与数据平滑MQ135输出的是模拟电压STM32的ADC1通道1来做采样。这里要注意的是ADC的参考电压是3.3V所以采样值换算成电压的公式是电压 ADC值 / 4095 * 3.3。这个换算公式看似简单但很多人会在数据类型上翻车如果ADC值和3.3都是整数类型直接相除会得到0必须转成浮点数。MQ135的另一个特性是上电初期输出不稳定。传感器内部的加热丝需要时间达到工作温度刚上电那几十秒输出会飘得很厉害。我代码里做了两个处理一是上电后延时30秒再开始采集这个延时在Proteus仿真时可以改小但实物必须这么干二是采样做了滑动平均滤波每秒钟采10次取平均值作为当前数据。uint16_t MQ135_GetAvgValue(void) { uint32_t sum 0; for (uint8_t i 0; i 10; i) { sum ADC_GetValue(); delay_ms(50); } return (uint16_t)(sum / 10); }滑动滤波本质上是用时间换稳定。单次采样值受噪声影响很大直接用会导致屏幕上数字不停跳动人看着难受阈值判断也容易误触发。取了10次平均值后数据平滑很多同时响应速度还是够快的不会因为滤波而漏掉真实的浓度变化。3.4 主循环状态机与报警联动逻辑主循环用了一个非常简单的状态机四个状态分别是正常显示、设置阈值、报警触发、按键菜单。状态机的状态由一个枚举变量控制按键在中断或扫描里修改这个变量主循环根据状态执行不同的逻辑。报警逻辑单独提一下不是简单的“超限就响”而是做了迟滞处理。比如湿度低于30%触发干燥报警但必须高于35%才能解除报警。这个5%的迟滞区间防止了数据在阈值附近抖动时蜂鸣器反复响。温度、空气质量也同理每个报警项都有独立的迟滞区间。这个设计是从工业控制里的施密特触发器借来的思路嵌入式开发里很实用。if (humidity threshold_low) { alarm_humidity 1; } else if (humidity threshold_low HYSTERESIS) { alarm_humidity 0; }报警触发后蜂鸣器不是一直响而是以2Hz频率间歇鸣叫LED同步闪烁。这个逻辑在buzzer.c里实现用定时器中断来翻转IO状态。直接用delay做响停交替会阻塞主循环导致OLED刷新和按键响应变卡务必用定时器。3.5 OLED显示SSD1306驱动与界面布局SSD1306的驱动代码网上很多核心思路就是初始化寄存器序列然后通过I2C往显存里写数据驱动芯片负责把显存内容刷到屏幕上。0.96寸OLED是128x64分辨率我开了整屏显存模式也就是代码里维护一个128*64/8共1KB的缓冲区所有绘制操作都在缓冲区上做最后用一次I2C批量传输刷新到屏幕。界面布局我设计了两个页面用按键切换。第一页显示实时的温湿度和空气质量等级第二页显示报警阈值设置。数据页面顶部用大号字体显示主要数据底部一行滚动显示状态提示。字符库用8x16的ASCII字模汉字用16x16的取模显示中文时就调用专用的字符绘制函数。这个方案的缺点是字库占用Flash空间16x16汉字一个字要32字节我选了十来个常用汉字占几百字节完全在可接受范围内。I2C通信速率要注意一点SSD1306支持的I2C时钟最高是400kHzSTM32的I2C外设默认配置是100kHz标准模式跑起来没问题。网上有人把I2C时钟配置到1MHz想加快刷新率结果屏幕直接不工作或者花屏就是这个原因。4. 仿真验证Proteus仿真的完整实践Proteus仿真是这个项目的一大亮点也是我强烈推荐先做的一步。硬件没到货之前就可以在电脑上把整个系统的逻辑跑通验证代码有没有低级错误、传感器读取时序对不对、报警逻辑触发是否可靠。我做这个项目的顺序是先画原理图再写代码然后用Proteus联调最后才动手买元件焊板子。4.1 Proteus仿真工程搭建步骤打开Proteus后先按下面的步骤搭工程新建工程选择“ schematic capture”模式在元件库搜索并放置STM32F103C8T6芯片。注意有的Proteus版本里芯片名字叫“STM32F103C8”没有T6后缀放置DHT11模型、MQ135模型用可调电阻电压表模拟、OLED12864模型、按键、LED、蜂鸣器按原理图连线芯片电源引脚VDD、VSS、VDDA都要接好复位引脚接上拉电阻双击STM32芯片在Program File里加载编译生成的hex文件点击运行仿真开始这里最容易被卡住的是第一步Proteus的元件库搜索和实际元件名有时候对不上比如OLED模块在8.x版本里用“OLED12864”或“SSD1306”这个名字不同版本不一样。找不到就直接搜索“OLED”然后从结果里看哪个模型带I2C接口。MQ135如果库里面没有可以用一个电位器加一个电压表头来模拟手动调节电位器相当于改变气体浓度效果一样。我仿真后期甚至直接用了这种方式来做阈值触发测试。4.2 仿真中遇到的三个典型问题第一个问题是STM32的仿真模型对复位电路的要求比真实芯片严格。我第一次仿真时按实物习惯没在复位引脚接上拉电阻结果仿真运行时程序停在那里不动也不进main函数。检查了芯片属性才发现Proteus的模型要求复位引脚必须有明确的电平定义接上10k上拉电阻后一切正常。第二个问题出在DHT11模型上。Proteus里自带的DHT11模型用起来没毛病但初始化时对主机起始信号的时序要求很严格主机拉低时间必须精确到18ms左右。我调试时发现如果延时函数写得不够准模型就直接不给响应。这个问题的排查方法是给延时函数打log或者用虚拟示波器挂在数据线上看波形。第三个问题是我们前面说到的ADC参考电压。Proteus里STM32的ADC模型默认参考电压就是3.3V这在常规用法下没问题。但如果你在仿真电路里给VDDA接了别的电压ADC采样值会和实物完全对不上。出现这种情况时先检查VDDA是不是干净的3.3V。4.3 仿真到实物迁移差异和注意事项仿真跑通后真正的考验才开始。仿真和实物之间存在几个显著差异提前知道能省不少调试时间。仿真是理想化的传感器模型不会受电源纹波影响但实物的ADC采样数值在没做滤波时会跳得让人怀疑人生。我第一次跑实物时MQ135输出的数值上下波动幅度达到了0.3V在空气质量等级判断上反复横跳。加了滑动滤波后显著改善但还是没法完全消除又加了一层“数据变化速率限制”每次最多允许变化5%才能更新显示。仿真里的延时是精确的但实物的8MHz晶振可能和标称值有几个ppm的偏差对DHT11这种微妙级时序的影响不大对UART通信就要注意了。实测STM32内部RC振荡器跑出来的波特率误差接近2%115200波特率下每10个字节就有一两个乱码。还有一点必须提醒仿真里用可调电阻模拟MQ135调起来很“干净”但真实MQ135的上电稳定时间长达几分钟第一次读到的数值基本都是漂的。别一上电看到数据异常就以为电路焊错了先让它预热几分钟再说。这也是我的代码里把“预热等待”做成一个独立状态机步骤的原因界面上会明确显示“Warming Up”。5. 常见问题与排查技巧实录这个项目从原理图到实物跑通我踩过的坑不少有些问题费了好大劲才定位把它们写成快速排查表希望对你有帮助。下面的经验都是实操中验证过的不是凭空想出来的。5.1 传感器读数异常的排查思路症状一DHT11总是读到0或者固定值。先检查数据线上拉电阻这个是最常见的。其次是检查GPIO配置是不是开漏模式单总线必须配置为开漏输出加外部上拉配置成推挽输出会导致通信失败。第三个检查点是延时函数精度如果延时不准确DHT11不会响应。症状二MQ135数值跳变剧烈。先确认采样是多通道ADC是否切换通道后没有足够的采样保持时间。STM32 ADC在切换通道后至少要等一个采样周期再启动转换否则数据会串。可以在启动转换前加一小段延时或者直接用软件连续采样后滤波。症状三OLED屏幕不亮或者显示乱码。最常见的和前面说到的I2C地址有关。另一个常见问题是SDA和SCL接反了大多数OLED模块上这两个引脚是相邻的焊错或者杜邦线插反的概率极高。5.2 编译和下载问题处理Keil编译工程时报“Error: L6218E: Undefined symbol”这类链接错误十有八九是漏了对应的C文件。检查一下工程里把用到的.c文件都添加进去了头文件的Include路径也配置好了。另一个与此类似的问题是函数定义了但没声明C99之前的标准对上文函数的调用是不做隐式声明的在头文件里补上函数声明即可。烧录不进去分两种情况一是提示找不到目标设备检查一下STM32的BOOT0引脚应该通过10k电阻下拉接地确保从Flash启动。二是用ST-Link时连接失败确认驱动装好然后检查接线是否正确。ST-Link的SWDIO接PA13、SWCLK接PA14GND一定要和板子共地不共地大概率连接失败。5.3 代码逻辑相关的隐蔽坑有一个隐蔽但又很坑的问题按键消抖和长按判断写在同一段代码里会导致长按识别失败。原因很简单短按消抖延时通常10ms左右长按判断需要持续检测电平几百毫秒两者如果共用一个循环逻辑会互相干扰。我的做法是用定时器产生1ms的心跳在主循环里通过计数方式实现短按和长按判断而不是用阻塞延时。还有一个经验全局变量不要满天飞。报警阈值、当前传感器数据这些变量如果分散在多个文件里直接用全局变量程序复杂了以后根本不知道在哪改的。我给每个功能模块都定义了带模块前缀的访问接口比如MQ135_GetValue()、Alarm_SetThreshold()模块内部的数据用static修饰外部只能通过接口访问。这样改起来心理负担小很多排查问题也有方向。5.4 快速排查速查表现象最可能原因验证方法解决方案芯片不工作电流异常大电源短路或接反万用表测3.3V对地电阻检查电源部分焊接和去耦电容方向程序烧录失败BOOT引脚配置错误测BOOT0电压BOOT0接10k下拉到地DHT11无响应上拉电阻缺失测数据线空闲电平加4.7k上拉到3.3VOLED花屏I2C速度过快或地址错误降速或扫描地址确认0x3C地址I2C时钟改100kHzMQ135数值漂移传感器未预热等待2分钟观察变化程序里增加预热等待蜂鸣器不响三极管接反测基极电压变化确认S8050引脚定义按键无反应GPIO未配置上拉测引脚电平内部上拉或外部加10k上拉这个速查表是我整个调试过程的浓缩每次卡住了就按表格顺序过一遍大多数问题五分钟内能定位。只有那种物理层问题比如焊桥、板子短路需要借助示波器或者万用表慢慢查。整个项目玩下来最大的体会是仿真帮你验证“单片机代码逻辑”但替代不了“真实硬件电路”。从Proteus转到实物你才能真正理解为什么传感器要预热、为什么电源要加去耦电容、为什么时序要留余量。建议你拿到开源工程后先按我上面的步骤在仿真里跑一遍然后再动手焊板子这样每一步出问题都有参照不会两眼一抹黑。这个工程还留了不少扩展空间比如加个ESP8266做数据上云、加个蓝牙模块推送到手机等到你把这套代码跑通了再往这些方向加功能会发现整个框架不用推倒重来直接在现有模块上扩展就行。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门