In this tutorial we will check how to obtain temperature measurements from a DHT22 sensor using the ESP32, the Arduino core and timer interrupts.
In order to interact with the DHT22 from the ESP32, we will need an auxiliary library. Please check this tutorial which explains how to install it and also how to wire the ESP32 to the DHT22.
For an introduction on the ESP32 timers, please check this previous post. It explains in detail the timer concepts, which will be important to understand the code below.
One important thing to remember about the ESP32 timers is that they are implemented with 64 bit counters and 16 bit prescalers. So later, in the coding section, we are going to be configuring the timer interrupt by setting the counter value at which the interrupt should be triggered.
For a tutorial that introduces the synchronization between a task and an Interrupt service Routine, please check here. As shown in that tutorial, we will take advantage of the FreeRTOS semaphores to achieve such synchronization.
In terms of implementation, our code will periodically read measurements from the DHT22 sensor. Nonetheless, instead of relying on polling or Arduino delays, we will use the timer interrupts to implement the periodicity of the measurements.
Note however that interrupt handling functions should run as fast as possible, so we should not communicate with the DHT22 inside those functions. So, the interrupts will only be responsible for signaling the Arduino main loop when its time to get a new measurement.
We will use an interval of 10 seconds between each measurement, which is a lot of time when we think about machine instructions and clock frequencies. So, between measurements, it doesn’t make sense to leave the main loop active, wasting precious CPU time.
So, we will use the semaphores to keep the main loop “blocked” while it is waiting for the next interrupt, leaving the CPU available for the scheduler to execute other tasks. We could have used a dedicated FreeRTOS task but since for this example we will only fetch and print measurements, we will keep the code simpler by using the main loop.
Naturally, this blocking architecture doesn’t add much to our simple example where our application doesn’t do anything else than interacting with the DHT22. Nonetheless, in a real application use case where multiple tasks may be executing concurrently, freeing the CPU from a task when it is not doing anything is of extreme importance.
On a final introductory note, please take in consideration that the DHT22 allows to both get temperature and humidity measurements. Nonetheless, for simplicity and to focus on the synchronization, we will only fetch temperature measurements.
To facilitate the interaction with the mentioned sensor, I’m using a DFRobot DHT22 module which already has all the electronics needed and exposes a wiring terminal that facilitates the connections to the ESP32.
The tests were performed using a DFRobot’s ESP32 module integrated in a ESP32 development board.
The first thing we are going to do is including the DHTesp.h library, so we can interact with the DHT22 using a high level API rather than having to worry about the lower level details of the communication protocol between the ESP32 and this sensor.
#include "DHTesp.h"
Then, we will need an object of class DHTesp, which we will use to get the measurements from the sensor. We will do it by calling some methods of this object, as we will see below.
DHTesp dht;
In order to be able to configure the timer, we will need to declare a pointer to a variable of type hw_timer_t. This will be used later to configure the timer.
hw_timer_t * timer;
To finalize the global variable declarations, we will need a semaphore, which will be used to synchronize the main loop and the Interrupt Service Routine.
SemaphoreHandle_t syncSemaphore;
Moving on to the setup function, we will start by opening a serial connection, so we can later output the measurements obtained from the DHT22.
Serial.begin(115200);
Next, we will create the semaphore and assign it to our global variable, so it can be used to synchronize the main loop and the Interrupt Service Routine. We will create a binary semaphore with a call to the xSemaphoreCreateBinary function. This function takes no arguments and will return a SemaphoreHandle_t that we should use in the next semaphore related calls.
Note that there are other types of semaphores in FreeRTOS, such as counting semaphores, but since we are going to do some simple synchronization with just need a binary one.
Another important thing to take in consideration is always making sure to initialize the synchronization primitive before setting up the interrupts, to avoid an interrupt occurring sooner than expected and making use of an uninitialized semaphore.
syncSemaphore = xSemaphoreCreateBinary();
Before configuring the interrupts, we also need to take care of initializing the DHT22 sensor interface. We do this by calling the setup method on the DHTesp object, passing as input the number of the microcontroller pin connected to the sensor. I’m using pin 27, but you can try with others.
Note that if we don’t pass any additional argument, the library will try to detect automatically the sensor connected to the ESP32. This procedure is performed because the library supports other sensors.
If you experience problems with this automatic detection, my recommendation is to explicitly pass as second argument of this method the value DHT22, which is defined here.
dht.setup(27);
Next, we will configure the timer and bind it to an interrupt. The details about the ESP32 timers were already covered here, so please take a look at that post if you are not yet familiar with them.
So, we will initialize the timer with a call to the timerBegin method, which will return a pointer to a structure of type hw_timer_t, which we will store in our previously declared global variable.
As first input, we need to pass the number of the timer we want to use. The ESP32 has 4 hardware timers and we should specify which one to use by passing a number from 0 to 3. We will be using timer 0.
As second argument, we need to pass the prescaler value, which will allow to divide the ESP32 timers’ clock signal frequency by a factor from 2 to 65536 (the prescaler has 16 bits). Since ESP32 boards clock frequency for the timers is usually 80 MHz (the value used by the Firebeetle board), we can divide it by 80 and we will get a signal of 1 MHz.
With this value, it means that in each second the counter of the timer will be incremented 1000000 times. In other words, this means that the counter value will be incremented at each microsecond.
So, when later we are specifying the counter value that will trigger the interrupt, we will be setting the value in microseconds.
The third value to pass to the timerBegin method is a Boolean value that will simply indicate if the counter should count up or down (true and false, respectively). We are going to set it to count up.
timer = timerBegin(0, 80, true);
Note that at this point the timer is not yet enabled, since we did not configure the value that should trigger the interrupt. But before we do that, we need to bind it to an interrupt handling function.
We do this by calling the timerAttachInterrupt function. As first input, it will receive the pointer to the initialized timer we obtained in the previous call.
As second input, it will receive the address of the Interrupt Service Routine function, which we will call onTimer. We will check its implementation below.
As third argument, we need to pass a Boolean value indicating if the interrupt to be generated is edge (true) or level (false). You can check the difference about them here or here. We will pass the value true so the generated interrupt is of edge type.
timerAttachInterrupt(timer, &onTimer, true);
After this we need to set the counter value that will trigger the interrupt. We do this with a call to the timerAlarmWrite function.
As first input, we will pass the pointer to the hw_timer_t variable, which we have stored before.
As second parameter, we need to specify the counter value that will trigger the interrupt. We will be assuming that an interrupt should be triggered every 10 seconds. Since with the prescaler used we are specifying the value in microseconds, then we will pass 10000000. You can try with other values as long as you respect the DHT22 minimum sampling period.
As third and final argument, we need to pass a Boolean value indicating if the counter should reload automatically upon generating the interrupt. We will pass the value true, so it reloads and the timer keeps firing periodically.
timerAlarmWrite(timer, 10000000, true);
Finally, we enable the timer by calling the timerAlarmEnable function and passing as input the pointer to the timer.
timerAlarmEnable(timer);
The final setup function can be seen below.
void setup() { Serial.begin(115200); syncSemaphore = xSemaphoreCreateBinary(); dht.setup(27); timer = timerBegin(0, 80, true); timerAttachInterrupt(timer, &onTimer, true); timerAlarmWrite(timer, 10000000, true); timerAlarmEnable(timer); }
Moving on to the main loop, we will handle there the sensor measurements. Nonetheless, we are going to be relying on interrupts instead of using the Arduino delay function, so our approach is a little bit different from the previous tutorials about interacting with the DHT22.
So, the first thing we will do is trying to obtain the semaphore. If there is no unit available to take in the semaphore, then the main loop should block until it becomes available, thus freeing the CPU for the scheduler to assign to other tasks.
We will try to obtain the semaphore with a call to the xSemaphoreTake function, passing as first input the semaphore and as second the value portMAX_DELAY, which will ensure that task stays blocked indefinitely until the unit becomes available.
xSemaphoreTake(syncSemaphore, portMAX_DELAY);
After the execution passes this point, it means an interrupt has occurred and unblocked the main loop, which means it is time to get another temperature measurement from the DHT22. We do this by calling the getTemperature method of the dht object, which will return a float. We will print its value to the serial port.
float temperature = dht.getTemperature(); Serial.print("Temperature: "); Serial.println(temperature);
After this, the main loop will get back to the beginning and try to get the semaphore again, being locked until a new interrupt occurs. The full Arduino loop is shown below
void loop() { xSemaphoreTake(syncSemaphore, portMAX_DELAY); float temperature = dht.getTemperature(); Serial.print("Temperature: "); Serial.println(temperature); }
To finalize, we need to write Interrupt Service Routine implementation. First, we need to recall from the previous post that we need to add the IRAM_ATTR attribute to its declaration, so it is placed in IRAM by the compiler.
In terms of implementation, it will be very simple. When the interrupt happens, we only want to unblock the Arduino main loop, so it executes the code to read from the sensor. As we have seen, the main loop is blocked on the semaphore, waiting indefinitely for a unit to be added to that semaphore.
So, the only thing we need to do is calling the xSemaphoreGiveFromISR function to add a unit to the semaphore, thus unblocking the main loop to execute a new iteration.
Note the “FromISR“ at the end of the function name, which indicates it is safe to call it from inside an Interrupt Service Routine. Inside an ISR, you should not use the xSemaphoreGive function, but always the xSemaphoreGiveFromISR function.
This function receives as first input the semaphore and as second an argument that we don’t need to use, so we can set it to NULL. You can read more about that second argument here.
void IRAM_ATTR onTimer() { xSemaphoreGiveFromISR(syncSemaphore, NULL); }
The final complete code can be seen below.
#include "DHTesp.h" DHTesp dht; hw_timer_t * timer; SemaphoreHandle_t syncSemaphore; void IRAM_ATTR onTimer() { xSemaphoreGiveFromISR(syncSemaphore, NULL); } void setup() { Serial.begin(115200); syncSemaphore = xSemaphoreCreateBinary(); dht.setup(27); timer = timerBegin(0, 80, true); timerAttachInterrupt(timer, &onTimer, true); timerAlarmWrite(timer, 10000000, true); timerAlarmEnable(timer); } void loop() { xSemaphoreTake(syncSemaphore, portMAX_DELAY); float temperature = dht.getTemperature(); Serial.print("Temperature: "); Serial.println(temperature); }
To test the code, simply compile it and upload it to the ESP32 using the Arduino core, assuming that the wiring between the device and the DHT22 sensor is already done and the power is on.
Once the procedure finishes, open the Arduino IDE serial monitor. It should start printing the measurements periodically, as shown below a figure 1.
Figure 1 – Output of the program, showing the DHT22 measurements.