STM32 HAL库与DHT11温湿度模块集成使用指南
今日小雨,闲来无事,翻出之前买来的DHT11模块,使用手册自己从头手写了一遍DHT11模块的代码。写下这篇博客来记录一下自己的学习过程
DHT11产品介绍
DHT11 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。 它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有极高的可靠性 与卓越的长期稳定性。传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。因此该产品具有品质卓越、超快响应、抗干扰 能力强、性价比极高等优点。每个DHT11传感器都在极为精确的湿度校验室中进行校准。校准系数以程序的形式储存在OTP内存中,传感器内部在检测信号的处 理过程中要调用这些校准系数。单线制串行接口,使系统集成变得简易快捷。超 小的体积、极低的功耗,信号传输距离可达20米以上,使其成为各类应用甚至 最为苛刻的应用场合的最佳选则。产品为 4 针单排引脚封装。连接方便,特殊 封装形式可根据用户需求而提供。
关于DHT11更加详细的介绍,可以找DHT11参考手册,这里不做赘述。
DHT11电源
DHT11的供电电压为 3-5.5V。传感器上电后,要等待 1s 以越过不稳定状态在此 期间无需发送任何指令。
DHT11数据格式
DHT11只有一根数据线,使用的是串行通信的方式,一次完整的通信中,数据位一共是40bit。分别如下:
8bit湿度整数数据+
8bit湿度小数数据 +
8bi温度整数数据+8bit温度小数数据 +
8bit校验和 (数据传送正确时校验和数据等于“8bit湿度整数数据+8bit湿度小数数据 +8bi温度整数数据+8bit温度小数数据”所得结果的末8位。)
DHT11通信协议
DHT11采用的是主从模式的通信方式。通信的流程为:
上图是DHT11通信的时序图,我们从左往右看,最开始为主机(stm32)发送开始信号,接着转为从机响应一个应答信号,接着数据一位一位的输出。下面详细的解释一下这个过程:
最开始,数据线为高电平,这是数据线的空闲状态,在写代码时,我们需要将GPIO设置为上拉输入,给 GPIO口一个上拉电阻,将数据线的默认电平设置为高电平。开始信号为拉低数据线至少18ms,接着拉高20-40us,后面的事情都是DHT11这个从机的事情了,开始信号结束后呢,DHT11模块会把数据线拉低80us,接着拉高80us,准备发送数据。然后再拉低50us,作为发送数据的开始,每一位数据的间隙都是这50us的低电平,发完40位数据,即最后一bit数据发送完毕后,DHT11会再次拉低总线50us,随后就进入了空闲状态,等待主机的开始信号了。欧克,DHT11通信流程梳理完毕,代码如何实现呢?下面是代码的实现过程。
DHT11代码实现
首先,DHT11只有一根数据线,所以我们只需要设置一个GPIO引脚,因为只有一个引脚,所以这个引脚需要根据情况来配置模式,当主机(stm32)需要发送开始信号时,这个GPIO得配置为输出模式,开始信号后,DHT11的响应信号和数据的读取,这时就需要将GPIO配置为输入模式。其次是在HAL库中没有微妙级别的延时函数,我们需要配置一个定时器来做一个简单微妙级别的延时函数,这个函数实现起来也很简单,将定时器的频率设置为1Mhz就行了,即1us 计数寄存器加1。具体实现参照下面的代码。
首先打开Cube MX创建一个工程,先打开外部晶振作为我们的时钟源
然后配置GPIO,我们使用PA11来作为数据线。
可以将GPIO设置为输出模式,输入模式都可以,将GPIO设置为上拉,让GPIO口的空闲电平保持为高电平。
在代码中,我们再对这个GPIO口的代码做封装。接下来配置一个定时器,因为只需要定时器做一个简单的计时功能,所以使用基本定时器就行了,我使用的芯片是f103rct6,基本定时器是TIM6和TIM7,这里选择定时器6,TIM6挂载在APB1时钟线上,在时钟树设置时,将APB1的频率设置为72M,那么定时器的预分频系数设置为72-1就可以得到1Mhz的频率,也就是1us计数一次。
然后把串口1打开将数据发送到电脑上,或者加一个oled驱动也行,这里就使用串口1来与电脑通信。
时钟树配置如图:
注意APB1的时钟频率为72M就对了。
欧克下面开始码代码:
首先把微妙延时函数解决:
这个代码可以实现us级别的延时。
void delay_us(uint32_t Delay)
{
__HAL_TIM_SET_COUNTER(&htim6, 0); //设置定时器当前计数寄存器为0
__HAL_TIM_ENABLE(&htim6); //开始计数
while (__HAL_TIM_GET_COUNTER(&htim6) < Delay) //延时
{
}
__HAL_TIM_DISABLE(&htim6); //延时完毕,关闭定时器
}
接下来重定向一下printf函数,将输出结构输出到串口1上
记得加上头文件#include“stdio.h”。
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//等待上一次串口数据发送完成
USART1->DR = (uint8_t) ch; //写DR,串口1将发送数据
return ch;
}
然后勾选这个按钮,就可以使用printf函数了
再写一个头文件
#ifndef __DHT11_H
#define __DHT11_H
#endif
#include "main.h"
#include "gpio.h"
#include "tim.h"
#include "stdio.h"
#include "stm32f1xx_hal.h"
#define DHT11_DAT_PIN GPIO_PIN_11
#define DHT11_DAT_Port GPIOA
void DHT11_Send(void);
uint8_t Is_DHT11_ASK(void);
uint8_t DHT11_Data(uint8_t *humi, uint8_t *temp);
typedef enum DHT11_STATUS{
DHT11_DAT_RESET,
DHT11_DAT_SET
}DHT11_STATUS;
接下来是dht11的c文件了,一上午写的代码,最开始出错了一直以为是时序逻辑错了,最后发现是cnt没有初始化,哎呦,设置一个变量的时候一定要有习惯给它初始化一下,不然就会出现一些意想不到的事情。
#include "DHT11.h"
/* 拉低数据线 */
static DHT11_STATUS DHT11_DATA_RESET(void)
{
DHT11_STATUS STA = DHT11_DAT_RESET;
HAL_GPIO_WritePin(DHT11_DAT_Port,DHT11_DAT_PIN,GPIO_PIN_RESET);
return STA;
}
/* 拉高数据线 */
static DHT11_STATUS DHT11_DATA_SET(void)
{
DHT11_STATUS STA = DHT11_DAT_SET;
HAL_GPIO_WritePin(DHT11_DAT_Port,DHT11_DAT_PIN,GPIO_PIN_SET);
return STA;
}
/* 获取数据线的状态 */
static GPIO_PinState DHT11_DATA_STA(void)
{
return HAL_GPIO_ReadPin(DHT11_DAT_Port,DHT11_DAT_PIN);
}
/* 主机输出模式 */
static void DHT11_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; //设置为推挽输出
GPIO_InitStruct.Pin = DHT11_DAT_PIN;
GPIO_InitStruct.Pull = GPIO_PULLUP; //加上上拉电阻,保证总线空闲时刻为高电平
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(DHT11_DAT_Port,&GPIO_InitStruct);
}
/* 从机输入模式 */
static void DHT11_INPUT(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; //设置为输入模式
GPIO_InitStruct.Pin = DHT11_DAT_PIN;
GPIO_InitStruct.Pull = GPIO_PULLUP; //加上上拉电阻,保证总线空闲时刻为高电平
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(DHT11_DAT_Port,&GPIO_InitStruct);
}
/* 开始信号 */
void DHT11_Send(void)
{
DHT11_OUT(); //切换为输出模式
DHT11_DATA_RESET(); //拉低数据线20毫秒作为开始信号
HAL_Delay(20);
DHT11_DATA_SET(); //拉高数据线并等待20-40us
delay_us(30);
}
/* 从机应答 */
uint8_t Is_DHT11_ASK(void)
{
uint8_t cnt = 0; //计时变量
DHT11_Send(); //发送开始信号
DHT11_INPUT(); // 切换为输入模式
while(DHT11_DATA_STA() == 1 && cnt<100)
{
cnt++;
delay_us(1); //等待低电平信号的到来
}
if(cnt >= 100)
return 1; //如果此时还是为高电平,则dht11没响应
else cnt = 0;
while(DHT11_DATA_STA() == 0 && cnt<100) //DHT11响应信号
{
cnt++;
delay_us(1);
}
return (cnt>100)?1:0; //判断响应信号是否正常
}
/* 接收数据(Bit) */
uint8_t DHT11_Data_Bit(void)
{
uint8_t cnt = 0;
while(1 == DHT11_DATA_STA() && cnt<100) //dht11响应信号后,会再次拉高数据线80us,用一个while循环等待数据信号的到来
{
cnt++;
delay_us(1);
}
if(cnt>=100)
{
return 1;
}
else cnt = 0;
while(0 == DHT11_DATA_STA() && cnt<100)
{
cnt++;
delay_us(1);
} //DHT11每发送一个数据位之前还会将数据线拉低50us,这个while循环也是用来等待数据位的到来
if(cnt>=100)
{
return 1;
}
/* 26~28us的高电平代表数据0,70us的高电平代表数据1*/
else delay_us(30); //延时30us 这是一个阈值,判断高电平信号的时间;
return (DHT11_DATA_STA() == 1) ? 1: 0; // 高电平时间大于26~28us返回1,反之为0;
}
/* 接收数据(Byte) */
uint8_t DHT11_Data_Byte(void)
{
uint8_t data = 0;
for(uint8_t i = 0;i<8;i++)
{
data <<= 1;
data |= DHT11_Data_Bit();
}
return data;
}
uint8_t DHT11_Data(uint8_t *humi, uint8_t *temp)
{
uint8_t buf[5]; //存储温湿度的数值
Is_DHT11_ASK();
for(uint8_t i =0; i<5;i++)
{
buf[i] = DHT11_Data_Byte();
}
if(buf[0]+buf[1]+buf[2]+buf[3] == buf[4])
{
*humi = buf[0];
*temp = buf[2];
}
}
最后将函数用到main函数中就可以输出数据到电脑上了
uint8_t t=0,h=0;
DHT11_Send();
while(Is_DHT11_ASK())
{
printf("error\r\n");
HAL_Delay(1000);
}
printf("success\r\n");
/* USER CODE END WHILE */
while(1)
{
DHT11_Data(&h,&t);
printf("湿度:%d %%RH 温度:%d^C\r\n",h,t);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
看一下效果:
ok,今天的学习就到此结束了。还请大家帮我指正一下我的不足。
作者:指针的方向