1. 为什么DHT11这么“老”了还是绕不开:一点心里话
如果你在嵌入式社区混过一阵子,大概率会看到两种截然不同的态度:一边是新手拿着DHT11做温湿度计、智能家居节点,另一边是“老炮”不屑一顾地说这玩意儿精度一般、时序别扭、单总线还娇气,随便来个中断就把时序打乱了,不如直接上I2C的SHT30。
但我说句实在话:DHT11仍然是目前入门单总线通信最合适的一个载体。原因不复杂——它慢、它简单、它便宜,而且它的协议足够“原生”,没有硬件控制器帮你兜底,每一位都得靠GPIO翻转和延时函数去抠。你把DHT11的时序彻底吃透了,回头看DS18B20、看各种单总线温湿度传感器,甚至自己用一根IO口去模拟一些低速的专用协议,都会顺畅很多。
这篇文章我打算把DHT11的器件原理、单总线通信协议、驱动代码一次讲透,按照“器件本身 → 协议机制 → 代码实现 → 实测踩坑”的顺序走一遍。内容面对的是已经会一点GPIO操作、想搞明白“为什么别人写的驱动要那样写”的开发者,也适合刚接触单片机不久、准备用STM32或国产MCU做一个小项目的朋友。
先说一个很容易劝退新手的点:DHT11的数据手册是中文的,翻译质量还行,但时序图画得比较“抽象”,很多关键参数散落在各处,一开始读很容易懵。我这篇文章会把时序参数整理成表格,对照着代码讲,尽量让你看完就能自己写一套驱动出来,而不是只会复制粘贴。
2. DHT11到底是个什么器件:从内部结构到电气特性
2.1 引脚定义和典型接法
DHT11常见的有三脚封装和四脚封装,买的时候注意看丝印。三脚封装从左到右一般是VCC(电源)、DATA(数据)、GND(地),四脚封装有一个脚是悬空的NC(Not Connected),其实用到的还是那三根。
供电范围很宽,3.3V到5.5V都能跑。但这里有个非常重要的细节:如果用3.3V供电,上拉电阻建议用4.7kΩ到10kΩ之间偏小的值;如果用5V供电,4.7kΩ到10kΩ都可以,看你MCU的IO耐压和逻辑电平是否兼容。5V供电时DATA引脚输出的高电平也是接近5V的,如果你的MCU是3.3V系统且不是5V tolerant的引脚,那就必须做电平转换,或者干脆选择3.3V供电,否则长期使用有损坏IO的风险。
数据引脚必须接一个上拉电阻,因为单总线通信的特点是线与——主机和从机都可以拉低总线,释放总线时靠上拉电阻把电平拉高。没有上拉电阻,总线就永远停在低电平,通信直接失败。
2.2 内部构成和测量原理
DHT11内部实际上集成了一个电阻式感湿元件和一个NTC测温元件,加上一个8位单片机做信号处理和单总线协议编码。所以它对外输出的不是模拟量,而是已经编码好的数字信号,这一点比早期的HS1101这类需要配合555电路测量频率的湿度传感器要友好太多。
测温范围0°C到50°C,精度±2°C;湿度范围20%RH到90%RH,精度±5%RH。采样周期大概是1秒一次,也就是两次读取之间如果间隔太短,第二次很可能会拿到上一次的缓存数据,甚至读到相同数据,这是正常现象,不是传感器坏了。
分辨率上,湿度是1%RH,温度是1°C,数据格式是整数位+小数位,但DHT11的小数位恒为0,这一点从协议解析的数据结构就能看出来——前两个字节是湿度整数和小数,后两个字节是温度整数和小数,DHT11实际输出时小数位是零,但协议上保留了位置,给后续改进型号DHT22(也叫AM2302)用。
有一点值得展开说:为什么DHT11的采样周期是1秒左右?因为湿敏电容需要时间与环境湿度达成平衡,热敏电阻也有热惰性,传感器本身做了限速处理。我在项目里曾经以500ms周期连续读取DHT11,结果数据好几分钟都不变化,一度怀疑代码有问题,后来查资料才确认是传感器自身更新频率的限制。所以设计采集逻辑时,采集周期设为1秒以上即可,刻意调快没有意义,白白浪费总线时间。
2.3 常用参数速查表
| 参数项目 | 数值 | 说明 |
|---|---|---|
| 供电电压 | 3.3V ~ 5.5V | 建议3.3V或5V,注意IO电平匹配 |
| 温度测量范围 | 0 ~ 50°C | 超出范围精度不保证 |
| 温度精度 | ±2°C | 典型值,实际可能略好 |
| 湿度测量范围 | 20%RH ~ 90%RH | 低于或高于此范围不建议使用 |
| 湿度精度 | ±5%RH | 典型值 |
| 采样周期 | 约1秒 | 读取间隔小于1秒时数据可能不更新 |
| 数据位宽 | 40 bit | 湿度16位+温度16位+校验8位 |
| 通信方式 | 单总线 | 一根数据线,半双工,主机主动发起 |
等你把这套表记熟了,选型的时候就能很快判断DHT11能用在什么场景,不能用在什么场景。比如要做一个冷链运输记录仪,温度范围会到零下20°C,DHT11就扛不住了,这时候得换DHT22或者DS18B20;如果只是做一个室内环境监测小摆件,DHT11绰绰有余。
3. 单总线协议拆解:每一次电平跳变都是有含义的
3.1 单总线通信的整体流程
DHT11的单总线协议属于严格的主机主动式通信,从机永远不主动发言,所有通信都由主机发起。一次完整的通信过程分为三个阶段:
- 主机发出起始信号(Start Signal)
- 从机(DHT11)发出响应信号(Response Signal)
- 从机连续发送40位数据,每一位都是“低电平+高电平”的组合形式
三个阶段之间没有额外的握手,没有ACK,没有重传机制,全靠电平的宽度说话。所以时序参数的精度直接决定通信是否成功,靠通讯协议本身纠错是不现实的,唯一能做的就是在读取完成后用校验字节验证数据是否有效,无效则丢弃这一帧。
这里其实暴露出单总线协议的脆弱性:它适合短距离、低速、单点通信场景,不适合长线、多节点、强干扰环境。如果你要把传感器引线放到1米以上,优先考虑用屏蔽线,或者干脆换成I2C总线加上PCA9600这类总线缓冲器,不然时序很容易被线缆电容拉垮。
3.2 起始信号和响应信号的理解
主机要发起通信时,先把数据线拉低,保持至少18微秒,最长不超过30微秒,然后再释放总线,上拉电阻把总线拉高。这一个“先低后高”的脉冲就是起始信号。
之后主机释放总线并切换到输入模式,等待DHT11的响应。DHT11会在主机释放总线后等待一小段时间,然后主动把总线拉低,保持80微秒左右,再释放总线,又产生一个80微秒左右的高电平,这就是响应信号。
有一个容易忽略的细节:主机拉低这个动作,时序长短在不同资料里写法不太一样,有的写“至少18ms”,这明显是笔误,实际是18微秒。如果按毫秒来延时,整个通信会超时到无法完成。我第一次自己写驱动时就是看了一份写错的资料,把18us写成了18ms,结果传感器永远没有响应,排查了很久才发现是单位错了。
还有个细节是主机释放总线后,需要几十微秒的切换时间才能把GPIO从输出模式切到输入模式并稳定读取。如果切换得太快,读到的高电平可能还是总线残压,或者刚好读到DHT11开始拉低的边缘,导致误判。
3.3 数据位“0”和“1”的区分逻辑
这是单总线协议最核心的部分,也是新手最容易弄混的地方。
DHT11发送每一位数据时,总线状态都是相同的:先是50微秒左右的低电平,然后是高电平。区别只在高电平的持续时间上:
- 数据位“0”:低电平50us,高电平26~28us
- 数据位“1”:低电平50us,高电平70us左右
换句话说,DHT11用“高电平的宽度”来编码0和1,而不是用电平本身。主机读取的时候,先等低电平结束,然后开始计时,直到总线再次变低,记录高电平持续了多少微秒。如果高电平时间比某个阈值(通常取40us或50us)长,就判定为1,否则判定为0。
这个采样思路在代码里实现为:
uint8_t dht11_read_bit(void) { uint8_t timeout = 0; // 等待低电平结束 while (gpio_read(DHT11_PIN) == 0) { if (++timeout > 100) return 0xFF; // 超时 delay_us(1); } // 低电平结束后,开始计算高电平持续时间 uint32_t high_time = 0; while (gpio_read(DHT11_PIN) == 1) { if (++high_time > 100) return 0xFF; delay_us(1); } return (high_time > 40) ? 1 : 0; }阈值为什么取40微秒而不是35或45?因为数据0的高电平典型值是26~28微秒,数据1的高电平典型值是70微秒,中间有大约40微秒的判读裕量。取40微秒做阈值,能容忍一定的时序漂移,同时又不会把0误判成1。这属于工程经验值,数据手册不会告诉你,但实测下来很稳。
3.4 40位数据帧的字节划分与校验
40位数据的排列顺序是:
- 湿度整数(8位)
- 湿度小数(8位)
- 温度整数(8位)
- 温度小数(8位)
- 校验和(8位)
校验和的计算规则很简单:把前四个字节相加,取低8位,如果等于校验字节,则数据有效。比如某次读到的湿度整数是45,湿度小数是0,温度整数是26,温度小数是0,则45 + 0 + 26 + 0 = 71,校验字节应该是71,否则帧无效。
这个校验算法非常朴素,但它能检出绝大多数单bit翻转错误。如果两个字节同时错误且恰好相互抵消,校验会漏过去,但这种概率在单总线这种短距离通信场景里低到可以忽略。
数据接收完之后,主机不需要发送任何结束信号,总线自然释放即可。DHT11也不会持续占用总线。整个通信耗时大约4毫秒左右,主要是起始信号1毫秒不到,响应信号0.2毫秒,40位数据每位约0.1毫秒,加起来在3到5毫秒之间。
3.5 时序参数速查表
| 时序阶段 | 参数 | 典型值 | 允许范围 |
|---|---|---|---|
| 主机起始信号低电平 | 拉低时间 | 20us | 18us ~ 30us |
| 从机响应低电平 | 拉低时间 | 80us | 75us ~ 85us |
| 从机响应高电平 | 拉高时间 | 80us | 75us ~ 85us |
| 数据位0高电平 | 高电平宽度 | 27us | 24us ~ 30us |
| 数据位1高电平 | 高电平宽度 | 70us | 68us ~ 75us |
| 数据位低电平 | 每位前导低电平 | 50us | 48us ~ 55us |
对照这张表再去看时序图,就清晰很多了。后面的代码里我会用这张表的参数作为延时基准。
4. 代码实战:从零手写一套可移植的DHT11驱动
4.1 硬件环境和软件框架约定
我先说明一下代码的运行环境假设:MCU采用STM32F103系列,标准外设库或HAL库均可,GPIO用PB12作为数据线,系统时钟72MHz。代码风格上我会尽量写成“弱硬件依赖”,让你只需要修改几个宏和底层GPIO操作函数就能移植到其他MCU上。
在动手之前,先把GPIO的方向切换搞清楚。DHT11通信过程中,主机需要做两件事:输出起始信号(输出模式,拉低一定时间)、读取数据(输入模式)。所以代码里必须有一个函数能把同一根GPIO在输入和输出模式间切换,这是单总线驱动的基础。
STM32标准库里的KZ_GPIO_SetMode函数大概是这样:
void DHT11_Pin_Mode_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); } void DHT11_Pin_Mode_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); }如果是HAL库,区别在于初始化结构体的字段名略有不同,但思路一模一样。还有一个小技巧:输出模式用推挽输出,输入模式用浮空输入,因为外部已经有上拉电阻了,浮空输入完全够用。如果你非要用上拉输入模式也可以,但要注意,MCU内部上拉电阻一般较弱(30kΩ到50kΩ),与外部4.7kΩ并联后总电阻更小,电平恢复速度更快,但功耗略增,一般项目里无所谓。
微秒级延时函数是另一个依赖项。STM32标准库的SysTick默认配置是1毫秒中断一次,精确到微秒需要自己写一个,采用SysTick的24位向下计数器配合72MHz主频,实现如下:
void delay_us(uint32_t us) { uint32_t start = SysTick->VAL; uint32_t ticks = us * 72; uint32_t now; while (1) { now = SysTick->VAL; if (start >= now) { if (start - now >= ticks) break; } else { if (start + (0xFFFFFF - now) >= ticks) break; } } }这段代码没有开启SysTick中断,只用了它的计数功能,不会干扰系统其他中断,稳定性实测可以。如果移植到其他平台,可以用定时器或__NOP()循环做延时,但要先用自己的逻辑分析仪或者示波器校准延时的实际长度,不能只看理论值。我之前在GD32上移植时,发现同样的72MHz主频下,NOP循环延时比STM32快了不少,需要重新校准,这说明不同内核的指令周期和Flash等待周期不一样,延时函数的可移植性比想象中要差。
4.2 读取一帧数据的完整驱动
以下代码是读取核心,整个流程严格按照前面的时序分析设计:
#define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_12 uint8_t DHT11_ReadData(uint8_t *humidity_int, uint8_t *humidity_dec, uint8_t *temperature_int, uint8_t *temperature_dec) { uint8_t data[5] = {0, 0, 0, 0, 0}; uint8_t i, j; // 1. 主机拉低起始信号 DHT11_Pin_Mode_Output(); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_RESET); delay_us(20); // 2. 释放总线,切换输入 GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); DHT11_Pin_Mode_Input(); // 3. 等待响应信号的低电平 uint32_t timeout = 0; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == 1) { if (++timeout > 100000) return 1; // 超时,约1ms } // 4. 等待响应信号的低电平结束 timeout = 0; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == 0) { if (++timeout > 100000) return 2; } // 5. 等待响应信号的高电平结束 timeout = 0; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == 1) { if (++timeout > 100000) return 3; } // 6. 连续读取40位数据 for (j = 0; j < 5; j++) { for (i = 0; i < 8; i++) { uint8_t bit = DHT11_ReadBitWithTimeout(); if (bit == 0xFF) return 4; data[j] = (data[j] << 1) | bit; } } // 7. 校验和验证 if (((data[0] + data[1] + data[2] + data[3]) & 0xFF) != data[4]) { return 5; } // 8. 输出数据 *humidity_int = data[0]; *humidity_dec = data[1]; *temperature_int = data[2]; *temperature_dec = data[3]; return 0; }这段代码里的超时判断为什么用循环计数而不是精确延时?因为阻塞式等电平变低时,电平可能永远不变,如果不用超时,主控就直接死循环了。这种死循环风险在工程里是不能接受的,尤其是有看门狗的场景,超时需要尽快退出并返回错误码,让上层决定是重试还是放弃。
读位的函数可以这样实现:
uint8_t DHT11_ReadBitWithTimeout(void) { uint32_t timeout = 0; // 等待低电平结束 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == 0) { if (++timeout > 100) return 0xFF; delay_us(1); } // 低电平结束后,计算高电平持续时间 timeout = 0; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == 1) { if (++timeout > 100) return 0xFF; delay_us(1); } return (timeout > 40) ? 1 : 0; }关于delay_us(1)套在循环里的性能问题,我想多说一句。因为每次循环都调用延时函数,函数调用本身的开销会导致判定阈值的实际值偏移。如果怕偏移,可以把延时函数改成宏定义或者内联函数,让编译优化掉一部分开销。实测在STM32F103的72MHz主频下,用上述写法判定成功率很高,不需要过度优化。
4.3 为什么“先等低电平结束再计时”而不是“直接测高电平”
很多第一次接触这个驱动的朋友会问:数据位开始时总线是低电平,那我能不能直接在发送完起始信号后就开始高速采样,记录整个波形,然后从波形里识别0和1?
当然可以,逻辑分析仪就是这个思路。MCU代码里也可以这么做,但没必要。因为MCU烧录到不同主频的芯片上时,如果采用连续采样,采样率的差异会直接影响对脉冲宽度的判断,而这个“先等低电平结束再计时高电平”的方式天然对低电平期间的抖动不敏感,只需要在高电平期间计时,阈值依然是40us,容错性更好。
反过来,如果从高电平开始时就计时,你会多等一个50us的低电平时间,白白增加耗时不谈,还要额外写一个“先等待高电平到来”的状态机,代码就复杂了。所以业界普遍采用“等低结束→测高宽度→判决”这个套路。
4.4 代码移植到其他平台时的改动点
换平台时你需要改动的点非常集中:
DHT11_Pin_Mode_Output和DHT11_Pin_Mode_Input两个函数,改成目标平台GPIO方向切换的实现。GPIO_ReadInputDataBit宏改为目标平台的读引脚函数。delay_us改成目标平台的微秒延时实现,并且用示波器先校准。DHT11_GPIO_PORT和DHT11_GPIO_PIN两个宏改为目标引脚。
本质上驱动的逻辑层(时序状态机和数据解析)与硬件层(引脚操作和延时)是分离的,只要守住这个边界,移植就很顺畅。如果项目里已经用了一个抽象层(比如HAL库的GPIO读写回调),同样可以只改底层回调。
5. 我实际项目中踩过的坑和排查思路
5.1 读取一直超时:不是代码问题,而是上拉电阻没焊
有一次我在一个温湿度采集板上调试DHT11,发现程序一直卡在“等待响应信号”的超时里。检查了一遍代码,时序延时都对,GPIO配置也对,就是读不到低电平。后来拿万用表量了一下DATA引脚的电平,发现一直悬空,再查原理图,元器件封装里上拉电阻的位号没贴料。
这个事给我印象很深:单总线协议对硬件上拉电阻是硬依赖,没有它,主机释放总线后电平就是悬空的,既不是高也不是低,读回来的值完全不可控。排查这类问题时,第一个动作应该是拿万用表或示波器看总线的静态电平,而不是反复检查代码。
5.2 高电平只有2.8V:MCU逻辑电平不兼容
另一个项目里,DHT11用5V供电,MCU用3.3V供电,数据线直接连到了MCU的普通IO口。结果时而读到正常数据,时而校验失败,且数据偶发出现0xFF。用示波器一看,DHT11输出的高电平接近5V,MCU的IO口确实能识别高电平(大多数3.3V MCU的高电平阈值在2.0V左右),问题在于5V电平对MCU内部的ESD保护二极管产生了额外压力,长期跑不稳,偶尔把引脚电平拉到临界区间。
解决方法是加一个电平转换,或者直接用3.3V给DHT11供电。3.3V供电时,DHT11的数据输出高电平接近3.3V,STM32 GPIO识别毫无压力,实测通信非常稳定。如果你非得用5V供电,可以加一个1kΩ串联电阻再进MCU,配合MCU的内部上拉,勉强能用,但不推荐。
5.3 数据偶发校验失败:中断正在打断时序
这个坑是几乎所有单总线设备的“经典剧目”。DHT11时序里,数据位0的高电平只有26~28us,数据位1的高电平也只有70us左右。如果此时MCU收到一个优先级较高的中断,比如定时器中断、串口中断、CAN中断,进入中断服务函数后要花几十微秒才能退出,那高电平宽度的测量就会严重失真,可能导致0变成1,校验和自然也对不上。
解决方法有三种:
- 在读取DHT11期间关闭可屏蔽中断,读完再开。这是最简单粗暴的方法,只适用于读取过程短(3~5ms)、且系统对中断延迟不敏感的场景。
- 把DHT11读取逻辑放在一个高优先级的中断里,但要注意嵌套优先级配置,避免它被别的中断打断。
- 使用定时器输入捕获功能,把电平宽度测量交给硬件完成,MCU只读捕获寄存器。这个方法最可靠,但代码复杂度上升不少。
我的实际做法是:在FreeRTOS项目里,用临界区保护DHT11的读取函数,同时关闭调度器切换,确保一段完整读取过程不被任务切换打断。对于周期性的无线发送中断,只能选择错开时间,或者干脆降低DHT11的读取频率,反正它一秒才更新一次,把读取时刻安排在系统负荷最低的位置即可。
5.4 为什么有时读到“0%RH”或“0°C”但校验通过了
这个现象初看很诡异:校验和明明是好的,但湿度是0,温度也是0。排查了一下,发现是我在读取时序时,数据位解析的阈值设置成了20us,结果把所有的1都误判成了0,导致四个数据字节全为0,校验和也是0,校验自然“通过”。
所以校验通过不等于数据正确,只能说传输过程中没有发生位翻转。如果四字节全为0,那只是校验函数的巧合。解决方法是加一层数据合理性判断:温度正常范围在0~50°C,湿度正常范围在20%RH~90%RH,如果读回的数据明显越界,即使校验通过也按无效处理。
这类问题也提醒我们:传感器的数据有效性判断不能只依赖协议自带的校验,还要结合行业知识和量程范围做二次过滤。这在工程上是特别值得养成的习惯。
5.5 读取间隔太短导致数据不刷新
前面提到过DHT11自身的采样周期约1秒。如果你的代码读取间隔只有200ms,那么大概率会连续多次读到同一帧数据。有些应用看着“数据没变”就以为是传感器坏了,实际上只是没到更新时间。
调试时可以这样做:先读一次,丢弃;延时1秒以上,再读一次,对比两次数据是否不同。如果不同,说明传感器在正常更新。在最终项目代码里,建议用时间戳控制读取频率,比如限定最小读取间隔为1.1秒或1.2秒,留出一点余量,避免刚好卡在传感器更新的边界上产生临界问题。
6. 驱动的一些进阶考虑
6.1 如果MCU主频不同,延时函数怎么办
不同主频下,基于SysTick或NOP的延时函数都要重新校准。最稳妥的办法是用一个逻辑分析仪接在GPIO上,故意把某个引脚在延时期间拉高,实测高电平宽度,再调整延时常量。
比如你在STM32F103(72MHz)上调好了一个20us的延时,搬到STM32G030(64MHz)上,同样的延时代码实际可能会变成22us或18us,虽然DHT11的起始信号允许18~30us的范围,但裕量小了总归不放心。校准一遍心里踏实。
6.2 从DHT11到DHT22的升级路径
如果你觉得DHT11的精度不够,DHT22(AM2302)是一个平滑的升级选择。DHT22同样是单总线协议,引脚完全兼容,但数据格式里小数位是真实有效的,湿度分辨率0.1%RH,温度分辨率0.1°C,量程也更大。协议时序和数据帧结构基本一致,校验方式也一样,所以驱动代码只需要微调(比如温度负数的处理),就能完成升级。
DHT22的温度数据在零下时用二进制补码表示,最高位为1。写驱动时如果直接按无符号数解析,零下温度会变成一个极大的正数,这时候要判断最高位是否为1,如果是就减去0x10000再除以10。DHT11协议里没有这个问题,因为DHT11的温度范围不涉及零下。
6.3 单总线的抗干扰设计
DHT11的单总线如果走线过长,容易受到电磁干扰。实测经验是:
- 线长尽量小于50cm,越短越好。
- 数据线和电源线、地线挨近走,减少环路面积。
- 如果必须走1米以上的延长线,在传感器端并联一个0.1uF的陶瓷电容到地,吸收高频干扰;也可以在数据线上串联一个几十欧姆的电阻,抑制振铃。
- 传感器和MCU的电源最好共用一颗100nF+10uF组合去耦电容,避免供电噪声直接耦合进数据线。
我见过有人把DHT11用好几米的网线拉到室外测环境温湿度,读回来的数据偶尔出错,加了一个瓷片电容后情况明显好转。这种小处理的成本极低,但效果立竿见影。
6.4 复用单总线驱动做其他器件
DHT11的单总线驱动掌握之后,做DS18B20(温度传感器)的驱动会轻松很多,因为DS18B20的通信基础同样是“主机拉低起始信号 → 从机响应脉冲 → 按位读写数据”,只不过DS18B20还支持总线上的多节点寻址(每个器件有唯一的64位ROM码),协议更复杂一些。但底层对电平宽度的测量逻辑是相通的,你在DHT11驱动里写好的“等待低电平结束→测高电平宽度→判决0/1”这套状态机,可以直接迁移过去用。
7. 关于DHT11,我最后想说的
很多人纠结DHT11精度不够、协议老气,然后费尽心思去论证SHT30更适合项目,这当然没错。但从学习的角度看,DHT11恰恰因为它没有硬件协议控制器帮衬,反而逼着你把时序、电平、上拉、中断延迟这些基本功全部过了一遍。你把这些东西弄明白了,以后看任何器件的驱动代码都会有一种“拆穿魔术”的感觉。
之前带过一个实习生,做温湿度采集项目时直接拷贝了一份DHT11驱动,跑通了就开始写报告。我特意让他把驱动删掉、只看数据手册自己写一遍,他写完拿过来给我看,我指出了几处阈值和超时处理上的细节,他改完之后跟我说:“原来之前抄的代码里,有很多让我疑惑但没深究的地方,现在全通了。”后来他去调其他传感器驱动,明显顺手很多。
所以这篇拆解的核心目的不是让你把DHT11驱动背下来,而是希望你理解每一次电平变化背后的原理,知道超时为什么要设、校验为什么能过、阈值为什么取40us。把这些点都串起来,单总线通信就不再是一个玄学,而是你能掌控的一种工具。
真到了项目里,如果你用的是STM32F103,我上面的代码可以直接编译跑起来;如果是其他MCU,按第4.4节列出的改动点改一下底层函数就行。遇到任何时序问题,别忘了先拿起示波器或逻辑分析仪看波形,再决定要不要动代码。数据手册和时序表放在前面,遇到问题回去对照,基本都能找到原因。