Saturday, January 14, 2012

Using a RHT03 (aliases: RHT-22, DHT22, AM2302) temperature/humidity sensor from Arduino

I picked up a nice compact little temperature and relative humidity sensor called the RHT03 for a project from Little Bird Electronics. It and very similar parts appear to go by the names RHT-22, DHT-22 and AM2302. You can find the part at SparkFun, Adafruit, etc too.

It took a lot more work to get it working than I expected, so I thought I'd write it up here for anyone else who is looking into it. There's sample code at the end of this post, but you should probably read the details because this is a quirky beast.

UPDATE: I since found a library on GitHub: nethoncho/Arduino-DHT22 that does a better job more simply and compactly. It works fine with my sensor. It needed some changes for Arduino 1.0 and some further tweaks to work how I wanted, so I've uploaded a fork here: https://github.com/ringerc/Arduino-DHT22.

Given that it's clearly a pretty commonplace part, I thought reading it couldn't be too tricky. Foolish me!Its datasheet almost in engrish and the sample code (warning: Word document) is pretty horrid. The sensor communicates over a custom (non-Dallas-compatible) 1-wire interface where the lengths of low and high pulses convey information. Parts of the datasheet are practically fiction.

Particular things to note when working with this sensor are:

  • When requesting a sensor read, you may have to actively pull the signal line up to +5V for 20-40μs after your initial 1 - 10ms low pulse.
  • The datasheet is a lie. In particular, instead of a neat 50μs low signal before sending a bit, my sensor may pull the line low for anything from 35 to 75μs. Similarly, 0-bit high pulses may be from 10-40μs instead of the documented 22-26μs, and 1-bit high pulses may be 60-85μs instead of 70μs. Actually, surprise surprise, this was probably mostly my code not masking the timer interrupt while conversing with the sensor, thus losing time when the timer ISR ran. Surprise, newbie code buggy. I still measure significant timing variation from the sensor with NetHoncho's code, but nothing like as bad.
  • Even when you're being tolerant of timing weirdness, you'll probably get the odd bad read. Adding a cap across the +5VDC input (as mentioned in the datasheet) doesn't seem to help, so it's probably not power fluctuation. Be prepared to re-try reads and remember to wait at least 2s between reads. ("The odd bad read" was probably the timer issue mentioned above, but you should be ready for them anyway)
  • (Didn't trouble me, but): Remember to let the sensor settle for at least 2s after power-on before attempting a read, and at least 2s between reads.
  • Your code must be extremely fast to avoid missing state transitions when capturing the bit stream, whether you're using a looping or interrupt-driven capture method. I landed up buffering the stream to an array of pulse lengths, then interpreting it after the full stream was buffered. This would be less of an issue if the timings from the sensor weren't so erratic to start with.

Having finally got it working, I thought I'd post some sample code. It's very much that - sample code - not a library suitable for direct re-use. In particular, this code does not handle millisecond counter roll-over at all. Reads during which the millisecond counter rolls over will fail. That said, at least the checksum is verified and negative temperatures are handled correctly. (By the way, a USB cable coming out of the freezer door seal looks really weird).

You should really look at the GitHub project for DHT-22 linked to above rather than using this code.

To try the code with an Arduino Uno R3 (ATMEGA328 based), connect the sensor's I/O wire (pin 2) to digital pin 2 on the Arduino. Connect sensor pin 1 to +5V and sensor pin 3 and 4 to GND. Theoretically you should be able to let pin 3 float, but I've seen reports that some sensors have their internal ground on pin 3 instead of pin 4, so it won't hurt to ground both.

I use pin 2 on the Arduino because it's one of the two pins that support external interrupts on the basic Arduino boards. Pin 3 will also work, just change sensorPin in the example code.

You could make the code work without requiring interrupts, either by busy-waiting in a loop to detect level changes or by using the pulseIn function to read pulse lengths. I haven't implemented that, but it shouldn't be hard. You can re-use all the code except readSensor, which must be rewritten to capture the timings array a non-interrupt-driven way. The trick is making it fast enough.

https://github.com/ringerc/scrapcode/blob/master/arduino_avr/RHT03_DHT22_humidity_temp_sensor.c

8 comments:

  1. I get an error: Requesting Data ... Bus Hung (with the example code). How may I fix this?
    At the moment, I'm using another code: arduino.cc/playground/Bulgarian/Dht22

    ReplyDelete
    Replies
    1. The sample code will never emit the text "Bus hung". Do you mean the DHT22 library I linked to, originally by NetHoncho? More specifics please.

      Delete
  2. I feel like I've gone through every sample for the dht22 and none of them work for me. I keep getting read failures or ACK timeouts. Using ladyada's code, I get a lot of timeouts and temperature and humidity levels in the +/- 3000 range. Not really sure what to do. Thought this would be a relatively simple upgrade from my dht11. Would appreciate any help

    ReplyDelete
  3. First, I'd like to thank you for all the work you did, it made my work a lot easier. I started with your code, but I've learned, you just can't do this with interrupts; it’s not the RHT03 that is all that flaky, it is the Arduino. I have one of those Saleae Logic Probes (the first time I used it for anything useful), and I took a look at what was going on. I modified your code so it would toggle another pin in the interrupt handler, and I monitored the output from the RHT03 and the pin I was toggling. What I saw was the mostly mirrored each other, but fairly regularly, the Arduino would fall behind a state change in the RHT03 by about 18 usecs, and that would throw off everything. What must be happening is an interrupt routine (probably the timer) is executing about 300 instructions (at 16MHz) at this time, and is screwing up the timing. I really can’t see any way around that using interrupts.
    I rewrote the read sensor routine to just poll the value of the sensor pin. Even that didn’t work, until I disabled interrupts as the poll was happening, and now it is rock solid (I’ve kind of mushed everything together and taken out comments for the post):
    boolean readSensor(int * temperature_decidegrees_c, int * rel_humidity_decipercent) {
    initState();

    pinMode(sensorPin, OUTPUT);
    digitalWrite(sensorPin, LOW);
    delayMicroseconds(3000);

    digitalWrite(sensorPin, HIGH);
    delayMicroseconds(30);

    pinMode(sensorPin, INPUT);

    int state=digitalRead(sensorPin);
    TCNT3=0;
    unsigned int counter=0;
    cli();
    while (counter!=0xffff)
    {
    counter++;
    if(state!=digitalRead(sensorPin))
    {
    state=digitalRead(sensorPin);
    timings[signalLineChanges] = TCNT3;
    TCNT3=0;
    counter=0;
    signalLineChanges++;
    }
    if(signalLineChanges==83)
    break;
    }
    sei();
    if (signalLineChanges != 83) {
    sprintf(errorMsgBuf, "*** MISSED INTERRUPTS ***: Expected 83 line changes, saw %i", signalLineChanges);
    flagError();
    } else {
    //debugPrintTimings(); // XXX DEBUG
    // TODO: memory barrier to ensure visibility of timings array
    // Feed the collected pulse sequence through the state machine to analyze it.
    analyseTimings(temperature_decidegrees_c, rel_humidity_decipercent);
    //}
    }
    return !errorFlag;
    }

    I first used Timer 3 because I thought it’s resolution (0.0625 usecs = 1 / 16MHz) might be the solution because micros() only has a 4usec resolution on the Mega ( and 8 usec on the Uno). Since the real solution was disabling the interrupts, you have to use a hardware clock, because micros() needs interrupts to be updated. I added the following code to the top of analyseTimings to convert the Timer 3 values to usecs, and then the rest of your code could run unmodified:
    for (int i=0;i<88;i++)
    timings[i]=(float)timings[i]*0.0625;

    Thanks for all your work, especially with decoding the bytes into a useable format; I’m glad I don’t have to do that now.

    ReplyDelete
    Replies
    1. Heh, that's not the Arduino that's flakey, it's my code. Shock, horror, newbie code buggy and scary, news at 11. Yeah.

      Thanks for your post; it feels really stupid not to have seen the issue with the timer in retrospect. I was thinking that when my ISR was running, no other ISR could run so I wouldn't have the timer jumping in and messing with my signalling - but had neglected to reverse that to "if the timer interrupt gets there first, its ISR will run and my interrupt can't run until afterwards and will lose time".

      It'd be possible to temporarily mask the timer interrupt while interrogating the sensor; if only the timer interrupt is masked and the interrupt on the DHT-22 line remains active then interrupt driven code for the sensor can still work. There's not much point though. With timing that tight it makes much more sense to do what NetHoncho did: disable interrupts, then busy-wait to kill time between state transitions during the short period of the sensor conversation.

      Again, thanks very much for your post and for pointing out the cause of my timing issues.

      Delete
  4. I was having problems getting DHT22 to read with Arduino...until i found your blog. Excellent... thanks mate!! next step is multiple concurrent DHT22 inputs to copare values in different places. thanks again. dav-o

    ReplyDelete
  5. i was having troubles, until i found this blog. excellent. thanks for posting this information, mate! the next step for me is to collect data from multiple dht22s around the place. thanks again!

    ReplyDelete
  6. Thank you very much for your post. I had lots of troubles too, with a library I created, I guess it was all about timing... Thank you for sharing this!

    ReplyDelete