news 2026/10/22 14:05:07

STM32环境温湿度监测系统设计:从选型到低功耗的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32环境温湿度监测系统设计:从选型到低功耗的工程实践

简介:这份资源面向学习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为例,外部晶振选8MHz,PLL倍频到72MHz,AHB不分频,APB1分频系数2得到36MHz,APB2不分频得到72MHz。I2C挂在APB1上,所以它的时钟源是36MHz。这个值后面算I2C时序要用到,别配错了。

接着开I2C1,模式选I2C,速度选标准模式100kHz或者快速模式400kHz。SHT30支持400kHz,AHT20也支持,所以直接上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地址是0x44(ADDR接GND)或0x45(ADDR接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模式全速跑大概几mA,Stop2模式能降到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(),问题就解决了。这个坑不常遇到,但一旦遇到就很隐蔽,因为逻辑分析仪上看波形是正常的,但数据就是不对。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:26:18

去除AI写作痕迹的实战指南:从检测原理到五步去痕法

1. 为什么你的AI初稿总有一股“塑料感”1.1 从两个真实场景说起先看两个场景。第一个场景&#xff1a;某内容团队用AI生成了一篇产品测评初稿&#xff0c;通篇结构工整、逻辑清晰&#xff0c;但发布后阅读完成率不到15%&#xff0c;评论区第一条是“这文章怎么一股机器人味儿”…

作者头像 李华
网站建设 2026/10/21 9:38:08

昇腾训推一体化实战:从模型微调到推理部署的完整指南

作为常年泡在大模型训练和推理一线的人&#xff0c;我这两年听到最多的吐槽&#xff0c;除了“显卡涨价”“数据清洗烦人”&#xff0c;就是“训练好好的&#xff0c;一到推理上线就各种水土不服”。模型在训练环境里跑得飞快&#xff0c;换到推理环境却要么精度对不上&#xf…

作者头像 李华
网站建设 2026/10/21 11:03:22

Spring Boot Admin实战踩坑:注册、告警与持久化完整排查指南

最近把 Spring Boot Admin 重新捡起来&#xff0c;给一套微服务环境补上监控面板。弄之前我以为这就是个“加依赖、写配置、看页面”的活&#xff0c;结果真正落地时踩了一串坑&#xff1a;注册不上、健康信息缺失、告警时灵时不灵、服务端一重启实例全部失联。整理下来发现&am…

作者头像 李华
网站建设 2026/10/21 9:54:47

AI Agent 落地实战:会议纪要与数据核对两大工作流场景拆解

1. 从“对话框”到“工作流”&#xff1a;AI Agent 落地的分水岭在哪大多数人第一次接触 AI 办公&#xff0c;都是从聊天框开始的。你问它答&#xff0c;你让它写一段它写一段&#xff0c;用起来像是一个反应很快的实习生。但真正把 AI 塞进日常业务的人很快会发现一个问题&…

作者头像 李华
网站建设 2026/10/19 6:50:35

PyQt QTableView实战:从自定义模型到表格美化与交互优化

做桌面应用&#xff0c;绕不开表格展示。不管是做设备管理、订单流水、用户列表还是日志查询&#xff0c;PyQt里的tableview&#xff08;准确说是QTableView&#xff09;基本是必用控件。我最早接触PyQt那会儿&#xff0c;第一反应是往QTableWidget里塞数据&#xff0c;简单是真…

作者头像 李华
网站建设 2026/10/19 15:33:31

基于双层模型的分布式光伏储能优化配置Matlab实现

干过分布式光伏储能项目的人应该都有同感&#xff1a;容量配错了&#xff0c;比设备故障还让人睡不着觉。光伏装多了&#xff0c;午间大发时段往往也是电价被压低的时候&#xff0c;发出来的电卖不上价&#xff1b;储能装小了&#xff0c;光伏大发时没地方存&#xff0c;峰谷价…

作者头像 李华