Interrupt-driven tilt detector
Please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.mit.edu) to authenticate, and then you will be redirected back to this page.
Overview
In lab05, we built two tilt detectors. One used the accelerometer readings to detect orientations sufficiently far away from horizontal. The second took advantage of the FXLS8974's (ok, fine, the FXLS8962) on-board processing to detect tilt directly (though of course under the hood the accelerometer is doing similar processing).
In both examples, we used polling meaning we had to repeatedly query the accelerometer hardware to see if it had changed. This means that there will be a latency between when a change occurs and when the next time we check that a change occcurs. In addition, checking all the time means we can't be sleeping.
In this exercise, we'll build an interrupt-driven tilt detector to reduce latency to the absolute minimum, and then explore using the accelerometer to wake up the ESP32C3 from sleep.
Interrupts
Interrupts are a signal to the processur to pause (aka interrupt) the current execution and execute a separate piece of code. They enable asynchronous execution of code. When the processor gets an interrupt request, it stops executing the current code, saves the relevant registers, executes the interrupt service routine (ISR) and then reloads the saved registers and continues on with the previous code.
We can have interrupts that are based on timers (interrupt every x milliseconds), GPIOs (when a GPIO input goes high, or low, or changes state), and so on. It can get tricky when multiple interrupts fire at approximately the same time, but we'll keep things simple here.
To use interrupts on the ESP32C3, we need to set up a few things.
Create a new ESP-IDF project. Copy over your final main.cpp(the one using getOrientation) from lab05 and make the following changes.
Telling the IMU to use INT1
First, we need to instruct the IMU to change the INT1 pin logic level upon an orientation change. The two library methods to do that are:
imu.setOrientInterruptPin(0); // use INT1 as the interrupt pin
imu.OrientInterruptEnable(1); // enable interrupts based on orientation changes
The first line says to use INT1 for the interrupt. You might recall that the FXLS8974 has two interrupt pins, INT1 and INT2. We've left INT2 unused on our boards, so only INT1 is connected.
The second line tells the IMU to enable interrupts, so that INT1 in this case will be driven high when the IMU wants to send an interrupt condition.
Place these where you have the other IMU setup methods (but before you turn on the readings with setMode).
So now with these two lines, if the ORIENT_STATUS register sets the NEW_ORIENT bit, the IMU raises the INT1 pin high1.
Telling the MCU how to handle interrupts
Now that IMU is sending interrupt logic signals, we need to configure the ESP32C3 to listen and react to those interrupt signals.
First, we need to tell the ESP32C3 that the pin connected to INT1 is an input, and that we want to use it for an interrupt. You can place these near where we set up the ledPin.
gpio_set_direction(accelPin, GPIO_MODE_INPUT);
gpio_set_intr_type(accelPin, GPIO_INTR_POSEDGE);
// Install GPIO ISR service
gpio_install_isr_service(0);
// Attach the interrupt handler to the GPIO pin
gpio_isr_handler_add(accelPin, orientationChange, NULL);
The first line tells the ESP32C3 that the accelPin connection is an input. This line is similar to other lines we use to set up GPIOs on the ESP32C3.
The second line is different, though, and tells the ESP32C3 that we'd like an interrupt to trigger when there is a rising edge. We can also trigger based on falling edge, or on logic level.
Then we "install" the ISR service for GPIOs, with a parameter that for advanced uses allows to, for example, set the priority level of the interrupt.
Finally, we tell the ESP32 that when the accelPin raises an interrupt, that we should execute the orientationChange function, which is our ISR.
The ISR
The third bit is our actual ISR. This is a short piece of code that gets called when the ISR fires.
Here's a simple one:
volatile bool orientChange = 0;
static void IRAM_ATTR orientationChange(void *arg) {
orientChange = 1;
gpio_set_level(ledPin, 1);
}
This ISR does only two things: it sets the global variable orientChange to 1, and turns on the LED. Which is great, because a common design pattern for an ISR is to set a flag or do some other very minimal processing and return. In general, ISRs should:
- be short and fast, to allow the CPU to get back to what it was doing as quickly as possible.
- avoid calling other functions, especially any blocking functions.
- use the
volatilekeyword for any global variables that we are changing, hereorientChange. That way the compiler will know to always read that variable fresh (rather than cached) when we want to access it, which is good because it can change suddenly.
xQueueCreate , xQueueSendFromISR, etc. We're going to keep it simple here and use global variables.Finally, notice that we declared the ISR using static void IRAM_ATTR. static restricts scope to this particular file (we don't want any other code calling this ISR), while IRAM_ATTR forces the compiler to put this ISR into the RAM of the ESP32C3 (instead of Flash), ensuring it can be accessed quickly. And quick is the name of the game here.
But here's the critical thing. Because we now turn on that LED in the ISR, and the ISR fires asynchronously, it should light up after tilt with no discernable latency.
Update your code
Now incorporate ISRs into your code. You'll be wanting to check with orientChange variable rather than orient.NewOrient, which also means that you don't need to query the IMU with imu.getOrientation(orient) until after you sense the orientChange. So no more polling, and when you get it to run, there should be no more latency for turning on the LED. Pretty neat.
To sleep, perchance to dream
A final use of our IMU that you may want to employ is to wake up a sleeping ESP32C3 if your system gets knocked over. And we can do that. We've already learned how to sleep and wakeup based on time. But we can also set the ESP32C3 to wake up based on GPIO input levels.
We have a final piece of code that does this here. Creat a final ESP-IDF project and put this main.cpp in there. It's quite similar to our other code, with a few key lines:
gpio_wakeup_enable(accelPin, GPIO_INTR_HIGH_LEVEL);
When setting up the GPIO, we also set it as a wakeup source.
Then, in our while(1) loop, we include the following:
esp_err_t err = esp_light_sleep_start();
if (esp_sleep_get_wakeup_cause()==ESP_SLEEP_WAKEUP_GPIO)
orientChange = 1;
After we start light sleep, the ESP32C3 pauses on that line. Upon wakeup, we retrieve the wakeup source, and if it's due to a GPIO, then we know it was due to an orientation change, and we set the appropriate variable to 1.
Because some people's serial monitors have issues with light sleep, we blink the LED after wakeup to signify that we did in fact wake up.
Note that we're not using interrupts here. There is no ISR. But the effect is similar. We can spend almost all our time sleeping, and when the IMU detects an orientation change, it forces the ESP32C3 to wake up, upon which time the ESP32C3 writes a few lines to the serial monitor and goes back to sleep.
Build and flash this code. You should see the LED flash when you tilt your IMU. It might even print to the serial monitor (or it might not). But now we know how to use the IMU (or any other source that can raise a GPIO) to wake up the ESP32C3.
Answer the questions below about this code.
Order the following steps:
- ESP wake-up from GPIO
- Tilt occurs
imu.getOrientationcalled- Orientation printed
- IMU sets
NewOrientflag high - IMU sets INT1 high
orientChangeset to False
After the ESP32C3 goes to sleep on line 73, which piece of code executes first after wakeup:
1 #include <stdio.h>
...
23 void app_main(void)
...
63 while(1) {
64 if (orientChange) {
...
74 if (esp_sleep_get_wakeup_cause()==ESP_SLEEP_WAKEUP_GPIO)