Dienstag, 9. September 2014

DHT22 AM2302 Sensor am STM32F103

Liebe Entwickler,

mein erstes Projekt mit dem STM32F103-Board ist die Ansteuerung eines DHT22-Sensors zur Temperatur- und Luftfeuchtigkeitsmessung. Mein Testaufbau sieht dann so aus:


Die schwarze Box rechts ist ein Logic-Analyzer von Saleae (möglicherweise ein chinesischer Nachbau davon ;-)), der mir enorm beim Debuggen geholfen hat.

Ein halb-englisch-halb-chinesisches Datenblatt vom DHT22 stellt beispielsweise der Händler Adafruit auf der Produktseite zur Verfügung. Ich selbst habe den Sensor nicht dort, sondern wie üblich bei Aliexpress bestellt. Mit Hilfe der ebenfalls auf der Produkseite angebotenen Arduino-Bibliothek ließ sich das Protokoll des DHT22 dann auch entschlüsseln. Insbesondere die Umwandlung der gelesenen Bits in eine Fließkommazahl habe ich von Adafruit geguttenbergt.

Zum Protokoll selbst:
Generell handelt es sich um ein serielles, unidirektionales Verfahren mit LSB-first. Es wird lediglich eine einzige Datenleitung zwischen Host und DHT22 verwendet. Sowohl Host als auch DHT22 können die Datenleitung auf Masse "ziehen" und sie dann wieder "loslassen". Ein PullUp-Widerstand stellt sicher, dass im Ruhezustand die Datenleitung auf dem Niveau der Versorgungsspannung liegt. Empfohlen wird ein 4k7..10k-Widerstand. Der im SMT32 integrierte PullUp hat etwa 40k, weshalb ich mich für einen externen PullUp mit 10k entschieden habe.

Eine Transaktion läuft ausgehend vom Ruhezustand wie folgt ab

  1. Der Host zieht die Datenleitung >500us auf Masse. Der DHT22 interpretiert dies als Startbefehl. Der Host zieht nun bis zum Ende der Übertragung nicht mehr auf Masse, sondern "lauscht" passiv.
  2. Etwa 20-40us nach dem "Loslassen" des Hosts zieht der DHT22 die Datenleitung für 80us auf Masse und zeigt damit seine Präsenz.
  3. Etwa 80us nach dem "Loslassen" beginnt die eigentliche Datenübertragung
  4. Für jedes Bit zieht der DHT22 die Leitung für 50us auf Masse
  5. Die Dauer des darauf folgenden Loslassens bestimmt die Polarität des Bit.
    • Wenn das Loslassen 26-28us dauert, ist dies als 0 zu interpretieren
    • Wenn das Loslassen etwa 70us dauert, ist dies als 1 zu interpretieren
  6. Insgesamt werden so 40bits übertragen

Eine komplette Transaktion sieht im Logic-Analyzer dann so aus:


Mein Ziel in diesem Projekt ist es, bis zu 16 DHT-Module parallel an einen Port des STM32 anzuschließen und diese in einer Interrupt-gesteuerten Schleife immer wieder abzufragen. Ich war der festen überzeugung, dass die Performance eines 72MHz-Prozessors probleeeeeemlos ausreicht, um das notwendige "Bitbanging" und das bisschen Aktualisieren von Datenarrays en passant zu erledigen.
Tja, wie man sich doch täuschen kann!
Tatsächlich wird das Timing in diesem Projekt zur sehr kritischen Komponente. Die Zykluszeiten des DHT22 Signals machen es erforderlich, dass zumindest im 20us-Zyklus abgetastet wird (Shannon-Theorem +  Sicherheitszuschlag ;-) ). In dieser Zeit schafft es der STM32 nicht, über alle 16 Bits des Ports zu iterieren und bei Bedarf ein Datenarray zu aktualisieren. Das Diassembly meines ersten Codeentwurfes zeigt im Abgleich mit der Taktzyklustabelle von ARM, weshalb das so ist (Man beachte den Performance-Counter oben rechts):


So einfache Schleifen oder if-Abfragen verbrauchen letztlich jeweils über 10 Takte. Alleine die 16malige Abfrage, ob für das betreffende Bit etwas zu tun ist, verschlingt über 300 Takte, also grob 5us. Das ist zwar jetzt ein Klagen auf hohem Niveau, aber alleine diese Prüfung macht 25% der mir zur Verfügung stehenden Zeit aus . Im Übrigen half da auch eine testweise aktivierte Compileroptimierung nicht viel.

Ich habe meinen Code weiter optimiert und die Anforderungen reduziert. Wesentliche Punkte waren

  • Der Port-Input-Befehl der SPL ("GPIO_ReadInputDataBits") dauert wegen der im Debug-Mode durchgeführten Assertions schon alleine etwa 3us. Natürlich geht das im Release schneller, aber zur Geschwindigkeitssteigerung dieser spezeillen Funktion beim Debuggen habe ich die SPL-Aufrufe durch direkte Registerzugriffe ersetzt.
  • Reduktion der unterstützten Pins (im Code nur noch 8, kann aber erhöht werden)
  • In einer ersten Version suchte ich sowohl nach der steigenden als auch nach der fallenden Flanke des DHT-Signals. Anhand der Zeitdauer des positiven Signals konnte der Code bestimmen, ob eine 0 oder eine 1 übertragen wurde. Die neue Codeversion wird ausschließlich bei fallenden Flanken aktiv. Die Polarität des Bits wird durch das Intervall zur vorherigen fallenden Flanke bestimmt. Insgesamt wird also weniger Code pro Sensor ausgeführt.
  • Einige Umstellungen im Code brachten einige wenige Taktzyklen.
Der Kerncode "doProtocol" ist so konzipiert, dass er aus Interrupthandlern von Timern und EXTIs aufgerufen werden kann, aber auch für den Aufruf aus einer synchronen pollenden Methode geeignet ist. Im Codebeispiel ist die letzte Möglichkeit umgesetzt.
Die Funktionsweise des Codes ist an sich sehr einfach:
  • Eine Zustandsverwaltung kümmert sich zunächst um das Reset-Signal und das Abwarten der Präsenzmeldungen. Sollte die Präsenzmeldung nicht wie im Datenblatt beschrieben erfolgen, werden Fehler zurückgegeben
  • Sobald die Zustandsverwaltung erkennt, dass die eigentliche Datenübertragung erfolgt, übergibt sie die Kontrolle immer an doBitbang2. Deren Funktionalität ist im Code anhand der Kommentare nachvollziehbar. Besonders hinweisen möchte ich auf zwei Sachverhalte
    • Es findet eine Timeout-Erkennung statt. Wenn der Sensor keine /zu wenige Signale liefert, kommt es nicht zu einem Fehler. Auch zu viele Signale (wie auch immer das passieren kann) werden effizient abgefangen und führen nicht zu einem Überlauf.
    • Die allererste fallende Flanke muss übersprungen werden, weil diese ja noch kein Bit repräsentiert, sondern lediglich den Start des Payloads repräsentiert.
Den kompletten Code findet ihr in Form eines Beispielprojektes unter https://stm32tutor.googlecode.com/svn/trunk/103dht22

Happy Coding - ich freue mich auf Verbesserungsvorschläge




Keine Kommentare:

Kommentar veröffentlichen