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:


No hay comentarios :