OrangePi ZERO 2 外设应用程序开发之温湿度传感器(DHT11)

如果想记录自己的温湿度,建立一个温湿度控制系统,奥松的 DHT11 温湿度传感器就非常适合。该传感器经过工厂校准,不需要任何外部部件即可工作。作为单总线的传感器,只需几个连接和一点代码,就可以开始测量相对湿度和温度。

在这里插入图片描述

一、DHT11 模块硬件概述

1. 硬件规格

DHT11 是 DHTxx 系列中使用最广泛的传感器之一,DHT11 可以以 ±2.0°C 的精度测量 0°C 至 50°C 的温度,以 5% 的精度测量 20 至 80% 的湿度。需要注意的是,DHT11 的采样率为 1Hz,这意味着它只能每秒提供一次新数据。

在这里插入图片描述

具体规格如下表;

工作电压3-5V
最大工作电流2.5mA max
湿度范围20-80% / 5%
温度范围0-50°C / ± 2°C
采样率1 Hz (每秒读取一次)
体积大小15.5mm × 12mm × 5.5mm
优势超低成本

2. 内部电路

DHT11传感器通常需要在输出引脚上安装一个外部 10KΩ 的上拉电阻,以便在传感器和主控芯片之间进行正确的通信。由于模块已经包含上拉电阻器和所有必要的支持电路(包括用于滤波电源噪声的去耦电容),因此可以直接使用。

在这里插入图片描述

如果拆下传感器的外壳,其实里面就是一个NTC热敏电阻和一个湿度传感元件。

在这里插入图片描述

湿度传感部件有两个电极,中间有一个保湿基底(通常是盐或导电塑料聚合物)。随着湿度的升高,基板吸收水蒸气,导致离子的释放和两个电极之间电阻的降低。电阻的变化与湿度成正比,可以测量湿度来估计相对湿度。

在这里插入图片描述

DHT11 还包括用于测量温度的 NTC(热敏电阻)。热敏电阻是一种电阻随温度变化的电阻器。从技术上讲,所有电阻器都是热敏电阻,因为它们的电阻随温度略有变化,但这种变化通常非常小,难以测量。热敏电阻的设计使其电阻随温度而急剧变化(每度 100Ω 或更大),而且电阻随着温度的升高而减小。

在这里插入图片描述

该传感器还包括一个 8 bits SOIC-14 封装的 IC 芯片。该芯片使用存储的校准系数测量和处理模拟信号,将模拟信号转换为数字信号,并输出包含温度和湿度的数字信号。

在这里插入图片描述

3. 引脚定义

DHT11 模块的连接相对简单,只有三个引脚:

在这里插入图片描述

VCCGND分别为 DHT11 的电源正负极,OUT引脚用于传感器和控制器之间的通信。需要注意的是,传感器上电后,要等待 1 秒以越过不稳定状态,在此期间无需发送任何指令。

二、DHT11的通信方式及代码

1. 数据格式

DHT11 与控制器之间采用单总线数据格式,一次通信时间 4ms 左右,数据分小数部分和整数部分,具体格式在下面说明,当前小数部分用于以后扩展,现读出为零。操作流程如下:

一次完整的数据传输为 40bits,高位先出。

数据格式:8bits 湿度整数数据 + 8bits 湿度小数数据 + 8bits 温度整数数据 + 8bits 温度小数数据 + 8bits 校验和

数据传送正确时校验和数据等于“8bits 湿度整数数据 + 8bits 湿度小数数据 + 8bits 温度整数数据 + 8bits 温度小数数据”所得结果的末 8 位。

下面是两个示例:

在这里插入图片描述

在这里插入图片描述

2. 操作时序

2-1. 通信过程时序图

在这里插入图片描述

由主机(控制器)发送一次起始信号后,DHT11 从低功耗模式转换到高速模式,等待主机起始信号结束后,DHT11 发送响应信号,送出 40bits 的数据,并触发一次信号采集,此时主机可选择读取数据。如果没有接收到主机发送起始信号,DHT11 不会主动进行温湿度采集,采集数据后转换到低速模式。

2-2. 起始信号与响应信号

在这里插入图片描述

总线空闲状态为高电平,主机把总线拉低等待 DHT11 响应,主机把总线拉低必须大于 18ms,保证 DHT11 能检测到起始信号。DHT11 接收到主机的起始信号后,等待主机起始信号结束,然后发送 80μs 低电平响应信号。主机发送起始信号结束后,延时等待 20-40μs 后,读取 DHT11 的响应信号,主机发送起始信号后,需要把引脚电平拉高,并切换到输入模式,准备读取 DHT11 的数据。

前面提到 DHT11 要在通电一秒后再读取数据,同时总线空闲时也处于高电平状态,所以在 OrangePi ZERO 2 对接入 DHT11 的 GPIO 可以进行如下的函数操作:

void gpioInit(int gpioPin)
{
    pinMode(gpioPin, OUTPUT);
    digitalWrite(gpioPin, HIGH);
    delay(1000);
}

每次从 DHT11 读取数据都需要一个起始信号,代码必然是复用的,因此可以封装成如下函数:

void DHT11StartSignal(int gpioPin)
{
    pinMode(gpioPin, OUTPUT);
    digitalWrite(gpioPin, HIGH);
    digitalWrite(gpioPin, LOW);
    delay(25);        //主机把总线拉低必须大于 18 ms,这里写 25 ms
    digitalWrite(gpioPin, HIGH);	//转化为高电平,等待 DHT11 的响应信号

    pinMode(gpioPin, INPUT);	//响应信号为 80μs 低电平和 80μs 的高电平准备信号

    pullUpDnControl(gpioPin, PUD_UP);	//香橙派进行上拉,增加稳定性,非必选
    delayMicroseconds(35);	//要求延时等待 20-40μs,这里写 35μs
}

这里用到了一个 wiringOP 库比较少用的函数,下面是详细介绍:

通用GPIO控制函数原型函数参数函数说明
void pullUpDnControl (int pin, int pud)pin:引脚
pud:拉电阻模式
可取的值:PUD_OFF 不启用任何拉电阻。关闭拉电阻。
PUD_DOWN 启用下拉电阻,引脚电平拉到 GND
PUD_UP 启用上拉电阻,引脚电平拉到 3.3v
对一个设置IO模式为 INPUT 的输入引脚设置拉电阻模式。

这个函数并非必要的,因为 DHT11 内部就用上拉电阻,这里添加只是为了更稳定。

2-3. 数据信号

总线为低电平,说明 DHT11 正在发送响应信号,DHT11 发送响应信号后,再把总线拉高 80μs,准备发送数据,每一位数据都以 50μs 低电平时隙开始,高电平的长短定了数据位是 “0”还是“1”。

数字 0 信号时序图:

在这里插入图片描述

数字 1 信号时序图:

在这里插入图片描述

由此可以看出,如果高电平时间为 26μs-28μs,则代表该位为 0。如果高电平时间为 70μs,则代表该位为 1。

如果读取响应信号为高电平,则说明 DHT11 没有响应,这时请检查线路是否连接正常。当最后一位数据传送完毕后,DHT11 拉低总线 50μs,随后总线由上拉电阻拉高进入空闲状态。

2. 读取数据

对 DHT11 的数据(共五个字节)可以分为两部分,一部分为前四个字节,也就是温湿度的数据,另一部分为最后一个字节,也就是校验和。前四个字节正好可以用一个无符号的整型变量存放,最后一个字节可以用一个无符号的字符型变量存放。考虑到校验和只在校验时有作用,因此不考虑把校验和作为全局变量使用,只定义一个uint32_t类型的全局变量dataBuffur

读取数据过程具体如下:

void readTmpAndHum(void)
{
	dataBuffur = 0;		//每次读取前先清零缓存
	for (uint8_t i = 0; i < DATA_BIT_LENGTH; i++) {		//#define DATA_BIT_LENGTH 32
		while (digitalRead(DHT11_DATA));		//等待拉低
		while (!digitalRead(DHT11_DATA));		//当电平拉高后,表示开始输入数据
		delayMicroseconds(HIGH_TIME);		//等待 32 μs
		dataBuffur <<= 1;		//每次读完一位数据,都左移一位,全程只需要移动 31 次
		if (digitalRead(DHT11_DATA))		//如果此时还是高电平,说明该位为 1
			dataBuffur |= 0x01;
	}
}

读完温湿度的数据后,再读校验和的数据,接着计算校验和是否正确,如果正确返回 0,否则返回 1。

uint8_t check(void)
{
	uint8_t checkSum = 0;
	uint8_t sum = 0;
	uint8_t i;

	for (i = 0; i < CHECKSUM_BIT_LENGTH; i++) {
		while (digitalRead(DHT11_DATA));
		while (!digitalRead(DHT11_DATA));
		delayMicroseconds(HIGH_TIME);
		checkSum <<= 1;
		if (digitalRead(DHT11_DATA))
			checkSum |= 0x01;
	}

	
	for (i = 0; i < EFFECTIVE_BYTES; i++)
		sum += (dataBuffur >> (8 * i) & 0xFF);		//移动 8 位或者 8 的倍数长度,每次都累加起来

	if (sum == checkSum)
		return 0;
	else
		return 1;
}

最后以创建线程的方式调用上述函数,香橙派每发送一次数据读取请求,就创建一个线程用于读取数据,这样有利于提高代码的健壮性和扩展性。为了保证线程不会卡死或者意外退出,在程序中也引入标准位对程序进行保护。同时对无效数据(如超过 50℃ 等)也会进行重测的操作,自动重测最多进行 5 次,5 次之后强行退出线程并报警。

具体代码如下:

void *readDHT11Data(void *arg)
{
	uint8_t attempt = 5;		//该变量为重测计数变量,该变量为 0 时,表示已经进行了五次重测,则均为失败。

	while (attempt) {
		DHT11StartSignal(DHT11_DATA);

		if (digitalRead(DHT11_DATA) == 0) {
			while (!digitalRead(DHT11_DATA));

			readTmpAndHum();

			if (check() || ((dataBuffur >> 8) & 0xFF) > 50) {		//校验和不正确或者温度大于 50℃ 都视为无效数据
				attempt--;		//每一次测量失败,减一次测量次数
				delay(500);		//500ms 后,重新测量
				continue;
			} else {
				printf("Humidity: %u.%u\%rh\n", (dataBuffur >> 24) & 0xFF,\
						(dataBuffur >> 16) & 0xFF);
				printf("Temperature: %u.%u℃\n",(dataBuffur >> 8) & 0xFF,\
						dataBuffur & 0xFF);
				blockFlag = 0;	//该标志位为全局变量,在规定时间内,该标准位一直为 1 的话,说明本线程运行超时,主线程会强制退出本线程
				return (void *)1;	//执行成功返回 1
			}
		} else {
			blockFlag = 0;		//温湿度传感器启动失败,强行结束线程
			printf("Sorry! The sensor is not running.\n");
			return (void *)0;
		} /* end of "if (digitalRead(DHT11_DATA) == 0)" */
	} /* end of "while (attempt)" */

	blockFlag = 0;		//连续 5 次测量数据失败,强行结束线程
	printf("Sorry! Failed to obtain data!\n");
	return (void *)2;
}

三、最终运行效果

1. DHT11 和 OrangePi ZERO 2 连接

5vGND不再赘述,DHT11 的数据线可以接在任意 GPIO 口,本篇接在 26 Pin 引脚的 7 号引脚,也就是 wiP 序号为 2 的引脚上。

接线图如下所示:

在这里插入图片描述

2. 完整代码及结果演示

#include <stdio.h>
#include <stdlib.h>
#include <wiringPi.h>
#include <pthread.h>
#include <stdint.h>

#define DHT11_DATA			2
#define DATA_BIT_LENGTH		32
#define CHECKSUM_BIT_LENGTH 8
#define HIGH_TIME			32
#define EFFECTIVE_BYTES		4

uint32_t dataBuffur;
uint32_t blockFlag;

void gpioInit(int gpioPin)
{
	pinMode(gpioPin, OUTPUT);
	digitalWrite(gpioPin, HIGH);
	delay(1000);
}

void DHT11StartSignal(int gpioPin)
{
	pinMode(gpioPin, OUTPUT);
	digitalWrite(gpioPin, HIGH);
	digitalWrite(gpioPin, LOW);
	delay(25);
	digitalWrite(gpioPin, HIGH);

	pinMode(gpioPin, INPUT);

	pullUpDnControl(gpioPin, PUD_UP);
	delayMicroseconds(35);
}

void readTmpAndHum(void)
{
	dataBuffur = 0;
	for (uint8_t i = 0; i < DATA_BIT_LENGTH; i++) {
		while (digitalRead(DHT11_DATA));
		while (!digitalRead(DHT11_DATA));
		delayMicroseconds(HIGH_TIME);
		dataBuffur <<= 1;
		if (digitalRead(DHT11_DATA))
			dataBuffur |= 0x01;
	}
}

uint8_t check(void)
{
	uint8_t checkSum = 0;
	uint8_t sum = 0;
	uint8_t i;

	for (i = 0; i < CHECKSUM_BIT_LENGTH; i++) {
		while (digitalRead(DHT11_DATA));
		while (!digitalRead(DHT11_DATA));
		delayMicroseconds(HIGH_TIME);
		checkSum <<= 1;
		if (digitalRead(DHT11_DATA))
			checkSum |= 0x01;
	}

	
	for (i = 0; i < EFFECTIVE_BYTES; i++)
		sum += (dataBuffur >> (8 * i) & 0xFF);

	if (sum == checkSum)
		return 0;
	else
		return 1;
}

void *readDHT11Data(void *arg)
{
	uint8_t attempt = 5;

	while (attempt) {
		DHT11StartSignal(DHT11_DATA);

		if (digitalRead(DHT11_DATA) == 0) {
			while (!digitalRead(DHT11_DATA));

			readTmpAndHum();

			if (check() || ((dataBuffur >> 8) & 0xFF) > 50) {
				attempt--;
				delay(500);
				continue;
			} else {
				printf("Humidity: %u.%u\%rh\n", (dataBuffur >> 24) & 0xFF,\
						(dataBuffur >> 16) & 0xFF);
				printf("Temperature: %u.%u℃\n",(dataBuffur >> 8) & 0xFF,\
						dataBuffur & 0xFF);
				blockFlag = 0;
				return (void *)1;
			}
		} else {
			blockFlag = 0;
			printf("Sorry! The sensor is not running.\n");
			return (void *)0;
		} /* end of "if (digitalRead(DHT11_DATA) == 0)" */
	} /* end of "while (attempt)" */

	blockFlag = 0;
	printf("Sorry! Failed to obtain data!\n");
	return (void *)2;
}


int main(void)
{
	pthread_t tid;
	uint32_t waitTime;

	if (wiringPiSetup() == -1) {
		printf("Sorry! Failed to initialize GPIO!\n");
		exit(1);
	}

	gpioInit(DHT11_DATA);

	while (1) {
		blockFlag = 1;
		waitTime = 5;

		if (pthread_create(&tid, NULL, readDHT11Data, NULL) != 0) {		//如果考虑到多线程并发,仍然有小 bug
			printf("[%s|%s|%d]: Thread creation failed!\n",\
					__FILE__, __func__, __LINE__);
			return -1;
		}

		while (waitTime && blockFlag) {		//线程执行 5 秒后,blockFlag仍然为置 0,说明线程卡死
			delay(1000);
			waitTime--;
		}

		if (1 == blockFlag) {		//将卡死的线程强行结束
			pthread_cancel(tid);
			printf("[%s|%s|%d]: Thread timeout! Exit!\n",\
					__FILE__, __func__, __LINE__);
		}
		delay(1000);
	}

	return 0;
}

执行结果:

在这里插入图片描述

编译时的小警告可以忽略。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Grayson Zheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值