
简介这份资源面向学习STM32嵌入式开发的学生与电子爱好者提供一套完整的环境温湿度监测系统设计方案可用于课程设计、毕业设计或单片机实训。系统以STM32为核心通过DS18B20采集温度、DHT11采集湿度将数据实时显示在LCD液晶屏上并支持手动设置温湿度上下限超限时触发声光报警——无源蜂鸣器播放音乐配合LED闪烁报警方式可按需调整。压缩包共195个文件约4.34MB包含46个c源文件与46个h头文件构成主要逻辑代码另有o、d、crf等编译中间文件以及uvprojx、uvoptx工程配置、hex烧录文件、map与lst映射清单等工程结构完整可直接用Keil打开编译。目前已有8724人学习下载读者可从中获取传感器驱动、LCD显示、阈值判断与声光报警的完整实现思路并参考工程目录组织与调试配置快速搭建自己的监测系统。1. 从一颗芯片到一块能用的板子STM32环境温湿度监测系统到底在做什么很多人第一次接触嵌入式都是从“环境温湿度监测”这个题目入手的。它看起来简单——一颗STM32一个温湿度传感器一块屏几根线——但真正动手做出来能稳定跑上几个月的系统和实验室里跑通一次就收工的Demo中间隔着一整套工程思维。这个标题里的“设计”两个字重点不在传感器多贵、屏幕多大而在于你如何把采集、处理、显示、报警、存储这几件事在有限的RAM和Flash里安排得明明白白。我见过太多人卡在同一个地方传感器读出来的数据跳得厉害或者跑几个小时就死机又或者电池供电撑不过两天。这些问题不是靠换一个更贵的传感器能解决的而是要从供电、时序、滤波、看门狗这些基础环节去排查。这篇文章面向的是想把这个题目真正做成可交付作品的开发者——不管你是学生做毕设还是工程师做产品原型下面这套路径都能让你少走几晚通宵的弯路。2. 选型定生死STM32型号、传感器和供电方案怎么定2.1 STM32F103还是STM32L4先看你的采样频率和功耗预算选型的第一步不是看价格而是看你的系统到底要跑多快、跑多久。温湿度监测这个场景采样频率通常很低——每秒一次都算高频了大多数时候是每分钟甚至每十分钟采一次。这意味着CPU大部分时间在睡觉真正的功耗大头在传感器和无线模块上。如果你做的是插电设备STM32F103C8T6这颗“国民芯片”完全够用72MHz主频、64KB Flash、20KB RAM跑一个温湿度采集加OLED显示的程序绰绰有余。它的优势是资料多、价格低、随便一个开发板都能上手。但如果你要做电池供电、要求一节纽扣电池撑半年以上那就得换STM32L4系列或者STM32U0系列它们的Stop模式电流能压到微安级而F103的Stop模式还在几十微安徘徊。我一般会按这个逻辑走先算功耗预算。假设用2000mAh的锂电池目标续航一年那么平均电流必须控制在0.23mA以内。传感器选低功耗的比如SHT30在单次测量模式下每次只消耗几毫安持续几毫秒平均下来可以忽略屏幕如果常亮那功耗直接爆表必须改成按键唤醒或者定时刷新。把这些算清楚再回头选STM32型号就不会出现“芯片选好了发现功耗降不下来”的尴尬。2.2 温湿度传感器DHT11、SHT30、AHT20的取舍与踩坑点DHT11是很多人的入门传感器便宜、单总线、直接出数字量。但它的精度是±2℃和±5%RH而且响应慢、一致性差同一批买回来五个能读出五个不同的值。如果你只是做个演示它没问题但如果你要写进报告或者做数据记录DHT11的数据会让你怀疑人生。SHT30和AHT20是I2C接口的精度分别是±0.3℃/±3%RH和±0.3℃/±2%RH价格也就几块钱。它们的优势是出厂校准、一致性好、支持CRC校验。我强烈建议直接用I2C传感器省下来的调试时间远比省下的几块钱值钱。唯一要注意的是I2C总线的上拉电阻——很多模块自带上拉但如果你自己画板子记得在SDA和SCL上各挂一个4.7kΩ到10kΩ的电阻到VCC否则通信会时好时坏这种“玄学”问题最耗人。2.3 供电方案LDO还是DC-DC电池供电必须算清这笔账如果你用USB供电那随便一个AMS1117-3.3都能用但要注意它的压差和发热。AMS1117在输入5V、输出3.3V、负载100mA时自身要消耗0.17W封装小的话会烫手。更麻烦的是当电池电压降到3.5V以下时AMS1117的输出就开始不稳STM32可能反复复位。电池供电场景下我一般会选低压差的LDO比如XC6206系列它的压差只有250mV左右静态电流也小。如果对效率要求更高比如用单节锂电供电那就上DC-DC降压效率能到90%以上。但DC-DC的开关噪声可能干扰传感器布局时要让电感远离模拟部分输出端多并几个电容滤波。这些细节在原理图阶段就要定下来等板子打回来再改就来不及了。3. 从零搭建CubeMX配置、I2C驱动和主循环的落地步骤3.1 用CubeMX把时钟树和I2C外设配到能跑的状态打开CubeMX选好芯片型号后第一件事是配时钟。以STM32F103C8T6为例外部晶振选8MHzPLL倍频到72MHzAHB不分频APB1分频系数2得到36MHzAPB2不分频得到72MHz。I2C挂在APB1上所以它的时钟源是36MHz。这个值后面算I2C时序要用到别配错了。接着开I2C1模式选I2C速度选标准模式100kHz或者快速模式400kHz。SHT30支持400kHzAHT20也支持所以直接上400kHz没问题。但如果你用的线比较长或者上拉电阻偏大400kHz可能出误码那就降到100kHz。配置完后生成代码CubeMX会自动初始化GPIO和I2C外设。/* i2c.c 中 CubeMX 生成的初始化片段 */ hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // 400kHz 快速模式 hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; // 占空比 2:1 hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE;这段代码里ClockSpeed设的是SCL频率DutyCycle在快速模式下才有效标准模式不用管。NoStretchMode建议保持Disable让从机可以拉低SCL做时钟延展否则某些传感器在转换期间会丢数据。3.2 SHT30单次测量模式的完整驱动代码与CRC校验SHT30的I2C地址是0x44ADDR接GND或0x45ADDR接VCC。单次测量模式下主机发送命令0x2C06高重复性、时钟拉伸使能然后等待测量完成再读6个字节温度高8位、温度低8位、温度CRC、湿度高8位、湿度低8位、湿度CRC。/* sht30.c 单次测量读取温湿度 */ #define SHT30_ADDR (0x44 1) // HAL库需要左移一位 uint8_t sht30_read(float *temp, float *humi) { uint8_t cmd[2] {0x2C, 0x06}; // 高重复性时钟拉伸使能 uint8_t data[6] {0}; uint16_t raw_temp, raw_humi; // 发送测量命令 if (HAL_I2C_Master_Transmit(hi2c1, SHT30_ADDR, cmd, 2, 100) ! HAL_OK) return 1; // 等待测量完成高重复性最大耗时15ms HAL_Delay(20); // 读取6字节数据 if (HAL_I2C_Master_Receive(hi2c1, SHT30_ADDR, data, 6, 100) ! HAL_OK) return 2; // CRC校验可选但强烈建议 if (sht30_crc8(data, 2) ! data[2]) return 3; if (sht30_crc8(data 3, 2) ! data[5]) return 4; raw_temp (data[0] 8) | data[1]; raw_humi (data[3] 8) | data[4]; *temp -45.0f 175.0f * raw_temp / 65535.0f; *humi 100.0f * raw_humi / 65535.0f; return 0; } /* CRC-8校验多项式0x31初值0xFF */ static uint8_t sht30_crc8(const uint8_t *data, uint8_t len) { uint8_t crc 0xFF; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t b 0; b 8; b) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc 1; } } return crc; }这段代码里HAL_Delay(20)是保守等待实际高重复性测量最大15ms但如果你开了时钟拉伸可以用HAL_I2C_Master_Receive直接阻塞读传感器会拉低SCL直到数据准备好。CRC校验千万别省I2C在电机、继电器附近容易受干扰没有CRC你根本不知道读到的数据是真是假。温度换算公式是数据手册里的标准公式湿度同理。3.3 主循环里做滑动平均滤波别让数据跳得像心电图传感器原始数据总会有波动尤其是湿度开门、呼吸都能让它跳几个百分点。直接显示原始值会让用户觉得系统“不稳”。最简单的处理是滑动平均开一个长度为N的数组每次新数据进来就替换最旧的那个然后求平均。/* filter.c 滑动平均滤波 */ #define FILTER_LEN 8 typedef struct { float buf[FILTER_LEN]; uint8_t idx; uint8_t count; } filter_t; float filter_update(filter_t *f, float new_val) { float sum 0; f-buf[f-idx] new_val; f-idx (f-idx 1) % FILTER_LEN; if (f-count FILTER_LEN) f-count; for (uint8_t i 0; i f-count; i) sum f-buf[i]; return sum / f-count; }FILTER_LEN取8是我常用的值再大响应就迟钝了再小滤波效果不明显。如果你要更平滑可以上卡尔曼滤波但温湿度这种慢变量滑动平均足够。注意数组长度和count的处理刚开始数据不够时只平均有效部分否则会拉低初始值。4. 避坑指南I2C死锁、数据跳变和看门狗复位的排查记录4.1 I2C总线死锁现象是SCL被拉低不放原因是主从复位不同步现象程序跑一段时间后I2C通信全部失败用逻辑分析仪看SCL一直低电平SDA可能高可能低。原因通常是主机在从机发送数据时复位或者从机在时钟拉伸期间主机异常导致从机一直等时钟但主机已经不管了。解决在I2C初始化前加一段总线恢复代码手动切换SCL为GPIO输出发送9个时钟脉冲然后发一个Stop条件。具体做法是把SCL和SDA都配成开漏输出拉高然后循环9次拉低再拉高SCL最后拉低SDA再拉高SDA产生Stop。这段代码放在MX_I2C1_Init()之前执行能解决大部分死锁。4.2 湿度读数突然跳到100%不是传感器坏了是冷凝水在作怪现象系统在空调房跑得好好的拿到室外或者厨房附近湿度直接跳到95%以上甚至100%温度也偏高。原因不是传感器故障而是传感器表面结露了。SHT30的湿度敏感层一旦有水膜读数就会失真而且需要很长时间才能恢复。解决首先传感器不要直接对着风口或者贴在金属壳上留出空气流通空间。其次如果应用场景湿度可能接近饱和选带加热功能的传感器比如SHT31它可以周期性开启加热器驱散冷凝。软件上如果连续多次读到湿度95%可以标记为“可能结露”在显示上给出提示而不是直接报数。4.3 看门狗复位后数据丢失备份寄存器没用对等于白喂狗现象加了独立看门狗IWDG后系统偶尔复位但复位后之前累积的统计数据全没了。原因是你把数据只放在RAM里复位后RAM被重新初始化。STM32有备份寄存器BKP由VBAT供电主电源掉电也不丢但很多人忘了在CubeMX里使能PWR_CLK和BKP_CLK也没写解锁代码。解决在HAL_Init()之后、看门狗初始化之前先使能__HAL_RCC_PWR_CLK_ENABLE()和__HAL_RCC_BKP_CLK_ENABLE()然后调用HAL_PWR_EnableBkUpAccess()解锁备份域。之后用HAL_RTCEx_BKUPWrite()写数据复位后先读出来判断是不是看门狗复位。注意备份寄存器只有几十字节别拿来存大数组。4.4 喂狗时间不够主循环里一个HAL_Delay就把狗喂死了现象看门狗超时设了2秒但系统还是频繁复位。查代码发现主循环里有个HAL_Delay(3000)在等传感器转换。原因很简单看门狗是独立运行的你延时3秒狗早饿了。解决要么把看门狗超时设长一点要么把长延时拆成短延时并在中间喂狗。更好的做法是用定时器中断来喂狗主循环只管业务喂狗在中断里做。但要注意如果主循环卡死中断还在跑狗就喂不到了所以喂狗中断里最好加一个标志位主循环定期置位中断检查标志再喂这样主循环卡死时中断就不会喂狗狗才能正常复位。5. 进阶技巧把数据存进Flash、用RTC做时间戳、低功耗模式实测5.1 用内部Flash模拟EEPROM存历史数据注意擦写寿命STM32F103没有真正的EEPROM但可以用内部Flash模拟。Flash的擦写寿命是1万次左右如果你每分钟存一次数据一天就是1440次不到一周就把一个扇区写废了。所以不能直接往同一个地址写要做磨损均衡把一页Flash分成多个记录槽每个槽存一条数据写满一页再擦除整页从头写。/* flash_log.c 简易Flash日志每页存32条记录 */ #define PAGE_ADDR 0x0801F000 // 最后一页根据芯片型号调整 #define PAGE_SIZE 2048 #define SLOT_SIZE 64 #define SLOT_COUNT (PAGE_SIZE / SLOT_SIZE) typedef struct { uint32_t timestamp; float temp; float humi; uint32_t crc; } log_record_t; uint8_t flash_log_write(log_record_t *rec) { static uint8_t slot 0; uint32_t addr PAGE_ADDR slot * SLOT_SIZE; // 检查是否写满写满则擦除整页 if (slot SLOT_COUNT) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase {0}; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress PAGE_ADDR; erase.NbPages 1; uint32_t err; HAL_FLASHEx_Erase(erase, err); HAL_FLASH_Lock(); slot 0; addr PAGE_ADDR; } // 写入数据 HAL_FLASH_Unlock(); uint32_t *p (uint32_t *)rec; for (uint8_t i 0; i sizeof(log_record_t) / 4; i) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i * 4, p[i]); } HAL_FLASH_Lock(); slot; return 0; }这段代码里PAGE_ADDR要根据你的芯片Flash大小来定别写到程序区去了。SLOT_SIZE取64字节一条记录16字节留了余量。每次写之前不用擦除因为Flash只能把1写成0不能把0写成1所以新槽位默认是0xFFFFFFFF直接写就行。写满一页再擦这样擦写次数降到原来的1/32。5.2 RTC时间戳让每条数据都知道自己“什么时候来的”没有时间戳的数据记录是没有灵魂的。STM32F103有RTC外设配一个32.768kHz晶振就能走时。CubeMX里使能RTC时钟源选LSE然后开启日历功能。初始化时设一个默认时间之后每次存数据前调HAL_RTC_GetTime()和HAL_RTC_GetDate()拿时间戳。注意一个坑HAL_RTC_GetTime()和HAL_RTC_GetDate()必须成对调用而且要先读Time再读Date否则日期可能不更新。另外RTC的备份域要接VBAT否则主电源掉电时间就丢了。如果你没有VBAT电池那每次上电时间都从默认值开始数据时间戳就乱了。5.3 低功耗实测Stop模式下电流从20mA降到80μA的配置清单如果你要做电池供电低功耗是绕不过去的。以STM32L4为例Run模式全速跑大概几mAStop2模式能降到1μA以下但外设要关干净。我实测过一套配置从20mA降到80μA关键操作如下操作项配置说明系统时钟降频到2MHz采集时用MSI处理完切回未用GPIO配成模拟输入避免浮空输入漏电传感器单次测量后断电用MOS管控制传感器VCC屏幕定时刷新后关背光背光是功耗大户调试接口关闭SWD调试完在代码里禁用RTC保留用LSE用于定时唤醒看门狗保留但超时设长低功耗下喂狗间隔要匹配进入Stop模式用HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI)唤醒后要重新配置系统时钟因为Stop模式下HSI会关掉。唤醒源可以用RTC闹钟或者外部中断。实测中80μA的平均电流里LDO静态电流占了30μA所以选LDO时一定要看静态电流参数别只看压差。5.4 一个我常犯的错误忘了在唤醒后重新初始化I2C低功耗模式唤醒后外设状态可能丢失。我遇到过好几次Stop模式唤醒后I2C读传感器一直失败查了半天发现是I2C外设需要重新初始化。后来我在唤醒后的代码里加了一句MX_I2C1_Init()问题就解决了。这个坑不常遇到但一旦遇到就很隐蔽因为逻辑分析仪上看波形是正常的但数据就是不对。希望帮到你。本文还有配套的精品资源点击获取