文章目录

前言

在上一篇中介绍了,使用GPIO模拟WS2812B的控制时序来实现对RGB灯的控制,本文继续使用GPIO模拟的方式来MCU实现与DHT11温湿度传感器的通信,获取温湿度信息。

模块简介

DHT11是采集温湿度的常用模块之一,其内部集成了检测湿度和温度的传感器以及一个8位的单片机用来处理温湿度的信息,正是得益于这个8位的内置MCU,让用户省去了温湿度的数据处理步骤,直接根据时序图获取温湿度即可。



硬件介绍

常见的模块有两种,一类是上图所示的四个管脚没有转接板的,这种模块在使用的时候,原理图绘制过程中一定要给DATA脚加上上拉电阻,否则大概率是无法正常通信获取数据的。



还有一种模块是3个管脚的,这种模块一般都是在转接板上加了上拉电阻的。为了降低电源对采集数据的影响,尽量选用LDO供电,下图中的C1也是起到一个电源滤波的作用。



这里笔者选择的是带有转接板的模块。


硬件连接

大致了解模块了之后,就需要对照原理图找到实际通信使用的GPIO,通过原理图,使用的GPIO是PA15。



通信时序

根据上一篇模拟的步骤,接下来就需要查看其通信时序,使用GPIO模拟时序来实现功能。
DHT11采用的是单总线(1-wire)的通信协议,使用一个数据线完成通信双方的数据交换,既然是单根数据线,那么它铁定是一个半双工的通信方式,同一时间要么MCU发送数据,也么DHT11发送数据。而且,DHT11并不能主动地给MCU传输数据,需要MCU给DHT11发送一个起始信号后,DHT11才会应答,进而进行数据传输。





需要注意的是DHT11与MCU间进行数据交换时,是轮流使用信号线的。
根据这段描述,可以大致总结出DHT11的通信时序:
1.主机(MCU)给DHT11(从机)发送一个起始信号;
2.DHT11(从机)接收到起始信号后,给主机返回一个应答以及准备数据传输的信号;
3.主机(MCU)接收到应答信号和数据传输的信号后,开始接收40bit的数据;
4.DHT11数据发送完成,产生结束信号,MCU停止接收数据。



DHT11的数据帧格式

DHT11一次会传输40bit的数据,其中每一位的定义如下:
第一个八位数据是湿度的整数位,第二个八位数据是湿度的小数位,第三个八位数据是温度的整数位,第四个八位是温度的小数位,最后一个八位数据是校验位,校验位=前四个八位数据之和,然后取低八位的值。



手册例子如下:



实际使用逻辑分析仪抓取的波形如下:



信号时序

弄清了大致的通信流程后,程序的整体框架有了,但是具体的信号表示方式还需要进一步查看手册。


1. 起始信号

首先来看起始信号,起始信号是MCU给DHT11发送的,此时MCU的GPIO作为输出脚,输出指定时长的高低电平。
起始信号对应的波形是下图红框里面的内容。



放大后如下图所示:



通过这个描述以及波形,可以看出起始信号首先需要MCU将数据脚拉低至少18ms,而后又拉高释放总线。
这里的拉高释放总线,怎么理解呢,前面提到了在DATA脚上始终有一个上拉电阻,当MCU与DHT11都没有输出控制DATA脚时,DATA脚的电压始终保持在高电平,也就是说DATA线是拉高空闲,释放状态。这里有个记住,只要MCU和DHT11不对DATA做控制时,DATA的默认状态就是高电平即可。


由此大致写出起始信号的函数:

/*********************************
函数名:DHT11_Start
函数功能:DHT11复位(起始信号开始转换)
形参:void
返回值:void
备注:
主机拉低至少18MS,再拉高20-40us,等待应答
**********************************/
void DHT11_Start(void)
{
    //DATA脚拉低18ms以上30ms以下
    DHT11_OUT_L;
    Systick_Delay_ms(20);
    //再拉高20-40us
    DHT11_OUT_H;
    Systick_Delay_us(30);
}

2.应答信号(响应信号)

DHT11的响应信号如下图红框所示,刚刚主机发送起始信号后,对DATA线进行了拉高释放,也就是说此时DATA线默认是高电平状态,当DHT11接收到起始信号后,会将DATA数据线进行一个拉低,这个拉低就叫做响应信号,响应后为了数据接收的稳定,DHT11也会再次将数据线拉高,释放数据线,告诉主机要准备开始传输数据。



这部分时序图放大后如下图所示:
DHT11会将DATA脚拉低约80us左右的时间,而后DHT11又会将数据脚拉高告诉主控要开始接收数据。
这里需要注意一点,此时的数据线是由DHT11来控制的,MCU要获取从DHT11上传输来的信号,需要将之前的输出模式切换为输入模式。具体的切换方式有两种方案,这个放到后面细说。知道此时是MCU读取DATA状态就可以。



这里先写个代码来获取应答信号,而数据准备发送的信号放到读取数据位的时候一起实现。

/*********************************
函数名:DHT11_Check
函数功能:DHT11应答检测
形参:void
返回值:u8
备注:返回1:未检测到DHT11的存在
返回0:检测到DHT11
**********************************/
u8 DHT11_Check(void)
{
    u8 retry=0;
    //等待起始信号的高电平时间结束
   while (DHT11_IN && retry<100)
    {
        retry++;
        Systick_Delay_us(1);
    };
    if(retry>=100)return 1;//如果DATA脚空闲时间超过100us还没有被DHT11拉低,说明应答失败
    else retry=0;
    //检测DHT11应答拉低的时间是否正常
  while (!DHT11_IN && retry<100)//DHT11会拉低40~80us
    {
        retry++;
        Systick_Delay_us(1);
    };
    if(retry>=100)return 1;    //如果低电平时间超过了100us说明应答失败
    return 0;//否则就应答成功
}

3.接收数据0与1

在获取到应答信号后,需要先处理一下预备发送数据的那一段时序;然后就是根据DATA的状态来判断数据位是0还是1了。与上一篇的WS2812B通信一样,DHT11的“1”和“0”也是由特定时长的高低电平组成的。
具体的逻辑“0”与逻辑“1”的表示如下图所示,都是先有一段54us的电平,然后根据高电平的时长来区分逻辑“0”与逻辑“1”。
逻辑“0”的高电平时间持续23-27us,逻辑“1”的高电平时间持续为68=74us。



那么怎么检测逻辑“0”和逻辑“1”呢,一个简单的方案,既然低电平的时间是一样的,那么读取的时候先等待低电平结束,然后延时超过40us,再去读取DATA的状态,如果此时还是高,则说明是逻辑“1”,如果变成了低则说明是逻辑“0”。
代码如下:

/*********************************
函数名:DHT11_Read_Bit
函数功能:从DHT11获取1位数据
形参:void
返回值:u8  1/0
备注:
数据线低50us表示开始传输数据,数据变为高电平时开始记录高电平持续时间
如果高电平时间持续26-28us表示数据位为0
如果高电平时间持续70us表示数据位为1
**********************************/
u8  DHT11_Read_Bit(void)
{
    u8 retry=0;
    //等待准备接收数据的信号结束
    while(DHT11_IN && retry<100)//等待变为低电平(应答后会拉低拉高,所以在这里要等待80)
    {
        retry++;
        Systick_Delay_us(1);
    }
    retry=0;
    //0和1的低电平时间段不作判定,直接等待
    while(!DHT11_IN &&retry<100)//等待变高电平
    {
        retry++;
        Systick_Delay_us(1);
    }
    Systick_Delay_us(40);//等待40us
    if(DHT11_IN)return 1;//“1”的高电平时间持续68us以上,延时40us后还是高电平
    else return 0;        //“0”的高电平时间持续26-28us,经过上面的延时已经变回低电平了。
}

4.获取数据

然后就是调用上面的bit接收函数来实现40bit的数据接收,高位在前,进而封装出对应的字节接收函数,然后接收5个字节的数据后,根据前面的数据帧格式,判断校验位,读取所需值即可。

/*********************************
函数名:DHT11_Read_Byte
函数功能:从DHT11获取1字节数据
形参:void
返回值:u8
备注:

**********************************/
u8  DHT11_Read_Byte(void)
{
    u8 i,dat;
  dat=0;
    for (i=0;i<8;i++) 
    {
           dat<<=1; 
        dat|=DHT11_Read_Bit();
    }
    return dat;
}

/*********************************
函数名:DHT11_Read_Data
函数功能:从DHT11获取数据
形参:void
返回值:u8 0,正常;1,读取失败
备注:
8bit湿度整数数据+8bit湿度小数数据
+8bi温度整数数据+8bit温度小数数据
+8bit校验和
**********************************/
u8  DHT11_Read_Data(u8 *temp,u8 *humi)
{
    u8 buf[5];
    u8 i;
    DHT11_Start();
    if(DHT11_Check()==0)
    {
        for(i=0;i<5;i++)//读取40位数据
        {
            buf[i]=DHT11_Read_Byte();
        }
        if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])//校验位判断
        {
            *humi=buf[0];
            *temp=buf[2];
        }
    }else return 1;
    return 0;
}

5结束信号

至于结束信号,是由DHT11产生的,一般不用做处理,MCU只用管接收完40bit的数据就行了。



输入输出切换

整个通信流程的代码在就大致是上面的内容了,但是还存在一个问题,GPIOA15在整个通信过程中既要作为输出提供起始信号,又要作为输入获取DHT11的数据,需要怎么实际操作呢,前面提到了有两种方案,其中之一也是笔者比较喜欢的,直接将GPIOA15初始化为开漏模式,此时负责输出高电平的PMOS被屏蔽,当我们对ODR写入1时,GPIOA15不再输出高点电平,也就是说MCU不再占用DATA脚,此时全靠外部上拉电阻与DHT11二者控制DATA线,此时直接调用库函数获取GPIOA15的高低电平就是实际的DATA状态了。

/*********************************
函数名:DHT11_Init
函数功能:DHT11初始化
形参:void
返回值:void
备注:既要输入又要输出
DHT11_DATA-----PA15-------开漏输出//jtag脚需要关闭
**********************************/
u8 DHT11_Init(void)//初始化DHT11
{
    GPIO_InitTypeDef  GPIO_InitStructure;//定义一个结构体的变量
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//初始化GPIOA端口的时钟
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_InitStructure.GPIO_Mode =  GPIO_Mode_Out_OD;//通用开漏输出
    GPIO_Init(GPIOA,&GPIO_InitStructure );
    DHT11_Start();  //复位DHT11
    return DHT11_Check();//等待DHT11的回应
}

还有一种方案是使用推挽输出模式,在获取GPIO状态是做切换,将GPIOA15切换回输入模式。为什么使用推挽模式时要切换模式呢,这是因为,在推挽输出模式下,GPIOA15要么在输出0要么在输出1,不可能出现开漏模式那种解除占用的情况,因此只能在整个过程中不断切换GPIO的输入输出模式才可以。
此法的初始化代码如下:

//PA11
#define DHT11_IO_IN()  {GPIOA->CRH&=0XFFFF0FFF;GPIOA->CRH|=8<<15;}//切换为输入模式
#define DHT11_IO_OUT() {GPIOA->CRH&=0XFFFF0FFF;GPIOA->CRH|=3<<15;} //切换为输出模式
//初始化DHT11的IO口 DQ 同时检测DHT11的存在
//返回1:不存在
//返回0:存在
u8 DHT11_Init(void)
{
     GPIO_InitTypeDef  GPIO_InitStructure;    
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);     //使能PA端口时钟
     GPIO_InitStructure.GPIO_Pin = DT;                 //PA15端口配置
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;          //推挽输出
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
     GPIO_Init(GPIOA, &GPIO_InitStructure);                 //初始化IO口
     GPIO_SetBits(GPIOA,DT);                         //PG11 输出高
    DHT11_Rst();  //复位DHT11
    return DHT11_Check();//等待DHT11的回应
}
//需要注意,使用此法时,上面的通讯流程代码都要稍作修改,代码太多了,这里就不贴出来了。

实际效果

在主函数调用初始化后可以正常获取到温湿度。


需要注意一点,市面上的DHT11由于厂家不同,其采样速率也不一样,有的模块可能每秒可以采集100次,但是有的模块只能每秒采集2次,单次获取的时间间隔小于这个时间就会出现乱码的情况。


总结

本文对常用的DHT11温湿度采集模块做了一个时序的模拟。文中如有不足欢迎大家批评指正。