单总线通信实战应用:DHT11器件原理+ 协议 + 代码全拆解
DHT11 传感器温湿度读取
DHT11 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有较高的可靠性与卓越的长期稳定性。传感器包括一个电容式感湿元件和一个 NTC 测温元件,并与一个高性能 8 位单片机相连接。

主要参数如下,其中关键参数我们进行标红:

1.DHT11 传感器通信协议解析
1.1 单总线传输的数据格式
单总线数据格式定义如下:

其中,数据格式部分为湿度数据 + 温度数据 + 数据校验位,温湿度数据的整数部分和小数部分各占 8 位。发送的数据中 MSB 位在前,为大端序,校验位为温湿度数据之和的第八位:


当校验数据不对时,说明数据接收产生错误,需要舍弃该次获取的数据,接收下一次数据。
这里,注意 DHT11 传感器温度测量范围为-20° 到 60°,当发送的温度数据为负数时,温度数据的小数部分,即数据低 8 位的最高位置为 1:

1.2 单总线通信时序和电气特性
单片机发送一次开始信号后,DHT11 从低功耗模式转换到高速模式,待主机开始信号结束后,DHT11 发送响应信号,送出 40bit 的数据,并触发一次信采集。信号发送如图所示:


1.3 单片机读取 DHT11 传感器步骤
- 传感器上电延时,空闲状态保持高电平:

- 主机呼叫从机等待应答:

- 从机应答,主机 IO 口由输出转为输入状态,等待数据接收:

- 主机按位接收数据:


5. 从机释放总线,结束通信:

2.DHT11 传感器驱动代码
这里,首先简述一下,我们读取 DHT11 传感器温湿度数据的方法:

我们在主机发送开始信号后,主机数据引脚转变为输入状态,释放总线,此后总线上所有电平状态变化都由从机控制,我们使用主机数据引脚读取高低电平变化,这里我们定义一个高低电平变化为一个脉冲,我们记录每一个脉冲信号在电平变化后电平保持的时间,通过对比高电平保持时间和 TH0、TH1 来判断 DHT11 传感器发送的数据位是 0 还是 1。40 位数据对应 80 个数据脉冲,从第四个脉冲开始(包括第四个脉冲),每隔两个脉冲后的高电平保持时间可作为判断 DHT11 传感器发送的数据位是 0 还是 1 的标志。
这里,我们首先自定义两个异常类,用于在 DHT11 数据读取产生错误时抛出异常:
# 自定义异常类:校验值错误
class InvalidChecksum(Exception):
"""
InvalidChecksum 异常类,用于指示校验值错误的异常情况。
该异常在数据校验失败时被抛出,通常用于检测数据完整性,如 CRC 校验错误或其他校验和错误。
Attributes:
message (str): 异常信息,描述校验失败的原因。
"""
pass
# 自定义异常类:数据脉冲计数错误
class InvalidPulseCount(Exception):
"""
InvalidPulseCount 异常类,用于指示数据脉冲计数错误的异常情况。
该异常通常在脉冲信号的数量不符合预期时被抛出,例如在信号解码过程中脉冲数量异常。
Attributes:
message (str): 异常信息,描述脉冲计数错误的原因。
"""
pass
然后定义了一个名为 DHT11 的类,用于与 DHT11 温湿度传感器进行交互,定义一些类相关的变量,并在类的构造函数中,初始化了一些实例变量,包括连接 DHT11 传感器的数据总线引脚、记录上次数据读取的时间、以及用于存储温度和湿度数据的变量。
# DHT11温湿度传感器类
class DHT11:
"""
DHT11类,用于通过GPIO接口读取DHT11温湿度传感器的数据。
该类封装了与DHT11传感器的通信,提供了获取温度和湿度的方法。
使用该类时,需要提供一个GPIO引脚用于与传感器通信,并且支持读取传感器数据。
Attributes:
pin (int): 连接 DHT11 数据引脚的 GPIO 号。
_last_measure (int): 上次数据读取的时间,单位为微秒。
_temperature (float): 温度数据,单位为摄氏度。
_humidity (float): 湿度数据,单位为百分比。
Methods:
measure(): 读取 DHT11 传感器数据,并更新温度和湿度值。
humidity(): 获取当前测量的湿度值(单位:%RH)。
temperature(): 获取当前测量的温度值(单位:℃)。
"""
# DHT11传感器类相关类变量:
# 引脚电平不改变的最大计数值
MAX_UNCHANGED = const(100)
# 两次数据读取间隔时间,单位为微秒,200000微秒,即200毫秒
MIN_INTERVAL_US = const(200000)
# 高电平持续时间,单位为微秒,50微秒
# 当DHT11发送数据1时,高电平持续时间大于50us(典型值为71us)
# 当DHT11发送数据0时,高电平持续时间小于50us(典型值为24us)
HIGH_LEVEL = const(50)
# 预期的脉冲数目,即 初始化后从机应答产生的4个脉冲+DHT11输出的40个数据位对应的高低电平脉冲
EXPECTED_PULSES = const(84)
def __init__(self, pin: int):
"""
初始化 DHT11 传感器。
Args:
pin (int): 连接 DHT11 数据引脚的 GPIO 号。
Returns:
None
"""
# 初始化实例变量,均为私有变量,外界不可访问
self._pin = pin
# 记录上次数据读取的时间
self._last_measure = time.ticks_us()
self._temperature = -1
self._humidity = -1
然后定义了一个名为 measure 的方法,用于读取温湿度数据。首先检查是否已经等待了足够的时间进行下一次测量,如果没有,则直接返回。然后发送初始化信号给从机,等待从机应答。接着捕获从机发送的高低电平脉冲持续时间序列,并将其转换为 5 字节数组。最后计算校验和,并更新温度和湿度数据。
def measure(self) -> None:
"""
读取 DHT11 传感器数据,并更新温度和湿度值。
Args:
None
Raises:
InvalidChecksum: 如果数据校验和错误。
InvalidPulseCount: 如果数据脉冲计数错误。
"""
# 获取当前时间
current_ticks = time.ticks_us()
# 检查是否已经等待了足够的时间进行下一次测量,若是没有,则直接返回
if time.ticks_diff(current_ticks, self._last_measure) < DHT11.MIN_INTERVAL_US and (
self._temperature > -1 or self._humidity > -1
):
return
# 发送初始化信号,呼叫从机
self._send_init_signal()
# 从机应答后发送数据,主机对应Pin转换为输入模数
# 获取从机发送的高低电平脉冲持续时间的序列
pulses = self._capture_pulses()
# 根据DHT11发送每位数据的高电平持续时间判断发送数据0还是1
# 将40位数据序列转换为5个字节数组输出
buffer = self._convert_pulses_to_buffer(pulses)
# 计算校验和,若校验和错误,则抛出InvalidChecksum异常
self._verify_checksum(buffer)
# 计算湿度数据,为湿度整数部分+小数部分
self._humidity = buffer[0] + buffer[1] / 10
# 计算温度数据,为温度整数部分+小数部分
self._temperature = buffer[2] + buffer[3] / 10
# 更新上次数据读取的时间 = 当前时间
self._last_measure = time.ticks_us()
其方法运行流程为:

measure 方法主要调用了以下几个方法:
- _send_init_signal 方法:发送初始化信号,呼叫从机

# 发送初始化信号
def _send_init_signal(self) -> None:
"""
发送初始化信号,呼叫从机。
Args:
None
Returns:
None
"""
# 主机引脚为下拉输出模式
self._pin.init(Pin.OUT, Pin.PULL_DOWN)
# 主机拉高总线50ms,此时DHT11引脚为输入状态检测外部信号
self._pin.value(1)
time.sleep_ms(50)
# 主机拉低总线18ms,发送初始化信号,呼叫从机
self._pin.value(0)
time.sleep_ms(18)
- _capture_pulses 方法:捕获 DHT11 传感器返回的 80 个脉冲的持续时间序列

# 捕获DHT11传感器返回的80个脉冲的持续时间序列
# 使用@micropython.native装饰器,将方法编译为机器码,提高运行效率
@micropython.native
def _capture_pulses(self) -> bytearray:
"""
捕获DHT11传感器返回的80个脉冲的持续时间序列。
Returns:
bytearray: 脉冲高低电平持续时间的序列。
Raises:
InvalidPulseCount: 如果捕获的脉冲数目不正确。
"""
# 将引脚转化为上拉输入模式
pin = self._pin
pin.init(Pin.IN, Pin.PULL_UP)
# 记录引脚电平的临时变量
val = 1
# 高低电平脉冲的计数变量
idx = 0
# 创建一个空数组,用于存储84个脉冲
# 其中:
# 前4个脉冲为从机应答产生的脉冲
# 后80个脉冲为数据位
transitions = bytearray(DHT11.EXPECTED_PULSES)
# 记录引脚电平不变次数的变量
unchanged = 0
# 记录当前时间
timestamp = time.ticks_us()
# 当引脚电平保持不变的次数超过DHT11.MAX_UNCHANGED时,跳出循环
while unchanged < DHT11.MAX_UNCHANGED:
# 检测引脚电平是否改变
if val != pin.value():
# 当idx大于DHT11.EXPECTED_PULSES时,说明捕获脉冲数目错误
# 抛出InvalidPulseCount异常
if idx >= DHT11.EXPECTED_PULSES:
raise InvalidPulseCount(
"Got more than {} pulses".format(DHT11.EXPECTED_PULSES)
)
now = time.ticks_us()
# transitions二进制序列记录了脉冲序列的持续时间
transitions[idx] = now - timestamp
timestamp = now
# idx自增
idx += 1
# val相当于取反操作
val = 1 - val
unchanged = 0
else:
# 引脚电平没有改变,记录引脚电平不变次数加1
unchanged += 1
# 数据接收完毕后,主机转换为下拉输出模式,结束通信
pin.init(Pin.OUT, Pin.PULL_DOWN)
# 判断接收的脉冲数目是否为期望的脉冲数目
if idx != DHT11.EXPECTED_PULSES:
# 若不是,则抛出InvalidPulseCount异常
raise InvalidPulseCount(
"Expected {} but got {} pulses".format(DHT11.EXPECTED_PULSES, idx)
)
# 返回捕获到脉冲的持续时间序列的后80个字节
# 忽略前4个字节,即从机应答产生的4个脉冲
return transitions[4:]
- _convert_pulses_to_buffer 方法:将捕获到的脉冲序列转换为 5 字节数组

# 使用@micropython.native装饰器,将方法编译为机器码,提高运行效率
@micropython.native
def _convert_pulses_to_buffer(self, pulses: bytearray) -> array.array:
"""
将捕获到的脉冲序列转换为5字节数组。
Args:
pulses (bytearray): 脉冲序列对应的二进制数组。
Returns:
array.array: 5字节数组,即湿度数据整数值,湿度数据小数值,温度数据整数值,温度数据小数值,校验和。
"""
# 捕获到脉冲的持续时间序列的后80个字节转换为对应二进制数据
binary = 0
# 根据高电平持续时间判断发送的二进制数据为0还是1
# 当DHT11发送数据1时,高电平持续时间大于50us(典型值为71us)
# 当DHT11发送数据0时,高电平持续时间小于50us(典型值为24us)
# range(0, len(pulses), 2)表示从索引0开始,每次递增2,直到达到pulses列表的长度为止
# 这段代码将会遍历pulses列表中的所有奇数索引的元素
for idx in range(0, len(pulses), 2):
# 将当前的binary值左移一位,并将pulses列表中对应索引的元素与DHT11.HIGH_LEVEL比较:
# 如果大于DHT11.HIGH_LEVEL,则将1加到binary中
# 否则将0加到binary中
# 这个过程相当于将pulses列表中的二进制数据拼接到一起,形成一个40位的二进制数
binary = binary << 1 | int(pulses[idx] > DHT11.HIGH_LEVEL)
# 将40位的二进制数分成5个字节,并将它们添加到buffer数组中
# 创建了一个元素为无符号字节类型的数组
buffer = array.array("B")
# shift变量表示要提取的二进制数的位数
# range(4, -1, -1)表示从4开始,每次递减1,直到-1为止
# 使用了一个从4到0的倒序循环,这意味着它会从最高位开始提取数据,直到最低位
for shift in range(4, -1, -1):
# 将binary右移shift*8位,然后与0xFF进行与运算,取到最低的8位
# 得到一个0~255之间的整数,其添加到buffer列表中
buffer.append(binary >> shift * 8 & 0xFF)
return buffer
- _verify_checksum 方法:验证校验和

# 使用@micropython.native装饰器,将方法编译为机器码,提高运行效率
@micropython.native
def _verify_checksum(self, buffer: array.array) -> None:
"""
验证校验和。
Args:
buffer (array.array): DHT11发送的40位数据对应的array数组。
Returns:
None
Raises:
InvalidChecksum: 如果校验和验证失败。
"""
checksum = 0
for buf in buffer[0:4]:
# 累加校验和
checksum += buf
# 取接收到数据校验和的低8位与buffer[4]-DHT11从机发送的校验和进行比较
if checksum & 0xFF != buffer[4]:
# 若不相等,则抛出InvalidChecksum异常
raise InvalidChecksum()
对于获取温湿度数据的功能来说,我们这里定义了两个方法,使用 @property 装饰器进行装饰,这样我们可以像访问对象属性一样调用该方法而无需显式调用,这两个方法实际上是调用了 measure() 方法来获取数据。:
# 获取湿度数据
# @property是一个装饰器,用于将一个方法转换为属性访问器
@property
def humidity(self) -> float:
"""
获取湿度数据。
该方法使用 @property 装饰器,可以像访问对象属性一样调用,而无需显式调用方法。
Args:
None
Returns:
float: 当前测量的湿度值(单位:%RH)。
Raises:
InvalidChecksum: 如果数据校验和错误。
InvalidPulseCount: 如果数据脉冲计数错误。
"""
self.measure()
return self._humidity
# 获取温度数据
@property
def temperature(self) -> float:
"""
获取温度数据。
该方法使用 @property 装饰器,可以像访问对象属性一样调用,而无需显式调用方法。
Args:
None
Returns:
float: 当前测量的温度值(单位:℃)。
Raises:
InvalidChecksum: 如果数据校验和错误。
InvalidPulseCount: 如果数据脉冲计数错误。
"""
self.measure()
return self._temperature
3.应用实验
首先,我们需将风雅一号板-环境与存储采集板插入到风雅一号板-通用兼容扩展板上,同时打开风雅一号板-环境与存储采集板上 SWITCH2 拨码开关的 WIRE 选项:


使用的引脚情况如下表所示:

这里,我们使用 GP13 连接 DHT11 的数据总线,并且使用 4.7K 上拉电阻与数据总线相连。需要注意是的是:
-
在读取的温湿度数值时,需连续读取 2 次才能获取实时数据,每次读出的数值实际上是上一次测量的结果
-
每次读取传感器间隔大于 2 秒即可获得准确的数据,不建议连续多次读取传感器
-
长时间暴露在太阳光下或强烈的紫外线辐射中,会使传感器性能降低
-
避免将元件长期放在结露、干燥以及具有酸性气体的环境中
-
气体的相对湿度受温度影响大,该传感器测量不同时间的相对湿度时需要保证温度近似
以下代码可以在我们提供的资料包中
elegance-devkit v1\Demo\37 OneWire_DHT11文件夹找到。
在主程序部分,我们首先声明两个保存温湿度数据的全局变量:
# ======================================== 全局变量 ============================================
# 湿度数据
humidity = 0.0
# 温度数据
temperature = 0.0
在初始化配置中,我们实例化数据线使用的引脚:
# ======================================== 初始化配置 ==========================================
# 延时等待设备初始化
time.sleep(3)
# 打印调试信息
print('FreakStudio : Using OneWire to read DHT11 sensor')
# 延时1s,等待DHT11传感器上电完成
time.sleep(1)
# 初始化单总线通信引脚,下拉输出
DHT11_PIN = Pin(13, Pin.OUT, Pin.PULL_DOWN)
# 初始化DHT11实例
dht11 = DHT11(DHT11_PIN)
在主程序部分,我们定期读取温湿度数据:
# ======================================== 主程序 ============================================
while True:
# 读取温湿度数据
temperature = dht11.temperature
humidity = dht11.humidity
# 打印温湿度数据
print("temperature: {}℃, humidity: {}%".format(temperature, humidity))
# 等待2秒
utime.sleep(2)
将代码烧录到树莓派 Pico 中,打开远程端口,可以看到树莓派 Pico 输出如下:

我们在 DHT11 传感器运行过程中使用打火机在传感器旁边燃烧:

暂时无法在飞书文档外展示此内容
可以看到 DHT11 传感器温度略有变化并且非常缓慢,实际上 DHT11 传感器并不适合测量绝对温度和绝对湿度,而适合测量相对温度和相对湿度,具有如下缺点:
- 精度较低:
DHT11温湿度传感器的精度不如一些高端传感器,其温度精度为 +2℃,湿度精度为 ±5%RH。 - 响应时间较长:
DHT11温湿度传感器的响应时间较长,需要一定时间才能稳定输出数据,不适合一些对实时性要求较高的应用场景。 - 传输距离受限:
DHT11温湿度传感器的传输距离较短,一般在几米范围内,如果需要远距离传输数据需要采用其他方法。

DHT11` 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有较高的可靠性与卓越的长期稳定性。传感器包括一个电容式感湿元件和一个 `NTC` 测温元件,并与一个高性能 8 位单片机相连接。
浙公网安备 33010602011771号