
简介面向嵌入式开发者和单片机学习者提供一套基于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℃否时序敏感两次读取间隔需大于 2sSHT30I2C±0.3℃ / ±2%RH否地址可配默认 0x44AHT20I2C±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/PB7I2C2 在 PB10/PB11如果这两组脚还要驱动 OLED 或按键就会打架ADC1 的多路通道共用一个转换器切换通道后的第一次转换结果要丢弃因为内部采样电容还没稳定。把温湿度挂在 I2C1、光照挂在 I2C2、气体挂在 ADC1 的 PA1三路外设互不干扰这是最省心的分配方式。另一个常见坑是烧录口占用。SWD 用的 PA13/PA14 默认被调试功能占用如果初始化代码里把这两个引脚复用成 GPIO 输出下次下载时调试器连不上只能按住复位再点下载非常难受。我一般把业务引脚全部避开 PA13/PA14CubeMX 里调试口保持 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 FlashCB 是 128KB选错会导致编译产物超容量第二RCC 里把 HSE 配成外部晶振再到 Clock Configuration 页面把系统时钟拉到 72MHz第三勾选需要的 I2C、USART、ADC调试口保留 SWD第四Project Manager 里 Toolchain 选 MDK-ARMCode 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 到 USART1Keil5 需勾选 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采样节奏才算合格。本文还有配套的精品资源点击获取