Using the PIO to Interface with the DHT11 and DHT22 sensors

The DHT11 and DHT22 are popular sensors for measuring temperature and humidity. While connecting these sensors to a microcontroller is very simple, communicating with them demands generating and measuring times of the order of microseconds. Let's see how the RP2040's PIO can help us.


How the DHT11 and DHT22 Communicates

Communication between these sensors and a microcontroller uses a single bi-directional signal. There is an internal pull-up resistor that forces this signal high when it is not driven by the microcontroller or the sensor. The image bellow shows the data pin when a reading is done:


Left: full reading, Right: zoom on the first byte

To request a reading, the microcontroller must drives the pin low for at least 18ms (DHT11) or 1ms (DHT22). After that, it reconfigure the pin for input to get the sensor response,

The sensor acknowledges the request by driving the  pin low for 80 ms and them high for another 80 ms. After that it will send a 40 bit answer. A zero is sent as low for 50 usec followed by hight for 26 to 28 usec. A one is sent as low for 50 usec followed by hight for 70 usec.

The 40 bits in the answer are interpreted as 5 bytes:
  • 2 bytes for humidity
  • 2 bytes for temperature
  • 1 byte for error check (it should be the sum of the previous four bytes)

The encoding of the values is different for the two models.

The DHT11 can read temperatures form 0 to 50C, with a 2 degree precision and send the values as separated  integer and decimal parts (the decimal part for temperature should be zero):

  • If the two bytes for humidity are 86 and 2, the humidity is 86.2%
  • If the two bytes for temperature are 19 and 0, the temperature is 19.0C
The DHT22 can read temperatures form -40 to +80C, with a 0.5 degree precision. The values 16 bit integer value in tenths, with most significant byte first. In the temperature, the most significant bit is the signal (0 is positive, 1 is negative):
  • If the two bytes for humidity are 2 and 86, , the humidity is (2*256+86)*0.1 = 59.8%
  • If the two bytes for temperature are 1 and 8, the temperature is (1*256+8)*0.1 = 33.6ºC.
  • If the two bytes for temperature are 129 e o 8, the temperature is -(1*256+8)*0.1 = -33.6ºC.
The datasheets also specify that there must be a 2 second interval between readings.

The "Bit Banging" Solution and its Problem

To figure out the bits, we must carefully measure how long the signal stays high in each bit. Without hardware assist, we will have to do that by testing the pin in a tight software loop. That what is used in many libraries (for an example, look at https://github.com/adafruit/DHT-sensor-library/blob/master/DHT.cpp). 

If an interrupt occurs in the loop, or processing is not fast enough, we may get the data wrong. If we turn off interrupts, the microcontroller will be "deaf" for a few milliseconds.

The PIO Solution

The PIO program shown ahead does the following:
  • Put the pin in output mode
  • Wait for the processor to write a value
  • Keep the pin low by the time defined by this value
  • Change the pin for input
  • Wait for the acknoledge
  • Read bits
  • Send back each byte received
This is done by a proper combination of configuration and programming. In the configuration we:
  • Set a clock of 1.4MHz (so 1 cycle is about 0,7 usec)
  • Select the pin connected to the sensor for input, set and jump condition
To read the sensor, the ARM program:
  • Configures the PIO
  • Starts the PIO
  • Writes the initial pulse value (969 if DHT11 or 54 if DHT22)
  • Reads 5 bytes from the PIO
  • Stops the PIO
All this can be done in MicroPython:
import utime
import rp2 
from rp2 import PIO, asm_pio
from machine import Pin
 
# PIO program
# automatically pushes each 8 bits of received data in the FIFO 
@asm_pio(set_init=(PIO.OUT_HIGH),autopush=True, push_thresh=8) 
def DHT_PIO():
    # wait for a request
    pull()
     
    # keeps pin in 0 for the requested time
    set(pindirs,1)              # set pin to output  
    set(pins,0)                 # set pin low
    mov (x,osr)
    label ('waitx')
    nop() [25] 
    jmp(x_dec,'waitx')          # wait time*26/clock=x
      
    # reads the acknoledge
    set(pindirs,0)              # change pin to input
    wait(1,pin,0)               # wait for high level
    wait(0,pin,0)               # wait for initial pulse
    wait(1,pin,0)
    wait(0,pin,0)               # wait for first bit
 
    # read the bits
    label('readdata')
    wait(1,pin,0)               # wait for pin to go high
    set(x,20)                   # register x is the timeout for low
    label('countdown')
    jmp(pin,'continue')         # count while pin is high
     
    # pin went low before timeout -> bit 0
    set(y,0)                 
    in_(y, 1)                   # puts a zero in result
    jmp('readdata')             # read next bit
     
    # pin is still high
    label('continue')
    jmp(x_dec,'countdown')      # decrement count
 
    # timeout -> bit 1
    set(y,1)                  
    in_(y, 1)                   # puts a one in result
    wait(0,pin,0)               # wait for pin get back to low
    jmp('readdata')             # ler o próximo bit
 
DHT11 = 0
DHT22 = 1
 
class DHT:
 
    # Construtor
    # dataPin: data pin
    # model:   DHT11 ou DHT22
    # smID:    state machine id
    def __init__(self, dataPin, model, smID=0):
        self.dataPin = dataPin
        self.model = model
        self.smID = smID
        self.sm = rp2.StateMachine(self.smID)
        self.lastreading = 0
        self.data=[]
     
    # read sensor
    def read(self):
        data=[]
        self.sm.init(DHT_PIO,freq=1400000,set_base=self.dataPin,
                     in_base=self.dataPin,jmp_pin=self.dataPin)
        self.sm.active(1)
        if self.model == DHT11:
            self.sm.put(969)     # wait 18 miliseconds
        else:
            self.sm.put(54)      # wait 1 milisecond
        for i in range(5):       # read 5 bytes
            data.append(self.sm.get())
        self.sm.active(0)
        total=0
        for i in range(4):
            total=total+data[i]
        if data[4] == (total & 0xFF):
            # checksum ok, salvar os dados
            self.data = data
            self.lastreading = utime.ticks_ms()
            return True
        else:
            return False
 
    # get data by reading or using stored data
    def getData(self):
        # make sure we have some data
        while len(self.data) == 0:
            if not self.read():
                utime.sleep_ms(2000)
             
        # only read if more than 2 seconds since previous reading
        agora = utime.ticks_ms()
        if self.lastreading > agora:
            self.lastreading = agora  # counter wraped up
        if (self.lastreading+2000) < agora:
            self.read()
     
    # returns the humidity
    def umidade(self):
        self.getData()
        if self.model == DHT11:
            return self.data[0] + self.data[1]*0.1
        else:
            return ((self.data[0] << 8) + self.data[1]) * 0.1
 
    # returns temperature
    def temperature(self):
        self.getData()
        if self.model == DHT11:
            return self.data[2] + self.data[3]*0.1
        else:
            s = 1
            if (self.data[2] & 0x80) == 1:
                s = -1
            return s * (((self.data[2] & 0x7F) << 8) + self.data[3]) * 0.1
 
#main program
dht_data = Pin(16, Pin.IN, Pin.PULL_UP)
dht = DHT(dht_data, DHT22, 0)
 
while True:
    print("Humidity: %.1f%%, Temperature: %.1fC" % 
          (dht.umidade(), dht.temperature()))
    utime.sleep_ms(3000)
The PIO code above was adapted from https://github.com/ashchap/PIO_DHT11_Python, using ideas from https://github.com/danjperron/PicoDHT22

Comments

Popular posts from this blog