在 CSDN 上搜索 STM32 项目,最不缺的就是“环境监测系统”这类题目:温湿度、烟雾、LCD 显示,几乎没有哪个嵌入式学习者能绕开它。但真把代码下载下来再看,很多人的体验是“点亮一时爽,移植火葬场”:要么只给了源码没有原理图,引脚一对不上;要么原理图与代码根本不是同一套硬件,模块电压、上下拉、I2C 地址全要自己猜;要么工程基于老版本标准库,新装的 Keil 根本编不过。
今天要拆的这套编号 A166 的开源 stm32 家居环境监测系统,最大的特点不是功能多炫,而是把“源码 + 原理图”一起开源了。对于正在做课程设计、毕业设计,或者想完整走一遍“硬件设计→驱动编写→整机联调”流程的开发者,这类资料恰恰是最有价值的:它能让你在真实硬件上跑通一条完整链路,而不是停留在点灯和串口打印阶段。
这篇文章不打算把仓库里的每行代码抄一遍,而是从一个复刻者的角度,讲清楚这套系统“为什么这样设计、原理图应该怎么读、核心驱动怎么写、上电跑不通怎么排”。哪怕你手上暂时没有同一套硬件,也可以把它当成一份 STM32 环境监测系统的通用设计模板来用。
1. 开源 STM32 项目很多,为什么这套值得拆
先说一个很多人没想透的问题:开源嵌入式项目的复刻成本,往往比想象中高得多。
硬件项目与纯软件项目最大的区别在于,它有三个互相绑定的部分:原理图决定引脚和电气连接,源码决定外设和逻辑,PCB 决定实际能不能稳定工作。如果你拿到的开源资料只包含 MDK 工程和 .c 文件,却没有配套原理图,那么代码里GPIO_PIN_6连的是什么传感器、DHT11 的 Data 引脚接在哪个端口、蜂鸣器是高电平触发还是低电平触发,全部要靠猜。猜错了,现象就是代码编译通过、下载正常,但传感器没有任何反应。
A166 这类“源码 + 原理图”配套发布的项目,直接解决了复刻中最容易卡住的一环。你不需要再对着原理图盲猜引脚,也不需要根据某颗芯片的参考手册重新设计全套硬件。拿到资料后,可以在原理图上先做“引脚对照”,再去看源码里的宏定义和初始化代码,上下文是闭环的。
从学习价值来看,这种项目适合三类人:
- 课程设计 / 毕业设计学生:需要一套能实际演示功能、能讲清楚原理的系统,源码和原理图配套让答辩时更有底气。
- 刚学完 STM32 基础外设的开发者:LED、按键、定时器、ADC、I2C 你都单独学过,但不知道如何组合成一个真实系统,这个项目就是很好的整合样例。
- 准备做智能家居入门硬件的开发者:通过它理解温湿度采集、气体浓度采集、报警输出、显示刷新这套基础架构。
还需要提醒一句:开源资料的价值在于“可复刻”,不在于“直接抄”。真正能让你提升的,是跟着原理图重新理一遍电路连接,再对照源码理解每个外设为什么这么配。下一节,我们先从系统整体架构开始拆。
2. 系统功能与硬件选型思路
2.1 功能清单
一套典型的 STM32 家居环境监测系统,通常包含如下功能:
- 环境温湿度采集:用 DHT11 读取温度和湿度数据。
- 空气质量 / 可燃气体检测:用 MQ-2 烟雾传感器检测环境中烟雾或可燃气体浓度。
- 本地显示:通过 0.96 寸 OLED 或 LCD 屏实时显示当前温湿度和气体浓度。
- 越限报警:当温度、湿度或气体浓度超过阈值时,驱动蜂鸣器或 LED 报警。
- 按键交互(可选):切换显示页面,或调整报警阈值。
A166 开源项目的具体资料结构以你实际下载到的内容为准,但从这类系统的功能需求看,硬件上基本都需要 MCU、传感器、显示模块、报警模块四大部分。无论源码怎么组织,软件也都是围绕着“采集 → 处理 → 显示 → 报警”这条数据流展开。
2.2 数据流与软件任务划分
把系统拆开看,MCU 承担的工作并不复杂,它是整个系统的“调度中心”:
- 按照固定周期读取 DHT11 数据,校验后得到温度和湿度。
- 周期性启动 ADC 转换,读取 MQ-2 模块输出的模拟电压。
- 根据阈值判断是否需要报警,控制蜂鸣器。
- 将数据格式化后刷新到 OLED 屏幕。
这里比较关键的工程问题是“多任务节奏怎么安排”。DHT11 单总线通信要求两次读取之间至少间隔 1 到 2 秒,否则容易失败;而 ADC 和 OLED 刷新则希望尽可能及时。很多新手把三个模块无脑塞进同一个 while 循环,结果 DHT11 读取失败率很高。正确做法应该是给 DHT11 采集定义一个节拍,主循环至少延时几百毫秒,保证传感器有足够时间恢复。
2.3 核心器件选型说明
下表是一套低成本环境监测系统的常见器件组合。不同开源项目的具体型号会有差异,但设计思路基本一致。
| 模块 | 常见型号 | 在系统中的作用 | 关键设计点 |
|---|---|---|---|
| 主控 MCU | STM32F103C8T6 | 数据采集、处理、控制 | Cortex-M3 内核,72MHz,Flash 64KB,SRAM 20KB |
| 温湿度传感器 | DHT11 | 采集温度和湿度 | 单总线协议,需外部上拉,采样间隔不小于 1s |
| 气体传感器 | MQ-2 | 检测烟雾 / 可燃气体 | 模拟电压输出,一般接 ADC 通道 |
| 显示模块 | 0.96 寸 OLED(SSD1306) | 实时显示监测数据 | I2C 或 SPI 接口,I2C 地址常见 0x3C |
| 报警模块 | 有源蜂鸣器 | 阈值越限报警 | 低电平或高电平触发,注意驱动方式 |
| 电源 | USB 5V / AMS1117-3.3 | 系统供电 | 模拟和数字部分注意去耦 |
选 STM32F103C8T6 作为主控的原因非常实际:成本低、封装小、资料多、完全够用。很多初学者可能会想“要不要用 STM32F407 甚至更强的芯片”,但对环境监测这种低速采集、简单控制的场景,F103C8T6 的 ADC、定时器、I2C、GPIO 资源已经绰绰有余。选更强的芯片不会让系统变得更好,只会让成本、布线复杂度和学习门槛变高。
3. 原理图关键模块拆解:不要只当成一张图
很多人拿到开源工程后第一件事就是打开 Keil 编译,把原理图丢在一边。这是比较可惜的,因为源码只是系统的一半,另一半藏在原理图里。特别是当你需要改引脚、换传感器、重新画 PCB 时,读不懂原理图的代价非常大。
3.1 STM32 最小系统:先确认芯片能不能跑起来
无论是自己画板还是阅读开源原理图,第一优先检查的一定是 MCU 最小系统。它包含四个必备部分:
- 电源:STM32F103C8T6 的工作电压是 2.0 到 3.6V,典型为 3.3V。常见的做法是 USB 5V 输入,通过 AMS1117-3.3 稳压到 3.3V。在 MCU 的 VDD 引脚附近需要放置 100nF 去耦电容,靠近电源输入处还要有 10uF 或 100uF 的电解电容。
- 复位电路:NRST 引脚接 10k 电阻到 3.3V、100nF 电容到 GND,这是最常见的上电复位结构。按下复位按键时,NRST 被拉低,芯片复位。
- 启动模式:BOOT0 引脚通常通过 10k 电阻下拉到 GND,让芯片从主 Flash 启动。如果 BOOT0 悬空或拉高,可能出现“下载成功但程序不运行”的怪问题。
- 时钟电路:F103C8T6 可以只用内部 HSI 时钟运行,但很多设计为了精确串口波特率,会外接 8MHz 晶振和两个 20pF 负载电容。
从复刻角度说,如果板子完全无法烧录或无法运行,优先量电源、查 BOOT0、查复位,这三处出问题的概率远高于代码。
3.2 DHT11 接口:上拉电阻才是隐藏考点
DHT11 使用单总线协议,它的 Data 引脚在空闲时需要保持高电平,传感器通过拉低总线来发起响应。因此,MCU 的 GPIO 与 DHT11 Data 引脚之间通常要有一个4.7kΩ 到 10kΩ 的上拉电阻。
这个细节在学习时特别容易被忽略,因为大多数人买到的 DHT11 是模块,模块上自己带了上拉电阻和滤波电容。但如果你照着开源项目的原理图画板,用的是裸 DHT11 元件,漏了上拉电阻会导致通信不稳定、数据偶发读取失败。
从代码配合的角度看,DHT11 的单总线通信要求主机能够“双向”控制 GPIO:既要能输出低电平发送起始信号,又要能释放总线读取传感器响应。如果用 HAL 库,最简单可靠的方式是把引脚配置为开漏输出,利用外部上拉完成电平释放。这样在读取阶段,GPIO 不再需要动态切换输入输出模式,代码会简洁很多。这一点在原理图上体现为 DHT11_DATA 上方多了一个上拉电阻,并不是可有可无的“洁癖”设计。
3.3 MQ-2 传感器接口:为什么是 ADC 而不是 GPIO
MQ-2 这类气体传感器本质上是“气敏电阻 + 加热电路”的组合。当环境中可燃气体浓度变化时,传感器内部导电率发生变化,模块通过比较电路或分压电路,将浓度变化转换为模拟电压。所以 MCU 这端拿到的是一个连续变化的模拟量,必须用 ADC 采集,不能直接当 GPIO 高电平判断。
原理图上,MQ-2 模块的 AO 引脚应该连接到 STM32 的一个 ADC 输入通道,例如 PA0 对应 ADC1_IN0。同时要注意 MQ-2 模块通常有两种供电选择:5V 或 3.3V。加热部分用 5V 会更稳定,但如果模块输出的 AO 电压超过 3.3V,就不能直接接 STM32 的 ADC 引脚。很多开源板子会把 MQ-2 的 AO 通过电阻分压或直接由 3.3V 供电的模块输出,阅读原理图时要仔细看电源网络标注。
3.4 蜂鸣器驱动:GPIO 不能直接推高功率负载
有源蜂鸣器内部自带振荡源,只要通电就会发声,MCU 只需要控制通断,不需要输出 PWM 波形。但蜂鸣器的工作电流通常有二三十毫安甚至更高,STM32 的 GPIO 输出能力有限。如果直接把蜂鸣器正极接 GPIO、负极接 GND,轻则声音很小,重则可能损坏 IO。
成熟原理图通常采用 NPN 三极管或 N-MOS 管驱动蜂鸣器,GPIO 通过基极电阻控制三极管导通。阅读原理图时,你会看到一个类似“GPIO → 1k 电阻 → 三极管基极,蜂鸣器接 VCC 和集电极”的结构。判断蜂鸣器是高电平触发还是低电平触发,不要只看代码里的GPIO_PIN_SET,一定要结合三极管接法:如果是 PNP 管且发射极接 VCC,通常是低电平导通;如果是 NPN 管且发射极接 GND,通常是高电平导通。
3.5 模块供电与电平匹配
这套系统的传感器和显示模块,很多是 3.3V 或 5V 兼容设计。OLED 模块大多可以直接由 3.3V 供电;MQ-2 模块的 VCC 可能要求 5V;DHT11 模块有 3.3V 和 5V 两个版本。
原理图上最容易出的问题就是 5V 传感器输出直接灌入 3.3V 单片机引脚。阅读资料时,重点关注模块 VCC 网络标号、AO/DO 与 MCU 引脚之间的连接关系。如果某个引脚直接接 5V 电源域,最好确认是否经过了电平转换或分压。
4. 开发环境搭建与工程配置要点
4.1 工具链组成
复刻这套系统建议使用的软件工具链如下:
- 芯片型号:STM32F103C8T6
- 开发方式:HAL 库 + STM32CubeMX 或标准库
- IDE:Keil MDK,也可以是 STM32CubeIDE
- 编译器:Arm Compiler v5 / v6 均可
- 烧录调试器:ST-LINK V2 或 DAP-Link
如果电脑上还没有 Keil MDK,安装时要注意:Keil MDK 主要用于 ARM 芯片,不能与 Keil C51 共用一个安装目录。很多新手同时装了 C51 和 MDK,最后发现编译 STM32 工程时找不到器件,就是因为 Pack 没有安装完整。
STM32F1 系列的器件支持包是Keil.STM32F1xx_DFP,没有这个 Pack,新建工程时选择器件列表里不会出现 STM32F103C8。下载的 Pack 文件双击即可安装,也可以在 Keil 的 Pack Installer 中在线安装。
4.2 使用 STM32CubeMX 完成基础外设配置
无论开源项目原来用的是标准库还是 HAL 库,理解 CubeMX 中的配置方式都能帮助你快速分析工程。以本系统为例,CubeMX 中的典型初始化配置如下表:
| 外设/引脚 | 配置内容 | 对应功能 |
|---|---|---|
| RCC | HSE Crystal/Ceramic Resonator | 使用外部 8MHz 晶振 |
| Clock | SYSCLK = 72MHz | 配置系统主频 72MHz |
| DHT11 Data(如 PB6) | GPIO Output Open Drain, Pull-up, Speed Low | 单总线数据引脚 |
| MQ-2 AO(如 PA0) | ADC1 IN0, 采样时间尽量长 | 气体浓度模拟采样 |
| OLED SCL/SDA | I2C1 或 GPIO 模拟 I2C | OLED 通信 |
| 蜂鸣器引脚 | GPIO Output Push-Pull | 报警输出 |
| 按键引脚 | GPIO Input Pull-up 或 Pull-down | 阈值/页面切换 |
生成代码时,勾选 “Generate peripheral initialization as a pair of .c/.h files per peripheral”,便于阅读每个外设的初始化代码。需要说明的是,如果你下载的开源项目基于标准库,代码结构和 CubeMX 生成的 HAL 工程会不同,但外设配置思路完全一致,只需要对照芯片参考手册就能转换。
4.3 Keil 工程中的几个容易踩的配置点
打开 Keil 工程后,第一步不是急着编译,而是检查三个地方:
- 器件型号:Options for Target → Device 中,芯片必须选 STM32F103C8。
- Flash 下载算法:Utilities → Settings → Flash Download 中是否有 STM32F10x Med-density 128K 的编程算法。如果算法选成 High-density,下载到 C8T6 上也可能出问题。
- C/C++ 预定义宏:HAL 库工程通常需要定义
STM32F103xB。如果宏定义与器件不匹配,外设寄存器地址可能错误,编译虽然能过但运行异常。
5. 核心源码解读与实现
配套原理图理解源码,重点看四部分代码:DHT11 时序读取、MQ-2 ADC 采集、OLED 显示调用、主循环调度。下面给出可以直接在 HAL 库工程中使用的核心代码思路。
5.1 DHT11 温湿度读取:单总线时序是最大考点
DHT11 数据帧长度为 40 位:8 位湿度整数 + 8 位湿度小数 + 8 位温度整数 + 8 位温度小数 + 8 位校验和。校验和为前四个字节之和的低 8 位,只有校验通过,数据才可信。
单总线通信的基本流程是:
- 主机把总线拉低至少 18ms,然后释放,产生起始信号。
- DHT11 响应:先拉低 80us,再拉高 80us。
- 之后开始输出 40 位数据。每一位都从 50us 低电平开始,随后高电平持续时间约 26~28us 表示“0”,高电平约 70us 表示“1”。
HAL 库中,HAL_Delay()只能做到毫秒级,而 DHT11 时序需要微秒级延迟,因此工程中通常会实现一个简单的微秒延时函数。下面是一份基于空循环的简易实现,需要注意空循环次数与主频相关,实际项目中如果发现读取时序不稳定,应改用定时器或 DWT 实现精确延时。
// 文件路径:DHT11/dht11.c(示例,具体以工程实际文件为准) #include "dht11.h" #include "main.h" // DHT11 数据引脚由 CubeMX 生成,宏定义在 main.h 中 // 假设配置为:开漏输出 + 外部上拉,引脚为 PB6 // #define DHT11_Pin GPIO_PIN_6 // #define DHT11_GPIO_Port GPIOB // 简易微秒延时,空循环实现,需根据主频校准 static void DHT11_DelayUs(uint32_t us) { uint32_t i; for (i = 0; i < us * 72; i++) { __NOP(); } } // 从 DHT11 读取一个字节 static uint8_t DHT11_ReadByte(void) { uint8_t i, data = 0; for (i = 0; i < 8; i++) { // 每一位先有 50us 低电平 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); // 延时约 40us,再判断电平高低 DHT11_DelayUs(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { data = (data << 1) | 0x01; // 等待高电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); } else { data = (data << 1); } } return data; } // 读取一次温湿度数据,成功返回 0 uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] = {0}; uint8_t i; // 1. 主机起始信号:拉低至少 18ms HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); HAL_Delay(20); // 2. 释放总线,进入接收状态(开漏输出 + 外部上拉自动拉高) HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); DHT11_DelayUs(30); // 3. 等待 DHT11 响应低电平 if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) { // 等待 80us 低电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); // 等待 80us 高电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); // 4. 连续读取 5 字节 for (i = 0; i < 5; i++) { data[i] = DHT11_ReadByte(); } // 5. 校验和判断 if ((data[0] + data[1] + data[2] + data[3]) == data[4]) { *humidity = data[0]; *temperature = data[2]; return 0; } } return 1; }这段代码里有几个关键点。
第一,DHT11 引脚必须开漏输出并且外部有上拉。开漏输出模式下,WritePin(GPIO_PIN_SET)其实只是“释放总线”,真正把电平拉到高的是外部上拉电阻。这样在接收阶段,你不必反复切换 GPIO 模式,直接ReadPin就能读到 DHT11 拉低或释放总线后的电平。
第二,while等待电平结束没有加超时处理。这在很多教科书代码里很常见,但存在隐患:如果 DHT11 损坏或接线错误,总线一直停留在某个电平,程序会卡死在循环里。实际项目建议用超时计数改写,比如等待超过一定次数就返回失败。
第三,连续两次读取的间隔不能太短。DHT11 的采样周期典型值是 1 秒或 2 秒,如果主循环 100ms 读一次,大概率会出现校验失败。所以主循环定期执行 DHT11 读取前,至少要HAL_Delay(1000),或者设计一个简单的软件定时器状态机。
5.2 MQ-2 烟雾浓度采集与阈值报警
MQ-2 模块 AO 引脚输出的模拟电压,在 STM32 端被 ADC 转换为 0~4095 的数字量。F103 的 ADC 是 12 位,参考电压通常为 3.3V,所以理论上 0 对应 0V,4095 对应 3.3V。实际判断报警时,不需要精确计算出气体浓度,只需要统计一个合适的阈值并做滤波,避免瞬时尖峰误报警。
下面是一个简洁的 MQ-2 采集函数:
// 文件路径:MQ2/mq2.c(示例,具体以工程实际文件为准) #include "mq2.h" #include "main.h" extern ADC_HandleTypeDef hadc1; // 获取 ADC 原始值,0~4095 uint16_t MQ2_GetAdcRaw(void) { uint16_t adc_value = 0; HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 50) == HAL_OK) { adc_value = HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); return adc_value; } // 判断是否超过报警阈值 uint8_t MQ2_CheckAlarm(uint16_t threshold) { uint16_t raw = MQ2_GetAdcRaw(); // 简单阈值得判断,也可以在这里增加多次采样取平均 if (raw > threshold) { return 1; } return 0; }在原理图设计时,MQ-2 的 AO 应接到 F103 的一个 ADC 输入通道。例如 PA0 对应 ADC1_IN0,CubeMX 配置时把 PA0 模式选为ADC1_IN0,采样时间选择较长档位(如 239.5 周期),以减小传感器输出阻抗对采样精度的影响。
实际使用时建议做两个优化。一是连续采样多次取平均,比如采样 5 次去掉最大最小再平均,能减少随机波动。二是报警阈值不要取临界值,给系统留出回差,也就是“超过多少开始报警”和“低于多少停止报警”使用两个不同阈值,避免在阈值附近反复触发蜂鸣器。
5.3 OLED 显示与信息刷新策略
0.96 寸 OLED 常用驱动芯片是 SSD1306,分辨率为 128x64。如果模块是 I2C 接口,那么通常只需要用到四个引脚:VCC、GND、SCL、SDA。I2C 地址一般为 0x3C 或 0x3D,如果屏幕没反应,可以先怀疑地址不对。
显示模块在工程里通常被封装为OLED_Init()、OLED_Clear()、OLED_ShowString()等接口。完整的 OLED 驱动代码动辄几百行,在开源工程里一般单独存放为oled.c和oled.h,复刻时不需要自己从头写,只需要调用它的基础接口,并确认它操作的是哪个 I2C 外设。
为了让主循环显示逻辑不至于太长,可以把每个页面的刷新封装成独立函数。例如:
void Display_Update(uint8_t temp, uint8_t humi, uint16_t smoke_raw) { char buf[32]; OLED_Clear(); sprintf(buf, "Temp: %d C", temp); OLED_ShowString(0, 0, (uint8_t *)buf); sprintf(buf, "Humi: %d %%", humi); OLED_ShowString(0, 2, (uint8_t *)buf); sprintf(buf, "Smoke: %d", smoke_raw); OLED_ShowString(0, 4, (uint8_t *)buf); }使用sprintf时,Keil 工程建议勾选微库(MicroLIB),否则浮点格式化输出可能占用大量 Flash 空间,甚至导致链接失败。如果不需要小数显示,也可以用整数拼接方式省去sprintf开销。
刷新 OLED 不必过于频繁。OLED 动态刷新率太高,不仅占用 CPU,长期运行还可能加速屏幕老化。对这类环境监测系统,1 秒刷新一次完全足够。读取 DHT11 的节奏也是 1 秒一次,两者正好可以同步。
5.4 主循环如何把各模块串起来
项目的main.c主循环逻辑并不复杂。核心就是:周期性读传感器 → 刷新显示 → 判断报警。一个典型的循环如下:
// 文件路径:Core/Src/main.c 主循环部分(示例) int main(void) { uint8_t temp = 0, humi = 0; uint16_t smoke_raw = 0; uint8_t dht11_ok = 0; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_I2C1_Init(); OLED_Init(); OLED_Clear(); while (1) { // 读取 DHT11,失败则保留上次数据 dht11_ok = DHT11_ReadData(&humi, &temp); if (dht11_ok != 0) { // 读取失败,可以根据实际工程决定是否提示 } // 读取 MQ-2 ADC 原始值 smoke_raw = MQ2_GetAdcRaw(); // 刷新 OLED 显示 Display_Update(temp, humi, smoke_raw); // 报警判断 if (MQ2_CheckAlarm(SMOKE_THRESHOLD)) { HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET); } HAL_Delay(1000); } }这个循环最需要注意的,就是主循环末尾的HAL_Delay(1000)。它既起到限制刷新率的作用,也保证 DHT11 两次读取之间留有足够时间。如果屏幕显示和传感器读取都正常,但 DHT11 经常校验失败,先检查是不是这个延时被改小了。
另外,报警判断可以再加一个“软件消抖”。例如连续三次检测到超过阈值才真正报警,否则不响应。这样可以避免偶尔的 ADC 波动导致蜂鸣器突然响一声又停。实现方式也很简单,就是记录连续超阈值次数,在中断或主循环中递增判断。
6. 编译烧录与上电验证
6.1 编译时常见的两个提示
打开工程直接编译,如果报错大量undefined symbol,第一反应应该是“某个源文件没有被加入工程”,而不是代码本身有问题。DHT11、OLED、MQ2 这些模块对应的.c文件可能分散在多个目录中,需要手动在 Keil 的 Project 窗口中添加到对应分组。另外还要确认头文件包含路径已经加到 Options → C/C++ → Include Paths 中。
如果编译通过但下载时报No Target Connected,基本上不是工程问题,而是调试器连接问题。先检查 ST-LINK 是否被电脑识别,再确认 SWDIO、SWCLK、GND 三根线是否接对。很多 STM32 最小系统板还需要给目标板单独供电,不能只靠 ST-LINK 的 3.3V 输出带动整个系统。
6.2 使用 STM32CubeProgrammer 命令行烧录
除了 Keil 自带的下载按钮,ST-LINK 也可以通过 STM32CubeProgrammer 的命令行工具烧录。烧录前需要先把源码编译生成.hex或.bin文件,并在 Keil 的 Output 选项卡勾选 “Create HEX File”。命令行烧录示例:
STM32_Programmer_CLI -c port=SWD mode=UR -w ./build/HomeMonitor.hex -v参数说明:-c port=SWD mode=UR表示通过 SWD 接口连接并设置热复位模式,-w表示写入文件,-v表示烧录后校验。如果你的调试器连接正常,命令执行后会显示下载进度和校验结果。
6.3 上电后的功能自检清单
系统上电后,建议按下面的顺序检查,而不是只看 OLED 有没有显示:
| 检查项 | 正确现象 | 失败时排查方向 |
|---|---|---|
| 电源指示 | 板载电源 LED 点亮,MCU 不过热 | 测量 3.3V 是否正常,检查 LDO 引脚 |
| OLED 初始化 | 屏幕亮起,显示字符清晰 | 检查 I2C 地址、SCL/SDA 接线、模块供电 |
| DHT11 数据 | 温度湿度接近环境实际值 | 检查上拉电阻、接线、读取间隔 |
| MQ-2 数据 | 数值随气体浓度变化 | 检查 ADC 引脚配置、传感器预热 |
| 蜂鸣器报警 | 超过阈值后响,低于阈值后停 | 检查三极管驱动方式、GPIO 电平极性 |
这里特别提醒一个容易被误判的现象:MQ-2 传感器刚上电时读数很高,过几分钟才慢慢下降。这是它的正常预热过程,不是故障。如果在没有明显烟雾的情况下 ADC 值持续偏高,不要急着调代码,先让传感器预热 3 到 5 分钟再判断阈值。
7. 常见问题与排查思路
从大量复刻 STM32 项目的经验来看,下面几个问题出现频率最高,这里整理成一张排查表:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 编译报错找不到头文件 | 源文件未加入工程或头文件路径缺失 | 查看错误提示中缺失的文件名 | 将对应.c文件加入分组,补全 Include Paths |
| 下载成功但程序不运行 | BOOT0 拉高或芯片型号选错 | 量 BOOT0 电压,检查 Options 中 Device | BOOT0 接 GND,重新选择 STM32F103C8 |
| OLED 黑屏无显示 | I2C 地址不对或 SCL/SDA 接反 | 用 I2C 扫描程序确认地址 | 将地址改为 0x3C 或 0x3D,交换 SCL/SDA |
| DHT11 偶发读取失败 | 读取间隔太快或上拉电阻缺失 | 在读取函数前后增加延时,检查原理图 | 主循环延时 1s 以上,补 4.7k 上拉 |
| MQ-2 数值始终接近满量程 | 传感器未预热、模块供电不足或 AO 直连 5V | 测量模块 VCC,等待预热,查看原理图网络 | 确保供电稳定,必要时增加分压或使用 3.3V 供电模块 |
| 蜂鸣器不响或声音很小 | 驱动管类型判断错误、极性接反 | 对照原理图确认高/低电平触发 | 调整代码中 GPIO 置位电平或检查三极管型号 |
| 程序运行卡死 | while等电平缺少超时 | 在线仿真查看卡死位置 | 为 DHT11 等时序等待增加超时保护 |
这些问题的共性特征是“硬件和软件没有对齐”。所以排查的第一原则永远是:先按原理图确认引脚和电平,再怀疑代码。很多同学一看到屏幕不亮就以为 OLED 驱动代码写错了,实际上很可能是地址不对或引脚接反。
8. 二次开发方向与工程建议
8.1 加入 ESP8266 / 4G 模块,实现数据上云
这套本地环境监测系统跑通后,多数人最想做的扩展是把数据传到手机或云平台。低成本做法是加一个 ESP8266 模块,通过串口与 STM32 通信。STM32 把温度和 ADC 值打包成 JSON 字符串,ESP8266 通过 AT 指令连接路由器,再通过 MQTT 协议把数据上报到云平台。
从工程上看,这一步的难度不在于 ESP8266 本身,而在于串口数据帧的设计。不要简单地把所有数据一次性发给 ESP8266,最好定义一个固定格式,例如AT+MQTTPUB=<topic>,<data>或自定义帧头帧尾。还要考虑 Wi-Fi 断开后的重连策略,否则设备运行几天后会出现“掉线但不再连接”的尴尬局面。
8.2 从“能演示”到“能长期运行”
课程设计阶段的系统,能开机演示 5 分钟就算成功。但如果想让这套环境监测系统真正在家里长期运行,还需要考虑几个工程问题:
第一,看门狗。STM32 虽然稳定,但程序如果因为外部干扰跑飞,看门狗可以在几百毫秒内复位系统。标准做法是初始化独立看门狗 IWDG,主循环定期“喂狗”。这是代码不需要改架构就能大幅提升可靠性的功能。
第二,低功耗。F103 全速运行时的功耗并不低,如果后续想用电池或太阳能供电,就要考虑睡眠模式。可以把系统改成“定时唤醒采集 + 完成后进入 Stop 模式”,用 RTC 或低功耗定时器做周期唤醒。
第三,ADC 滤波。传感器原始数据直接用于显示时,波动往往很明显。除了多次采样取平均,还可以用一阶低通滤波公式filtered = filter_value + (new_value - filter_value) / coefficient,让数值变化更平滑。这个系数在代码中通常取 4 到 8,值越大滤波越强,响应越慢。
8.3 资料许可与二次发布注意事项
开源硬件项目的常见许可包括 MIT、GPL、CC BY-SA 等。使用 A166 这类开源项目的源码和原理图时,建议先看仓库中是否包含 LICENSE 文件。如果用于课程设计或学习,绝大多数许可都允许自由使用;但如果想把修改后的版本发布到自己的仓库、写成商品售卖,就需要确认是否符合原作者的开源许可要求,例如保留版权声明或采用相同许可开源。
从保护自己的角度,二次开发时最好在代码注释和 README 中明确标注“基于原项目 A166 修改”,并列出修改点。这不只是法律层面的合规问题,也是对原作者的尊重。
8.4 学习路线建议:不要停留在“能跑”
如果你正在用这套项目完成课程设计,建议按下面的顺序深入学习,而不是停留在“代码能跑、屏幕能显示”这一步:
- 打开原理图,逐个模块查看它和 STM32 引脚的连接关系,然后在代码里找到对应的宏定义或初始化函数。
- 用调试器在线仿真,给 DHT11 读取函数打断点,观察单总线时序中 GPIO 的电平变化。
- 尝试修改报警阈值,并观察 MQ-2 输出值如何变化。
- 自己重新画一版原理图并打样,至少完成一次从零到一的复刻。
- 在现有基础增加一个传感器或通信模块,完成一次有自己思考的改动并把过程记录下来。
完成第 5 步后,你已经不是在“复刻别人的项目”,而是在构建自己的作品。很多嵌入式岗位的面试官更看重这个改进过程,而不是“我下载了一个开源工程并烧录成功”。
9. 一些值得单独提醒的细节
前面几节按流程拆完了这套系统,最后想单独提醒几个容易被忽略的细节。这些细节不会让程序编译报错,但会直接影响你复刻时的体验。
第一,是 Keil 工程中未使用函数的裁剪问题。如果你在工程里定义了报警函数却没有调用,编译器仍然可能把它编译进最终镜像,消耗 Flash。在 C/C++ 选项卡中勾选One ELF Section per Function,再在链接选项中使用--remove或--gc-sections参数,可以减少无用代码,特别适合帮那些 Flash 空间紧张的项目腾地方。
第二,是 STM32F103C8T6 的 Flash 容量限制。它的内部 Flash 是 64KB,注意不要被某些下载算法命名为“128K”迷惑。实际上,F103C8T6 的 Flash 可以按 128KB 的 Medium-density 算法下载,但真实容量只有 64KB。如果工程编译后超过 64KB,就不适合继续在这个芯片上堆功能了,应该考虑更换芯片或优化代码,而不是强制下载。
第三,是复位后 OLED 的初始化时序。STM32 主频和外设初始化完成后,OLED 模块可能还没完成内部上电复位。如果 OLED 初始化后屏幕出现异常,可以在OLED_Init()前加入 100ms 左右的延时,让模块稳定。这类“玄学”问题,很多其实是上电时序导致的。
第四,是测试报警功能时的安全意识。如果系统用 MQ-2 检测可燃气体,测试时不要使用明火或高浓度可燃气体靠近传感器,以免触发真实事故。对于课设、毕设场景,完全可以用打火机气体在远距离、通风环境下做短时测试,或者用手按住传感器改变环境浓度,安全永远是第一位。
这套 A166 开源项目能不能真正发挥作用,关键不在于你下载了多少文件,而在于你是否愿意把原理图打开、把源码逐行读懂,并且亲手修改其中一个功能。如果你只是想要一份能交差的代码,它最多帮你完成作业;如果你把它当成一份“完整的产品设计案例”来学习,那它带给你的就是一套从原理图到源码、从驱动到整机的嵌入式工程思维。