news 2026/10/28 9:09:11

STM32F1驱动DHT11实战:单总线时序与GPIO模式切换详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F1驱动DHT11实战:单总线时序与GPIO模式切换详解

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

1.1 一颗老芯片的生存逻辑

STM32F1系列是ST在2007年前后推出的基于ARM Cortex-M3内核的32位微控制器,代表作STM32F103C8T6,也就是圈子里常说的“蓝板”“最小系统板”。十几年过去了,国产替代、RISC-V、各种新系列层出不穷,但你去电子市场转一圈,F103的出货量依然稳得离谱。原因不复杂:便宜、资料多、生态成熟、供货稳定,而且它的性能覆盖了绝大多数入门到中端的控制场景——跑个温湿度采集、驱动个屏幕、做个电机控制、搞个串口网关,绰绰有余。

我接触F1系列是从一块十几块钱的最小系统板开始的,当时想的是“先点个灯再说”。结果一路做下来,从GPIO到定时器、从ADC到I2C、从串口中断到DMA搬运,几乎每一个外设都在F1上踩过坑。后来换过F4、换过G0、换过国产的GD32,但每次遇到新项目要快速验证一个想法,手边最顺手的还是那块F103。这不是情怀,是效率——你不需要翻半天参考手册就能把外设配起来,社区里随便一搜就有现成代码,出了问题也大概率有人遇到过。

这篇文章我想聊的不是“F1有多好”,而是围绕一个非常具体的场景——用STM32F1驱动DHT11温湿度传感器——把F1系列的核心使用逻辑、外设配置思路、实操细节和常见坑一次性讲透。DHT11这个传感器便宜、简单、单总线协议,看起来没什么技术含量,但恰恰因为它“简单”,很多人第一次做的时候反而容易翻车:时序对不上、读出来全是0、偶尔成功偶尔失败、换块板子就不行了。这些问题背后其实是对F1的GPIO模式、延时精度、中断优先级、电源噪声等基础问题的理解不到位。

所以这篇文章适合谁看?如果你是刚拿到F103最小系统板的新手,想找一个完整的、能跑通的项目来练手,那DHT11是很好的起点。如果你已经用过F1但总觉得“能用但不知道为什么”,那这篇文章会帮你把底层逻辑补齐。如果你是要做产品原型的工程师,需要快速评估F1能不能扛住你的需求,这里也有实际的参数和踩坑记录可以参考。

1.2 DHT11为什么成了F1的经典搭档

DHT11是一款含有已校准数字信号输出的温湿度复合传感器,测量范围温度0到50摄氏度、湿度20%到90%RH,精度分别是正负2摄氏度和正负5%RH。它的通信方式是单总线,一根数据线完成所有交互,不需要额外的时钟线。这个特性让它特别适合和F1搭配:F1的GPIO资源丰富,随便找一个空闲引脚就能用;单总线协议对时序要求虽然严格,但F1的主频(通常72MHz)完全能通过软件延时精确控制微秒级时序。

更重要的是,DHT11的价格极低,模块形式通常自带上拉电阻和滤波电容,直接插杜邦线就能用。对于学习F1的GPIO输入输出切换、微秒延时、中断与轮询的取舍来说,这是一个成本几乎可以忽略的实验平台。你不需要担心烧坏传感器,也不需要复杂的电路设计,重点全在代码和时序上。

但“简单”不等于“随便写写就能跑”。我见过太多人卡在第一步:按照网上的代码抄了一遍,串口打印出来永远是“读取失败”或者“温度0湿度0”。然后开始怀疑传感器坏了、板子坏了、杜邦线坏了,换了一圈发现还是不行。问题往往出在几个非常具体的地方:GPIO模式切换的时机不对、延时函数在72MHz下的实际精度不够、没有正确处理DHT11的响应信号、电源纹波导致传感器复位、或者中断打断了关键时序。这些细节在数据手册里不会手把手教你,但在实际调试中每一个都能让你卡半天。

2. 核心原理拆解:单总线时序与F1的GPIO控制

2.1 DHT11的单总线协议到底在干什么

DHT11的通信过程可以拆成四个阶段:主机发送起始信号、传感器响应、数据传输、结束释放。整个过程围绕一根数据线展开,这根线在空闲时被上拉电阻拉高,主机和传感器通过拉低或释放来传递信息。

起始信号是主机主动拉低数据线至少18毫秒,然后释放。这个18毫秒很关键,它告诉传感器“我要开始读数据了”。传感器检测到这个低电平后,会等待20到40微秒,然后拉低总线80微秒作为响应,再拉高80微秒,表示“我准备好了”。接下来传感器连续输出40位数据,每一位都以50微秒的低电平开始,然后用高电平的持续时间来区分0和1:高电平持续26到28微秒表示0,持续70微秒表示1。40位数据依次是湿度整数、湿度小数、温度整数、温度小数、校验和,校验和等于前四个字节相加的低八位。

整个过程对时序的要求是微秒级的。F1在72MHz下,一个机器周期大约是13.9纳秒,一条简单的指令几个周期,所以用软件延时循环来产生微秒级延时是完全可行的。但前提是你得知道你的延时函数在72MHz下到底延时了多久,而不是随便写个for循环就觉得“差不多”。

2.2 F1的GPIO模式切换为什么容易出错

F1的GPIO有八种模式:输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。在DHT11的应用中,数据线需要在输出和输入之间动态切换。主机发送起始信号时,GPIO配置为推挽输出,可以主动拉低和拉高;发送完起始信号后,GPIO要切换为输入模式(通常是浮空输入或上拉输入),让传感器接管总线。

问题就出在这个切换上。很多人在初始化时把GPIO配成推挽输出,发送完起始信号后忘记切换成输入,结果传感器拉低总线时,主机的推挽输出还在驱动高电平,两边打架,读出来的数据全是乱的。另一种情况是切换了但没切换干净,比如只改了CRL寄存器的某几位,导致模式处于不确定状态。

正确的做法是写一个GPIO模式切换函数,在需要输出时调用一次,需要输入时再调用一次。切换的时候要注意F1的GPIO配置是通过CRL和CRH寄存器控制的,每个引脚占4个位,低8位在CRL,高8位在CRH。配置输入模式时,MODE位设为00,CNF位根据是否需要上拉下拉来设置;配置输出模式时,MODE位设为11(50MHz输出),CNF位设为00(推挽)或01(开漏)。

还有一个容易被忽略的点:切换模式后最好加一个短暂的延时,让总线电平稳定下来再继续操作。这个延时不需要很长,几个微秒就够,但在高速切换时能避免读到错误的电平。

2.3 微秒延时在72MHz下的实际精度

F1的SysTick定时器可以用来做精确延时,但很多人图省事直接用for循环做软件延时。软件延时的好处是不占用定时器资源,坏处是精度依赖于编译器的优化等级和指令周期。在72MHz下,如果编译器开了优化,一个简单的for循环可能被优化掉或者执行速度远超预期。

我的做法是用SysTick做一个微秒级的延时基准。SysTick的时钟源可以选择HCLK的八分之一或者HCLK本身。如果选HCLK,72MHz下每个计数周期约13.9纳秒,重装载值设为72就能得到大约1微秒的延时。但要注意SysTick是24位的递减计数器,重装载值不能超过0xFFFFFF。对于DHT11需要的几十微秒到几十毫秒的延时,可以用SysTick配合循环来实现。

如果你不想动SysTick,也可以用定时器做一个微秒延时。比如用TIM4,预分频器设为71,这样计数器每72个时钟周期计一次数,也就是1微秒计一次。然后通过读取计数器的值来判断延时是否到达。这种方法不占用中断,也不影响其他定时器。

实测下来,软件延时在DHT11的应用中是可以接受的,因为DHT11对时序的容忍度其实比数据手册上写的要宽一些。但如果你发现读出来的数据偶尔正确偶尔错误,首先怀疑的就是延时精度。用示波器或者逻辑分析仪抓一下波形,看看起始信号的低电平是不是真的到了18毫秒,数据位的高电平持续时间是不是在预期范围内,问题就一目了然了。

3. 完整实操:从零搭建DHT11采集系统

3.1 硬件准备与接线要点

你需要的东西不多:一块STM32F103C8T6最小系统板、一个DHT11模块(最好是带PCB和上拉电阻的那种)、一根USB转串口模块(用来打印数据)、若干杜邦线。如果用的是裸传感器而不是模块,还需要自己加一个4.7k到10k的上拉电阻,接在数据线和VCC之间。

接线方面,DHT11模块通常有三个引脚:VCC、DATA、GND。VCC接3.3V或5V都可以,DHT11的工作电压范围是3.3V到5.5V。DATA接F1的任意一个GPIO,我习惯用PA0或者PB12,这两个引脚在最小系统板上引出方便。GND共地。

这里有一个实际经验:不要用太长的杜邦线。我试过用30厘米的杜邦线连接DHT11,读出来的数据随机性很大,换成10厘米的短线就稳定了。原因是长线引入了更大的分布电容和天线效应,单总线的高频时序容易受到干扰。如果确实需要延长,建议用屏蔽线或者在数据线和地之间加一个小电容(比如100pF)滤波。

电源方面,DHT11对电源纹波比较敏感。如果你用的是USB供电,通常没问题;但如果和电机、继电器等大功率设备共用电源,最好给DHT11单独加一个LDO或者RC滤波。我遇到过一种情况:DHT11和一个小型风扇共用5V电源,风扇一转,DHT11就读数失败。后来在DHT11的VCC和GND之间并了一个100微法的电解电容,问题解决。

3.2 GPIO初始化与模式切换函数

先看GPIO的初始化。以PA0为例,我们需要使能GPIOA的时钟,然后配置PA0为推挽输出模式,初始输出高电平。代码如下:

void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_0); }

然后是模式切换函数。输出模式用推挽输出,输入模式用浮空输入。浮空输入的好处是不加内部上拉,避免和外部上拉电阻冲突;如果你没有外部上拉电阻,可以用上拉输入,但F1的内部上拉电阻阻值较大(约40k),可能不足以在高速时序下快速拉高总线,所以还是建议加外部上拉。

void DHT11_Set_Mode(uint8_t mode) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; if(mode == 0) { GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; } else { GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; } GPIO_Init(GPIOA, &GPIO_InitStructure); }

注意每次切换模式后,最好加一个几微秒的延时,让总线电平稳定。这个延时可以用简单的for循环实现,因为此时对精度的要求不高。

3.3 微秒延时函数的实现与校准

我用SysTick来实现微秒延时。首先配置SysTick的时钟源为HCLK,也就是72MHz。然后写一个延时函数:

void delay_us(uint32_t us) { uint32_t temp; SysTick->LOAD = 72 * us; SysTick->VAL = 0; SysTick->CTRL = 0x00000005; do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1 << 16))); SysTick->CTRL = 0x00000004; SysTick->VAL = 0; }

这个函数的原理是设置重装载值为72乘以微秒数,启动定时器,等待计数到0的标志位置位,然后关闭定时器。注意SysTick的时钟源选择位是CTRL寄存器的第2位,设为1表示使用HCLK。第0位是使能位,第16位是计数到0的标志位。

这个延时函数的精度取决于HCLK的实际频率。如果你的F1不是跑在72MHz,比如超频到128MHz或者降频到48MHz,需要相应调整重装载值。校准的方法很简单:写一个循环,调用delay_us(1000)一千次,用示波器或者逻辑分析仪测量一个GPIO翻转的时间,看看是不是接近1秒。如果有偏差,按比例调整乘数。

3.4 完整的读取流程与代码实现

读取DHT11的完整流程分为五步:发送起始信号、等待响应、读取40位数据、校验、返回结果。下面逐步说明。

发送起始信号:主机拉低总线至少18毫秒,然后拉高20到40微秒,然后切换为输入模式等待传感器响应。

void DHT11_Start(void) { DHT11_Set_Mode(0); // 输出模式 GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 拉低 delay_ms(20); // 至少18ms GPIO_SetBits(GPIOA, GPIO_Pin_0); // 拉高 delay_us(30); // 20-40us DHT11_Set_Mode(1); // 输入模式 }

等待响应:传感器会拉低总线80微秒,然后拉高80微秒。我们需要检测这个响应信号。如果超过一定时间没有检测到,就返回错误。

uint8_t DHT11_Wait_Response(void) { uint32_t timeout = 0; while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) && timeout < 100) { delay_us(1); timeout++; } if(timeout >= 100) return 1; // 超时 timeout = 0; while(!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) && timeout < 100) { delay_us(1); timeout++; } if(timeout >= 100) return 1; timeout = 0; while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) && timeout < 100) { delay_us(1); timeout++; } if(timeout >= 100) return 1; return 0; // 响应正常 }

读取40位数据:每一位都以50微秒的低电平开始,然后高电平持续26到28微秒表示0,持续70微秒表示1。我们在高电平期间读取总线状态,如果高电平持续时间超过某个阈值(比如40微秒),就认为是1,否则是0。

uint8_t DHT11_Read_Byte(void) { uint8_t i, byte = 0; for(i = 0; i < 8; i++) { while(!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)); // 等待低电平结束 delay_us(40); byte <<= 1; if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)) { byte |= 1; } while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)); // 等待高电平结束 } return byte; }

这里用delay_us(40)作为阈值判断。如果高电平在40微秒后仍然存在,说明这一位是1;如果已经变低,说明是0。这个阈值的选择需要根据实际波形调整,因为DHT11的0和1的高电平时间分别是26到28微秒和70微秒,40微秒正好在中间。

校验和计算:前四个字节相加,取低八位,与第五个字节比较。

uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i; DHT11_Start(); if(DHT11_Wait_Response() != 0) return 1; for(i = 0; i < 5; i++) { buf[i] = DHT11_Read_Byte(); } if(buf[4] == (buf[0] + buf[1] + buf[2] + buf[3])) { *humi = buf[0]; *temp = buf[2]; return 0; } return 1; }

这个代码在大多数情况下能正常工作,但如果你发现读取成功率不高,可以尝试在关键位置增加超时检测,避免死循环。另外,读取过程中最好关闭中断,因为中断会打断时序,导致数据位判断错误。

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

4.1 读取失败问题速查表

现象可能原因排查方法解决方案
串口一直打印读取失败起始信号低电平时间不够用示波器测PA0低电平持续时间确保delay_ms(20)准确,检查系统时钟配置
温度湿度都是0GPIO模式切换错误检查切换函数是否被调用确认发送起始信号后切换为输入模式
偶尔成功偶尔失败延时精度不够测量delay_us的实际延时校准SysTick或改用定时器延时
数据校验和错误中断打断了时序在读取期间关闭中断使用__disable_irq()和__enable_irq()
换块板子就不行系统时钟频率不同检查HSE和PLL配置根据实际主频调整延时参数
数据随机跳变电源纹波或线太长测量VCC纹波,缩短杜邦线加滤波电容,换短线

4.2 中断干扰的实测与处理

DHT11的读取过程大约需要4到5毫秒,期间如果发生中断,特别是耗时较长的中断(比如串口接收中断、定时器中断),就会打断时序,导致数据位判断错误。我实测过,在读取DHT11时如果串口以115200波特率持续接收数据,读取失败率会明显上升。

解决办法有两个:一是在读取期间关闭全局中断,读完再打开;二是把DHT11的读取放在高优先级中断或者主循环中,确保不会被其他任务打断。关闭全局中断的方法最简单:

__disable_irq(); status = DHT11_Read_Data(&temp, &humi); __enable_irq();

但要注意,关闭中断的时间不能太长,否则会影响其他实时性要求高的任务。DHT11的读取时间大约5毫秒,对于大多数应用来说是可以接受的。如果你的系统有严格的中断响应要求,可以考虑用定时器输入捕获的方式来读取DHT11,把时序判断交给硬件,但这样会增加代码复杂度。

4.3 电源与布线的实战经验

电源问题是我踩过最多的坑。有一次做了一个带DHT11和继电器的板子,继电器一吸合,DHT11就读数失败。用示波器看VCC,发现继电器动作时电源上出现了几百毫伏的尖峰。后来在DHT11的VCC和GND之间加了一个100微法电解电容和一个0.1微法陶瓷电容,问题解决。

布线上,DHT11的数据线尽量短,并且远离高频信号线(比如晶振、PWM输出线)。如果必须长距离连接,可以考虑用双绞线,数据线和地线绞在一起,减少干扰。另外,DHT11的DATA引脚不要直接接到F1的5V容忍引脚上然后指望内部上拉,外部上拉电阻是必须的,4.7k到10k都可以,我一般用4.7k。

还有一个细节:DHT11上电后需要等待1秒左右才能稳定,不要一上电就立刻读取。我的做法是在初始化后加一个delay_ms(1000),然后再开始读取。这个等待时间在数据手册里没有明确写,但实测下来不加的话第一次读取大概率失败。

4.4 从DHT11延伸到其他单总线设备

DHT11的读取逻辑其实可以复用到很多单总线设备上,比如DS18B20温度传感器。DS18B20的时序和DHT11不同,但核心思路是一样的:主机发送复位脉冲、等待响应、读写数据位。掌握了DHT11的GPIO模式切换和微秒延时,再去看DS18B20的手册就会觉得轻松很多。

如果你想把DHT11的代码改造成更通用的单总线驱动,可以把GPIO操作抽象成宏或者函数指针,把延时函数也抽象出来,这样换一个引脚或者换一个芯片只需要改几个宏定义。我在实际项目中就是这么做的,一个dht11.c文件里把底层GPIO操作和上层协议分开,移植到不同的F1板子上只需要改引脚定义和延时参数。

最后分享一个小技巧:如果你觉得软件延时的精度不好控制,可以用F1的定时器输入捕获功能来测量DHT11的高电平持续时间。具体做法是把数据线接到一个定时器的输入捕获通道上,配置为上升沿和下降沿捕获,通过捕获值计算高电平时间,然后判断是0还是1。这种方法不依赖软件延时,抗中断干扰能力更强,但需要占用一个定时器资源。对于DHT11这种低速传感器来说,有点杀鸡用牛刀,但如果你同时要读多个单总线设备,或者系统中断负载很重,这个方案值得考虑。

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

STM32串口DMA+空闲中断实现不定长数据接收方案解析

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

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

STM32寄存器操作实战:从GPIO到定时器的白话手册

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

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

WorkBuddy+MyBooks:批量更新电子书元数据实战

1. 从一条书库更新需求说起&#xff1a;WorkBuddy 到底能帮我们做什么手里攒了几百本电子书&#xff0c;散落在各个文件夹里&#xff0c;文件名五花八门&#xff0c;有的带 ISBN&#xff0c;有的只有书名加作者&#xff0c;还有的干脆就是“新建文件夹 (3)”。每次想找一本书&a…

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

小米小爱音箱Art拆解:OLED屏幕与内部元器件全面解析

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

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

AI网关在金融行业落地:模型路由、数据脱敏与灰度发布实战

1. 金融 AI 项目做了一堆&#xff0c;为什么最后全卡在落地环节这两年我在金融圈里做了不少 AI 项目&#xff0c;和银行、保险、券商的技术团队都打过交道。大家有个共同的感受&#xff1a;算法模型的 demo 都跑得通&#xff0c;但一进生产环境就开始出事。模型调用的地址五花八…

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

TMS320F28335光伏逆变器调试指南:从原理图到并网离网切换

简介&#xff1a;这份资源面向电力电子、光伏逆变器方向的工程师与高校学生&#xff0c;提供一套基于TMS320F28335的光伏离网并网逆变器完整设计参考&#xff0c;涵盖硬件原理图、PCB印制板图与DSP软件源码&#xff0c;适合作为产品开发或课题设计的实战模板。压缩包共79个文件…

作者头像 李华