Building a Button Matrix using ESP Button Component

Embedded Systems

In this tutorial, we’ll explore how to create a button matrix using the ESP IoT Solution button component. This is particularly useful in applications requiring multiple button inputs, like keypads or control panels. We’ll use the ESP-IDF framework for programming and a Wokwi Membrane Keypad for hardware interaction.

Line by line Explanation

The provided source code for the ESP-IDF button matrix is a great example of how to handle multiple button inputs in an embedded system.

Let’s break it down into two key parts: initializing the button matrix and configuring button callbacks.

Initializing the Button Matrix

The initialization of the button matrix is done in the initialize_keypad function. Here’s a step-by-step explanation:

  1. Defining GPIO Pins:

    int32_t row_gpio[4] = {14, 13, 12, 11};
    int32_t col_gpio[4] = {10, 9, 46, 3};
    • This code specifies the GPIO pins used for the keypad’s rows and columns. The ESP32 connects to the keypad through these GPIOs.
  2. Button Configuration:

    button_config_t cfg = {
        .type = BUTTON_TYPE_MATRIX,
        .long_press_time = 3000, // in ms
        .short_press_time = 500, // in ms
        ...
    };
    • Here, a button_config_t struct is initialized, setting the button type to a matrix. It also defines the time durations for long and short presses.
  3. Creating Buttons:

    for (int i = 0; i < 4; i++) {
        cfg.matrix_button_config.row_gpio_num = row_gpio[i];
        for (int j = 0; j < 4; j++) {
            cfg.matrix_button_config.col_gpio_num = col_gpio[j];
            g_btns[i * 4 + j] = iot_button_create(&cfg);
            ...
        }
    }
    • This nested loop iterates over rows and columns, creating buttons using iot_button_create and storing them in g_btns.

Configuring Button Callbacks

Callbacks are functions called in response to various button events. Each button can have multiple callbacks for different events like press down, release, etc.

  1. Registering Callbacks:

    iot_button_register_cb(g_btns[i * 4 + j], BUTTON_PRESS_DOWN, button_press_down_cb, NULL);
    • This line registers a callback for the “press down” event. Similar lines are used for other events like “press up”, “single click”, etc.
  2. Callback Functions:

    • Each callback function is defined to perform a specific action. For example, button_press_down_cb logs the button press down event.
    static void button_press_down_cb(void *arg, void *data) {
        ESP_LOGI(TAG, "BTN%s: BUTTON_PRESS_DOWN", get_btn_index((button_handle_t)arg));
    }
    • This function uses get_btn_index to determine which button was pressed and logs the event.
  3. Handling Different Press Types:

    • The code includes various callback functions for handling single clicks, double clicks, long presses, etc. Each callback function is tailored to its specific event.

Testing in Wokwi Simulator

To test the button matrix with an ESP32 in the Wokwi Simulator, follow these steps.

Connection of the Keypad and ESP32

Connect the rows and columns of the keypad to the ESP32 using the following ASCII diagram:

Keypad    ESP32
R1  ----> GPIO 14
R2  ----> GPIO 13
R3  ----> GPIO 12
R4  ----> GPIO 11
C1  ----> GPIO 10
C2  ----> GPIO 9
C3  ----> GPIO 46
C4  ----> GPIO 3

Serial Output Interpretation

When a button is pressed, the ESP32 will output different statuses to the serial monitor, indicating the type of press:

  • BUTTON_LONG_PRESS_HOLD is printed repeatedly as long as a button is held down.
  • BUTTON_PRESS_UP appears when the button is released.
  • BUTTON_PRESS_DOWN is shown when a button is initially pressed.
  • For single and double clicks, BUTTON_SINGLE_CLICK and BUTTON_DOUBLE_CLICK messages are displayed.
  • BUTTON_PRESS_REPEAT_DONE is printed after a single or double click is recognized.

Example output in the serial console:

(88295) BUTTON TEST: BTN6: BUTTON_LONG_PRESS_HOLD
(88315) BUTTON TEST: BTN6: BUTTON_LONG_PRESS_HOLD
(88335) BUTTON TEST: BTN6: BUTTON_LONG_PRESS_HOLD
(88355) BUTTON TEST: BTN6: BUTTON_PRESS_UP
(89685) BUTTON TEST: BTN9: BUTTON_PRESS_DOWN
(90765) BUTTON TEST: BTN9: BUTTON_PRESS_UP
(91275) BUTTON TEST: BTN9: BUTTON_SINGLE_CLICK
(91275) BUTTON TEST: BTN9: BUTTON_PRESS_REPEAT_DONE
(95185) BUTTON TEST: BTN8: BUTTON_PRESS_DOWN
(97665) BUTTON TEST: BTN8: BUTTON_PRESS_UP
(98175) BUTTON TEST: BTN8: BUTTON_SINGLE_CLICK
(98175) BUTTON TEST: BTN8: BUTTON_PRESS_REPEAT_DONE
(100815) BUTTON TEST: BTN1: BUTTON_PRESS_DOWN
(101935) BUTTON TEST: BTN1: BUTTON_PRESS_UP
(102435) BUTTON TEST: BTN1: BUTTON_SINGLE_CLICK
(102435) BUTTON TEST: BTN1: BUTTON_PRESS_REPEAT_DONE

This setup allows you to test various button interactions and observe their corresponding outputs in the Wokwi Simulator.

Full source code

The following is the complete source code of driving a matrix keypad using the button component.

#include "esp_log.h"
#include "iot_button.h"

static const char *TAG = "BUTTON TEST";

#define BUTTON_NUM 16
static button_handle_t g_btns[BUTTON_NUM] = {0};

#define NUMBER_OF_ROWS 4
#define NUMBER_OF_COLUMNS 4

#define LONG_PRESS_DURATION_MS 1500
#define SHORT_PRESS_DURATION_MS 500


const char* keys[NUMBER_OF_ROWS][NUMBER_OF_COLUMNS] = {
    {"1", "2", "3", "A"},
    {"4", "5", "6", "B"},
    {"7", "8", "9", "C"},
    {"*", "0", "#", "D"}
};

static const char* get_btn_index(button_handle_t btn)
{
    for (size_t i = 0; i < BUTTON_NUM; i++) {
        if (btn == g_btns[i]) {
            // Calculate row and column from the linear index
            size_t row = i / NUMBER_OF_ROWS; // Integer division to get the row
            size_t col = i % NUMBER_OF_COLUMNS; // Modulo operation to get the column
            return keys[row][col];
        }
    }
    return NULL; // Return NULL if the button is not found
}

static void button_press_down_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_PRESS_DOWN", get_btn_index((button_handle_t)arg));
}

static void button_press_up_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_PRESS_UP[%d]", get_btn_index((button_handle_t)arg), iot_button_get_ticks_time((button_handle_t)arg));
}

static void button_press_repeat_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_PRESS_REPEAT[%d]", get_btn_index((button_handle_t)arg), iot_button_get_repeat((button_handle_t)arg));
}

static void button_single_click_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_SINGLE_CLICK", get_btn_index((button_handle_t)arg));
}

static void button_double_click_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_DOUBLE_CLICK", get_btn_index((button_handle_t)arg));
}

static void button_long_press_start_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_LONG_PRESS_START", get_btn_index((button_handle_t)arg));
}

static void button_long_press_hold_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_LONG_PRESS_HOLD[%d],count is [%d]", get_btn_index((button_handle_t)arg), iot_button_get_ticks_time((button_handle_t)arg), iot_button_get_long_press_hold_cnt((button_handle_t)arg));
}

static void button_press_repeat_done_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "BTN%s: BUTTON_PRESS_REPEAT_DONE[%d]", get_btn_index((button_handle_t)arg), iot_button_get_repeat((button_handle_t)arg));
}

void initialize_keypad() {
    // Define the GPIO pins for the keypad rows and columns
    int32_t row_gpio[NUMBER_OF_ROWS] = {14, 13, 12, 11}; // Example GPIOs for rows
    int32_t col_gpio[NUMBER_OF_COLUMNS] = {10, 9, 46, 3}; // Example GPIOs for columns

    button_config_t cfg = {
        .type = BUTTON_TYPE_MATRIX,
        .long_press_time = LONG_PRESS_DURATION_MS,
        .short_press_time = SHORT_PRESS_DURATION_MS,
        .matrix_button_config = {
            .row_gpio_num = 0,
            .col_gpio_num = 0,
        }
    };

    for (int i = 0; i < NUMBER_OF_ROWS; i++) {
        cfg.matrix_button_config.row_gpio_num = row_gpio[i];
        for (int j = 0; j < NUMBER_OF_COLUMNS; j++) {
            cfg.matrix_button_config.col_gpio_num = col_gpio[j];
            g_btns[i * NUMBER_OF_ROWS + j] = iot_button_create(&cfg);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_PRESS_DOWN, button_press_down_cb, NULL);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_PRESS_UP, button_press_up_cb, NULL);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_PRESS_REPEAT, button_press_repeat_cb, NULL);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_SINGLE_CLICK, button_single_click_cb, NULL);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_DOUBLE_CLICK, button_double_click_cb, NULL);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_LONG_PRESS_START, button_long_press_start_cb, NULL);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_LONG_PRESS_HOLD, button_long_press_hold_cb, NULL);
            iot_button_register_cb(g_btns[i * NUMBER_OF_ROWS + j], BUTTON_PRESS_REPEAT_DONE, button_press_repeat_done_cb, NULL);
        }
    }
}

void app_main(void) {
    initialize_keypad();
}