ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

STM32室内环境监测系统:传感器选型、I2C通信与ESP8266上云实战

STM32室内环境监测系统:传感器选型、I2C通信与ESP8266上云实战 简介基于STM32的室内环境监测系统设计文档是一份面向电子、自动化或嵌入式方向学生及入门开发者的毕业设计/课程设计参考资料。文档以STM32F103C8T6单片机为核心系统讲解了温湿度、声音、光强、甲醛等多类传感器采集电路以及按键、LCD12864显示等模块的硬件设计与接口实现内容覆盖系统总体方案、单片机最小系统、各模块电路原理和设计要点适合需要完成类似软硬件课题或理解传感器信号链路的读者参考。该资源为单个doc文档大小1.97MB携带方便可直接用于阅读、编辑和后续扩展。文档中包含了MIC信号放大与滤波、TEMT6000光强采集、DHT11温湿度单总线通信、甲醛检测UART接口及12864显示等具体电路细节并配有硬件系统框图和接口连接说明能够帮助读者快速建立整体设计思路并迁移到实际项目中。目前已有214人学习下载对于正在筹备嵌入式系统设计、环境监测类课题的开发者具有较好的参考价值。1. STM32室内环境监测系统先把数据路径和心理预期理清楚室内环境监测系统在毕业设计和工程预研里出现频率很高但多数人做完只得到一个“能显示数字”的演示板真正的问题反倒在演示之后丢数据、显示跳变、报警误触发、连上WiFi后串口卡死。这个以STM32为主控的室内环境监测系统涉及的是温湿度、光照、气体浓度等多路采集到数据处理、显示、报警乃至上云的一条完整链路。设计开始之前不需要先纠结用哪个传感器型号而是先回答一个问题数据给谁看、往哪走。单机显示和局域网推送决定了MCU的引脚资源、存储需求、是否需要RTOS和网络协议栈这个问题定下来再往下选型才有依据。2. 传感器选型与信号链DHT11、SHT30、BH1750、MQ-2的接法与参数门道2.1 传感器接口类型决定引脚规划和代码结构STM32的GPIO数量在F103C8T6上只有37个可用引脚如果同时接温湿度、光照、气体、OLED和WiFi模块引脚的分配直接决定后面接线会不会乱。按接口类型分DHT11/DHT22是单总线一个引脚既做输出又做输入占用资源最少但时序要求严格SHT30和BH1750是I2C设备只需两根信号线就能挂多个器件MQ-2这类气体传感器输出模拟电压要接到STM32的ADC引脚。做这个系统设计时我一般先把三类接口分开单总线、I2C、ADC各留一组引脚尽量避免一个引脚同时复用多个功能后续调试串口和烧录器也必须空出来。SHT30和BH1750都支持I2C地址分别为0x44SHT30默认和0x23BH1750 ADDR引脚接地地址位可以规划在同一条总线上但要注意OLED如果也用I2C且地址是0x3C那么总线上会有三个设备。I2C总线上拉电阻一般选择4.7kΩ线长超过20cm时改为2.2kΩ否则在高速通信或长线上会出现数据毛刺。2.2 供电与电源AMS1117不是随便搭的传感器模块的供电是这个项目里最容易被低估的部分。MQ-2的加热电流在150mA到180mADHT11、BH1750等数字传感器正常工作电流只有几毫安但ESP8266在WiFi发射瞬间电流会冲到300mA以上如果把所有模块都接在同一个AMS1117-3.3V稳压器后面压降会造成传感器读数周期性跳动。对于基于STM32的毕业设计或原型我建议USB 5V直接用AMS1117-3.3V给MCU和I2C传感器供电ESP8266单独用一颗低噪声LDO或者直接进外部5VMQ-2则从5V取电并把输出引脚接到STM32的ADC注意ADC输入范围不能超过3.3V需要用分压电阻把传感器输出限制在0到3.3V之间。AMS1117的输出电容建议用10uF钽电容并联100nF陶瓷电容单独用陶瓷电容时LDO容易自激震荡这在STM32系统里会导致ADC基准不稳。提示DHT11读取间隔不能小于1秒如果定时器中断里每秒读一次在SHT30和DHT11混用的系统中要先把DHT11的读取调度放在非阻塞任务里否则主循环会被单总线时序卡住。2.3 关键参数对照表下面这个表列出三种常见选型组合可做为设计中先定方案再画原理图的参考传感器/模块接口供电关键参数典型场景DHT11单总线3.3V/5V精度±2℃、±5%RH采样周期1s低成本温度与湿度采集SHT30I2C3.3V精度±0.2℃分辨率0.01℃更可靠的温湿度监测BH1750I2C3.3V/5V量程1-65535lx功耗约1mA室内光照度检测MQ-2模拟电压5V加热预热时间约1分钟输出非线性烟雾和可燃气体阈值报警OLED 0.96I2C/SPI3.3V地址0x3CI2C本地实时显示参数门道在于数字接口虽然方便但也带来两个隐性成本一是I2C设备地址冲突二是单总线时序误差。MQ-2这类模拟输出则要把ADC的参考电压当作一个重要因素考虑STM32F103内部参考电压只有1.29V上下用作百分比或非线性阈值时需要做一次多点标定。3. 开发环境与数据采集keil5装芯片包、标准库新建工程、GPIO与I2C读写3.1 从keil5安装STM32芯片包到标准库工程在keil5新建STM32工程时最常遇到的坑是没有安装对应芯片的设备支持包。打开Pack Installer在搜索框输入STM32F1勾选Keil::STM32F1XX_DFP版本选和当前MDK兼容的正式版安装完成后新建工程时才能找到STM32F103C8。如果只安装了C51的Pack就会出现找不到器件的情况这点和标准库无关。也有部分用户已经装了芯片包但新建工程时仍找不到Flash这是因为没有在Utilities或Target页勾选正确的Flash算法。标准库新建工程时我习惯用这套最小结构启动文件、内核相关文件、外设库源文件、用户主文件。启动文件在Libraries文件夹里的对应器件型号下外设库需要加入stm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_i2c.c、stm32f10x_adc.c等不需要的外设可以裁掉但要保留stm32f10x_it.c以处理中断跳转。操作STM32的GPIO时标准库写法比寄存器直观例如在初始化I2C引脚时先开启GPIOB和AFIO时钟再配置PB6/PB7为开漏输出然后设置I2C外设时钟。HAL库在CubeMX下配置更方便但工程体积和RAM占用都比标准库大本项目功能不复杂标准库或HAL都可以关键是核对中断优先级分组和系统时钟频率。3.2 软件I2C读取SHT30地址换算和时序细节硬件I2C在STM32上使用简单但多设备总线且没有示波器时软件I2C更容易排查问题。SHT30的I2C地址0x44左移一位后得到0x88写地址和0x89读地址很多新人卡在等不到ACK上就是这里算错了。下面的软件I2C代码在标准库工程中可以直接编译重点是起始信号、停止信号和等待ACK函数#include stm32f10x.h #include soft_i2c.h #define SDA_GPIO GPIOB #define SDA_PIN GPIO_Pin_7 #define SCL_GPIO GPIOB #define SCL_PIN GPIO_Pin_6 static void SDA_OUT(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 开漏输出配合外部上拉 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(SDA_GPIO, GPIO_InitStructure); } static void SDA_IN(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(SDA_GPIO, GPIO_InitStructure); } static void I2C_Start(void) { SDA_OUT(); SCL_HIGH(); SDA_HIGH(); delay_us(5); SDA_LOW(); // 时钟高电平期间SDA拉低产生起始条件 delay_us(5); SCL_LOW(); } static void I2C_Stop(void) { SDA_OUT(); SCL_LOW(); SDA_LOW(); delay_us(3); SCL_HIGH(); delay_us(3); SDA_HIGH(); delay_us(5); } static void I2C_WriteByte(uint8_t dat) { SDA_OUT(); for (uint8_t i 0; i 8; i) { if (dat 0x80) SDA_HIGH(); else SDA_LOW(); delay_us(3); SCL_HIGH(); delay_us(3); SCL_LOW(); dat 1; } } static uint8_t I2C_WaitAck(void) { SDA_IN(); SCL_HIGH(); delay_us(5); uint8_t ack (GPIO_ReadInputDataBit(SDA_GPIO, SDA_PIN) Bit_RESET) ? 0 : 1; SCL_LOW(); SDA_OUT(); return ack; } static uint8_t I2C_ReadByte(void) { uint8_t dat 0; SDA_IN(); for (uint8_t i 0; i 8; i) { SCL_HIGH(); delay_us(3); dat 1; if (GPIO_ReadInputDataBit(SDA_GPIO, SDA_PIN) Bit_SET) { dat | 0x01; } SCL_LOW(); delay_us(3); } return dat; } static void I2C_Ack(void) { SDA_OUT(); SDA_LOW(); SCL_HIGH(); delay_us(3); SCL_LOW(); SDA_IN(); } static void I2C_NAck(void) { SDA_OUT(); SDA_HIGH(); SCL_HIGH(); delay_us(3); SCL_LOW(); SDA_IN(); } uint8_t SHT30_ReadTempHum(float *temp, float *hum) { // 发送0x2C 0x06表示开启高重复性周期测量 I2C_Start(); I2C_WriteByte(0x88); if (I2C_WaitAck()) return 1; I2C_WriteByte(0x2C); I2C_WaitAck(); I2C_WriteByte(0x06); I2C_WaitAck(); I2C_Stop(); delay_ms(10); // 等待测量完成 I2C_Start(); I2C_WriteByte(0x89); if (I2C_WaitAck()) return 2; uint8_t d[6]; for (uint8_t i 0; i 6; i) { d[i] I2C_ReadByte(); if (i 5) I2C_Ack(); else I2C_NAck(); } I2C_Stop(); // 根据SHT30数据手册换算实际物理量 *temp -45.0f 175.0f * (float)((d[0] 8) | d[1]) / 65535.0f; *hum 100.0f * (float)((d[3] 8) | d[4]) / 65535.0f; return 0; }这段代码要留意的是开漏输出和浮空输入的切换。开漏模式下外部4.7k上拉保证电平可靠SDA_IN后在时钟高电平时读取引脚电平如果忘了把SDA切回输出模式后续写数据时SDA永远拉不低I2C通信就会卡死在第一个ACK。2C 06命令是高重复性测量周期约15ms实际睡眠10ms已经足够若用低重复性0x2C 0x10等待时间可缩短到5ms但数据跳变会稍大。如果采用HAL库把上面的GPIO操作替换成HAL_GPIO_WritePin和HAL_GPIO_ReadPin即可I2C时序逻辑不需要改动。软件I2C在系统主频72MHz下用delay_us延时比较稳定若把系统时钟配成8MHz内部时钟delay参数要重新标定否则SHT30会返回超时。3.3 采集数据的软件滤波滑动平均与边界校验直接从传感器读到的数据不适合直接显示DHT11数字输出在全量程内有±1℃的量化台阶SHT30在无风环境下也会因空气流动出现0.1℃级波动。常见的做法是三步处理合法性检查、滑动平均、变化率限幅。合法性检查过滤掉总线误码产生的明显错误数据比如温湿度为0或大于100滑动平均用一个长度为8的环形数组每次新值进来求平均变化率限幅则把相邻两次采集的跳变限制在合理范围防止异常值闯入报警判断。#define FILTER_SIZE 8 static float temp_buf[FILTER_SIZE]; static uint8_t temp_index 0; float Temp_Filter(float new_val) { if (new_val -40.0f || new_val 125.0f) { return temp_buf[(temp_index FILTER_SIZE - 1) % FILTER_SIZE]; } temp_buf[temp_index] new_val; temp_index (temp_index 1) % FILTER_SIZE; float sum 0.0f; for (uint8_t i 0; i FILTER_SIZE; i) { sum temp_buf[i]; } return sum / FILTER_SIZE; }这个滤波函数在处理串口和DMA传输时不会造成阻塞但要注意它依赖定时器每1秒填充一次缓冲如果外部环境温度变化很快例如空调启动时8秒平均会造成明显滞后。此时可以把窗口缩小到4或改用带权重的指数平均new_avg old_avg * 0.7 new_val * 0.3。报警系统里更建议做滞回比较阈值设为26℃触发、降到25℃以下才解除避免风扇或蜂鸣器在阈值附近反复抖动。4. 显示、调度与阈值报警OLED刷新、定时器分频和按键去抖4.1 OLED挂在I2C总线上地址冲突与上拉电阻0.96寸OLED模块最常见的主控是SSD1306I2C模式下从机地址为0x3C断电后改地址跳线可变成0x3D。这个地址和SHT30、BH1750并不冲突但三条总线上从机增多后每个从机的输入电容会叠加在一起总线延时变长原有4.7k上拉电阻可能让SCL上升沿变缓。排查时不必换电阻先用示波器看SCL上升沿如果边沿超过1us就换2.2k上拉。OLED刷新时不要每一帧都全屏清屏SSD1306驱动写整屏最多需要8192字节通过I2C传输需要约250ms这么长的阻塞式刷新会让STM32按键响应急剧变慢所以显示要采用局部更新策略只更新变化的数字区域把温度、湿度、光照分别放在固定的坐标区每1秒只写这些区域。4.2 定时器分频与1秒采集调度基于STM32的毕业设计中主循环里放delay_ms是最容易让系统卡死的写法。SHT30的I2C读取占用大约20msDHT11的单总线读取占用10ms以上OLED刷新能占到几百毫秒这些阻塞时间叠加后按键扫描和串口收包就不是实时响应了。常见的替代方案是用TIM2做1秒时基在主循环中通过标志位判断是否采集和刷新。以STM32F103主频72MHz为例把TIM2预分频设为7200-1自动重载值为10000-1这样定时器每1秒溢出一次void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); collect_flag 1; // 主循环检测到该标志后执行采集任务 } } void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler 7200 - 1; // 72MHz / 7200 10kHz TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 10000 - 1; // 10kHz下计数10000次为1秒 TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }主循环里只做三件事检查collect_flag、读取并滤波、刷新局部显示其余时间留给按键扫描和串口函数。这种调度方式的功耗比delay方式低不少因为MCU在无任务时能进入WFI低功耗状态。如果想跑FreeRTOS就多建一个定时器和两个任务但UCOS和FreeRTOS在这个量级下对系统帮助有限简单的前后台轮询反而可控性更好。4.3 阈值报警与按键模块的去抖处理报警部分牵涉到按键设置阈值和状态判断。STM32按键模块电路设计上常见选择是按键一端接GPIO、另一端接GNDGPIO内部上拉而STM32F103内部上拉阻值约30-50k如果按键线缆超过10cm就最好外部加10k上拉。读取按键时不能直接在下降沿中断里做判断机械按键抖动通常在5-10ms直接读取会得到多次跳动。uint8_t Key_Scan(void) { static uint8_t key_last 1; // 上一次电平状态默认高电平 static uint8_t debounce_cnt 0; uint8_t key_now GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN); if (key_now ! key_last) { debounce_cnt; // 电平变化时累加直到稳定 if (debounce_cnt 5) { key_last key_now; debounce_cnt 0; if (key_now 0) { return KEY_PRESSED; // 消抖后确认一次按下 } } } else { debounce_cnt 0; } return KEY_NONE; }这里消抖的本质不是固定delay而是连续多次采样同一电平适用于短按键快速设置。调校阈值时用这组规则温度超过上限并且连续两次采样都高于上限才触发报警蜂鸣器响之后要等3秒才能再次触发防止同一事件重复报警。实际项目中浮点比较在编译器浮点精度不一致时偶发误判更好的做法是把温度放大10倍变成整数再比较比如263表示26.3℃。5. 用ESP8266把数据推给上位机以及三个最值得提前踩的坑5.1 ESP8266 AT指令透传的写法单机显示之外这个系统设计还预留了数据上云能力。ESP8266与STM32连接原理图不算复杂核心是交叉接串口、共地以及CH_PD拉高。直接使用AT固件时先把ESP8266的串口波特率设为115200然后用ATCWMODE1配置为Station模式ATCWJAP命令连接路由器TCP透传时用ATCIPSTART建立连接最后用ATCIPSEND发送数据。发送JSON格式信息时注意字符串拼接不能一次性写太长数据较大时分成多次发送每次发送后等待SEND OK。char json[128]; sprintf(json, {\t\:%.1f,\h\:%.1f,\l\:%.0f}, temp, humi, lux); UART_SendString(ATCIPSEND); UART_SendNumber(strlen(json)); UART_SendString(\r\n); delay_ms(50); UART_SendString(json);AT指令前的\r\n必须带全很多ESP8266固件对不带换行的AT指令无响应。联网模块上电后大约需要2-3秒才能完成启动如果串口发送过早报文会被丢弃常见做法是启动后延时3秒再依次发送查询指令。由于ESP8266功耗峰值高WiFi发射时3.3V电压跌落会导致模块自行重启这也是“联网后读不到数据”的最常见原因。5.2 整机验证与三坑提醒整机调试时先从串口开始可信度最高。把STM32的USART1接到USB转TTL用串口调试助手每秒打印一次温度、湿度、光照和报警标志位确认数据稳定后再接显示和网络。不要在没串口日志的情况下直接接ESP8266不然分不清是传感器错误还是网络失败。第二个值得提前踩的坑是ADC基准如果光照用光敏电阻ADC实现STM32的VDDA必须经过LC滤波否则数码管和继电器动作时ADC读数会跳十几个值。第三个坑是看门狗和堵死循环的关系如果程序中用了IWDG但主循环里OLED刷新是阻塞式的长刷新时看门狗超时复位会让系统始终在重启这类现象要看RCC_GetFlagStatus的复位标志来区分而不是仅怀疑代码死循环。最后给一个实用技巧在STM32待机模式下把RTC闹钟设为每10秒唤醒一次醒来后1秒内完成采集和发送再回到待机这样整机电流能从几十毫安降到微安级别适合室内无人长期监测的场景里省电。整个系统的最后验证标准不是屏幕能显示数值而是连续运行24小时后数据不丢、报警不误报、设备不复位通常能稳定跑过48小时再把看门狗打开才算具备交付条件。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表