STM32+DHT22底层驱动与时序分析全记录

AM2302就是DHT22 作为DHT11的增强版本,DHT22的精度可以到小数点后一位。而DHT11只能到达整数的精度。

作为一名早期的Arduino-Scratch 开发者。 控制这种单总线协议的设备无疑是十分简单的,都被封装好了。 如下:

1
2
3
4
5
6
7
#include <DHT22.h>  
#define pinDATA SDA
DHT22 dht22(pinDATA);

//下面两行代码放到loop()
float t = dht22.getTemperature();
float h = dht22.getHumidity();

而如何和通过单总线通讯和具体的硬件设计,当时的我尚未知晓。 如果只停留在这个认知,无疑是很肤浅的。

外围电路设计

根据数据手册 Pasted image 20260428150648 我设计了如下电路 Pasted image 20260428150747 其中R7为上拉电阻

到这里,我以为这个模块已经大功告成了,觉得获取数据和写程序不过尔尔。 等到我开始使用stm开始编程,发现存在以下问题:

  1. hal库没有提供μs级别的delay
  2. 时序逻辑有问题 接下来我们来逐步解决。

delay_us的实现

我使用了DWT delay,通过逻辑分析仪实测还是比较准确的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DWT 初始化(在 main 中调用一次)
void DWT_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
// 微秒延时(自动适配系统时钟)
void delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
// 自动计算:1us = 系统时钟频率 / 1,000,000 个周期
uint32_t ticks = us * (SystemCoreClock / 1000000);
while((DWT->CYCCNT - start) < ticks);
}
  • DEMCR寄存器:启用内核的调试和跟踪功能(必须先开,否则DWT不可用)
  • DWT->CYCCNT:一个32位硬件计数器,每个CPU时钟周期自动累加1
  • DWT->CTRL:控制寄存器的CYCCNTENA位使能计数

特点:不占用CPU额外资源,纯硬件自动计数,无中断开销 但是delay_us() 是 busy-wait,CPU在循环里空转,会占用CPU时间

  • SystemCoreClock 是系统时钟频率(单位Hz,例如72,000,000表示72MHz)
  • SystemCoreClock / 1,000,000 = 每微秒运行的CPU周期数
  • 乘以us = 总周期数

什么意思? 也就是系统1秒钟假设运行72,000,000次,那么1μs(\(10^-6\)秒)=72,000,000/1,000,000=72

也就是72个cpu周期,就是1μs

解决了delay的问题,接下来我们要解决时序逻辑问题

时序逻辑

这是AM2302的数据定义 Pasted image 20260428154137 Pasted image 20260428154215 当总线是闲置时,因为上拉电阻。总线此时为高电平。

  • 现在,微控制器想要获取信号,此时拉低总线电平(至少800μs)也就是0.8ms
  • 然后传感器拉低80μs,再拉高80μs,响应起始信号
  • 输出40位数据,16位湿度,16位温度,8位校验数据
  • 接受到数据后,经过校验,处理符号并把数据除以10,我们就能得到数据。

具体分析

1发送起始信号

  • 微处理器拉低一段时间
  • 然后总线恢复高电平

2.响应信号

Pasted image 20260428154746

  • 传感器拉低总线80μs
  • 传感器拉高总线80μs
  • 提示获取响应

3.数据传输

Pasted image 20260428155729 也就是当传感器相应后,发出40位数据

  • 16位湿度
  • 16位温度
  • 8位检验 那么怎么表示0和1
  • 拉高26μs(典型值)为0
  • 拉高70μs(典型值)为1 从逻辑分析图上看,我们可以说,瘦的是0,胖的是1

这里我获取了一次数据,我们完整走一次数据的传输和获取。

湿度

Pasted image 20260428154949 我们结合图片 得到 0000 0010 1101 1100 换算成10进制是 732 也就是湿度是73.2%

温度

Pasted image 20260428160528 获取到数据 0000 0001 0000 1001 换算成十进制就是265 也就是温度是26.5℃ 注意 该数据第一位是符号位,0位正,1为负

校验

Pasted image 20260428160759 得到数据 11101000 我们来校验这个校验位是否正确 00000010 + 11011100 +00000001 +00001001 =11011110+00001010=11101000 也就是数据是无误的

太好了,我们现在终于获取到了实时的数据了!!!!

现在我要给你泼冷水 此时获取到的数据并不是实时的。 Pasted image 20260428161334 此时获取到的不是实时温度,而是前一次的测量值。

哈哈。。。。。 让我们再获取一次。现在我们终于得到了实时的温湿度了。

4.程序设计

最后附上这个单总线的信号特性。我们可以开始设计程序 Pasted image 20260428161832

我分配了PA1引脚给这个DHT22,我们就以PA1为例:

  1. 我们要设置PA1为输出模式,推荐开漏,拉低引脚,使用HAL Delay 1ms,然后释放总线
  2. 我们设置引脚为输入模式,等待传感器拉低 -> 传感器拉低 -> 传感器拉高
  3. 此时传感器拉高结束,开始获取数据
  4. 我们弄一个for循环 循环40次,每一个获取一个bit
    1. 我们跳过低电平
    2. 现在电平被拉高,我们记录高电平的时间,注意,我们要有超时保护。
    3. 然后我们以高电平的时长判断数据是0还是1,我们可以以40,或者50μs作为分界
    4. 然后存入数据
  5. 此时,我们获得了一个40bit的数据,此时我们需要校验和数据处理
  6. 校验成功后,以指针返回或者以结构体返回数据。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

int DHT22_Read(float *humidity, float *temperature)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
uint8_t data[5] = {0};
uint8_t bit = 0;
uint32_t tick_start;
uint32_t high_time;
uint32_t start_dwt;

// ========== 1. 发送起始信号 ==========
GPIO_InitStruct.Pin = dht22_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(dht22_GPIO_Port, &GPIO_InitStruct);

HAL_GPIO_WritePin(dht22_GPIO_Port, dht22_Pin, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(dht22_GPIO_Port, dht22_Pin, GPIO_PIN_SET);

// ========== 2. 切换为输入模式 ==========
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(dht22_GPIO_Port, &GPIO_InitStruct);

// 等待DHT22响应(拉低总线)
tick_start = HAL_GetTick();
while(HAL_GPIO_ReadPin(dht22_GPIO_Port, dht22_Pin) == GPIO_PIN_SET)
{
if((HAL_GetTick() - tick_start) > 2) return -1; // 2ms超时
}

// 等待DHT22拉高(跳过80us低电平)
tick_start = HAL_GetTick();
while(HAL_GPIO_ReadPin(dht22_GPIO_Port, dht22_Pin) == GPIO_PIN_RESET)
{
if((HAL_GetTick() - tick_start) > 1) return -1;
}

// 等待DHT22拉低(跳过80us高电平,准备开始数据)
tick_start = HAL_GetTick();
while(HAL_GPIO_ReadPin(dht22_GPIO_Port, dht22_Pin) == GPIO_PIN_SET)
{
if((HAL_GetTick() - tick_start) > 1) return -1;
}

// ========== 3. 读取40位数据 ==========
for(int i = 0; i < 40; i++)
{
// 等待当前bit开始的低电平(50us)
while(HAL_GPIO_ReadPin(dht22_GPIO_Port, dht22_Pin) == GPIO_PIN_RESET);

// 测量高电平持续时间(微秒)
start_dwt = DWT->CYCCNT;
while(HAL_GPIO_ReadPin(dht22_GPIO_Port, dht22_Pin) == GPIO_PIN_SET)
{
// 超时保护(超过2ms)
if(((DWT->CYCCNT - start_dwt) / (SystemCoreClock / 1000000)) > 2000)
return -1;
}
high_time = (DWT->CYCCNT - start_dwt) / (SystemCoreClock / 1000000);

// 根据高电平持续时间判断bit(阈值50us)
if(high_time > 50)
{
bit = 1; // 高电平约70us表示 '1'
}
else
{
bit = 0; // 高电平约26-28us表示 '0'
}

// 存入数据
data[i / 8] <<= 1;
data[i / 8] |= bit;
}

// ========== 4. 校验 ==========
uint8_t checksum = data[0] + data[1] + data[2] + data[3];
if(checksum != data[4]) return -2;

// ========== 5. 计算 ==========
uint16_t raw_humidity = (data[0] << 8) | data[1];
uint16_t raw_temp = (data[2] << 8) | data[3];
if (raw_temp & 0x8000)
{
raw_temp = raw_temp & 0x7FFF;
*temperature = -(raw_temp / 10.0f);
}
else
{
*temperature = raw_temp / 10.0f;
}
*humidity = raw_humidity / 10.0f;
return 0;
}

参考资料 AM2302 -PDF数据手册-参考资料-立创商城