本文我们利用树莓派的GPIO口来跟数字湿温度计模块DHT11进行通信取得温度和湿度数据并显示在数码管上,通过按钮来切换显示温度或湿度。
最终效果
硬件
- 数码管
- 杜邦线
- 面包板
- 按钮1只
- 数字湿温度计DHT11模块。(我们这个教程里用到的所有电子元件均可在淘宝购买到)
原理说明
原始的DHT11模块有4根引脚,长成这个样子:
由图可以看出4根引脚里除了VCC,GND,DATA以外,还有一个引脚是N/A,也就是不使用。(不使用引出来干嘛?好看吗?不解)
本文使用的是又被封装了一次的模块,去掉了无用的引脚。其他3个引脚保留。功能完全一样,所以如果你手头上的DHT11是有4根引脚的请忽略N/A针脚,其他的跟我使用的这种完全通用。3个针脚分别连接到3.3v电源,GND和任意GPIO口上。根据数据手册(文末提供下载)的说明,总线(DATA引脚)在空闲状态需要保持高电平状态,所以我们除了将DATA引脚接到一个GPIO口上,还要通过一个4.7K(经实测2K左右的就够了)的电阻将DATA引脚并联到VCC上。这个电阻也称上拉电阻,电阻就是一般的电阻,只是在这里起的作用是上拉电平的作用所以称之为上拉电阻。
与DHT11通信时,发送和接收信息都在一根DATA口上,这种只用1根总线的数据传输方式称为单总线模式。
向DHT11发送数据时,GPIO口需要设置为OUTPUT模式,从DHT11接收数据时GPIO口需要切换成INPUT模式。
具体通信的时序如下:
- 由于有上拉电阻存在,总线(DATA)空闲状态为高电平。
- 树莓派GPIO口设置为OUTPUT模式。
- 树莓派向DHT11发送起始信号。方式是GPIO口设置低电平并持续一段时间,根据数据手册的说明,这段时间必须大于18毫秒,保证DHT11能检测到起始信号。
- 树莓派起始信号输出完毕,切换到输入模式,等待DHT11响应。一旦切换到输入模式GPIO口就不再输出电平信号,总线处于释放状态,由于有上拉电阻的存在,总线被拉回高电平。
- 在总线被拉回至高电平通知DHT11主机已经准备好接受数据以后,DHT11还会继续等待20-40us左右以后才会开始发送反馈信号。
- DHT11开始发送反馈信号,总线被DHT11拉低,持续80us左右。
- 这个持续了80us左右的低电平的反馈信号结束以后,DHT11又会将DATA口拉回高电平并再次持续80us左右。
- DHT11开始正式传输40bit的二进制数据(0或1)。每一个bit的数据(0或者1)总是由一段持续50us的低电平信号开始,再由一段持续26us-28us(数据0)或者持续70us(数据1)的高电平结束。一直到40位数据传输完毕。这40位的数据内容是:
8bit湿度整数数据 + 8bit湿度小数数据 + 8bi温度整数数据 + 8bit温度小数数据 + 8bit校验和。而校验和数据应该等于“湿度整数数据+湿度小数数据+温度整数数据+温度小数数据”所得结果的末8位。
我们的程序就应该遵循上述时序来与DHT11进行数据通信。
硬件连接
下面的连接图只标出了DHT11的连线和上拉电阻的连线方法。数码管和按钮的连线请参考上一篇。
关键代码
void readDHT11() {
int i,j,cnt = 0;
for (j = 0; j < RETRY; ++j)
{
for (i = 0; i < 5; ++i) {
data[i] = 0;
}
// GPIO口模式设置为输出模式
pinMode (DATA, OUTPUT) ;
// 先拉高DATA一段时间,准备发送开始指令
digitalWrite (DATA, HIGH);
usleep(500000); // 500 ms
// 拉低DATA口,输出开始指令(至少持续18ms)
digitalWrite (DATA, LOW);
usleep(TIME_START);
digitalWrite (DATA, HIGH);
// 开始指令输出完毕,切换到输入模式,等待DHT11输出信号。
// 由于有上拉电阻的存在,所以DATA口会维持高电平。
pinMode (DATA, INPUT);
// 在DATA口被拉回至高电平通知DHT11主机已经准备好接受数据以后,
// DHT11还会继续等待20-40us左右以后才会开始发送反馈信号,所以我们把这段时间跳过去
// 如果长时间(1000us以上)没有低电平的反馈表示有问题,结束程序
cnt=0;
while (digitalRead(DATA) == HIGH) {
cnt++;
if (cnt > MAXCNT)
{
printf("DHT11未响应,请检查连线是否正确,元件是否正常工作。\n");
exit(1);
}
}
// 这个反馈响应信号的低电平会持续80us左右,但我们不需要精确计算这个时间
// 只要一直循环检查DATA口的电平有没有恢复成高电平即可
cnt=0;
while (digitalRead(DATA) == LOW) {
cnt++;
if (cnt > MAXCNT)
{
printf("DHT11未响应,请检查连线是否正确,元件是否正常工作。\n");
exit(1);
}
}
// 这个持续了80us左右的低电平的反馈信号结束以后,DHT11又会将DATA口拉回高电平并再次持续80us左右
// 然后才会开始发送真正的数据。所以跟上面一样,我们再做一个循环来检测这一段高电平的结束。
cnt=0;
while (digitalRead(DATA) == HIGH) {
cnt++;
if (cnt > MAXCNT)
{
printf("DHT11未响应,请检查连线是否正确,元件是否正常工作。\n");
exit(1);
}
}
// ### ### ### ### ### ### ### 40bit的数据传输开始 ### ### ### ### ### ### ### #
for (i = 0; i < 40; i++)
{
// 每一个bit的数据(0或者1)总是由一段持续50us的低电平信号开始
// 跟上面一样我们用循环检测的方式跳过这一段
while (digitalRead(DATA) == LOW) {
}
// 接下来的高电平持续的时间是判断该bit是0还是1的关键。
// 根据DHT11的说明文档,我们知道 这段高电平持续26us-28us左右的话表示这是数据0。
// 如果这段高电平持续时间为70us左右表示这是数据1。
// 方法1:在高电平开始的时候记下时间,在高电平结束的时候再记一个时间,
// 通过计算两个时间的间隔就能得知是数据0还是数据1了。
// 方法2:在高电平开始的以后我们延时40us,然后再次检测DATA口:
// (a) 如果此时DATA口是低电平,表示当前位的数据已经发送完并进入下一位数据的传输准备阶段(低电平50us)了。
// 由于数据1的高电平持续时间是70us,所以如果是数据1,此时DATA口应该还是高电平才对,
// 据此我们可以断言刚才传输的这一位数据是0。
// (b) 如果延时40us以后DATA口仍然是高电平,那么我们可以断言这一位数据一定是1了,因为数据0只会持续26us。
// 方法3:循环检测电平状态并计数,每检查一次如果电平状态没变就让计数器加一,一直到电平状态变成低电平为止。
// 数据0的高电平持续时间短,所以计数一定比数据1的计数少,由于微秒级别的延时太短,这个计数会有一定误差。
// 我们需要先在自己的树莓派上用printf打印出每一位数据计数的结果,然后观察计数结果来设定一个阈值,
// 高于这个阈值的就认为的数据1,低于这个值的就认为是数据0。
// 我们这里采用简单易行的方法3。
// 实际上,由于要求的时序太短,在树莓派上很难通过方法1和方法2实现。这也是我使用c而不是python的原因。
// 特别是方法2,因为linux里有一个系统级的延时函数usleep,单位确实是微秒。
// 貌似用usleep(40)就可以了,实际测试的结果是延时远远超过40us。其原因是usleep这个函数的延时方法是
// 暂停当前进程并放开cpu权限让cpu可以在这段时间里去处理其他任务。这样做的好处是不会浪费cpu资源,
// 但问题是当系统将cpu权限交还给我们的进程的这个过程本身就要耗费若干ms(毫秒哦)
// 所以导致usleep这个函数实际上没有办法做到延时几十微秒。
cnt=0;
while (digitalRead(DATA) == HIGH) {
// 当所有数据传输完以后,DHT11会放开总线,DATA口就会被上拉电阻一直拉高。
// 所以如果超过一定时间电平还没有被拉低就表示所有的数据已经传输完毕,停止检测。
cnt++;
if (cnt > MAXCNT)
{
break;
}
}
if (cnt > MAXCNT)
{
break;
}
// 将当前位的计数保存起来
bits[i] = cnt;
}
// 整理数据,将位数据转成5个数字
for (i = 0; i < 40; ++i) {
data[i/8] <<= 1;
if (bits[i] > VAL)
{
data[i/8] |= 1;
}
//下面这句话就是用来测试自己的设备应该设定多少阈值的测试代码
//printf("bits[%d] = %d (%d) \n", i, bits[i], bits[i]>200?1:0 );
}
// 往屏幕上输出取得的数据
for (i = 0; i < 5; ++i) {
printf("data[%d] = %d \n", i, data[i] );
}
// 用校验和来检查接收数据是否完整
if (data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF) ) {
printf("校验成功! \n");
// 将湿度,温度数据赋值给显示用的湿度,温度变量
shidu = data[0];
wendu = data[2];
break;
} else {
printf("校验不成功,重新取值! \n");
// 校验不成功,重新取值,连续10次取值不成功就放弃。一般连线和逻辑正确的话连续10次取值出错是不可能的。
continue;
}
}
}
校验和
下图是实际运行时利用校验和检测到数据出现了接收错误的情况
资源下载
系列文章
- 树莓派GPIO入门01-使用GPIO接口控制发光二极管闪烁
- 树莓派GPIO入门02-GPIO控制LED亮度,制作呼吸灯效果
- 树莓派GPIO入门03-GPIO控制RGB彩色LED灯
- 树莓派GPIO入门04-使用按钮
- 树莓派GPIO入门05-驱动数码管显示数字
- 树莓派GPIO入门06-跟数字湿温度计DHT11通信【当前文章】
- 树莓派GPIO入门07-利用声音传感器制作声控灯
- 树莓派GPIO入门08-使用74HC595芯片驱动数码管(一)
- 树莓派GPIO入门08-使用74HC595芯片驱动数码管(二)
- 树莓派GPIO入门09-使用MAX7219芯片驱动8位数码管
- 树莓派GPIO入门10-使用TLC5940芯片输出多路PWM
- 树莓派GPIO入门11-驱动液晶屏幕(一)
- 树莓派GPIO入门11-驱动液晶屏幕(二)
4 comments
这里是不是应该加括号先算按位与再判断相等?`(data[4] == (data[0] + data[1] + data[2] + data[3]) & 0xFF )`=>`(data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF) )`,==优先级高于&
我这里不加运行时似乎也先计算按位与了,否则不可能出现校验和正确的情况(莫非都是巧合?没时间验证了。。)
但确实不应该依赖编译器。严谨的做法确实是应该加上括号。谢谢指正。原文已修正。
为什么是 `shidu = data[0];`,不是16位代表一个数字吗.查了一下dht11和dht22的40bit数据含义不一样,解析代码要改一下,博主是对的,我的dht22得改一下。
谢谢你的回复,祝你成功