DHT11 这款温湿度传感器 几乎是所有 MCU 入门第一个传感器, 现在看来有些不合时宜, 毕竟过于廉价,数据不太靠谱,远不如 AHT10 好用。早年买了两个,按例程读出数据后就吃灰了。某日看到有人说自己按datasheet去读,得不到需要的数据回应。于是仔细看了下DHT11的数据传输,觉得蛮有意思的,想着自己算是入门好几年的老鸟,硬核一点,不用别人的库,自己写几句代码来读一下这个传感器看看?
DHT11 数据传输详解https://blog.csdn.net/Alex_68/article/details/108730534
上文是要啃一下的,时序图并不算复杂,大概原理就是:
1.均由 MCU 发起读数据操作。没有传输时, 数据脚持续高电平。
2.MCU 发起读数据操作时,把数据脚拉低再拉高一次。
3.DHT11 收到上述电平变化后, 也把数据脚拉低再拉高一次作为响应,表示我要开始发送数据了。
4.接下来 DHT11 把数据脚拉低再拉高40次, 即为40位数据的发送。数据是 0 还是 1 由高电平持续时间决定,和低电平时间无关。 大概可以理解和 摩斯电码 一样一样的。
5.结束再次把数据脚拉低再拉高一次,高电平就一直保持了,直到 MCU 发起下一次读数据操作。
第一步,先采集下数据脚的电平情况看看:
实践出真知,下图为一帧数据的实测波形。按图右边设置示波器,点点左上角的运行按钮就能捕获到完美的波形。
如没有示波器,最简单就是 循环 “读数据脚、串口打印”,也能获得数据脚电平变化情况。
Serial.print(digitalRead(data_pin));
实际测试发现可能是因为串口太慢,上面的语句可以捕获到电平变化,但是不足以用来分析数据。
嗯,办法总是有的,按存储示波器的玩法,间隔20微秒,连续采样200次存入数组(总时间4ms左右,上面示波器的截图也能大概看出来)。采样完成后再集中打印出来。( 间隔越小越精准,需要的采样次数越多,一行显示不全了,嗯,超宽带鱼显示器在这里就比较方便了。上面20微秒200次是经过一些测试总结的,不要轻易改变)
实测,如下代码可以在串口监视器看到一些010111这样的输出,这里的01并不是是需要的数据,而是反映的数据脚电平高低变化。
#define DHT11_PIN 11
boolean data[200];
void setup(){
pinMode(12, OUTPUT);digitalWrite(12, HIGH); // DHT11 Vcc
pinMode(9, OUTPUT);digitalWrite(9, LOW); // DHT11 Gnd
Serial.begin(115200);
}
void loop(){
/*HDT11空闲时为高电平。需要读数时,MCU先拉低30ms,再拉高30μs,注意两者时间单位 */
pinMode(DHT11_PIN, OUTPUT);
digitalWrite(DHT11_PIN, LOW); delayMicroseconds(30000);
digitalWrite(DHT11_PIN, HIGH);delayMicroseconds(30);
/*开始检测DHT的数据返回*/
pinMode(DHT11_PIN, INPUT);
for(int i=0;i<200;i++){
data[i]=digitalRead(DHT11_PIN);
delayMicroseconds(20);
}
for(int i=0;i<200;i++)Serial.print(data[i]);
Serial.println();
delay(5000);
}
从下图可以看出,1有单个的,和多个(3个或4个),分别对应长短电平,那么接下来需要做的就是“解码”
如果上面看起来有些别扭,修改下代码,把01用上下线段表示就比较像逻辑分析仪了:
第二步, 上面证实数据传输无误,继续判断高电平时间,就获得40位数据了:
MCU 需要干其他事的可能需要用到中断比较好,这里仅仅是验证数据传输,就用 while 语句傻等了。
#define DHT11_PIN 11
boolean data[40];
long time_stamp;
void setup(){
pinMode(12, OUTPUT);digitalWrite(12, HIGH); // DHT11 Vcc
pinMode(9, OUTPUT);digitalWrite(9, LOW); // DHT11 Gnd
Serial.begin(115200);
}
void loop(){
/*HDT11空闲时为高电平。需要读数时,MCU先拉低30ms,再拉高30μs,注意两者时间单位 */
pinMode(DHT11_PIN, OUTPUT);
digitalWrite(DHT11_PIN, LOW); delayMicroseconds(30000);
digitalWrite(DHT11_PIN, HIGH);delayMicroseconds(30);
/*开始检测DHT的数据返回*/
pinMode(DHT11_PIN, INPUT);
while (!digitalRead(DHT11_PIN)) {} //刚开始应该是低电平,等待高电平出现后继续 , 括号不能省略!
while ( digitalRead(DHT11_PIN)) {} //高电平期间继续等待,出现低电平则为数据开始传输
for(int i=0;i<40;i++){
data[i]=0;
while (!digitalRead(DHT11_PIN)){} //等待出现高电平
time_stamp = micros(); //开始计时
while ( digitalRead(DHT11_PIN)){} //等待出现低电平,即高电平结束了
if (( micros()- time_stamp) > 50) data[i]=1;
}
for(int i=0;i<40;i++)Serial.print(data[i]); Serial.println();
delay(5000);
}
上面代码可以在串口监视器上看到如下输出,40位不多不少,比较理想:
第三步,40位数据拆分转换。
第二步里的程序通用,能读出 DHT11 DHT22 两种传感器的数据,毕竟同一个公司的产品,格式都是连续5个8位共计40位数据:
湿度高8位、湿度低8位、温度高8位、温度低8位、校验8位
DHT11 只需要处理 湿度高8位 温度高8位 , 获得的整数即为需要的读数,没有小数的。
湿度高8位、湿度低8位、温度高8位、温度低8位、校验8位
DHT22 需要把16位全部读出来的整数除以10,数据含有1位小数的。
湿度高8位、湿度低8位、温度高8位、温度低8位、校验8位
第二步里的程序把数据都存在data[ i ] 里面了。
以DHT11 湿度值为例, 按位读出 data[0 ] ~ data[7 ] 并乘以该位的二进制值即可。
这里可以用 POW函数按位循环相加,但实测与预期有些出入,暂时用笨点的办法:
dht11_humidity = 128*data[0] + 64*data[1] + 32*data[2] + 16*data[3] + 8*data[4] + 4*data[5] + 2*data[6] + data[7];
比如上图标黄的 00100110 就是 0+0+32+0+0+6+2+0 = 40, 看到这里,二进制 基本功还是需要一些的,理解不了需要补些课。
需要注意的是:
实验性质,校验位未作处理,实际使用中数值偶有跳动,影响不大。
温度可能会产生负数值,这里暂不做处理,具体看手册,并不难。