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