#include #include #include #include #include #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 [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 [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 [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 -> start blinking GPIO\n" " -> same as 'pin '\n" " delay -> set blink delay (%u..%u)\n" " pwm [brightness] -> start PWM (%d..%d, current: %u%%)\n" " brightness -> set PWM brightness (%d..%d)\n" " freq -> 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); }