ESP32 GPIO in C++ Part 3

ESP32 GPIO Custom Event and Queue in C++

Add options for custom event loops or queue messaging to the ESP32 input interrupts created in ESP32 GPIO in C++ Part 2

This is part 3 of a 3 part tutorial

ESP32 GPIO in C++ Part 1
ESP32 GPIO in C++ Part 2
ESP32 GPIO in C++ Part 3

In part 2 of this series, we separated the GpioInput and GpioOutput class implementations into separate C++ files to improve the maintainability of the code.

Then we implemented more options to the Input pins and then we added a basic interrupt handler in which we pushed an event to the default system event loop when something happens on the input, a button pushed in this example.

In this tutorial, we are going to expand on interrupt handling. While it is fine to use the default system loop for basic programs, there are instances where we would want to use a separate user defines event loop or send the interrupt data to a queue.

We are going to provide options to use the default event loop, a custom event loop, or to notify a new task using a queue.

When to use Events or Queues

Use events for signaling when the chip has reached a set state, for example, when a button has been pushed or a sensor measurement has been completed.

Queues are better used to copy data between tasks. For example, if you have a task that takes temperature measurements and another task that displays the temperature on a display, then a queue should be used to copy the data from the task that takes the measurement value to the task that displays the data.

To bring the above example together; When the temperature sensor has completed taking the measurement, then an interrupt will trigger an event signaling that the temperature measurement is complete. The event will then send the temperature data to the display task using queues.

Create a struct to hold interrupt arguments

We are unable to use variables directly in the Interrupt Service Routine because the ISR is a static method. However, we need to pass data to the ISR to tell it what to do.

Fortunately, we can pass a void * type to the ISR whenever an interrupt is triggered. The void * is special in that it can hold any data type.

Handy as that is, we can still only send one variable. That is why we are going to pass a struct that can hold multiple variables.

We are going to remove the following variable from the GpioBase class and add it as a private variable to the GpioOutput class in cppgpio.h:

gpio_num_t _pin;

This will be the only change made to GpioOutput. No further changes need to be done.

Create the Struct

We need to move some existing private variables into a struct for the ISR to have access to the data.

Add the following code to the private section of the GpioInput class in cppgpio.h:

        struct interrupt_args
        {
            bool _event_handler_set = false;
            bool _custom_event_handler_set = false;
            bool _queue_enabled = false;
            gpio_num_t _pin;
            esp_event_loop_handle_t _custom_event_loop_handle {nullptr};
            xQueueHandle _queue_handle {nullptr};
        } _interrupt_args;

This will create a variable with the name _interrupt_args as type interrupt_args and will hold 6 variables.

  • _event_handler_set A boolean that indicated if any event loop has been set. This will be used in the ISR to check whether or not to post an event to the system loop
  • _custom_event_handler_set A boolean flag to indicate whether or not a user-created event loop is being used
  • _queue_enabled A boolean flag to indicate if a queue will be used
  • _pin The variable indicating the input pin on this instance.
  • _custom_event_loop_handle The handle for the custom even loop if used. Initialized to a nullptr.
  • _queue_handle The handle to the queue if a queue is used. Initialized to a nullptr.

I will explain more about the individual variable as we implement their functions.

Some of these variables have already been declared. Go ahead and remove the duplicates outside of the struct.

Modify the implementations to use the structs.

All of the methods that used any of the variables that have been moved into the struct will now generate an error if we attempt to build the project. We need to change all of those variables to point to the struct.

Fortunately, VS Code provides tools to do this easily. Go to cppgpio_input.cpp and select any instance of _pin. Highlight the entire text, right-click and select change all occurrences, or press Ctrl+F2, and then type in _interrupt_args._pin. Do the same for all the variables that were moved into the struct.

Custom event loop and queue

We are now going to implement the custom event and queue, however, we must always try to keep in mind what could go wrong. For instance, by granting options as to how to handle interrupts we must think about what might happen if we set up the pin to use the event and then later to use the queue or a custom loop.

To allow for such instances, we are going to design our GPIO library to only use the most recent method that was set.

Update the Header

Open up gpiocpp.h for the following sections.

Create variables to track the state.

Create the following private variables in the GpioInput class:

bool _event_handler_set = false;
esp_event_handler_t _event_handle = nullptr;
static portMUX_TYPE _eventChangeMutex;
  • _event_handler_set: A boolean flag the tracks whether or not a default event handler has been set.
  • _event_handle: Stores the handle used for a custom even loop. Used to unregister the input from the custom even loop.
  • _eventChangeMutex: Mutex to lock out interrupts when assigning events loops or queues.
Create method prototypes

Create the private method prototype:

esp_err_t _clearEventHandlers();

This method will unregister any events registered for the specific input.

Create the following public method prototypes:

esp_err_t setEventHandler(esp_event_loop_handle_t Gpio_e_l, esp_event_handler_t Gpio_e_h);

This is an override of the method used to set up the use of the default system event loop. By adding an event loop handle we can set up the use of a custom user event loop without having to create a new method.

void setQueueHandle(xQueueHandle Gpio_e_q);

This function accepts a handle to a queue. This handle will be used to tell the interrupt where to post the message from the interrupt to.

Update the source file

Initialize static variables

Add the following line of code in cppgpio_input.cpp where the other static variable has been initialized:

portMUX_TYPE GpioInput::_eventChangeMutex = portMUX_INITIALIZER_UNLOCKED;

This initializes a mutex variable into the unlock state.

Update method that sets the default event handler

Update the method created in part 2 of the GPIO tutorial series to look like the following:

    esp_err_t GpioInput::setEventHandler(esp_event_handler_t Gpio_e_h)
    {
        esp_err_t status{ESP_OK};

        taskENTER_CRITICAL(&_eventChangeMutex);

        status = _clearEventHandlers();

        status = esp_event_handler_instance_register(INPUT_EVENTS, _interrupt_args._pin, Gpio_e_h, 0, nullptr);

        if (ESP_OK == status)
        {
            _interrupt_args._event_handler_set = true;
        }

        taskEXIT_CRITICAL(&_eventChangeMutex);

        return status;
    }

Lets us take a look at the code that was added:

taskENTER_CRITICAL(&_eventChangeMutex);

Before we do anything, we disable all the interrupts up to the system level. This prevents any interrupt from triggering while we are setting up how interrupts should be handled.

status = _clearEventHandlers();

This method unregisters the GPIO input pin from any event handlers that it may be registered to. The method will be discussed in more detail when we implement it.

Implement the method that sets the user event handler

We are now going to implement the second version of the setEventHandler method which will register the input pin to a user-created event loop.

Below is the code to implement the method, then we will look at how it works:

    esp_err_t GpioInput::setEventHandler(esp_event_loop_handle_t Gpio_e_l, esp_event_handler_t Gpio_e_h)
    {
        esp_err_t status{ESP_OK};

        taskENTER_CRITICAL(&_eventChangeMutex);

        status = _clearEventHandlers();

        status |= esp_event_handler_instance_register_with(Gpio_e_l, INPUT_EVENTS, _interrupt_args._pin, Gpio_e_h, 0, nullptr);

        if (ESP_OK == status)
        {
            _event_handle = Gpio_e_h;
            _interrupt_args._custom_event_loop_handle = Gpio_e_l;
            _interrupt_args._custom_event_handler_set = true;
        }

        taskEXIT_CRITICAL(&_eventChangeMutex);

        return status;
    }

The code is very similar to the other version of the setEventHandler method.

Let us look at only the differences:

esp_err_t GpioInput::setEventHandler(esp_event_loop_handle_t Gpio_e_l, esp_event_handler_t Gpio_e_h)

We have an extra argument, esp_event_loop_handle_t Gpio_e_l, which allows us to accept a handle to a user-created event loop.

esp_event_handler_instance_register_with(Gpio_e_l, INPUT_EVENTS, _interrupt_args._pin, Gpio_e_h, 0, nullptr);

In this method, we use the esp_event_handler_instance_register_with function with accepts a handle for a user-created event loop to register the input pin to an event loop.

_event_handle = Gpio_e_h;

We save the event handle passed into this method into a private variable. We will need this to unregister the input pin from the user event loop.

_interrupt_args._custom_event_loop_handle = Gpio_e_l;

We save the event loop handle passed into the method into a variable in the struct that we created earlier. We need this handle to unregister the input from the user event loop. We also need to pass this handle as an argument to the ISR to be able to post the interrupt to the correct event loop.

Setup the interrupt to use a queue

Below is the code to implement the method that will tell the ISR to send interrupts to a queue:

    void GpioInput::setQueueHandle(xQueueHandle Gpio_e_q)
    {
        taskENTER_CRITICAL(&_eventChangeMutex);
        _clearEventHandlers();
        _interrupt_args._queue_handle = Gpio_e_q;
        _interrupt_args._queue_enabled = true;
        taskEXIT_CRITICAL(&_eventChangeMutex);
    }

Let us look at how this method works:

_interrupt_args._queue_handle = Gpio_e_q;

We save the queue handle into the struct that will be sent to the ISR when an interrupt is triggered.

_interrupt_args._queue_enabled = true;

We set a flag that tells the ISR to send the interrupt to a queue.

Implement a method to reset queues and event handlers

We need a way to prevent a case in which multiple ways are defined to handle interrupts. This method will reset the class to a state where no event handlers or queues have been set. If we then call this function any time an event handler or queue is set, then only that setting will be valid irrespective of previous setups.

Add the following code to implement the method that will unregister the input pin from any event handler and remove it from any queues:

    esp_err_t GpioInput::_clearEventHandlers()
    {
        esp_err_t status {ESP_OK};

        if(_interrupt_args._custom_event_handler_set)
        {
            esp_event_handler_unregister_with(_interrupt_args._custom_event_loop_handle, INPUT_EVENTS, _interrupt_args._pin, _event_handle);
            _interrupt_args._custom_event_handler_set = false;
        }
        else if (_event_handler_set)
        {
            esp_event_handler_instance_unregister(INPUT_EVENTS, _interrupt_args._pin, nullptr);
            _event_handler_set = false;
        }

        _interrupt_args._queue_handle = nullptr;
        _interrupt_args._queue_enabled = false;

        return status;
    }

Let us unpack the method:

        if(_interrupt_args._custom_event_handler_set)
        {
            esp_event_handler_unregister_with(_interrupt_args._custom_event_loop_handle, INPUT_EVENTS, _interrupt_args._pin, _event_handle);
            _interrupt_args._custom_event_handler_set = false;
        }

If the custom event handler flag has been set then we call the method to unregister the input pin from the user event loop and then we clear the flag.

        else if (_interrupt_args._event_handler_set)
        {
            esp_event_handler_instance_unregister(INPUT_EVENTS, _interrupt_args._pin, nullptr);
            _interrupt_args._event_handler_set = false;
        }

If the input pin is registered to the default system event loop then we unregister the pin and clear the flag.

_interrupt_args._queue_handle = nullptr;
_interrupt_args._queue_enabled = false;

Here we clear the flag that tells the ISR to notify a queue when an interrupt was triggered and set the queue handle to a nullptr.

Update the Interrupt Service Routine

Replace the ISR (Interrupt Service Routine) with the following block of code:

void IRAM_ATTR GpioInput::gpio_isr_callback(void *args)
    {
        int32_t pin = (reinterpret_cast<struct interrupt_args *>(args))->_pin;
        bool custom_event_handler_set = (reinterpret_cast<struct interrupt_args *>(args))->_custom_event_handler_set;
        bool event_handler_set = (reinterpret_cast<struct interrupt_args *>(args))->_event_handler_set;
        bool queue_enabled = (reinterpret_cast<struct interrupt_args *>(args))->_queue_enabled;
        esp_event_loop_handle_t custom_event_loop_handle = (reinterpret_cast<struct interrupt_args *>(args))->_custom_event_loop_handle;
        xQueueHandle queue_handle = (reinterpret_cast<struct interrupt_args *>(args))->_queue_handle;

        if (queue_enabled)
        {
            xQueueSendFromISR(queue_handle, &pin, NULL);
        }
        else if (custom_event_handler_set)
        {
            esp_event_isr_post_to(custom_event_loop_handle, INPUT_EVENTS, pin, nullptr, 0, nullptr);
        }
        else if (event_handler_set)
        {
            esp_event_isr_post(INPUT_EVENTS, pin, nullptr, 0, nullptr);
        }
    }

Let us unpack what this code will do.

        int32_t pin = (reinterpret_cast<struct interrupt_args *>(args))->_pin;
        bool custom_event_handler_set = (reinterpret_cast<struct interrupt_args *>(args))->_custom_event_handler_set;
        bool event_handler_set = (reinterpret_cast<struct interrupt_args *>(args))->_event_handler_set;
        bool queue_enabled = (reinterpret_cast<struct interrupt_args *>(args))->_queue_enabled;
        esp_event_loop_handle_t custom_event_loop_handle = (reinterpret_cast<struct interrupt_args *>(args))->_custom_event_loop_handle;
        xQueueHandle queue_handle = (reinterpret_cast<struct interrupt_args *>(args))->_queue_handle;

Here we extract all of the variables in the struct that we created earlier. We need to do this because we cannot access non-static variables in a static method.

Click here for more information on C++ Casting Operators

Note that it is not necessary to assign all of the variables to new local variables, I do it to improve readability which will make it significantly easier to read and maintain the code later on.

         if (queue_enabled)
        {
            xQueueSendFromISR(queue_handle, &pin, NULL);
        }
        else if (custom_event_handler_set)
        {
            esp_event_isr_post_to(custom_event_loop_handle, INPUT_EVENTS, pin, nullptr, 0, nullptr);
        }
        else if (event_handler_set)
        {
            esp_event_isr_post(INPUT_EVENTS, pin, nullptr, 0, nullptr);
        }

This block of code decides where the interrupt will be sent to. It is important to keep ISRs as short as possible.

The ISR checks which flag is set:

  • If the queue enables flag is set then the input pin number is sent to the queue associated with the queue handle.
  • If the custom event handle is set then the ISR post an event to the event loop associated with the custom event loop handle
  • If the event handle is set then the ISR post an even to the default system evel loop.

Complete header and source file

Complete Header

#pragma once

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_event.h"
#include "driver/gpio.h"

namespace CPPGPIO
{
    ESP_EVENT_DECLARE_BASE(INPUT_EVENTS);

    class GpioBase
    {
    protected:
        bool _active_low = false;
    }; // GpioBase Class

    class GpioInput : public GpioBase
    {
    private:
        esp_err_t _init(const gpio_num_t pin, const bool activeLow);
        static bool _interrupt_service_installed;

        esp_event_handler_t _event_handle = nullptr;
        static portMUX_TYPE _eventChangeMutex;

        esp_err_t _clearEventHandlers();

        struct interrupt_args
        {
            bool _event_handler_set = false;
            bool _custom_event_handler_set = false;
            bool _queue_enabled = false;
            gpio_num_t _pin;
            esp_event_loop_handle_t _custom_event_loop_handle {nullptr};
            xQueueHandle _queue_handle {nullptr};
        } _interrupt_args;

    public:
        GpioInput(const gpio_num_t pin, const bool activeLow);
        GpioInput(const gpio_num_t pin);
        GpioInput(void);
        esp_err_t init(const gpio_num_t pin, const bool activeLow);
        esp_err_t init(const gpio_num_t pin);
        int read(void);

        esp_err_t enablePullup(void);
        esp_err_t disablePullup(void);
        esp_err_t enablePulldown(void);
        esp_err_t disablePulldown(void);
        esp_err_t enablePullupPulldown(void);
        esp_err_t disablePullupPulldown(void);
        
        esp_err_t enableInterrupt(gpio_int_type_t int_type);
        esp_err_t setEventHandler(esp_event_handler_t Gpio_e_h);
        esp_err_t setEventHandler(esp_event_loop_handle_t Gpio_e_l, esp_event_handler_t Gpio_e_h);
        void setQueueHandle(xQueueHandle Gpio_e_q);
        static void IRAM_ATTR gpio_isr_callback(void* arg);
    }; // GpioInput Class

    class GpioOutput : public GpioBase
    {
    private:
        gpio_num_t _pin;
        int _level = 0;
        esp_err_t _init(const gpio_num_t pin, const bool activeLow);

    public:
        GpioOutput(const gpio_num_t pin, const bool activeLow);
        GpioOutput(const gpio_num_t pin);
        GpioOutput(void);
        esp_err_t init(const gpio_num_t pin, const bool activeLow);
        esp_err_t init(const gpio_num_t pin);
        esp_err_t on(void);
        esp_err_t off(void);
        esp_err_t toggle(void);
        esp_err_t setLevel(int level);
    }; // GpioOutput Class
} // Gpio Namespace

Complete source file

#include "cppgpio.h"

namespace CPPGPIO
{
    // Static variable initializations
    bool GpioInput::_interrupt_service_installed{false};
    portMUX_TYPE GpioInput::_eventChangeMutex = portMUX_INITIALIZER_UNLOCKED;

    ESP_EVENT_DEFINE_BASE(INPUT_EVENTS);

    void IRAM_ATTR GpioInput::gpio_isr_callback(void *args)
    {
        int32_t pin = (reinterpret_cast<struct interrupt_args *>(args))->_pin;
        bool custom_event_handler_set = (reinterpret_cast<struct interrupt_args *>(args))->_custom_event_handler_set;
        bool event_handler_set = (reinterpret_cast<struct interrupt_args *>(args))->_event_handler_set;
        bool queue_enabled = (reinterpret_cast<struct interrupt_args *>(args))->_queue_enabled;
        esp_event_loop_handle_t custom_event_loop_handle = (reinterpret_cast<struct interrupt_args *>(args))->_custom_event_loop_handle;
        xQueueHandle queue_handle = (reinterpret_cast<struct interrupt_args *>(args))->_queue_handle;

        if (queue_enabled)
        {
            xQueueSendFromISR(queue_handle, &pin, NULL);
        }
        else if (custom_event_handler_set)
        {
            esp_event_isr_post_to(custom_event_loop_handle, INPUT_EVENTS, pin, nullptr, 0, nullptr);
        }
        else if (event_handler_set)
        {
            esp_event_isr_post(INPUT_EVENTS, pin, nullptr, 0, nullptr);
        }
    }

    esp_err_t GpioInput::_init(const gpio_num_t pin, const bool activeLow)
    {
        esp_err_t status{ESP_OK};

        _active_low = activeLow;
        _interrupt_args._pin = pin;

        gpio_config_t cfg;
        cfg.pin_bit_mask = 1ULL << pin;
        cfg.mode = GPIO_MODE_INPUT;
        cfg.pull_up_en = GPIO_PULLUP_DISABLE;
        cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
        cfg.intr_type = GPIO_INTR_DISABLE;

        status |= gpio_config(&cfg);

        return status;
    }

    GpioInput::GpioInput(const gpio_num_t pin, const bool activeLow)
    {
        _init(pin, activeLow);
    }

    GpioInput::GpioInput(const gpio_num_t pin)
    {
        _init(pin, false);
    }

    GpioInput::GpioInput(void)
    {
    }

    esp_err_t GpioInput::init(const gpio_num_t pin, const bool activeLow)
    {
        return _init(pin, activeLow);
    }

    esp_err_t GpioInput::init(const gpio_num_t pin)
    {
        return _init(pin, false);
    }

    int GpioInput::read(void)
    {
        return _active_low ? !gpio_get_level(_interrupt_args._pin) : gpio_get_level(_interrupt_args._pin);
    }

    esp_err_t GpioInput::enablePullup(void)
    {
        return gpio_set_pull_mode(_interrupt_args._pin, GPIO_PULLUP_ONLY);
    }

    esp_err_t GpioInput::disablePullup(void)
    {
        return gpio_set_pull_mode(_interrupt_args._pin, GPIO_FLOATING);
    }

    esp_err_t GpioInput::enablePulldown(void)
    {
        return gpio_set_pull_mode(_interrupt_args._pin, GPIO_PULLDOWN_ONLY);
    }

    esp_err_t GpioInput::disablePulldown(void)
    {
        return gpio_set_pull_mode(_interrupt_args._pin, GPIO_FLOATING);
    }

    esp_err_t GpioInput::enablePullupPulldown(void)
    {
        return gpio_set_pull_mode(_interrupt_args._pin, GPIO_PULLUP_PULLDOWN);
    }

    esp_err_t GpioInput::disablePullupPulldown(void)
    {
        return gpio_set_pull_mode(_interrupt_args._pin, GPIO_FLOATING);
    }

    esp_err_t GpioInput::enableInterrupt(gpio_int_type_t int_type)
    {
        esp_err_t status{ESP_OK};

        // Invert triggers if active low is enabled
        if (_active_low)
        {
            switch (int_type)
            {
            case GPIO_INTR_POSEDGE:
                int_type = GPIO_INTR_NEGEDGE;
                break;
            case GPIO_INTR_NEGEDGE:
                int_type = GPIO_INTR_POSEDGE;
                break;
            case GPIO_INTR_LOW_LEVEL:
                int_type = GPIO_INTR_HIGH_LEVEL;
                break;
            case GPIO_INTR_HIGH_LEVEL:
                int_type = GPIO_INTR_LOW_LEVEL;
                break;
            default:
                break;
            }
        }

        if (!_interrupt_service_installed)
        {
            status = gpio_install_isr_service(0);
            if (ESP_OK == status)
            {
                _interrupt_service_installed = true;
            }            
        }

        if (ESP_OK == status)
        {
            status = gpio_set_intr_type(_interrupt_args._pin, int_type);
        }

        if (ESP_OK == status)
        {
            status = gpio_isr_handler_add(_interrupt_args._pin, gpio_isr_callback, (void*) &_interrupt_args);
        }
        return status;
    }

    esp_err_t GpioInput::setEventHandler(esp_event_handler_t Gpio_e_h)
    {
        esp_err_t status{ESP_OK};

        taskENTER_CRITICAL(&_eventChangeMutex);

        status = _clearEventHandlers();

        status = esp_event_handler_instance_register(INPUT_EVENTS, _interrupt_args._pin, Gpio_e_h, 0, nullptr);

        if (ESP_OK == status)
        {
            _interrupt_args._event_handler_set = true;
        }

        taskEXIT_CRITICAL(&_eventChangeMutex);

        return status;
    }

    esp_err_t GpioInput::setEventHandler(esp_event_loop_handle_t Gpio_e_l, esp_event_handler_t Gpio_e_h)
    {
        esp_err_t status{ESP_OK};

        taskENTER_CRITICAL(&_eventChangeMutex);

        status = _clearEventHandlers();

        status |= esp_event_handler_instance_register_with(Gpio_e_l, INPUT_EVENTS, _interrupt_args._pin, Gpio_e_h, 0, nullptr);

        if (ESP_OK == status)
        {
            _event_handle = Gpio_e_h;
            _interrupt_args._custom_event_loop_handle = Gpio_e_l;
            _interrupt_args._custom_event_handler_set = true;
        }

        taskEXIT_CRITICAL(&_eventChangeMutex);

        return status;
    }

    void GpioInput::setQueueHandle(xQueueHandle Gpio_e_q)
    {
        taskENTER_CRITICAL(&_eventChangeMutex);
        _clearEventHandlers();
        _interrupt_args._queue_handle = Gpio_e_q;
        _interrupt_args._queue_enabled = true;
        taskEXIT_CRITICAL(&_eventChangeMutex);
    }

    esp_err_t GpioInput::_clearEventHandlers()
    {
        esp_err_t status {ESP_OK};

        if(_interrupt_args._custom_event_handler_set)
        {
            esp_event_handler_unregister_with(_interrupt_args._custom_event_loop_handle, INPUT_EVENTS, _interrupt_args._pin, _event_handle);
            _interrupt_args._custom_event_handler_set = false;
        }
        else if (_interrupt_args._event_handler_set)
        {
            esp_event_handler_instance_unregister(INPUT_EVENTS, _interrupt_args._pin, nullptr);
            _interrupt_args._event_handler_set = false;
        }

        _interrupt_args._queue_handle = nullptr;
        _interrupt_args._queue_enabled = false;

        return status;
    }
} // Namespace CPPGPIO

Create a user loop and queue in main.cpp

Main Header

Modify main.h to look like the following:

#pragma once

#include <iostream>

#include "cppgpio.h"

// Main class used for testing only
class Main final
{
public:
    void run(void);
    void setup(void);

    esp_event_loop_handle_t gpio_loop_handle {};

    // LED pin on my NodeMCU
    CPPGPIO::GpioOutput cppLed { GPIO_NUM_2, true };
    // Repurpose the BOOT button to work as an input
    CPPGPIO::GpioInput cppButton { GPIO_NUM_0 };
    // A second input to demonstrate Event_ID different event handlers
    CPPGPIO::GpioInput cppButton2 { GPIO_NUM_12 };

    // Event Handler for cppButton
    static void  button_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data);
    // Event Handler for cppButton2
    static void  button2_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data);
    // Event Handler for custom loop
    static void task_custom_event(void* handler_args, esp_event_base_t base, int32_t id, void* event_data);
    // Handle for the queue
    static xQueueHandle button_evt_queue;
    // Prototype for the task
    static void gpio_task_example(void *arg);

}; // Main Class

The following are the only new additions to main.h

static void task_custom_event(void* handler_args, esp_event_base_t base, int32_t id, void* event_data);

Prototype for our event handler that will be invoked when an event is sent to our user event loop.

static xQueueHandle button_evt_queue;

The handle for the queue that we are going to create.

static void gpio_task_example(void *arg);

A prototype for a task that we are going to create. The queue is going to run in the task.

Main Source

Modify main.cpp to look like the following:

#include "main.h"

Main App;

xQueueHandle Main::button_evt_queue {nullptr};

void Main::run(void)
{
    vTaskDelay(pdMS_TO_TICKS(200));
    cppLed.setLevel(cppButton.read());
}

void Main::setup(void)
{
    button_evt_queue = xQueueCreate(10, sizeof(uint32_t));
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

    esp_event_loop_args_t gpio_loop_args;
    gpio_loop_args.queue_size = 5;
    gpio_loop_args.task_name = "loop_task"; // Task will be created
    gpio_loop_args.task_priority = uxTaskPriorityGet(NULL);
    gpio_loop_args.task_stack_size = 2048;
    gpio_loop_args.task_core_id = tskNO_AFFINITY;

    esp_event_loop_create(&gpio_loop_args, &gpio_loop_handle); // Create Custom Event Loop
    esp_event_loop_create_default();    // Create System Event Loop

    cppButton.enableInterrupt(GPIO_INTR_NEGEDGE);
    cppButton.setQueueHandle(button_evt_queue);

    cppButton2.enablePullup();
    cppButton2.enableInterrupt(GPIO_INTR_NEGEDGE);
    cppButton2.setEventHandler(gpio_loop_handle, &task_custom_event);
}

extern "C" void app_main(void)
{
    App.setup();

    while (true)
    {
        App.run();
    }    
}

void  Main::button_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data)
{
    std::cout << "Button triggered interrupt with ID: " << id << '\n';
}

void  Main::button2_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data)
{
    std::cout << "Button triggered interrupt with ID: " << id << '\n';
}

void Main::task_custom_event(void* handler_args, esp_event_base_t base, int32_t id, void* event_data)
{
    std::cout << "Button triggered interrupt with ID: " << id << " With Custom Loop\n";
}

void Main::gpio_task_example(void *arg)
{
   uint32_t io_num;
    for (;;)
    {
        if (xQueueReceive(Main::button_evt_queue, &io_num, portMAX_DELAY))
        {
            std::cout << "Interrupt triggered from pin " << (int)io_num << " and send to queue\n";
        }
    }
}

Short Explanation

xQueueHandle Main::button_evt_queue {nullptr};

The xQueueHandle has to be initialized to a nullptr (or NULL in c). If this is not done then the linker will fail.

 button_evt_queue = xQueueCreate(10, sizeof(uint32_t));

Create an event queue

xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

Create a task and name it gpio_task_example. The name is the same as the prototype that we defined in the header earlier static void gpio_task_example(void *arg);

This task will wait for something to be sent to a queue.

esp_event_loop_args_t gpio_loop_args;
    gpio_loop_args.queue_size = 5;
    gpio_loop_args.task_name = "loop_task"; // Task will be created
    gpio_loop_args.task_priority = uxTaskPriorityGet(NULL);
    gpio_loop_args.task_stack_size = 2048;
    gpio_loop_args.task_core_id = tskNO_AFFINITY;

    esp_event_loop_create(&gpio_loop_args, &gpio_loop_handle); // Create Custom Event Loop

Create a variable that holds the settings for the user event loop and then create the user event loop using the settings.

Creating this event loop will create a task named loop_task. You can give it any appropriate name.

void Main::gpio_task_example(void *arg)
{
   uint32_t io_num;
    for (;;)
    {
        if (xQueueReceive(Main::button_evt_queue, &io_num, portMAX_DELAY))
        {
            std::cout << "Interrupt triggered from pin " << (int)io_num << " and send to queue\n";
        }
    }
}

An endless loop waiting for something to be sent to the queue defined by the queue handle button_evt_queue

Conclusion

We now have a fully functional GPIO library capable of doing just about all of the general GPIO functions including:

  • Output
  • Input
  • Interrupt
    • Queue
    • Default event loop
    • User created event loop

This concludes the GPIO library. The project will be renamed one more time when we create PWM and analog classes in the same project. We will then have a more complete library to easily use the IO of the ESP32.

To download this project from Github, click here.

Thanks for reading.

Similar Posts