ESP32 Arduino Tutorial: PIR motion sensor and interrupts
In this tutorial we will check how to interact with a PIR motion sensor using an interrupt based approach, using the Arduino core running on the ESP32.
In the previous tutorial, we covered the basics on how to interact with the PIR sensor. Nonetheless, we followed a polling approach, which involves periodically checking the state of the sensor data pin connected to the ESP32. If it is in a HIGH state, then it means some movement is currently being detected.
Nonetheless, we can rely on the transition from LOW to HIGH in the data pin of the sensor when motion is detected to trigger an interrupt on the ESP32, thus signaling the event. That way, we avoid polling and we can use those computation cycles to do something more useful in our program.
To achieve this, we will need to use some FreeRTOS primitives, more precisely semaphores. You can read here a previous post about semaphores using the Arduino core and the ESP32.
Semaphores are typically used to guarantee mutual exclusive access to resources shared amongst tasks and also for synchronization purposes.
In our case, we are going to make the main task that is executing our code (in this example, it will be the Arduino loop) block in a semaphore call. While this task is blocked, the FreeRTOS scheduler can allow other tasks to execute.
Then, when an interrupt occurs, we will basically release the semaphore so, when the interrupt finishes, the task will unblock and execute the logic associated with the detection of motion.
In this tutorial we will use a DFRobot’s PIR sensor module, which already contains all the electronics we need to connect the sensor to a microcontroller and start using it. Please check here for the connection diagram to the ESP32.
We start our code by declaring a global variable to hold the number of the GPIO of the ESP32 that will be connected to the sensor. This way, we can easily change it later if needed.
const byte interruptPin = 22;
Then, we need to declare a semaphore also as a global variable, so it can be accessed by both the interrupt service routine and the main code.
Moving on to the Arduino setup, we start by opening a serial connection to output the results of executing our program.
Next, we will create the semaphore. Since we are just doing some simple synchronization, we don’t need to create a counting semaphore and we can create a binary semaphore instead.
Note that although a binary semaphore can be seen as a mutex, there are actually some differences that you can read here. Nonetheless, for synchronization purposes like the one we are implementing, the recommended primitive to use is a binary semaphore .
So, to create a binary semaphore, we simply need to call the xSemaphoreCreateBinary function, which takes no arguments and will return a SemaphoreHandle_t value. This is an handle that we will use to reference the semaphore in the related function calls.
Note that it is important to create the semaphore before attaching the interrupt to make sure the interrupt service routine doesn’t start to use the semaphore before it is initialized.
syncSemaphore = xSemaphoreCreateBinary();
Since we are going to work with a GPIO operating as input, we need to declare its operation mode as such.
To do it, we call the pinMode function, passing as first input the number of the pin and as second the operation mode. We are going to use the INPUT_PULLUP to guarantee that the pin will be at a known state (VCC) when no signal is applied. Otherwise, if we left the pin unconnected, it could float between GND and VCC (LOW and HIGH digital levels, respectively), which would trigger multiple undesired interrupts.
Next we will attach the interrupt to the pin by calling the attachInterrupt function. As first input, we will pass the result of calling the digitalPinToInterrupt function, which is used to convert a pin number to the corresponding internal interrupt number.
As second argument, we will pass the function that will handle the interrupt. We call it handleInterrupt and we will check later how to implement it.
As third and final argument we need to specify what type of change in the digital level of the pin will trigger the interrupt. In our case, we know that motion is detected when the sensor data pin goes from LOW to HIGH, which means we want to detect a rising edge on the signal. Thus, we simply pass the constant RISING as third argument.
Moving on to the Arduino loop function, we will handle there the detection of motion. But, as we have stated before, we don’t want to be constantly polling the sensor or wasting CPU cycles verifying some variable signaled by the interrupt.
So, what we will do is trying to obtain the semaphore we previously initialized. Note that the semaphore is binary (it either has one unit to be taken or none), and it is initialized with no units.
So, when the task tries to obtain the semaphore, it will block if there is no unit available and the FreeRTOS scheduler can assign CPU time to other tasks.
To try to obtain the semaphore, we need to call the xSemaphoreTake function. This function receives as first input the semaphore and as second the number of FreeRTOS ticks to wait in case the semaphore has no unit to take.
In our case, since we want the task to block indefinitely until the semaphore is available, we use the portMAX_DELAY value. Thus, the task will stay blocked without timeout, until there is one unit available to take from the semaphore.
After that, we print a message indicating that motion was detected, since we know that the main loop will only be unblocked when motion is detected. When testing the code, we will be able to confirm that the task is indeed blocked because if it did not, the program would continuously print the message to the serial port.
To finalize, we will declare the interrupt service routine that will execute when the pin connected to the sensor goes from LOW to HIGH, which happens when motion is detected, as already mentioned.
So basically, when this happens, what we want to do is unblocking the Arduino main loop. This is done by simply adding an unit to the semaphore. We do this by calling the xSemaphoreGiveFromISR function.
Note that there is a “FromISR” at the end of the function name, indicating that this call is safe to perform from inside an interrupt service routine. If we wanted to give an unit to the semaphore from other FreeRTOS task and not from an ISR, we would use the xSemaphoreGive function instead.
The xSemaphoreGiveFromISR function receives as first input the semaphore.
As second argument, this function can receive a variable that will be set to the value pdTRUE if giving the semaphore caused a task to unblock, and the unblocked task has a priority higher than the currently running task . In our case we don’t need this, so we will simply pass NULL to this second argument.
To test the previous code, simply compile it and upload to your ESP32 after doing all the wiring needed between the sensor and the microcontroller.
Once the procedure finishes, open the Arduino IDE serial monitor. While you are not moving in front of the sensor, nothing should get printed. As soon as you move, a “Motion detected” message should get printed, as indicated in figure 1.
Figure 1 – Interrupt based motion detection with the ESP32.