ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103环境监测系统:DHT11+SGP30+OLED+蜂鸣器实战

STM32F103环境监测系统:DHT11+SGP30+OLED+蜂鸣器实战 简介这款空气检测仪课程设计以STM32F103为主控结合DHT11温湿度传感器、SGP30气体传感器、蜂鸣器与OLED显示模块实现温湿度及CO2、甲烷等有害气体浓度的实时监测与超限报警适合嵌入式初学者、高校课程设计及电子爱好者参考。资源包内含205个文件压缩包大小7.32MB以C源码.c、头文件.h、编译产生的目标文件.o/.d/.crf、Keil工程配置.uvprojx/.uvoptx及Hex烧录文件等为主同时附带工程清理脚本与说明文档便于直接打开工程进行编译、烧录与二次开发。已有396人学习下载。通过该资源可获取完整工程代码、外设驱动配置、传感器数据读取与报警逻辑实现硬件连接和模块化编程思路清晰适合作为毕业设计或智能家居项目的起步模板。 如果你手里正好有一块吃灰的STM32F103板子别让它继续在抽屉里堆灰了。用DHT11做温湿度采集、SGP30检测环境里的CO2和TVOC、OLED屏实时显示数据、蜂鸣器在空气质量或温湿度超标时发出提醒这套组合就是很典型的嵌入式环境监测小系统。它不需要外接电脑上电就能独立跑起来数据自己刷新超标自己报警看起来像个小产品。对刚学完点灯、想真正把一个外设组合项目跑通的入门者来说这套组合能一次性把GPIO、定时器、单总线协议、I2C通信、中断和状态机全部串起来对要做课程设计或者桌面空气质量小助手的同学来说这也是一套可以直接参考的完整方案。我前后复现过两版这个项目第一版连线乱成一团第二版才算把代码和硬件都收拾利索。这篇文章会把方案选型、硬件连接、软件实现和联调避坑全部盘一遍部分细节基于常见实践做了补充。看完你能直接照着搭也能在搭的过程中少走几个弯路。1. 这套组合到底能做什么方案定位与硬件选型逻辑1.1 老将STM32F103为什么依然能打ST的STM32F103系列是Cortex-M3内核主频72MHzFlash从16KB到512KB可选RAM最大64KB。放在今天看指标并不夸张但它的外设资源对绝大多数传感器读取、控制、显示类项目来说完全够用。而且这芯片的资料密度是全网最高的中文参考手册、标准外设库V3.5、HAL库例程、各种开发板原理图随便一搜就是一大堆遇到问题基本都能搜到答案。对新手来说资料生态往往比极限性能更重要。另一个现实原因是便宜。最小系统板十几二十块钱芯片本片批量采购价更是低到可以忽略坏了不心疼。做产品原型、参赛作品、毕业设计用F103起步成本压力几乎为零。它的3.3V供电也正好和大多数传感器模块、OLED屏对接不需要额外电平转换硬件设计省掉一大半麻烦。1.2 传感器选型DHT11和SGP30为什么是“入门最优解”温湿度传感器没有选DHT22或者SHT30是因为DHT11的定位本来就是“够用且便宜”。它测温度范围0到50摄氏度精度正负2度湿度范围20%到90%RH精度正负5%RH。这个精度干不了计量级的活但判断室内是否闷热、是否潮湿、要不要开窗通风完全够用。更关键的是它是单总线接口只有一个数据脚学一次通信协议能搞懂时序概念性价比很高。SGP30则是盛思锐出的MEMS金属氧化物气体传感器走I2C接口输出eCO2等效CO2和TVOC总挥发性有机物。相比老一代的MQ系列模拟气体传感器SGP30不需要校准模拟电路数字输出直接读芯片内部还带自动基线校正算法精度和一致性都上了一个台阶。缺点是测量前需要一段通电老化时间新传感器上电初期数据会漂跑个半天一天会稳定很多。这部分属于传感器特性不是代码问题提前知道能少焦虑半天。蜂鸣器和OLED是典型的执行与显示单元。蜂鸣器负责超阈值报警OLED负责把读数展示出来。整套系统闭环下来就是传感器采数据主控处理数据屏幕显示数据蜂鸣器响应数据。逻辑链非常完整非常适合用来建立“传感器-主控-执行器”的整体认知。1.3 整套系统一分钟工作流程上电后STM32F103完成各外设初始化OLED先清屏并显示系统启动提示然后进入主循环。每个循环里主控定时读取DHT11得到温度湿度读取SGP30得到CO2和TVOC把它们格式化后刷新到OLED指定位置。如果任意指标超过预设阈值蜂鸣器按照设定模式鸣叫指标回到安全范围蜂鸣器停止。整个循环的测量频率不用太快DHT11建议读取间隔不低于1秒SGP30官方建议也是1秒读一次OLED每秒刷一次完全够用。2. 硬件连接与最小系统设计别在电路上翻车2.1 最小系统的标准电路先检查一遍如果你用的是开发板这部分可以跳过。但如果你用的是自己画的裸板或者最小系统板上电前最好按下面几点自查一遍。STM32F103的最小系统需要保证3.3V供电正常且电源引脚旁边有0.1uF退耦电容8MHz外部晶振接在OSC_IN和OSC_OUT上各配一个20pF左右的负载电容NRST复位引脚通过10k电阻上拉到3.3V并接0.1uF电容到地BOOT0和BOOT1有明确的跳线或者下拉电阻正常运行必须确保BOOT0为低电平VDDA引脚最好串联一个小磁珠或直接接3.3V保证ADC和内部参考电压稳定。这里面最容易翻车的是复位电路。有些板子为了省事把NRST直接悬空结果芯片偶尔进入异常状态不跑程序怎么排查都找不到原因。复位引脚保持上拉是底线别省。2.2 推荐引脚分配与接线表我第一版做的时候引脚分配比较随意后来发现软件I2C和单总线引脚如果靠得太近或者接口不合理后期调试会很痛苦。这次直接按下面这张表接逻辑清晰也方便照着写代码模块引脚说明DHT11 DATAPA1单总线数据脚需接4.7k到10k上拉电阻到3.3VDHT11 VCC3.3V模块供电部分模块兼容5V但建议统一3.3VDHT11 GNDGND接地SGP30 SCLPB6I2C时钟线接4.7k上拉到3.3VSGP30 SDAPB7I2C数据线接4.7k上拉到3.3VSGP30 VCC3.3V模块供电注意SGP30不支持5VSGP30 GNDGND接地OLED SCLPB6与SGP30共用I2C总线OLED SDAPB7与SGP30共用I2C总线OLED VCC3.3V0.96寸OLED模块供电OLED GNDGND接地蜂鸣器 IOPA0低电平触发经过NPN三极管驱动DHT11的数据脚和I2C的SCL、SDA都需要上拉电阻。很多模块板上已经自带上拉如果你用的是裸传感器而不是模块务必自己在电路上补一个4.7k电阻。上拉缺失最常见的现象是通信不稳定DHT11偶发读回0或者校验失败I2C设备偶发无应答。2.3 蜂鸣器驱动的两个坑有源无源、上电误触发蜂鸣器分有源和无源两种。有源蜂鸣器内部自带振荡电路给一个高电平或低电平它就会自己发声无源蜂鸣器需要外部提供一定频率的方波才能发声通常用定时器PWM或者GPIO翻转来驱动。新手建议首选有源蜂鸣器代码简单给电平就响。如果想用无源蜂鸣器播放不同音调那就需要定时器输出PWM频控逻辑会复杂一些适合后期进阶玩。第二个坑是上电瞬间蜂鸣器可能“嘟”一声。原因是STM32F103的GPIO在复位期间默认是浮空输入或默认电平如果蜂鸣器驱动用的是NPN三极管高电平导通复位瞬间引脚电平不确定很可能会误触发。解决思路有三条一是在蜂鸣器控制引脚外部加一个10k下拉电阻让复位期间电平被钳在低电平二是改用低电平触发的有源蜂鸣器模块引脚复位高电平时蜂鸣器反而不会响三是在主程序最开始就把蜂鸣器引脚初始化为低电平并立即拉低缩短误响窗口。实际项目中我习惯把下拉电阻和低电平触发结合起来效果最稳。3. 软件实现从标准库到HAL库核心代码怎么看3.1 开发环境与库的选择开发环境基本就是Keil MDK5也可以用STM32CubeIDE两者都行。库的选择有两条路线一是ST标准外设库V3.5二是STM32CubeMX生成的HAL库。标准库代码紧凑适合想搞懂寄存器底层逻辑的人HAL库代码量大但抽象程度高配合CubeMX可以快速生成初始化代码更适合做图形化配置。我两种都写过结论是这个项目规模不大用哪个库都能完成关键是别在工程中途切换。搜索“stm32f103库v3.50下载”基本能找到全套标准库CubeMX则直接在线生成HAL工程选一条走到头就行。工程结构上建议把每个外设封装成独立模块例如bsp_dht11.c、bsp_sgp30.c、bsp_oled.c、bsp_beep.c每个模块配套头文件主函数只管业务流程。这样后期换传感器、改逻辑不用在主函数里东翻西找。模块化习惯越早建立写工程时越舒服。3.2 DHT11单总线时序这个项目里最难啃的骨头DHT11用的是单总线协议一根线上既要主机发信号又要从机回数据。可以把它想象成一条窄路主机和传感器轮流走谁走之前都得先确认路上空出来。通信过程大致分四步主机拉低总线至少18毫秒然后释放总线DHT11收到起始信号后先拉低80微秒响应再拉高80微秒随后开始逐个发送40位数据每一位数据都由一个50微秒的低电平开始紧接着是一段高电平高电平持续26到28微秒代表数据0持续70微秒代表数据140位数据发送结束后释放总线。40位数据的排列是湿度整数、湿度小数、温度整数、温度小数、校验和。校验和等于前四个字节相加的低8位如果算出来和第五个字节不一致这一帧数据就丢弃。DHT11的温湿度小数部分在实际器件上通常为0但协议里保留了这个字段读取时不要忽略。下面是一个用标准库实现的读取单字节函数核心在于用微妙级延时判断高电平宽度uint8_t DHT11_ReadByte(void) { uint8_t i, data 0; for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 0); // 等50us低电平结束 delay_us(40); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 1) { data | (0x80 i); // 高电平超过40us判断为1 } while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 1); // 等高电平结束 } return data; }这里有个关键经验判断数据0还是数据1临界值通常取50微秒。代码里延时40微秒后再读电平是因为要留出提前量确保不会在低电平期间误读。延时函数的精度直接影响通信成功率务必用SysTick做阻塞延时或者使用DWT计数器别用普通for循环裸奔因为编译器优化级别一变延时时间就不准了。另外整个读取过程要关中断避免中断服务函数打断微秒级时序。可以用临界区保护比如在读取前执行__disable_irq()读完后执行__enable_irq()。这一步不做通信会间歇性失败而且很难查。3.3 SGP30的I2C读取命令、校验和字节序SGP30的I2C从机地址是0x58注意这是7位地址写代码时要左移一位拼上读写标志位。它的核心测量命令是0x20 0x03发送这个命令后等待20毫秒左右从设备读回6个字节前两个字节是eCO2第三个字节是CRC校验后两个字节是TVOC第六个字节是CRC校验。CRC算法是CRC-8多项式为x^8 x^5 x^4 1初始值0xFF。如果把CRC校验逻辑做进去可以过滤掉一大部分I2C干扰导致的错误数据。读取SGP30的流程可以这样写uint8_t sgp30_cmd[2] {0x20, 0x03}; uint8_t sgp30_buf[6]; uint8_t co2_h, co2_l, tvoc_h, tvoc_l; uint16_t co2_ppm, tvoc_ppb; I2C_Start(); I2C_SendByte(0x58 1); I2C_SendByte(0x20); I2C_SendByte(0x03); I2C_Stop(); delay_ms(20); I2C_Start(); I2C_SendByte(0x58 1 | 0x01); for (i 0; i 6; i) { sgp30_buf[i] I2C_RecvByte(i 5 ? 0 : 1); // 最后一字节回NACK } I2C_Stop(); if (CRC8_Check(sgp30_buf[0], sgp30_buf[1], sgp30_buf[2]) 0) { co2_ppm (sgp30_buf[0] 8) | sgp30_buf[1]; } if (CRC8_Check(sgp30_buf[3], sgp30_buf[4], sgp30_buf[5]) 0) { tvoc_ppb (sgp30_buf[3] 8) | sgp30_buf[4]; }注意字节序是大端模式也就是高字节在前。好多人在这个地方栽跟头把高字节和低字节搞反读出来的CO2数值会完全没逻辑。另外一个经验是STM32F103的硬件I2C模块在标准库下偶尔会有总线忙、卡死的情况网上也很多吐槽所以这个项目强烈建议用软件模拟I2C就是两个GPIO翻转来模拟时序。虽然代码多一点但是时序完全可控排查问题容易而且OLED和SGP30共用一根总线软件I2C处理起来更灵活。SGP30还有一个特性是内部自动基线校正。传感器通电后会持续学习环境气体把洁净空气下的基线存到内部寄存器里。测量间隔官方要求是1秒别为了刷新率强行提高读取频率频繁读取不仅数据没意义还可能干扰内部基线算法。新传感器上电前半天数据会有波动跑一段时间后会稳定下来这是器件的正常物理特性不是你代码有bug。3.4 OLED显示与汉字取模0.96寸OLED屏大多数用的是SSD1306驱动芯片分辨率为128x64接口方式有I2C、SPI和并口。最省引脚的就是I2C模式默认从机地址是0x3C正好可以和SGP30挂在同一条I2C总线上地址不冲突。OLED的显示原理是芯片内部有一块1KB的GRAM主控把要显示的数据写入GRAMSSD1306再把GRAM内容刷新到屏幕。因此只要初始化正确主控可以先把显示内容写入缓冲区再一次性更新屏幕避免逐字刷新产生闪烁。初始化序列各家基本一致核心是打开电荷泵、设置行列起始地址、选择寻址模式、点亮显示。初始化完成后屏幕会亮。如果你只是给OLED接上电源而不做任何初始化SSD1306内部RAM上电内容是随机的屏幕往往会亮的杂乱无章这是正常现象不是屏坏了。第一次看到OLED上电就亮不要慌初始化代码跑完就会按预期显示了。显示汉字需要字模。常用取模软件是PCtoLCD2002取模方式选择阴码、逐行式、顺向生成16x16点阵的字模然后放到代码里。如果只是显示数字和字母用8x6或16x8的ASCII字模就够了。显示数字时注意数值类型如果CO2是uint16_t而用printf格式化到一个字符串缓冲区记得用%u而不是%d避免负数转换后显示数字溢出或者吓人的值。4. 联调、常见问题与排查技巧实录4.1 一套从零到显示的联调顺序我最开始做这个项目时犯过一个比较蠢的错误所有模块一次性接好、一次性烧录然后一堆问题同时出现根本不知道从哪查起。后来总结出一套联调顺序效率提升明显。先把OLED独立调通。只接OLED烧录一个显示固定字符的测试程序看到屏幕正常显示“Hello”再继续。这个过程验证I2C总线和OLED初始化代码没问题。然后把SGP30接上在同一根I2C总线上读取CO2和TVOCOLED分两行显示。如果SGP30读不到把OLED拔掉只接SGP30测试这样可以快速区分是总线冲突还是地址错误。再接DHT11单独读取温湿度并显示在OLED上观察数据是否跳动。最后再接蜂鸣器设置一个临时阈值比如温度超过30度就响做整机联动测试。分步联调的好处是每一层都有明确的验证标准出问题不用猜。4.2 常见问题速查表现象可能原因排查与解决思路OLED上电就亮但全是乱点未初始化或初始化异常确认初始化代码执行检查I2C地址是否为0x3COLED完全暗屏供电不足、I2C上拉缺失、地址错误测量VCC是否为3.3V检查SCL/SDA上拉电阻用I2C扫描程序确认设备地址DHT11一直读回0或校验失败上拉电阻缺失、时序被中断打断、采样间隔过短补4.7k上拉读取前关中断确保两次读取间隔大于1秒SGP30读回数据固定不变传感器还在预热老化阶段通电老化至少半天洁净环境下观察数值缓慢变化SGP30 CRC校验连续失败I2C速率异常、总线干扰检查上拉电阻降低软件I2C时钟频率确认测量间隔不低于1秒蜂鸣器上电瞬间误响复位期间GPIO电平不确定且驱动高电平导通引脚外接10k下拉或改用低电平触发的蜂鸣器模块OLED刷新时明显闪烁全屏清屏加全量刷新改为只刷新变化区域的坐标缓冲或者使用双缓冲局部更新4.3 几个我踩过的坑和判断思路先说DHT11的经验。我第一版代码里用了for循环做微秒延时编译优化等级从-O0调到-O2后DHT11的通信成功率从稳定变成了间歇性失败查了好几个小时才怀疑到延时上。换了SysTick延时后就再没出过这个问题。所以凡是遇到DHT11读数时好时坏先检查延时函数和中断状态再怀疑硬件接线。另外读取DHT11前主机拉低起始信号的时间要严格保证18毫秒以上短了传感器不响应长了也不会有额外问题但别太短。再说SGP30。我第一次上电测试连续一小时读到的CO2都是400ppm左右TVOC也是几ppb一度以为是传感器坏了。后来查手册才知道SGP30出厂后需要进行老化运行初期数据基本处于“无参考”状态芯片会通过内部算法慢慢建立基线这个过程可能持续几十个小时。所以不要用刚上电的数据评判好坏先让系统跑一个下午数据趋势合理才算正常。另外SGP30对供电纹波比较敏感如果OLED刷新瞬间数据偶发跳变检查一下模块供电必要时在SGP30电源引脚旁边加一个10uF电容。OLED的问题反而最少。唯一一次踩坑是I2C总线挂了两颗器件后硬件I2C偶发死锁表现为OLED整个屏幕固定不刷新但主控程序还在跑。后来统一改成软件I2C这个问题再也没出现过。如果非要使用硬件I2C可以给I2C总线加上总线超时恢复机制但这对于入门项目来说性价比太低。最后说一下蜂鸣器报警逻辑。我建议在主循环里做一个连续判断比如连续3次采样都超过阈值才触发报警。这样做可以有效防止传感器单次偶发跳变造成的误报警。报警方式也可以做成不同模式比如CO2超标用短响两下停顿一下温湿度超标用长响用户听到声音就能分辨是哪种指标出问题。这类小细节虽然不影响功能但会让整个项目更有“产品感”。最后说几句实在话这套组合我前后复现了两遍第一遍是学习阶段第二遍是帮朋友做课程设计。最大的感受是真正难的不是任何一个单独模块而是如何把时序要求不同的外设组合到一起而不互相干扰。DHT11的微秒时序、SGP30的通信周期、OLED的刷新策略、蜂鸣器的触发时机每一项单独拎出来都不难合在一起就需要你在代码结构上做取舍。如果后续想继续扩展建议朝三个方向走一是加ESP8266或ESP32模块把温湿度和空气数据上传到云端做历史曲线这就变成了一个物联网数据采集节点二是把DHT11换成I2C接口的DHT20或SHT30绕开单总线的时序问题采集稳定性会更高三是把SGP30的基线数据定时写入Flash避免每次断电重启后传感器又从头开始老化这一步能让设备长期运行时的数据更可靠。先把这套系统跑稳再一步步往上叠加嵌入式这条路就是这么练出来的。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进