Let’s talk about GPIO (Part 3)

We made the buttons on the screen look so good you’ll want to lick them.

Steve Jobs

Last time we build a small demo app to blink a light on a Sparkfun ESP32 Thing+. Today, we’re going to extend that to use a button click to toggle the light instead. In order to do that, we will take our blink demo and add some things to it. Let’s also look a little more at the minimal requirements for an IDF project. Of course, the first thing is to check IDF is correctly configured in our environment:

jcrisp@embedded-dev:~/workspace/click$ idf.py --version
ESP-IDF v5.0

The minimum files you need in an IDF project are a CMakeLists.txt and a main folder with a C language file in it. CMakeLists.txt looks like

# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(click)

Well, that’s easy. The only thing you usually need to change is the project(name) entry. So next we need to define our main application. This needs three files in the main folder. First, another CMakeLists.txt which in our trivial example just defines the name of the main file

idf_component_register(SRCS "click_main.c"
                       INCLUDE_DIRS ".")

Next, we need the optional but useful project configuration file Kconfig.projbuild. This file defines the menu structure in idf.py menuconfig

menu "Example Configuration"

    orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"

    config BLINK_GPIO
        int "Blink GPIO number"
        range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
        default 13 if IDF_TARGET_ESP32
        default 8 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32H2 || IDF_TARGET_ESP32C2
        default 18 if IDF_TARGET_ESP32S2
        default 48 if IDF_TARGET_ESP32S3
        default 5
        help
            GPIO number (IOxx) to blink on and off or the RMT signal for the addressable LED.
            Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink.

    config CLICK_GPIO
        int "Click GPIO number"
        range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
        default 0
        help
            GPIO number (IOxx) to which the button is connected
            Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink.

    config CLICK_FILTER
        bool
        prompt "Click Filter (ESP32S3 only, requires IDF > v5.1)" if IDF_TARGET_ESP32S3 && IDF_VER > 5.1
        default true if IDF_TARGET_ESP32S3 && IDF_VER > 5.1
        default false
        help
           GPIO glitch filter, only available on the ESP32S3
           
    config BLINK_PERIOD
        int "Blink period in ms"
        range 10 3600000
        default 1000
        help
            Define the blinking period in milliseconds.

endmenu

In our config, we define options for the BLINK and CLICK GPIO pins. On the Sparkfun ESP32 Thing+ the button is connected to GPIO0 (GPIO ZERO). There are a couple of things to note about this pin 0. Firstly, it is one of the strap pins used during power on reset to configure the ESP32 bootstrap. Holding this pin LOW during bootstrap may boot the ESP32 into bootloader mode rather than application mode. This feature may be disabled by one of the eFuses (A future topic). The main use of this is to force bootloader mode when the application is corrupt. After bootstrap and when the application is running, this pin operates as a normal GPIO pin. It is common on ESP32 boards to see this pin connected to a momentary push button.

Now that we have told IDF what the main file is, and setup our configuration with idf.py set-target ESP32 we can define the click_main.c file:

/* Click example based on :

   Blink Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#ifdef CONFIG_CLICK_FILTER
#include "driver/gpio_filter.h"
#endif
#include "esp_log.h"
#include "sdkconfig.h"

#define TRUE 1
#define FALSE 0

static const char *TAG = "click";

static uint8_t s_led_state = 1; // We start off with the LED lit

static void blink_led(void)
{
    /* Set the GPIO level according to the state (LOW or HIGH)*/
    gpio_set_level(CONFIG_BLINK_GPIO, s_led_state);
}

static void configure_led(void)
{
    ESP_LOGI(TAG, "Example configured to blink GPIO LED! %d", CONFIG_BLINK_GPIO);
    gpio_reset_pin(CONFIG_BLINK_GPIO);
    /* Set the GPIO as a push/pull output */
    gpio_set_direction(CONFIG_BLINK_GPIO, GPIO_MODE_OUTPUT);
    gpio_set_level(CONFIG_BLINK_GPIO, 1);
}

// ISR ISR ISR ISR!
static IRAM_ATTR void button_handler(void *arg) {
    /* Toggle the LED state */
    s_led_state = !s_led_state;
    blink_led();
}

static void configure_button(void)
{
    ESP_LOGI(TAG, "Using button %d", CONFIG_CLICK_GPIO);
    gpio_reset_pin(CONFIG_CLICK_GPIO);
    gpio_config_t in = {
        .pin_bit_mask = 1 << CONFIG_CLICK_GPIO,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = TRUE,
        .pull_down_en = FALSE,
        .intr_type = GPIO_INTR_POSEDGE // POSEDGE is triggered when the button is released
    };

    ESP_ERROR_CHECK(gpio_config(&in));
#ifdef CONFIG_CLICK_FILTER
    // Enable the hardware glitch filter on the button pin
    gpio_glitch_filter_handle_t filter;
    gpio_pin_glitch_filter_config_t filter_cfg = {
        .clk_src = GLITCH_FILTER_CLK_SRC_DEFAULT,
        .gpio_num = CONFIG_CLICK_GPIO
    };
    ESP_ERROR_CHECK(gpio_new_pin_glitch_filter(&filter_cfg, &filter));
    ESP_ERROR_CHECK(gpio_glitch_filter_enable(filter));
#endif
    ESP_ERROR_CHECK(gpio_isr_handler_add(CONFIG_CLICK_GPIO, button_handler, NULL));
    ESP_ERROR_CHECK(gpio_intr_enable(CONFIG_CLICK_GPIO));
}

void app_main(void)
{
    // Enable per-GPIO pin interrupt handler trampoline
    ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_IRAM));

    /* Configure the peripheral according to the LED type */
    configure_led();
    configure_button();

    while (1) {
        vTaskDelay(CONFIG_BLINK_PERIOD / portTICK_PERIOD_MS);
    }

}

The new functions define the button configuration, and then attach an interrupt handler to the button. Note that the interrupt handler has the additional IRAM_ATTR which instructs the compiler linker to put the ISR routine into fast interrupt ram. Also note that ISRs should be _very_ fast and not call any complex functions. Usually an ISR will just set a flag or semaphore or fire an event for the non-ISR code to handle.

In our case the ISR just toggles the state of the LED pin

We use POSEDGE as our trigger since the button on the ESP32 Thing+ pulls the pin LOW when pressed, and it is usual to trigger button behavior on button release, thus causing a LOW to HIGH transition which is a POSEDGE condition.

For more recent chips like the ESP32S3 there is an extra hardware glitch filter which may be enabled on input pins. The glitch filter provides a degree of debounce, which occurs when a button connects momentarily, then disconnects, then connects again, as it is closed. This bouncecan cause multiple button press signals to fire, and is usually managed in software which watches the button state and uses a short timeout to ensure that the button has finished bouncing and is reading correctly. On the ESP32S3 and other modern ESP32, this functionality has been implemented in hardware.