Diese Seite ist Teil des Projekts Automatische Gewächshauslüftung.

Das folgende Testprogramm benutzt Interrupts zur Kontrolle des Einlesevorgangs. Im diesem Programm ist der Programm-Rahmen (main.cpp) für einen Arduino geschrieben. Das vereinfacht die Datenausgebe ein wenig, da das Objekt Serial hierzu zur Verfügung steht. Das Einlesen der Daten erfolgt jedoch über ein Standard-C-Programm (DHT11.c), das bei der Gewächshaus-Lüftung verwendet werden soll. Die allen zugänglichen Funktionen und Variablen werden in DHT11.h veröffentlicht.

Das zugehörige Atmel Studio-Projekt zum Download.

Anschlussplan Anschlussplan:

Die Stromversorgung erfolgt über den Arduino. Die Datenleitung des DHT11 ist mit dem Pin 8 des Arduino verbunden und mit einem Pullup-Widerstand von ca. 4,7 - 10 kΩ versehen. Der Pin 8 ist der Pin mir der Input-Capture-Funktion (IC).

Auslesevorgang

Zunächst eine Übersicht zum Vorgang des Datenauslesens. Das Messprinzip ist relativ einfach: In der Funktion Dht11BeginRead() wird der Messvorgang eingeleitet, d.h. das Startsignal ausgegeben und auf die sichere Übernahme der Datenleitung durch den DHT11 gewartet. Danach werden die Zeit zwischen zwei fallenden Flanken über den Input Capture-Mechanismus des Timer1 detektiert. Ist die Zeitdauer ca. 70 µs wird eine 0 erkannt, ist sie etwa 120 µs liegt eine 1 vor. Die Bereitschaftsmeldung des DHT11 wird ausgeblendet. Ein Timeout, d.h. Aussetzer bei den fallenden Flanken, wird per Output Compare A Match erkannt. Während des Messvorgangs wird der aktuelle Status des Vorgangs in der Variablen Dht11Valid präsentiert. Das Messergebnis nach einem erfolgreichen Auslesen wird in den Variablen Temperature und Humidity abgelegt.

DHT11.h:

#ifdef __cplusplus
extern "C"{
#endif

extern volatile uint8_t Temperature; // Letzter Messwert Temperatur
extern volatile uint8_t Humidity;    // Letzter Messwert Luftfeuchtigkeit
extern volatile int8_t  Dht11Valid;  // Status des Messvorgangs
                                     //  0: Messung aktiv;
                                     //  1: Gültige Messung;
                                     // -1: Timeout;
                                     // -2: Zeitfehler, Bit-Zeit zu klein;
                                     // -3: Zeitfehler, Bit-Zeit zu groß;
                                     // -4: Prüfsumme stimmt nicht

void Dht11BeginRead(); // Startet eine Messung

#ifdef __cplusplus
} // extern "C"
#endif

Die Einfassung der Deklaration in einen extern "C"-Block stellt sicher, dass der CPP-Compiler des Hauptprogramms die korrekte Namenskonventionen benutzt. Die Messwert-Routine ist in C geschrieben.

Dht11Valid liefert Informationen über den Status des aktuellen bzw. letzten Messvorgang.

In den Fehlerfällen -1 .. -4 sind die Werte in den Variablen Temperature und Humidity ungültig. Sie enthalten den Wert der letzten gültigen Datenübertragung. Nach einem Fehler empfiehlt es sich, mindestens 10 ms bis zur nächsten Datenübertragung zu warten, damit der DHT11 eine unvollständige Übertragung beenden kann.

Timing

Der Prescaler des Timer ist auf 64 eingestellt. Bei einer Taktfrequenz von 16 MHz ist die Taktdauer damit 4 µs. Dies ist zum einem kurz genug, dass die einzelnen Periodenzeiten selbst einer erheblichen Ungenauigkeit noch sauber voneinander getrennt werden können. Zum anderen sind alle Zeiten deutlich kleiner als 256 Takte und können in einem Byte abgelegt werden.

// Counter-Periode = 4µs = Taktdauer
// Bei Beobachtung der fallenden Flanke
// Start: 160µs (80µs + 80µs) = 40 Takte (wird ausgeblendet)
// 0-Bit:  80µs (50µs + 28µs) ca. 20 Takte (BIT_MIN .. BIT_0_1)
// 1 Bit: 120µs (50µs + 70µs) = 30 Takte (BIT_0_1 .. BIT_MAX)
// Overflow: 60 Takte

#define BIT_MIN 15   // Bit-Zeiten mit kleineren Taktzahlen als BIT_MIN, sind zu kurz => Fehler
#define BIT_0_1 25   // Bit-Zeiten mit kleineren Taktzahlen als BIT_0_1 stellen 0 dar, längere 1
#define BIT_MAX 35   // Bit-Zeiten mit größeren Taktzahlen als BIT_MAX, sind zu lang => Fehler
#define TIMEOUT 60   // Taktzahl, nach deren Verstreichen ohne erneute fallende Flanke auf Timeout erkannt wird

Speicher

// Öffentliche Variablen
volatile uint8_t  Temperature; // Ergebnis der Temperaturmessung
volatile uint8_t  Humidity;    // Ergebnis der Feuchtemessung
volatile int8_t   Dht11Valid;  // Status des Messvorgangs
                               //  0: Messung aktiv;
                               //  1: Gültige Messung;
                               // -1: Timeout;
                               // -2: Zeitfehler, Bit-Zeit zu klein;
                               // -3: Zeitfehler, Bit-Zeit zu groß;
                               // -4: Prüfsumme stimmt nicht

// Interne Variablen
static uint16_t FallingEdge;   // Zählerstand bei vorhergehender Flanke
static int8_t   BitCount;      // Nummer des Bits, das aktuell eingelesen wird (0-basiert)
static uint8_t  Data[5];       // Speicher für die 5 zu übertragenden Bytes

Start des Auslesen

#define PORT_IC PORTB // Port des benutzen ICP
#define DDR_IC  DDRB  // zugeh. DDR
#define P_IC    (1 << PB0) // Bitmaske für ICP

// Datenübertragung vom DHT11 einleiten
void Dht11BeginRead()
{ Dht11Valid = 0;   // Status: Messvorgang gestartet
  BitCount = -1;    // -1: Erste Flanke is kein gültiges Bit, sondern Bereischaftsmeldung

  // Datenleitung für ca. 20 ms auf low legen
  DDR_IC  |=  P_IC; // P_IC als Output konfigurieren
  PORT_IC &= ~P_IC; // Ausgang auf Low
  _delay_ms(20);    // 20 ms dauerndes Startsignal
  PORT_IC |= P_IC;  // Ausgang auf High

  _delay_us(40);    // dem DHT11 die Kontrolle der Datenleitung übergeben
  DDR_IC &= ~P_IC;  // P_IC als Input konfigurieren

  _delay_us(40);    // sicherstellen, dass der DHT11 die Kontrolle übernommen hat

  // Timer1 konfigurieren & aktivieren
  TCCR1A = 0;       // Arduino modifiziert den Timer, deshalb komplett bestücken
  TCCR1C = 0;
  TCCR1B = (1<<CS11) | (1 << CS10) | (0 << ICES1);  // Prescaler ist 64 => Counter-Takt = 4µs
                                                     // Input Capture mit fallender Flanke
  OCR1A  = TIMEOUT; // OCRA für Timeout-Bestimmung vorbereiten
  TCNT1  = 0;       // Zähler zurücksetzen

  TIFR1   = (1<<ICF1) |(1<<OCF1A);  // Input Capture Flag & Output Compare A Match Flag löschen
  TIMSK1 |= (1<<ICIE1)|(1<<OCIE1A); // Input Capture Interrupt (ICP) & Output Compare A Match Interrupt freigeben
}

Datenübertragung

Die fallenden Flanken lösen einen Input Capture Interrupt aus. Je nach Zeitdauer zwischen zwei Flanke wird auf ein 0-Bit, ein 1-Bit oder auf Fehler erkannt. In BitCount wird die lfd. Nummer des aktuellen Bits mitgezählt (beginnend mit 0). BitCount wird mit -1 vorbelegt. Der erste Interrupt wird nämlich durch die fallende Flanke der Bereitschaftsmeldung ausgelöst.

Zum Schluss wird die Prüfsumme getestet und das Ergebnis zurückgemeldet.

Damit sich das Programm nicht aufhängt, wenn bei der Übertagung etwas schiefgeht, ist ein Timeout-Mechanismus eingebaut. Hierzu wird wegen der einstellbaren der Output Compare A Match benutzt. Die Zeitschwelle wird beim jeweils beim Eintreffen einer fallenden Flanke vorgestellt.

// ISR für den Input Capture Interrupt
ISR(TIMER1_CAPT_vect)
{ uint16_t ICR = ICR1;               // Zwischenspeicher vermeidet wiederholte Zugriffe auf das Register
  uint8_t  time = ICR - FallingEdge; // Zeit messen
  FallingEdge = ICR;                 // Neue Startzeit
  OCR1A = ICR + TIMEOUT;             // Timeout-Erkennung anpassen, TIMEOUT-Takte bis zur nächsten Flanke

  // Bereitschaftsmeldung ignorieren
  if (BitCount < 0)
  { BitCount = 0;
    return;
  }

  // Zeit auswerten
  if (time < BIT_MIN) // Periode zu kurz
  { Dht11Valid = -2;
    TIMSK1 &= ~((1<<ICIE1)|(1<<OCIE1A)); // Fehler: Interrupts sperren
    return;
  }
  if (time > BIT_MAX) // Periode zu lang
  { Dht11Valid = -3;
    TIMSK1 &= ~((1<<ICIE1)|(1<<OCIE1A)); // Fehler: Interrupts sperren;
    return;
  }

  // Die eingelesenen Bits speichern
  Data[BitCount/8] <<= 1;   // Bereits eingelesene Bits nach links schieben
  if (time > BIT_0_1)       // 0 oder 1?
    Data[BitCount/8] |= 1;
  BitCount++;

  if (BitCount == 40) // alle Bits eingelesen
  { if (Data[4] != ((Data[0] + Data[1] + Data[2] + Data[3]) & 0xFF)) // stimmt die Prüfsumme?
    { Dht11Valid = -4;
    }
    else
    { Humidity    = Data[0]; // Ausgabe-Werte speichern
      Temperature = Data[2];
      Dht11Valid = 1;        // Ergebnis ist gültig
    }

    TIMSK1 &= ~((1<<ICIE1)|(1<<OCIE1A)); // Fertig: Interrupts sperren
  }
}


// ISR für den Output Compare A Match Interrupt
// Dies bedeutet Timeout
ISR(TIMER1_COMPA_vect)
{ TIMSK1 &= ~((1<<ICIE1)|(1<<OCIE1A)); // Interrupts sperren
  Dht11Valid = -1; // Ergebnis ist Timeout
}

Hauptprogramm

#include "Arduino.h"
#include "AppVersion.h"
#include "DHT11.h"

// the setup routine runs once when you press reset:
void setup()
{ Serial.begin(9600);
  Serial.println(F("DHT11 Test, Version " VERSIONSTRING));
}

// the loop routine runs over and over again forever:
void loop()
{ Dht11BeginRead();  // Mesuung starten

  while(Dht11Valid == 0); // Warten, bis die Messung abgeschlossen ist

  Serial.println(F("----------")); // Ergebnis ausgeben

  if (Dht11Valid == 1)
  { Serial.print(F("Hum: "));
    Serial.print(Humidity);
    Serial.print(F(" Temp: "));
    Serial.println(Temperature);
  }
  else
  { Serial.print(F("Invalid: "));
    Serial.println(Dht11Valid);
  }
  delay(2000);
}

Der Trick mit AppVersion ist übrigens hier erklärt: Build-Nummer: Immer Eins drauf!