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.
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.
| 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 |
You may disagree with me and want to run a quick check. For your convenience, there's a code to do so. I didn't bother to write it manually, but I have tested it and it worked for me.
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.
#include <stdio.h> #include <string.h> #include <ctype.h> #include <inttypes.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #include "driver/uart.h" #include "driver/uart_vfs.h" #include "esp_vfs_dev.h" #include "esp_system.h" #include "driver/ledc.h" /* * CONFIG */ // Defaults static uint32_t g_blink_delay_ms = 100; // Console config #define CLI_LINE_MAX 128 #define HIST_MAX 10 // PWM configuration #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-255 #define LEDC_FREQUENCY 5000 // 5 kHz // Conservative "test-safe GPIO" set for ESP32-S3 modules. 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]); // ====== RUNTIME STATE ====== static gpio_num_t g_current_pin = -1; static bool g_blinking = false; static bool g_pwm_mode = false; static uint8_t g_pwm_brightness = 50; // 0-99 scale static uint32_t g_pwm_frequency = LEDC_FREQUENCY; static TaskHandle_t g_blink_task = NULL; /* ================== GPIO 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 (pin >= 0) { 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 = -1; printf("Stopped. Pin released to INPUT.\n"); } 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); } static bool setup_pwm(gpio_num_t gpio) { // Stop previous PWM if any 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, // Use configurable 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\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, // Start at 0 duty .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) { if (level > 99) level = 99; g_pwm_brightness = level; // Convert 0-99 scale to 0-255 duty cycle uint32_t duty = (level * 255) / 99; ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty); ledc_update_duty(LEDC_MODE, LEDC_CHANNEL); printf("PWM brightness set to %d%% (duty: %"PRIu32"/255)\n", level, duty); } static void set_pwm_frequency(uint32_t freq) { if (freq < 100) freq = 100; if (freq > 50000) freq = 50000; // Conservative max for LEDC g_pwm_frequency = freq; if (g_pwm_mode) { // Reconfigure PWM with new frequency 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); // Restore brightness set_pwm_brightness(g_pwm_brightness); } else { printf("Failed to set PWM frequency: %s\n", esp_err_to_name(ret)); } } else { printf("PWM frequency set to %" PRIu32 " Hz (will apply when PWM starts)\n", g_pwm_frequency); } } 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(); // Stop any previous mode g_current_pin = gpio; g_blinking = true; if (xTaskCreatePinnedToCore(blink_task, "blink_task", 2048, (void*)(intptr_t)gpio, tskIDLE_PRIORITY + 1, &g_blink_task, tskNO_AFFINITY) != pdPASS) { printf("Error: Failed to create blink task\n"); g_blinking = false; g_current_pin = -1; 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(); // Stop any previous mode 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: %d%%, frequency: %" PRIu32 " Hz\n", g_pwm_brightness, g_pwm_frequency); } else { printf("no active output\n"); } return; } if (strcmp(cmd, "stop") == 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); if (v < 10) v = 10; g_blink_delay_ms = (uint32_t)v; printf("Set blink delay to %" PRIu32 " ms.\n", g_blink_delay_ms); return; } if (strncmp(cmd, "pwm ", 4) == 0) { // Parse "pwm <pin> <brightness>" or "pwm <pin>" char *args = line + 4; trim(args); int gpio = -1, brightness = g_pwm_brightness; // Default to current brightness if (sscanf(args, "%d %d", &gpio, &brightness) >= 1) { if (brightness < 0) brightness = 0; if (brightness > 99) brightness = 99; start_pwm((gpio_num_t)gpio, (uint8_t)brightness); } else { printf("Usage: pwm <pin> [brightness 0-99]\n"); printf("Current brightness: %d%%. Omit brightness to use current.\n", g_pwm_brightness); } return; } if (strncmp(cmd, "brightness ", 11) == 0 || strncmp(cmd, "bright ", 7) == 0) { // Allow both "brightness XX" and "bright XX" char *arg = (cmd[6] == ' ') ? line + 7 : line + 11; int brightness = atoi(arg); if (brightness >= 0 && brightness <= 99) { if (g_pwm_mode) { set_pwm_brightness((uint8_t)brightness); } else { printf("Error: PWM mode not active. Use 'pwm <pin> [brightness]' first.\n"); } } else { printf("Brightness must be 0-99\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\n" " pwm <pin> [brightness] -> start PWM (0-99, current: %d%%)\n" " brightness <level> -> set PWM brightness (0-99)\n" " freq <frequency> -> set PWM frequency (Hz)\n" " stop -> stop and release pin\n", g_pwm_brightness); return; } if (all_digits(line)) { start_blink((gpio_num_t)atoi(line)); return; } printf("Unknown. Type 'help' for commands.\n"); } /* ================== CONSOLE ================== */ 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 of buffer size_t cursor = 0; // cursor index in [0..pos] char hist[HIST_MAX][CLI_LINE_MAX] = {{0}}; int hist_count = 0; // number of valid entries int hist_head = 0; // next insert index (ring buffer) int hist_view = -1; // -1 not browsing; else index into hist // helpers #define WRITE_STR(s) uart_write_bytes(uart_num, (s), strlen(s)) auto void prompt(void) { WRITE_STR("\r\n> "); fflush(stdout); } auto void redraw_line(void) { // Clear line, reprint prompt + buffer, then move cursor left if needed WRITE_STR("\r\x1b[2K> "); if (pos) uart_write_bytes(uart_num, line, pos); if (pos > cursor) { char seq[16]; int n = snprintf(seq, sizeof(seq), "\x1b[%zuD", (size_t)(pos - cursor)); uart_write_bytes(uart_num, seq, n); } } auto void push_history(const char *cmd) { if (!cmd[0]) return; int last = (hist_head - 1 + HIST_MAX) % HIST_MAX; if (hist_count > 0 && strncmp(hist[last], cmd, CLI_LINE_MAX) == 0) return; strncpy(hist[hist_head], cmd, CLI_LINE_MAX - 1); hist[hist_head][CLI_LINE_MAX - 1] = 0; hist_head = (hist_head + 1) % HIST_MAX; if (hist_count < HIST_MAX) hist_count++; } auto void load_history(int idx) { strncpy(line, hist[idx], CLI_LINE_MAX - 1); line[CLI_LINE_MAX - 1] = 0; pos = cursor = strlen(line); redraw_line(); } auto bool in_history(void) { return hist_view != -1; } prompt(); enum { ESC_IDLE, ESC_ESC, ESC_CSI, ESC_TILDE } 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 (word left/right) if (ch == 'b' || ch == 'B') { // word-left if (cursor > 0) { while (cursor > 0 && isspace((unsigned char)line[cursor-1])) cursor--; while (cursor > 0 && !isspace((unsigned char)line[cursor-1])) cursor--; redraw_line(); } esc = ESC_IDLE; continue; } if (ch == 'f' || ch == 'F') { // word-right if (cursor < pos) { while (cursor < pos && !isspace((unsigned char)line[cursor])) cursor++; while (cursor < pos && isspace((unsigned char)line[cursor])) cursor++; redraw_line(); } esc = ESC_IDLE; continue; } // Unknown ESC seq -> ignore esc = ESC_IDLE; 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 == '~') { // Handle [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; redraw_line(); } } else if (p == 1) { // Home cursor = 0; redraw_line(); } else if (p == 4) { // End cursor = pos; redraw_line(); } esc = ESC_IDLE; continue; } // Final byte for standard arrows/home/end if (ch == 'A') { // Up if (hist_count) { if (!in_history()) 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; } load_history(hist_view); } } else if (ch == 'B') { // Down if (in_history()) { int newest = (hist_head - 1 + HIST_MAX) % HIST_MAX; if (hist_view != newest) { hist_view = (hist_view + 1) % HIST_MAX; load_history(hist_view); } else { hist_view = -1; pos = cursor = 0; line[0] = 0; redraw_line(); } } } else if (ch == 'C') { // Right if (cursor < pos) { cursor++; redraw_line(); } } else if (ch == 'D') { // Left if (cursor > 0) { cursor--; redraw_line(); } } else if (ch == 'H') { // Home cursor = 0; redraw_line(); } else if (ch == 'F') { // End cursor = pos; redraw_line(); } esc = ESC_IDLE; continue; } // Start of escape? if (ch == 0x1B) { esc = ESC_ESC; continue; } // CR / LF -> execute if (ch == '\r' || ch == '\n') { WRITE_STR("\r\n"); line[pos] = 0; if (pos) { push_history(line); hist_view = -1; } handle_command(line); pos = cursor = 0; line[0] = 0; prompt(); continue; } // Ctrl-A / Ctrl-E: home/end if (ch == 0x01) { cursor = 0; redraw_line(); continue; } if (ch == 0x05) { cursor = pos; redraw_line(); continue; } // Backspace / Delete-left if (ch == 0x08 || ch == 0x7F) { if (cursor > 0) { memmove(&line[cursor - 1], &line[cursor], pos - cursor); cursor--; pos--; line[pos] = 0; redraw_line(); } continue; } // Ctrl-U: clear line if (ch == 0x15) { pos = cursor = 0; line[0] = 0; redraw_line(); continue; } // Ctrl-L: redraw if (ch == 0x0C) { redraw_line(); 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; redraw_line(); } continue; } // Printable ASCII: insert at cursor if (isprint(ch)) { if (pos + 1 < CLI_LINE_MAX) { if (in_history()) { hist_view = -1; /* keep text */ } memmove(&line[cursor + 1], &line[cursor], pos - cursor); line[cursor++] = (char)ch; pos++; line[pos] = 0; redraw_line(); } continue; } // ignore other control bytes } // Prevent task from returning (FreeRTOS requirement) while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } } /* ================== 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, 512, 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)); printf("\nESP32-S3 GPIO Tester with PWM (ESP-IDF)\n"); printf("Type 'help' for commands. Advanced editing supported.\n"); print_test_safe_pins(); xTaskCreatePinnedToCore(console_task, "console", 4096, NULL, tskIDLE_PRIORITY + 2, NULL, tskNO_AFFINITY); vTaskDelete(NULL); }
Flash from Arduino IDE. Use the ESP32 board package by Espressif, pick ESP32S3 Dev Module, pick 115200 baud in Serial Monitor.
#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); } }
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.