GPIO模拟时序控制外设2——DHT11

前言

在上一篇中介绍了,使用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温湿度采集模块做了一个时序的模拟。文中如有不足欢迎大家批评指正。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值