ESP-IDFを使ってみる

高速GPIO


こちら
にESP32の低レベルGPIOアクセスのやり方が紹介されています。
GPIO_OUT_W1TS_REG と GPIO_OUT_W1TC_REG を直接操作することで、GPIO0からGPIO31をON/OFFできます。
そこで、レジスター操作で、どの程度GPIOのON/OFFが早くなるのか、以下のコードで試してみました。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "hal/gpio_ll.h" // ESP-IDF Ver5
#include "driver/gpio.h"
#include "esp_log.h"

#define _gpio_set_level(GPIO_PIN) (GPIO.out_w1ts = (1 << GPIO_PIN))
#define _gpio_clear_level(GPIO_PIN) (GPIO.out_w1tc = (1 << GPIO_PIN))

void func_gpio_set_level(int GPIO_PIN) {
        GPIO.out_w1ts = (1 << GPIO_PIN);
}

void func_gpio_clear_level(int GPIO_PIN) {
        GPIO.out_w1tc = (1 << GPIO_PIN);
}


#define GPIO_PIN 2

#define TAG "MAIN"

void app_main()
{
        //gpio_pad_select_gpio( GPIO_PIN );
        gpio_reset_pin( GPIO_PIN );
        gpio_set_direction( GPIO_PIN, GPIO_MODE_OUTPUT );
        gpio_set_level( GPIO_PIN, 0 );

        gpio_set_level( GPIO_PIN, 1 );
        vTaskDelay(100);
        gpio_set_level( GPIO_PIN, 0 );
        vTaskDelay(100);

        GPIO.out_w1ts = (1 << GPIO_PIN);
        vTaskDelay(100);
        GPIO.out_w1tc = (1 << GPIO_PIN);
        vTaskDelay(100);

        _gpio_set_level( GPIO_PIN );
        vTaskDelay(100);
        _gpio_clear_level( GPIO_PIN );
        vTaskDelay(100);

        TickType_t start;
        TickType_t end;
        TickType_t diff;
        start = xTaskGetTickCount();
        for(long i=0;i<1000000;i++) {
                gpio_set_level( GPIO_PIN, 1 );
                gpio_set_level( GPIO_PIN, 0 );
        }
        end = xTaskGetTickCount();
        diff = end - start;
        ESP_LOGI(TAG,"diff(gpio_set_level)=%d", diff);

        start = xTaskGetTickCount();
        for(long i=0;i<1000000;i++) {
                _gpio_set_level( GPIO_PIN );
                _gpio_clear_level( GPIO_PIN );
        }
        end = xTaskGetTickCount();
        diff = end - start;
        ESP_LOGI(TAG,"diff(register)=%d", diff);

        start = xTaskGetTickCount();
        for(long i=0;i<1000000;i++) {
                func_gpio_set_level( GPIO_PIN );
                func_gpio_clear_level( GPIO_PIN );
        }
        end = xTaskGetTickCount();
        diff = end - start;
        ESP_LOGI(TAG,"diff(func)=%d", diff);
}

GPIO2にLEDをつなげて実行すると、3回LEDが点滅するので、ちゃんとGPIOのON/OFFが動いていることが分かります。
結果は以下の様になりました。
I (13816) MAIN: diff(gpio_set_level)=75
I (13936) MAIN: diff(register)=12
I (14216) MAIN: diff(func)=28

レジスター操作の方がgpio_set_level()よりも、5倍程度高速に動くことが分かりました。
また、レジスターを操作する場合、関数呼び出しにするよりも、マクロで埋め込んだ方が2倍程度早くなります。
ESP32のTechnical Reference Manualはこ ちらからダウンロードすることができます。

ESP32のモデルによりGPIOの数は違います。
ESP32ではGPIOは40個で、GPIO00からGPIO31までのレジスターと、GPIO32からGPIO39までのレジスターに分かれて います。
ESP32-S2/S3ではGPIOが54個に拡張されていて、GPIO00からGPIO31までのレジスターと、GPIO32からGPIO53 までの レジスターに分かれています。
ESP32-C3ではGPIOは22個で、GPIO00からGPIO21までのレジスターだけです。
全てのモデルで共通して使えるGPIOは、GPIO00からGPIO21となります。

GPIO00からGPIO31までのレジスターはGPIO.out_w1ts/GPIO.out_w1tcですが、
GPIO32からGPIO53 までの レジスターはGPIO.out1_w1ts.val/GPIO.out1_w1tc.valと少し名前が変わります。



ESP-IDF V4.xやV5.0にはパラレルIOの関数は有りません。
こ ちらにi2sを使ったパラレルlcdドライバーが公開されていますが、ソースは結構難解です。
このサンプルを元に、8ビットパラレルTFTのライブラリをこちらで 公開しています。



ESP32-C3のサポートに伴い、gpio_pad_select_gpio() が gpio_reset_pin() に変更されました。
ESP32-C3のGPIO18やGPIO19は、gpio_pad_select_gpio()では初期化できなくなりました。



ESP-IDF V5.1から、Dedicated GPIOと言う機能が追加されました。
32個までのGPIOを一括して操作することができるParallel IO interfaceです。
ESP32やESP32Sシリーズに有るGPIO_OUT_W1TS_REG/GPIO_OUT_W1TC_REG と似ていますが、
レジスター操作ではなく、APIとして提供されていて、32個までの任意のGPIOを1つのグループとして登録し、
そのグループに対する一括操作を提供します。
グループ内のGPIOは連続していなくても構いません。
ESP32S3を使って、以下のコードで違いを確認してみました。
DEBUGを1に設定すると、ゆっくり動作します。
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "hal/gpio_ll.h"
#include "driver/dedic_gpio.h"

#define DEBUG 0

#ifdef CONFIG_IDF_TARGET_ESP32
int GPIO_PIN[] = {12, 13, 14, 15, 16, 17, 18, 19};
#else
int GPIO_PIN[] = {21, 46, 18, 17, 19, 20, 3, 14};
#endif


#define gpio_digital_write(GPIO_PIN, data) \
        do { \
                if (data) { \
                        gpio_set_level( GPIO_PIN, 1 ); \
                } else { \
                        gpio_set_level( GPIO_PIN, 0 ); \
                } \
        } while (0)

#define reg_digital_write(GPIO_PIN, data) \
    do { \
        if (data) { \
            if ( GPIO_PIN < 32 ) GPIO.out_w1ts = (1 << GPIO_PIN); \
            else GPIO.out1_w1ts.val = (1 << (GPIO_PIN - 32)); \
        } else { \
            if ( GPIO_PIN < 32 ) GPIO.out_w1tc = (1 << GPIO_PIN); \
            else GPIO.out1_w1tc.val = (1 << (GPIO_PIN - 32)); \
        } \
    } while (0)

void task1(void *pvParameters)
{
        TaskHandle_t taskHandle = (TaskHandle_t)pvParameters;
        for (int io=0;io<8;io++) {
                gpio_reset_pin( GPIO_PIN[io] );
                gpio_set_direction( GPIO_PIN[io], GPIO_MODE_OUTPUT );
                gpio_set_level( GPIO_PIN[io], 0 );
        }
        uint32_t value = 0;
        int32_t counter = 1000000;
        int direction = 1;
        TickType_t start = xTaskGetTickCount();
        for (int i=0;i<counter;i++) {
                gpio_digital_write(GPIO_PIN[0], value & 0x01);
                gpio_digital_write(GPIO_PIN[1], value & 0x02);
                gpio_digital_write(GPIO_PIN[2], value & 0x04);
                gpio_digital_write(GPIO_PIN[3], value & 0x08);
                gpio_digital_write(GPIO_PIN[4], value & 0x10);
                gpio_digital_write(GPIO_PIN[5], value & 0x20);
                gpio_digital_write(GPIO_PIN[6], value & 0x40);
                gpio_digital_write(GPIO_PIN[7], value & 0x80);

                if (DEBUG) {
                        ESP_LOGI(pcTaskGetName(NULL), "value=0x%"PRIx32, value);
                        vTaskDelay(50);
                        if (direction == 1) {
                                value = (value << 1) + 1;
                                if (value == 0xff) direction = -1;
                        } else {
                                value = value >> 1;
                                if (value == 0) direction = 1;
                        }
                } else {
                        value++;
                        if (value == 0x100) value = 0;
                }
        }
        TickType_t end = xTaskGetTickCount();
        TickType_t diff = end - start;
        ESP_LOGI(pcTaskGetName(NULL),"diff=%"PRIu32, diff);
        xTaskNotifyGive( taskHandle );
        vTaskDelete(NULL);
}

void task2(void *pvParameters)
{
        TaskHandle_t taskHandle = (TaskHandle_t)pvParameters;
        for (int io=0;io<8;io++) {
                gpio_reset_pin( GPIO_PIN[io] );
                gpio_set_direction( GPIO_PIN[io], GPIO_MODE_OUTPUT );
                gpio_set_level( GPIO_PIN[io], 0 );
        }
        uint32_t value = 0;
        int32_t counter = 1000000;
        int direction = 1;
        TickType_t start = xTaskGetTickCount();
        for (int i=0;i<counter;i++) {
                reg_digital_write(GPIO_PIN[0], value & 0x01);
                reg_digital_write(GPIO_PIN[1], value & 0x02);
                reg_digital_write(GPIO_PIN[2], value & 0x04);
                reg_digital_write(GPIO_PIN[3], value & 0x08);
                reg_digital_write(GPIO_PIN[4], value & 0x10);
                reg_digital_write(GPIO_PIN[5], value & 0x20);
                reg_digital_write(GPIO_PIN[6], value & 0x40);
                reg_digital_write(GPIO_PIN[7], value & 0x80);

                if (DEBUG) {
                        ESP_LOGI(pcTaskGetName(NULL), "value=0x%"PRIx32, value);
                        vTaskDelay(50);
                        if (direction == 1) {
                                value = (value << 1) + 1;
                                if (value == 0xff) direction = -1;
                        } else {
                                value = value >> 1;
                                if (value == 0) direction = 1;
                        }
                } else {
                        value++;
                        if (value == 0x100) value = 0;
                }
        }
        TickType_t end = xTaskGetTickCount();
        TickType_t diff = end - start;
        ESP_LOGI(pcTaskGetName(NULL),"diff=%"PRIu32, diff);
        xTaskNotifyGive( taskHandle );
        vTaskDelete(NULL);
}

void task3(void *pvParameters)
{
        TaskHandle_t taskHandle = (TaskHandle_t)pvParameters;
        for (int io=0;io<8;io++) {
                gpio_reset_pin( GPIO_PIN[io] );
                gpio_set_direction( GPIO_PIN[io], GPIO_MODE_OUTPUT );
                gpio_set_level( GPIO_PIN[io], 0 );
        }
        ESP_LOGI(pcTaskGetName(NULL), "Install fast GPIO bundle for line address control");
        dedic_gpio_bundle_config_t dedic_gpio_conf = {
                .flags.out_en = true,
                .gpio_array = (int[])
                {
                        GPIO_PIN[0], GPIO_PIN[1], GPIO_PIN[2], GPIO_PIN[3],
                        GPIO_PIN[4], GPIO_PIN[5], GPIO_PIN[6], GPIO_PIN[7]
                },
                .array_size = 8,
        };
        dedic_gpio_bundle_handle_t dedic_gpio_bundle_handle;
        ESP_ERROR_CHECK(dedic_gpio_new_bundle(&dedic_gpio_conf, &dedic_gpio_bundle_handle));
        uint32_t value = 0;
        int32_t counter = 1000000;
        int direction = 1;
        TickType_t start = xTaskGetTickCount();
        for (int i=0;i<counter;i++) {
                dedic_gpio_bundle_write(dedic_gpio_bundle_handle, 0xFF, value);

                if (DEBUG) {
                        ESP_LOGI(pcTaskGetName(NULL), "value=0x%"PRIx32, value);
                        vTaskDelay(50);
                        if (direction == 1) {
                                value = (value << 1) + 1;
                                if (value == 0xff) direction = -1;
                        } else {
                                value = value >> 1;
                                if (value == 0) direction = 1;
                        }
                } else {
                        value++;
                        if (value == 0x100) value = 0;
                }
        }
        TickType_t end = xTaskGetTickCount();
        TickType_t diff = end - start;
        ESP_LOGI(pcTaskGetName(NULL),"diff=%"PRIu32, diff);
        xTaskNotifyGive( taskHandle );
        vTaskDelete(NULL);
}

void app_main()
{
        TaskHandle_t taskHandle = xTaskGetCurrentTaskHandle();
        uint32_t value;

#if 0
        xTaskCreate(&task1, "TASK1", 1024*4, (void *)taskHandle, 2, NULL);
        value = ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
        ESP_LOGI(pcTaskGetName(NULL), "ulTaskNotifyTake value=%"PRIu32, value);
#endif

#if 1
        xTaskCreate(&task2, "TASK2", 1024*4, (void *)taskHandle, 2, NULL);
        value = ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
        ESP_LOGI(pcTaskGetName(NULL), "ulTaskNotifyTake value=%"PRIu32, value);
#endif

#if 0
        xTaskCreate(&task3, "TASK3", 1024*4, (void *)taskHandle, 2, NULL);
        value = ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
        ESP_LOGI(pcTaskGetName(NULL), "ulTaskNotifyTake value=%"PRIu32, value);
#endif
}

結果は以下の様になりました。
gpio_set_level()よりも14倍程度高速に、レジスター操作よりも2倍程度高速に動くことが分かりました。
I (3117) TASK1: diff=272
I (3117) main: ulTaskNotifyTake value=1
I (3717) TASK2: diff=52
I (3717) main: ulTaskNotifyTake value=1
I (3717) MAIN: Install fast GPIO bundle for line address control
I (3917) TASK3: diff=20
I (3917) main: ulTaskNotifyTake value=1
I (3917) main_task: Returned from app_main()

こ ちらに、この機能を使ったSoftwareSPI、SoftwareI2C、SoftwareUARTのサンプルが公開されています が、
元々これらの機能では任意のGPIOが使えるので、どの程度役に立つのか不明です。

Dedicated GPIOは、ESP32SシリーズとESP32Cシリーズで使用することができます。
なぜか、ESP32では以下の様にビルドが通りません。
ain/libmain.a(main.c.obj):(.literal.task3+0x1c): undefined reference to `dedic_gpio_new_bundle'
/home/nop/.espressif/tools/xtensa-esp32-elf/esp-12.2.0_20230208/xtensa-esp32-elf/bin/../lib/gcc/xtensa-esp32-elf/12.2.0/../../../../xtensa-esp32-elf/bin/ld: esp-idf/main/libmain.a(main.c.obj):(.literal.task3+0x20): undefined reference to `dedic_gpio_bundle_write'
/home/nop/.espressif/tools/xtensa-esp32-elf/esp-12.2.0_20230208/xtensa-esp32-elf/bin/../lib/gcc/xtensa-esp32-elf/12.2.0/../../../../xtensa-esp32-elf/bin/ld: esp-idf/main/libmain.a(main.c.obj): in function `task3':
/home/nop/rtos/dedicated-gpio/main/main.c:130: undefined reference to `dedic_gpio_new_bundle'
/home/nop/.espressif/tools/xtensa-esp32-elf/esp-12.2.0_20230208/xtensa-esp32-elf/bin/../lib/gcc/xtensa-esp32-elf/12.2.0/../../../../xtensa-esp32-elf/bin/ld: /home/nop/rtos/dedicated-gpio/main/main.c:140: undefined reference to `dedic_gpio_bundle_write'
/home/nop/.espressif/tools/xtensa-esp32-elf/esp-12.2.0_20230208/xtensa-esp32-elf/bin/../lib/gcc/xtensa-esp32-elf/12.2.0/../../../../xtensa-esp32-elf/bin/ld: /home/nop/rtos/dedicated-gpio/main/main.c:145: undefined reference to `dedic_gpio_bundle_write'
collect2: error: ld returned 1 exit status



ESP32-C6ではパラレルIOドライバー(PARLIO)が追加されました。
こ ちらにサンプルが公開されていますが、単純なパラレルIOではなく、クロックを使用した連続GPIO操作の様です。
公式サンプルではHUB75仕様のマトリックスLEDを操作していますが、HUB75仕様のマトリックスLEDを持っていないので、
使い方が良く分かりません。
HUB75仕様については、こ ちらに詳しく紹介されています。

続く...