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.
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.
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 = espressif32board = esp32-s3-devkitc-1framework = espidf; N16R8: 16 MB Flash OPI, 8 MB PSRAM OPIboard_build.flash_size = 16MBboard_build.arduino.partitions = default_16MB.csvboard_build.cmake_extra_args = -DCONFIG_ESPTOOLPY_FLASHSIZE_16MB=yboard_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/ttyUSB0monitor_speed = 115200
Sistema de logging
#include "esp_log.h"static const char *TAG = "MI_APP"; // etiqueta para filtrar logs en el monitorvoid 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
Tipo
Tamaño típico
Etimología / significado del nombre
char
1 byte
character — originalmente para almacenar caracteres ASCII
short
2 bytes
short integer — entero “corto”
int
4 bytes (en ESP32)
integer — entero
long
4 bytes (en ESP32)
long integer — entero “largo”
long long
8 bytes
entero de longitud doble larga
float
4 bytes
floating point — punto flotante (precisión simple)
double
8 bytes
double precision — doble precisión
void
sin tamaño
void = “vacío”; indica ausencia de tipo/valor
_Bool / bool
1 byte
boolean (via <stdbool.h>) — booleano
El calificador unsigned elimina el signo, duplicando el rango positivo:
signed char s = -5; // -128 .. 127unsigned 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 .. 127uint8_t b = 200; // unsigned 8-bit → 0 .. 255int16_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:
Tipo
Definición interna
Significado del nombre
esp_err_t
int32_t
ESPerror type — código de retorno de funciones
gpio_num_t
int (enum)
GPIOnumber type — número de pin GPIO
gpio_mode_t
enum
modo del GPIO (entrada/salida/etc.)
gpio_pull_mode_t
enum
modo del pull-up/down interno
TickType_t
uint32_t
tipo de tick del scheduler FreeRTOS
BaseType_t
int32_t
tipo base de FreeRTOS (retorno de funciones del RTOS)
UBaseType_t
uint32_t
versión unsigned del anterior
TaskHandle_t
puntero opaco
referencia a una tarea FreeRTOS
QueueHandle_t
puntero opaco
referencia a una cola FreeRTOS
SemaphoreHandle_t
puntero opaco
referencia a un semáforo FreeRTOS
nvs_handle_t
uint32_t
handle al namespace NVS
esp_netif_t*
puntero a struct
interfaz 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éneostypedef 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 nombretypedef 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");}
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ónicafor (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 0uint8_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 infinitofor (;;) { vTaskDelay(pdMS_TO_TICKS(100));}
4.2 while
// Condición evaluada ANTES de cada iteraciónint 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 FreeRTOSwhile (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 infinitovoid 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):
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ápidastatic 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:
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:
Scan → descubrir dispositivos BLE cercanos
Connect → establecer conexión GATT
Discover services → buscar el servicio HID (UUID estándar 0x1812)
Enable notifications en la característica de HID Report (UUID 0x2A4D)
Recibir reportes → decodificar botones, ejes y gatillos
ESP-IDF ≥ 5.0 usa NimBLE como stack BLE por defecto (más liviano que Bluedroid).
/* * 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 receptortypedef 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)); }}
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.
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 USB
Descripción
Casos de uso
HID
Human Interface Device
Teclado, ratón, gamepad, joystick
CDC-ACM
Serial virtual
Puerto COM sin driver adicional
MSC
Mass Storage
Pendrive virtual (SPIFFS, SD Card)
MIDI
Instrumento musical
Controlador MIDI
Audio
Dispositivo de audio
Micrófono, altavoz virtual
DFU
Device Firmware Upgrade
Actualizació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
/* * 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.
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>🆕 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érico
Header ESP-IDF
Notas clave
GPIO digital
driver/gpio.h
Todos los pines; ISR disponible
PWM / LEDC
driver/ledc.h
8 canales high-speed, cualquier GPIO
ADC
esp_adc/adc_oneshot.h
ADC1 (GPIO 1-10), ADC2 con restricciones en Wi-Fi
I²C master
driver/i2c_master.h
2 buses, cualquier GPIO, 100/400/1000 kHz
SPI master
driver/spi_master.h
SPI2 y SPI3, hasta 80 MHz
UART
driver/uart.h
3 puertos UART
RMT
driver/rmt_tx.h + rmt_rx.h
WS2812, IR, NEC, encoders
TWAI (CAN 2.0)
driver/twai.h
Bus CAN estándar
Touch
driver/touch_sensor.h
GPIO 1-14 tienen sensor capacitivo
Temp. interna
driver/temp_sensor.h
Sensor de temperatura del die, ±1°C
Deep Sleep
esp_sleep.h
~10 µA; wake por GPIO, timer, ULP
Timer alta res.
esp_timer.h
Resolución microsegundos
OTA remota
esp_ota_ops.h
Actualización de firmware por red
PSRAM extra
esp_psram.h
8 MB OPI para buffers grandes (video, audio, ML)
Sensor Hall
N/A en S3
Solo 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