Entorno: VSCode + PlatformIO · Framework: ESP-IDF
Microcontrolador: ESP32-S3 N16R8 DevKit-C (PDF) Chip: Xtensa LX7 dual-core · 16 MB Flash · 8 MB PSRAM OPI · Wi-Fi + BT 5.0 · USB-OTG nativo

Para hacer:

  • Revisar el documento
  • Poner las consideraciones de más abajo en dónde corresponde
  • Corregir lo que esté mal
  • Sustituir anglicismos raros por palabras en español
  • Corregir la forma de hablar de la IA, usar lenguaje impersonal como si fuera un informe
  • Investigar y agregar uso de IA con ESP32 ✅ 2026-03-18
  • Completar 3.5 Funciones
  • Agregar glosario ✅ 2026-03-18
  • Corregir enlaces del índice ✅ 2026-03-18

Advertencia

Este documento está hecho todo con IA, a medida que lo lea voy a ir cambiando, corrigiendo y mejorándolo.


Consideraciones

Sobre el LED integrado: el WS2812B en GPIO 48 usa el periférico RMT para generar los pulsos de ~400 ns con precisión nanosegundos — no es possible manejarlo con gpio_set_level directo como en Arduino con NeoPixel sin librería. El componente espressif/led_strip abstrae todo eso. Sobre el mando Xbox: el ejemplo usa NimBLE como host BLE central. Lo más delicado aquí es que el formato del reporte HID varía entre versiones de firmware del mando. El struct xbox_report_t cubre el caso más común, pero si los valores salen extraños, lo correcto es hacer un dump raw del reporte (ESP_LOG_BUFFER_HEX) y mapear los bytes manualmente comparando con el protocolo documentado. Sobre USB-OTG: el loop principal necesita llamar tud_task() frecuentemente (o en una tarea dedicada a alta prioridad). Si lo pones en una tarea separada con while(1){ tud_task(); vTaskDelay(1); } puedes separarlo del resto de la lógica limpiamente. Sobre WebSockets + Android: la combinación más práctica para control en red local es el servidor web con WebSocket (sección 13) y abrirlo en una WebView nativa de Android — te da una interfaz completa sin escribir código Java/Kotlin para la lógica de UI.


Tabla de contenido

  1. De Arduino a ESP-IDF: el cambio de paradigma
  2. Tipos de datos en C y ESP-IDF
  3. Control de flujo
  4. Bucles
  5. GPIO
  6. Wi-Fi
  7. HTTP: Cliente y Servidor Web Embebido
  8. Bluetooth LE: HID y control con mando Xbox
  9. ESP-NOW: comunicación directa entre chips
  10. NVS: almacenamiento permanente de datos
  11. USB-OTG: simulación de dispositivos USB
  12. Control desde una app Android
  13. Servidor web alojado en el chip
  14. Referencia rápida de periféricos del ESP32-S3 N16R8
  15. Glosario

1. De Arduino a ESP-IDF: el cambio de paradigma

setup() + loop()app_main() + tareas FreeRTOS

En Arduino el runtime llama a setup() una vez y luego a loop() en un bucle infinito sobre el hilo principal. En ESP-IDF no existe ese runtime de Arduino: el punto de entrada es app_main(), que se ejecuta como una tarea FreeRTOS de alto nivel. Cuando app_main() retorna, la tarea muere — por eso habitualmente se coloca al final un bucle infinito o se delega el trabajo a otras tareas y se suspende.

// Arduino
void setup() { /* inicialización */ }
void loop()  { /* lógica repetida */ }
 
// ESP-IDF
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
 
void app_main(void) {
    // inicialización
 
    while (1) {
        // lógica repetida
        vTaskDelay(pdMS_TO_TICKS(10));  // cede el procesador (≈ yield)
    }
}

vTaskDelay en lugar de delay(). pdMS_TO_TICKS(ms) convierte milisegundos a ticks del scheduler. Nunca uses bucles de espera activa (while(condicion){}) sin ceder el CPU — hambreas al scheduler y puedes disparar el watchdog.

Configuración de platformio.ini

[env:esp32-s3-devkitc-1]
platform  = espressif32
board     = esp32-s3-devkitc-1
framework = espidf
 
; N16R8: 16 MB Flash OPI, 8 MB PSRAM OPI
board_build.flash_size        = 16MB
board_build.arduino.partitions = default_16MB.csv
board_build.cmake_extra_args  = -DCONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
board_build.psram_type        = opi
 
; Puerto del USB-UART (el de programación — el puerto con chip CP2102/CH340)
upload_port  = /dev/ttyUSB0   ; Linux. En Windows: COM3, etc.
monitor_port = /dev/ttyUSB0
monitor_speed = 115200

Sistema de logging

#include "esp_log.h"
 
static const char *TAG = "MI_APP";  // etiqueta para filtrar logs en el monitor
 
void app_main(void) {
    ESP_LOGI(TAG, "Inicio — valor: %d", 42);   // Info    (verde)
    ESP_LOGW(TAG, "Advertencia");               // Warning (amarillo)
    ESP_LOGE(TAG, "Error crítico");             // Error   (rojo)
    ESP_LOGD(TAG, "Debug detallado");           // Debug   (desactivado por defecto)
}

ESP_LOG* añade timestamp, nivel y tag automáticamente. Es el equivalente de Serial.println() pero con estructura y filtrado por nivel.

Manejo de errores con esp_err_t

La mayor parte de la API de ESP-IDF devuelve esp_err_t (un int32_t typedef). El macro ESP_ERROR_CHECK lanza un abort si el valor no es ESP_OK, útil durante desarrollo:

#include "esp_err.h"
 
esp_err_t resultado = alguna_funcion_espidf();
 
if (resultado != ESP_OK) {
    ESP_LOGE(TAG, "Falló con: %s", esp_err_to_name(resultado));
}
 
// O de forma compacta (aborta en error — para desarrollo):
ESP_ERROR_CHECK(alguna_funcion_espidf());

2. Tipos de datos en C y ESP-IDF

2.1 Tipos nativos de C

TipoTamaño típicoEtimología / significado del nombre
char1 bytecharacter — originalmente para almacenar caracteres ASCII
short2 bytesshort integer — entero “corto”
int4 bytes (en ESP32)integer — entero
long4 bytes (en ESP32)long integer — entero “largo”
long long8 bytesentero de longitud doble larga
float4 bytesfloating point — punto flotante (precisión simple)
double8 bytesdouble precision — doble precisión
voidsin tamañovoid = “vacío”; indica ausencia de tipo/valor
_Bool / bool1 byteboolean (via <stdbool.h>) — booleano

El calificador unsigned elimina el signo, duplicando el rango positivo:

signed   char  s = -5;     // -128  .. 127
unsigned char  u = 250;    //    0  .. 255

Paralelo con Arduino: en Arduino usas byte (alias de uint8_t), word (alias de uint16_t), boolean (alias de bool). En C puro no existen esos aliases salvo que los definas tú o incluyas <Arduino.h>.

2.2 Tipos de ancho fijo: <stdint.h>

Resuelven el problema de que el tamaño de int varía entre plataformas (2 bytes en AVR, 4 en ARM/Xtensa). La convención del nombre es:

[u] int [N] _t
 │       │   └── _t = "typedef" (convención POSIX para tipos derivados)
 │       └────── N bits: 8, 16, 32, 64
 └────────────── 'u' = unsigned; omitido = signed
#include <stdint.h>
 
int8_t   a = -100;          // signed  8-bit  → -128 .. 127
uint8_t  b = 200;           // unsigned 8-bit →    0 .. 255
int16_t  c = -30000;
uint16_t d = 60000;
int32_t  e = -2000000000L;
uint32_t f = 4000000000UL;
int64_t  g = -9000000000000LL;
uint64_t h = 18000000000000ULL;

Regla práctica en ESP-IDF: usa siempre tipos de ancho fijo para registros de hardware, protocolos y buffers. Usa int solo para contadores de bucles y lógica de propósito general donde el tamaño exacto no importa.

2.3 Tipos específicos de ESP-IDF

ESP-IDF define sus propios typedefs semánticos para mayor legibilidad:

TipoDefinición internaSignificado del nombre
esp_err_tint32_tESP error type — código de retorno de funciones
gpio_num_tint (enum)GPIO number type — número de pin GPIO
gpio_mode_tenummodo del GPIO (entrada/salida/etc.)
gpio_pull_mode_tenummodo del pull-up/down interno
TickType_tuint32_ttipo de tick del scheduler FreeRTOS
BaseType_tint32_ttipo base de FreeRTOS (retorno de funciones del RTOS)
UBaseType_tuint32_tversión unsigned del anterior
TaskHandle_tpuntero opacoreferencia a una tarea FreeRTOS
QueueHandle_tpuntero opacoreferencia a una cola FreeRTOS
SemaphoreHandle_tpuntero opacoreferencia a un semáforo FreeRTOS
nvs_handle_tuint32_thandle al namespace NVS
esp_netif_t*puntero a structinterfaz de red (Wi-Fi, Ethernet)

2.4 Calificadores de tipo relevantes en embedded

// volatile: indica al compilador que el valor puede cambiar externamente
// (ISR, hardware). Impide optimizaciones que cacheen el valor en registro.
volatile uint32_t contador_isr = 0;
 
// const: valor inmutable. El compilador puede ubicarlo en Flash (ROM).
const uint8_t TABLA_SENOS[256] = { /* ... */ };
 
// static (en función): persiste entre llamadas (≈ variable global local).
void contar(void) {
    static uint32_t veces = 0;
    veces++;
    ESP_LOGI("CNT", "Llamado %lu veces", veces);
}
 
// static (en archivo): limita la visibilidad al archivo de compilación.
// Es buena práctica para variables y funciones que no deben ser globales.
static const char *TAG = "MODULO_X";

2.5 Punteros — en contexto embedded

Los punteros son omnipresentes en ESP-IDF porque las API trabajan con buffers y estructuras pasadas por referencia:

void rellenar_buffer(uint8_t *buf, size_t len, uint8_t valor) {
    for (size_t i = 0; i < len; i++) {
        buf[i] = valor;    // equivalente a *(buf + i) = valor
    }
}
 
void app_main(void) {
    uint8_t datos[64];
    rellenar_buffer(datos, sizeof(datos), 0xFF);
 
    // La flecha '->' desreferencia y accede al campo en un solo paso.
    // 'ptr->campo' es equivalente a '(*ptr).campo'
    // Esto es muy común en los callbacks de ESP-IDF:
    //   ip_event_got_ip_t *ev = (ip_event_got_ip_t *)event_data;
    //   ESP_LOGI(TAG, "IP: " IPSTR, IP2STR(&ev->ip_info.ip));
}

2.6 Structs y enums

// Struct: agrupación de campos heterogéneos
typedef struct {
    char     ssid[32];
    char     password[64];
    uint8_t  canal;
    bool     activo;
} config_wifi_t;
 
config_wifi_t mi_wifi = {
    .ssid     = "MiRed",       // inicialización por nombre de campo (C99)
    .password = "clave123",
    .canal    = 6,
    .activo   = true
};
 
// Enum: conjunto de constantes enteras con nombre
typedef enum {
    ESTADO_IDLE = 0,
    ESTADO_CONECTANDO,
    ESTADO_CONECTADO,
    ESTADO_ERROR
} estado_wifi_t;
 
estado_wifi_t estado = ESTADO_IDLE;

3. Control de flujo

3.1 if / else if / else

int temperatura = leer_sensor();
 
if (temperatura > 80) {
    ESP_LOGW(TAG, "Temperatura crítica: %d°C", temperatura);
    apagar_sistema();
} else if (temperatura > 60) {
    ESP_LOGI(TAG, "Temperatura elevada");
    activar_ventilador();
} else {
    ESP_LOGI(TAG, "Temperatura normal");
}

3.2 Operador ternario

// condicion ? valor_si_verdadero : valor_si_falso
const char *estado_str = (temperatura > 60) ? "CALIENTE" : "NORMAL";
uint8_t nivel_pwm = (activo) ? 255 : 0;

3.3 switch / case

Ideal para máquinas de estado, que son un patrón central en firmware:

typedef enum { ST_IDLE, ST_WIFI, ST_HTTP, ST_SLEEP } estado_t;
estado_t estado = ST_IDLE;
 
void procesar_estado(void) {
    switch (estado) {
        case ST_IDLE:
            ESP_LOGI(TAG, "Esperando...");
            estado = ST_WIFI;
            break;  // SIN break → fallthrough (deliberado o bug, según contexto)
 
        case ST_WIFI:
            conectar_wifi();
            estado = ST_HTTP;
            break;
 
        case ST_HTTP:
            enviar_datos();
            estado = ST_SLEEP;
            break;
 
        case ST_SLEEP:
            esp_deep_sleep(10 * 1000000ULL);  // 10 s en microsegundos
            break;
 
        default:
            ESP_LOGE(TAG, "Estado desconocido: %d", estado);
            estado = ST_IDLE;
            break;
    }
}

Fallthrough deliberado — cuando múltiples casos comparten lógica:

switch (cmd) {
    case CMD_RESET:
    case CMD_REBOOT:      // ambos caen aquí sin break entre ellos
        reiniciar();
        break;
    case CMD_OTA:
        iniciar_ota();
        break;
}

3.4 goto

Raramente recomendable, pero tiene un uso idiomático en C para manejo de errores con limpieza de recursos — evita anidar if profundamente en funciones de inicialización:

esp_err_t inicializar_todo(void) {
    esp_err_t ret;
 
    ret = paso_a();
    if (ret != ESP_OK) goto error_a;
 
    ret = paso_b();
    if (ret != ESP_OK) goto error_b;
 
    ret = paso_c();
    if (ret != ESP_OK) goto error_c;
 
    return ESP_OK;
 
error_c: limpiar_b();
error_b: limpiar_a();
error_a:
    ESP_LOGE(TAG, "Inicialización falló: %s", esp_err_to_name(ret));
    return ret;
}

3.5 Funciones

Estructura

La estructura básica de una función es la siguiente:

retorno nombre(argumentos) {
 cuerpo
}

Por ejemplo:

int suma(int a, int b){
 return a + b;
}

Declaración y definición

  • Declaración (Prototipo): Avisa al compilador que la función existe. Se suele poner arriba del app_main o en archivos de cabecera (.h).

    • Ejemplo: int sumar(int a, int b);
  • Definición: Es el código real (la implemetación) de la función.


4. Bucles

4.1 for

// Forma canónica
for (int i = 0; i < 10; i++) { /* ... */ }
 
// Múltiples variables (C99)
for (int i = 0, j = 9; i < j; i++, j--) { /* ... */ }
 
// Sin cuerpo — busca la primera posición con valor 0
uint8_t buf[64] = { /* ... */ };
int pos;
for (pos = 0; pos < 64 && buf[pos] != 0; pos++);
// pos ahora apunta al primer cero, o vale 64 si no encontró ninguno
 
// Bucle infinito
for (;;) {
    vTaskDelay(pdMS_TO_TICKS(100));
}

4.2 while

// Condición evaluada ANTES de cada iteración
int intentos = 0;
while (!wifi_conectado() && intentos < 20) {
    vTaskDelay(pdMS_TO_TICKS(500));
    intentos++;
}
 
// Bucle infinito — el más idiomático en app_main y tareas FreeRTOS
while (1) {
    procesar_eventos();
    vTaskDelay(pdMS_TO_TICKS(10));
}

4.3 do...while

La condición se evalúa DESPUÉS — garantiza al menos una ejecución:

int reintentos;
do {
    reintentos++;
    resultado = intentar_conexion();
} while (resultado != ESP_OK && reintentos < 5);

4.4 break y continue

for (int i = 0; i < 100; i++) {
    if (i == 50) break;      // sale del bucle inmediatamente
    if (i % 2 == 0) continue; // salta al siguiente ciclo (solo impares pasan)
    ESP_LOGI(TAG, "Impar: %d", i);
}

4.5 Bucles con FreeRTOS — tareas concurrentes

En ESP-IDF el patrón habitual NO es meter todo en app_main. Se crean tareas independientes, cada una con su propio stack y prioridad:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
 
// Cada tarea es una función que contiene un bucle infinito
void tarea_sensores(void *params) {
    while (1) {
        float temp = leer_temperatura();
        ESP_LOGI("SENSOR", "Temp: %.1f°C", temp);
        vTaskDelay(pdMS_TO_TICKS(2000));  // espera 2 s cediendo el CPU
    }
    vTaskDelete(NULL);  // solo se llega aquí si se sale del while (no habitual)
}
 
void tarea_led(void *params) {
    while (1) {
        gpio_set_level(GPIO_NUM_2, 1);
        vTaskDelay(pdMS_TO_TICKS(500));
        gpio_set_level(GPIO_NUM_2, 0);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}
 
void app_main(void) {
    // xTaskCreate(funcion, nombre, stack_bytes, param, prioridad, handle_out)
    xTaskCreate(tarea_sensores, "sensores", 4096, NULL, 5, NULL);
    xTaskCreate(tarea_led,      "led",      2048, NULL, 3, NULL);
    // app_main puede retornar; las tareas siguen corriendo en el scheduler
}

5. GPIO — El LED integrado y más

El LED integrado del ESP32-S3-DevKitC-1

El DevKit-C v1.1 lleva un LED RGB WS2812B (LED inteligente de un solo hilo de datos) en el GPIO 48. No es un LED simple — es un LED addressable controlado por un protocolo de pulsos temporizados de alta precisión (análogo a los NeoPixel de Arduino).

¿Qué más puedes hacer con ese pin GPIO 48?

  • Conectar una tira de LEDs WS2812 en cadena — el DevKit actúa como primer nodo
  • Usarlo como GPIO digital genérico (salida/entrada estándar), perdiendo la retroalimentación visual del LED del kit
  • Conectar sensores de 1 hilo como el DHT22 con librería adaptada
  • Generar señales para control de servos o cualquier señal digital timing-sensitiva mediante RMT

5.1 Driver RMT para WS2812 (LED del DevKit)

ESP-IDF usa el periférico RMT (Remote Control Transceiver) para generar los pulsos de precisión nanosegundos que requiere el WS2812.

Dependencia en idf_component.yml (crea este archivo en la raíz del proyecto):

dependencies:
  espressif/led_strip: "^2.5.0"
#include "led_strip.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
 
#define LED_GPIO    GPIO_NUM_48
#define LED_COUNT   1            // solo 1 LED en el DevKit
 
static const char *TAG = "LED_RGB";
static led_strip_handle_t led_strip;
 
void init_led(void) {
    led_strip_config_t strip_cfg = {
        .strip_gpio_num   = LED_GPIO,
        .max_leds         = LED_COUNT,
        .led_pixel_format = LED_PIXEL_FORMAT_GRB,  // WS2812 usa orden GRB
        .led_model        = LED_MODEL_WS2812,
        .flags.invert_out = false,
    };
 
    led_strip_rmt_config_t rmt_cfg = {
        .clk_src        = RMT_CLK_SRC_DEFAULT,
        .resolution_hz  = 10 * 1000 * 1000,   // 10 MHz de resolución
        .flags.with_dma = false,
    };
 
    ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_cfg, &rmt_cfg, &led_strip));
    led_strip_clear(led_strip);  // apaga el LED al iniciar
}
 
void set_color(uint8_t r, uint8_t g, uint8_t b) {
    led_strip_set_pixel(led_strip, 0, r, g, b);
    led_strip_refresh(led_strip);
}
 
void app_main(void) {
    init_led();
 
    // Ciclo de colores
    const struct { uint8_t r, g, b; const char *nombre; } colores[] = {
        {255,   0,   0, "Rojo"},
        {  0, 255,   0, "Verde"},
        {  0,   0, 255, "Azul"},
        {255, 255,   0, "Amarillo"},
        {  0, 255, 255, "Cian"},
        {255,   0, 255, "Magenta"},
        {255, 255, 255, "Blanco"},
        {  0,   0,   0, "Apagado"},
    };
 
    while (1) {
        for (int i = 0; i < 8; i++) {
            ESP_LOGI(TAG, "%s", colores[i].nombre);
            set_color(colores[i].r, colores[i].g, colores[i].b);
            vTaskDelay(pdMS_TO_TICKS(800));
        }
    }
}

5.2 GPIO digital genérico (salida / entrada con pull)

#include "driver/gpio.h"
 
#define PIN_LED    GPIO_NUM_2   // cualquier GPIO externo
#define PIN_BTN    GPIO_NUM_0   // botón BOOT del DevKit
 
void configurar_gpio(void) {
    // --- Salida ---
    gpio_config_t out_cfg = {
        .pin_bit_mask = (1ULL << PIN_LED),
        .mode         = GPIO_MODE_OUTPUT,
        .pull_up_en   = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    gpio_config(&out_cfg);
 
    // --- Entrada con pull-up interno ---
    gpio_config_t in_cfg = {
        .pin_bit_mask = (1ULL << PIN_BTN),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,    // botón activo en LOW (nivel 0)
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    gpio_config(&in_cfg);
}
 
void app_main(void) {
    configurar_gpio();
 
    while (1) {
        if (gpio_get_level(PIN_BTN) == 0) {    // presionado → LOW
            gpio_set_level(PIN_LED, 1);
        } else {
            gpio_set_level(PIN_LED, 0);
        }
        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

5.3 GPIO con interrupciones (ISR)

El patrón recomendado: la ISR solo enqueue un evento en una cola FreeRTOS; una tarea normal procesa el evento. Esto evita hacer trabajo costoso dentro de la ISR.

#include "driver/gpio.h"
#include "freertos/queue.h"
 
static QueueHandle_t gpio_evt_queue;
 
// IRAM_ATTR: la función se coloca en RAM (no Flash) para ejecución rápida
static void IRAM_ATTR gpio_isr_handler(void *arg) {
    uint32_t gpio_num = (uint32_t)arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
 
void tarea_gpio(void *params) {
    uint32_t gpio_num;
    while (1) {
        if (xQueueReceive(gpio_evt_queue, &gpio_num, portMAX_DELAY)) {
            ESP_LOGI("ISR", "Interrupción en GPIO %lu", gpio_num);
        }
    }
}
 
void app_main(void) {
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
 
    gpio_config_t cfg = {
        .pin_bit_mask = (1ULL << GPIO_NUM_0),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
        .intr_type    = GPIO_INTR_NEGEDGE,  // flanco de bajada
    };
    gpio_config(&cfg);
 
    gpio_install_isr_service(0);
    gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void*)GPIO_NUM_0);
 
    xTaskCreate(tarea_gpio, "gpio_task", 2048, NULL, 10, NULL);
}

5.4 PWM con LEDC

El periférico LEDC (LED Control) genera PWM de hardware en cualquier pin GPIO:

#include "driver/ledc.h"
 
#define LEDC_TIMER      LEDC_TIMER_0
#define LEDC_MODE       LEDC_LOW_SPEED_MODE
#define LEDC_OUTPUT_IO  GPIO_NUM_4
#define LEDC_CHANNEL    LEDC_CHANNEL_0
#define LEDC_DUTY_RES   LEDC_TIMER_13_BIT  // resolución: 0..8191
#define LEDC_FREQUENCY  5000               // Hz
 
void init_pwm(void) {
    ledc_timer_config_t timer = {
        .speed_mode      = LEDC_MODE,
        .timer_num       = LEDC_TIMER,
        .duty_resolution = LEDC_DUTY_RES,
        .freq_hz         = LEDC_FREQUENCY,
        .clk_cfg         = LEDC_AUTO_CLK,
    };
    ledc_timer_config(&timer);
 
    ledc_channel_config_t ch = {
        .speed_mode = LEDC_MODE,
        .channel    = LEDC_CHANNEL,
        .timer_sel  = LEDC_TIMER,
        .intr_type  = LEDC_INTR_DISABLE,
        .gpio_num   = LEDC_OUTPUT_IO,
        .duty       = 0,
        .hpoint     = 0,
    };
    ledc_channel_config(&ch);
}
 
void set_pwm_duty(uint8_t pct) {
    uint32_t duty = (pct * 8191) / 100;
    ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
    ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
 
void app_main(void) {
    init_pwm();
 
    // Fade suave 0→100→0 (control de brillo, velocidad de motor, etc.)
    while (1) {
        for (int i = 0; i <= 100; i++) { set_pwm_duty(i); vTaskDelay(pdMS_TO_TICKS(10)); }
        for (int i = 100; i >= 0; i--) { set_pwm_duty(i); vTaskDelay(pdMS_TO_TICKS(10)); }
    }
}

5.5 ADC (lectura analógica)

#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
 
void app_main(void) {
    adc_oneshot_unit_handle_t adc1;
    adc_oneshot_unit_init_cfg_t init_cfg = { .unit_id = ADC_UNIT_1 };
    adc_oneshot_new_unit(&init_cfg, &adc1);
 
    adc_oneshot_chan_cfg_t ch_cfg = {
        .bitwidth = ADC_BITWIDTH_12,   // 0..4095
        .atten    = ADC_ATTEN_DB_12,   // rango 0..3.3V (aprox)
    };
    adc_oneshot_config_channel(adc1, ADC_CHANNEL_0, &ch_cfg);  // GPIO 1
 
    // Calibración para linealizar la curva del ADC
    adc_cali_handle_t cali;
    adc_cali_curve_fitting_config_t cali_cfg = {
        .unit_id  = ADC_UNIT_1,
        .chan     = ADC_CHANNEL_0,
        .atten    = ADC_ATTEN_DB_12,
        .bitwidth = ADC_BITWIDTH_12,
    };
    adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali);
 
    while (1) {
        int raw, mv;
        adc_oneshot_read(adc1, ADC_CHANNEL_0, &raw);
        adc_cali_raw_to_voltage(cali, raw, &mv);
        ESP_LOGI("ADC", "Raw: %d | Voltaje: %d mV", raw, mv);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

Nota: ADC2 tiene restricciones cuando Wi-Fi está activo. Preferir ADC1 (GPIO 1-10) siempre que sea posible.


6. Wi-Fi

6.1 Conexión como Station (cliente)

Patrón canónico usando el sistema de eventos de ESP-IDF:

#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "freertos/event_groups.h"
 
#define WIFI_SSID      "TuRedWiFi"
#define WIFI_PASS      "TuContraseña"
#define WIFI_MAX_RETRY 10
 
static const char *TAG = "WIFI";
static EventGroupHandle_t wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1
static int s_retry = 0;
 
static void wifi_event_handler(void *arg, esp_event_base_t base,
                               int32_t event_id, void *event_data) {
    if (base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry < WIFI_MAX_RETRY) {
            esp_wifi_connect();
            s_retry++;
            ESP_LOGW(TAG, "Reintento %d/%d", s_retry, WIFI_MAX_RETRY);
        } else {
            xEventGroupSetBits(wifi_event_group, WIFI_FAIL_BIT);
        }
    } else if (base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *ev = (ip_event_got_ip_t *)event_data;
        ESP_LOGI(TAG, "IP obtenida: " IPSTR, IP2STR(&ev->ip_info.ip));
        s_retry = 0;
        xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
}
 
void wifi_init_sta(void) {
    wifi_event_group = xEventGroupCreate();
 
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
 
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
 
    esp_event_handler_instance_t inst_wifi, inst_ip;
    esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                        &wifi_event_handler, NULL, &inst_wifi);
    esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                        &wifi_event_handler, NULL, &inst_ip);
 
    wifi_config_t wifi_cfg = {
        .sta = {
            .ssid     = WIFI_SSID,
            .password = WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
    ESP_ERROR_CHECK(esp_wifi_start());
 
    EventBits_t bits = xEventGroupWaitBits(wifi_event_group,
                        WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                        pdFALSE, pdFALSE, portMAX_DELAY);
 
    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "Conectado a %s", WIFI_SSID);
    } else {
        ESP_LOGE(TAG, "No se pudo conectar a %s", WIFI_SSID);
    }
}
 
void app_main(void) {
    ESP_ERROR_CHECK(nvs_flash_init());  // NVS requerido por el stack Wi-Fi
    wifi_init_sta();
}

6.2 Modo Access Point (AP)

void wifi_init_ap(void) {
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_ap();
 
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
 
    wifi_config_t ap_cfg = {
        .ap = {
            .ssid           = "ESP32-S3-AP",
            .ssid_len       = strlen("ESP32-S3-AP"),
            .channel        = 1,
            .password       = "esp32pass",
            .max_connection = 4,
            .authmode       = WIFI_AUTH_WPA2_PSK,
        },
    };
    esp_wifi_set_mode(WIFI_MODE_AP);
    esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
    esp_wifi_start();
 
    ESP_LOGI(TAG, "AP iniciado. SSID: ESP32-S3-AP | IP gateway: 192.168.4.1");
}

7. HTTP: Cliente y Servidor Web Embebido

7.1 Cliente HTTP GET

#include "esp_http_client.h"
#include <string.h>
 
#define BUFFER_SIZE 2048
static char response_buf[BUFFER_SIZE];
static int  response_len = 0;
 
esp_err_t http_event_handler(esp_http_client_event_t *evt) {
    switch (evt->event_id) {
        case HTTP_EVENT_ON_DATA:
            if (!esp_http_client_is_chunked_response(evt->client)) {
                int copy = (response_len + evt->data_len < BUFFER_SIZE)
                           ? evt->data_len : BUFFER_SIZE - response_len - 1;
                memcpy(response_buf + response_len, evt->data, copy);
                response_len += copy;
                response_buf[response_len] = '\0';
            }
            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGI("HTTP", "Respuesta completa (%d bytes)", response_len);
            break;
        default: break;
    }
    return ESP_OK;
}
 
void hacer_get(const char *url) {
    response_len = 0;
    esp_http_client_config_t config = {
        .url           = url,
        .event_handler = http_event_handler,
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_err_t err = esp_http_client_perform(client);
 
    if (err == ESP_OK) {
        ESP_LOGI("HTTP", "Status: %d | Body: %s",
                 esp_http_client_get_status_code(client), response_buf);
    }
    esp_http_client_cleanup(client);
}
 
// Ejemplo real: consultar la API de clima Open-Meteo
void app_main(void) {
    nvs_flash_init();
    wifi_init_sta();
    hacer_get("http://api.open-meteo.com/v1/forecast"
              "?latitude=40.4&longitude=-3.7&current_weather=true");
}

7.2 Cliente HTTP POST (enviar datos JSON)

void hacer_post(const char *url, const char *json_body) {
    esp_http_client_config_t config = { .url = url };
    esp_http_client_handle_t client = esp_http_client_init(&config);
 
    esp_http_client_set_method(client, HTTP_METHOD_POST);
    esp_http_client_set_header(client, "Content-Type", "application/json");
    esp_http_client_set_post_field(client, json_body, strlen(json_body));
 
    esp_err_t err = esp_http_client_perform(client);
    if (err == ESP_OK) {
        ESP_LOGI("HTTP", "POST OK — status: %d",
                 esp_http_client_get_status_code(client));
    }
    esp_http_client_cleanup(client);
}
 
void ejemplo_post(void) {
    float temp = 25.3f;
    char payload[128];
    snprintf(payload, sizeof(payload),
             "{\"dispositivo\":\"esp32s3\",\"temperatura\":%.1f}", temp);
    hacer_post("http://mi-servidor.local/api/datos", payload);
}

8. Bluetooth LE: HID y control con mando Xbox

8.1 Conceptos BLE relevantes

Los mandos Xbox One S (firmware 2019+) y Xbox Series X/S usan Bluetooth LE HID (Human Interface Device). El ESP32-S3 puede actuar como BLE HID Host para recibir sus reportes de botones y ejes.

El flujo es:

  1. Scan → descubrir dispositivos BLE cercanos
  2. Connect → establecer conexión GATT
  3. Discover services → buscar el servicio HID (UUID estándar 0x1812)
  4. Enable notifications en la característica de HID Report (UUID 0x2A4D)
  5. Recibir reportes → decodificar botones, ejes y gatillos

ESP-IDF ≥ 5.0 usa NimBLE como stack BLE por defecto (más liviano que Bluedroid).

8.2 Activar BLE en platformio.ini

board_build.cmake_extra_args =
    -DCONFIG_BT_ENABLED=y
    -DCONFIG_BT_BLE_ENABLED=y
    -DCONFIG_BT_NIMBLE_ENABLED=y
    -DCONFIG_BT_NIMBLE_ROLE_CENTRAL=y

8.3 Ejemplo completo: Host BLE para mando Xbox

/*
 * ESP32-S3 como BLE HID Host para mando Xbox
 *
 * Pasos de uso:
 *   1. Pon el mando en modo pairing: mantén el botón Xbox hasta que parpadee rápido
 *   2. El ESP32-S3 escanea, lo detecta por nombre ("Xbox Controller")
 *   3. Se conecta automáticamente y empieza a recibir reportes
 *
 * Los valores de joystick/gatillos se pueden mapear directamente a GPIO/PWM
 * para controlar motores, servos, robots, etc.
 */
 
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "host/ble_gattc.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_log.h"
#include <string.h>
#include <stdlib.h>  // abs()
 
static const char *TAG = "XBOX_BLE";
 
static uint16_t conn_handle        = BLE_HS_CONN_HANDLE_NONE;
static uint16_t report_val_handle  = 0;
 
/* ------------------------------------------------------------------
 * Estructura de reporte HID del mando Xbox
 * Nota: el formato varía por versión de firmware. Este cubre el caso
 * más común para Xbox One S / Series X con BLE.
 * ------------------------------------------------------------------ */
typedef struct __attribute__((packed)) {
    uint8_t  report_id;
    uint16_t buttons;
    uint8_t  trigger_l;
    uint8_t  trigger_r;
    int16_t  thumb_lx;
    int16_t  thumb_ly;
    int16_t  thumb_rx;
    int16_t  thumb_ry;
} xbox_report_t;
 
#define BTN_A       (1 << 0)
#define BTN_B       (1 << 1)
#define BTN_X       (1 << 3)
#define BTN_Y       (1 << 4)
#define BTN_LB      (1 << 6)
#define BTN_RB      (1 << 7)
#define BTN_MENU    (1 << 11)
#define BTN_VIEW    (1 << 10)
#define DEADZONE    3000        // zona muerta para joysticks
 
/* ------------------------------------------------------------------
 * Procesa cada reporte recibido del mando
 * ------------------------------------------------------------------ */
static void procesar_reporte(const uint8_t *data, uint16_t len) {
    if (len < sizeof(xbox_report_t)) return;
    const xbox_report_t *rep = (const xbox_report_t *)data;
    if (rep->report_id != 0x01) return;
 
    if (rep->buttons & BTN_A)    ESP_LOGI(TAG, "[A]");
    if (rep->buttons & BTN_B)    ESP_LOGI(TAG, "[B]");
    if (rep->buttons & BTN_X)    ESP_LOGI(TAG, "[X]");
    if (rep->buttons & BTN_Y)    ESP_LOGI(TAG, "[Y]");
    if (rep->buttons & BTN_LB)   ESP_LOGI(TAG, "[LB]");
    if (rep->buttons & BTN_RB)   ESP_LOGI(TAG, "[RB]");
    if (rep->buttons & BTN_MENU) ESP_LOGI(TAG, "[Menu]");
 
    if (rep->trigger_l > 10)
        ESP_LOGI(TAG, "LT: %d", (rep->trigger_r * 100) / 255);
 
    if (abs(rep->thumb_lx) > DEADZONE || abs(rep->thumb_ly) > DEADZONE)
        ESP_LOGI(TAG, "L-Stick  X:%d  Y:%d", rep->thumb_lx, rep->thumb_ly);
    if (abs(rep->thumb_rx) > DEADZONE || abs(rep->thumb_ry) > DEADZONE)
        ESP_LOGI(TAG, "R-Stick  X:%d  Y:%d", rep->thumb_rx, rep->thumb_ry);
 
    /*
     * EJEMPLO DE APLICACIÓN: control diferencial de robot
     *
     * int vel_base = rep->thumb_ly / 256;  // -128..127
     * int giro     = rep->thumb_lx / 256;
     * int motor_l  = vel_base + giro;
     * int motor_r  = vel_base - giro;
     * set_motor(MOTOR_L, constrain(motor_l, -127, 127));
     * set_motor(MOTOR_R, constrain(motor_r, -127, 127));
     */
}
 
/* ------------------------------------------------------------------
 * Descubrimiento GATT y habilitación de notificaciones
 * ------------------------------------------------------------------ */
static int chars_cb(uint16_t conn_h, const struct ble_gatt_error *err,
                    const struct ble_gatt_chr *chr, void *arg) {
    if (err->status != 0 || chr == NULL) {
        if (report_val_handle && err->status != 0) {
            // Habilita notificaciones escribiendo en el CCCD (val_handle + 1)
            uint8_t val[2] = {0x01, 0x00};
            struct os_mbuf *om = ble_hs_mbuf_from_flat(val, 2);
            ble_gattc_write_no_rsp(conn_h, report_val_handle + 1, om);
            ESP_LOGI(TAG, "Notificaciones HID habilitadas");
        }
        return 0;
    }
    if (chr->uuid.u16.value == 0x2A4D) {  // HID Report UUID
        report_val_handle = chr->val_handle;
        ESP_LOGI(TAG, "HID Report en handle 0x%04X", report_val_handle);
    }
    return 0;
}
 
static int svcs_cb(uint16_t conn_h, const struct ble_gatt_error *err,
                   const struct ble_gatt_svc *svc, void *arg) {
    if (err->status != 0 || svc == NULL) return 0;
    if (svc->uuid.u16.value == 0x1812) {  // HID Service UUID
        ESP_LOGI(TAG, "Servicio HID encontrado");
        ble_gattc_disc_all_chrs(conn_h, svc->start_handle,
                                svc->end_handle, chars_cb, NULL);
    }
    return 0;
}
 
/* ------------------------------------------------------------------
 * Eventos GAP: scan, conexión, desconexión, notificaciones
 * ------------------------------------------------------------------ */
static int gap_cb(struct ble_gap_event *ev, void *arg) {
    switch (ev->type) {
        case BLE_GAP_EVENT_DISC: {
            struct ble_hs_adv_fields fields;
            ble_hs_adv_parse_fields(&fields, ev->disc.data, ev->disc.length_data);
            if (fields.name && fields.name_len > 0) {
                char name[33] = {0};
                memcpy(name, fields.name, fields.name_len < 32 ? fields.name_len : 32);
                if (strstr(name, "Xbox") || strstr(name, "Controller")) {
                    ESP_LOGI(TAG, "Mando encontrado: %s — conectando...", name);
                    ble_gap_disc_cancel();
                    ble_gap_connect(BLE_OWN_ADDR_PUBLIC, &ev->disc.addr,
                                    5000, NULL, gap_cb, NULL);
                }
            }
            break;
        }
        case BLE_GAP_EVENT_CONNECT:
            if (ev->connect.status == 0) {
                conn_handle = ev->connect.conn_handle;
                ESP_LOGI(TAG, "Conectado (handle=%d) — descubriendo servicios...",
                         conn_handle);
                ble_gattc_disc_all_svcs(conn_handle, svcs_cb, NULL);
            }
            break;
        case BLE_GAP_EVENT_DISCONNECT:
            ESP_LOGW(TAG, "Desconectado — reiniciando scan...");
            conn_handle = BLE_HS_CONN_HANDLE_NONE;
            report_val_handle = 0;
            ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER,
                         &(struct ble_gap_disc_params){.passive=0,.filter_duplicates=1},
                         gap_cb, NULL);
            break;
        case BLE_GAP_EVENT_NOTIFY_RX: {
            uint8_t buf[sizeof(xbox_report_t) + 4];
            uint16_t len = OS_MBUF_PKTLEN(ev->notify_rx.om);
            if (len > sizeof(buf)) len = sizeof(buf);
            os_mbuf_copydata(ev->notify_rx.om, 0, len, buf);
            procesar_reporte(buf, len);
            break;
        }
        default: break;
    }
    return 0;
}
 
static void on_sync(void) {
    ESP_LOGI(TAG, "BLE listo — iniciando scan...");
    ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER,
                 &(struct ble_gap_disc_params){.passive=0,.filter_duplicates=1},
                 gap_cb, NULL);
}
 
static void nimble_task(void *p) { nimble_port_run(); nimble_port_freertos_deinit(); }
 
void app_main(void) {
    ESP_ERROR_CHECK(nvs_flash_init());
    nimble_port_init();
    ble_hs_cfg.sync_cb = on_sync;
    ble_svc_gap_device_name_set("ESP32-S3-XboxHost");
    nimble_port_freertos_init(nimble_task);
    ESP_LOGI(TAG, "Pon el mando Xbox en modo pairing (mantén Xbox hasta que parpadee)");
}

9. ESP-NOW: comunicación directa entre chips

ESP-NOW es el protocolo propietario de Espressif para comunicación directa chip-a-chip. Sin Wi-Fi AP, latencia ~1 ms, hasta 250 bytes por paquete, alcance hasta ~480 m en campo abierto.

Nodo emisor

#include "esp_now.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "nvs_flash.h"
 
static const char *TAG = "ESPNOW_TX";
 
// MAC del nodo receptor (obtenla con `esp_wifi_get_mac` o leyendo el sticker del chip)
static uint8_t peer_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
 
// Estructura de datos — debe ser idéntica en emisor y receptor
typedef struct {
    uint32_t timestamp;
    float    temperatura;
    float    humedad;
    uint8_t  flags;
} sensor_data_t;
 
static void send_cb(const uint8_t *mac, esp_now_send_status_t status) {
    ESP_LOGI(TAG, "Envío a " MACSTR ": %s",
             MAC2STR(mac), status == ESP_NOW_SEND_SUCCESS ? "OK" : "FAIL");
}
 
void espnow_init(void) {
    esp_netif_init();
    esp_event_loop_create_default();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_storage(WIFI_STORAGE_RAM);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();
 
    esp_now_init();
    esp_now_register_send_cb(send_cb);
 
    esp_now_peer_info_t peer = {
        .channel = 0,
        .ifidx   = WIFI_IF_STA,
        .encrypt = false,
    };
    memcpy(peer.peer_addr, peer_mac, 6);
    esp_now_add_peer(&peer);
}
 
void app_main(void) {
    nvs_flash_init();
    espnow_init();
 
    sensor_data_t datos;
    uint32_t t = 0;
 
    while (1) {
        datos.timestamp   = t++;
        datos.temperatura = 22.5f + (esp_random() % 100) / 100.0f;
        datos.humedad     = 55.0f;
        datos.flags       = 0x01;
 
        esp_now_send(peer_mac, (uint8_t *)&datos, sizeof(datos));
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Nodo receptor

#include "esp_now.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "freertos/queue.h"
 
static const char *TAG = "ESPNOW_RX";
static QueueHandle_t rx_queue;
 
typedef struct {
    uint32_t timestamp;
    float    temperatura;
    float    humedad;
    uint8_t  flags;
} sensor_data_t;
 
static void recv_cb(const esp_now_recv_info_t *info,
                    const uint8_t *data, int len) {
    if (len != sizeof(sensor_data_t)) return;
    sensor_data_t buf;
    memcpy(&buf, data, sizeof(buf));
    xQueueSendFromISR(rx_queue, &buf, NULL);
}
 
void tarea_rx(void *p) {
    sensor_data_t d;
    while (1) {
        if (xQueueReceive(rx_queue, &d, portMAX_DELAY)) {
            ESP_LOGI(TAG, "[T=%lu] Temp=%.1f°C | Hum=%.1f%%",
                     d.timestamp, d.temperatura, d.humedad);
            if (d.temperatura > 30.0f) {
                ESP_LOGW(TAG, "¡Temperatura elevada! Activando alarma");
            }
        }
    }
}
 
void app_main(void) {
    nvs_flash_init();
    rx_queue = xQueueCreate(10, sizeof(sensor_data_t));
 
    // Init Wi-Fi igual que el emisor
    esp_netif_init();
    esp_event_loop_create_default();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();
 
    esp_now_init();
    esp_now_register_recv_cb(recv_cb);
 
    xTaskCreate(tarea_rx, "espnow_rx", 4096, NULL, 5, NULL);
    ESP_LOGI(TAG, "Receptor ESP-NOW listo");
}

Topologías útiles:

  • Point-to-point: un emisor, un receptor (ejemplo anterior)
  • Star: un maestro, hasta 20 esclavos (máx. peers registrados)
  • Broadcast: envía a FF:FF:FF:FF:FF:FF — todos los chips en el canal lo reciben sin necesidad de registrarse como peer

10. NVS: almacenamiento permanente de datos

NVS = Non-Volatile Storage. Sistema de almacenamiento clave-valor en Flash, análogo a Preferences de Arduino o EEPROM. Sobrevive reinicios y cortes de energía. Soporta tipos escalares, strings y blobs binarios.

#include "nvs_flash.h"
#include "nvs.h"
 
static const char *TAG    = "NVS";
static const char *NVS_NS = "config";   // namespace — como un directorio
 
/* -------- Escritura -------- */
void nvs_guardar_config(const char *ssid, const char *pass, uint8_t brillo) {
    nvs_handle_t h;
    ESP_ERROR_CHECK(nvs_open(NVS_NS, NVS_READWRITE, &h));
 
    ESP_ERROR_CHECK(nvs_set_str(h, "ssid",     ssid));
    ESP_ERROR_CHECK(nvs_set_str(h, "password", pass));
    ESP_ERROR_CHECK(nvs_set_u8(h,  "brillo",   brillo));
 
    // Contador de arranques
    int32_t arranques = 0;
    nvs_get_i32(h, "arranques", &arranques);
    nvs_set_i32(h, "arranques", arranques + 1);
 
    ESP_ERROR_CHECK(nvs_commit(h));  // flush a Flash — obligatorio
    nvs_close(h);
    ESP_LOGI(TAG, "Config guardada (arranque #%ld)", arranques + 1);
}
 
/* -------- Lectura -------- */
void nvs_leer_config(void) {
    nvs_handle_t h;
    esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &h);
    if (err != ESP_OK) {
        ESP_LOGW(TAG, "NVS vacío — primer arranque");
        return;
    }
 
    char ssid[32] = {0}, pass[64] = {0};
    size_t s1 = sizeof(ssid), s2 = sizeof(pass);
    uint8_t brillo = 128;
    int32_t arranques = 0;
 
    if (nvs_get_str(h, "ssid",     ssid, &s1)    == ESP_OK) ESP_LOGI(TAG, "SSID: %s", ssid);
    if (nvs_get_str(h, "password", pass, &s2)    == ESP_OK) ESP_LOGI(TAG, "Pass: %s", pass);
    nvs_get_u8(h,  "brillo",    &brillo);
    nvs_get_i32(h, "arranques", &arranques);
    ESP_LOGI(TAG, "Brillo: %d | Arranques: %ld", brillo, arranques);
    nvs_close(h);
}
 
/* -------- Struct completa como blob binario -------- */
typedef struct {
    float    offset_temp;
    uint16_t intervalo_ms;
    uint8_t  modo;
} calibracion_t;
 
void nvs_guardar_calibracion(const calibracion_t *cal) {
    nvs_handle_t h;
    nvs_open(NVS_NS, NVS_READWRITE, &h);
    nvs_set_blob(h, "calibracion", cal, sizeof(calibracion_t));
    nvs_commit(h);
    nvs_close(h);
}
 
void nvs_leer_calibracion(calibracion_t *cal) {
    calibracion_t defaults = { .offset_temp = 0.0f, .intervalo_ms = 1000, .modo = 1 };
    *cal = defaults;
 
    nvs_handle_t h;
    if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return;
    size_t size = sizeof(calibracion_t);
    nvs_get_blob(h, "calibracion", cal, &size);
    nvs_close(h);
}
 
void app_main(void) {
    // Init NVS — borra si hay corrupción o cambio de versión de partición
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
        ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_LOGW(TAG, "NVS corrupto — borrando y reiniciando...");
        nvs_flash_erase();
        nvs_flash_init();
    }
 
    nvs_leer_config();                             // intenta leer (puede estar vacío)
    nvs_guardar_config("MiRed", "clave123", 200);  // guarda (o sobreescribe)
    nvs_leer_config();                             // verifica
 
    calibracion_t cal;
    nvs_leer_calibracion(&cal);
    ESP_LOGI(TAG, "Calibración: offset=%.2f intervalo=%d ms",
             cal.offset_temp, cal.intervalo_ms);
}

Partición NVS personalizada para proyectos con más datos: añade una línea en partitions.csv:

nvs_user, data, nvs, 0x310000, 0x100000   ; 1 MB

Y accede con nvs_open_from_partition("nvs_user", "mi_ns", ...).


11. USB-OTG: simulación de dispositivos USB

El ESP32-S3 incluye un controlador USB Full-Speed 12 Mbps nativo. El segundo conector USB-C del DevKit (marcado USB) está conectado directamente a este periférico — sin chip UART intermedio.

ESP-IDF usa TinyUSB como stack USB. Clases disponibles:

Clase USBDescripciónCasos de uso
HIDHuman Interface DeviceTeclado, ratón, gamepad, joystick
CDC-ACMSerial virtualPuerto COM sin driver adicional
MSCMass StoragePendrive virtual (SPIFFS, SD Card)
MIDIInstrumento musicalControlador MIDI
AudioDispositivo de audioMicrófono, altavoz virtual
DFUDevice Firmware UpgradeActualización de firmware por USB
# platformio.ini — agrega a cmake_extra_args:
-DCONFIG_TINYUSB_ENABLED=y
-DCONFIG_TINYUSB_HID_ENABLED=y
-DCONFIG_TINYUSB_CDC_ENABLED=y

11.1 Emulación de teclado USB

/*
 * El ESP32-S3 aparece como teclado HID estándar.
 * Al presionar el botón BOOT, escribe "Hola ESP32-S3!" + Enter en el PC.
 * Compatible con Windows, Linux, macOS sin instalar drivers.
 */
 
#include "tinyusb.h"
#include "class/hid/hid_device.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
 
static const char *TAG = "USB_KBD";
 
static const uint8_t hid_report_descriptor[] = {
    TUD_HID_REPORT_DESC_KEYBOARD()
};
 
static const tusb_desc_device_t descriptor_dev = {
    .bLength            = sizeof(tusb_desc_device_t),
    .bDescriptorType    = TUSB_DESC_DEVICE,
    .bcdUSB             = 0x0200,
    .bDeviceClass       = 0x00,
    .bDeviceSubClass    = 0x00,
    .bDeviceProtocol    = 0x00,
    .bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,
    .idVendor           = 0x303A,   // Espressif VID
    .idProduct          = 0x4004,
    .bcdDevice          = 0x0100,
    .iManufacturer      = 0x01,
    .iProduct           = 0x02,
    .iSerialNumber      = 0x03,
    .bNumConfigurations = 0x01,
};
 
static const char *string_descriptors[] = {
    "\x09\x04",
    "Espressif",
    "ESP32-S3 Keyboard",
    "123456",
};
 
static const tinyusb_config_t tusb_cfg = {
    .device_descriptor       = &descriptor_dev,
    .string_descriptor       = string_descriptors,
    .string_descriptor_count = 4,
    .external_phy            = false,
};
 
/* Callbacks obligatorios de TinyUSB */
uint8_t const *tud_hid_descriptor_report_cb(uint8_t itf) {
    return hid_report_descriptor;
}
uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id,
                                hid_report_type_t type, uint8_t *buf, uint16_t len) {
    return 0;
}
void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t type,
                            const uint8_t *buf, uint16_t bufsize) {
    if (bufsize > 0) {
        uint8_t leds = buf[0];
        ESP_LOGI(TAG, "LEDs: NumLk=%d CapsLk=%d", !!(leds & 1), !!(leds & 2));
    }
}
 
/* Envía un carácter (tabla ASCII→HID simplificada para a-z, 0-9, espacio, Enter) */
static void enviar_char(char c) {
    uint8_t keycode = 0, modifier = 0;
 
    if      (c >= 'a' && c <= 'z') keycode = HID_KEY_A + (c - 'a');
    else if (c >= 'A' && c <= 'Z') { keycode = HID_KEY_A + (c - 'A'); modifier = KEYBOARD_MODIFIER_LEFTSHIFT; }
    else if (c >= '1' && c <= '9') keycode = HID_KEY_1 + (c - '1');
    else if (c == '0')  keycode = HID_KEY_0;
    else if (c == ' ')  keycode = HID_KEY_SPACE;
    else if (c == '!')  { keycode = HID_KEY_1; modifier = KEYBOARD_MODIFIER_LEFTSHIFT; }
    else if (c == '\n') keycode = HID_KEY_RETURN;
 
    if (keycode && tud_hid_ready()) {
        uint8_t keys[6] = {keycode};
        tud_hid_keyboard_report(0, modifier, keys);
        vTaskDelay(pdMS_TO_TICKS(30));
        tud_hid_keyboard_report(0, 0, NULL);
        vTaskDelay(pdMS_TO_TICKS(30));
    }
}
 
static void escribir_texto(const char *txt) {
    for (int i = 0; txt[i]; i++) enviar_char(txt[i]);
}
 
void app_main(void) {
    gpio_config_t btn = {
        .pin_bit_mask = (1ULL << GPIO_NUM_0),
        .mode         = GPIO_MODE_INPUT,
        .pull_up_en   = GPIO_PULLUP_ENABLE,
    };
    gpio_config(&btn);
 
    ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
    ESP_LOGI(TAG, "Teclado USB listo. Presiona BOOT para escribir.");
 
    while (1) {
        tud_task();
 
        if (gpio_get_level(GPIO_NUM_0) == 0) {
            vTaskDelay(pdMS_TO_TICKS(50));  // debounce
            if (gpio_get_level(GPIO_NUM_0) == 0) {
                escribir_texto("Hola ESP32-S3!\n");
                while (gpio_get_level(GPIO_NUM_0) == 0) vTaskDelay(pdMS_TO_TICKS(10));
            }
        }
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

11.2 Emulación de gamepad USB genérico

/*
 * Gamepad USB genérico: 2 ejes analógicos, 8 botones.
 * Compatible con Windows/Linux/macOS como DirectInput sin drivers.
 * Uso: conecta un joystick físico al ADC del ESP32-S3.
 */
 
#include "tinyusb.h"
#include "class/hid/hid_device.h"
#include "esp_adc/adc_oneshot.h"
#include "driver/gpio.h"
 
/* Descriptor HID para gamepad personalizado */
static const uint8_t gamepad_report_desc[] = {
    0x05, 0x01, 0x09, 0x05, 0xA1, 0x01,  // Usage Page: Generic Desktop, Usage: Gamepad
    0x85, 0x01,                            // Report ID 1
    // 8 botones (1 bit cada uno)
    0x05, 0x09, 0x19, 0x01, 0x29, 0x08, 0x15, 0x00, 0x25, 0x01,
    0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
    // 2 ejes X/Y (signed 8-bit)
    0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F,
    0x75, 0x08, 0x95, 0x02, 0x81, 0x02,
    0xC0
};
 
typedef struct __attribute__((packed)) {
    uint8_t buttons;
    int8_t  axis_x;
    int8_t  axis_y;
} gamepad_report_t;
 
uint8_t const *tud_hid_descriptor_report_cb(uint8_t itf) { return gamepad_report_desc; }
uint16_t tud_hid_get_report_cb(uint8_t i, uint8_t id, hid_report_type_t t,
                                uint8_t *b, uint16_t l) { return 0; }
void tud_hid_set_report_cb(uint8_t i, uint8_t id, hid_report_type_t t,
                            const uint8_t *b, uint16_t l) {}
 
void app_main(void) {
    // Init ADC para el joystick físico
    adc_oneshot_unit_handle_t adc1;
    adc_oneshot_new_unit(&(adc_oneshot_unit_init_cfg_t){.unit_id=ADC_UNIT_1}, &adc1);
    adc_oneshot_chan_cfg_t ch = {.bitwidth=ADC_BITWIDTH_12, .atten=ADC_ATTEN_DB_12};
    adc_oneshot_config_channel(adc1, ADC_CHANNEL_0, &ch);  // eje X → GPIO 1
    adc_oneshot_config_channel(adc1, ADC_CHANNEL_1, &ch);  // eje Y → GPIO 2
 
    // Botones en GPIO
    for (int p : (int[]){GPIO_NUM_5, GPIO_NUM_6, GPIO_NUM_7, GPIO_NUM_8}) {
        gpio_config(&(gpio_config_t){
            .pin_bit_mask = (1ULL << p),
            .mode         = GPIO_MODE_INPUT,
            .pull_up_en   = GPIO_PULLUP_ENABLE,
        });
    }
 
    tinyusb_driver_install(&(tinyusb_config_t){.external_phy=false});
 
    while (1) {
        tud_task();
        if (tud_hid_ready()) {
            int raw_x, raw_y;
            adc_oneshot_read(adc1, ADC_CHANNEL_0, &raw_x);
            adc_oneshot_read(adc1, ADC_CHANNEL_1, &raw_y);
 
            gamepad_report_t rep = {
                .axis_x  = (int8_t)((raw_x - 2048) / 16),
                .axis_y  = (int8_t)((raw_y - 2048) / 16),
                .buttons = ((!gpio_get_level(GPIO_NUM_5)) << 0) |
                           ((!gpio_get_level(GPIO_NUM_6)) << 1) |
                           ((!gpio_get_level(GPIO_NUM_7)) << 2) |
                           ((!gpio_get_level(GPIO_NUM_8)) << 3),
            };
            tud_hid_report(1, &rep, sizeof(rep));
        }
        vTaskDelay(pdMS_TO_TICKS(8));  // ~125 Hz de reporte
    }
}

11.3 Puerto serie virtual CDC-ACM

/*
 * El ESP32-S3 aparece como un puerto COM/ttyACM en el PC.
 * Útil para debug independiente del puerto UART de programación,
 * o para comunicación bidireccional con aplicaciones de PC.
 */
 
#include "tinyusb.h"
#include "tusb_cdc_acm.h"
 
static uint8_t cdc_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1];
 
void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
    size_t rx_size = 0;
    if (tinyusb_cdcacm_read(itf, cdc_buf, sizeof(cdc_buf)-1, &rx_size) == ESP_OK) {
        cdc_buf[rx_size] = '\0';
        ESP_LOGI("CDC", "← PC: %s", cdc_buf);
        // Eco de vuelta al PC
        tinyusb_cdcacm_write_queue(itf, cdc_buf, rx_size);
        tinyusb_cdcacm_write_flush(itf, 0);
    }
}
 
void app_main(void) {
    tinyusb_driver_install(&(tinyusb_config_t){.external_phy=false});
 
    tinyusb_config_cdcacm_t cdc = {
        .usb_dev          = TINYUSB_USBDEV_0,
        .cdc_port         = TINYUSB_CDC_ACM_0,
        .rx_unread_buf_sz = 64,
        .callback_rx      = tinyusb_cdc_rx_callback,
    };
    tusb_cdc_acm_init(&cdc);
 
    while (1) {
        char msg[64];
        snprintf(msg, sizeof(msg), "Uptime: %llu ms\r\n",
                 esp_timer_get_time() / 1000);
        tinyusb_cdcacm_write_queue(0, (uint8_t *)msg, strlen(msg));
        tinyusb_cdcacm_write_flush(0, 0);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

12. Control desde una app Android

Tres enfoques, de menor a mayor infraestructura requerida:

Enfoque A — BLE GATT (recomendado para control directo)

El ESP32-S3 expone un servicio GATT personalizado. La app Android escribe características para enviar comandos y suscribe notificaciones para recibir datos en tiempo real.

/*
 * ESP32-S3 como periférico BLE GATT
 *
 * Service UUID 0x1234:
 *   Characteristic 0x1235 (Write)  → recibe "R,G,B" o "ON"/"OFF" para controlar el LED
 *   Characteristic 0x1236 (Notify) → envía temperatura como string cada 2 s
 *
 * En Android: MIT App Inventor con componente BluetoothLE
 *   - ConnectWithAddress(mac_del_esp32)
 *   - WriteStringValue(0x1234, 0x1235, "255,0,0")
 *   - RegisterForStringValues(0x1234, 0x1236) → evento StringValuesChanged
 */
 
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "services/gap/ble_svc_gap.h"
#include "esp_log.h"
#include <string.h>
#include <stdio.h>
 
static const char *TAG = "BLE_PERIPH";
static uint16_t conn_notif  = BLE_HS_CONN_HANDLE_NONE;
static uint16_t temp_handle;
 
static int cmd_cb(uint16_t conn_h, uint16_t attr_h,
                  struct ble_gatt_access_ctxt *ctxt, void *arg) {
    if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
        char buf[32] = {0};
        uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
        if (len > 31) len = 31;
        os_mbuf_copydata(ctxt->om, 0, len, buf);
 
        int r, g, b;
        if (sscanf(buf, "%d,%d,%d", &r, &g, &b) == 3) {
            set_color(r, g, b);
            ESP_LOGI(TAG, "Android → Color R:%d G:%d B:%d", r, g, b);
        } else if (strcmp(buf, "ON")  == 0) set_color(255, 255, 255);
        else if   (strcmp(buf, "OFF") == 0) set_color(0, 0, 0);
    }
    return 0;
}
 
static const struct ble_gatt_svc_def gatt_svcs[] = {
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID16_DECLARE(0x1234),
        .characteristics = (struct ble_gatt_chr_def[]) {
            {
                .uuid      = BLE_UUID16_DECLARE(0x1235),
                .access_cb = cmd_cb,
                .flags     = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
            },
            {
                .uuid       = BLE_UUID16_DECLARE(0x1236),
                .access_cb  = NULL,
                .val_handle = &temp_handle,
                .flags      = BLE_GATT_CHR_F_NOTIFY,
            },
            { 0 }
        },
    },
    { 0 }
};
 
void tarea_notif(void *p) {
    while (1) {
        if (conn_notif != BLE_HS_CONN_HANDLE_NONE) {
            char s[16];
            snprintf(s, sizeof(s), "%.1f", 23.5f);  // usa sensor real aquí
            struct os_mbuf *om = ble_hs_mbuf_from_flat(s, strlen(s));
            ble_gattc_notify_custom(conn_notif, temp_handle, om);
        }
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}
 
static int gap_cb(struct ble_gap_event *ev, void *arg) {
    if (ev->type == BLE_GAP_EVENT_CONNECT && ev->connect.status == 0) {
        conn_notif = ev->connect.conn_handle;
    } else if (ev->type == BLE_GAP_EVENT_DISCONNECT) {
        conn_notif = BLE_HS_CONN_HANDLE_NONE;
        // Re-advertise para aceptar nueva conexión
        ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
                          &(struct ble_gap_adv_params){
                              .conn_mode=BLE_GAP_CONN_MODE_UND,
                              .disc_mode=BLE_GAP_DISC_MODE_GEN},
                          gap_cb, NULL);
    }
    return 0;
}
 
static void on_sync(void) {
    ble_svc_gap_device_name_set("ESP32-S3-Control");
    ble_gatts_count_cfg(gatt_svcs);
    ble_gatts_add_svcs(gatt_svcs);
    ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
                      &(struct ble_gap_adv_params){
                          .conn_mode=BLE_GAP_CONN_MODE_UND,
                          .disc_mode=BLE_GAP_DISC_MODE_GEN},
                      gap_cb, NULL);
    ESP_LOGI(TAG, "Anunciando como 'ESP32-S3-Control'");
}
 
static void ble_task(void *p) { nimble_port_run(); nimble_port_freertos_deinit(); }
 
void app_main(void) {
    nvs_flash_init();
    init_led();
    nimble_port_init();
    ble_hs_cfg.sync_cb = on_sync;
    ble_svc_gap_init();
    ble_svc_gatt_init();
    nimble_port_freertos_init(ble_task);
    xTaskCreate(tarea_notif, "notif", 2048, NULL, 5, NULL);
}

Enfoque B — Wi-Fi + REST API

La app Android hace peticiones HttpURLConnection o usa Retrofit al servidor HTTP del ESP32-S3 (ver Sección 13). Ventaja: sin emparejamiento, funciona desde cualquier dispositivo en la red.

Enfoque C — WebSockets

Para control en tiempo real con baja latencia sobre Wi-Fi. ESP-IDF ≥ 5.0 soporta WebSocket nativo en esp_http_server (ejemplo en la sección siguiente).


13. Servidor web alojado en el chip

El ESP32-S3 sirve una interfaz HTML/CSS/JS completa con WebSockets para datos en tiempo real. No se necesita ningún servidor externo.

/*
 * Servidor web completo con:
 *   GET  /           → interfaz HTML con controles RGB y datos en tiempo real
 *   GET  /api/status → JSON con uptime, heap libre, temperatura
 *   POST /api/led    → {"r":R,"g":G,"b":B} para controlar el LED
 *   POST /api/reset  → reinicia el chip
 *   WS   /ws         → WebSocket bidireccional para datos en tiempo real
 */
 
#include "esp_http_server.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "mdns.h"
#include "esp_log.h"
#include "cJSON.h"
#include <string.h>
 
static const char *TAG = "WEBSERVER";
static httpd_handle_t servidor = NULL;
static int ws_fds[4] = {-1, -1, -1, -1};
 
/* HTML inline — en proyectos grandes usa SPIFFS para servir archivos */
static const char *HTML =
"<!DOCTYPE html><html lang='es'>"
"<head><meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>ESP32-S3</title>"
"<style>"
"body{font-family:sans-serif;max-width:500px;margin:0 auto;padding:20px;background:#1a1a2e;color:#eee}"
"h1{color:#e94560;text-align:center}.card{background:#16213e;border-radius:12px;padding:20px;margin:15px 0}"
"input[type=range]{width:100%;accent-color:#e94560}"
"button{background:#e94560;color:#fff;border:none;padding:10px 20px;border-radius:8px;cursor:pointer;margin:5px}"
"button:hover{background:#c73652}"
"#colorBox{width:100%;height:60px;border-radius:8px;margin:10px 0;transition:background .3s}"
".row{display:flex;align-items:center;gap:10px;margin:8px 0}label{width:20px;font-weight:bold}"
"#estado{color:#4ecca3;font-family:monospace;font-size:.9em;white-space:pre-wrap}"
"</style></head>"
"<body>"
"<h1>&#127381; ESP32-S3 Control</h1>"
"<div class='card'>"
"<h2>LED RGB</h2>"
"<div id='colorBox'></div>"
"<div class='row'><label style='color:red'>R</label><input type='range' id='r' min=0 max=255 value=0 oninput='upd()'></div>"
"<div class='row'><label style='color:lime'>G</label><input type='range' id='g' min=0 max=255 value=0 oninput='upd()'></div>"
"<div class='row'><label style='color:dodgerblue'>B</label><input type='range' id='b' min=0 max=255 value=0 oninput='upd()'></div>"
"<div style='text-align:center'>"
"<button onclick='c(255,0,0)'>Rojo</button>"
"<button onclick='c(0,255,0)'>Verde</button>"
"<button onclick='c(0,0,255)'>Azul</button>"
"<button onclick='c(255,255,255)'>Blanco</button>"
"<button onclick='c(0,0,0)'>OFF</button>"
"</div></div>"
"<div class='card'>"
"<h2>Estado en tiempo real</h2>"
"<pre id='estado'>Conectando...</pre>"
"</div>"
"<div class='card' style='text-align:center'>"
"<button onclick='fetch(`/api/reset`,{method:`POST`})' style='background:#555'>Reiniciar chip</button>"
"</div>"
"<script>"
"let ws=new WebSocket('ws://'+location.host+'/ws');"
"ws.onmessage=e=>document.getElementById('estado').textContent=e.data;"
"ws.onclose=()=>setTimeout(()=>location.reload(),3000);"
"function upd(){"
"let r=document.getElementById('r').value,g=document.getElementById('g').value,b=document.getElementById('b').value;"
"document.getElementById('colorBox').style.background=`rgb(${r},${g},${b})`;"
"fetch('/api/led',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({r:+r,g:+g,b:+b})});"
"}"
"function c(r,g,b){document.getElementById('r').value=r;document.getElementById('g').value=g;document.getElementById('b').value=b;upd();}"
"</script></body></html>";
 
/* ---- Handlers ---- */
static esp_err_t h_raiz(httpd_req_t *req) {
    httpd_resp_set_type(req, "text/html");
    return httpd_resp_send(req, HTML, strlen(HTML));
}
 
static esp_err_t h_status(httpd_req_t *req) {
    cJSON *j = cJSON_CreateObject();
    cJSON_AddStringToObject(j, "chip", "ESP32-S3");
    cJSON_AddNumberToObject(j, "uptime_s",  esp_timer_get_time() / 1000000);
    cJSON_AddNumberToObject(j, "heap_free", esp_get_free_heap_size());
    cJSON_AddNumberToObject(j, "temp",      25.3);
    char *s = cJSON_Print(j);
    httpd_resp_set_type(req, "application/json");
    httpd_resp_send(req, s, strlen(s));
    free(s); cJSON_Delete(j);
    return ESP_OK;
}
 
static esp_err_t h_led(httpd_req_t *req) {
    char body[128]; int n = httpd_req_recv(req, body, sizeof(body)-1);
    if (n <= 0) return ESP_FAIL;
    body[n] = '\0';
    cJSON *j = cJSON_Parse(body);
    if (!j) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON inválido"); return ESP_FAIL; }
    set_color(cJSON_GetObjectItem(j,"r")->valueint,
              cJSON_GetObjectItem(j,"g")->valueint,
              cJSON_GetObjectItem(j,"b")->valueint);
    cJSON_Delete(j);
    httpd_resp_set_type(req, "application/json");
    return httpd_resp_send(req, "{\"ok\":true}", 11);
}
 
static esp_err_t h_reset(httpd_req_t *req) {
    httpd_resp_send(req, "{\"ok\":true}", 11);
    vTaskDelay(pdMS_TO_TICKS(500));
    esp_restart();
    return ESP_OK;
}
 
/* ---- WebSocket ---- */
static esp_err_t h_ws(httpd_req_t *req) {
    if (req->method == HTTP_GET) {
        int fd = httpd_req_to_sockfd(req);
        for (int i = 0; i < 4; i++) if (ws_fds[i] == -1) { ws_fds[i] = fd; break; }
        return ESP_OK;
    }
    httpd_ws_frame_t f = {.type=HTTPD_WS_TYPE_TEXT};
    uint8_t buf[64] = {0}; f.payload = buf;
    httpd_ws_recv_frame(req, &f, sizeof(buf));
    ESP_LOGI(TAG, "WS ← cliente: %s", buf);
    return ESP_OK;
}
 
static void ws_broadcast(const char *msg) {
    httpd_ws_frame_t f = {.type=HTTPD_WS_TYPE_TEXT, .payload=(uint8_t*)msg, .len=strlen(msg)};
    for (int i = 0; i < 4; i++) {
        if (ws_fds[i] != -1)
            if (httpd_ws_send_frame_async(servidor, ws_fds[i], &f) != ESP_OK)
                ws_fds[i] = -1;
    }
}
 
void tarea_ws(void *p) {
    char msg[128];
    while (1) {
        snprintf(msg, sizeof(msg),
                 "Uptime : %llu s\nHeap   : %lu B libres\nTemp   : %.1f°C",
                 esp_timer_get_time()/1000000, esp_get_free_heap_size(), 25.3f);
        ws_broadcast(msg);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
 
/* ---- Inicio del servidor ---- */
static const httpd_uri_t uris[] = {
    {"/",           HTTP_GET,  h_raiz,  .is_websocket=false},
    {"/api/status", HTTP_GET,  h_status,.is_websocket=false},
    {"/api/led",    HTTP_POST, h_led,   .is_websocket=false},
    {"/api/reset",  HTTP_POST, h_reset, .is_websocket=false},
    {"/ws",         HTTP_GET,  h_ws,    .is_websocket=true },
};
 
void iniciar_servidor(void) {
    httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
    cfg.max_open_sockets = 8;
    cfg.lru_purge_enable = true;
    ESP_ERROR_CHECK(httpd_start(&servidor, &cfg));
    for (int i = 0; i < 5; i++) httpd_register_uri_handler(servidor, &uris[i]);
    memset(ws_fds, -1, sizeof(ws_fds));
    xTaskCreate(tarea_ws, "ws_bcast", 4096, NULL, 5, NULL);
    ESP_LOGI(TAG, "Servidor listo en http://esp32s3.local/");
}
 
void app_main(void) {
    nvs_flash_init();
    init_led();
    wifi_init_sta();         // sección 6
 
    // mDNS: acceso por nombre en lugar de IP
    mdns_init();
    mdns_hostname_set("esp32s3");          // → http://esp32s3.local/
    mdns_instance_name_set("ESP32-S3 Panel");
    mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
 
    iniciar_servidor();
}

Resultado: abre el navegador (o una WebView en una app Android) y navega a http://esp32s3.local/. Los sliders RGB controlan el LED en tiempo real. El panel de estado se actualiza cada segundo por WebSocket sin polling.


14. Referencia rápida de periféricos del ESP32-S3 N16R8

PeriféricoHeader ESP-IDFNotas clave
GPIO digitaldriver/gpio.hTodos los pines; ISR disponible
PWM / LEDCdriver/ledc.h8 canales high-speed, cualquier GPIO
ADCesp_adc/adc_oneshot.hADC1 (GPIO 1-10), ADC2 con restricciones en Wi-Fi
I²C masterdriver/i2c_master.h2 buses, cualquier GPIO, 100/400/1000 kHz
SPI masterdriver/spi_master.hSPI2 y SPI3, hasta 80 MHz
UARTdriver/uart.h3 puertos UART
RMTdriver/rmt_tx.h + rmt_rx.hWS2812, IR, NEC, encoders
TWAI (CAN 2.0)driver/twai.hBus CAN estándar
Touchdriver/touch_sensor.hGPIO 1-14 tienen sensor capacitivo
Temp. internadriver/temp_sensor.hSensor de temperatura del die, ±1°C
Deep Sleepesp_sleep.h~10 µA; wake por GPIO, timer, ULP
Timer alta res.esp_timer.hResolución microsegundos
OTA remotaesp_ota_ops.hActualización de firmware por red
PSRAM extraesp_psram.h8 MB OPI para buffers grandes (video, audio, ML)
Sensor HallN/A en S3Solo en ESP32 original (no S3)

15. Glosario

SoC (Sistema en un Chip): Circuito integrado que contiene el microprocesador (arquitectura Xtensa LX7 de 32 bits y doble núcleo), los coprocesadores, periféricos de hardware y los transceptores de radio (Wi-Fi 4 y Bluetooth 5 de baja energía).

N16 (Memoria Flash de 16 MB): Memoria no volátil integrada en el empaquetado del módulo. Almacena el código ejecutable (firmware), el cargador de arranque y el sistema de archivos (como SPIFFS o LittleFS). Utiliza una interfaz SPI cuádruple (Quad SPI).

R8 (PSRAM de 8 MB): Memoria RAM pseudoestática externa integrada en el módulo. Proporciona memoria de trabajo adicional, esencial para cargas de trabajo que requieren mucha memoria, como el manejo de interfaces gráficas o buffers de audio. Utiliza una interfaz SPI octal (Octal SPI).

Módulo: Placa de circuito impreso soldable por montaje superficial que encapsula el SoC ESP32-S3, la memoria Flash, la PSRAM, el oscilador de cristal y el blindaje electromagnético.

Placa de Desarrollo (DevKit-C): Placa de circuito impreso base que aloja el módulo ESP32-S3, exponiendo sus pines a través de cabezales y añadiendo componentes para su alimentación, comunicación y reinicio manual.

LDO (Regulador de Baja Caída de Tensión): Componente de estado sólido en la placa de desarrollo que reduce los 5 voltios provenientes de las conexiones USB a los 3.3 voltios estrictamente requeridos para la lógica del módulo.

Puente USB a UART: Circuito integrado (generalmente CP2102, CH340 o CH343) conectado al primer puerto tipo C de la placa de desarrollo. Convierte el protocolo USB en un flujo de datos serie asíncrono (UART) conectado a los pines TX y RX del ESP32-S3 para la programación convencional y la depuración por consola.

USB Nativo (USB OTG / USB Serial JTAG): Controlador físico (PHY) integrado directamente en el silicio del ESP32-S3. El segundo puerto tipo C de la placa de desarrollo se conecta físicamente de forma directa a los pines GPIO 19 (D-) y GPIO 20 (D+). Permite la emulación de dispositivos (teclados, ratones, almacenamiento masivo), actuar como host USB o flashear el microcontrolador mediante hardware JTAG interno sin necesidad de un chip puente.

Botón EN (Habilitación / Reinicio): Interruptor táctil que conecta el pin de habilitación (CH_EN) del módulo a tierra. Al presionarlo, desactiva el regulador interno del chip, forzando un ciclo de encendido y un reinicio completo del sistema (Hard Reset).

Botón BOOT (Arranque): Interruptor táctil conectado al pin GPIO 0. Modifica el estado lógico de este pin. Si se presiona durante el ciclo de inicio (al soltar el botón EN o al conectar la placa), altera la secuencia de arranque, haciendo que la ROM de arranque entre en el modo de descarga serie, quedando a la espera de recibir un nuevo firmware en lugar de ejecutar el existente en la memoria Flash.

Pines de Configuración de Arranque (Strapping Pins): Conjunto de pines específicos (GPIO 0, 3, 45 y 46 en el ESP32-S3) cuyo estado lógico (alto o bajo) es muestreado por los registros de hardware en los milisegundos iniciales del arranque. Estos estados definen parámetros críticos de bajo nivel, como la fuente de arranque, el voltaje de la memoria Flash y los mensajes de depuración de la ROM.

GPIO (Pines de Entrada/Salida de Propósito General): Contactos expuestos a lo largo de los bordes de la placa de desarrollo. Permiten leer señales digitales y analógicas (mediante convertidores analógico-digital, ADC), generar modulación por ancho de pulsos (PWM) e implementar protocolos de comunicación por hardware como el circuito inter-integrado (I2C), interfaz periférica serie (SPI) y bus de red de área de controlador (TWAI/CAN).

Antena de Traza: Pista de cobre desnuda ubicada en el extremo libre del módulo, diseñada con una impedancia y geometría específicas para funcionar como un radiador de radiofrecuencia en la banda de 2.4 GHz. En algunas variantes, es reemplazada por un conector U.FL/IPEX para antenas externas.

Coprocesador de Ultra Baja Potencia (ULP): Núcleo secundario de muy bajo consumo diseñado para mantenerse activo durante los estados de suspensión profunda. Permite leer sensores externos (mediante convertidores analógico-digitales o interfaces inter-integradas) y despertar a los núcleos principales únicamente cuando se cumple una condición programada, ahorrando energía.

Fusibles Electrónicos (eFuses): Bloque de memoria programable una sola vez en el silicio. Almacena parámetros inalterables y críticos de fábrica, como la dirección de control de acceso al medio (MAC), datos de calibración de voltaje, y las claves criptográficas necesarias para habilitar el arranque seguro y el cifrado de la memoria de almacenamiento.

Aceleradores Criptográficos: Hardware dedicado que procesa operaciones de seguridad y cifrado (AES, SHA, RSA, HMAC, generador de números aleatorios reales) directamente en el silicio. Alivia a los núcleos principales de estas cargas matemáticas, acelerando la validación de certificados y el establecimiento de conexiones de red seguras.

Matriz de Contactos de Propósito General (GPIO Matrix): Arquitectura de enrutamiento lógico interno. Permite asignar de forma dinámica la entrada y salida de la mayoría de los periféricos internos (como controladores de comunicación serie, contadores, o modulación por ancho de pulsos) a casi cualquier terminal físico externo, brindando alta flexibilidad en el trazado de las pistas del circuito impreso.

Extensiones de Instrucciones Vectoriales (PIE): Conjunto de instrucciones adicionales incorporadas en la unidad lógico-aritmética del procesador principal. Optimizan el procesamiento simultáneo de múltiples flujos de datos, reduciendo drásticamente los ciclos de reloj necesarios para ejecutar cálculos matemáticos en redes neuronales, procesamiento de señales digitales y filtrado de sensores.

Cristal Oscilador (XTAL): Componente piezoeléctrico externo al chip pero integrado en el módulo (frecuentemente de 40 megahercios). Proporciona la frecuencia de reloj base estricta requerida para sincronizar todas las operaciones lógicas y de radiofrecuencia.

Lazo de Seguimiento de Fase (PLL): Circuito interno que toma la frecuencia base proporcionada por el cristal oscilador y la multiplica para generar la señal de reloj principal de alta velocidad (típicamente 240 megahercios) para los núcleos del procesador, así como las frecuencias derivadas para los diferentes buses de datos y periféricos.

Controlador de Acceso Directo a Memoria (DMA): Subsistema que administra la transferencia de bloques de datos de forma autónoma entre la memoria de trabajo y los periféricos (como la red inalámbrica o las interfaces de pantalla) sin requerir ciclos de instrucción de los núcleos principales.

Cubierta de Blindaje Electromagnético: Cápsula metálica soldada sobre los componentes críticos del módulo. Evita que la propia radiación de radiofrecuencia de la antena desestabilice al procesador o a las memorias, y bloquea el ruido eléctrico externo, asegurando el cumplimiento de las normativas de certificación de telecomunicaciones.

Memoria Caché: Memoria estática interna de muy alta velocidad y baja latencia, ubicada entre los núcleos de procesamiento y las memorias externas (Flash y PSRAM). Amortigua la diferencia de velocidad almacenando temporalmente las instrucciones y datos de acceso frecuente, evitando cuellos de botella al leer a través de los buses serie.

Sensor de Temperatura Interno: Circuito analógico embebido en el microcontrolador que mide la temperatura de la propia pastilla de silicio. Se utiliza exclusivamente para rutinas de protección y monitoreo térmico del procesador, disparando reducciones en la frecuencia de reloj para evitar daños por sobrecalentamiento.


Guía elaborada para ESP-IDF 5.x · ESP32-S3-DevKitC-1 v1.1 · PlatformIO