DHT1 Temperature Sensor Library for the Raspberry Pico

The Raspberry Pico, or shorthand Pico, is a new microcontroller from the Raspberry Pi foundation. It provides a dual core ARM processor, 2MB of flash memory, and 26 GPIO pins. You can program the Pico with either a C/C++ SDK or MicroPython. I became fascinated by this device and started to develop a library for controlling a shift register, specifically the HC595N shift register. To get a better grip on the Pico C SDK, I looked for another sensor for which I could develop a library, and found the DHT11 temperature sensor.

The DHT11 sensor is connected via one data pin only. It also has a proprietary protocol for exchanging data: Send pulses of low and high voltage to activate the sensor, then read pulses that represent a bit pattern that encodes the temperature readings.

This article explains the first part of my development journey: Understanding the DHT11 protocol, creating the library folder structure, and then try to read the temperature data.

This article originally appeared at my blog.

Datasheet

The first touchpoint with any sensor is to get a datasheet and understand how it works in general. My reference for the DHT11 sensor is the documentation on mouser.com. The more often you read a datasheet, the more information you will get out of it. My primary concerns at are summarized in the following list:

  • Pin layout: The sensor is sold as three or four pin version. The four-pin version is actually three pins too - they just soldered (for convenience) another pin. Anyway, the pins are v-in, ground, and data.
  • Input voltage: Supports 3 - 5.5V, so it can be directly connected to the Pico.
  • Communication protocol: The data pin serves as input and output. An MCU sends a characteristic signal to perform a "wake-up" call on the DHT11. The sensor acknowledges this wake up, and then sends 40bit data
  • Additional operational constraints: The sensor should not be requested within 1 second of powering up, and it should not be queried more often than every 2 seconds.

With these facts we can begin the implementation.

Library Setup

The library is separated into my trusted structure from the shift register library:

  • ./ Contains the central CMakeLists.txt file for three different build types: library only, tests, and examples.
  • ./include Contains the header file
  • ./src The library source files and its accompanying cmake configuration
  • ./test Using the CMocka framework, tests validate that the library objects and functions are correct
  • ./examples Simple recipes how to use the library

Core Objects & Functions

The library exposes the struct object DHT11 with this signature:

typedef struct DHT11
{
  uint8_t DATA_PIN;

  double last_temp;
  double last_humidity;

  double temp_measurements[30];
  double humidity_measurements[30];

} DHT11;

The sensor object is created with dht11_new, and then provides these functions:

  • dht11_probe() - Take a new measurement from the sensor, but only when it’s at least two seconds after the last try
  • dht11_process() - A helper method that receives the sequenced 40bit data, calculates the checksum, and updates the internal values when the probe was valid.
  • dht11_get_last_temperature_measurement() - Returns the most recent temperature measurement
  • dht11_get_last_humidity_measurement() - Returns the most recent humidity measurement
  • dht11_get_historic_temperature_measurement() - Retrieves a temperature measurement from the last 30 probes
  • dht11_get_historic_humidity_measurement() - Retrieves a humidity measurement from the last 30 probes

Each function comes with a test suite - see test.c in the github repository. I will not detail the tests in this article, but will focus on getting the sensor to work. If you are interested in my test approach, read my earlier articles about unit tests with CMocka and testing the shift register library.

Imperative Protocol Verification

The very first version of the program can be summarized as imperative statement with explicit protocol verification.

The general idea is to define blocks of code that handle the three phases sensor startup, sensor confirmation and sensor data. Then, each phase would use a counter and a sleep_us call to explicitly wait for the time as specified in the data sheet. I would then output a debug statement after each phase to confirm that the sensor behaves as expected.

Startup and Conformation

The startup signal is simple:

  • Set the data pin to output
  • Send LOW for 18ms
  • Send HIGH for 20us
// I Activate DHT
{
  gpio_put(pin, 0);              // LOW
  sleep_ms(18);              // 18us

  gpio_put(pin, 1);              // HIGH
  sleep_us(20);              // 20us
}

To test the sensor confirmation phase, the concrete steps are:

  • Set the data pin to input
  • Wait for up to 80us that a LOW value is read
  • Print a debug statement
  • Wait for up to 80us that a HIGH value is read

The code in particular:

// II Read DHT Confirmation
{
  gpio_set_dir(pin, GPIO_IN);
  bool init_state = 0;

  int count = 0;
  while(init_state = !gpio_get(pin)) {          // EXPECT LOW 80us
    count++;
    sleep_us(1);
  };

  printf("DHT Confirmation LOW: %d\n", count);

  count = 0;
  while(init_state = gpio_get(pin)) {
    count++;
    sleep_us(1);
  };
  printf("DHT Confirmation HIGH: %d\n", count);  // EXPECT HIGH 80us
}

With this initial code, I started to test the sensor.

Mind the Hardware

After spending about 4 hours of trying explicit wait, and then even to constantly write/read data via the GPIO pin, I could not get any response from the sensor.

Either my approach is just utterly wrong, or I got the protocol confused, but it corresponds to what I saw in other blog articles. So I connected the sensor with an Arduino, uploaded the trusted Adafruit DHT Lib exmaple sketch, and .. the code failed! I only saw the error message Failed to read from DHT sensor!. The sensor was broken, and all that time wasted.

So I ordered a new sensor, uploaded the Arduino sketch, and could get these results on the serial terminal:

Humidity: 46.00%  Temperature: 24.30°C 75.74°F  Heat index: 23.99°C 75.18°F

The sensor was broken! I wasted a lot of time and energy into fixing code while the problem was in the hardware. A crucial lesson in embedded software developmetn.

Getting the Timings Right

Even with a working sensor, I had no success to get the timing right. Neither reading the datasheet and double checking with another datasheet, nor looking into the official Pico example and also into an Arduino library for the same sensor type helped. Both libraries work with explicit calls to sleep for the amount of time as specified by the sensor, but I could not get it done. Investing further project time to study the timing effects of the various statements in my code - like what is the impact of the counter, and what of the printf - was also not working.

Taking some time off, and studying other projects, I found out that MicroPython implementations for this sensor are available. And these libraries are using the Pico PIO system, a special hardware system on the Pico that you program with assembly-like language. I gave up on pure C, and decided to investigate the utilization of Pico. The next blog article details how Pico can be used.

Conclusion

My first attempt to read temperature data from a DHT sensor failed. I thought long about the process and whether I should write about a failed attempt. Definitely I should because I want to share these essential learnings. First, always check that the hardware you are working with is functioning. I wasted 4 hours to fix code that was not working on a, well, broken sensor. Second, to the least of my knowledge, getting very precise timings with just the C SDK is difficult - if you have made better progress in this direction, tell me.

The next article will continue the sensor development indirectly: We will learn the basics of the PIO system, and see how to include it in a C program.

8