尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32空气监测系统开发指南:从传感器选型到固件实现

STM32空气监测系统开发指南:从传感器选型到固件实现 简介基于STM32单片机的空气监测系统毕业设计项目包适合电子、自动化、计算机等专业的学生和开发者用于毕业设计、课程设计或嵌入式入门进阶可解决空气环境数据实时采集、显示与远程查看等问题。压缩包共312个文件整体约15.32MB内含STM32标准库驱动源码.c/.h、Keil工程配置文件.uvprojx/.uvoptx、编译中间文件及hex烧录文件同时附带LCD显示、传感器采集相关代码、说明文档.md/.txt和运行截图.png另有小程序端代码.js/.wxml/.wxss用于数据展示。项目源码采用模块化设计覆盖定时器、ADC、LCD、I2C、CAN、USART等常见外设驱动有助于理解传感器采集、屏幕显示和通信交互的完整流程。配套文档进一步给出系统方案、硬件连接与调试说明可作为毕业设计说明书的重要参考也便于在现有代码基础上修改和扩展。已有200人学习下载代码经过测试运行成功适合相关专业学生直接用于毕设、课设或初期项目验证。1. 从一颗传感器到一套可交付的系统STM32空气监测设计的真实工作量做环境监测类毕设的人通常有两种预期一种是以为买块开发板、插上传感器、串口打印几行数据就算完事另一种是以为要自己从晶体管开始搭电路。实际上这个题目的真实工作量落在中间——你需要在有限的一两周内把传感器选型、信号采集、数据处理、显示与上报、以及一份能讲清楚的文档完整串起来。STM32之所以成为这类项目的主流选择不是因为它性能最强而是因为它的外设资源、库函数生态和调试工具链让一个人在一台电脑前就能完成从裸机到带操作系统的完整闭环。本文按一个可复现的路线展开先定硬件架构再写固件框架接着打通数据链路最后讲调试和打包交付时那些容易让答辩翻车的细节。2. 空气监测系统的硬件选型与传感器接口设计2.1 监测什么目标气体的量程、分辨率与成本取舍空气监测这个说法太宽先要把它拆成可测量的物理量。毕设场景下最常覆盖的是四类温湿度SHT30/DHT22、PM2.5颗粒物PMS7003、CO2浓度SGP30/MH-Z19、甲醛或TVOCSGP30等效。选型时核心矛盾在成本和接口复杂度上。DHT22最便宜但精度和稳定性都差SHT30走I2C精度±0.3°C价格在十元左右适合做主控板内置传感器。PM2.5传感器几乎全是串口输出PMS7003每秒主动发一帧32字节数据解析难度低但要注意它的风扇供电需要5V且电流可达100mA不能直接从STM32的3.3V引脚取电。CO2传感器里MH-Z19是串口SGP30是I2C前者量程5000ppm后者靠算法估算初次上电需要约12小时自校准。选型的核心原则是优先选接口类型少的传感器I2C设备共用总线串口设备各占一个UARTADC类型的模拟气体传感器如MQ系列虽然便宜但需要额外搭建放大电路和校准曲线毕设阶段不建议碰。参数SHT30PMS7003SGP30MH-Z19接口I2CUARTI2CUART供电2.4~5.5V5V1.8~3.3V4.5~5.5V数据内容温度湿度PM1.0/2.5/10CO2TVOCCO2关键坑点地址冲突需配置风扇供电须独立需12h自校准需预热180s参考成本¥8¥45¥40¥80选型之后要画一张电源树。STM32F103C8T6的3.3V由AMS1117从5V降压得到SHT30和SGP30挂3.3VPMS7003和MH-Z19挂5V串口电平注意匹配。如果直接用USB 5V供电PMS7003启动瞬间的电流跌落会导致单片机复位解决办法是在5V母线上并一个470μF电解电容或在传感器供电线上串一个SS14肖特基二极管做隔离。2.2 原理图设计要点I2C上拉、串口电平与去耦布局原理图里最常见的坑是I2C上拉电阻。STM32的I2C引脚是开漏输出必须外接上拉电阻才能产生高电平。对于100kHz标准模式两个上拉电阻各4.7kΩ即可如果想跑400kHz快速模式换成2.2kΩ。但注意SGP30的数据手册对上升沿时间有要求上拉电阻太小会增大灌电流一般取4.7kΩ最稳。串口电平匹配这块PMS7003的TX输出是3.3V电平与STM32兼容但MH-Z19的TX是5V电平直接连到STM32的PA10可能损坏引脚。最稳妥的做法是两个传感器共用一个UART通过一个74LVC1T45电平转换芯片把5V侧降到3.3V。如果不想加芯片在MH-Z19的TX到STM32的RX之间串一个1kΩ电阻也能凑合工作但抗干扰能力会下降。去耦电容的布局建议是每颗IC的电源引脚旁边放一个100nF陶瓷电容放置在尽可能靠近VCC引脚的位置主电源入口放一个10μF钽电容。这部分看似不起眼但PMS7003工作时电机换向产生的尖峰如果没被吸收很容易让ADC采样值出现周期性的跳变。2.3 用STM32CubeMX完成引脚分配与外设初始化先打开STM32CubeMX芯片选STM32F103C8T6时钟配置里把HSE设为8MHz晶振系统时钟倍频到72MHz。这个环节新手容易直接点默认但外设引脚的分配会影响后续布线建议一开始就按下面的分组设置I2C1PB6SCLPB7SDA标准模式100kHz挂SHT30和SGP30USART1PA9TXPA10RX115200-8-N-1接PMS7003USART2PA2TXPA3RX9600-8-N-1接MH-Z19如果选这个模块USART3PB10TXPB11RX用于ESP8266或调试日志输出生成代码后MX_I2C1_Init()等函数会出现在main.c中。这里有一个重要调整把I2C的时钟速率从默认的100kHz改成400kHz因为在100kHz下SGP30的0x2008测量命令从发出到数据就绪需等待50ms周期会拉到100ms以上刷屏不够流畅。3. 固件架构与STM32核心代码实现3.1 裸机还是RTOSf103c8t6的资源边界在哪F103C8T6有64KB Flash和20KB RAM。如果只做传感器采集、OLED显示、串口上报裸机主循环完全够用。如果加上ESP8266的AT指令解析、按键菜单、掉电存储事件状态会变得复杂裸机写起来容易变成一坨if嵌套此时引入FreeRTOS或RT-Thread Nano是更合理的选择。FreeRTOS在F103上跑三个任务大约占4KB RAM和8KB Flash剩余资源依然充足。从工程化角度我更推荐裸机加状态机的组合因为答辩时你能把每个状态转换讲清楚而RTOS容易让人追问任务调度和优先级反转的问题。但如果项目里接了多个串口传感器每个传感器的数据帧到达时间不可预测用RTOS的事件组来同步会比裸机写非阻塞解析舒服得多。3.2 状态机驱动的主循环采集、处理、显示分离主循环的代码结构决定后期调参的效率推荐用时间片轮询加状态机的方式组织。先定义系统状态typedef enum { SYS_BOOT 0, // 上电初始化 SYS_SENSOR_WARMUP, // 传感器预热等待数据稳定 SYS_MEASURE, // 正常采集 SYS_LOWPOWER, // 长时间无操作进入低功耗 SYS_ERROR // 传感器无响应 } sys_state_t; uint32_t last_measure_tick 0; uint32_t measure_interval 2000; // 每2秒采集一轮 while (1) { uint32_t now HAL_GetTick(); switch (current_state) { case SYS_SENSOR_WARMUP: // MH-Z19需要预热180秒PMS7003需要30秒稳定 // 此阶段OLED显示倒计时不采集数据 if (now - boot_tick 30000) { current_state SYS_MEASURE; } break; case SYS_MEASURE: if (now - last_measure_tick measure_interval) { sht30_read(temp, humi); sgp30_read(co2, tvoc); pms7003_parse_latest(pm25); // 数据打包上传或显示 oled_show_all(temp, humi, co2, pm25); last_measure_tick now; } break; default: break; } }逻辑说明这个循环的核心是HAL_GetTick()提供的时间基准它由SysTick中断驱动每毫秒递增一次。所有传感器读取函数必须是非阻塞的如果某个传感器I2C通信失败不能在主循环里用HAL_Delay(50)死等否则会影响其他传感器的采集周期。measure_interval参数是按项目需求可调的风道环境的监测周期通常1~2秒足够如果是室内空气质量监测可以放宽到5秒以降低功耗。3.3 传感器驱动层I2C读取SHT30的完整实现与分析温度湿度传感器是所有信号里最基础的一路它的驱动可以当模板复用。我用STM32的HAL库实现基准是SHT30的0x2C06一次性测量命令#define SHT30_ADDR_WRITE 0x88 // 0x44左移1位写地址 #define SHT30_CMD_MEASURE 0x2C06 uint8_t sht30_read(float *temperature, float *humidity) { uint8_t cmd[2] {0x2C, 0x06}; uint8_t buf[6] {0}; // 1. 发送测量命令 HAL_I2C_Master_Transmit(hi2c1, SHT30_ADDR_WRITE, cmd, 2, 100); // 2. 等待测量完成 HAL_Delay(20); // 3. 读取6字节数据 HAL_I2C_Master_Receive(hi2c1, 0x89, buf, 6, 100); // 4. 校验最后一个字节是CRC8校验码 uint8_t crc crc8_sht30(buf, 2); if (crc ! buf[2]) { return 1; // 温度CRC校验失败 } crc crc8_sht30(buf 3, 2); if (crc ! buf[5]) { return 2; // 湿度CRC校验失败 } // 5. 原始ADC值转物理量 uint16_t raw_t (buf[0] 8) | buf[1]; uint16_t raw_h (buf[3] 8) | buf[4]; *temperature -45.0f 175.0f * raw_t / 65535.0f; *humidity 100.0f * raw_h / 65535.0f; return 0; }参数说明写地址0x88是把SHT30的7位设备地址0x44左移一位并补上写标志位0读地址0x89是左移一位补1。第4步的CRC校验是SHT30协议的一部分初始值为0xFF多项式为0x31实现很短但必须写对否则偶尔会出现读出的温湿度跳变一个不合理值如果你在调试中发现数据偶发异常但设备没坏先检查这一步。第5步的转换公式是数据手册给出的线性映射不需要额外校准。3.4 串口传感器PMS7003的无缓冲帧解析PMS7003每帧32字节帧头固定为0x42 0x4D。它的坑在于单片机开机时传感器可能已经发送了一半数据或者UART接收缓冲中有残余字节所以直接按帧头找第0字节是常见错误。我一般用状态机解析代码片段如下#define PMS_FRAME_LEN 32 static uint8_t pms_rx_buf[PMS_FRAME_LEN]; static uint8_t pms_rx_index 0; static uint8_t pms_rx_state 0; // 在UART1接收中断里逐字节调用 void pms7003_uart_isr_handler(uint8_t byte) { switch (pms_rx_state) { case 0: if (byte 0x42) pms_rx_state 1; break; case 1: if (byte 0x4D) { pms_rx_state 2; pms_rx_index 2; pms_rx_buf[0] 0x42; pms_rx_buf[1] 0x4D; } else { pms_rx_state 0; // 帧头错误重新等待 } break; case 2: pms_rx_buf[pms_rx_index] byte; if (pms_rx_index PMS_FRAME_LEN) { // 帧满校验帧尾两个字节之和为0 uint16_t sum 0; for (int i 0; i 30; i) sum pms_rx_buf[i]; if ((sum 0xFFFF) ((pms_rx_buf[30] 8) | pms_rx_buf[31])) { pms_data_update(pms_rx_buf[4]); // 偏移4字节取PM2.5值 } pms_rx_state 0; } break; } }说明这个状态机的核心思想是逐字节消费串口数据不依赖任何定时器或缓冲区大小。注意最后一步的校验和算法PMS协议是以帧头到校验字节前所有字节的累加和末尾16位作为帧尾所以遍历0到29共30字节和帧尾的16位比较。比较前记得用(sum 0xFFFF)截断因为累加可能溢出。4. 数据展示、本地存储与远程上报链路4.1 OLED显示的人机交互与实时刷屏策略0.96寸OLED是空气监测项目的主流显示方案SSD1306驱动I2C接口128x64分辨率。刷屏上的最大问题是OLED的I2C带宽限制。SSD1306的I2C时钟写寄存器上限是400kHz而全屏刷新需要8页×128字节1024字节数据传输即使400kHz下也需要约40ms如果每100ms刷一次I2C总线会被OLED占死没时间读传感器数据。我的处理方式是把显示内容分区刷新。固定文字标题、单位只在初始化时写入SSD1306的GDDRAM之后每次只更新数字所在区域的4~5个字符块。把sprintf格式化好的字符串按字符取模调用oled_show_string(x, y, text)只改写对应坐标区域刷一轮数据的总耗时可以压到5ms以内。4.2 用ESP8266做WiFi上报AT指令与数据帧格式设计ES8266负责把监测数据推送到远端或局域网内的MQTT Broker。这里需要明确ESP8266通过串口和STM32通信典型模型是STM32作为主机发送AT指令ESP8266作为从机执行网络操作。最关键的设计是不要让STM32阻塞等待AT指令应答。一个常用方案是发送指令后记录当前状态在串口接收中断里根据响应内容进行状态迁移。温度、湿度、PM2.5、CO2等数据整合成一帧JSON格式样例为{dev:air_monitor_01,t:25.6,h:48.5,pm25:23,co2:680,tvoc:120}在STM32侧打包JSON的注意点是浮点格式。使用%.1f打印浮点要开启Newlib的浮点支持在Keil的Options→Target→Code Generation里勾选Use MicroLIB。MicroLIB能显著减小固件体积但它默认不完整支持浮点printf格式化需要在printf之前加一句setvbuf(stdout, NULL, _IONBF, 0)或直接改用sprintf否则就会出现温度显示为?的现象。如果不勾MicroLIB而用标准库printf要占据大约12KB Flash在64KB的F103上会吃紧。4.3 Modbus RTU帧接收程序跑数据上报时最容易翻车的地方搜索热词里“modbus单片机帧接收数据程序”是个高频需求。如果你准备让这套空气监测系统接入上位机组态软件把主机设计成Modbus从机是工程上最稳的方案。Modbus RTU的帧问题是帧间间隔超过3.5个字符时间就算断帧而9600波特率下3.5字符约3.6ms115200下约0.3ms。如果还用传统的堵在中断里拼缓冲区的方式主循环稍慢就会丢帧。实际工程中推荐的做法是配合空闲中断IDLE来断帧在STM32的HAL库中对应HAL_UARTEx_ReceiveToIdle_IT。以下是一个示例uint8_t modbus_rx_buffer[64]; uint8_t modbus_rx_len 0; void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart huart3) { // 假设Modbus从机用USART3 modbus_rx_len Size; // Size是本次空闲中断接收到的字节数直接就是完整一帧 modbus_process_frame(modbus_rx_buffer, modbus_rx_len); // 重新开启空闲接收 HAL_UARTEx_ReceiveToIdle_IT(huart3, modbus_rx_buffer, sizeof(modbus_rx_buffer)); } } // 初始化时启动 HAL_UARTEx_ReceiveToIdle_IT(huart3, modbus_rx_buffer, sizeof(modbus_rx_buffer));参数说明Size代表从上次接收到本次空闲之间积累的字节数因为回调触发点就是空闲事件所以这里天然作为一帧的结束。相比自己写的超时断帧逻辑它能省去HAL_GetTick判断且不占用主循环。Modbus从机处理完请求后需要把返回的数据帧放入发送缓冲再在TXE中断里逐字节发送否则长时间占用主循环会再次丢帧。5. 调试手段、数据校准与完整项目资料的整理方式5.1 串口日志分级与最小调试法则调试这套系统时最有效的工具不是IDE自带的仿真器而是从USART2输出日志信息。建议做一个极简的日志宏分级级别为ERR、WARN、INFO、DBG通过宏开关控制编译#define LOG_LEVEL 3 // 0ERR, 1WARN, 2INFO, 3DBG #define LOG_PRINT(level, fmt, ...) do { \ if ((level) LOG_LEVEL) { \ printf([%lu] fmt \r\n, HAL_GetTick(), ##__VA_ARGS__); \ } \ } while (0) // 使用示例 LOG_PRINT(2, SHT30 CRC err: %d, crc);使用%lu打印HAL_GetTick()能直观看到每个事件发生的时间串口助手的显示也易于观察传感器之间的采集周期关系。调试时可以把日志输出映射到USART2数据上报的Modbus和ESP8266放在USART3各占各的串口避免日志和数据互相污染。用不上仿真的场景绝大多数出在时序问题上串口日志足够定位。5.2 传感器同步采集与异常值的滤波处理四个传感器同时采集后先做粗大误差剔除再做滑动平均滤波。温度、湿度、PM2.5和CO2的变化速率不同滤波窗口大小也不同。温度窗口取5次PM2.5取10次因为PM2.5波动最剧烈、容易受风扇扰动影响。中位值平均滤波法比较适合PM2.5连续采5个值去掉最大最小再算平均#define PM_FILTER_SIZE 5 uint16_t pm25_median_filter(uint16_t new_value) { static uint16_t buf[PM_FILTER_SIZE]; static uint8_t index 0; uint16_t sorted[PM_FILTER_SIZE]; buf[index] new_value; index (index 1) % PM_FILTER_SIZE; // 复制数组并排序 memcpy(sorted, buf, sizeof(buf)); // 冒泡排序取中间值代码略 return sorted[PM_FILTER_SIZE / 2]; }这个滤波器的特点是对脉冲干扰有较强的抑制力同时响应速度不至于延迟太多。注意传感器的数据手册中通常会有“响应时间”指标比如SGP30的响应时间约10秒在做滤波时不要叠加过长的窗口否则显示值变化会出现明显的滞后感。5.3 温湿度补偿在空气监测中的应用气体传感器对温湿度敏感其中SGP30的TVOC算法内置了温湿度补偿但前提是要给传感器写入当前的温湿度值。SGP30的set_compensation命令需要传入温度和湿度。STM32侧先把SHT30读出的浮点温湿度转换为固定点整数然后组合成两个16位参数// 温度补偿参数格式0x10倍的温度值0x4000偏移 uint16_t comp_temp (uint16_t)((temp 25.0f) * 10.0f); // 湿度补偿参数格式0x400倍的实际湿度值 uint16_t comp_humi (uint16_t)(humi * 400.0f); uint8_t cmd[5] {0x20, 0x60, (comp_temp 8) 0xFF, comp_temp 0xFF, 0x00}; HAL_I2C_Master_Transmit(hi2c1, 0x58 1, cmd, 5, 100);这个补偿数值写不对SGP30在温湿度波动大的场景下读数会漂移。如果对精度有进一步要求MH-Z19这类官方有温湿度补偿接口的传感器也可以按类似方式处理。5.4 毕业设计资料的完整组织与最终复盘项目源码、原理图、PCB、芯片手册、文档最终要作为完整交付物打包。从工程交付角度看建议把文件分成五块项目工程源码不包含编译器生成的临时文件、Altium或立创EDA的原理图导出PDF、系统框图与流程图Visio或draw.io产出、答辩PPT只放关键帧、以及一份约6000字的设计文档。源码目录建议按下述结构组织这同时也方便后续维护air_monitor/ ├── Core/ │ ├── Inc/ │ ├── Src/ │ └── Start/ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── Sensors/ │ ├── sht30.c │ ├── sgp30.c │ ├── pms7003.c │ └── mhz19.c ├── App/ │ ├── display.c // OLED界面 │ ├── measure.c // 数据采集调度 │ └── report.c // 数据上报与Modbus处理 ├── Docs/ │ ├── design_notes.md │ └── datasheet/ └── README.mdREADME里写清楚硬件接线表、软件环境版本STM32CubeMX版本、Keil版本、默认串口参数和JSON格式说明这部分往往是答辩时评委快速了解项目的入口。做一个合格的毕设项目代码可以不是最优的但文档里必须能看出你对每个设计决策有明确的权衡依据——传感器为什么选它、串口缓冲区开多大、为什么用空闲中断而不是定时器断帧回答好这些“为什么”显然比单纯把功能跑通更有价值。本文还有配套的精品资源点击获取
返回列表