DHT11 Sensor de Humedad y Temperatura

Hace tiempo que quería hacer un montaje con un sensor de temperatura para hacer una especie de data logger -por alguna razón a todos nos encantan los datos meteorológicos jejeje-. Aproveché y me decidí a compar un sensor que además registraba la humedad y además me permitiría escribir un protocolo y jugar así también con los relojes del Arduino Mega 2560. El sensor en cuestión es el DHT11. La verdad es que las características técnicas son muy malas:

  • precisión +/- 2º de Tª en el peor caso
  • precisión +/- 5% de HR en el peor caso
  • rango de Tª: de 0 - 50 ºC
  • rango de HR: de 20 - 90% … No voy a alargarme en indicar lo penoso que es porque está en pdf, pero sí que no me decidí por su hermano mayor DHT22 por la diferencia de precio.

La otra cosa que quería probar es escribir un protocolo. Este sensor envía los datos por un sólo cable y tiene su propio protocolo de comunicación -una cosa buena es que puedes enviar los datos a unos 20m sin ningún artificio ¡¡no todo tenía que ser malo!!-.

Aquí tenemos el protocolo:

DHT11 Protocolo

DHT11 Protocolo

Si leemos la documentación veremos que para despertar al sensor tenemos que pasar de nivel alto/bajo y esperar en el nivel bajo 18ms. Después poner ‘data’ a nivel alto unos 40µs y esperar -línea en negro-.

Tras esto, el sensor nos indicará que ha recibido nuestra petición poniendo ‘data’ a nivel bajo 80µs y después 80µs a nivel alto.

Ahora el sensor enviará los datos uno a uno -40bits en total con el checksum- empezando cualquier bit -es decir un 0/1- con 50µs a nivel bajo y ahora sí dependiendo de si es un ‘0’ o un ‘1’ tendremos duraciones distintas a nivel alto (insisto en que la documentación está todo y muy bien explicado).

Como he dicho anteriormente, también quería experimentar con los temporizadores que lleva el Atmel del Arduino Mega pero eso lo explicaré después.

¿Cómo escribo el protocolo?

Bien, tenemos que seguir la documentación pero los puntos importantes ya los he expuesto aquí. Veamos parte del código.

Inicialmente despertamos al sensor:

// Diferentes constantes de temporizacion para la
// sincronizacion de los datos
static unsigned int ACKHIGH = 1;
static unsigned int ACKLOW = 18;
static unsigned int ACKHIGHWAIT = 40;
static unsigned int DHTTIMEOUT = 10;
...
// Indicar al sensor que queremos que nos envie datos
pinMode(xdataPin, OUTPUT);
digitalWrite(xdataPin, HIGH);
delay(ACKHIGH);
digitalWrite(xdataPin, LOW);
delay(ACKLOW);
digitalWrite(xdataPin, HIGH);
delayMicroseconds(ACKHIGHWAIT);
pinMode(xdataPin, INPUT);

como ves es muy simple (te recomiendo como siempre que utilices constantes en lugar de numeritos -constantes mágicas- que al final no nos indican nada al pasar el tiempo…) la transcripción es directa.

Ahora esperamos a que el sensor nos indique que ha recibido la petición:

// Ahora el DHT tiene que indicar el "enterado"
 time = micros();
 while (digitalRead(xdataPin) == LOW) {
   if (micros() - time > DHTLOWSTART) {
     xerror = TIMEOUT1;
     return;
   }
 }
time = micros();
while (digitalRead(xdataPin) == HIGH) {
 if (micros() - time > DHTHIGHSTART) {
   xerror = TIMEOUT2;
   return;
 }
}

Y finalmente, una vez que estamos aquí, leer los datos:

// Ya esta enterado. Esperar los datos
 for (unsigned int i = 0; i < SIZE; i++) {
   time = micros();
   while (digitalRead(xdataPin) == LOW) {
     if (micros() - time > DHTBITSTART) {
       xerror = TIMEOUT3;
       return;
     }
   }
   // Bit: 0 o 1
   time = micros();
   while ((digitalRead(xdataPin) == HIGH) && (micros() - time < DHTBIT1));
   (micros() - time > DHTBIT0) ? (data[i] = 1) : (data[i] = 0);
 }

Como te habrás dado cuenta hago un uso extensivo de funciones como delay( ) o micros( ). Estas funciones dejan al micro inútil puesto que producen espera activa y esto no es nada bueno -se considera una muy mala práctica de programación…- Aquí es donde entran en juego los temporizadores. Los temporizadores del Atmega funcionan independientemente así que esperar tiempos no implica que el micro no pueda hacer otra cosa. Por otra parte, también quería que mientras se realiza la comunicación no se moleste al micro -no creo que haya posibilidad de regiones críticas condicionales o semáforos, pero está claro que puedo deshabilitar las interrupciones-. Empecemos con los temporizadores.

El ATmega2560 tiene 6 temporizadores:

  • 2 de 8bits
  • 4 de 16bits

El tiempo más largo que tenemos que controlar son los 18ms y tenemos que nuestro Arduino funciona con un cristal de 16Mhz.

Yo quería utilizar un timer de 8bits, por ejemplo el Timer0. Haciendo un simple cálculo vemos que podemos contar: 256 * 1 / 16Mhz = 16us Vaya… pero aquí vienen los prescalers a ayudarnos. Podemos dividir la frecuencia del reloj por:

  • 8
  • 64
  • 256
  • 1024

si realizamos los cálculos ahora tenemos:

  • 8: 256 x 1 / 2Mhz = 128us
  • 64: 256 x 1 / 250Khz = 1.024ms
  • 256: 256 x 1/ 62.5Khz = 4.096ms
  • 1024: 256 x 1 / 15.625Khz = 16.384ms

Pues va a se que no llegamos con ninguno… pero bueno vamos a utilizar un truco. Lo normal sería utilizar la señal de overflow del Timer0, pero como no queremos interrupciones no va a ser posible. Lo que haremos será contar 18 veces 1ms jajaja

Escogeremos pues el preescalado de 64 por varias razones:

  • se acerca a nuestro valor de 1ms
  • nos da una buena precisión -démonos cuenta que al aumentar el preescalado disminuimos la precisión-

Lo primero que haremos será guardar los registros para poder manipularlos y después dejarlos como estaban -a saber qué tocamos de Arduino jejeje-

// Utilizaremos el Timer0 como reloj
uint8_t oldTCCR0A = TCCR0A; // Al final queremos dejar el reloj como estaba
uint8_t oldTCCR0B = TCCR0B; // Al final queremos dejar el reloj como estaba

Como siempre… nuestras constantes

// Tenemos un xtal de 16Mhz, el TIMER0 es de 8 bits y lo preescalamos a 64
static unsigned int _1MS = 250;
static unsigned int _40US = 10;
static unsigned int _80US = 20;
static unsigned int _52US = 13;

Una forma nada engorrosa de deshabilitar las interrupciones -y preocuparnos de volver a habilitarlas si salimos al haber un error- es mediante

ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {...
}

Escogemos el preescalado -esto es un poco confuso puesto que la configuración se realiza en diferentes registros y de forma fraccionada-

// Modo normal del timer y preescalado a 64
TCCR0A = NORMAL;
TCCR0B = NORMAL_64;
/* Otra forma de hacerlo...
TCCR0A = 0;
TCCR0B = 0;
TCCR0B |= _BV(CS01) | _BV(CS00);

Y ahora actuamos como anteriormente. Tenemos que hacer lo mismo que hacíamos mediantes los delays( ) y micros( ) pero ahora con los timers

//delay(ACKHIGH);
TCNT0 = 0;
while (TCNT0 <= _1MS);
...
//delay(ACKLOW);
for (unsigned int i = 0; i < 18; i++) {
 TCNT0 = 0;
 while (TCNT0 <= _1MS);
}

Ciertamente es un código muy sucio en comparación con el anterior y no dejamos de abandonar la espera activa… pero si controlamos los cambios de estados mediante interrupciones en un pin limitaríamos físicamente la clase, ya que no todos los pines disponen de esta características -avisar si cambian de estado-.

Al final, este segundo código es algo mejor que el primero, pero se trataba de experimentar, aunque ya adelanto que el primero y más secillo no falla tampoco…

¿Qué falta?

Añadir una tarjeta SD para almacenar los datos, que se recogerán 1 vez por minuto Crear una aplicación para graficar los datos Utilizar un RTC para indizar los datos Lo que haré será ir cambiando este post al ir añadiendo funcionalidades.