基于STM32的环境检测系统:从传感器选型到数据标定全指南
2026/9/13 20:00:49 网站建设 项目流程

简介:面向嵌入式开发者和单片机学习者,提供一套基于STM32F103C8T6的多节点环境检测系统完整工程。系统由多个检测基点与基站组成,可同时监测不同位置的环境数据,接入DHT11、SGP30、MQ-135等传感器完成温湿度与空气质量采集,通过OLED显示,并在数据异常时借助HC-05和SIM800C实现本地提醒与手机短信报警。压缩包共452个文件,约11.16MB,包含88个c源码、83个h头文件、2个uvprojx工程文件,以及hex、axf烧录文件和bat辅助脚本,另有大量o、d等编译中间文件,可直接打开工程查看代码并烧录验证。已有1834人学习下载。通过对照源码与工程结构,可快速理解多节点组网与基站通信思路,掌握传感器驱动、显示、蓝牙及短信模块的调用方法,适合课程设计、毕业设计及嵌入式入门进阶实践。

1. 基于STM32的环境检测系统,先想清楚检测什么再动手

温室大棚、设备机房、实验室或者家里的鱼缸,这类场景对温湿度、光照和空气质量的监测需求高度相似,但不少人一上来就把传感器插到板上,读出一串乱码才开始怀疑硬件。基于STM32的环境检测系统,本质上是把「传感器信号 → 单片机采集 → 数据处理 → 输出展示」这条链路一次想通,再落成一块能稳定跑几个月的板子。

真正的难点不在 STM32 本身,而在信号链路的匹配:数字传感器走 I2C/SPI,模拟传感器要过 ADC 还得解决基准电压和噪声,气体传感器的加热电流会扰动板上电源。把这些边界处理干净,核心代码量反而很小。

这套方案适合两类人:做课程设计或毕业设计、需要把方案讲圆的学生,以及做产品原型验证、想用最小成本确认选型和采样策略的工程师。下文按我自己做这类项目的顺序展开,从选型、工程搭建、数据调试一路讲到标定验证。

2. STM32环境检测系统的传感器选型与信号链路

选传感器之前,先给系统定三个硬指标:测量范围、允许误差、采样间隔。指标没定就买模块,后面所有调试都会变成和精度较劲。

2.1 先定指标再选传感器:精度、量程与响应时间

比如温室监控要求温度 -10~50℃、湿度 20%~90%RH、误差不超过 ±0.5℃ 和 ±3%RH,那就别用 DHT22。DHT22 标称 ±0.5℃ / ±2%RH,但湿度迟滞明显,适合低成本粗略监测;SHT30 或 AHT20 这类 I2C 数字传感器精度更高、漂移更小,价格只贵几块钱。光照用 BH1750,自带 ADC 和 lux 换算,省一路模拟通道;空气污染只做定性判断就用 MQ 系列,接 ADC 读电压即可,但要注意它内部有加热丝,需要预热几分钟数据才稳。

传感器接口典型精度/量程是否需预热主要坑
DHT22单总线±0.5℃ / ±2%RH,-40~80℃时序敏感,两次读取间隔需大于 2s
SHT30I2C±0.3℃ / ±2%RH地址可配,默认 0x44
AHT20I2C±0.3℃ / ±2%RH上电后有初始化命令序列,不能直接读
BH1750I2C1~65535 lx连续测量和单次测量模式不要混用
MQ-135ADC 模拟定性气体浓度是(3~5 分钟)加热电流影响电源纹波,需单独供电或加电容

这个表不是参数手册,是提醒你关注三件事:读时序是否苛刻、上电是否需要初始化流程、模拟型传感器对板上电源质量的要求。DHT22 这类单总线器件对时序要求高,中断一多就容易误读,工程上我宁愿换 I2C 器件省心;MQ 系列如果和数字传感器共用一个 3.3V LDO,加热瞬间会把电压拉低,造成旁边温度读数跳变,常见做法是给 MQ 的加热端独立供电,或者至少串联一个 100μF 电解电容。

2.2 I2C 与 ADC 通道分配:STM32 外设够不够用

STM32F103C8T6 提供两个 I2C、三个 USART、一个 12 位 ADC,对环境检测这种低速场景绰绰有余,但引脚复用要提前规划。I2C1 的 SCL/SDA 在 PB6/PB7,I2C2 在 PB10/PB11,如果这两组脚还要驱动 OLED 或按键就会打架;ADC1 的多路通道共用一个转换器,切换通道后的第一次转换结果要丢弃,因为内部采样电容还没稳定。把温湿度挂在 I2C1、光照挂在 I2C2、气体挂在 ADC1 的 PA1,三路外设互不干扰,这是最省心的分配方式。

另一个常见坑是烧录口占用。SWD 用的 PA13/PA14 默认被调试功能占用,如果初始化代码里把这两个引脚复用成 GPIO 输出,下次下载时调试器连不上,只能按住复位再点下载,非常难受。我一般把业务引脚全部避开 PA13/PA14,CubeMX 里调试口保持 SWD 模式,不做任何复用。

2.3 传感器初始化与读取代码:I2C 读 SHT30 的最小实现

用 STM32CubeMX 生成工程后,I2C 读写直接调 HAL 库接口。下面这段是读 SHT30 温湿度的核心流程,包含发送测量命令、等待测量完成、读取 6 字节结果:

// 假设 hi2c1 已由 CubeMX 初始化;SHT30 写地址 0x89,读地址 0x88 #define SHT30_ADDR_W 0x89 #define SHT30_CMD_HIGH 0x2C #define SHT30_CMD_LOW 0x06 typedef struct { float temperature; float humidity; } SHT30_Data; uint8_t SHT30_ReadOnce(SHT30_Data *out) { uint8_t cmd[2] = {SHT30_CMD_HIGH, SHT30_CMD_LOW}; uint8_t buf[6]; // 单次测量命令:0x2C 0x06 表示高重复度,时钟拉伸模式 if (HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR_W, cmd, 2, 100) != HAL_OK) return 0; HAL_Delay(20); // 留出测量时间余量,SHT30 高重复度约 15ms if (HAL_I2C_Master_Receive(&hi2c1, 0x88, buf, 6, 100) != HAL_OK) return 0; uint16_t rawT = (buf[0] << 8) | buf[1]; uint16_t rawH = (buf[3] << 8) | buf[4]; if (rawT == 0 || rawH == 0) return 0; // 全 0 多半是总线异常 out->temperature = -45.0f + 175.0f * rawT / 65535.0f; out->humidity = 100.0f * rawH / 65535.0f; return 1; }

代码逻辑说明:SHT30 的单次测量命令由两字节组成,0x2C 0x06是高重复度模式,重复度越高采样越稳但耗时越长;发完命令后主机必须等待至少测量时间再读,否则读到的是旧数据。HAL 函数最后一个参数 100 是超时毫秒数,I2C 总线被外部干扰拉死时,超时机制保证程序不会永久阻塞在等待里,这个参数在长时间无人值守的检测系统里很关键。

参数上要注意的不是 float 精度,而是延时策略:如果系统里有多个中断频繁打断 HAL_Delay,测量间隔会被拉长,这时不要靠延时对齐采样节奏,应该把「发起测量」和「读取结果」拆开,用定时器驱动状态机。这也是下一章要把工程骨架和定时器讲清楚的原因。

3. 用 Keil5 和 STM32CubeMX 搭好开发环境与工程骨架

工程骨架是整条链路里最无聊但最容易卡住人的部分,这里把 Keil5、芯片包、时钟树和最小 GPIO 工程一次说透。

3.1 芯片包安装与新建工程:从 CubeMX 生成到 Keil5 编译

目前初学和产品原型最顺的搭配是 STM32CubeMX 生成初始化代码,再用 Keil5 编译下载。两个工具的安装顺序有讲究:先装 Keil5,再装对应的 STM32 芯片支持包(Pack),最后装 CubeMX。芯片包没装的话,Keil5 新建工程时芯片列表是空的,打开别人给的工程会直接提示找不到 Device。

CubeMX 里新建工程的关键步骤只有四步:第一,在 Part Number 输入具体型号,比如 STM32F103C8T6,注意 C8 是 64KB Flash,CB 是 128KB,选错会导致编译产物超容量;第二,RCC 里把 HSE 配成外部晶振,再到 Clock Configuration 页面把系统时钟拉到 72MHz;第三,勾选需要的 I2C、USART、ADC,调试口保留 SWD;第四,Project Manager 里 Toolchain 选 MDK-ARM,Code Generator 勾选生成外设初始化函数,这样每个外设的初始化代码独立成模块,后续增删外设不牵连 main。

提示:打开工程提示 Device 不存在时,先回 Pack Installer 确认对应芯片包已经安装,再去翻代码没意义。

这一步容易翻车的地方是芯片包版本和 Keil5 版本不匹配。新版 Pack 有时要求较新的 Keil5,装了旧版会弹出版本过低提示。我的做法是 Keil5 用稳定版,Pack 用带 ST 官方库的版本,不要追最新,追新版往往只是 UI 变化,对你读环境传感器没有实际收益。

3.2 时钟树与晶振参数:为什么 72MHz 不是随便配的

STM32F103 最高主频 72MHz,但这不是默认值。上电默认是内部 8MHz HSI 经过 8 分频得到 1MHz,这个频率下串口波特率误差大、定时器计数不准,所以必须配置时钟树。CubeMX 的 HSE 外部晶振值要填成板子实际频率,常见开发板是 8MHz,也有 12MHz 或 25MHz 的,填错的话 PLL 倍频结果不对,表现就是串口乱码、延时快一倍或慢一倍,排查方向却经常被带偏到波特率设置上。

晶振起振还和负载电容有关。OSC_IN/OSC_OUT 两个引脚需要外接负载电容,典型值 10~22pF,具体按晶振手册的负载电容 CL 计算。程序卡在 HAL_RCC_ClockConfig 超时、HSE 始终起振失败时,我一般先检查两个对地电容是否虚焊,再看晶振引脚是否被覆铜或者排线干扰,最后怀疑晶振本身。

3.3 用 GPIO 翻转验证工具链:最小可运行工程

骨架搭好的第一件事不是接传感器,而是先验证 GPIO 输出。下面这段代码在主循环里翻转 LED,如果板载灯按预期闪烁,说明时钟、芯片包、下载链路三者正常,之后再接传感器时,出问题可以确定是外设配置,而不是工具链坏了。

// main.c 主循环,led 接 PC13(常见最小系统板板载灯) int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // CubeMX 生成:PC13 配置为推挽输出 while (1) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); HAL_Delay(500); // 阻塞 500ms,只用于验证,不能用于采样定时 } }

这里有两个值得注意的参数。HAL_Delay 的 500 表示毫秒,底层依赖 SysTick 中断,如果 SysTick 被更高优先级中断阻塞,延时就会失真,所以环境检测系统里不能用它做精确采样定时。GPIO 初始化时,CubeMX 默认引脚速度是低速,驱动 OLED 或走线较长的信号时最好改成高速,否则上升沿变缓,I2C 快速模式下面容易出通信错误。

4. 串口调试、OLED 显示与数据滤波处理

传感器能读出数之后,剩下的工作都在「让数据变得可读、可靠」上。串口、定时器、滤波这三件事按顺序做,系统就基本成型了。

4.1 用串口把传感器读数变成可读日志

传感器接好后的第一步调试永远是串口打印。CubeMX 里给 USART1 配置 115200-8-N-1,把 printf 重定向到串口,比逐个变量打断点高效得多。重定向的关键是重写 fputc,同时 Keil5 工程里要勾选 MicroLIB,否则半主机模式会让程序卡在 printf 上。

#include <stdio.h> // 重定向 printf 到 USART1,Keil5 需勾选 Use MicroLIB int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100); return ch; } // 主循环里直接打印温湿度 printf("T=%.2f C, RH=%.2f %%\r\n", sht30.temperature, sht30.humidity);

逻辑说明:HAL_UART_Transmit 四个参数分别是串口句柄、发送缓冲区、发送长度、超时时间,这里的 100 毫秒超时意味着串口异常时函数返回错误而不是无限等,配合 printf 使用时要留意,如果程序卡在打印,先看 MicroLIB 有没有勾选。串口调试助手收到乱码,优先检查波特率,其次回头查晶振频率配置,最后再怀疑 USB 转串口芯片驱动。

如果要走网络上报,常见做法是在 USART2 挂 ESP8266 模块,用 AT 指令把 JSON 数据转发到服务端。环境检测数据量很小,每秒一包足够,ESP8266 只做透传,业务逻辑全部留在 STM32 侧,这样网络异常也不影响本地采集和显示。

4.2 用定时器驱动采样节奏,别让 HAL_Delay 背锅

环境检测要求采样间隔稳定,比如每秒采一次、每分钟上报一次。HAL_Delay 受中断影响会漂移,正确做法是用定时器中断产生心跳:CubeMX 里把 TIM2 配成 1 秒中断,在中断回调里置一个采样标志,主循环检测到标志后才执行读取、打印、刷新 OLED。

TIM2 的溢出时间由三个参数决定:预分频器 Prescaler、自动重装值 Period、时钟源频率。以 72MHz 为例,Prescaler = 7199、Period = 9999 得到 1 秒中断:定时器时钟是 72MHz / 7200 = 10kHz,计数 10000 次正好 1 秒。计算时先定分频让计数频率是整数,再定重装值,避免小数造成时间漂移。

注意别在定时器中断回调里做耗时的 I2C 读取。I2C 传输一个字节要多个时钟周期,加上传感器响应时间,整次读取可能耗时几十毫秒,放在中断里会拖垮主循环。我的做法是中断里只置标志位,主循环检测到标志后清标志并执行完整采集流程。这样采样时刻的精度由定时器保证,采集耗时只影响处理结束时间,不影响下一次采样起点。

4.3 传感器噪声与偶发跳变:中值滤波和限幅滤波

温湿度读数偶尔跳到离谱值,不一定是硬件坏了,可能是总线干扰或电源波动。先做限幅滤波:记录上一次有效读数,偏差超过设定阈值就丢弃本次数据,保留上次值;再做中值滤波平抑随机噪声。下面是 5 点中值滤波的实现,直接放在读取函数之后:

// 滑动窗口 5 点中值滤波,输入为每次读到的物理量 float MedianFilter5(float input) { static float buf[5]; static uint8_t idx = 0; float tmp[5]; buf[idx] = input; idx = (idx + 1) % 5; for (uint8_t i = 0; i < 5; i++) tmp[i] = buf[i]; // 5 个元素排序取中间值,冒泡在数据量小时足够快 for (uint8_t i = 0; i < 4; i++) { for (uint8_t j = i + 1; j < 5; j++) { if (tmp[j] < tmp[i]) { float t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } return tmp[2]; }

参数说明:缓冲区长度 5 表示输出滞后最多 4 个采样周期,对每秒采样一次的环境参量完全可以接受;想更快响应温度突变,可以改成 3 点中值。滤波放在单位换算之后还是之前,我习惯放在物理量上,因为温度在 0℃ 附近时 ADC 原始值到摄氏度的映射不是均匀的,滤波放在物理层上更直观。阈值滤波的阈值按传感器精度设定,比如温度阈值设 2℃,超过就认为是干扰;如果系统放在冷库门口,开门的瞬间温变就是真实的,阈值设太小反而会滤掉真实变化,这一点要根据安装位置决定。

5. 标定与验证:让 STM32 环境检测系统的数据可信

5.1 两点标定法修正传感器偏差

传感器出厂有离散性,尤其是模拟量通道。把系统放在恒温环境中,用一台已校准的数字温湿度计作为参照,记录两组数据做两点标定。设采集原始值为 x,参照仪表读数为 y,用线性修正 y = kx + b。两组对应值代入后解出 k 和 b,在代码里直接替换换算公式,比如温度从-45.0f + 175.0f * raw / 65535.0f改成k * raw + b。需要特别说明的是,这个修正只对当前这块板子有效,换板子必须重新标定,因为 ADC 参考电压和传感器个体差异都会变,把标定系数写死在代码里时,记得加注释标明标定日期和参照仪表型号。

5.2 用秒表和参照仪表做三分钟验证

标定后做一次完整验证:把系统放到室温环境,旁边放参照仪表,每 10 秒手动记录一次串口输出,连续记录 3 分钟。重点看三个指标:稳态偏差是否落在标定精度内、是否有偶发跳变值、采样间隔是否稳定。检查采样间隔最直接的办法是在主循环里翻转一个空闲 GPIO,用逻辑分析仪量方波周期,周期等于设定采样间隔说明定时器配置正确。GPIO 翻转代码用HAL_GPIO_TogglePin放在采样处理完的位置,示波器上量两个上升沿之间的宽度即可。如果方波周期抖动超过 2ms,优先排查滤波函数是否耗时过长,或者串口打印在高波特率下是否阻塞了主流程,这两处是环境检测系统时钟抖动最常见的来源。量出来的方波周期稳定在目标值 ±1ms,采样节奏才算合格。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询