文章目录
如果想记录自己的温湿度,建立一个温湿度控制系统,奥松的 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 模块的连接相对简单,只有三个引脚:
VCC
和GND
分别为 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 连接
5v
和GND
不再赘述,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;
}
执行结果:
编译时的小警告可以忽略。