这几天做毕业设计,要用到DHT11温湿度传感器,本来想着多一事不如少一事,随便找了几篇文章,对比了一下就随便找了个文章的代码尝试使用,结果不尽人意,试了好几个都移植失败,在移植过程中发现,目前网上的Dht11使用stm32hal库版本编写的代码都太过混乱,不易于移植,有的在读取函数中添加串口打印函数,有的全程使用原生的GPIOx,GPIO_PIN_x,有的胡乱命名,有的使用whlie循环实现微秒级延时,可是大家使用的MCU主频不一致,总之移植起来非常不方便。
尝试几个无果后,决定自己动手,丰衣足食,在参考了一些还不错的代码的逻辑后,编写了这个DHT11的驱动程序。借鉴了HAL库采用的模块化编写方式,以及面向对象的思维,我希望能写一个易于移植,易于使用的DHT11驱动程序。
我将IO口,引脚号,模块状态,数据缓冲区,微秒级延时函数这些必要的特性封装到了一个结构体当中,使用init函数进行初始化,这样在移植时就首先避免了之前遇到的全程使用原生的GPIOx,GPIO_PIN_x导致移植时添加工作量。
我将微秒级延时函数以函数指针的形式传入到dht11的句柄中,这样就避免了有人使用while循环,有人使用定时器,有人主频72MHZ,有人主频64MHZ等等千奇百怪的微秒级延时函数实现方式。移植时只要传入一个指向void delayUs(uint16_t us)的函数指针,具体的实现根据实际情况来。同时驱动内部的毫秒级延时函数也是基于微秒级延时函数。我是通过定时器来实现的微秒级延时,毕竟定时器不受中断影响,且最准确,具体实现如下
void delay_us(uint16_t us)
{
TIM1->CNT=0;
while(TIM1->CNT<us);
}
需要注意的是如果要使用我这种延时,需要将一个计数周期设为1us
同时我将读取函数和其他程序逻辑进行分离,避免之前遇到的在读取函数中添加串口打印函数这种影响读取效率的事情发生,读取时只需要传入指向保存温度,湿度数据的指针,如果需要对数据进行其他处理,可以在离开读取函数后进行,确保模块化设计
由于毕设和低功耗有关,就了解了一些常见低功耗做法,所以提供了一些反初始化的函数,反初始化会将数据引脚置为模拟态,模拟态下几乎没有电流,可以避免引脚悬空,大大降低功耗。DHT11的供电电流:测量 0.3mA 待机 60μA,如果在使用时,配合模块的VCC引脚的关闭,就可以大大降低待机时的功耗。在毕设中我就计划使用一个三极管开关电路和一个MCU引脚控制所有外接模块的VCC,毕竟大多数模块都没有低功耗设计,且不需要长期开启。休眠后只留下一个外部中断引脚,其他的全部置为模拟态,预计可以将平均电流从几百毫安降低到100uA以下
网上有很多有关DHT11的使用说明,我就不赘述这些原理了,主打一个易于移植,拿上来,看一下就能使用
驱动程序的头文件如下
#ifndef __DHT11_H
#define __DHT11_H
#include "main.h"
#include "stm32f1xx_hal.h"
#define DHT11_CLOCK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define DHT11_IO_INPUT 0
#define DHT11_IO_OUTPUT 1
#define DHT11_IO_ANALOG 2
typedef enum
{
DHT11_ON,
DHT11_OFF,
DHT11_ERROR,
FAULT
} Dht11State;
typedef struct Dht11Typedef
{
GPIO_TypeDef *IO;
GPIO_InitTypeDef DataIO;
Dht11State state;
uint8_t dta[5];
void (*DelayUs)(uint16_t);
} Dht11Typedef;
extern Dht11Typedef hdht111;
void Dht11_init(Dht11Typedef *hdht11, GPIO_TypeDef *IO, uint32_t Pin, void (*DelayUs)(uint16_t));
void Dht11_deinit(Dht11Typedef *hdht11);
void Dht11_read_data(Dht11Typedef *hdht11, double *temp, double *humi);
Dht11State Dht11_Check(Dht11Typedef *hdht11);
#endif
驱动文件本体如下
#include "dht11.h"
Dht11Typedef hdht111 = {0};
static uint8_t Dht11_dataVerification(Dht11Typedef *hdht11);
static void dht11_delay_ms(Dht11Typedef *hdht11, uint16_t ms);
static void Dht11_DataIO_init(Dht11Typedef *hdht11, uint8_t mode);
static uint8_t Dht11_read_bit(Dht11Typedef *hdht11);
static uint8_t Dht11_read_byte(Dht11Typedef *hdht11);
static void Dht11_IO_deinit(Dht11Typedef *hdht11);
static void Dht11_Reset(Dht11Typedef *hdht11);
/**
* @brief 输出模式下,控制引脚输出
* @param hdht11 : DHT11 handle
* @param PinState : 引脚输出状态
* @retval None
*/
#define Dht11_DataPinWrite(hdht11, PinState) \
do \
{ \
HAL_GPIO_WritePin((hdht11)->IO, (hdht11)->DataIO.Pin, (PinState)); \
} while (0);
/**
* @brief 输入模式下,读取引脚输入状态
* @param hdht11 : DHT11 handle
* @retval PinState
*/
#define Dht11_DataPinRead(hdht11) HAL_GPIO_ReadPin((hdht11)->IO, (hdht11)->DataIO.Pin)
/**
* @brief 用于实现毫秒级延时
* @note 该函数利用传入DHT11句柄中的微秒级延时函数进行延时,请确保DelayUs函数准确
* @param hdht11 : DHT11 handle
* @retval None
*/
static void dht11_delay_ms(Dht11Typedef *hdht11, uint16_t ms)
{
uint16_t i = 0;
for (i = 0; i < ms; i++)
{
hdht11->DelayUs(1000);
}
}
/**
* @brief 用于Dht11数据引脚的初始化,输出模式,输入模式,模拟模式
* @param hdht11 : DHT11 handle
* @param mode :输出模式:DHT11_IO_OUTPUT,输入模式:DHT11_IO_INPUT,模拟模式:DHT11_IO_ANALOG
* @retval None
*/
static void Dht11_DataIO_init(Dht11Typedef *hdht11, uint8_t mode)
{
if (mode == DHT11_IO_OUTPUT)
{
Dht11_DataPinWrite(hdht11, GPIO_PIN_SET);
hdht11->DataIO.Mode = GPIO_MODE_OUTPUT_PP;
hdht11->DataIO.Pull = GPIO_NOPULL;
hdht11->DataIO.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(hdht11->IO, &hdht11->DataIO);
}
else if (mode == DHT11_IO_INPUT)
{
hdht11->DataIO.Mode = GPIO_MODE_INPUT;
hdht11->DataIO.Pull = GPIO_PULLUP;
hdht11->DataIO.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(hdht11->IO, &hdht11->DataIO);
}
else if (mode == DHT11_IO_ANALOG)
{
hdht11->DataIO.Mode = GPIO_MODE_ANALOG;
hdht11->DataIO.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(hdht11->IO, &hdht11->DataIO);
}
}
/**
* @brief 用于DHT11数据校验
* @param hdht11 : DHT11 handle
* @retval 0:数据错误
* @retval 1:数据正确
*/
static uint8_t Dht11_dataVerification(Dht11Typedef *hdht11)
{
return hdht11->dta[0] + hdht11->dta[1] + hdht11->dta[2] + hdht11->dta[3] == hdht11->dta[4];
}
/**
* @brief 用于Dht11的初始化,包括指定数据引脚,指定微秒级延时实现方法,IO时钟使能
* @note 该函数会延时1秒用于确保DHT11启功稳定,请在dht11.h中指定使能的时钟
* @param hdht11 : DHT11 handle
* @param IO:数据引脚使用的IO口
* @param Pin:数据引脚使用的引脚号
* @param DelayUs :指向微秒级延时函数的函数指针
* @retval None
*/
void Dht11_init(Dht11Typedef *hdht11, GPIO_TypeDef *IO, uint32_t Pin, void (*DelayUs)(uint16_t))
{
hdht11->IO = IO;
hdht11->DataIO.Pin = Pin;
hdht11->DelayUs = DelayUs;
hdht11->state = DHT11_ON;
DHT11_CLOCK_ENABLE();
Dht11_DataIO_init(hdht11, DHT11_IO_OUTPUT);
dht11_delay_ms(hdht11, 1000);
Dht11_Check(hdht11);
}
/**
* @brief 用于Dht11的反初始化,主要改变模块状态,反初始化数据引脚
* @param hdht11 : DHT11 handle
* @retval None
*/
void Dht11_deinit(Dht11Typedef *hdht11)
{
Dht11_IO_deinit(hdht11);
hdht11->state = DHT11_OFF;
}
/**
* @brief 用于Dht11的数据引脚反初始化,将引脚变为模拟态
* @note 引脚为模拟态可降低功耗
* @param hdht11 : DHT11 handle
* @retval None
*/
static void Dht11_IO_deinit(Dht11Typedef *hdht11)
{
Dht11_DataIO_init(hdht11, DHT11_IO_ANALOG);
}
/**
* @brief 用于Dht11的读取一个bit
* @param hdht11 : DHT11 handle
* @retval 1:bit=1
* @retval 0:bit=0
*/
static uint8_t Dht11_read_bit(Dht11Typedef *hdht11)
{
uint8_t re = 0;
while (Dht11_DataPinRead(hdht11) && re < 110) // 等待变为低电平
{
re++;
hdht11->DelayUs(1);
}
re = 0;
while (!Dht11_DataPinRead(hdht11) && re < 110) // 等待变高电平
{
re++;
hdht11->DelayUs(1);
}
hdht11->DelayUs(40); // 等待40us
if (Dht11_DataPinRead(hdht11))
return 1;
else
return 0;
}
/**
* @brief 用于Dht11的读取一个byte
* @param hdht11 : DHT11 handle
* @retval dat
*/
static uint8_t Dht11_read_byte(Dht11Typedef *hdht11)
{
uint8_t i, dat;
dat = 0;
for (i = 0; i < 8; i++)
{
dat <<= 1;
dat |= Dht11_read_bit(hdht11);
}
return dat;
}
/**
* @brief 用于Dht11的读取温湿度
* @param hdht11 : DHT11 handle
* @param temp:保存温度数据的指针
* @param humi:保存湿度数据的指针
* @retval None
*/
void Dht11_read_data(Dht11Typedef *hdht11, double *temp, double *humi)
{
uint8_t i;
Dht11_Reset(hdht11);
if (Dht11_Check(hdht11) == DHT11_ON)
{
for (i = 0; i < 5; i++)
{
hdht11->dta[i] = Dht11_read_byte(hdht11);
}
if (Dht11_dataVerification(hdht11))
{
*humi = (double)hdht11->dta[0] + (double)hdht11->dta[1] / 100;
*temp = (double)hdht11->dta[2] + (double)hdht11->dta[3] / 100;
}
}
}
/**
* @brief 用于Dht11的检查状态
* @param hdht11 : DHT11 handle
* @retval DHT11_OFF:未开启DHT11的数据引脚
* @retval DHT11_ON:DHT11正常开启
* @retval DHT11_ERROR:DHT11异常
*/
Dht11State Dht11_Check(Dht11Typedef *hdht11)
{
if (hdht11->state == DHT11_OFF)
{
return DHT11_OFF;
}
else
{
uint8_t re = 0;
Dht11_DataIO_init(hdht11, DHT11_IO_INPUT); // 设置为输入
while (Dht11_DataPinRead(hdht11) && re < 100) // DHT11会拉低40~80us
{
re++;
hdht11->DelayUs(1);
};
if (re >= 100)
return DHT11_ERROR;
else
re = 0;
while (!Dht11_DataPinRead(hdht11) && re < 100) // DHT11拉低后会再次拉高40~80us
{
re++;
hdht11->DelayUs(1);
};
if (re >= 100)
return DHT11_ERROR;
return DHT11_ON;
}
}
/**
* @brief 用于Dht11的复位
* @param hdht11 : DHT11 handle
* @retval None
*/
void Dht11_Reset(Dht11Typedef *hdht11)
{
Dht11_DataIO_init(hdht11, DHT11_IO_OUTPUT); // 设置为输出
Dht11_DataPinWrite(hdht11, GPIO_PIN_RESET); // 低电平
dht11_delay_ms(hdht11, 20); // 拉低至少18ms
Dht11_DataPinWrite(hdht11, GPIO_PIN_RESET); // 高电平
hdht11->DelayUs(60); // 主机拉高20~40us
}
由于我毕设是跑系统的,所有这里展示两个使用案例,一个是我毕设的,另一个是由AI生成的裸机使用例程,这个AI生成的能否正常使用有待商榷,我没有实验验证,但大体不会有误
先是我自己的代码片段
//主函数完成外设初始化后,对延时函数,DHT11的初始化代码片段
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ADC1_Init();
MX_TIM1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
delay_init();
Dht11_init(&hdht111,GPIOB,GPIO_PIN_10,delay_us);
OLED_Init();
//与DHT11相关的任务片段
void DHT11_handle(void *argument)
{
/* USER CODE BEGIN DHT11_handle */
OledInfo dht11_data = {0};
double temp=0,humi=0;
dht11_data.Line=3;
/* Infinite loop */
for (;;)
{
Dht11_read_data(&hdht111,&temp,&humi);
sprintf(dht11_data.info,"H:%.2f T:%.2f",humi,temp);
osMessageQueuePut(Oled_QueueHandle, &dht11_data, 0, osWaitForever);
osDelay(1000);
}
/* USER CODE END DHT11_handle */
}
//与显示有关的代码片段
void Oled_handle(void *argument)
{
/* USER CODE BEGIN Oled_handle */
OledInfo oledinfo={0};
char clearLine[17]=" ";
/* Infinite loop */
for (;;)
{
if(osMessageQueueGet(Oled_QueueHandle, &oledinfo, 0, osWaitForever)==osOK)
{
OLED_ShowString(oledinfo.Line,1,clearLine);
OLED_ShowString(oledinfo.Line,1,oledinfo.info);
}
}
/* USER CODE END Oled_handle */
}
这是效果图
再是由AI生成的裸机使用案例,目测无误
#include "main.h"
#include "dht11.h"
#include "usart.h"
#include <stdio.h>
void delay_us(uint16_t us);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
Dht11Typedef hdht11 = {0};
Dht11_init(&hdht11, GPIOA, GPIO_PIN_0, delay_us);
double temperature, humidity;
while (1)
{
Dht11_read_data(&hdht11, &temperature, &humidity);
char buffer[50];
sprintf(buffer, "Temperature: %.2f C, Humidity: %.2f%%\r\n", temperature, humidity);
HAL_UART_Transmit(&huart1, (uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY);
HAL_Delay(2000); // 每隔2秒钟读取一次数据
}
}
总之使用方法就是定义一个DHT11句柄,选好自己要用的引脚,实现微秒级延时,然后据此使用Dht11_init()函数初始化它,读取时使用Dht11_read_data按照要求传入参数就可以便捷读取