☰
STM32F1与DHT11温湿度采集实战:从GPIO模拟单总线到串口输出
2026/10/27 9:21:04 网站建设 项目流程

1. 为什么STM32F1到今天还值得折腾

搞嵌入式这行的,绕不开STM32。尤其是F1系列,说是32位MCU里的“国民芯片”一点不夸张。我手上这块STM32F103C8T6最小系统板,蓝色药丸那种,买的时候不到十五块钱,已经在抽屉里躺了三年,最近翻出来接了个DHT11温湿度传感器,跑了一整套数据采集加串口输出的流程,稳得很。很多人觉得F1老了,该上F4或者G0了,但说实话,对于刚入门、做小项目、搞课程设计或者验证一个简单逻辑的人来说,F1的性价比和资料丰富程度依然是第一梯队。

这篇文章我想聊的不是那种“点个灯就收工”的入门教程,而是围绕STM32F1这个平台,把DHT11温湿度采集这件事从头到尾讲透。包括为什么选F1、DHT11的时序到底怎么啃、GPIO模式怎么配、串口怎么把数据吐出来、遇到读数为零或者校验失败怎么排查。如果你手头正好有一块F103的板子和一个DHT11,跟着走一遍,基本能跑通。如果你还在犹豫要不要学F1,看完你应该会有答案。

STM32F1系列基于ARM Cortex-M3内核,主频常见72MHz,片上Flash从16KB到1MB不等,SRAM从6KB到96KB,外设涵盖USART、SPI、I2C、ADC、定时器、CAN等。以F103C8T6为例,64KB Flash、20KB SRAM、37个GPIO、3个USART、2个SPI、2个I2C、2个ADC、4个定时器,这个配置放在今天做温湿度采集、电机控制、简单人机交互,绰绰有余。关键是它的参考手册、标准外设库、HAL库、社区帖子、视频教程,积累量巨大,你遇到的坑基本都有人踩过。

DHT11这个传感器也是老面孔了,单总线数字输出,测湿范围20%到90%RH,精度±5%RH,测温范围0到50℃,精度±2℃,采样周期不低于1秒。它便宜、接线简单,但时序要求比较死,很多新手卡在“读出来全是0”或者“校验和错误”上。把F1和DHT11放一起,就是一个非常典型的“低成本MCU加低成本传感器”的组合,适合用来练手单总线协议、GPIO输入输出切换、微秒级延时、串口调试这一整套基本功。

2. 动手之前的整体设计思路

2.1 为什么用GPIO模拟单总线而不是找现成外设

DHT11用的是自定义的单总线协议,不是标准I2C也不是标准SPI。STM32F1的GPIO可以配置成开漏输出加外部上拉,也可以配置成推挽输出,但DHT11的通信流程要求主机先拉低至少18毫秒作为起始信号,然后释放总线,再切换成输入模式去读传感器响应。这个过程中,同一根引脚要在输出和输入之间来回切换,而且时序是微秒级的。

STM32F1的硬件I2C有已知的锁死问题,硬件SPI又不匹配DHT11的协议格式,所以最稳妥的方案就是用普通GPIO加软件延时来模拟时序。具体来说,选一个普通IO,比如PA0或者PB12,配置为推挽输出用于发送起始信号,然后在读取阶段切换为浮空输入或上拉输入。上拉电阻方面,DHT11模块通常自带上拉,如果是裸传感器,需要在数据线和VCC之间接一个4.7k到10k的电阻。

注意:不要用STM32F1的硬件I2C去接DHT11,协议完全对不上,而且F1的I2C外设在某些工况下会卡死总线,排查起来非常头疼。

2.2 系统时钟与延时精度怎么保证

DHT11的时序对延时精度有要求。比如起始信号拉低至少18ms,响应信号的低电平80微秒、高电平80微秒,数据位的“0”是高电平26到28微秒,“1”是高电平70微秒。如果用HAL库的HAL_Delay,最小单位是毫秒,根本不够用。所以必须自己实现微秒级延时。

在STM32F1上,最常用的做法是用SysTick定时器或者一个通用定时器来做微秒延时。SysTick默认配置成1毫秒中断,我们可以改它的重装载值来实现微秒计数,但更干净的方式是单独用一个定时器,比如TIM4,预分频到1MHz,也就是每计数一次1微秒,然后通过读计数器来实现延时。另一种做法是使用DWT周期计数器,但F1的DWT在某些调试配置下才可用,不如定时器通用。

我实测下来,用TIM4做微秒延时最稳。配置步骤是:使能TIM4时钟,设置预分频器为72-1(假设系统时钟72MHz),自动重装载值设为最大值65535,然后写一个delay_us函数,先记录当前计数器值,再循环等待直到差值达到目标微秒数。注意要处理计数器溢出的情况,虽然65535微秒约65毫秒,一般延时不会超过这个范围,但严谨起见还是加上溢出判断。

2.3 数据格式与校验逻辑

DHT11一次完整传输是40位数据,顺序是:湿度整数8位、湿度小数8位、温度整数8位、温度小数8位、校验和8位。对于DHT11来说,小数部分通常都是0,因为它的分辨率就是1%RH和1℃。校验和等于前四个字节相加后取低8位。如果校验失败,说明时序有偏差或者传感器没响应,需要丢弃这次数据重新读。

这里有个细节:DHT11在每次读取之间需要至少1秒的间隔,如果读得太快,传感器可能不响应或者返回上一次的旧数据。我在代码里加了一个简单的状态机,用SysTick的毫秒计数来判断距离上次读取是否超过1秒,如果不够就跳过本次采集。

3. 核心细节解析与实操要点

3.1 GPIO模式切换的时机与陷阱

DHT11通信过程中,GPIO要在输出和输入之间切换。具体流程是:

  1. 主机拉低数据线至少18ms,此时GPIO是推挽输出,输出低电平。
  2. 主机释放数据线,切换为输入模式,等待DHT11响应。
  3. DHT11拉低80微秒,再拉高80微秒,作为响应信号。
  4. 之后DHT11依次输出40位数据,每位以50微秒低电平开始,然后高电平持续26到28微秒表示“0”,持续70微秒表示“1”。
  5. 最后DHT11拉低50微秒,释放总线。

在切换GPIO模式时,有一个容易忽略的点:从输出模式切换到输入模式时,如果外部上拉不够强,总线可能会悬空一段时间,导致读到错误的起始位。解决办法是在释放总线之前,先把输出寄存器置高,然后再切换为输入模式,这样内部上拉会短暂维持高电平。另外,切换模式后不要立刻去读引脚,稍微等几个微秒让电平稳定。

实操心得:我在PA0上接DHT11时,发现如果不加上拉电阻,读出来的数据全是0。后来在数据线和3.3V之间焊了一个4.7k电阻,问题解决。所以如果你用的是裸传感器而不是模块,上拉电阻一定要加。

3.2 微秒延时的实现与校准

前面提到用TIM4做微秒延时,这里给出具体配置和代码思路。假设系统时钟是72MHz,TIM4挂载在APB1总线上,APB1的时钟是36MHz,但定时器时钟是APB1的两倍,也就是72MHz。所以预分频器设为72-1,计数频率就是1MHz,每计数一次1微秒。

// TIM4初始化,用于微秒延时 void TIM4_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 65535; TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); TIM_Cmd(TIM4, ENABLE); } // 微秒级延时 void delay_us(uint16_t us) { uint16_t start = TIM_GetCounter(TIM4); uint16_t target = us; uint16_t current; while (1) { current = TIM_GetCounter(TIM4); if (current >= start) { if ((current - start) >= target) break; } else { // 计数器溢出 if ((65535 - start + current) >= target) break; } } }

这段代码的逻辑是:记录进入函数时的计数器值,然后不断读取当前值,计算差值。如果当前值大于等于起始值,直接相减;如果当前值小于起始值,说明发生了溢出,用65535减去起始值再加上当前值。这个实现简单可靠,实测在72MHz下误差在1微秒以内。

3.3 读取时序的精确控制

读取40位数据的函数是整个代码的核心。每一位的读取流程是:先等待低电平结束,然后延时30微秒左右,再采样引脚电平。如果此时是高电平,说明这一位是“1”,否则是“0”。但这里有个关键点:延时30微秒这个值需要根据实际高电平持续时间来调整。对于“0”,高电平持续26到28微秒,30微秒时已经变成低电平;对于“1”,高电平持续70微秒,30微秒时还是高电平。所以30微秒是一个比较安全的采样点。

不过,由于中断可能会打断延时,导致采样点偏移,所以在读取DHT11期间最好关闭全局中断。我试过不关中断,偶尔会出现校验和错误,关掉之后连续读几百次都没有失败。关中断的时间大概在几毫秒左右,对大多数应用来说可以接受。

// 读取一个字节 uint8_t DHT11_ReadByte(void) { uint8_t i, byte = 0; for (i = 0; i < 8; i++) { // 等待低电平结束 while (DHT11_DATA_IN() == 0); delay_us(30); byte <<= 1; if (DHT11_DATA_IN() == 1) { byte |= 1; } // 等待高电平结束 while (DHT11_DATA_IN() == 1); } return byte; }

这段代码里,DHT11_DATA_IN()是一个宏,用来读取引脚电平。等待低电平结束的循环可能会因为传感器无响应而卡死,所以实际产品代码里应该加上超时计数,比如循环超过1000次就退出并返回错误。

4. 完整实操流程与关键环节实现

4.1 硬件连接与供电检查

先确认手头的硬件:STM32F103C8T6最小系统板一块、DHT11模块或裸传感器一个、杜邦线若干、USB转串口模块一个(用于看数据)。接线方面,DHT11模块通常有三个引脚:VCC、DATA、GND。VCC接3.3V,GND接GND,DATA接PA0。如果你用的是裸传感器,需要在DATA和VCC之间接一个4.7k到10k的上拉电阻。

供电方面,DHT11工作电压是3.3V到5.5V,STM32F1的IO是3.3V电平,所以直接接3.3V没问题。但要注意,有些DHT11模块标称5V供电,实际在3.3V下也能工作,但精度可能略有下降。我实测3.3V供电时,湿度读数比5V时低2%到3%,温度基本一致。如果对湿度精度要求高,可以考虑给DHT11单独供5V,然后在DATA线上加电平转换,但大多数场景下3.3V直接接也够用。

串口方面,用USART1,PA9是TX,PA10是RX,接USB转串口模块的RX和TX,波特率设115200。这样可以把温湿度数据打印到电脑上,方便观察。

4.2 工程搭建与库的选择

我用的开发环境是Keil MDK 5,库选择标准外设库,版本3.5.0。虽然ST现在主推HAL库,但标准库在F1上更轻量,代码量小,执行效率高,而且网上资料多。如果你习惯用STM32CubeMX加HAL库,也可以,但要注意HAL库的微秒延时需要自己实现,HAL_Delay只有毫秒级。

工程搭建步骤:

  1. 新建Keil工程,选择STM32F103C8。
  2. 添加启动文件startup_stm32f10x_md.s。
  3. 添加标准外设库的源文件:stm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_usart.c、stm32f10x_tim.c。
  4. 在stm32f10x_conf.h中使能对应的头文件。
  5. 配置系统时钟为72MHz,在system_stm32f10x.c中确认SYSCLK_FREQ_72MHz已定义。

注意:标准库的stm32f10x.h中有一个HSE_VALUE宏,默认是8000000,如果你板子上的晶振是8MHz,就不用改;如果是12MHz,要改成12000000,否则串口波特率会不对。

4.3 代码结构与主循环设计

整个工程的文件结构建议这样组织:

  • main.c:主循环,负责初始化、调用读取函数、通过串口输出。
  • dht11.c / dht11.h:DHT11驱动,包含GPIO初始化、微秒延时、读取字节、读取温湿度。
  • usart.c / usart.h:串口初始化,重定向printf。
  • tim.c / tim.h:TIM4初始化,提供delay_us。

主循环的逻辑很简单:每隔2秒读一次DHT11,如果读取成功,通过串口打印温度和湿度;如果失败,打印错误信息。读取成功的判断依据是校验和正确。

int main(void) { USART1_Init(115200); TIM4_Init(); DHT11_Init(); printf("STM32F1 DHT11 Test\r\n"); while (1) { if (DHT11_ReadData(&temp, &humi) == 0) { printf("Temp: %d C, Humi: %d %%RH\r\n", temp, humi); } else { printf("DHT11 read error\r\n"); } delay_ms(2000); } }

这里的delay_ms可以用SysTick实现,也可以用TIM4配合循环。我习惯用SysTick,因为标准库的SysTick_Config默认就是1毫秒中断,直接用它做毫秒计数很方便。

4.4 串口输出的重定向与调试

标准库没有直接支持printf到串口,需要重定向fputc函数。在usart.c里加上:

int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }

然后在Keil的Target选项中勾选“Use MicroLIB”,否则printf会卡住。这个坑我踩过好几次,明明代码没问题,串口就是没输出,后来发现是没开MicroLIB。

调试的时候,如果串口助手收到乱码,先检查波特率是否一致,再检查系统时钟配置是否正确。如果收到的是空白,检查TX和RX是否接反,以及USB转串口模块的驱动是否装好。

5. 常见问题与排查技巧实录

5.1 读出来全是0或者255

这是最常见的问题。原因通常有三个:一是上拉电阻没接,总线在释放后处于悬空状态,读到的电平不确定;二是GPIO模式切换不对,比如释放总线后没有及时切换为输入模式;三是延时不准,导致采样点错过了有效电平。

排查步骤:先用示波器或者逻辑分析仪看数据线上的波形。如果没有仪器,可以用万用表测数据线在空闲时的电压,正常应该是3.3V左右。如果只有1V多或者0V,说明上拉有问题。然后检查代码中GPIO切换的时机,确保在拉低18ms之后,先把输出寄存器置高,再切换为输入模式。

5.2 校验和错误但数据看起来正常

有时候读出来的温湿度值在合理范围内,但校验和不匹配。这种情况通常是某一位的高电平采样点偏了,比如把“0”读成了“1”,或者把“1”读成了“0”。解决办法是调整采样延时。我试过把30微秒改成28微秒或者32微秒,观察校验成功率的变化。最终在30微秒时成功率最高,连续读100次只有1到2次失败。

另外,如果中断频繁打断读取过程,也会导致采样点偏移。在读取DHT11的几毫秒内关闭全局中断,可以显著降低错误率。关中断的代码是__disable_irq()和__enable_irq(),注意成对使用。

5.3 传感器响应但数据不变

如果每次读出来的温湿度都一样,而且不随环境变化,可能是传感器进入了某种锁定状态。DHT11在供电不稳定或者读取间隔太短时,可能会返回上一次的缓存数据。解决办法是确保每次读取间隔大于1秒,并且在读取失败后等待至少2秒再重试。另外,检查VCC电压是否稳定,如果电压低于3.0V,传感器可能工作不正常。

5.4 串口打印乱码或没有输出

先确认MicroLIB是否勾选,再检查波特率。如果用的是外部晶振,确认HSE_VALUE和实际晶振频率一致。如果用的是内部RC振荡器,F1的内部RC精度较差,串口波特率可能会有偏差,建议用外部晶振。另外,检查USART的GPIO配置是否正确,PA9要配置为复用推挽输出,PA10配置为浮空输入或上拉输入。

问题现象可能原因排查方法解决措施
读数全0上拉缺失测数据线空闲电压加4.7k上拉电阻
读数全255GPIO模式错误检查切换代码释放前先置高再切输入
校验和错误采样点偏移调整延时值30微秒附近微调
数据不变读取间隔太短检查循环延时间隔大于1秒
串口乱码时钟配置错误确认HSE_VALUE改为实际晶振频率
串口无输出MicroLIB未开检查Keil选项勾选Use MicroLIB

独家避坑技巧:在DHT11_ReadByte函数里,等待低电平结束的while循环一定要加超时。我遇到过传感器接触不良时,程序卡在这个循环里出不来,看门狗都救不了。加一个计数器,循环超过200次就强制退出并返回错误,这样系统不会死机。

6. 从F1和DHT11延伸出去的一些经验

STM32F1加DHT11这个组合,看起来简单,但把每一个细节都做扎实,其实能学到不少东西。比如GPIO的八种模式,你在点灯的时候可能只用过推挽输出,但做单总线通信时,浮空输入、上拉输入、开漏输出这些模式都要理解清楚。再比如微秒延时,用定时器实现和用空指令循环实现,精度和可移植性差别很大。还有串口重定向、校验和计算、超时处理,这些都是嵌入式开发的基本功。

如果你已经跑通了DHT11,下一步可以试试把这些数据通过OLED显示出来,或者通过蓝牙模块传到手机,或者存到SD卡里。也可以换一个传感器,比如SHT30,用I2C接口,对比一下单总线和I2C的差异。F1的I2C虽然有点坑,但把时钟频率降到100kHz,加上超时处理,也能稳定工作。

我个人在实际操作中的体会是,不要一上来就追求复杂的项目,把一个小模块吃透,比泛泛地做十个模块更有价值。DHT11的时序手册我前后读了不下十遍,每次都能发现之前忽略的细节。比如手册里写“主机拉低至少18ms”,这个“至少”意味着你可以拉低20ms、30ms,但不能少于18ms。再比如“数据位以50微秒低电平开始”,这个50微秒是典型值,实际可能在48到52微秒之间波动,所以采样点要留有余量。

最后再分享一个小技巧:如果你手头没有逻辑分析仪,可以用另一块STM32或者Arduino做一个简单的逻辑抓取器,把数据线的电平变化通过串口打印出来,虽然精度有限,但足以判断有没有响应信号和大致的数据波形。这个方法我在没有仪器的阶段用过很多次,能解决大部分时序问题。

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

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

立即咨询