User Tools

Site Tools


tamiwiki:users:6r1d:diymall_esp32_s3_fixture

This is an old revision of the document!


There’s something great about ESP32-WROOM modules: you can prototype fast, use test fixtures to debug, then pop a module off, solder it onto a board, and voilà, you’ve got a working device. Or a non-working one. Usually it's one of those two.

Introduction

I've been using a fixture by DIYMall for some time at this point. From my perspective, its main feature is the support for ESP32-S3-WROOM-1U and ESP32-S3-WROOM-1 (datasheet).

It is very helpful as a fixture. It is not helpful whatsoever when it comes to proper documentation. If you open the DIYMalls site, it shows a 403 and gives you nothing.

So, the fixture is easy to buy on AliExpress, for example, here, it works without hassle, and it will also require you trace all pins unless you have a reference.

Thus, I've traced everything I found so you don't have to.

Pin assignments

ESP32-S3-Wroom contact pad Datasheet reference Arduino pin Board pin
39 IO1 1 D36
38 IO2 2 D35
4 IO4 4 D1
5 IO5 5 D2
6 IO6 6 D3
7 IO7 7 D4
12 IO8 8 D9
17 IO9 9 D14
18 IO10 10 D15
19 IO11 11 D16
20 IO12 12 D17
21 IO13 13 D18
22 IO14 14 D19
8 IO15 15 D5
9 IO16 16 D6
10 IO17 17 D7
11 IO18 18 D8
23 IO21 21 D20
31 IO38 38 D28
32 IO39 39 D29
33 IO40 40 D30
34 IO41 41 D31
35 IO42 42 D32
24 IO47 47 D21
25 IO48 48 D22
36 RXD0 36 D33
37 TXD0 37 D34

Notes

  • GPIO pin 48 is the onboard LED, usually blue.
  • Initially, I thought that onboard USB-UART is connected to something else than RXD0 / TXD0. It was not. It is the main UART.

Tracing code

You might disagree with me and want to double-check. Excellent! More eyes (and feedback) mean better information.

Grab your trusty 30-year-old LED soldered to a pair of DuPont sockets and start tracing. If you don’t have one handy (like me), grab your scope instead.1)

For convenience, here’s the code to do the job. I didn’t bother writing it from scratch, but I tested it, and it worked just fine for me.

ESP-IDF (+ history and PWM debug)

This code gives you a predictable interactive workflow with history.

> help
Commands:
  list                          -> show test-safe GPIOs
  status                        -> show current pin/mode
  pin <gpio>                    -> start blinking GPIO
  <gpio>                        -> same as 'pin <gpio>'
  delay <ms>                    -> set blink delay
  pwm <pin> [brightness]        -> start PWM (0-99, current: 50%)
  brightness <level>            -> set PWM brightness (0-99)
  freq <frequency>              -> set PWM frequency (Hz)
  stop                          -> stop and release pin

> 48
Stopped. Pin released to INPUT.
Blinking GPIO 48 at 100 ms.

> 42
Stopped. Pin released to INPUT.
Blinking GPIO 42 at 100 ms.

> 47
Stopped. Pin released to INPUT.
Blinking GPIO 47 at 100 ms.

> 46
Restricted: GPIO 46 is not in the test-safe set.

> delay 40
Set blink delay to 40 ms.

> delay 10
Set blink delay to 10 ms.

As for using it, you should know the gist at this point.

mkdir pin_tracer && cd pin_tracer
idf.py create-project . && idf.py reconfigure && idf.py set-target esp32s3
# fill the code
# update the CMake configs
idf.py build
idf.py flash monitor

CMake config (code):

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

CMake config (project):

# 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(.)
esp32_s3_pin_tracer.c
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <inttypes.h>
#include <stdbool.h>
 
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
 
#include "driver/gpio.h"
#include "driver/uart.h"
#include "driver/uart_vfs.h"
#include "esp_err.h"
#include "esp_vfs_dev.h"
#include "esp_system.h"
#include "driver/ledc.h"
 
/*======== CONFIG ========*/
// Blink defaults
static uint32_t g_blink_delay_ms = 100;
 
// Console config
#define CLI_LINE_MAX 128
#define HIST_MAX     10
 
// PWM configuration (safe defaults)
#define LEDC_TIMER              LEDC_TIMER_0
#define LEDC_MODE               LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL            LEDC_CHANNEL_0
#define LEDC_DUTY_RES           LEDC_TIMER_8_BIT   // 0..(2^RES-1)
#define LEDC_FREQUENCY_DEFAULT  5000               // 5 kHz
 
/*======== SAFE GPIO CANDIDATES ========*/
static const gpio_num_t TEST_SAFE_GPIO[] = {
    1,  2,
    4,  5,  6,  7,
    8,  9,  10, 11, 12, 13, 14,
    15, 16, 17, 18,
    /* 19,20 excluded (USB D-/D+) */
    21,
    /* 22..34 not bonded on typical modules */
    /* 35,36,37 excluded (PSRAM on some variants) */
    38, 39, 40, 41, 42,          /* JTAG-capable if configured */
    /* 43,44 excluded (UART0 console) */
    47, 48                       /* may be 1.8 V on R16V */
};
static const size_t TEST_SAFE_GPIO_COUNT = sizeof(TEST_SAFE_GPIO)/sizeof(TEST_SAFE_GPIO[0]);
 
/*======== CONSTANTS & HELPERS ========*/
 
/* Sentinels */
#define HIST_NONE        (-1)
#define INVALID_GPIO     ((gpio_num_t)-1)
 
/* Bounds */
#define BLINK_DELAY_MIN_MS  10u
#define BLINK_DELAY_MAX_MS  60000u
 
#define BRIGHT_MIN          0
#define BRIGHT_MAX          99
 
#define FREQ_MIN_HZ         100u
#define FREQ_MAX_HZ         50000u  /* conservative for 8-bit @ low-speed */
 
/* Task sizes & priorities */
#define BLINK_STACK         2048
#define CONSOLE_STACK       4096
#define BLINK_TASK_PRIO     (tskIDLE_PRIORITY + 1)
#define CONSOLE_TASK_PRIO   (tskIDLE_PRIORITY + 2)
 
/* UART driver buffer */
#define UART_RX_BUF         512
 
/* Generic clamps */
static inline uint32_t clamp_u32(uint32_t v, uint32_t lo, uint32_t hi) {
    return (v < lo) ? lo : (v > hi) ? hi : v;
}
static inline int clamp_int(int v, int lo, int hi) {
    return (v < lo) ? lo : (v > hi) ? hi : v;
}
 
/* LEDC helpers */
static inline uint32_t ledc_max_duty(void) {
    // LEDC_DUTY_RES is enum like LEDC_TIMER_8_BIT → 8
    return (1u << LEDC_DUTY_RES) - 1u;
}
static inline uint32_t brightness_to_duty(uint8_t level) {
    // Map BRIGHT_MIN..BRIGHT_MAX → 0..max_duty; guard divide
    const uint32_t maxd = ledc_max_duty();
    const uint32_t denom = (BRIGHT_MAX > 0) ? (uint32_t)BRIGHT_MAX : 1u;
    return (uint32_t)level * maxd / denom;
}
 
/* Tiny predicates */
static inline bool is_valid_gpio(gpio_num_t pin) { return pin != INVALID_GPIO; }
 
/*======== RUNTIME ========*/
static gpio_num_t   g_current_pin      = INVALID_GPIO;
static bool         g_blinking         = false;
static bool         g_pwm_mode         = false;
static uint8_t      g_pwm_brightness   = 50;                 // BRIGHT_MIN..BRIGHT_MAX
static uint32_t     g_pwm_frequency    = LEDC_FREQUENCY_DEFAULT;
static TaskHandle_t g_blink_task       = NULL;
 
/*======== CLI ========*/
typedef struct {
    uart_port_t uart;
    char  *line;
    size_t *pos;
    size_t *cursor;
    char  (*hist)[CLI_LINE_MAX];
    int   *hist_count, *hist_head, *hist_view;
} cli_ctx_t;
 
/*---- CLI helpers ----*/
static inline void cli_write(cli_ctx_t *c, const char *s) {
    uart_write_bytes(c->uart, s, strlen(s));
}
 
static void cli_prompt(cli_ctx_t *c) {
    cli_write(c, "\r\n> ");
    fflush(stdout);
}
 
static void cli_redraw(cli_ctx_t *c) {
    // Clear line, print prompt + buffer, then move cursor left if needed
    cli_write(c, "\r\x1b[2K> ");
    if (*c->pos) uart_write_bytes(c->uart, c->line, *c->pos);
    if (*c->pos > *c->cursor) {
        char seq[16];
        int n = snprintf(seq, sizeof(seq), "\x1b[%zuD", (size_t)(*c->pos - *c->cursor));
        uart_write_bytes(c->uart, seq, n);
    }
}
 
static void cli_push_hist(cli_ctx_t *c, const char *cmd) {
    if (!cmd[0]) return;
    int last = (*c->hist_head - 1 + HIST_MAX) % HIST_MAX;
    if (*c->hist_count > 0 && strncmp(c->hist[last], cmd, CLI_LINE_MAX) == 0) return;
    strncpy(c->hist[*c->hist_head], cmd, CLI_LINE_MAX - 1);
    c->hist[*c->hist_head][CLI_LINE_MAX - 1] = 0;
    *c->hist_head = (*c->hist_head + 1) % HIST_MAX;
    if (*c->hist_count < HIST_MAX) (*c->hist_count)++;
}
 
static void cli_load_hist(cli_ctx_t *c, int idx) {
    strncpy(c->line, c->hist[idx], CLI_LINE_MAX - 1);
    c->line[CLI_LINE_MAX - 1] = 0;
    *c->pos = *c->cursor = strlen(c->line);
    cli_redraw(c);
}
 
static inline bool cli_in_hist(cli_ctx_t *c) { return *c->hist_view != HIST_NONE; }
 
/*======== HELPERS ========*/
static bool is_test_safe(gpio_num_t gpio) {
    for (size_t i = 0; i < TEST_SAFE_GPIO_COUNT; ++i) {
        if (TEST_SAFE_GPIO[i] == gpio) return true;
    }
    return false;
}
 
static void print_test_safe_pins(void) {
    printf("\nTest-safe GPIOs (ESP32-S3):\n  ");
    for (size_t i = 0; i < TEST_SAFE_GPIO_COUNT; ++i) {
        printf("%s%d", (i ? ", " : ""), TEST_SAFE_GPIO[i]);
    }
    printf("\nExcluded (reason): 0,3,45,46 (boot/strap/JTAG) | 19,20 (USB D-/D+) | 35-37 (PSRAM on some) | 43,44 (UART0 console)\n");
    printf("Note: On some R16V modules, GPIO47/48 are 1.8 V only.\n");
}
 
static void release_pin(gpio_num_t pin) {
    if (is_valid_gpio(pin)) {
        gpio_set_level(pin, 0);
        gpio_reset_pin(pin); // back to default (input/hi-z)
    }
}
 
static void stop_pwm(void) {
    if (g_pwm_mode) {
        ledc_stop(LEDC_MODE, LEDC_CHANNEL, 0);
        g_pwm_mode = false;
    }
}
 
static void stop_blink(void) {
    if (g_blink_task) {
        TaskHandle_t t = g_blink_task;
        g_blink_task = NULL; // signal task to exit
        for (int i = 0; i < 20 && eTaskGetState(t) != eDeleted; ++i) {
            vTaskDelay(pdMS_TO_TICKS(5));
        }
    }
    g_blinking = false;
}
 
static void stop_all(void) {
    stop_blink();
    stop_pwm();
    release_pin(g_current_pin);
    g_current_pin = INVALID_GPIO;
    printf("Stopped. Pin released to INPUT.\n");
}
 
/*======== BLINK TASK ========*/
static void blink_task(void *arg) {
    gpio_num_t pin = (gpio_num_t)(intptr_t)arg;
    gpio_config_t io = {
        .pin_bit_mask = 1ULL << pin,
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io);
    gpio_set_level(pin, 0);
    while (g_blink_task == xTaskGetCurrentTaskHandle()) {
        gpio_set_level(pin, 1);
        vTaskDelay(pdMS_TO_TICKS(g_blink_delay_ms));
        gpio_set_level(pin, 0);
        vTaskDelay(pdMS_TO_TICKS(g_blink_delay_ms));
    }
    release_pin(pin);
    vTaskDelete(NULL);
}
 
/*======== PWM ========*/
static bool setup_pwm(gpio_num_t gpio) {
    if (g_pwm_mode) ledc_stop(LEDC_MODE, LEDC_CHANNEL, 0);
    // Configure LEDC timer
    ledc_timer_config_t ledc_timer = {
        .speed_mode      = LEDC_MODE,
        .timer_num       = LEDC_TIMER,
        .duty_resolution = LEDC_DUTY_RES,
        .freq_hz         = g_pwm_frequency,
        .clk_cfg         = LEDC_AUTO_CLK
    };
    esp_err_t ret = ledc_timer_config(&ledc_timer);
    if (ret != ESP_OK) {
        printf("PWM timer config failed: %s (try lower freq or lower duty resolution)\n", esp_err_to_name(ret));
        return false;
    }
    // Configure LEDC channel
    ledc_channel_config_t ledc_channel = {
        .speed_mode = LEDC_MODE,
        .channel    = LEDC_CHANNEL,
        .timer_sel  = LEDC_TIMER,
        .intr_type  = LEDC_INTR_DISABLE,
        .gpio_num   = gpio,
        .duty       = 0,
        .hpoint     = 0
    };
    ret = ledc_channel_config(&ledc_channel);
    if (ret != ESP_OK) {
        printf("PWM channel config failed: %s\n", esp_err_to_name(ret));
        return false;
    }
    return true;
}
 
static void set_pwm_brightness(uint8_t level) {
    level = (uint8_t)clamp_int(level, BRIGHT_MIN, BRIGHT_MAX);
    g_pwm_brightness = level;
    const uint32_t duty = brightness_to_duty(level);
    ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
    ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
    printf("PWM brightness set to %u%% (duty: %" PRIu32 "/%" PRIu32 ")\n",
           (unsigned)level, duty, ledc_max_duty());
}
 
static void set_pwm_frequency(uint32_t freq) {
    g_pwm_frequency = clamp_u32(freq, FREQ_MIN_HZ, FREQ_MAX_HZ);
    if (g_pwm_mode) {
        ledc_timer_config_t ledc_timer = {
            .speed_mode      = LEDC_MODE,
            .timer_num       = LEDC_TIMER,
            .duty_resolution = LEDC_DUTY_RES,
            .freq_hz         = g_pwm_frequency,
            .clk_cfg         = LEDC_AUTO_CLK
        };
        esp_err_t ret = ledc_timer_config(&ledc_timer);
        if (ret == ESP_OK) {
            printf("PWM frequency set to %" PRIu32 " Hz\n", g_pwm_frequency);
            set_pwm_brightness(g_pwm_brightness); // restore duty for new timer base
        } else {
            printf("Failed to set PWM frequency: %s\n", esp_err_to_name(ret));
        }
    } else {
        printf("PWM frequency set to %" PRIu32 " Hz (applies when PWM starts)\n", g_pwm_frequency);
    }
}
 
/*======== MODE START ========*/
static void start_blink(gpio_num_t gpio) {
    if (!is_test_safe(gpio)) {
        printf("Restricted: GPIO %d is not in the test-safe set.\n", gpio);
        return;
    }
    stop_all();
    g_current_pin = gpio;
    g_blinking = true;
    if (xTaskCreatePinnedToCore(blink_task, "blink_task", BLINK_STACK, (void*)(intptr_t)gpio,
                                BLINK_TASK_PRIO, &g_blink_task, tskNO_AFFINITY) != pdPASS) {
        printf("Error: Failed to create blink task\n");
        g_blinking = false;
        g_current_pin = INVALID_GPIO;
        return;
    }
    printf("Blinking GPIO %d at %" PRIu32 " ms.\n", g_current_pin, g_blink_delay_ms);
}
 
static void start_pwm(gpio_num_t gpio, uint8_t brightness) {
    if (!is_test_safe(gpio)) {
        printf("Restricted: GPIO %d is not in the test-safe set.\n", gpio);
        return;
    }
    stop_all();
    if (!setup_pwm(gpio)) {
        printf("Failed to setup PWM on GPIO %d\n", gpio);
        return;
    }
    g_current_pin = gpio;
    g_pwm_mode = true;
    set_pwm_brightness(brightness);
    printf("PWM started on GPIO %d at %" PRIu32 " Hz\n", gpio, g_pwm_frequency);
}
 
/*======== COMMAND PARSER ========*/
static void trim(char *s) {
    size_t len = strlen(s);
    while (len && (s[len-1] == '\r' || s[len-1] == '\n' || isspace((unsigned char)s[len-1]))) s[--len] = 0;
    size_t i = 0;
    while (s[i] && isspace((unsigned char)s[i])) i++;
    if (i) memmove(s, s+i, strlen(s+i)+1);
}
 
static bool all_digits(const char *s) {
    if (!*s) return false;
    for (const char *p = s; *p; ++p) if (!isdigit((unsigned char)*p)) return false;
    return true;
}
 
static void handle_command(char *line) {
    trim(line);
    if (!*line) return;
 
    char cmd[64];
    strncpy(cmd, line, sizeof(cmd)-1);
    cmd[sizeof(cmd)-1] = 0;
    for (char *p = cmd; *p; ++p) *p = (char)tolower((unsigned char)*p);
 
    if (strcmp(cmd, "list") == 0) {
        print_test_safe_pins();
        return;
    }
    if (strcmp(cmd, "status") == 0) {
        printf("Pin: %d, mode: %s, ",
               (int)g_current_pin, g_blinking ? "blink" : (g_pwm_mode ? "pwm" : "idle"));
        if (g_blinking) {
            printf("delay: %" PRIu32 " ms\n", g_blink_delay_ms);
        } else if (g_pwm_mode) {
            printf("brightness: %u%%, frequency: %" PRIu32 " Hz\n",
                   (unsigned)g_pwm_brightness, g_pwm_frequency);
        } else {
            printf("no active output\n");
        }
        return;
    }
    if (strcmp(cmd, "stop") == 0 || strcmp(cmd, "pwm off") == 0 || strcmp(cmd, "mode off") == 0) {
        stop_all();
        return;
    }
    if (strncmp(cmd, "pin ", 4) == 0) {
        int gpio = atoi(line + 4);
        start_blink((gpio_num_t)gpio);
        return;
    }
    if (strncmp(cmd, "delay ", 6) == 0) {
        int v = atoi(line + 6);
        g_blink_delay_ms = clamp_u32((uint32_t)v, BLINK_DELAY_MIN_MS, BLINK_DELAY_MAX_MS);
        printf("Set blink delay to %" PRIu32 " ms.\n", g_blink_delay_ms);
        return;
    }
    if (strncmp(cmd, "pwm ", 4) == 0) {
        // "pwm <pin> [brightness]"
        char *args = line + 4; trim(args);
        int gpio = -1, brightness = g_pwm_brightness;
        if (sscanf(args, "%d %d", &gpio, &brightness) >= 1) {
            brightness = clamp_int(brightness, BRIGHT_MIN, BRIGHT_MAX);
            start_pwm((gpio_num_t)gpio, (uint8_t)brightness);
        } else {
            printf("Usage: pwm <pin> [brightness %d-%d]\nCurrent brightness: %u%%\n",
                   BRIGHT_MIN, BRIGHT_MAX, (unsigned)g_pwm_brightness);
        }
        return;
    }
    if (strncmp(cmd, "brightness ", 11) == 0 || strncmp(cmd, "bright ", 7) == 0) {
        const char *arg = (strncmp(cmd, "bright ", 7) == 0) ? (line + 7) : (line + 11);
        int brightness = atoi(arg);
        brightness = clamp_int(brightness, BRIGHT_MIN, BRIGHT_MAX);
        if (g_pwm_mode) {
            set_pwm_brightness((uint8_t)brightness);
        } else {
            printf("Error: PWM mode not active. Use 'pwm <pin> [brightness]' first.\n");
        }
        return;
    }
    if (strncmp(cmd, "freq ", 5) == 0 || strncmp(cmd, "frequency ", 10) == 0) {
        char *arg = (cmd[4] == ' ') ? (line + 5) : (line + 10);
        int freq = atoi(arg);
        if (freq > 0) set_pwm_frequency((uint32_t)freq);
        else printf("Frequency must be positive\n");
        return;
    }
    if (strcmp(cmd, "help") == 0) {
        printf("Commands:\n"
               "  list                          -> show test-safe GPIOs\n"
               "  status                        -> show current pin/mode\n"
               "  pin <gpio>                    -> start blinking GPIO\n"
               "  <gpio>                        -> same as 'pin <gpio>'\n"
               "  delay <ms>                    -> set blink delay (%u..%u)\n"
               "  pwm <pin> [brightness]        -> start PWM (%d..%d, current: %u%%)\n"
               "  brightness <level>            -> set PWM brightness (%d..%d)\n"
               "  freq <frequency>              -> set PWM frequency (Hz, %u..%u)\n"
               "  pwm off | mode off | stop     -> stop output and release pin\n",
               BLINK_DELAY_MIN_MS, BLINK_DELAY_MAX_MS,
               BRIGHT_MIN, BRIGHT_MAX, (unsigned)g_pwm_brightness,
               BRIGHT_MIN, BRIGHT_MAX,
               FREQ_MIN_HZ, FREQ_MAX_HZ);
        return;
    }
    if (all_digits(line)) { // bare number = pin
        start_blink((gpio_num_t)atoi(line));
        return;
    }
    printf("Unknown. Type 'help' for commands.\n");
}
 
/*======== CONSOLE TASK ========*/
static void console_task(void *arg) {
    const uart_port_t uart_num = UART_NUM_0;
    char   line[CLI_LINE_MAX] = {0};
    size_t pos = 0;              // length used
    size_t cursor = 0;           // caret position
    char hist[HIST_MAX][CLI_LINE_MAX] = {{0}};
    int  hist_count = 0;
    int  hist_head  = 0;
    int  hist_view  = HIST_NONE;
    cli_ctx_t C = {
        .uart       = uart_num,
        .line       = line,
        .pos        = &pos,
        .cursor     = &cursor,
        .hist       = hist,
        .hist_count = &hist_count,
        .hist_head  = &hist_head,
        .hist_view  = &hist_view
    };
    cli_prompt(&C);
    enum { ESC_IDLE, ESC_ESC, ESC_CSI } esc = ESC_IDLE;
    char csi_param_buf[4] = {0};
    int  csi_param_len = 0;
    while (1) {
        uint8_t ch;
        int got = uart_read_bytes(uart_num, &ch, 1, pdMS_TO_TICKS(30));
        if (got != 1) continue;
        // Escape handling (arrows/Home/End/Del and Alt-B/F)
        if (esc == ESC_ESC) {
            if (ch == '[') { esc = ESC_CSI; csi_param_len = 0; continue; }
            // Alt-b / Alt-f
            if (ch == 'b' || ch == 'B') {
                if (cursor > 0) {
                    while (cursor > 0 && isspace((unsigned char)line[cursor-1])) cursor--;
                    while (cursor > 0 && !isspace((unsigned char)line[cursor-1])) cursor--;
                    cli_redraw(&C);
                }
                esc = ESC_IDLE; continue;
            }
            if (ch == 'f' || ch == 'F') {
                if (cursor < pos) {
                    while (cursor < pos && !isspace((unsigned char)line[cursor])) cursor++;
                    while (cursor < pos &&  isspace((unsigned char)line[cursor])) cursor++;
                    cli_redraw(&C);
                }
                esc = ESC_IDLE; continue;
            }
            esc = ESC_IDLE; // unknown ESC seq
            continue;
        } else if (esc == ESC_CSI) {
            if (ch >= '0' && ch <= '9') {
                if (csi_param_len < (int)sizeof(csi_param_buf)-1) csi_param_buf[csi_param_len++] = (char)ch;
                continue;
            }
            if (ch == '~') {
                // [3~ delete, [1~ home, [4~ end
                int p = atoi(csi_param_buf);
                if (p == 3) { // Delete forward
                    if (cursor < pos) {
                        memmove(&line[cursor], &line[cursor+1], pos - cursor - 1);
                        pos--; line[pos] = 0;
                        cli_redraw(&C);
                    }
                } else if (p == 1) {
                    cursor = 0; cli_redraw(&C);
                } else if (p == 4) {
                    cursor = pos; cli_redraw(&C);
                }
                esc = ESC_IDLE; continue;
            }
            // Final byte for standard arrows/home/end
            if (ch == 'A') { // Up
                if (hist_count) {
                    if (!cli_in_hist(&C)) hist_view = (hist_head - 1 + HIST_MAX) % HIST_MAX;
                    else {
                        int oldest = (hist_head - hist_count + HIST_MAX) % HIST_MAX;
                        if (hist_view != oldest) hist_view = (hist_view - 1 + HIST_MAX) % HIST_MAX;
                    }
                    cli_load_hist(&C, hist_view);
                }
            } else if (ch == 'B') { // Down
                if (cli_in_hist(&C)) {
                    int newest = (hist_head - 1 + HIST_MAX) % HIST_MAX;
                    if (hist_view != newest) { hist_view = (hist_view + 1) % HIST_MAX; cli_load_hist(&C, hist_view); }
                    else { hist_view = HIST_NONE; pos = cursor = 0; line[0] = 0; cli_redraw(&C); }
                }
            } else if (ch == 'C') { // Right
                if (cursor < pos) { cursor++; cli_redraw(&C); }
            } else if (ch == 'D') { // Left
                if (cursor > 0) { cursor--; cli_redraw(&C); }
            } else if (ch == 'H') { // Home
                cursor = 0; cli_redraw(&C);
            } else if (ch == 'F') { // End
                cursor = pos; cli_redraw(&C);
            }
            esc = ESC_IDLE;
            continue;
        }
        if (ch == 0x1B) { esc = ESC_ESC; continue; } // start ESC
        // CR / LF -> execute
        if (ch == '\r' || ch == '\n') {
            cli_write(&C, "\r\n");
            line[pos] = 0;
            if (pos) { cli_push_hist(&C, line); hist_view = HIST_NONE; }
            handle_command(line);
            pos = cursor = 0; line[0] = 0;
            cli_prompt(&C);
            continue;
        }
        // Ctrl-A / Ctrl-E
        if (ch == 0x01) { cursor = 0; cli_redraw(&C); continue; }
        if (ch == 0x05) { cursor = pos; cli_redraw(&C); continue; }
        // Backspace
        if (ch == 0x08 || ch == 0x7F) {
            if (cursor > 0) {
                memmove(&line[cursor - 1], &line[cursor], pos - cursor);
                cursor--; pos--;
                line[pos] = 0;
                cli_redraw(&C);
            }
            continue;
        }
        // Ctrl-U: clear line
        if (ch == 0x15) { pos = cursor = 0; line[0] = 0; cli_redraw(&C); continue; }
        // Ctrl-L: redraw
        if (ch == 0x0C) { cli_redraw(&C); continue; }
        // Ctrl-W: delete previous word
        if (ch == 0x17) {
            size_t start = cursor;
            while (start > 0 && isspace((unsigned char)line[start-1])) start--;
            while (start > 0 && !isspace((unsigned char)line[start-1])) start--;
            if (start < cursor) {
                memmove(&line[start], &line[cursor], pos - cursor);
                pos -= (cursor - start);
                cursor = start;
                line[pos] = 0;
                cli_redraw(&C);
            }
            continue;
        }
        // Printable ASCII: insert at cursor
        if (isprint(ch)) {
            if (pos + 1 < CLI_LINE_MAX) {
                if (cli_in_hist(&C)) { hist_view = HIST_NONE; /* keep recalled text */ }
                memmove(&line[cursor + 1], &line[cursor], pos - cursor);
                line[cursor++] = (char)ch;
                pos++;
                line[pos] = 0;
                cli_redraw(&C);
            }
            continue;
        }
        // ignore other control bytes
    }
}
 
/*======== APP ========*/
void app_main(void) {
    const int baud = 115200;
    const uart_port_t uart_num = UART_NUM_0;
    uart_config_t cfg = {
        .baud_rate  = baud,
        .data_bits  = UART_DATA_8_BITS,
        .parity     = UART_PARITY_DISABLE,
        .stop_bits  = UART_STOP_BITS_1,
        .flow_ctrl  = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_DEFAULT,
    };
    ESP_ERROR_CHECK(uart_driver_install(uart_num, UART_RX_BUF, 0, 0, NULL, 0));
    ESP_ERROR_CHECK(uart_param_config(uart_num, &cfg));
    uart_vfs_dev_use_driver(uart_num);
    uart_vfs_dev_port_set_rx_line_endings(uart_num, ESP_LINE_ENDINGS_CRLF);
    uart_vfs_dev_port_set_tx_line_endings(uart_num, ESP_LINE_ENDINGS_CRLF);
    vTaskDelay(pdMS_TO_TICKS(200));
    uart_flush_input(uart_num);
    printf("\nESP32-S3 GPIO Tester with PWM (ESP-IDF)\n");
    printf("Type 'help' for commands. Cursor keys, history, word jumps supported.\n");
    print_test_safe_pins();
    xTaskCreatePinnedToCore(console_task, "console", CONSOLE_STACK, NULL,
                            CONSOLE_TASK_PRIO, NULL, tskNO_AFFINITY);
    // Don't return; keep prompt active
    vTaskDelete(NULL);
}

Arduino (basic version)

Flash from Arduino IDE. Use the ESP32 board package by Espressif, pick ESP32S3 Dev Module, pick 115200 baud in Serial Monitor.

esp32_s3_pin_tracer.ino
#include <Arduino.h>
 
// ====== CONFIG ======
// Blink timing (change at runtime with: delay <ms>)
static uint32_t BLINK_DELAY_MS = 100;
 
// Conservative "test-safe GPIO" set for ESP32-S3 modules.
// Excludes pins that commonly serve boot strapping, USB D-/D+, PSRAM/flash, UART0 console, or JTAG,
// which can interfere with programming, boot, or board-level functions during quick tests.
//
// Notes (summarized from the ESP32-S3 module/chip docs):
// - 0, 3, 45, 46 are strapping/boot-related or JTAG-related; driving them can change boot modes or disable debug.
// - 19, 20 are the on-chip USB D-/D+ differential pair; toggling them breaks USB comms.
// - 35, 36, 37 are wired to Octal PSRAM on some variants; not available as GPIO there.
// - 43 (U0TXD), 44 (U0RXD) are UART0 console pins used by Serial; poking them disrupts logs/flashing.
// - 47, 48 form a differential SPI clock pair; safe as GPIO but mind 1.8 V I/O level on some R16V variants.
const uint8_t TEST_SAFE_GPIO[] = {
  1, 2,                      // ADC/Touch-capable, OK for digital use
  4, 5, 6, 7,                // "
  8, 9, 10, 11, 12, 13, 14,  // "
  15, 16, 17, 18,            // "
  // 19,20 excluded (USB D-/D+)
  21,  // plain GPIO
  // 22..34 don't exist on S3 modules
  // 35,36,37 excluded (PSRAM on some variants)
  38, 39, 40, 41, 42,  // JTAG-capable if configured, but OK as GPIO when JTAG is not in use
  // 43,44 excluded (UART0 TX/RX used by Serial)
  47, 48  // differential clock pair; OK as GPIO (watch 1.8 V I/O on some R16V variants)
};
const size_t TEST_SAFE_GPIO_COUNT = sizeof(TEST_SAFE_GPIO) / sizeof(TEST_SAFE_GPIO[0]);
 
// ====== RUNTIME STATE ======
static int currentPin = -1;
static bool blinking = false;
 
// ====== HELPERS ======
bool isTestSafe(int gpio) {
  for (size_t i = 0; i < TEST_SAFE_GPIO_COUNT; ++i) {
    if ((int)TEST_SAFE_GPIO[i] == gpio) return true;
  }
  return false;
}
 
void printTestSafePins() {
  Serial.println(F("\nTest-safe GPIOs (ESP32-S3):"));
  for (size_t i = 0; i < TEST_SAFE_GPIO_COUNT; ++i) {
    Serial.print(F(i ? ", " : "  "));
    Serial.print(TEST_SAFE_GPIO[i]);
  }
  Serial.println();
  Serial.println(F("Excluded (reason): 0,3,45,46 (boot/strap/JTAG) | 19,20 (USB D-/D+) | 35-37 (PSRAM on some) | 43,44 (UART0 console)"));
}
 
void stopBlink() {
  if (currentPin >= 0) {
    digitalWrite(currentPin, LOW);
    pinMode(currentPin, INPUT);  // leave safe
  }
  blinking = false;
  currentPin = -1;
  Serial.println(F("Stopped. Pin released to INPUT."));
}
 
void startBlink(int gpio) {
  if (!isTestSafe(gpio)) {
    Serial.print(F("Restricted: GPIO "));
    Serial.print(gpio);
    Serial.println(F(" is not in the test-safe set."));
    return;
  }
  if (blinking && currentPin == gpio) {
    Serial.print(F("Already blinking GPIO "));
    Serial.println(gpio);
    return;
  }
  // switch pin if needed
  if (blinking && currentPin != gpio) stopBlink();
 
  currentPin = gpio;
  pinMode(currentPin, OUTPUT);
  digitalWrite(currentPin, LOW);
  blinking = true;
 
  Serial.print(F("Blinking GPIO "));
  Serial.print(currentPin);
  Serial.print(F(" at "));
  Serial.print(BLINK_DELAY_MS);
  Serial.println(F(" ms."));
}
 
// Parse commands like:
//   list
//   pin 10
//   delay 250
//   stop
void handleCommand(String line) {
  line.trim();
  if (line.length() == 0) return;
 
  line.toLowerCase();
  if (line == "list") {
    printTestSafePins();
    return;
  }
  if (line == "stop") {
    stopBlink();
    return;
  }
  if (line.startsWith("pin ")) {
    int gpio = line.substring(4).toInt();
    startBlink(gpio);
    return;
  }
  if (line.startsWith("delay ")) {
    int v = line.substring(6).toInt();
    if (v < 10) v = 10;  // clamp a bit
    BLINK_DELAY_MS = (uint32_t)v;
    Serial.print(F("Set blink delay to "));
    Serial.print(BLINK_DELAY_MS);
    Serial.println(F(" ms."));
    return;
  }
 
  // Single-number shortcut: just type the GPIO number
  bool allDigits = true;
  for (size_t i = 0; i < (size_t)line.length(); ++i) {
    if (!isDigit(line[i])) {
      allDigits = false;
      break;
    }
  }
  if (allDigits) {
    startBlink(line.toInt());
    return;
  }
 
  Serial.println(F("Commands:"));
  Serial.println(F("  list               -> show test-safe GPIOs"));
  Serial.println(F("  pin <gpio>         -> start blinking that GPIO"));
  Serial.println(F("  <gpio>             -> same as 'pin <gpio>'"));
  Serial.println(F("  delay <ms>         -> set blink delay"));
  Serial.println(F("  stop               -> stop and release pin"));
}
 
void setup() {
  Serial.begin(115200);
  // give USB-Serial/JTAG a moment
  delay(400);
  Serial.println(F("\nESP32-S3 GPIO Quick Tester"));
  Serial.println(F("Type 'list' to see test-safe pins, 'pin <gpio>' to blink, 'delay <ms>' to change speed, 'stop' to release.\n"));
  printTestSafePins();
}
 
void loop() {
  // Process serial line input
  static String line;
  while (Serial.available()) {
    char c = (char)Serial.read();
    if (c == '\r') continue;
    if (c == '\n') {
      handleCommand(line);
      line = "";
    } else {
      line += c;
    }
  }
 
  // Blink the selected pin
  if (blinking && currentPin >= 0) {
    digitalWrite(currentPin, HIGH);
    delay(BLINK_DELAY_MS);
    digitalWrite(currentPin, LOW);
    delay(BLINK_DELAY_MS);
  } else {
    delay(5);
  }
}

Feedback

If you’d like to suggest corrections or aren’t satisfied with how I wrote this short article, feel free to ping me on Telegram.

This topic might deserve to be expanded into a document outside of my personal page. Then again, I’m not sure how many people around here actually use ESP32-S3.

1)
Marie Antoinette reference unintended.
tamiwiki/users/6r1d/diymall_esp32_s3_fixture.1760937940.txt.gz · Last modified: by 6r1d