☰
STM32F1与DHT11温湿度传感器实战:从硬件连接到驱动开发
2026/10/27 9:19:40 网站建设 项目流程

1. 为什么STM32F1至今仍是入门嵌入式的首选

如果你在电子论坛或者技术群里问一句“新手该从哪款单片机入手”,十个人里至少有六个会告诉你:STM32F1系列。这个答案从十年前一直延续到今天,多少有点反直觉——毕竟现在Cortex-M4、M7甚至带NPU的芯片满天飞,一颗主频只有72MHz、Flash最大也就512KB的老将,凭什么还能占据入门市场的半壁江山?

答案其实不复杂。STM32F1系列(典型型号如STM32F103C8T6、STM32F103ZET6)是ST在2007年前后推出的基于ARM Cortex-M3内核的通用MCU,它的定位就是“够用、便宜、资料多”。72MHz主频、64KB到512KB Flash、20KB SRAM、丰富的外设(USART、SPI、I2C、ADC、定时器、CAN),这套配置放在今天做温湿度采集、电机控制、简单人机交互、数据记录仪这类任务,依然绰绰有余。更关键的是,它的中文资料、开源例程、开发板生态极其庞大,你遇到的几乎每一个问题,都有人踩过坑并留下了解决方案。

我这次要聊的,就是围绕STM32F1系列做的一个典型小项目:用DHT11温湿度传感器采集环境数据,通过STM32F103读取并在串口或OLED上显示。这个组合看起来简单,但真正动手做的时候,从供电、时序、数据校验到代码结构,处处都是细节。很多新手卡在“读出来全是0”或者“湿度值乱跳”上,一卡就是好几天。这篇文章我会把整个链路拆开,从选型理由、硬件连接、DHT11单总线协议解析、代码实现、常见故障排查,到实际调试中的经验技巧,全部讲透。无论你是刚接触STM32的新手,还是想复习一下单总线驱动写法的老手,都能从里面找到能直接用的东西。

2. DHT11与STM32F1的硬件搭配细节

2.1 DHT11到底是个什么样的传感器

DHT11是一款含有已校准数字信号输出的温湿度复合传感器,内部由一个电阻式感湿元件和一个NTC测温元件组成,并连接到一个8位单片机。它的测量范围是温度0到50摄氏度,精度正负2摄氏度;湿度20%到90%RH,精度正负5%RH。采样周期不低于1秒,也就是说你最快每秒读一次,再快它也不会给你新数据。

它的通信方式叫“单总线”,一根数据线既传命令又传数据,双向半双工。这个设计的好处是省引脚,坏处是对时序要求极其严格。DHT11的数据线需要外接一个4.7k到10k的上拉电阻到3.3V或5V,因为它的输出是开漏结构,没有上拉电阻的话总线拉不高,读出来全是0。

很多人第一次用DHT11会犯一个错误:以为它和I2C传感器一样,接上SCL、SDA就能用。实际上DHT11只有一根DATA线,而且它对上拉电阻的阻值、线长、电源质量都很敏感。线太长、上拉太大、电源纹波大,都会导致通信失败。

2.2 STM32F103与DHT11的引脚连接方案

以最常见的STM32F103C8T6最小系统板为例,我一般把DHT11的DATA脚接到PA0或者PB12这类普通GPIO上。为什么不接带特殊功能的引脚?因为DHT11的时序需要精确的微秒级延时,用普通GPIO配合__NOP()或者定时器做延时最可控。如果接到I2C或SPI的复用引脚上,虽然也能用,但初始化时容易忘记关闭复用功能,导致电平异常。

供电方面,DHT11可以接3.3V也可以接5V。接3.3V时,STM32的GPIO识别高电平没问题;接5V时,如果STM32的引脚不是5V容忍的,就需要加电平转换。STM32F103的大部分GPIO是5V容忍的(标注FT的引脚),但为了省事,我建议直接接3.3V,这样总线和MCU同电平,最稳定。

实际接线就三根:VCC接3.3V,GND接GND,DATA接PA0,然后在DATA和VCC之间并一个4.7k电阻。如果你用的是模块(比如那种三针的DHT11模块),板上通常已经集成了上拉电阻,直接插就行。但要注意,有些廉价模块的上拉电阻是10k,线长超过20厘米时可能拉高不够快,读数据会失败。这种情况可以把上拉换成4.7k,或者缩短线长。

2.3 电源与去耦的实战经验

DHT11虽然功耗不高,但它对电源噪声比较敏感。我遇到过好几次这样的情况:用USB供电时读数据正常,一换成电池供电就开始乱跳。后来用示波器看,发现电池供电时3.3V轨上有几十毫伏的纹波,正好落在DHT11的敏感频段。

解决办法很简单:在DHT11的VCC和GND之间并一个0.1uF的陶瓷电容,位置尽量靠近传感器引脚。如果条件允许,再并一个10uF的钽电容。这样能把电源纹波压下去,读数稳定性会明显提升。另外,STM32F103的VDDA和VSSA也要做好滤波,虽然DHT11不直接依赖ADC,但电源干净对整个系统都有好处。

还有一点:DHT11的DATA线如果走线较长,比如超过半米,最好用屏蔽线,或者在外面套一个磁环。我在一个工业环境的小项目里,DHT11的线拉了1.5米,结果每次电机启动时湿度就跳变。后来加了磁环并缩短到0.5米,问题才解决。所以如果你要做产品,传感器尽量靠近MCU,别为了布线方便把线拉太长。

3. 单总线协议里那些容易读错的时间窗口

3.1 一次完整通信的时序拆解

DHT11的通信过程分四步:主机发送起始信号、DHT11响应、传输40位数据、总线释放。每一步的时间窗口都很窄,差几微秒就可能读错。

主机起始信号:MCU把DATA线拉低至少18毫秒,然后拉高20到40微秒,接着释放总线(设为输入模式)。DHT11检测到这个起始信号后,会等待20到40微秒,然后把总线拉低80微秒作为响应信号,再拉高80微秒,之后开始传输数据。

40位数据里,前16位是湿度整数和小数,中间16位是温度整数和小数,最后8位是校验和。校验和等于前四个字节相加的低8位。如果校验失败,说明传输过程中有误码,这次数据就不能用。

每一位数据的表示方式:每bit以50微秒的低电平开始,然后是26到28微秒的高电平表示“0”,70微秒的高电平表示“1”。所以判断0和1的关键,就是测量高电平的持续时间。这个时间窗口很窄,如果用软件延时来采样,必须保证延时函数的精度。

3.2 微秒延时为什么不能用HAL_Delay

HAL库的HAL_Delay()是基于SysTick的毫秒级延时,最小分辨率是1毫秒。而DHT11的时序需要微秒级判断,比如区分26微秒和70微秒,用毫秒延时根本做不到。

正确的做法是用__NOP()指令做空循环延时,或者用定时器做微秒级计时。以STM32F103主频72MHz为例,一个__NOP()大约14纳秒(1/72MHz),循环几十次就能得到微秒级延时。但要注意,编译器优化等级会影响__NOP()的实际执行时间。我一般把延时函数写成__attribute__((optimize("O0"))),禁止编译器优化,保证每次循环时间一致。

更稳妥的方案是用一个基本定时器(比如TIM4)做一个微秒计数器。初始化时把预分频设为71,这样计数器每加1就是1微秒。需要延时时读取当前计数值,循环等待直到达到目标值。这种方法不受编译器优化影响,精度也更高。我在实际项目中更倾向用定时器方案,因为一旦调通,换芯片或换主频时只需要改预分频值,延时逻辑不用动。

3.3 读取数据时的临界区保护

DHT11的读取过程需要连续几十毫秒不能被打断。如果在这期间发生了中断,比如SysTick中断或者串口接收中断,时序就会被破坏,读出来的数据大概率是错的。

所以读取DHT11时,我一般会在起始信号之前关闭全局中断,读完40位数据后再打开。用__disable_irq()和__enable_irq()这对函数就行。但要注意,关中断的时间不能太长,否则会影响其他实时任务。DHT11一次读取大约需要4到5毫秒(起始18毫秒加响应和数据传输),关中断5毫秒对大多数应用来说是可以接受的。如果你的系统里有对中断延迟要求极高的任务,那就需要考虑用DMA或者硬件外设来辅助,但DHT11这种单总线协议很难用硬件外设直接支持,所以关中断仍是最简单的方案。

还有一个细节:关中断之前要确保SysTick的计数不会溢出。如果关中断时间超过1毫秒,SysTick的毫秒计数会丢,导致HAL_GetTick()返回值偏小。对于DHT11这种5毫秒左右的关中断,影响不大,但如果你的程序依赖精确的毫秒计时,就要在关中断后手动补偿。

4. 从零写一个稳定的DHT11驱动

4.1 GPIO模式切换的封装

DHT11的DATA线需要在输出和输入之间切换。输出时用于主机拉低总线,输入时用于读取DHT11的响应和数据。STM32的GPIO模式切换需要配置CRL或CRH寄存器,用HAL库的话就是HAL_GPIO_Init()。

我一般封装两个函数:DHT11_SetPinOutput()和DHT11_SetPinInput()。输出模式配置为推挽输出、无上拉、50MHz速度;输入模式配置为浮空输入或上拉输入。这里有个坑:如果配置成上拉输入,而外部已经有4.7k上拉电阻,内部上拉会并联,导致上拉阻值变小,拉高更快,但也会增加功耗。一般用浮空输入就行,外部上拉已经够了。

切换模式时要注意,先设置模式再操作电平,否则可能出现短暂的输出冲突。比如从输入切到输出时,如果ODR寄存器里还是上次的值,可能会瞬间输出高电平或低电平。我习惯在切到输出模式之前,先把ODR对应的位设成1(高电平),这样切过去之后总线不会被意外拉低。

4.2 起始信号与响应检测的代码实现

起始信号的代码逻辑:把DATA脚设为输出,拉低至少18毫秒,然后拉高20到40微秒,再设为输入,等待DHT11拉低。等待的时候要加超时,防止DHT11没接好导致死循环。

void DHT11_Start(void) { DHT11_SetPinOutput(); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); Delay_ms(20); // 拉低至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); Delay_us(30); // 拉高20-40us DHT11_SetPinInput(); }

响应检测:DHT11会先拉低80微秒,再拉高80微秒。我们需要在设为输入后,等待总线变低,再等待总线变高。如果超时还没等到,就返回错误。

uint8_t DHT11_WaitResponse(void) { uint32_t timeout = 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { if (++timeout > 10000) return 1; // 超时 } timeout = 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET) { if (++timeout > 10000) return 1; } return 0; }

这里的超时计数用简单的循环变量就行,因为每次循环大约几百纳秒,10000次差不多几毫秒,足够覆盖80微秒的响应时间。

4.3 40位数据的逐位读取与校验

读取每一位数据的逻辑:先等待50微秒的低电平结束,然后延时30微秒后采样。如果此时是高电平,说明是“1”;如果是低电平,说明是“0”。为什么延时30微秒?因为“0”的高电平只有26到28微秒,30微秒后已经变低;而“1”的高电平有70微秒,30微秒时还是高。

uint8_t DHT11_ReadByte(void) { uint8_t byte = 0; for (int i = 0; i < 8; i++) { while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); // 等待低电平结束 Delay_us(30); 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; }

读完5个字节后,计算校验和:if (data[4] == (data[0] + data[1] + data[2] + data[3])),相等则数据有效。注意DHT11的小数字节通常为0,所以湿度就是data[0],温度就是data[2]。

4.4 数据滤波与异常值处理

即使代码写对了,DHT11偶尔也会返回异常值,比如湿度突然跳到90%又跳回来。这可能是电源波动或电磁干扰导致的。我一般会在应用层加一个简单的滑动平均滤波:连续读5次,去掉一个最大值和一个最小值,剩下的取平均。这样能过滤掉大部分偶发跳变。

另外,如果校验失败,不要立刻丢弃,可以重试一次。DHT11的采样周期是1秒,所以重试间隔至少1秒。我通常的做法是:第一次读失败,等1秒再读一次;如果还失败,就上报错误,等下一次周期再试。不要连续快速重试,因为DHT11内部需要时间完成一次测量。

5. 调试过程中最常遇到的五个坑

5.1 读出来全是0或者全是1

这是最典型的问题。原因通常有三个:上拉电阻没接、DATA线接错引脚、或者延时函数不准。先检查硬件:用万用表量DATA线在空闲时是不是3.3V,如果不是,说明上拉没起作用。再检查代码:确认DHT11_PIN和DHT11_PORT定义正确,别把PA0写成PA1。

如果硬件没问题,那就是延时函数的问题。用示波器看起始信号的低电平时间是不是18到20毫秒,高电平是不是20到40微秒。如果低电平只有几毫秒,说明Delay_ms(20)实际没延时那么久,可能是系统时钟配置错了。STM32F103默认用内部8MHz RC振荡器,如果没配置PLL到72MHz,HAL_Delay()的毫秒数会差很多。

5.2 湿度值偶尔跳到90%以上

DHT11的湿度测量范围是20%到90%,超过90%时它可能输出90%或者乱码。如果你在潮湿环境(比如浴室、温室)使用,读到90%以上是正常的,但如果是干燥环境突然跳到90%,那就是干扰。

我遇到过一次,DHT11和电机共用一组电源,电机启动时湿度就跳到90%。后来给DHT11单独加了一个LDO供电,问题消失。所以如果系统里有大功率负载,传感器供电一定要隔离或者加滤波。

5.3 校验和总是对不上

校验和错误说明数据传输过程中有误码。最常见的原因是读取过程中被中断打断。检查一下有没有开启SysTick以外的中断,比如串口接收中断、定时器中断。如果有,在读取DHT11之前关中断,读完再开。

另一个原因是延时函数的精度不够。比如用HAL_Delay(1)来延时1毫秒,但实际可能延时了1.2毫秒,累积起来就会导致采样点偏移。建议用定时器做微秒延时,或者用__NOP()循环并校准。

5.4 换一块板子就读不出来了

这通常是因为不同板子的晶振频率或PLL配置不同。比如一块板子用8MHz晶振,另一块用12MHz,如果延时函数是按72MHz写的,换到另一块板子上主频可能不是72MHz,延时就不准了。解决办法是在SystemClock_Config()里确认系统时钟频率,并让延时函数根据SystemCoreClock变量动态计算循环次数。

还有一种情况:不同批次的DHT11响应时间略有差异。有的DHT11响应快,有的慢。如果代码里的超时阈值太紧,换一个传感器就可能超时。建议把超时阈值放宽一些,比如等待响应的循环次数从10000增加到20000。

5.5 用HAL库的GPIO读写太慢

HAL库的HAL_GPIO_ReadPin()和HAL_GPIO_WritePin()函数有函数调用开销,在72MHz下每次调用可能消耗几百纳秒。对于DHT11这种微秒级时序,几百纳秒的偏差累积起来可能影响判断。如果发现读数据不稳定,可以直接操作寄存器:用GPIOA->IDR读输入,用GPIOA->BSRR写输出。这样能省掉函数调用,时序更准。

比如读PA0:if (GPIOA->IDR & GPIO_PIN_0);拉低PA0:GPIOA->BRR = GPIO_PIN_0;拉高PA0:GPIOA->BSRR = GPIO_PIN_0。直接操作寄存器在STM32F1上很常见,尤其是对时序敏感的场景。

6. 把数据用起来:串口打印与OLED显示

6.1 串口重定向printf的配置

读出温湿度后,最方便的查看方式就是串口打印。STM32F1的USART1接PA9和PA10,配置成115200波特率、8位数据、无校验、1位停止位。要让printf()输出到串口,需要重定向fputc()函数:

int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }

然后在Keil或STM32CubeIDE里勾选“Use MicroLIB”,否则printf()可能无法正常工作。打印格式可以写成:printf("Temp: %d C, Humi: %d %%\r\n", temp, humi);。注意DHT11的温度和湿度都是整数,不需要小数点。

6.2 OLED显示温湿度的页面布局

如果你用的是0.96寸I2C OLED(SSD1306驱动),可以用现成的库(比如u8g2或自己写的驱动)。显示内容我一般分三行:第一行显示“DHT11 Sensor”,第二行显示“Temp: 25 C”,第三行显示“Humi: 60 %”。字体用6x8或8x16都行,8x16更清晰但占空间。

刷新频率不要太高,DHT11本身1秒才更新一次数据,OLED每秒刷新一次就够了。如果刷新太快,OLED的I2C通信会占用CPU时间,反而影响DHT11的读取时序。我一般把OLED刷新放在DHT11读取之后,两者间隔几百毫秒,避免相互干扰。

6.3 数据记录与阈值报警的扩展思路

如果你想把数据记录下来,可以加一个SD卡模块,用SPI接口,每隔一分钟把温湿度写入CSV文件。或者用STM32F1的RTC做时间戳,记录每次采集的时间。阈值报警也很简单:当温度超过30度或湿度低于30%时,点亮一个LED或者让蜂鸣器响。这些扩展都不复杂,核心还是先把DHT11的读取做稳定。

我在一个温室监控项目里,用STM32F103加DHT11和ESP8266做了个简单的物联网节点。DHT11每5秒读一次,数据通过串口发给ESP8266,再上传到服务器。运行了半年多,除了偶尔因为电源波动导致一次校验失败外,整体很稳定。关键就是电源滤波和关中断保护做到位了。

7. 关于STM32F1与DHT11组合的个人体会

这个组合我用了很多年,从学生时代的课程设计到后来的小批量产品,DHT11和STM32F103的搭配几乎没让我失望过。它不高端,但足够可靠;它不复杂,但细节很多。每次有人问我“为什么我的DHT11读不出来”,我都能从上面那些坑里找到对应的原因。

如果你刚开始接触STM32,我建议就从DHT11这个项目入手。它涉及GPIO输入输出切换、微秒延时、单总线协议解析、校验和计算、串口通信,几乎涵盖了嵌入式开发的基础技能。把这个项目跑通,你对STM32的GPIO和定时器会有完全不一样的理解。

最后分享一个小技巧:在DHT11的DATA线和GND之间并一个100pF的电容,能滤掉高频噪声,对读数稳定性有帮助。这个电容不要太大,否则会延缓边沿,导致时序判断出错。100pF到1nF之间比较合适,具体值可以根据实际布线调整。我在几个干扰较大的环境里试过,加了之后校验失败率明显下降。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询