domingo, 9 de agosto de 2026

Encender LED del ESP32-C3 desde página web

Muy buenas a todos y todas!!!

En este post quiero explicar como crear una pagina web dentro de nuestro ESP32C3.
La idea es un proyecto sencillo pero completo que te permita encender y apagar el LED integrado de tu ESP32-C3 desde cualquier navegador.

Como extra, en la pantalla integrada en el ESP32 también nos indicará el estado del LED On o Off

Voy a intentar explicar cada parte del programa por separado antes de ver el programa completo.


Sistema de archivos

El ESP32 dispone de varios métodos para almacenar una página web, desde una pequeña, como en este ejemplo, donde usaremos PROGMEM o usar la memoria Flash interna (unos 4MB) para almacenar el HTML, CSS o registros.

PROGMEM, LittleFS, SPIFFS o incluso las tarjetas SD son las opciones de almacenamiento más comunes:


Sistema de archivos Descripción Cuándo usarlo
PROGMEM Los archivos se incluyen como cadenas de texto en el código y se compilan junto con el programa. Para páginas pequeñas y estáticas que no van a cambiar. Es la opción más simple si solo tienes un archivo HTML pequeño.
LittleFS Un sistema de archivos en la flash, diseñado para ser ligero y resistente a cortes de energía. Es la opción más equilibrada para la mayoría de los proyectos. Soporta archivos, carpetas y es fácil de usar con un plugin en el IDE de Arduino.
Puedes ver un ejemplo de como usar LittleFS aquí.
SPIFFS El predecesor de LittleFS. También funciona en la flash, pero es menos eficiente y ya no se recomienda para nuevos proyectos. Para mantener compatibilidad con código antiguo. Se recomienda migrar a LittleFS.
Tarjeta SD Usa una tarjeta microSD externa conectada al ESP32 por SPI o SDIO. Para proyectos que necesitan almacenar muchos archivos (imágenes, vídeos, logs) o cuando el espacio en la flash es insuficiente.


🛠️ Componentes necesarios
ESP32-C3 con OLED de 0.42, Antena Cerámica, Wifi, Bluetooth
ESP32-C3OLED de 0.42" Placa de Desarrollo ESP32-C3 con OLED de 0.42", Antena Cerámica, Wifi, Bluetooth.

¿Qué necesitamos saber primero?

El LED integrado en placa o LED_BUILTIN

En la mayoría de las placas basadas en el ESP32-C3, el LED integrado está conectado al GPIO 8.

En algunas placas como la XIAO ESP32-C3, puede estar en el GPIO 21, consulta la documentación de tu placa para confirmar.

Nota: Este pin esta invertido, es decir que para apagar hay que ponerlo en estado HIGH y para encender el LED hay que ponerlo en LOW.


Librerías para el proyecto

Una librería Arduino es un paquete de código que simplifica la programación.
Además nos proporciona funciones y ejemplos para controlar hardware complejo como sensores, pantallas o motores sin necesidad de conocer los detalles técnicos internos.


Librería ¿Para qué sirve?
WiFi.h Permite que el ESP32-C3 se conecte a tu red o cree su propio punto de acceso
WebServer.h Crea un servidor HTTP en el ESP32-C3 para servir páginas web y recibir peticiones

Explicación del código

Paso 1: Incluir las librerías y definir credenciales


#include <WiFi.h>
#include <WebServer.h>
#include <U8g2lib.h>


// Credenciales de tu red Wi-Fi
const char* ssid = "TU_SSID";
const char* password = "TU_CONTRASEÑA";

Librerías:

#include <WiFi.h> nos da acceso a las funciones de conectividad inalámbrica.
#include <WebServer.h> nos permite crear un servidor web que escuche peticiones HTTP en el puerto 80 que es el estándar para páginas web.
Gestiona peticiones GET y POST, y se configura fácilmente asignando funciones a diferentes rutas URL


Credenciales:

const char* ssid = "TU_SSID"; El nombre de tu red domestica.
const char* password = "TU_CONTRASEÑA"; La contraseña de la red.


Pantalla:

#include <U8g2lib.h> Puedes revisar la información sobre como usar la pantalla 0.42" aquí

Paso 2: Configurar el LED y el servidor


// El LED integrado en la mayoría de placas ESP32-C3 está en el GPIO 8
const int LED_PIN = 8;

// Creamos el objeto del servidor web en el puerto 80
WebServer server(80);

// Variable para guardar el estado del LED
bool ledState = false;

Definir el LED:

const int LED_PIN = 8; El pin GPIO8 sera el LED.


Crear el servidor:

WebServer server(80); crea un servidor que escuchará peticiones HTTP en el puerto 80. Cuando un navegador se conecte a la IP del ESP32, el servidor procesará la petición


Paso 3: La página HTML (el "frontend")

El código de la página web se guarda como una cadena de texto en el programa del ESP32. Cuando alguien visita la IP del ESP32, el servidor envía este HTML al navegador




const char HTML_PAGE[] PROGMEM = R"HTML(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Control LED ESP32-C3</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      margin: 0;
      background: #1a1a2e;
      color: #eee;
    }
    button {
      padding: 20px 50px;
      font-size: 1.5rem;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      margin: 10px;
    }
    .btn-on {
      background: #4CAF50;
      color: white;
    }
    .btn-off {
      background: #f44336;
      color: white;
    }
    .btn-on:hover { background: #45a049; }
    .btn-off:hover { background: #d32f2f; }
    .estado {
      font-size: 1.2rem;
      margin: 20px;
      padding: 10px 30px;
      border-radius: 20px;
      background: #16213e;
    }
  </style>
</head>
<body>
  <h1>⚡ Control LED</h1>
  <div class="estado" id="estadoLED">
    LED: <span id="estadoTexto">Desconocido</span>
  </div>
  <div>
    <button class="btn-on" onclick="controlarLED('on')">🔆 ENCENDER</button>
    <button class="btn-off" onclick="controlarLED('off')">🔅 APAGAR</button>
  </div>

  <script>
    // Función que se ejecuta al hacer clic en un botón
    function controlarLED(accion) {
      // Envía una petición GET al ESP32: /led?accion=on o /led?accion=off
      fetch('/led?accion=' + accion)
        .then(response => response.text())
        .then(data => {
          // Actualiza el texto del estado con la respuesta del ESP32
          document.getElementById('estadoTexto').textContent = data;
        });
    }

    // Al cargar la página, consulta el estado actual del LED
    window.onload = function() {
      fetch('/estado')
        .then(response => response.text())
        .then(data => {
          document.getElementById('estadoTexto').textContent = data;
        });
    };
  </script>
</body>
</html>
)HTML";


Explicación del código HTML/JavaScript:

  1. La página tiene dos botones: "ENCENDER" y "APAGAR".

  2. Al hacer clic, la función JavaScript controlarLED() envía una petición fetch() a la URL /led?accion=on o /led?accion=off.

  3. El ESP32 recibe esta petición, cambia el estado del LED y responde con el nuevo estado.

  4. También hay una función que consulta el estado actual al cargar la página (window.onload).


Paso 4: Los "handlers" (el "backend")

Los Handlers o controladores son funciones que se ejecutan en respuesta a un evento específico.
En el contexto de un servidor web en un ESP32, un handler es una función que se asigna a una ruta URL por ejemplo, /led y se ejecuta automáticamente cuando el servidor recibe una petición HTTP a esa dirección, procesando la solicitud y generando una respuesta.


// Maneja la petición a la raíz "/" enviando la página HTML
void handleRoot() {
  server.send(200, "text/html", HTML_PAGE);
}

// Maneja la petición para cambiar el estado del LED
void handleLED() {
  String accion = server.arg("accion"); // Obtiene el valor del parámetro "accion"

  if (accion == "on") {
    digitalWrite(LED_PIN, LOW);  // Enciende el LED (recordemos que el pin esta invertido)
    ledState = true;
    printScreen = true;
    server.send(200, "text/plain", "ENCENDIDO");
  } else if (accion == "off") {
    digitalWrite(LED_PIN, HIGH);   // Apaga el LED
    ledState = false;
    printScreen = true;
    server.send(200, "text/plain", "APAGADO");
  } else {
    server.send(400, "text/plain", "Acción no válida");
  }
}

// Maneja la petición para consultar el estado actual
void handleEstado() {
  if (ledState) {
    server.send(200, "text/plain", "ENCENDIDO");
  } else {
    server.send(200, "text/plain", "APAGADO");
  }
}

Explicación de los handlers:

handleRoot(): Cuando alguien visita la URL raíz (/), el servidor envía el código HTML que hemos guardado.

handleLED(): Cuando se visita /led?accion=on, el servidor extrae el valor del parámetro accion, enciende o apaga el LED físicamente con digitalWrite(), y responde con el nuevo estado.

handleEstado(): Cuando se visita /estado, el servidor responde con el estado actual del LED sin modificarlo.



Paso 5: Configuración inicial (setup)

El setup() de nuestro programa es el "director de orquesta" que prepara todo el escenario: configura el LED, conecta el ESP32-C3 a la red Wi-Fi, asigna las rutas del servidor web y lo pone en marcha.
Sin esta configuración inicial, el servidor no sabría qué hacer cuando reciba una petición HTTP, ni el LED estaría listo para ser controlado.


void setup() {
  Serial.begin(115200);

  // Configurar el pin del LED como salida
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW); // Inicia apagado

  // Conectar a la red Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Conectando a Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConectado!");
  Serial.print("Dirección IP: ");
  Serial.println(WiFi.localIP());

  // Asignar funciones a las rutas del servidor
  server.on("/", handleRoot);
  server.on("/led", handleLED);
  server.on("/estado", handleEstado);

  // Iniciar el servidor
  server.begin();
  Serial.println("Servidor web iniciado.");
}


Paso 6: El bucle principal (loop)

El loop() en nuestro programa del LED es el "guardia de la puerta" que está siempre atento.
Se ejecuta sin parar, y su única misión es llamar a server.handleClient() para asegurarse de que cualquier petición web que llegue sea atendida al instante.
Sin él, el ESP32-C3 no podría responder a los clics en los botones de la página web, y el proyecto dejaría de funcionar.


void loop() {
  server.handleClient(); // Atiende las peticiones HTTP entrantes
}

Explicación:

El método server.handleClient() es el que escucha y procesa las peticiones HTTP.
Si no se llama continuamente, el servidor no responderá a las peticiones del navegador


Programa completo

Aquí tienes el código completo listo para copiar y pegar en la Arduino IDE:


/*
  Control del LED integrado del ESP32-C3 desde página web
  Basado en los ejemplos de Fab Academy y proyectos de la comunidad
  Fuente: Fab Academy 2026, Week 15 [citation:12]
*/

#include <WiFi.h>
#include <WebServer.h>
#include <U8g2lib.h>


// ===== CONFIGURACIÓN =====
// Reemplaza con las credenciales de tu red Wi-Fi
const char* ssid = "TU_SSID";
const char* password = "TU_CONTRASEÑA";

// ========== PANTALLA ==========
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 6, 5);
const int X_OFFSET = 28;
const int Y_OFFSET = 24;
bool printScreen = true;

// En la mayoría de placas ESP32-C3, el LED integrado está en GPIO 8
// Si usas XIAO ESP32-C3, usa GPIO 21
const int LED_PIN = 8;

// ===== VARIABLES GLOBALES =====
WebServer server(80);
bool ledState = false;

// ===== PÁGINA HTML =====
const char HTML_PAGE[] PROGMEM = R"HTML(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Control LED ESP32-C3</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      margin: 0;
      background: #1a1a2e;
      color: #eee;
    }
    h1 {
      font-weight: 300;
      letter-spacing: 0.1em;
    }
    .estado {
      font-size: 1.2rem;
      margin: 20px;
      padding: 10px 30px;
      border-radius: 20px;
      background: #16213e;
    }
    .botones {
      display: flex;
      gap: 20px;
      flex-wrap: wrap;
      justify-content: center;
    }
    button {
      padding: 20px 50px;
      font-size: 1.5rem;
      border: none;
      border-radius: 12px;
      cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
      min-width: 150px;
    }
    button:active {
      transform: scale(0.95);
    }
    .btn-on {
      background: #4CAF50;
      color: white;
      box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
    }
    .btn-on:hover {
      background: #45a049;
      box-shadow: 0 6px 20px rgba(76, 175, 80, 0.5);
    }
    .btn-off {
      background: #f44336;
      color: white;
      box-shadow: 0 4px 15px rgba(244, 67, 54, 0.3);
    }
    .btn-off:hover {
      background: #d32f2f;
      box-shadow: 0 6px 20px rgba(244, 67, 54, 0.5);
    }
    .footer {
      margin-top: 30px;
      font-size: 0.8rem;
      color: #666;
    }
  </style>
</head>
<body>
  <h1>⚡ Control LED</h1>
  <div class="estado" id="estadoLED">
    LED: <span id="estadoTexto">Desconocido</span>
  </div>
  <div class="botones">
    <button class="btn-on" onclick="controlarLED('on')">🔆 ENCENDER</button>
    <button class="btn-off" onclick="controlarLED('off')">🔅 APAGAR</button>
  </div>
  <div class="footer">ESP32-C3 - Servidor Web</div>

  <script>
    function controlarLED(accion) {
      fetch('/led?accion=' + accion)
        .then(response => response.text())
        .then(data => {
          document.getElementById('estadoTexto').textContent = data;
        })
        .catch(error => {
          console.error('Error:', error);
          document.getElementById('estadoTexto').textContent = 'Error';
        });
    }

    window.onload = function() {
      fetch('/estado')
        .then(response => response.text())
        .then(data => {
          document.getElementById('estadoTexto').textContent = data;
        })
        .catch(error => {
          console.error('Error:', error);
          document.getElementById('estadoTexto').textContent = 'Error de conexión';
        });
    };
  </script>
</body>
</html>
)HTML";

// ===== HANDLERS DEL SERVIDOR =====

void handleRoot() {
  server.send(200, "text/html", HTML_PAGE);
}

void handleLED() {
  String accion = server.arg("accion");

  if (accion == "on") {
    digitalWrite(LED_PIN, LOW);
    ledState = true;
    printScreen = true;
    server.send(200, "text/plain", "ENCENDIDO");
  } else if (accion == "off") {
    digitalWrite(LED_PIN, HIGH);
    ledState = false;
    printScreen = true;
    server.send(200, "text/plain", "APAGADO");
  } else {
    server.send(400, "text/plain", "Acción no válida");
  }
}

void handleEstado() {
  if (ledState) {
    server.send(200, "text/plain", "ENCENDIDO");
  } else {
    server.send(200, "text/plain", "APAGADO");
  }
}

void handleNotFound() {
  server.send(404, "text/plain", "Página no encontrada");
}

// ===== SETUP =====
void setup() {
  delay(200);
  Serial.begin(115200);
  delay(200);
  
  // Configurar pantalla
  u8g2.begin();
  u8g2.setContrast(200);

  // Configurar LED
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);

  // Conectar a Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Conectando a Wi-Fi");
  
  int intentos = 0;
  while (WiFi.status() != WL_CONNECTED && intentos < 30) {
    delay(500);
    Serial.print(".");
    intentos++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n✅ Conectado!");
    Serial.print("📡 Dirección IP: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("\n❌ No se pudo conectar a Wi-Fi");
    Serial.println("Verifica las credenciales en el código");
  }

  // Configurar rutas del servidor
  server.on("/", handleRoot);
  server.on("/led", handleLED);
  server.on("/estado", handleEstado);
  server.onNotFound(handleNotFound);

  // Iniciar servidor
  server.begin();
  Serial.println("🌐 Servidor web iniciado en el puerto 80");
  Serial.println("Abre un navegador y visita la IP indicada");
}

// ===== LOOP =====
void loop() {
  server.handleClient();
  if (printScreen)
  {
    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_6x10_tf);
    if (ledState)
      u8g2.drawStr(X_OFFSET + 10, Y_OFFSET + 20, "ON");
  	else
      u8g2.drawStr(X_OFFSET + 10, Y_OFFSET + 20, "OFF");
    u8g2.sendBuffer();
    printScreen = false;
  }
}

Cómo usar el programa

  1. Configura las credenciales Wi-Fi: Reemplaza "TU_SSID" y "TU_CONTRASEÑA" con los datos de tu red.

  2. Verifica el pin del LED: Si tu placa no usa el GPIO 8 (como la XIAO ESP32-C3 que usa GPIO 21), cambia la línea const int LED_PIN = 8; al pin correcto.

  3. Carga el programa a tu ESP32-C3 desde la Arduino IDE.

  4. Abre el Monitor Serie para ver la IP que obtiene el ESP32.

  5. Desde cualquier navegador en la misma red, visita http://[IP_del_ESP32]/.

  6. Haz clic en los botones para encender o apagar el LED.


ESP32C3 Interfaz web led

Resumen de conceptos clave y conclusión

Concepto Explicación
Servidor web El ESP32-C3 actúa como un pequeño servidor que aloja una página web en su memoria.
Peticiones HTTP El navegador envía peticiones GET al ESP32 (ej: /led?accion=on) para controlar el LED.
fetch() Función JavaScript que permite hacer peticiones HTTP sin recargar la página (AJAX).
server.on() Asigna una función a una ruta URL. Cuando alguien visita esa URL, se ejecuta la función correspondiente.
server.handleClient() Escucha y procesa las peticiones HTTP entrantes. Debe llamarse constantemente en el loop().

Ya tenemos todo lo necesario para controlar el LED integrado en el ESP32-C3 funcional desde cualquier navegador.


Saludos a todos y todas!!!


miércoles, 5 de agosto de 2026

Reloj con pantalla TFT 3.5" 8 bits y ESP32

Muy buenas a todos y todas y bienvenidos a un nuevo ejemplo de como reutilizar una vieja pantalla con interfaz de 8bits conectandola a un ESP32 DEV KIT V1.

La librería TFT_eSPI se ha convertido en el estándar de facto para manejar pantallas TFT en proyectos con microcontroladores ESP32.

La eficiencia y flexibilidad de esta librería la hacen ideal para implementar tanto relojes digitales como analógicos.

En este artículo exploraremos ambas aproximaciones, analizando sus arquitecturas y particularidades.

Antes de empezar veamos que componentes nos hacen falta para el proyecto:


🛠️ Componentes necesarios
ESP32 DEVKIT V1
ESP32 DEVKIT V1 Placa de Desarrollo Inalámbrica WiFi ESP32S esp32 DEVKIT V1.
Pantalla TFT 3,5 pulgadas
Pantalla TFT 3,5" Resolución 480x320 píxeles, interfaz paralela 8 bits.
Cables Dupont hembra hembra
Cables Dupont Kit de 40pin 10cm 2,54mm 1pin hembra a hembra .


Una vez que tenemos los componentes podemos seguir esta guía para conectar los cables, aunque no así para la configuración de la pantalla



Fundamentos del reloj digital

El reloj digital es la opción más directa y legible. La implementación de este tipo de reloj se basa en mostrar en la pantalla los dígitos que se actualizan cada segundo. Reloj digital con ESP32 y pantalla TFT paralela 8 bits

La librería TFT_eSPI permite manejar diferentes estrategias de renderizado, desde la más simple hasta técnicas avanzadas con sprites.


Enfoque para un reloj básico:

El método más sencillo consiste en dibujar directamente los dígitos sobre la pantalla usando las funciones de la propia librería tft.drawNumber() o tft.print().
En este caso, se redibuja todo el reloj cada segundo. Aunque funcional, puede producir parpadeo si la actualización no se realiza de una manera limpia.

Una mejora en el código consiste en actualizar solo los dígitos que cambian en cada momento.
Por ejemplo, si el minuto pasa de 45 a 46, solo se redibuja el dígito de las unidades. Esto reduce significativamente el parpadeo y la carga de procesamiento.
El código típico utiliza variables como byte omm = 99 para recordar el estado anterior y comparar


Extracción de la hora de compilación

Para inicializar el reloj sin necesidad de conexión WiFi, se puede extraer la hora del momento de compilación usando la macro __TIME__. Una función auxiliar llamada conv2d() convierte los caracteres en valores numéricos:


static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9') v = *p - '0';
  return 10 * v + *++p - '0';
}


Código ejemplo para un ESP32 DEVKIT V1 de un reloj digital


/*
  Reloj Digital para DOIT ESP32 DEVKIT V1 con pantalla TFT de 480x320
  Basado en el ejemplo de la librería TFT_eSPI.
  La hora se inicializa con la hora de compilación.

  - User_Setup.h -> #define ILI9341_DRIVER
  - User_Setup_Select.h -> #include <User_Setups/Setup70d_ILI9488_S3_Parallel.h>
  - User_Setups/Setup70d_ILI9488_S3_Parallel.h ->

      #define USER_SETUP_ID 146

      #define TFT_PARALLEL_8_BIT

      #define ILI9481_DRIVER

      // ESP32 S3 pins used for the parallel interface TFT
      #define TFT_CS    2
      #define TFT_DC   32  // Data Command control pin - must use a GPIO in the range 0-31
      #define TFT_RST  15

      #define TFT_WR   33  // Write strobe control pin - must use a GPIO in the range 0-31
      #define TFT_RD    4

      #define TFT_D0   12  // Must use GPIO in the range 0-31 for the data bus
      #define TFT_D1   13  // so a single register write sets/clears all bits
      #define TFT_D2   26
      #define TFT_D3   25
      #define TFT_D4   17
      #define TFT_D5   16
      #define TFT_D6   27
      #define TFT_D7   14
*/

#include <TFT_eSPI.h> // Incluye la librería para la pantalla
#include <SPI.h>

TFT_eSPI tft = TFT_eSPI(); // Crea el objeto de la pantalla

uint32_t targetTime = 0; // Variable para controlar el tiempo de actualización

// Función para extraer números de la cadena de tiempo de compilación
static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9') v = *p - '0';
  return 10 * v + *++p - '0';
}

// Obtiene la hora, minutos y segundos de la hora de compilación
uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6);

// Variables para controlar el parpadeo de los dos puntos ":"
byte omm = 99, oss = 99;
int xcolon = 0, xsecs = 0;

void setup() {
  tft.init();           // Inicializa la pantalla
  tft.setRotation(1);   // Ajusta la orientación (puedes probar con 0, 1, 2, 3)
  tft.fillScreen(TFT_BLACK);

  // Configura el color y tamaño del texto
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setTextSize(12);   // Ajusta el tamaño para tu pantalla de 480x320
  
  targetTime = millis() + 1000; // Programa la primera actualización en 1 segundo
}

void loop() {
  if (targetTime < millis()) {
    targetTime = millis() + 1000; // Programa la siguiente actualización

    // --- Lógica para incrementar la hora ---
    ss++;
    if (ss == 60) {
      ss = 0;
      omm = mm;
      mm++;
      if (mm > 59) {
        mm = 0;
        hh++;
        if (hh > 23) hh = 0;
      }
    }

    // --- Dibujar la hora en pantalla ---
    int xpos = 80; // Posición inicial X del texto (ajústalo a tu gusto)
    int ypos = 130; // Posición inicial Y del texto (ajústalo a tu gusto)

    // Redibuja horas y minutos solo cuando cambian
    if (omm != mm) {
      omm = mm;
      tft.setCursor(xpos, ypos);
      
      // Dibuja las horas con cero a la izquierda si es necesario
      if (hh < 10) tft.print("0");
      tft.print(hh);
      
      // Dibuja los dos puntos y guarda su posición para parpadear
      xcolon = tft.getCursorX();
      tft.print(":");
      
      // Dibuja los minutos con cero a la izquierda si es necesario
      if (mm < 10) tft.print("0");
      tft.print(mm);
      
      xsecs = tft.getCursorX(); // Guarda la posición para los segundos

    }

    // --- Dibujar los segundos y hacer parpadear los dos puntos cada segundo ---
    if (oss != ss) {
      oss = ss;
      
      // Borra los segundos anteriores y los vuelve a dibujar
      tft.setCursor(xsecs, ypos); // Posición más abajo para los segundos
      tft.setTextColor(TFT_WHITE, TFT_BLACK);
      tft.print(":");
      if (ss < 10) tft.print("0");
      tft.print(ss);

      // Lógica para parpadear los dos puntos ":"
      if (ss % 2 == 0) {
        tft.setTextColor(TFT_WHITE, TFT_BLACK); // Atenúa el color
        tft.setCursor(xcolon, ypos);
        tft.print(":");
      } else {
        tft.setTextColor(TFT_WHITE, TFT_BLACK); // Restaura el color
        tft.setCursor(xcolon, ypos);
        tft.print(":");
      }
    }
  }
}


Ejemplo de pantalla con reloj analógico

Reloj analógico: geometría y precisión

Crear un reloj analógico es técnicamente más exigente ya que requiere dibujar una esfera estática y dos o tres manecillas que giran con el tiempo.


La complejidad reside en el cálculo de posiciones y en la gestión del redibujado para evitar estelas.


Cálculo de ángulos y coordenadas para las manecillas del reloj

Las manecillas se posicionan mediante trigonometría básica. Partiendo del centro de la esfera, se calcula el ángulo correspondiente a la hora y luego se proyecta sobre el radio de la manecilla:

  • Segundero: 6 grados por segundo o lo que es lo mismo 360º / 60.

  • Minutero: 6 grados por minuto, más una corrección por los segundos transcurridos

  • Horario: 30 grados por hora o 360º / 12, más una corrección por los minutos transcurridos


Redibujado: el problema de las estelas

El desafío principal del reloj analógico es evitar que las manecillas dejen estelas al moverse. Si se redibuja toda la esfera cada segundo, el parpadeo es molesto. La solución óptima para este problema es redibujar solo las manecillas, de esta manera eliminamos un casi seguro parpadeo de la pantalla.


Técnica de borrado diferencial

El método más eficiente para dibujar las manecillas funciona así:

  • Se dibuja la esfera del reloj completa una sola vez en el setup().

  • En cada iteración del loop(), se calculan los nuevos ángulos para las posiciones de las manecillas.

  • Las manecillas antiguas se borran pintándolas del color de fondo (negro) en la posición anterior.

  • Se dibujan las nuevas manecillas en la posición actual.

Para que esta técnica funcione hay que guardar las coordenadas anteriores en variables como osx, osy, omx, omy, ohx, ohy.

Configuración para pantallas de 480x320

Para pantallas con resolución 480x320, las coordenadas del centro suelen ser el punto 240 y el punto 160.
El radio máximo para que quepa la esfera completa en la pantalla es de aproximadamente 150 píxeles.
Vamos a distribuir las longitudes de las manecillas de la siguiente manera:


#define CENTER_X 240
#define CENTER_Y 160
#define RADIUS 150
#define HOUR_LENGTH 67     // ~45% del radio
#define MINUTE_LENGTH 97   // ~65% del radio
#define SECOND_LENGTH 112  // ~75% del radio

Marcas horarias y minutos

La esfera incluye 12 marcas principales para las horas y 60 marcas secundarias para los minutos. Las de hora son más largas el código típico utiliza radios de 100 y 114 para los extremos.


/*
  Reloj analógico para DOIT ESP32 DEVKIT V1 con pantalla TFT de 480x320
  Basado en el ejemplo de la librería TFT_eSPI.
  La hora se inicializa con la hora de compilación.

  - User_Setup.h -> #define ILI9341_DRIVER
  - User_Setup_Select.h -> #include <User_Setups/Setup70d_ILI9488_S3_Parallel.h>
  - User_Setups/Setup70d_ILI9488_S3_Parallel.h ->

      #define USER_SETUP_ID 146

      #define TFT_PARALLEL_8_BIT

      #define ILI9481_DRIVER

      // ESP32 S3 pins used for the parallel interface TFT
      #define TFT_CS    2
      #define TFT_DC   32  // Data Command control pin - must use a GPIO in the range 0-31
      #define TFT_RST  15

      #define TFT_WR   33  // Write strobe control pin - must use a GPIO in the range 0-31
      #define TFT_RD    4

      #define TFT_D0   12  // Must use GPIO in the range 0-31 for the data bus
      #define TFT_D1   13  // so a single register write sets/clears all bits
      #define TFT_D2   26
      #define TFT_D3   25
      #define TFT_D4   17
      #define TFT_D5   16
      #define TFT_D6   27
      #define TFT_D7   14
*/

#include <TFT_eSPI.h> // Incluye la librería para la pantalla
#include <SPI.h>

TFT_eSPI tft = TFT_eSPI(); // Crea el objeto de la pantalla
// --- Configuración del reloj ---
#define CENTER_X 240    // Centro X de la pantalla (480/2)
#define CENTER_Y 160    // Centro Y de la pantalla (320/2)
#define RADIUS 150      // Radio máximo que cabe en la pantalla

// Colores de las agujas
#define COLOR_HOUR   TFT_WHITE
#define COLOR_MINUTE TFT_YELLOW
#define COLOR_SECOND TFT_RED
#define COLOR_DIAL   TFT_DARKGREY

// Grosor de las agujas (en píxeles)
#define THICKNESS_HOUR   8
#define THICKNESS_MINUTE 5
#define THICKNESS_SECOND 2

// --- Variables para la hora ---
static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9') v = *p - '0';
  return 10 * v + *++p - '0';
}

uint8_t hh = conv2d(__TIME__);
uint8_t mm = conv2d(__TIME__ + 3);
uint8_t ss = conv2d(__TIME__ + 6);

uint32_t targetTime = 0;

// --- Variables para dibujar las agujas ---
float lastHourAngle = -1;
float lastMinuteAngle = -1;
float lastSecondAngle = -1;

// --- Funciones de dibujo ---

// Dibuja la esfera del reloj (solo se dibuja una vez)
void drawDial() {
  tft.fillScreen(TFT_BLACK);
  
  // Círculo exterior
  tft.drawCircle(CENTER_X, CENTER_Y, RADIUS, COLOR_DIAL);
  tft.drawCircle(CENTER_X, CENTER_Y, RADIUS - 2, TFT_WHITE);
  
  // Marcas de las horas (12 marcas grandes)
  for (int i = 0; i < 12; i++) {
    float angle = i * 30.0 * M_PI / 180.0 - M_PI / 2;
    int x1 = CENTER_X + (RADIUS - 20) * cos(angle);
    int y1 = CENTER_Y + (RADIUS - 20) * sin(angle);
    int x2 = CENTER_X + RADIUS * cos(angle);
    int y2 = CENTER_Y + RADIUS * sin(angle);
    tft.drawLine(x1, y1, x2, y2, TFT_WHITE);
  }
  
  // Marcas de los minutos (60 marcas pequeñas)
  for (int i = 0; i < 60; i++) {
    if (i % 5 == 0) continue; // Saltar las marcas de horas
    float angle = i * 6.0 * M_PI / 180.0 - M_PI / 2;
    int x1 = CENTER_X + (RADIUS - 10) * cos(angle);
    int y1 = CENTER_Y + (RADIUS - 10) * sin(angle);
    int x2 = CENTER_X + (RADIUS - 4) * cos(angle);
    int y2 = CENTER_Y + (RADIUS - 4) * sin(angle);
    tft.drawLine(x1, y1, x2, y2, TFT_WHITE);
  }
  
  // Centro del reloj (punto central)
  tft.fillCircle(CENTER_X, CENTER_Y, 6, TFT_RED);
  tft.fillCircle(CENTER_X, CENTER_Y, 3, TFT_WHITE);
  drawNumbers();
}

// Los números 1-12
void drawNumbers() {
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(2);
  tft.setTextDatum(MC_DATUM);
  
  for (int i = 1; i <= 12; i++) {
    float angle = i * 30.0 * M_PI / 180.0 - M_PI / 2;
    int x = CENTER_X + (RADIUS - 35) * cos(angle);
    int y = CENTER_Y + (RADIUS - 35) * sin(angle);
    tft.drawString(String(i), x, y);
  }
}

// Dibuja una aguja
void drawHand(float angle, int length, int thickness, uint16_t color, bool erase = false) {
  // Calcular el punto final de la aguja
  int x = CENTER_X + length * cos(angle);
  int y = CENTER_Y + length * sin(angle);
  
  // Color de borrado (negro) si estamos borrando
  uint16_t drawColor = erase ? TFT_BLACK : color;
  
  // Dibujar la aguja como una línea gruesa
  tft.drawLine(CENTER_X, CENTER_Y, x, y, drawColor);
  
  // Para hacerla más gruesa, dibujamos líneas paralelas
  if (thickness > 1) {
    for (int i = 1; i < thickness; i++) {
      // Desplazamiento perpendicular
      float perpAngle = angle + M_PI / 2;
      int offsetX = i * cos(perpAngle);
      int offsetY = i * sin(perpAngle);
      tft.drawLine(CENTER_X + offsetX, CENTER_Y + offsetY, x + offsetX, y + offsetY, drawColor);
      tft.drawLine(CENTER_X - offsetX, CENTER_Y - offsetY, x - offsetX, y - offsetY, drawColor);
    }
  }
}

// Convierte tiempo a ángulo (en radianes)
float timeToAngle(int value, int maxValue, float offset = 0) {
  return (value * 2.0 * M_PI / maxValue) - M_PI / 2 + offset;
}

// --- Setup ---
void setup() {
  tft.begin();
  tft.setRotation(1); // Ajusta la orientación (0, 1, 2, 3)
  tft.setTextSize(1);
  
  // Dibujar la esfera una sola vez
  drawDial();
  
  // Calcular los ángulos iniciales
  lastHourAngle = timeToAngle(hh % 12, 12) + timeToAngle(mm, 60) / 12.0;
  lastMinuteAngle = timeToAngle(mm, 60) + timeToAngle(ss, 60) / 60.0;
  lastSecondAngle = timeToAngle(ss, 60);
  
  // Dibujar las agujas iniciales
  drawHand(lastHourAngle, RADIUS * 0.45, THICKNESS_HOUR, COLOR_HOUR);
  drawHand(lastMinuteAngle, RADIUS * 0.65, THICKNESS_MINUTE, COLOR_MINUTE);
  drawHand(lastSecondAngle, RADIUS * 0.75, THICKNESS_SECOND, COLOR_SECOND);
  
  targetTime = millis() + 1000;
}

// --- Loop ---
void loop() {
  if (targetTime < millis()) {
    targetTime = millis() + 1000;
    
    // Incrementar tiempo
    ss++;
    if (ss == 60) {
      ss = 0;
      mm++;
      if (mm > 59) {
        mm = 0;
        hh++;
        if (hh > 23) hh = 0;
      }
    }
    
    // Calcular nuevos ángulos
    float hourAngle = timeToAngle(hh % 12, 12) + timeToAngle(mm, 60) / 12.0;
    float minuteAngle = timeToAngle(mm, 60) + timeToAngle(ss, 60) / 60.0;
    float secondAngle = timeToAngle(ss, 60);
    
    // --- Borrar agujas viejas (dibujar en negro) ---
    drawHand(lastHourAngle, RADIUS * 0.45, THICKNESS_HOUR, TFT_BLACK, true);
    drawHand(lastMinuteAngle, RADIUS * 0.65, THICKNESS_MINUTE, TFT_BLACK, true);
    drawHand(lastSecondAngle, RADIUS * 0.75, THICKNESS_SECOND, TFT_BLACK, true);
    
    // --- Dibujar agujas nuevas ---
    drawHand(hourAngle, RADIUS * 0.45, THICKNESS_HOUR, COLOR_HOUR);
    drawHand(minuteAngle, RADIUS * 0.65, THICKNESS_MINUTE, COLOR_MINUTE);
    drawHand(secondAngle, RADIUS * 0.75, THICKNESS_SECOND, COLOR_SECOND);
    
    // --- Guardar ángulos para la próxima iteración ---
    lastHourAngle = hourAngle;
    lastMinuteAngle = minuteAngle;
    lastSecondAngle = secondAngle;
  }
}


Reflexión final

La elección entre reloj digital y analógico depende del propósito.
El digital es más legible y fácil de implementar, ideal para aplicaciones donde la información debe ser clara y directa.
El analógico añade un valor estético y resulta especialmente atractivo en interfaces de usuario o pantallas de bienvenida.


Ambos enfoques se benefician de la eficiencia de TFT_eSPI, pero el analógico requiere un cuidado adicional en la gestión del redibujado para evitar estelas y ofrecer una experiencia fluida.
La técnica de borrado diferencial explicada es la solución más equilibrada en términos de rendimiento y calidad visual.

Con un par de retoques, como un calendario, puede ser un buen gadget para ponerlo en tu escritorio para saber en que hora del día estas.

Aquí tienes otros enlaces de este blog relacionados con el microcontrolador ESP32 y pantallas:


viernes, 31 de julio de 2026

Ejemplos Arduino: LEDC (LED Control) en el ESP32

Muy buenas a todos y todas a la vuelta al canal!!

El periférico LEDC está diseñado principalmente para controlar la intensidad de LEDs, aunque también pero también es muy útil para generar señales PWM para otros propósitos tales como controlar motores DC.

El ESP32-C3 cuenta con 6 canales que pueden generar formas de onda independientes, ideales para controlar LEDs o dispositivos que necesiten PWM tan solo cambiando algunos parámetros de la configuración como el temporizador,la confirmación del canal y el cambio de la señal PWM.


¿Cuando se implementó ledc?

La API de LEDC para Arduino-ESP32 está documentada y disponible como parte del núcleo (core) de ESP32 en Arduino.
La versión actual (a partir de la 3.0.0) incluye cambios importantes en la sintaxis, como la sustitución de:


ledcAttachPin() // V < 3.0.0

por


ledcAttach() // Versión > 3.0.0

Para la versión 3.0.0 del core de ESP32 para Arduino, ya no se usa la función ledcSetup() ni la función ledcAttachPin(), sino que se recomienda utilizar la función ledcAttach(pin, freq, resolution)
ESP32 C3 Mini 0.42 Pinout
ESP32 C3 mini 0.42" pinout

Funciones y definiciones relacionadas

1. ledcAttach()

Función:


bool ledcAttach(uint8_t pin, uint32_t freq, uint8_t resolution);

La función ledcAttach configura un pin para PWM con la frecuencia (uint32_t freq) y resolución (uint8_t resolution)indicadas.
El canal LEDC se selecciona automáticamente.

Devuelve true si la configuración es exitosa.


2. ledcAttachChannel()

Función:


bool ledcAttachChannel(uint8_t pin, uint32_t freq, uint8_t resolution, uint8_t channel);

Con ledcAttachChannel configuramos un pin para PWM con la frecuencia, resolución y canal específicos.

Devuelve true si la configuración es exitosa.


3. ledcWrite()

Función:


void ledcWrite(uint8_t pin, uint32_t duty);

Establece el ciclo de trabajo (duty cycle) para el pin LEDC.

El valor de duty depende de la resolución: para 10 bits, el rango es de 0 a 1023.


4. ledcRead()

Función:


uint32_t ledcRead(uint8_t pin);

Lee el ciclo de trabajo configurado para el pin.


5. ledcReadFreq()

Función:


uint32_t ledcReadFreq(uint8_t pin);

Con ledcReadFreq lee la frecuencia configurada para el pin que hemos seleccionado.


Relación entre frecuencia y resolución


6. ledcDetach()

Función:


bool ledcDetach(uint8_t pin);

Desvincula el pin del periférico LEDC para usarlo como GPIO digital.


7. ledcChangeFrequency()

Función:


uint32_t ledcChangeFrequency(uint8_t pin, uint32_t freq, uint8_t resolution);

Cambia la frecuencia y resolución de un pin LEDC ya configurado.


8. ledcFade()

Función:


bool ledcFade(uint8_t pin, uint32_t start_duty, uint32_t target_duty, int max_fade_time_ms);

Configura e inicia un desvanecimiento (fade) automático por hardware desde un valor inicial hasta un valor objetivo.

Devuelve true si se inicia correctamente.


9. ledcWriteTone()

Función:


uint32_t ledcWriteTone(uint8_t pin, uint32_t freq);

Genera un tono PWM al 50% en la frecuencia especificada.

10. ledcWriteNote()

Función:


uint32_t ledcWriteNote(uint8_t pin, note_t note, uint8_t octave);

Por último tenemos ledcWriteNote que genera un tono a partir de una nota musical y una octava.


LEDC ¿es mejor o peor que analogWrite?

En ESP32, analogWrite() es una capa de abstracción sobre LEDC.
La principal diferencia es que analogWrite() simplifica el uso, pero limita el control.


Característica analogWrite() LEDC nativo
Resolución Fija a 8 bits (0-255) Configurable (1-14 bits en ESP32-C3)
Frecuencia Fija (~5 kHz) Totalmente configurable
Gestión de canales Automática, puede causar conflictos Manual, 6 canales independientes
Funciones avanzadas No disponible Fade por hardware, interrupciones
Complejidad de código Muy baja Media

Conclusión: analogWrite() es perfecto para prototipos rápidos y proyectos simples.
LEDC es necesario cuando se necesita precisión, frecuencias personalizadas o múltiples canales PWM sin interferencias .


🛠️ Componentes necesarios
ESP32-C3 con OLED de 0.42, Antena Cerámica, Wifi, Bluetooth
ESP32-C3OLED de 0.42" Placa de Desarrollo ESP32-C3 con OLED de 0.42", Antena Cerámica, Wifi, Bluetooth.

Relación entre frecuencia y resolución

En el ESP32-C3, la frecuencia y la resolución están interrelacionadas. A mayor frecuencia, menor resolución máxima disponible, y viceversa.

Ejemplos prácticos:

  • 5 kHz con 13 bits: 8192 niveles de intensidad.
  • 40 MHz con 1 bit: Solo 50% fijo.

Si se intenta configurar una combinación no soportada, la API mostrará un error:


E (196) ledc: requested frequency and duty resolution cannot be achieved, try reducing freq_hz or duty_resolution. div_param=128 


Ejemplos para ESP32-C3 con pantalla OLED 0.42"

La placa "ESP32-C3 OLED 0.42" tiene la pantalla conectada por I2C: en el pin GPIO5 corresponde a SDA y el pin GPIO6 para SCL.


Ejemplo 1: Uso de analogWrite (simplificado)

Primero veamos un ejemplo clásico programa con analogWrite()



#include <Wire.h> 
#include <Adafruit_SSD1306.h>

#define OLED_SDA 5
#define OLED_SCL 6
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define LED_PIN 8

void setup() {
  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  for (int brightness = 0; brightness <= 255; brightness += 5) {
    analogWrite(LED_PIN, brightness);

    display.clearDisplay();
    display.setCursor(30, 30);
    display.print("AnalogWrite: ");
    display.println(brightness);

    int porcentaje = map(brightness, 0, 255, 0, 100);
    display.setCursor(30, 40);
    display.print("Inten.: ");
    display.print(porcentaje);
    display.println("%");

    display.display();
    delay(30);
  }
  for (int brightness = 255; brightness >= 0; brightness -= 5) {
    analogWrite(LED_PIN, brightness);

    display.clearDisplay();
    display.setCursor(30, 30);
    display.print("AnalogWrite: ");
    display.println(brightness);

    int porcentaje = map(brightness, 0, 255, 0, 100);
    display.setCursor(30, 40);
	display.print("Inten.: ");
    display.print(porcentaje);
    display.println("%");

    display.display();
    delay(30);
  }
}


Ejemplo 2: Ledc PWM en LED y mostrar valor en OLED (I2C)

En este ejemplo tendremos la salida del valor Ledc en la pantalla OLED 0.42". El pin 8 del LED builtin el estado de Ledc.

NOTA: el encendido del LED builtin esta invertido, es decir cuando la señal Ledc esta en 0 el LED builtin estará a su máxima intensidad y apagado en su valor más alto.



 

#include <Wire.h> 
#include <Adafruit_SSD1306.h>

#define OLED_SDA 5
#define OLED_SCL 6
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define LED_PIN 8
#define PWM_FREQ 5000
#define PWM_RES 10
#define MAX_DUTY ((1 << PWM_RES) - 1) // 1023 para 10 bits

int duty = 0;
int step = 5;

void setup() {
  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  ledcAttach(LED_PIN, PWM_FREQ, PWM_RES);
  ledcWrite(LED_PIN, 0);
}

void loop() {
  duty += step;
  if (duty >= MAX_DUTY || duty <= 0) step = -step;

  ledcWrite(LED_PIN, duty);

  display.clearDisplay();
  display.setCursor(30, 30);
  display.print("Duty: ");
  display.setCursor(30, 40);
  display.print(duty);
  display.print(" / ");
  display.println(MAX_DUTY);

  int porcentaje = map(duty, 0, MAX_DUTY, 0, 100);
  display.setCursor(30, 50);
  display.print("Intensidad: ");
  display.print(porcentaje);
  display.println("%");

  display.display();
  delay(20);
}


Ejemplo 3: Ledc fade automático por hardware y mostrar estado


 
#include <Wire.h> 
#include <Adafruit_SSD1306.h>

#define OLED_SDA 5
#define OLED_SCL 6
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define LED_PIN 8
#define PWM_CHANNEL 0
#define PWM_FREQ 5000
#define PWM_RES 10
#define MAX_DUTY ((1 << PWM_RES) - 1)

void setup() {
  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  ledcAttachChannel(LED_PIN, PWM_FREQ, PWM_RES, PWM_CHANNEL);
 ledcWrite(LED_PIN, 0);

  display.display();
  delay(1000);
}

void loop() {
  display.clearDisplay();
  display.setCursor(30, 30);
  display.println("Fade IN");
  display.display();

  ledcFade(LED_PIN, 0, MAX_DUTY, 2000);
  delay(2500);

  display.clearDisplay();
  display.setCursor(30, 30);
  display.println("Fade OUT");
  display.display();

  ledcFade(LED_PIN, MAX_DUTY, 0, 2000);
  delay(2500);
}


Ejemplo 4: Ledc PWM para ESC de motor brushed

Para controlar un ESC, se necesita una señal PWM de 50 Hz con pulsos entre 1000 µs y 2000 µs.



#define ESC_PIN 8 
#define PWM_FREQ 50 
#define PWM_RES 14 
#define MAX_DUTY ((1 << PWM_RES) - 1)

// Para 14 bits: 16384 = 100% (20 ms)
// 1000 µs = 1 ms → (1/20)16384 = 819
// 2000 µs = 2 ms → (2/20)16384 = 1638

void setup() {
  ledcAttach(ESC_PIN, PWM_FREQ, PWM_RES);
  ledcWrite(ESC_PIN, 819); // Neutro
  delay(3000);
}

void loop() {
  ledcWrite(ESC_PIN, 819); // Neutro
  delay(2000);
  ledcWrite(ESC_PIN, 1638); // Máximo
  delay(2000);
}


También puedes revisar el repositorio supermini_esp32c3_sketches para ejemplos muy variados y completos.


Conclusión sobre LEDC en el ESP32-C3

El periférico LEDC del ESP32-C3 es una herramienta extremadamente versátil y potente para generar señales PWM, muy superior a la función analogWrite() que muchos conocen de Arduino.

Mientras que analogWrite() está limitada a 8 bits de resolución y frecuencias fijas, LEDC ofrece un control total con resoluciones de hasta 14 bits y frecuencias completamente configurables.
Esto hace que Ledc sea ideal para aplicaciones que requieren precisión, como el control de motores DC, servomotores, atenuación de LEDs profesionales o incluso generación de tonos y notas musicales.

Lo bueno:

El ESP32-C3 dispone de 6 canales independientes, lo que permite manejar múltiples dispositivos PWM simultáneamente sin interferencias.
Una de sus características más destacadas es la capacidad de realizar desvanecimientos (fade) automáticos por hardware, lo que descarga al procesador de esta tarea y permite transiciones suaves sin necesidad de interrupciones constantes.


Lo malo:

Sin embargo, hay que tener en cuenta algunas limitaciones importantes. El ESP32-C3 solo funciona en modo "low speed", a diferencia de otros ESP32 que también tienen modo "high speed".
Además, existe una relación de compromiso entre frecuencia y resolución: a mayor frecuencia, menor resolución disponible, y viceversa. Por ejemplo, a 5 kHz se puede alcanzar 13 bits de resolución, pero a 40 MHz apenas se obtiene 1 bit.

Aquí tienes otros enlaces de este blog relacionados con el microcontrolador ESP32 y Arduino:


jueves, 16 de julio de 2026

DCCpp Animations Controller, dale vida a tu maqueta!

DCCpp LMD Animation Controller Web

¿Qué es DCCpp Animations Controller?

Muy buenas a todos y todas!

DCCpp Animations Controller es una aplicación web que te permite controlar automáticamente las animaciones de tu maqueta de ferrocarril como luces, desvíos, fuentes, servos, etc.

Según un ciclo día/noche, configurable con parámetros personalizados, la duración del día completo o el retardo.



La web se conecta directamente a tu Arduino por USB o Bluetooth mediante Web Serial API y envía comandos DCCpp de forma automática e inteligente.

¡¡¡Sin necesidad de complicados programas para configurar accesorios o desvíos!!!

✅ DCCpp Animations Controller es ideal para:

  • Encender farolas al anochecer y apagarlas al amanecer.

  • Activar animaciones o desvíos solo durante el día.

  • Controlar luces de edificios, paso a nivel, etc.

  • Simular el comportamiento real de una maqueta.

🚀 Modo de uso

Conexión del hardware

Para utilizar el sistema automatizado de luces y accesorios solo necesitamos:

  • Central DCC Conectada por puerto serie o Bluetooth.

  • Navegador Chrome, Edge o Brave (Web Serial API).

Acceso a la web

DCCpp Animations Controller

DCCpp Animations Controller

Lógica para simulación 🌞día y 🌙noche


Ciclo completo = 8 minutos (ejemplo)
├── DÍA (4 minutos)   → Se ENCIENDEN animaciones de DÍA
│                       Se APAGAN animaciones de NOCHE
|
└── NOCHE (4 minutos) → Se ENCIENDEN animaciones de NOCHE
                        Se APAGAN animaciones de DÍA

Primeros pasos

  • 1. 🔌 Conectar → Selecciona el puerto COM de tu Arduino.

    DCCpp LMD Animation Web

  • 2. ➕ Añadir animaciones → Define qué y cuándo se activa.

  • 3. 🌙 Configurar ciclo → Duración del día/noche y retardos.

  • 4. ▶️ Dejarlo funcionar → Las animaciones se activan solas.


Funciones principales

  • Ciclo (minutos):
    Duración total de día + noche Min: 1 Max: 60 (ej: 8 min = 4 día + 4 noche).

  • Retardo (segundos):
    Retraso aleatorio máximo al encender/apagar Min: 0 Max: 300 simulando imprecisión real en el encendido/apagado.

  • Barra de progreso:
    Muestra el avance del ciclo actual.

  • Próxima animación:
    Indica cuál será la siguiente en ejecutarse.


Panel de animaciones

Panel de animaciones día y noche para DCCpp LMD

Botones del panel de animaciones:

  • ➕ Nueva animación:
    Situado en la parte izquierda, crea una nueva animación. Ver añadir animaciones.

  • 📋 Ver comandos:
    Muestra información sobre los comandos más comunes.

  • 💾 Exportar JSON:
    Guarda todas tus animaciones y configuración en un archivo formato JSON.

  • 📂 Importar JSON:
    Carga una configuración en formato JSON guardada previamente.

  • 🗑️ Limpiar:
    Elimina todas las animaciones.

Botones de cada animación

  • ✅ | ⭕: Activa | Desactiva animación

  • ✏️ Editar: Modifica la animación

  • 🗑️ Eliminar: Borra la animación

  • 🔘 Test: Envía el comando manualmente (prueba)


➕ Añadir animaciones

DCCpp Animations Controller

Es hora de crear nuestra primera animación como pueden ser las farolas de una plaza que se encenderán al empezar la noche y definidas en la central con el com la ID 15

  • Nombre:
    Identificador de la animación "Farolas plaza"

  • Momento:
    ¿Día o Noche? Noche.

  • Tipo:
    LUCES | EFECTOS | SEMAFORO | COMANDO.
    Seleccionamos LUCES.

  • Comando base:
    Comando T15 sin el 1 o 0 final.


Comunicación Serial

DCCpp Animations Controller monitor serial

Para la comunicación de la central podremos usar las conexiones seriales que tengamos disponibles en la central. Generalmente el puerto USB al que conectamos la central.

Pero Animations Controller también puede hacer uso de la comunicación bluetooth en el caso de que tengamos un módulo HC-06 y un adaptador bluetooth instalado en el ordenador.

En el caso de usar la comunicación bluetooth también podremos gestionar los comandos desde un teléfono móvil.

  • 🔗 Conectar:
    Abre una ventana para seleccionar el puerto COM o tty al que esta conectada la central DCCpp.

  • ⛔ Desconectar:
    Cierra la comunicación con la central.

  • 📤 Enviar:
    Envía le comando escrito de manera manual.

  • 🧹 Limpiar:
    Limpia el monitor de respuestas.

  • Comando manual:
    Envía cualquier comando DCCpp ejem: t1 4 80 1.

  • Monitor de respuestas:
    Muestra las respuestas seriales que proporciona la central DCCpp indicando también la hora.


Log de Transiciones

Log de transiciones DCCpp Animations Controller

En el Log de Transiciones tendremos la información interna del programa como el momento del día en el que estamos, cuando se encenderán o apagarán o apagarán entre otros datos.


Conversor de direcciones DCC

Conversor de IDs DCCpp Animations Controller

Recomendaciones Para el usuario

  • Usa Chrome o Edge, Firefox necesita el plugin externo WebSerial for Firefox.

  • Empareja primero los dispositivos Bluetooth (si usas conexión inalámbrica).

  • Prueba cada animación con el botón 🔘 antes de dejarlo automático.

  • Configura retardos largos (30-60s) para simular cambios graduales.

  • Exporta tu configuración como respaldo.


❓ Preguntas frecuentes

¿Sirve cualquier central DCC?

- Si, cualquier central DCCpp incluidas la DCC++ original o las más punteras como DCCex


¿Puedo usar Bluetooth en lugar de USB?

- , empareja tu ESP32 o HC-05 con el ordenador y aparecerá en el selector de puertos.


¿Puedo usar Firefox?

Si pero... Firefox no soporta Web Serial API, necesita el plugin WebSerial for Firefox.


¿Puedo cambiar el ciclo mientras funciona?

- , solo cambia los minutos y pulsa "Actualizar". Las animaciones se reprogramarán.


¿Qué pasa si pierdo la conexión?

- El programa sigue ejecutándose localmente en tu navegador. Al reconectar, seguirá enviando comandos.


¿Puedo usar el móvil?

- , pero... Android necesita Chrome y emparejar Bluetooth desde ajustes del sistema primero. pero la interfaz no esta adaptada para estos dispositivos.


🔗 Enlace:

🌐 Acceso a la web: https://dccppanimations.infotronikblog.com

Otros proyectos con para la central DCCpp LMD:

Saludos a todos y todas!!!