简介这是一份以STM32F103为核心的空气检测仪课程设计资料面向嵌入式、单片机方向的初学者或课程设计学生完整实现温湿度与有害气体浓度检测、报警以及OLED显示功能。资料围绕DHT11温湿度传感器、SGP30气体传感器、蜂鸣器模块和OLED屏幕展开涉及I2C、ADC、定时器等STM32外设驱动适合用于单片机综合实训、毕业设计参考或学习SGP30气体检测应用。压缩包为zip格式总大小7.32MB共205个文件。主要包含大量C语言源码与头文件.c/.h、Keil工程编译产物.o/.crf/.axf/.hex、链接脚本与调试配置.uvprojx/.uvoptx/.scvd以及说明文档.txt/.htm可帮助了解工程完整构建过程便于直接打开工程重新编译或烧录验证。目前已有396人浏览学习适合需要快速上手STM32气体检测项目的读者。通过这份资料能够拿到完整的代码工程、外设驱动和电路模块组织方式便于复现实验现象或在此基础上扩展PM2.5、甲醛等更多环境监测功能。1. 这块板子到底能做出什么先搞清需求再谈代码做嵌入式的人都有个共同经验单模块的驱动全部能跑通一合在一起就各种黑匣子问题。STM32F103作为主控外接DHT11温湿度传感器、SGP30气体传感器、有源/无源蜂鸣器、OLED显示屏这套组合在智能家居环境监测、实验室空气品质预警、农业大棚环境采集这些场景里非常常见。它要解决的实际问题是把温度、湿度、二氧化碳当量eCO2、总挥发性有机物TVOC这几个关键环境参数用低成本传感器采集回来在本地 OLED 上实时显示并在数值超标时驱动蜂鸣器报警。适合的人群也很明确正在做嵌入式课程设计、毕业设计的学生以及刚入门单片机想要挑战「多模块整合」的开发者——注意难点从来不是单个传感器怎么读而是四个模块共用一个主控时的时序编排、总线冲突、电源分配。这篇文章不准备给你抄一份 800 行的完整固件而是把选型逻辑、模块驱动的边界参数、整合时的坑一次讲清楚。跟着走一遍之后你不仅能复现这套环境监测样机还能举一反三把它改造成带屏幕的空气质量检测仪或者是智能家居中控的雏形。2. 硬件选型与系统架构为什么是 STM32F103而不是 51 或 ESP82662.1 主控的选型理由外设数量与资料密度是硬指标STM32F103 这个系列被用滥了但被用滥恰恰说明它是可靠的。DHT11用的是单总线协议需要 MCU 提供一个能精确控制微秒级延时的 GPIOSGP30和大多数OLED屏幕走的是 I2C 总线需要 MCU 提供硬件 I2C 外设或者用软件模拟蜂鸣器则只需要一个普通的 GPIO 或者定时器的 PWM 输出。这几项要求STM32F103 的基础型号如 F103C8T6 都能满足它有 2 个 I2C、3 个 USART、多个定时器、37 个 GPIOLQFP48 封装工作电压 2.0V~3.6V主频 72MHz。相比 51 单片机F103 的硬件 I2C 至少能让你少踩一半的时序坑相比 ESP8266F103 虽然没有 WiFi但它的实时性更可控——环境监测这个场景压根不需要连网数据采集、阈值判断、本地显示全都在单片机上完成用 WiFi 反而是多余的复杂度。选择它的另一个理由是 Debug 成本低SWD 接口 便宜的基础版调试器就能在线打断点这对排查 I2C 通信和中断优先级的问题至关重要。如果你预算充足选 F103RCT6 或 F103ZET6 也没问题引脚更多但 C8T6 的 64KB Flash 对这套系统完全够用。2.2 各模块硬件特性速览接口、电压、功耗一张表看明白把四个模块的关键电气参数列成一张表方便你拉通看接口匹配情况模块通信接口工作电压关键说明DHT11单总线1-Wire3.3V~5V采样周期 ≥1s数据位 40bit湿度精度 ±5%RH温度精度 ±2℃SGP30I2C地址 0x581.62V~1.98V模块一般带稳压输出 eCO2 和 TVOC上电后需要约 15 秒预热期蜂鸣器GPIO 或 PWM取决于有源/无源有源直接高电平响无源需要给定频率的方波OLED 0.96 寸I2C 或 SPI3.3V~5V常见 I2C 地址 0x3CSSD1306 驱动芯片注意这里的电压问题SGP30 传感器芯片本身的供电电压是 1.62V~1.98V但你买到的 SGP30 模块几乎都集成了电平转换和稳压电路所以用 3.3V 供电没问题。有些模块板载 LDO允许 5V 供电读取时逻辑电平也是 3.3V。DHT11 就比较特殊它是开漏输出需要外部上拉电阻工作电压虽可以到 5V但接在 3.3V 的 STM32F103 上时最好也把供电统一到 3.3V否则返回的高电平是 5V长期下来可能损坏 GPIO。2.3 整体架构一主多从的总线分配策略我的习惯做法是给 I2C1 总线挂 OLED 和 SGP30两条设备共用同一组 SCL/SDA 引脚这样能省下 GPIODHT11 单独占一个 GPIO蜂鸣器占一个 GPIO。判断哪两个设备可以共享总线时要确认它们的 I2C 地址不冲突——SGP30 地址是 0x58SSD1306 默认是 0x3C7 位地址不冲突可以放心挂同一条总线。大多数 STM32F103 开发板板载的 OLED 和传感器排座已经帮你把上拉电阻接好了但如果你是自己飞线搭的板子必须在 SCL 和 SDA 上各加一个 4.7kΩ 上拉电阻到 3.3V这是 I2C 总线能稳定工作的前提。// 引脚分配示例以 STM32F103C8T6 为例 // PB6 - I2C1_SCLPB7 - I2C1_SDA接 OLED 与 SGP30 // PA1 - GPIO 输入接 DHT11 DATA 引脚需外部上拉 4.7kΩ // PA2 - GPIO 推挽输出接蜂鸣器控制端 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // DHT11 数据线配置为开漏输入配上拉 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 蜂鸣器推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);上面这段配置的逻辑是DHT11 的数据线要既能输出低电平触发信号又能释放总线读取传感器返回的波形所以用开漏输出加外部上拉单片机 GPIO 内部的上拉电阻约 30~50kΩ阻抗偏高外部再并一个 4.7kΩ 的上拉能把信号边沿拉得更陡否则 DHT11 返回的微弱上拉信号可能导致读到的时序不准确。蜂鸣器引脚用推挽输出因为它只是单向驱动。实际项目中我一般会把蜂鸣器的驱动电流也考虑进去——即便是有源蜂鸣器直接推 GPIO 也能响但声音发闷加一个 NPN 三极管放大驱动更稳妥这个细节放到避坑章节再说。3. 各模块驱动拆解从最小代码到可复用驱动函数3.1 DHT11 单总线时序一切意外的根源是延时精度DHT11 的通信协议是全双工单总线主机先发一个低电平脉冲启动信号然后释放总线传感器会拉低总线应答随后连续输出 40 位数据。每一位数据的区分是靠高电平的持续时间26~28μs 的高电平代表「0」70μs 左右的高电平代表「1」。这就意味着主控制 MCU 读取每一位时需要在高电平开始后等待大约 40μs 再去采样——早了会读到 0 的尾巴晚了会错过 1 的电平窗口。// DHT11 读取一字节数据的核心函数 uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (uint8_t i 0; i 8; i) { // 等待高电平开始 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) RESET); // 延时约 40us避开0电平窗口 delay_us(40); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET) { data | (0x80 i); // 高电平持续超过 40us判定为 1 } // 等待高电平结束确保下一位读取起始状态正确 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET); } return data; }这段代码的逻辑与大多数网上的参考实现一致先等总线变高再延时 40μs延时结束后的电平状态就决定了这一位是 0 还是 1。最关键的参数是delay_us(40)这个延时函数不能用普通的for循环空转实现因为编译器的优化等级不同空循环的实际微秒数会差出好几倍——正确做法是用定时器做微秒延时或者用 DWT 内核计数器实现精确延时。实测中遇到的各种 DHT11 数据异常十有八九都出在这个 40μs 窗口上。若你的延时函数偏长会把本该是 1 的信号误读为 0温湿度数值看起来就明显偏低、而且每次读的结果还不太一致。3.2 有源蜂鸣器与无源蜂鸣器驱动方式完全是两回事蜂鸣器按内部结构分为有源和无源两种这里的「源」指振荡源。有源蜂鸣器内部自带振荡电路通电即发出固定频率的声音单片机只需要给高电平或者低电平就能驱动无源蜂鸣器内部没有振荡电路必须由单片机提供一定频率的方波例如 2.7kHz 的方波。选型时如果没注意标签买回来的是无源蜂鸣器你会遇到「代码全对但不响」的尴尬。// 有源蜂鸣器高电平触发 void Buzzer_ON_Active(void) { GPIO_SetBits(BUZZER_GPIO_PORT, BUZZER_GPIO_PIN); } // 无源蜂鸣器PWM 输出 2700Hz 方波 void Buzzer_Init_TIM2(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_OCInitTypeDef TIM_OC_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStruct.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz 计数频率 TIM_InitStruct.TIM_Period 370 - 1; // 1MHz / 370 ≈ 2700Hz TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStruct); TIM_OC_InitStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OC_InitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OC_InitStruct.TIM_Pulse 185; // 占空比 50% TIM_OC1Init(TIM2, TIM_OC_InitStruct); TIM_Cmd(TIM2, ENABLE); }这两份驱动对应两种硬件如果你手里是有源蜂鸣器直接操作 GPIO 即可如果是无源蜂鸣器需要占用一个定时器的 PWM 通道。参数方面TIM_Prescaler和TIM_Period共同决定了输出频率公式是频率 72MHz / (分频值 1) / (周期值 1)。上例中 72 分频后计时器以 1MHz 计数周期设为 370得到的频率约 2702Hz这是无源蜂鸣器发声比较响亮的频段之一。实际调试时你可以把 370 改成 200~500 之间的值感受音调变化选一个耳朵听着最舒服的。3.3 SGP30 的 I2C 读取流程比你想的多一步SGP30 是一个非常「聪明」的传感器模块它内部有算法可以直接输出 eCO2等效二氧化碳浓度单位 ppm和 TVOC总挥发性有机化合物单位 ppb不需要你去算气体浓度拟合。但代价是它有状态——上电后传感器内部算法需要约 15 秒预热预热期间的 eCO2/TVOC 数据是无效的在持续工作的过程中它还会周期性进行基线校准期间数据也可能波动。// SGP30 读取 eCO2 和 TVOC 的 I2C 命令序列 uint8_t sgp30_cmd_measure[2] {0x20, 0x08}; // 测量命令初始化空气质量数据 void SGP30_Read_Data(uint16_t *eco2, uint16_t *tvoc) { uint8_t buf[6] {0}; // 第一步发送测量命令 I2C_WriteBytes(SGP30_I2C_ADDR, sgp30_cmd_measure, 2); delay_ms(50); // 命令处理时间数据手册要求 12ms 以上 // 第二步读取 6 字节数据 I2C_ReadBytes(SGP30_I2C_ADDR, buf, 6); *eco2 (buf[0] 8) | buf[1]; *tvoc (buf[3] 8) | buf[4]; }SGP30 的工作流程并不复杂但有两个值得强调的参数第一I2C_WriteBytes发送指令后必须等待至少 12ms 才能发起读操作我习惯留出 50ms 余量。第二SGP30 支持的最大 I2C 速率是 400kHz如果你用软件模拟 I2C循环里的延时会让实际速率低于 100kHz这没关系慢一点反而稳定如果你用硬件 I2C要把I2C_InitStructure.I2C_ClockSpeed设置成 100kHz 或 400kHz不建议超过 400kHz。很多参考资料会省略一个细节SGP30 内部有基线补偿算法当传感器长时间不上电、环境发生剧烈变化时刚上电的读数可能会飙升到一个离谱的数值然后在几分钟内慢慢回落。这不是传感器坏了而是算法正在重新收敛。处理办法有两种——简单粗暴的做法是上电后 2 分钟内不把 SGP30 的数值用于报警判断讲究一点的做法是给模块加一个手动基线保存/恢复的 I2C 命令把校准好的基线存入 MCU 的 Flash 中下次上电直接写入传感器。后面进阶章节会给出这个命令的实现思路。3.4 OLED 显示驱动SSD1306 的初始化序列与取模方式0.96 寸 OLED 屏几乎全部采用 SSD1306 控制芯片I2C 模式时只需 4 根线VCC、GND、SCL、SDA。初始化 SSD1306 需要发送一串配置命令把显示时钟分频、电荷泵、寻址模式这些寄存器配置好。这些配置代码在各类资料中高度雷同但有一个细节常常被忽略——如果你只发送了初始化命令而没有开启电荷泵屏幕会一直保持黑屏无论你怎么写显示数据都没反应。// SSD1306 初始化关键命令序列I2C 模式128x64 uint8_t init_cmds[] { 0xAE, // 关闭显示 0xD5, 0x80, // 设置显示时钟分频 0xA8, 0x3F, // 设置多路复用率 64 0xD3, 0x00, // 设置显示偏移 0 0x40, // 设置显示起始行 0 0x8D, 0x14, // 开启电荷泵0x14 为关键值 0xA1, // 段重映射 0xC8, // COM 扫描方向 0xDA, 0x12, // COM 引脚配置 0x81, 0xCF, // 对比度设置 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH 失调节点 0xA4, // 全部显示开启 0xA6, // 正常显示非反色 0xAF // 打开显示 };初始化完成后SSD1306 的显示缓存是 1KB每 8 行像素构成一页共 8 页。往屏幕写数据时先设置页地址和列地址然后连续写 128 字节。大多数库会提供一个OLED_ShowString()函数内部通过取模数组把 ASCII 字符转成点阵数据。如果你要显示中文需要用取模软件生成 16x16 点阵数据这一步涉及的位序问题先取高位还是低位会影响显示效果调试时字是花的不是参数问题而是取模顺序与显示函数不匹配。我一般会把 SGP30 的数值显示在 OLED 的第一二行温湿度放在三四行蜂鸣器报警状态放在第五行这样人机界面层级清楚调试时一眼能看出问题出在哪个模块。4. 多模块整合主循环的任务划分与报警调度4.1 分时轮流采样为什么不能全放在一个 while(1) 里四个模块整合时新手最容易犯的错是把所有采集逻辑按顺序堆在一个主循环里结果 DHT11 的 40μs 位读取延时把 SGP30 的 I2C 时序拖垮或者 OLED 显示刷新过慢导致看起来像死机。环境监测系统天然有各自不同的采样周期——DHT11 官方数据手册要求两次读取间隔不小于 1 秒SGP30 以约 1Hz 的频率输出数据OLED 显示内容的刷新频率没必要超过 5Hz蜂鸣器报警则是事件驱动。所以主循环的正确结构应该是「轮询 时间片」用一个定时器产生 1ms 的时基每次进入主循环时用时间戳判断哪个模块到了采样时刻。// 主循环的时间片调度示例 uint32_t tick HAL_GetTick(); uint32_t last_dht_time 0, last_sgp30_time 0, last_oled_time 0; while (1) { tick HAL_GetTick(); // DHT11 每 1500ms 采样一次 if (tick - last_dht_time 1500) { last_dht_time tick; DHT11_Read(temp, humi); Alarm_Check_TempHumidity(temp, humi); // 温湿度越界判断 } // SGP30 每 1000ms 采样一次 if (tick - last_sgp30_time 1000) { last_sgp30_time tick; SGP30_Read_Data(eco2, tvoc); Alarm_Check_AirQuality(eco2, tvoc); // 空气质量越界判断 } // OLED 每 500ms 刷新一次 if (tick - last_oled_time 500) { last_oled_time tick; OLED_ShowEnvInfo(temp, humi, eco2, tvoc); } // 蜂鸣器报警由标志位驱动不阻塞主循环 Buzzer_Update(); }上面这个调度逻辑的要点在于每个模块的采样频率独立控制前一次采集是否完成不需要阻塞后一次采集。DHT11 采样时主控会被阻塞十几毫秒这期间 SGP30 的 I2C 不会发起新请求自然也就不会冲突。Buzzer_Update()里只做一件事——根据全局报警标志决定蜂鸣器引脚输出如果有报警持续时间超过 3 秒自动停止响铃但保持报警状态在 OLED 上闪烁这样可以避免蜂鸣器一直刺耳地叫。4.2 阈值报警算法温度、eCO2、TVOC 的三级判据报警逻辑的合理性决定了这个系统能不能真正用起来。温度超过 38℃ 报警湿度高于 70%RH 报警eCO2 超过 1000ppm 报警TVOC 超过 500ppb 报警——这些阈值只是初始参考值实际项目中你需要根据环境调整。更重要的是加一个「连续 N 次越界才触发」的去抖机制因为环境监测的传感器读数会有小范围抖动比如人在传感器旁边咳嗽一声TVOC 会瞬间飙升然后又回落如果立即报警会导致误报频发。// 越界连续计数去抖报警 #define ALARM_THRESHOLD_TEMP 38.0f // 温度阈值 ℃ #define ALARM_THRESHOLD_ECO2 1000 // eCO2 阈值 ppm #define ALARM_CONSECUTIVE_COUNT 3 // 连续越界次数 uint8_t temp_over_count 0; uint8_t eco2_over_count 0; uint8_t alarm_flag_temp 0, alarm_flag_eco2 0; void Alarm_Check_TempHumidity(float temp, float humi) { if (temp ALARM_THRESHOLD_TEMP) { temp_over_count; if (temp_over_count ALARM_CONSECUTIVE_COUNT) { alarm_flag_temp 1; } } else { temp_over_count 0; alarm_flag_temp 0; } }参数ALARM_CONSECUTIVE_COUNT取 3 的含义是连续三次采样间隔约 1.5 秒都越界才进入报警状态这个值不算大既能抑制偶然波动又不会让真实报警来得太迟钝。如果你做的是实验室环境监测可以把 3 改成 5给人员留出响应时间如果你做的是工业现场的气体预警建议改成 2 并同时启动声音和屏幕双重报警。需要注意的是温度阈值用浮点数比较而 eCO2 阈值是整数两者在代码里的处理方式略有不同——浮点比较在嵌入式里涉及精度问题如果没开 FPUF103 的浮点运算比整数慢很多建议把温度乘 10 变成整数再比较。4.3 报警响应与蜂鸣器控制状态机取代简单开关报警状态如果只是「越界-响-恢复-停」的二值逻辑代码看起来简单实际用起来会发觉一个尴尬的问题温度从 39℃ 回落到 37.9℃报警停了但下一秒又涨回 38.1℃蜂鸣器就会频繁启动和停止刺耳且让人烦躁。所以我会给报警加一个迟滞区间温度超过 38℃ 报警38℃ 以下但高于 36.5℃ 时蜂鸣器仍然保持静音但 OLED 上的状态图标继续闪烁等温度降到 36.5℃ 以下才彻底解除报警。这个 1.5℃ 的滞后带能明显提升使用体验。同时报警蜂鸣不能是无间隔的连续长鸣否则长时间报警时使用者的耳朵受不了。我给蜂鸣器定义了三种触发状态短鸣0.2 秒响、1 秒停、长鸣连续响、10 秒后自动停、间歇鸣0.5 秒响、0.5 秒停持续到人员手动复位。默认越界报警用间歇鸣可以让值班人员快速定位到对应区域。这些状态切换都在Buzzer_Update()内部完成主循环不需要关心具体音调和时间组合。5. 五个必踩的坑现象、原因与解决这部分是这套系统里最容易让人翻车的几个问题全部是我在调试实际样机时遇到的真实场景按「现象 → 原因 → 解决」的顺序写。5.1 OLED 白屏或灰屏上拉电阻与电荷泵问题现象程序烧录后 OLED 屏幕完全无显示或者只有淡淡的一层均匀背光没有任何文字。原因两种可能一是 I2C 总线缺少上拉电阻导致 SCL/SDA 无法被拉高到逻辑高电平通信根本建立不起来二是初始化命令里缺少0x8D, 0x14这两条电荷泵开启命令SSD1306 内部的 DC-DC 升压电路未工作屏幕虽有供电但驱动电压不足。解决先检查硬件示波器或万用表量 SCL/SDA 引脚对 GND 的电压正常情况下空闲时应为 3.3V 左右如果接近 0V在两条线上各加一个 4.7kΩ 上拉电阻到 3.3V。如果电压正常再检查初始化命令序列确认0x8D后面跟的确实是0x14而不是0x10——0x10是关闭电荷泵写错会让屏幕一直灰屏。软件上再检查一下 I2C 地址SSD1306 的 7 位地址常规是 0x3C但有些屏幕是 0x3D扫描一下总线就能确认。5.2 DHT11 读数恒为 255 或全零延时函数精度不够现象温湿度读取函数返回 255 或 0x00无论怎么更换传感器都一样。原因DHT11 对时序的要求其实很宽松——它的数据手册上给出的高低电平时间范围不小但你的延时函数必须落在合理区间。用for循环写延时函数时编译器开了-O2优化后循环可能被直接优化掉一部分导致延时严重缩水或者系统时钟配置错误实际主频不是 72MHzdelay_us函数的参数是按时钟周期算的主频不对所有时间参数都被缩放。解决统一切换到基于 SysTick 的延时函数或者用 TIM 定时器的微秒级延时。检查 RCC 时钟树配置确认系统主频确实是 72MHz——我的习惯是在主程序一开头读RCC_GetClocksFreq()打印出来确认。再把 DHT11 读取函数里的delay_us(40)换成delay_us(50)看数据是否恢复。如果恢复说明原来的延时偏短。5.3 SGP30 数值疯涨且长时间不回落预热期与基线问题现象刚上电时 eCO2 读数跳到 2000ppm 甚至 4000ppm持续几分钟后缓慢回落到正常值附近。原因SGP30 出厂时没有校准基线传感器内部的算法需要一定时间收敛。如果环境里有酒精、油漆等挥发性气体或者传感器刚焊完松香残留未清洗收敛过程会更长。这是传感器的正常行为不是硬件损坏。解决软件层做两件事——上电后前 60 秒内不把 SGP30 的读数传递给报警逻辑只更新 OLED 显示也可以显示「预热中」如果传感器在固定位置长期工作建议在读数稳定并经过人工确认后执行基线保存命令命令字0x20, 0x87把基线数据存入 MCU 的 Flash每次上电后等待 15 秒再调用基线恢复命令写回传感器。如果你有检查过项目现场的空气质量可以同位置放一个参考设备做一次对比校准阈值。5.4 蜂鸣器声音发闷或无声有源无源搞混 驱动电流不足现象代码里 GPIO 置高了蜂鸣器不响或声音很小换一个蜂鸣器又响了。原因你手上的是无源蜂鸣器但代码按有源方式写。无源蜂鸣器本质是个线圈通直流电只会把振膜吸住不会发声需要给 PWM 方波才能响。另一个原因是 GPIO 灌电流有限单个引脚最大输出约 20mA整个 VDD 电流受限蜂鸣器启动瞬间的电流需求可能超过 MCU 能提供的量电压跌落导致声音发闷。解决先看蜂鸣器外壳上的标注通常有源蜂鸣器会印「内有振荡电路」或者型号里带 H 字样。无法确认时最简单的方法是给引脚一个直流高电平有源会响无源不响。如果确定是无源按前面的 PWM 初始化代码驱动如果确定是有源但声音小加一个 NPN 三极管如 S8050注意这是常规器件型号做开关放大集电极接蜂鸣器负端发射极接地基极串 1kΩ 电阻接 GPIO。注意这个三极管型号是通用半导体器件不是特定品牌。5.5 I2C 挂两个设备后 OLED 正常但 SGP30 读不到地址冲突和总线仲裁现象OLED 能显示但 SGP30 的 I2C 读操作返回 NACK数据全是 0xFF。原因两个设备挂同一总线时其中一个设备的地址与 SGP30 冲突或者总线上某一时刻有两个主设备同时在发起通信。STM32F103 的硬件 I2C 在检测到总线忙时会主动等待但如果 SGP30 的供电不稳内部逻辑电压过低它就无法对 I2C 地址进行应答表现为主机收到 NACK。解决先确认 SGP30 模块供电电压——它和 OLED 不要共用同一个 µC 的 3.3V 稳压输出如果你的开发板上 3.3V 稳压器输出能力只有 100mA两个模块加蜂鸣器启动瞬间会拉低电压导致 SGP30 间歇性不响应。把 SGP30 供电改成独立的 3.3V 稳压输出或者用万用表实测 SGP30 电源引脚在通信时的电压。如果电压没问题再用 I2C 扫描程序扫一下总线上所有地址确认 0x58 确实有设备响应。6. 进阶技巧逻辑分析仪验证时序、基线存储、低功耗改造这个系统做到能跑、能显示、能报警只是完成了第一版。真正把它变成可以长期稳定运行的设备我的经验是补三件事。第一件是学会用逻辑分析仪验证 I2C 和 DHT11 时序。市面上几十块钱的逻辑分析仪就能满足需求把探针夹在 SCL/SDA 和 DHT11 数据线上抓一段波形对比数据手册上的时序图能直接看出delay_us的误差到底有多大。我的习惯是写一小段循环测试代码让主控反复读取各传感器同时用逻辑分析仪抓三个通道观察 DHT11 的每一位高电平持续时间是否落在「0 位 26~28μs、1 位 70μs」的判定区间附近。不要等到整套系统调不通才开始抓波形那样信息量太大反而不容易定位。第二件是给 SGP30 添加基线保存功能。查数据手册可以找到两个命令地址0x20加命令码0x87用于获取基线值返回 4 字节基线数据地址0x20加命令码0x8E用于把基线值写回传感器。MCU 这边把这 4 字节存入 STM32F103 内部 Flash 的最后一个扇区记得先擦后写下次上电读完基线后延时 15 秒再写回。这个技巧能大幅缩短传感器在长期断电后再上电时的数值收敛时间对需要随开随用的便携式空气质量仪意义很大。第三件是低功耗改造思路。如果这套系统以后要做成电池供电的便携设备需要处理三个地方主芯片在数据采集间隙进入 STOP 模式用 RTC 闹钟定时唤醒OLED 在长时间无操作时关闭显示按一下按键再点亮SGP30 和 DHT11 的供电用 MOS 管做开关只有采样瞬间才通电采完立刻断电这能把平均功耗从毫安级降到微安级。另外 DHT11 的采样周期可以从 1.5 秒拉长到 10 秒反正温度和湿度在分钟级别上几乎不会突变。把这三个技巧做完这套系统就不再是「能演示的课设」而是有实际产品雏形的设备了。我个人的习惯是写完固件后先在桌面上连续跑 48 小时每天早上记一次读数看数值和报警状态是否稳定再进到下一步。数据不会骗人跑得越久越能暴露出定时器溢出、看门狗误触发、Flash 擦写次数这些平时看不到的毛病。希望这些经验能帮你把这块板子真正落到自己的项目中。本文还有配套的精品资源点击获取