news 2026/10/27 9:15:56

STM32F1驱动DHT11温湿度传感器:从时序解析到代码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F1驱动DHT11温湿度传感器:从时序解析到代码实现

1. 为什么STM32F1到今天还值得花时间学

1.1 一颗老芯片的生存逻辑

如果你最近在电子论坛或者开源硬件社区里逛,会发现一个挺有意思的现象:一边是各种高性能新片子层出不穷,一边是大量的人在讨论STM32F1系列,尤其是配上DHT11温湿度传感器做环境监测的项目。这看起来有点矛盾,但仔细想想又特别合理。

STM32F1是意法半导体基于ARM Cortex-M3内核推出的微控制器系列,最经典的型号比如STM32F103C8T6,也就是大家常说的“蓝板”或者“最小系统板”。它的主频通常跑在72MHz,拥有64KB到128KB的Flash、20KB的SRAM,外设资源包括多个定时器、USART、SPI、I2C、ADC等等。这些参数放在今天看确实不算亮眼,但它的价格、生态和资料丰富程度,让它在入门级嵌入式开发里依然占据着不可替代的位置。

我个人的判断是:STM32F1系列是学习嵌入式开发性价比最高的平台之一。原因很简单——它的参考手册、标准外设库、HAL库、各种例程和社区问答已经积累到了一个恐怖的程度。你遇到的几乎任何一个基础问题,都能在中文或英文社区里找到现成的答案。对于新手来说,这意味着学习曲线会平缓很多;对于有经验的工程师来说,这意味着做快速原型验证时不用在底层驱动上浪费时间。

1.2 DHT11为什么总跟STM32F1绑在一起

DHT11是一款非常基础的温湿度复合传感器,它内部集成了一个电阻式湿度测量元件和一个NTC测温元件,输出的是经过校准的数字信号。它的测量范围是湿度20%到90%RH、温度0到50摄氏度,精度分别是±5%RH和±2摄氏度。这个精度放在工业场景里肯定不够看,但对于室内环境监测、农业大棚简易监控、学生实验这类应用来说完全够用。

DHT11和STM32F1之所以经常被放在一起,核心原因是两者的定位高度匹配。DHT11是单总线通信,只需要一根数据线就能完成温湿度读取,对MCU的引脚资源占用极少。而STM32F1的GPIO操作灵活,配合微秒级延时可以轻松实现DHT11的时序要求。更重要的是,这个组合的成本极低——一块STM32F103最小系统板加上一个DHT11模块,总花费可能不到二十块钱,但你能学到的知识点覆盖了GPIO控制、精确延时、单总线协议解析、串口输出、LCD显示等多个方面。

1.3 这篇文章能帮你解决什么问题

接下来我会从硬件选型、开发环境搭建、DHT11时序解析、代码实现、常见问题排查这几个维度,把STM32F1驱动DHT11这件事彻底讲透。不管你是刚接触单片机的学生,还是想快速验证一个环境监测方案的工程师,都能从里面找到可以直接复用的内容。我不会只贴一段代码就完事,而是会把每个关键决策背后的原因讲清楚,比如为什么用SysTick做微秒延时而不是随便写个for循环,为什么DHT11的起始信号要拉低至少18毫秒,这些细节才是决定项目能不能跑通的关键。

2. 硬件选型与开发环境搭建

2.1 核心器件选型对比

在动手之前,先把硬件方案确定下来。STM32F1系列型号很多,DHT11也有不同的封装形式,选对了能省不少事。

器件类型推荐型号关键参数选型理由
主控芯片STM32F103C8T672MHz, 64KB Flash, 20KB SRAM资料最多,价格最低,最小系统板遍地都是
温湿度传感器DHT11湿度20-90%RH, 温度0-50°C单总线协议简单,成本极低,适合入门
调试器ST-Link V2SWD接口支持在线调试和下载,比串口下载方便太多
显示模块OLED 0.96寸 I2C128x64分辨率比LCD1602接线少,显示内容丰富
电源3.3V或5VDHT11支持3.3V-5.5V和STM32F1的3.3V电平兼容

这里重点说一下调试器的选择。很多人刚开始为了省钱用USB转TTL模块通过串口下载程序,但这种方式每次下载都要手动切换BOOT引脚,而且没法在线调试。ST-Link V2的价格现在也就十几块钱,支持SWD接口,可以直接在IDE里打断点、看变量、单步执行,调试效率完全不是一个级别。我强烈建议在这个环节不要省。

DHT11模块通常有三种形式:裸传感器、带PCB板载上拉电阻的模块、带LED指示灯的模块。建议直接买带PCB的模块,因为它已经集成了4.7kΩ到10kΩ的上拉电阻,省去了自己焊接的麻烦。裸传感器也不是不能用,但需要在数据线和VCC之间外接一个上拉电阻,否则信号可能不稳定。

2.2 开发环境搭建的两种路线

STM32F1的开发环境主要有两条路线:Keil MDK和STM32CubeIDE。两者各有优劣,选择哪个取决于你的具体需求。

Keil MDK是国内用户最多的嵌入式开发环境,它的优势是编译速度快、调试功能成熟、中文资料丰富。缺点是需要手动安装器件支持包,而且标准外设库的配置比较繁琐。如果你打算用标准外设库来开发,Keil是首选。

STM32CubeIDE是ST官方推出的免费IDE,基于Eclipse和GCC工具链。它的最大优势是集成了STM32CubeMX配置工具,可以通过图形化界面配置引脚、时钟、外设,然后自动生成初始化代码。对于新手来说,这种方式能大幅降低入门门槛。缺点是Eclipse的界面响应速度不如Keil流畅,编译大型工程时速度偏慢。

我个人的建议是:如果你之前没有嵌入式开发经验,直接从STM32CubeIDE入手,用HAL库开发。虽然HAL库的效率比标准库略低,但对于DHT11这种低速传感器来说完全没影响。如果你已经熟悉了标准库,继续用Keil加标准库也没问题,网上关于STM32F1标准库的例程更多。

2.3 工程创建的关键配置项

不管用哪种环境,创建工程时有几个配置项必须注意。

时钟配置方面,STM32F103C8T6的外部晶振通常是8MHz,经过PLL倍频到72MHz作为系统时钟。在CubeMX里配置时,HSE选择Crystal/Ceramic Resonator,PLL Source选择HSE,然后PLL MUL设置为9倍频,这样8MHz乘以9就是72MHz。APB1总线最高36MHz,APB2总线最高72MHz,这些在时钟树里都能直观看到。

调试接口配置方面,一定要在SYS选项卡里把Debug设置为Serial Wire,否则下载一次程序后SWD引脚可能被禁用,导致下次无法连接。这个坑我踩过不止一次,每次都要用BOOT0拉高的方式擦除芯片才能恢复。

GPIO配置方面,DHT11的数据引脚需要配置为开漏输出或者推挽输出,同时在读取时需要切换为输入模式。在CubeMX里可以先配置为GPIO_Output,然后在代码里手动切换模式。也可以配置为GPIO_Input,在需要输出时临时改为输出模式。两种方式都可以,我习惯用后者,因为DHT11的大部分时间都处于空闲状态,数据线由上拉电阻拉高。

串口配置方面,建议至少启用一个USART用于打印调试信息。波特率设置为115200,8位数据位,无校验,1位停止位。这样在调试DHT11时可以通过串口助手实时看到读取到的温湿度值,比用OLED显示更直观。

3. DHT11单总线协议深度解析

3.1 单总线通信的物理层原理

DHT11使用的单总线协议,顾名思义就是只用一根数据线完成双向通信。这根线同时承担了发送命令和接收数据的任务,所以通信过程必须严格分时复用。

总线空闲时,数据线由上拉电阻拉到高电平。主机和从机都通过拉低总线来发送信号,通过释放总线来让上拉电阻把电平拉高。这种通信方式对时序的要求非常严格,因为双方没有独立的时钟线来同步,全靠约定的时间长度来区分不同的信号。

DHT11的通信过程可以分为四个阶段:主机发送起始信号、从机响应信号、从机发送40位数据、总线恢复空闲。每个阶段的时间要求都不一样,下面逐一拆解。

3.2 起始信号与响应信号的时序要求

主机发送起始信号的流程是:先把数据线拉低至少18毫秒,然后释放总线拉高20到40微秒,最后切换为输入模式等待从机响应。

为什么是18毫秒?这是DHT11数据手册里明确规定的。如果拉低时间不够,DHT11可能检测不到起始信号;如果拉低时间过长,也不会损坏传感器,但会浪费时间。我一般用20毫秒,留一点余量。

从机检测到起始信号后,会等待主机释放总线,然后拉低总线80微秒作为响应信号,再拉高80微秒准备发送数据。这个80微秒的低电平加80微秒的高电平,就是DHT11的“存在脉冲”,告诉主机它已经准备好了。

这里有个容易出错的地方:主机在释放总线后,必须尽快切换为输入模式。如果还保持在输出模式,STM32的GPIO会继续驱动总线,和DHT11的输出产生冲突。严重的话可能损坏引脚,轻则导致通信失败。我在早期调试时因为忘了切换模式,折腾了大半天才找到原因。

3.3 40位数据的编码规则

DHT11发送的40位数据分为五个字节:第一个字节是湿度的整数部分,第二个字节是湿度的小数部分(DHT11固定为0),第三个字节是温度的整数部分,第四个字节是温度的小数部分(DHT11固定为0),第五个字节是校验和。

校验和的计算规则是前四个字节相加,取低8位。比如湿度整数35、湿度小数0、温度整数26、温度小数0,那么校验和就是35+0+26+0=61。如果读取到的校验和不是61,说明数据传输过程中出现了错误,需要重新读取。

每个数据位的表示方式是:先拉低总线50微秒,然后拉高总线。如果高电平持续26到28微秒,表示数据“0”;如果高电平持续70微秒,表示数据“1”。所以区分0和1的关键就是测量高电平的持续时间。

这个编码方式有个特点:每个数据位都以50微秒的低电平开始,所以位与位之间的间隔是固定的。接收方可以在检测到低电平后,延时40微秒左右再采样总线电平,如果此时是高电平就是1,低电平就是0。但更可靠的方式是测量高电平的持续时间,因为延时采样对时序的容错性较差。

3.4 微秒级延时的实现方案

DHT11的时序要求精确到微秒级,所以必须有一个可靠的微秒延时函数。常见的实现方式有三种。

第一种是用SysTick定时器。SysTick是Cortex-M3内核自带的一个24位递减计数器,可以配置为1微秒中断一次。在中断里维护一个全局变量,延时函数通过比较计数值来实现。这种方式精度高,不占用额外定时器资源,但需要处理好中断优先级。

第二种是用通用定时器,比如TIM2到TIM5。配置预分频器让计数器每微秒加一,然后通过读取计数值来实现延时。这种方式也很精确,但会占用一个定时器。

第三种是用空循环。通过实测调整循环次数来近似微秒延时。这种方式最简单,但精度受编译器优化等级和主频影响很大,换个芯片或者改个优化选项就可能不准。

我推荐用SysTick方案,因为它是内核自带的,不占用外设资源,而且精度足够。具体实现时,把SysTick配置为1微秒中断一次,在中断服务函数里对一个全局变量做递减操作。延时函数先设置这个变量为目标值,然后循环等待它减到零。注意这个变量要声明为volatile,防止编译器优化掉。

4. 完整代码实现与逐段解析

4.1 GPIO模式切换的封装

DHT11的数据引脚需要在输出和输入模式之间频繁切换,所以先封装两个函数来简化操作。

// 设置DHT11数据引脚为输出模式 void DHT11_SetPinOutput(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_PORT, &GPIO_InitStruct); } // 设置DHT11数据引脚为输入模式 void DHT11_SetPinInput(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT11_PORT, &GPIO_InitStruct); }

这里用推挽输出模式来驱动总线,因为需要主动拉低和拉高。输入模式配置了上拉电阻,这样总线空闲时会被拉到高电平。虽然DHT11模块上已经有上拉电阻了,但STM32内部的上拉可以起到辅助作用,增强信号稳定性。

4.2 起始信号的发送与响应检测

// 发送起始信号并等待DHT11响应 // 返回值:0-成功,1-超时失败 uint8_t DHT11_Start(void) { uint8_t retry = 0; // 配置为输出模式 DHT11_SetPinOutput(); // 拉低总线至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(20); // 释放总线,拉高20-40us HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(30); // 切换为输入模式,等待DHT11响应 DHT11_SetPinInput(); // 等待DHT11拉低总线(响应信号) while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { retry++; delay_us(1); if (retry > 100) return 1; // 超时 } // 等待DHT11释放总线 retry = 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET) { retry++; delay_us(1); if (retry > 100) return 1; // 超时 } // 等待DHT11再次拉低(准备发送数据) retry = 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { retry++; delay_us(1); if (retry > 100) return 1; // 超时 } return 0; }

这段代码里有三个等待循环,分别对应DHT11响应信号的三个阶段:拉低80微秒、拉高80微秒、再拉低准备发送数据。每个循环都设置了超时退出机制,防止DHT11没有响应时程序卡死。超时阈值设为100微秒,因为DHT11的响应信号最长也就80微秒左右,超过100微秒还没检测到变化就说明通信异常。

4.3 40位数据的读取与校验

// 读取一个字节(8位数据) uint8_t DHT11_ReadByte(void) { uint8_t i, byte = 0; for (i = 0; i < 8; i++) { // 等待50us低电平结束 while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); // 延时40us后采样 delay_us(40); // 读取数据位 byte <<= 1; if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { byte |= 1; } // 等待高电平结束 while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET); } return byte; } // 读取温湿度数据 // 返回值:0-成功,1-失败 uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i; if (DHT11_Start() != 0) return 1; for (i = 0; i < 5; i++) { buf[i] = DHT11_ReadByte(); } // 校验 if (buf[0] + buf[1] + buf[2] + buf[3] != buf[4]) { return 1; } *humi = buf[0]; *temp = buf[2]; return 0; }

读取字节的逻辑是:每个数据位都以低电平开始,所以先等待低电平结束。然后延时40微秒,此时如果数据位是“1”,高电平还在持续(70微秒),采样到高电平;如果数据位是“0”,高电平已经结束(26微秒),采样到低电平。采样完成后,再等待高电平结束,准备接收下一位。

这个40微秒的延时是关键。DHT11的“0”高电平持续26到28微秒,“1”高电平持续70微秒。40微秒正好在两者之间,所以能可靠区分。如果延时太短,比如20微秒,那么“0”和“1”都还在高电平,无法区分;如果延时太长,比如60微秒,“1”可能已经结束,导致误判为“0”。

4.4 主循环与串口输出

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); uint8_t temp, humi; char msg[64]; while (1) { if (DHT11_ReadData(&temp, &humi) == 0) { sprintf(msg, "Temp: %d C, Humi: %d %%\r\n", temp, humi); HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), 100); } else { HAL_UART_Transmit(&huart1, (uint8_t *)"Read Error\r\n", 12, 100); } HAL_Delay(2000); // DHT11采样间隔不能少于1秒 } }

主循环里每2秒读取一次DHT11,因为DHT11的采样周期不能少于1秒,否则内部可能还没完成上一次测量,导致数据不准确。串口输出格式用了sprintf来拼接字符串,注意缓冲区要足够大,64字节对于这个格式来说绰绰有余。

5. 常见问题排查与实战避坑指南

5.1 读取失败问题速查表

现象可能原因排查方法解决方案
始终返回超时接线错误检查VCC、GND、DATA是否接对重新接线,确认DHT11引脚定义
偶尔读取成功延时不准用示波器测量延时函数实际时间调整SysTick配置或改用定时器
数据校验失败时序偏差检查40us采样点是否准确微调延时值,尝试35-45us范围
温度湿度值异常供电不足测量DHT11的VCC电压换用独立3.3V或5V供电
上电后一直失败上拉电阻缺失检查数据线是否有上拉外接4.7kΩ-10kΩ上拉电阻

5.2 那些文档里不会写的实操经验

第一个坑是DHT11的上电等待时间。DHT11数据手册里写的是上电后需要等待1秒才能读取,但实际测试下来,有些批次的模块需要等待2秒甚至更久。如果你上电后立即读取,很可能会失败。我的做法是在初始化完成后加一个2秒的延时,然后再进入主循环。

第二个坑是中断对时序的干扰。DHT11的通信过程对时序要求很严格,如果在这期间发生了中断,而且中断服务函数执行时间较长,就可能导致时序错乱。我在调试时遇到过因为串口接收中断导致DHT11读取失败的情况。解决办法是在DHT11通信期间关闭全局中断,通信完成后再打开。但要注意关闭中断的时间不能太长,否则会影响其他外设的响应。

第三个坑是杜邦线的质量。DHT11和STM32之间的连接线如果太长或者质量太差,信号边沿会变缓,导致时序判断出错。我建议连接线不要超过20厘米,而且尽量用质量好一点的杜邦线。如果条件允许,直接把DHT11模块插在STM32最小系统板旁边,用短跳线连接。

第四个坑是DHT11的响应信号检测。有些代码在等待DHT11拉低总线时没有设置超时,如果DHT11没有接好或者损坏,程序就会卡死在while循环里。一定要加超时机制,这是保证系统稳定性的基本要求。

5.3 提升读取稳定性的几个技巧

第一个技巧是多次读取取有效值。DHT11偶尔会出现校验失败的情况,这是正常的。可以在代码里实现重试机制,如果第一次读取失败,隔100毫秒后再试一次,最多重试3次。这样能大幅降低读取失败的概率。

第二个技巧是软件滤波。如果连续读取到的温度值波动较大,可以做一个简单的滑动平均滤波。比如维护一个长度为5的数组,每次读取到新值后替换最旧的值,然后取平均值输出。这样显示出来的数据会平稳很多。

第三个技巧是电源去耦。在DHT11的VCC和GND之间并联一个0.1微法的陶瓷电容,可以滤除电源噪声,提高通信稳定性。这个电容在DHT11模块上通常已经集成了,但如果是裸传感器就需要自己加。

第四个技巧是避免频繁读取。DHT11的采样周期不能少于1秒,但也不建议读取太频繁。我一般设置2秒读取一次,这样既能及时反映环境变化,又不会给传感器太大压力。如果是电池供电的应用,可以设置更长的间隔,比如5秒或10秒。

6. 从DHT11延伸到更多传感器应用

6.1 单总线协议的通用性

DHT11使用的单总线协议虽然简单,但它的核心思想——通过时间长度来编码信息——在很多传感器中都有应用。比如DS18B20温度传感器也是单总线协议,通信流程和DHT11类似,只是数据格式和时序参数不同。掌握了DHT11的驱动方法后,再去看DS18B20的代码会发现很多相似之处。

再比如红外遥控接收头,它输出的也是通过高低电平持续时间来编码的信号。虽然协议细节不同,但处理思路是一样的:检测边沿、测量时间、解码数据。所以DHT11这个项目虽然简单,但它训练的是嵌入式开发中最基础也最重要的能力——对时序的精确控制。

6.2 多传感器融合的扩展思路

在实际项目中,DHT11往往不是单独使用的。比如做一个环境监测节点,可能还需要光照传感器、气压传感器、空气质量传感器等。STM32F1的I2C和SPI接口可以方便地连接这些传感器。

以I2C为例,STM32F1的I2C1可以挂载多个从设备,每个设备有独立的地址。比如OLED显示屏地址通常是0x78,BMP280气压传感器地址是0x76或0x77,BH1750光照传感器地址是0x23。只要地址不冲突,就可以共用一条I2C总线。这样整个系统只需要两根线就能连接多个传感器,接线非常简洁。

如果传感器数量更多,还可以考虑用SPI接口。SPI的速度比I2C快,但需要更多的引脚。STM32F1的SPI1和SPI2可以分别连接不同的设备,通过片选引脚来区分。

6.3 数据上云与远程监控

DHT11采集到的温湿度数据,除了本地显示,还可以通过无线模块上传到服务器。常见的方案是加一个ESP8266或ESP32模块,通过串口和STM32F1通信。STM32F1负责采集传感器数据,ESP模块负责网络通信,两者分工明确。

具体实现时,STM32F1通过USART发送AT指令给ESP模块,ESP模块连接到WiFi后,通过MQTT协议把数据发布到服务器。服务器端可以用各种方式接收和存储数据,比如用数据库记录历史数据,用Web页面展示实时曲线。

这个方案的好处是STM32F1的负担很轻,只需要处理传感器读取和串口通信,复杂的网络协议栈由ESP模块处理。而且ESP模块的价格也很低,整体成本增加不多。

6.4 低功耗设计的考虑

如果项目是电池供电的,低功耗就是一个必须考虑的问题。STM32F1支持多种低功耗模式,包括睡眠模式、停止模式和待机模式。对于DHT11这种间歇性工作的传感器,可以在两次读取之间让MCU进入停止模式,把功耗降到微安级别。

具体做法是:读取完DHT11数据后,关闭不必要的外设时钟,配置一个定时器或RTC在指定时间后唤醒MCU,然后进入停止模式。唤醒后重新初始化时钟和外设,再次读取DHT11。这样平均功耗可以做到很低,用两节AA电池就能运行几个月。

不过要注意的是,DHT11本身的工作电流在测量时大约1毫安,待机时大约60微安。如果对功耗要求特别严格,可以考虑用更低功耗的传感器,比如SHT30,它的待机电流只有0.2微安。但SHT30是I2C接口,价格也比DHT11贵不少,需要根据具体需求权衡。

7. 调试工具与效率提升方法

7.1 逻辑分析仪的妙用

调试DHT11这种时序敏感的传感器,逻辑分析仪是神器。它可以把数据线上的电平变化完整记录下来,然后以波形图的形式展示。你可以直观地看到起始信号的拉低时间、DHT11的响应脉冲、每个数据位的高电平持续时间,从而判断时序是否符合预期。

市面上几十块钱的八通道逻辑分析仪就够用了,配套的软件可以设置采样率、触发条件、协议解码。有些软件甚至自带DHT11协议解码器,能直接把波形翻译成温湿度值,非常方便。

如果没有逻辑分析仪,用示波器也可以,但示波器通常只有两到四个通道,而且存储深度有限,不如逻辑分析仪适合看长时间的时序。不过示波器看信号质量更直观,比如上升沿是否陡峭、有没有振铃等。

7.2 串口打印的调试技巧

串口打印是最基础的调试手段,但用好了效率也很高。我习惯在DHT11驱动的关键步骤加上打印语句,比如起始信号发送完成、收到响应、每个字节读取完成等。这样如果通信失败,能快速定位到是哪一步出了问题。

但要注意的是,串口打印本身会消耗时间,如果在DHT11通信过程中插入打印语句,可能会干扰时序。所以调试完成后要把这些打印语句删掉或者用宏定义控制编译。我一般用条件编译来实现:

#define DHT11_DEBUG 0 #if DHT11_DEBUG #define DHT11_LOG(fmt, ...) printf(fmt, ##__VA_ARGS__) #else #define DHT11_LOG(fmt, ...) #endif

这样在调试时把DHT11_DEBUG设为1,发布时设为0,既方便调试又不影响最终代码。

7.3 用OLED实时显示数据

除了串口输出,用OLED显示屏实时显示温湿度也很实用。0.96寸的I2C OLED价格便宜,接线简单,只需要SCL和SDA两根线。显示内容可以包括温度、湿度、读取状态等。

OLED的驱动库推荐用u8g2或者SSD1306的专用库。u8g2功能强大,支持多种字体和图形,但占用Flash较多。SSD1306的库更轻量,适合资源紧张的STM32F103C8T6。如果Flash空间不够,可以只显示数字和简单字符,用最小的字体。

显示刷新频率不用太高,和DHT11的读取频率一致就行,每2秒更新一次。这样既不会闪烁,也不会占用太多CPU时间。

8. 项目扩展与进阶方向

8.1 加入数据记录功能

如果想把温湿度数据保存下来,可以加一个SD卡模块或者EEPROM芯片。SD卡通过SPI接口连接,可以存储大量数据,适合长时间记录。EEPROM比如AT24C02通过I2C接口连接,容量小但读写简单,适合存储配置参数和少量历史数据。

数据记录格式可以用CSV,每行包含时间戳、温度、湿度。时间戳可以用RTC实时时钟提供,STM32F1内部有RTC外设,配合32.768kHz晶振可以保持准确的时间。如果没有RTC,也可以用开机后的运行时间作为相对时间戳。

8.2 添加报警功能

环境监测系统通常需要报警功能,比如温度超过阈值时触发蜂鸣器或者发送通知。STM32F1的定时器可以输出PWM驱动蜂鸣器,GPIO可以驱动LED指示灯。报警阈值可以通过按键设置,存储在EEPROM里掉电不丢失。

如果需要远程报警,可以结合前面提到的无线模块,通过MQTT发布报警消息。服务器端收到消息后可以推送通知到手机或者触发其他动作。

8.3 多节点组网监测

单个DHT11的监测范围有限,如果要覆盖更大的区域,可以部署多个节点,每个节点用一个STM32F1加一个DHT11,通过无线模块组网。常见的组网方式有WiFi、蓝牙、ZigBee、LoRa等。

WiFi适合有现成路由器的场景,成本低但功耗高。LoRa适合远距离低功耗的场景,传输距离可达几公里,但需要额外的网关。ZigBee适合自组网多跳传输,但协议栈复杂,开发门槛较高。

对于入门级的组网需求,我建议先用WiFi方案,因为ESP8266模块便宜且资料多。每个节点通过WiFi连接到同一个路由器,然后通过MQTT协议把数据发布到服务器。服务器端用一个简单的程序订阅所有节点的主题,就能汇总显示所有节点的数据。

8.4 从裸机到RTOS的升级

当项目功能越来越复杂,裸机的前后台架构可能会变得难以维护。这时候可以考虑引入RTOS,比如FreeRTOS。FreeRTOS在STM32F1上运行很流畅,RAM占用也不大。

用RTOS的好处是可以把不同的任务分开,比如DHT11读取任务、显示刷新任务、通信任务、报警检测任务等。每个任务独立运行,通过队列或信号量通信,代码结构更清晰。而且RTOS的延时函数会自动让出CPU,不会像裸机那样空转等待。

不过RTOS也带来了新的复杂性,比如任务优先级设置、栈大小分配、资源共享等问题。如果项目规模不大,裸机架构反而更简单直接。我的建议是先用裸机把功能跑通,确实觉得维护困难了再考虑上RTOS。

9. 我在这类项目里踩过的坑和总结的经验

9.1 时序调试的耐心比技术更重要

DHT11的驱动代码本身并不复杂,但调试过程往往很磨人。有时候代码看起来完全正确,但就是读不出数据。这时候不要急着改代码,先用逻辑分析仪或者示波器看看实际波形,确认硬件层面的时序是否正确。我遇到过好几次是因为杜邦线接触不良导致信号时有时无,换了线就好了。

另外,DHT11的时序参数在不同批次之间可能有细微差异。数据手册上写的是典型值,实际模块可能需要微调。比如40微秒的采样延时,如果读出来全是0或者全是1,可以尝试在35到45微秒之间调整,找到最稳定的值。

9.2 代码的健壮性要从一开始就考虑

很多人在写DHT11驱动时只考虑正常情况,没有加超时和重试机制。结果一旦传感器没接好或者出现干扰,程序就卡死了。我的习惯是每个等待循环都加超时,每次读取失败都重试,最多重试3次。这样即使偶尔出现异常,系统也能自动恢复。

还有一点是校验和检查。DHT11的校验和虽然简单,但能有效发现传输错误。如果校验失败,直接丢弃这次数据,不要试图“修正”。因为温湿度数据变化很慢,丢一次数据对整体影响不大,但用错误的数据可能导致误判。

9.3 文档和注释要写给自己看

嵌入式项目的代码往往过几个月就要修改或者移植,如果当时没有写清楚注释,回头再看就要花很多时间重新理解。我习惯在DHT11驱动的每个函数前面写清楚功能、参数、返回值,在关键时序的地方标注时间要求。比如“拉低至少18ms”这样的注释,能提醒自己不要随意改动。

另外,把硬件接线方式也记录在代码注释里,比如“DHT11 DATA接PA0,VCC接3.3V,GND接GND”。这样换一块板子或者重新接线时,不用翻原理图就能快速恢复。

9.4 从简单项目里学到的通用能力

DHT11这个项目看起来很简单,但它涵盖的知识点很全面:GPIO输入输出模式切换、微秒级延时实现、单总线协议解析、校验和计算、串口通信、状态机设计、超时处理、重试机制。这些能力在后续做更复杂的项目时都会用到。

比如GPIO模式切换,在驱动其他单总线设备或者模拟时序协议时都会用到。微秒级延时,在驱动WS2812灯带或者红外发射时也是基础。校验和计算,在通信协议里随处可见。所以不要觉得DHT11太简单就不认真做,把每个细节都搞清楚,后面做复杂项目时就能少走很多弯路。

9.5 关于选型和替代方案的思考

虽然DHT11很便宜,但它的精度和响应速度确实有限。如果项目对测量精度有要求,比如需要0.5摄氏度以内的精度,DHT11就不够用了。这时候可以考虑DHT22,它的精度是±0.5摄氏度,价格比DHT11贵一些但仍在可接受范围内。

如果需要更快的响应速度,比如每秒读取多次,DHT11的1秒采样周期就不满足要求。这时候可以选SHT30或者SHT31,它们通过I2C接口通信,采样速率可以到每秒10次以上,而且精度更高。当然价格也更高,需要根据项目预算和需求来权衡。

我的建议是:学习阶段用DHT11完全够用,因为它的协议简单,适合理解单总线通信的原理。实际产品中如果对精度和速度有要求,再换更高级的传感器。STM32F1的代码框架不用大改,只需要替换传感器驱动部分即可。

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

I3C波形抓取与总线解码指南:示波器、逻辑分析仪实战要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 9:15:28

电机驱动器PWM啸叫抑制:硬件与软件降噪实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 9:13:34

C51单片机Proteus仿真源码300例:信号链验证工程包

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 9:13:31

只跑Vectorless不够?VCD驱动Dynamic IR Drop实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

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

STM32嵌入式开发从入门到精通:内核、时钟、外设与实战避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

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

嵌入式Linux驱动开发:从能跑通到能扛项目的关键进阶指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华