简介:本资源是一套基于STM32F103ZET6微控制器与DHT11传感器的嵌入式温湿度检测完整工程,面向嵌入式初学者及单片机课程实践者,解决环境参数采集、协议解析与串口通信等典型物联网感知层开发问题。压缩包共141个文件,含33个头文件(.h)定义外设驱动与协议结构、32个源文件(.c)实现GPIO时序控制、DHT11单总线通信、UART数据发送及系统初始化逻辑,另有.o、.d、.axf、.hex等编译中间与输出文件,以及Keil MDK工程配置(.uvprojx、.uvoptx)、链接脚本(.sct)和批处理工具(keilkilll.bat),整体大小为3.85MB。已有7354人学习下载,资源结构规范,包含标准STM32固件库(如stm32f10x_rcc.c、stm32f10x_adc.c等)与完整可编译工程,无需额外配置即可烧录运行,特别适合掌握Cortex-M3底层驱动、传感器协议时序分析及嵌入式调试全流程的实战训练。 STM32F103ZET6驱动DHT11这个项目,我最早是在实验室帮学弟调代码时接触到的。当时他板子上的DHT11死活读不出数据,串口打印全是0,我过去一看,时序代码里延时全是瞎写的,GPIO模式也没切换,能读出来才怪。后来我自己认真啃了一遍DHT11的手册和单总线协议,把整个流程理顺之后发现,这其实是一个非常典型的“简单外设不简单”的项目——它不涉及复杂的外设寄存器配置,也不像I2C、SPI那样有标准协议栈可用,核心全靠GPIO模拟时序。但恰恰是这种“朴素”的通信方式,把单片机最基础的GPIO操作、延时精度、上拉电阻设计、数据校验这些基本功全都串起来了。如果你能把DHT11彻底调通,再看DS18B20、单总线温湿度传感器、甚至一些自定义单总线协议,基本都能轻松上手。
这篇文章我就从项目整体思路、DHT11单总线协议拆解、HAL库代码实现到常见问题排查,完整过一遍。适合刚学完STM32但还不太清楚怎么把传感器接上来、怎么写驱动的新手,也适合那些用标准库写过一遍、想转到HAL库重新梳理逻辑的开发者。
1. 项目整体设计与开发思路
1.1 为什么选STM32F103ZET6 + DHT11这个组合
先说结论:这套组合是入门嵌入式传感采集的“黄金搭档”,但不是性能最强的搭档。
STM32F103ZET6属于大容量增强型产品,Cortex-M3内核,主频最高72MHz,Flash有512KB,RAM有64KB,片上外设极其丰富。你拿它去点个DHT11,说句实话是大炮打蚊子。也正因为如此,这个项目最适合拿来练手——你不需要考虑资源紧张的问题,所有精力都可以集中在逻辑和时序本身。
DHT11呢,是个数字温湿度传感器,单总线通信,能测20%-90%RH湿度(±5%RH误差)和0-50℃温度(±2℃误差)。精度确实一般,一个DHT11只要几块钱,但胜在接口简单、驱动量小、响应速度也能接受。对于室内环境监测、大棚温湿度记录、学习型项目,完全够用。
从学习价值上看,DHT11有两个关键点非常值得深挖:一是单总线协议对时序要求极高,微秒级的延时都不能乱;二是它只有一根数据线,既要支持主机拉低总线发起读操作,又要支持传感器拉低总线发送响应,GPIO方向必须反复切换。你在驱动它的时候,实际上是在实践三件事:微秒级延时的实现、GPIO输入输出的敏捷切换、数据校验的处理。
1.2 开发方式选型:标准库还是HAL库
这个项目用标准库写,网上能搜到一大堆参考代码,多数是操作寄存器或者直接用GPIO_WriteBit、GPIO_ReadInputDataBit,配合Delay_us延时函数去实现的。对于想深入理解寄存器底层的人来说,标准库版本值得读一遍,代码量小,逻辑一目了然。
但我推荐你用HAL库,理由很实际:现在ST官方主推HAL库,CubeMX可以完成时钟树、GPIO、串口的可视化配置,工程初始化代码全部自动生成,省去了重复造轮子的时间。DHT11驱动本身逻辑不复杂,HAL库里GPIO的读写API也足够满足需求,不会因为封装造成性能瓶颈。唯一需要注意的是,HAL库的函数调用有一定开销,尤其是GPIO_WritePin和GPIO_ReadPin在循环里高频调用时,时序余量可能会被压缩。但只要延时函数够准,驱动代码写得干净,完全没问题。
我个人的观点是:如果你以后要用STM32做项目,HAL库是绕不开的;如果你想把单片机的底层吃透,标准库或者寄存器版本是必修课。这个项目完全可以作为从标准库过渡到HAL库的桥梁,两边各写一遍,收获非常大。
1.3 硬件接线方案与原理图要点
DHT11是3引脚封装(也有4脚的贴片版本,但第四脚NC悬空),引脚定义分别是VCC、DATA、GND。接线非常简单:
- VCC接3.3V或5V都可以,DHT11供电范围是3.3V-5.5V,但建议统一用3.3V,避免电平转换问题
- DATA接单片机的任意一个GPIO,本文以PB12为例
- GND接GND
核心的硬件细节是:DATA线上必须接一个上拉电阻,阻值4.7kΩ到10kΩ之间都可以,推荐4.7kΩ。因为DHT11的数据线和主机之间是开漏关系,总线空闲时必须保持高电平。你在自己做板子或者用面包板搭电路时,这个上拉电阻经常会被漏掉,结果就是通信失败——这是DHT11项目最常见的硬件原因之一。
如果你用嘉立创EDA画原理图,建议在DHT11的DATA引脚和3.3V之间画一个4.7kΩ电阻,同时要在VCC和GND之间加一个100nF的去耦电容,靠近DHT11放置,能有效降低电源噪声对传感器读数的影响。还有一点值得注意,DHT11的数据线尽量短,不要从电机、继电器旁边走线,否则电磁干扰会直接把时序打乱。
2. 读懂DHT11单总线协议:项目实施前必啃的硬骨头
2.1 DHT11的“单总线”到底怎么理解
DHT11的数据通信只有一根线,叫单总线(1-Wire),跟DS18B20是同一种通信思想,但两者的时序定义完全不同,不能混用。
单总线的核心特点是半双工、异步、低速。同一时刻总线上只能有一个设备在驱动电平,其他设备必须处于高阻状态。DHT11作为从机,平时完全沉默,只有主机发起“起始信号”之后,它才会开始响应并发送数据。
这就像你和一个人打电话,正常情况下对方不吭声,等你先喊一声“喂”,他才开始说话。而且这通电话只有一根线,只能一个人说,另一个人听。DHT11就是典型的这种通信模式。
从电平角度看,总线空闲是高电平。主机发起通信时,先把总线拉低一段时间(起始信号),然后释放总线并等待DHT11响应。DHT11检测到起始信号后,会先把总线拉低一段时间(响应信号),再拉高一段时间,然后开始逐位发送40bit数据。整个过程完全靠电平跳变的时间长度来区分含义。
2.2 时序拆解:起始信号、响应信号、数据位
DHT11的完整通信过程可以拆成五个阶段,每一个阶段的延时都必须控制在手册规定的范围内,下面按顺序讲:
第一步:主机拉低总线,持续至少18ms。这个低电平就是“起始信号”,目的是让DHT11从待机状态醒来,准备发送数据。很多新手在这里延时写太短,比如只拉低几毫秒就释放,DHT11根本没有反应过来,后面肯定读不到数据。
第二步:主机释放总线,然后延时20-40us。这一步是给DHT11留出“感知”起始信号的时间。注意这个延时不能太长,一旦超过40us,DHT11可能认为主机还在发信号,就不会进入响应流程了。
第三步:DHT11拉低总线80us,再拉高80us。这两个动作合起来是DHT11的响应信号,告诉主机“我准备好了,接下来要发数据了”。如果是读不到数据或者数据为全0,第一步排查这里——示波器或者逻辑分析仪挂上去,看有没有这个80us的低电平和80us的高电平。
第四步:DHT11连续发送40bit数据,每一位的格式都是先拉低50us,再拉高。引脚上表现为一个50us的低电平脉冲,紧接着一个高电平脉冲。这个高电平持续26-28us代表“0”,持续70us代表“1”。判断数据位是0还是1,就是测量这个高电平的宽度。
第五步:40bit数据发送完成之后,DHT11主动释放总线,总线被上拉电阻拉回高电平,整个通信过程结束。
这里有一个非常重要的细节:DHT11每次通信时间是固定的,从上电到完成一次采样大约需要4ms。而且DHT11内部采样频率较低,两次读取间隔最好大于1秒,最少也不要低于500ms。如果你在循环里连续快速读取,DHT11会来不及刷新内部数据,返回的都是上一次的缓存值。
2.3 40bit数据格式与校验算法
DHT11一次通信发送40bit数据,按顺序依次是:
- 湿度整数部分,8bit
- 湿度小数部分,8bit
- 温度整数部分,8bit
- 温度小数部分,8bit
- 校验和,8bit
湿度小数部分和温度小数部分在DHT11实际输出中始终为0,因为DHT11的分辨率就是1%RH和1℃,小数位没有实际意义。但DHT22(也叫AM2302)会把小数位填充真实数据,因为它的分辨率是0.1%RH和0.1℃。所以如果你以后换传感器,数据解析代码基本可以复用,只是小数位的含义变了。
校验和的算法非常朴素:前四个字节相加,取低8位,和第五个字节比较,相等则校验通过。
实际代码里我一般这样写:
uint8_t check_sum = dht11_data[0] + dht11_data[1] + dht11_data[2] + dht11_data[3]; if (check_sum == dht11_data[4]) { // 数据有效 }校验的好处是不用担心线路干扰导致的错误数据,配合“连续多次读取结果一致再使用”的策略,基本能过滤掉99%的异常读数。
2.4 时序里最容易踩的隐藏坑
DHT11的时序坑非常多,我按实际踩过的顺序列几个最典型的:
第一个坑:微秒级延时不准。很多开发板的delay_us是用while循环减变量实现的,但不同编译优化级别下循环时间会变。我在Keil MDK默认-O0优化下写的延时,到了-O2优化之后直接加速了2-3倍,时序全乱。解决办法是用定时器做微秒延时,或者用DWT(数据观察点触发单元)的CYCCNT寄存器,这种延时跟编译器优化无关,只跟主频有关,稳定可靠。
第二个坑:GPIO方向切换时机不对。主机释放总线时必须把GPIO切回输入模式(或者复用开漏模式),如果还保持推挽输出高电平,等于一直拉着总线,DHT11根本没法控制总线电平,通信必然失败。
第三个坑:读取数据时序采样点太靠后。判断0还是1,核心是测量高电平持续时间。我建议在上升沿之后第40us的时候读一次引脚电平——这个时间点0和1的差距最明显,稳定性最好。如果你等到第60us再读,1没问题,但0可能已经被拉了,读到的还是0,问题不大;但如果你在上升沿后20us去读,1和0都处于高电平阶段,读出来全是1。
3. HAL库驱动DHT11的完整实现
3.1 用CubeMX完成基础初始化配置
打开STM32CubeMX,新建工程,芯片型号选择STM32F103ZET6。配置项主要有三个:
RCC时钟:选择HSE外部晶振,在Clock Configuration页面把系统时钟设为72MHz。DHT11的时序对时钟频率并不是严格敏感,但72MHz下微秒延时最容易计算。
GPIO引脚:把PB12设置为GPIO_Output,初始电平设为High。这里有个细节,DHT11的数据线通信过程中需要主机切换输入输出,CubeMX里默认配置成输出模式即可,代码中通过函数动态切换。
USART串口:如果你想调试时把温湿度数据打印到电脑上,配置一个串口。我通常用USART1,波特率115200,参数8-N-1。CubeMX会自动生成串口初始化代码,后面直接用HAL_UART_Transmit发送数据就行。
工程生成方式:Toolchain选择MDK-ARM,生成完成后用Keil打开。
3.2 微秒延时方案选型
HAL库自带HAL_Delay函数,单位是毫秒,精度无法满足DHT11微秒级的时序需求。你需要一个us级的延时函数。最简单的方案是用定时器,比如用TIM4做1us为单位的计时:
static void delay_us_tim(uint32_t us) { __HAL_TIM_SET_COUNTER(&htim4, 0); HAL_TIM_Base_Start(&htim4); while (__HAL_TIM_GET_COUNTER(&htim4) < us); HAL_TIM_Base_Stop(&htim4); }TIM4的预分频系数设为72-1,自动重载值设为0xFFFF,这样计数器每计数一次正好是1us。这个函数的优点是原理简单、精度稳定,缺点是调用HAL_TIM_Base_Start和Stop的开销比较大,每次调用有几百纳秒的额外延迟,在时序临界点的位置要注意补偿。
如果不想用定时器,另一个推荐方案是根据内核时钟周期做延时,也就是DWT延时。Cortex-M3内核有一个CYCCNT寄存器,每来一个内核时钟周期就加一,72MHz下1us就是72个周期,利用它可以实现非常精确的微秒延时:
static void delay_us_dwt(uint32_t us) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; while (DWT->CYCCNT < us * 72); }这种方式的优点是代码量极小、精度极高、不占用定时器资源,我强烈推荐你在工程里用这个方案。其实Cyclone系列调试器调试裸机程序时也常用CYCCNT做代码耗时统计,是一个非常好用的内核特性。
3.3 DHT11驱动代码逐段解析
先定义GPIO操作的宏,方便后面的读写:
#define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_PIN_12 #define DHT11_OUT_HIGH() HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET) #define DHT11_OUT_LOW() HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET) #define DHT11_IN_READ() HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN)然后是GPIO模式切换函数,输入模式和推挽输出模式之间的切换,用HAL库的标准写法:
void DHT11_Pin_Mode_Output(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_GPIO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); } void DHT11_Pin_Mode_Input(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_GPIO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); }接下来是读取一个字节的函数。DHT11的位读取是按顺序进行的:先等低电平开始,再等低电平结束(也就是高电平开始),然后延时40us采样电平。我把核心逻辑注释写在代码里,方便对照:
uint8_t DHT11_Read_Byte(void) { uint8_t data = 0; for (int i = 0; i < 8; i++) { // 等待低电平开始 while (DHT11_IN_READ() == GPIO_PIN_RESET); // 低电平结束,进入高电平,此时开始计时 delay_us_dwt(40); // 延时40us后采样,高电平持续超过40us则为1 if (DHT11_IN_READ() == GPIO_PIN_SET) { data = (data << 1) | 0x01; } else { data = (data << 1) | 0x00; } // 等待当前位的高电平结束 while (DHT11_IN_READ() == GPIO_PIN_SET); } return data; }这个函数有个潜在问题:如果DHT11没有正常输出,while循环会一直等,如果总线上是低电平或者高电平卡死,程序会死锁。在实际工程里,建议给while加一个超时退出机制。一个简单有效的做法是设置一个计数器,若循环次数超过某个值则直接返回0,并在上层识别为读取失败。
我给一个带超时判断的读位版本:
uint8_t DHT11_Read_Byte_Timeout(uint32_t timeout) { uint8_t data = 0; uint32_t cnt = 0; for (int i = 0; i < 8; i++) { cnt = 0; while (DHT11_IN_READ() == GPIO_PIN_RESET) { if (++cnt > timeout) return 0; } delay_us_dwt(40); if (DHT11_IN_READ() == GPIO_PIN_SET) { data = (data << 1) | 0x01; } else { data = (data << 1) | 0x00; } cnt = 0; while (DHT11_IN_READ() == GPIO_PIN_SET) { if (++cnt > timeout) return 0; } } return data; }3.4 主逻辑与数据解析
读取函数封装成一次完整的DHT11读取过程:
uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] = {0}; // 主机拉低起始信号,至少18ms DHT11_Pin_Mode_Output(); DHT11_OUT_LOW(); HAL_Delay(20); // 释放总线,切换输入模式,拉高由外部上拉电阻完成 DHT11_OUT_HIGH(); delay_us_dwt(30); DHT11_Pin_Mode_Input(); // 等待DHT11响应:先拉低80us,再拉高80us // 这里需要等待低电平出现,然后等待低电平结束,再等待高电平结束 uint32_t cnt = 0; while (DHT11_IN_READ() == GPIO_PIN_SET) { if (++cnt > 10000) return 1; } cnt = 0; while (DHT11_IN_READ() == GPIO_PIN_RESET) { if (++cnt > 10000) return 1; } cnt = 0; while (DHT11_IN_READ() == GPIO_PIN_SET) { if (++cnt > 10000) return 1; } // 读取40bit数据 for (int i = 0; i < 5; i++) { data[i] = DHT11_Read_Byte_Timeout(10000); } // 恢复总线为空闲高电平,切换为输出高 DHT11_Pin_Mode_Output(); DHT11_OUT_HIGH(); // 校验和验证 if ((data[0] + data[1] + data[2] + data[3]) == data[4]) { *humidity = data[0]; *temperature = data[2]; return 0; } return 2; }主函数里调用就非常简单:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_TIM4_Init(); uint8_t hum, temp; uint8_t res; while (1) { res = DHT11_Read_Data(&hum, &temp); if (res == 0) { char msg[64]; sprintf(msg, "Temp: %d.%d C, Hum: %d.%d %%RH\r\n", temp >> 8, temp % 256, hum >> 8, hum % 256); HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 1000); } else { char msg[32]; sprintf(msg, "DHT11 read failed: %d\r\n", res); HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 1000); } HAL_Delay(2000); } }等等,上面这段sprintf的写法有误导性,我实际使用时是按照整数和小数分开打印的,也就是说湿度的高字节是整数部分、低字节是小数部分。DHT11的小数部分为0,所以简单的做法是:
sprintf(msg, "Temp: %d C, Hum: %d %%RH\r\n", temp, hum);如果只想看整数,直接用data[0]和data[2]就行。
4. 实测结果与常见问题排查实录
4.1 我实测的一组数据
调通之后,我把DHT11放在桌面上,室温大约26℃左右,用串口助手连续采集了15分钟,每隔1秒读一次。输出的数据基本稳定在温度26-27℃,湿度52%-56%之间波动。
有一个现象值得注意:湿度读数在小范围内持续跳动,比如52、53、54、53这样跳,这是正常的。DHT11在空气中的响应本身就存在一定波动,并不是传感器的故障。反过来,如果湿度读数长时间纹丝不动,反而要检查一下传感器是不是坏了或者环境是不是太干燥了。另外,如果用手捏住DHT11,几秒后温度和湿度读数都会明显上升,说明传感器响应正常,只是热响应速度确实不快,DHT11大概需要2-3秒稳定到新环境值。
还有一个实用经验是:读取间隔最好设在2秒以上。DHT11手册上建议读取间隔不小于1秒,但实际测试中,1秒间隔偶尔会出现校验失败,2秒之后基本稳如老狗。原因可能是DHT11内部采样和刷新需要时间,如果读取过于频繁,上一次的数据还没更新,或者内部状态机还没就绪,就会导致误码。
4.2 常见问题速查表
我在调试和帮人排查的过程中,整理了下面几个高频问题:
| 现象 | 主要原因 | 解决办法 |
|---|---|---|
| 读到的全是0 | 起始信号时间太短,DHT11未响应;或引脚未正确切换输入模式 | 检查起始低电平是否达到18ms以上;释放总线后改用输入模式读取 |
| 数据有时正确有时乱码 | 微秒延时不准,采样点偏移 | 换用定时器延时或DWT延时;检查主频是否为72MHz |
| 校验和一直失败 | 电源纹波大、电池供电电量不足、数据线过长或靠近干扰源 | 加100nF去耦电容;缩短数据线长度;改用稳压电源供电 |
| 串口打印乱码 | 波特率不匹配、系统时钟配置错误 | 检查串口配置和CubeMX时钟树设置 |
| 读取函数卡死 | while循环没有超时保护,总线某处拉死了 | 给每个等待循环加上超时退出机制 |
| 温度正常但湿度偏高 | 传感器靠近发热元件或人体 | 调整传感器安装位置,避免热源影响 |
| 偶尔读到255或0xFF | 线路接触不良或DHT11损坏 | 重新插拔传感器,用万用表测量引脚电压 |
4.3 排查DHT11不工作的标准流程
如果你遇到DHT11完全不出数,不要急着换传感器或者重写代码,按照这个流程排查一遍,基本能定位问题:
第一步:确认供电。用万用表量DHT11的VCC和GND之间是否有稳定电压,至少有3.3V。如果电压低于3V,DHT11的时序会偏移,通信大概率失败。
第二步:确认上拉。DATA引脚和VCC之间必须有上拉电阻,如果面包板上只插了三条杜邦线而没有电阻,那DATA线悬空的时候电压是不稳定的,根本不能通信。
第三步:确认GPIO模式切换。用逻辑分析仪或者示波器看总线波形,主机发起起始信号后,总线上是否出现DHT11的响应波形(80us低+80us高)。如果响应波形正常,说明硬件和起始时序都没问题;如果看不到响应,问题在起始信号或传感器本身。
第四步:检查延时时间。软件仿真或者示波器实测主机输出的起始低电平时间是否在18ms左右。有的代码里HAL_Delay在中断环境下会被扰乱,导致延时明显变短,这时需要改在无中断环境下调用,或者换用基于定时器的延时。
第五步:换一个DHT11。DHT11是很便宜的传感器,但质量参差不齐,个别模块不稳定。我遇到过几次,所有时序都对,就是读不出数据,换一个DHT11就好了。
说一个重要的经验:DHT11的读数据过程对中断非常敏感。如果主循环里开了很高频率的定时器中断,刚好在读取每一位的采样点附近触发,采样到的电平宽度就会变,导致数据错乱。我的做法是在启动一次DHT11读取之前关掉那些非关键中断,读完再恢复。如果你的系统里必须保留中断,可以考虑把DHT11的驱动放到一个独立的、定时触发的任务里,并且在这个任务执行期间关闭被抢占的中断。
5. 扩展方向与实测体会
5.1 从这个项目能扩展出去的方向
DHT11调通之后,我觉得有两个方向特别值得继续做:
一个方向是换DHT22(AM2302)。DHT22的通信时序和DHT11几乎一模一样,只是数据格式里小数位有意义,测量范围更大(-40到80℃,0到100%RH),精度也更高。你只需要微调解析代码,其他完全复用。换上去之后,整个系统的数据质量会有肉眼可见的提升,对于小型气象站、室内空气质量监测这类场景更合适。
另一个方向是接OLED屏幕本地显示。ST7789或者SSD1306的OLED屏都是常见的I2C或者SPI接口,配合STM32F103ZET6绰绰有余。整个系统做成一个独立的小型温湿度计,比单纯串口打印要直观得多,而且正好能练一下GUI代码的编写和页面切换逻辑。
还有一些更进阶的思路:比如把温湿度数据通过ESP8266模块上传到服务器,或者在本机用FreeRTOS创建一个采集任务,把DHT11的读取放到独立任务中,用信号量控制采集间隔。这些对操作系统的任务调度、资源管理也会有更深的理解。
5.2 个人实际调试过程中的一点体会
写到最后,说点实在的。我第一次调DHT11时,用的是一段网上抄来的标准库代码,抄过来之后发现一运行就卡死,为了查问题把时序图对着手册一帧一帧地比,到最后才发现是GPIO开漏输出和上拉电阻没配合好。后来我把整段逻辑拆开,边分析边重写,才真正把DHT11这一套搞明白。
这个项目给我的最大感受是:DHT11让我重新理解了“简单外设”这四个字。它的硬件设计很朴素,跟SPI、I2C这类工业标准总线完全没法比,但就是要求你把每一微秒都安排得明明白白,把GPIO的方向切换、上拉电阻的作用、微秒延时的实现方式、数据校验的思路全部打通。如果你能把这个传感器彻底调稳,再去碰那些协议复杂的外设,会轻松很多。
另外,调试时一定要善用串口打印和逻辑分析仪。不要只靠串口打印“读不出来”这样粗糙的结论,逻辑分析仪能直接看到总线上每一段波形的宽度,一眼就能看出是起始信号太短、响应信号没有出现、还是数据位采样时间不对。没有逻辑分析仪的话,可以用示波器代替,效果类似。
现在每次跑这个Demo,看着串口助手一行行温度湿度数据刷出来,还是会觉得挺有意思的。下一次我再调DHT22的时候,至少不用从零开始了。
本文还有配套的精品资源,点击获取