jueves, 20 de agosto de 2026

Emuladores para MS-DOS

Muy buenas a todos y todas y Bienvenidos a los 90's!!!

Hoy en día tener un emulador instalado en nuestro PC es algo normal, incluso hay recopilatorios de estos tipo RetroPie que reúnen una gran variedad de este tipo de programas.

Incluso emuladores del propio MS-DOS como DosBox.

Pero cuando hablamos de emuladores en en sistema operativo MS-DOS nos estamos adentrando en el origen de cualquier emulador.

Estamos hablando de 1995 o 1996 y la consola elegida fue la nintendo NES.
Luego vinieron muchos más como la GameBoy, GameBoy Color, GameBoy Advance y también las consolas de Sega allá por el año 1997. Ah! y también para ordenadores de 8 bits de los 80 como Amstrad CPC.

Por supuesto esto fue un gran hito en la informática y, pese a sus limitaciones, democratizo poder jugar a cualquier consola de la época en el PC que tenías en el salón.

Si hablamos del primer emulador NES gratuito estamos hablando de NESticle creado por Bloodlust Software en Abril de 1997. Fue el primer emulador de NES freeware de gran impacto mundial que hizo que pudiéramos jugar a títulos de consola en MS-DOS y Windows.


Máquina de prueba

Para probar los emuladores voy a utilizar mi Pentium 166MMX que aunque este procesador es del año 1997. Como la mayoría de estos emuladores aparecieron por esos años desde NESticle en 1995 a NO$GBA en 2002.


NESticle en Pentium 166MMX

Lista de emuladores para MS-DOS

  • Emulador NES


    NESticle es uno de los emuladores más importantes y legendarios del mundo retro. Fue el primero en hacer que jugar a la NES en un PC fuera rápido, fácil y accesible para todo el mundo.
    Emulador NESticle para MS-DOS

    Programado para PCs modestos: Fue programado en C++ y lenguaje ensamblador, lo que lo hacía increíblemente rápido.
    Sus requisitos eran de unos 25 MHz, esto le permite funcionar perfectamente en un 486 DX2, y sobraba para una máquina tipo Pentium.

    Pruebas realizadas
    • DOSBOX: No he tenido ningún problema, se puede jugar perfectamente.
    • PC: Funciona perfectamente tanto el vídeo como el sonido.

    Descargar emulador NESticle para MS-DOS

    Emulador NESticle para MS-DOS
    Emulador NESticle para MS-DOS


  • Emuladores GameBoy

    Para la primera portátil de Nintendo tenemos NO$GMB.
    Emulador NO$GMB para MS-DOS

    NO$GMBes el abuelo de la emulación de Game Boy en DOS. Una de sus grandes ventajas es la velocidad bruta. Desarrollado por Martin Korth entre 1997 y 2000, y está programado en lenguaje ensamblador, lo que lo hace increíblemente rápido, algo ideal para este tipo de máquinas que rara vez superan los 66MHz.

    Es, con diferencia, el emulador de Game Boy más rápido que existe Originalmente para DOS, tiene versiones optimizadas que funcionan incluso en PCs XT y AT.

    Pruebas realizadas
    • DOSBOX: No he tenido ningún problema, se puede jugar perfectamente.
    • PC: Funciona perfectamente tanto el vídeo como el sonido.

    Descargar emulador NO$GMB para MS-DOS.

    Emulador NO$GMB para MS-DOS
    Emulador NO$GMB para MS-DOS

  • Emuladores GameBoy Advance


    NO$GBA salio originalmente en 2002. Fue un salto enorme respecto a sus predecesores.
    Disponible para Windows y MS-DOS aunque también hay una versión para Linux y Mac usando Wine.

    Emulador NO$GBA para MS-DOS

    Consolas que emula:

    • Game Boy Advance (GBA)
    • Nintendo DS (NDS) con limitaciones
    • Nintendo DSi según versión

    Contaba con depurador integrado muy potente que permite analizar el código de las ROMs en tiempo real. Esto lo convirtió en la herramienta favorita de los desarrolladores de homebrew y hackers.
    También tiene soporte para BIOS para usar archivos BIOS originales de la GBA y NDS para una emulación más precisa.

    Pruebas realizadas
    • DOSBOX: Muy lento.
    • PC: Muy lento.

    Descargar emulador NO$GBA para MSDOS.

    Emulador NO$GBA para MS-DOS


  • Emuladores Sega Game Gear


    MASSAGE es un emulador para las máquinas Sega Master System y Game Gear que funciona sobre MS-DOS.


    Emulador Sega MASSAGE para MS-DOS
    Emulador Sega MASSAGE para MS-DOS

    Massage fue desarrollado por James McKay y su enfoque principal es emular juegos de la Master System y Game Gear en PCs de la época.

    Una de sus características más destacadas es que está optimizado para configuraciones de PC modestas, por lo que debería funcionar hasta en un 486.

    Incluye opciones de emulación por módem o cable serie (null-modem), lo que permitía jugar en red o enlazar dos máquinas. Incluso cuenta con un modo de chat para comunicarse durante la partida.

    Pruebas realizadas
    • DOSBOX: En este caso dependerá del juego, si es un juego muy rápido como Sonic, la experiencia se vera mermada sobre todo en el sonido. Por otro lado juegos tipo puzzle se puede jugar perfectamente
    • PC: Funciona perfectamente tanto el vídeo como el sonido.

    Descargar emulador MASSAGE para MS-DOS..

    Emulador Sega MASSAGE para MS-DOS
    Emulador Sega MASSAGE para MS-DOS

  • Emuladores Amstrad CPC

    NO$CPC es el emulador elegido para hacer estas pruebas y está disponible para MS-DOS

    Cuenta con dos versiones, una para modo real y otra para modo protegido y a también una versión para Windows.

    Al estar programado en lenguaje ensamblador, cuenta con versiones optimizadas para 386 y 486, e incluso para IBM PC XT/AT.

    Ofrece la posibilidad de emular todo tipo de máquinas Amstrad:

    • Amstrad 464
    • Amstrad 664
    • Amstrad 6128
    • Amstrad 6128+

    Incluye un depurador muy potente, algo ideal si eres programador de Z80. La velocidad y sonido son totalmente configurables asi como la gestión de todas las unidades de disco.

    Pruebas realizadas
    • DOSBOX: No he tenido ningún problema, se puede utilizar y jugar perfectamente.
    • PC: Funciona perfectamente tanto el vídeo como el sonido.

    Descargar emulador NO$CPC para MS-DOS..





Espero que les haya gustado esta pequeña recopilación de emuladores para MS-DOS. Como vemos esto de emular consolas no es algo nuevo, hace más de 3 decadas que la gente ha estado creando este tipo de programas para

lunes, 17 de agosto de 2026

Listado de funciones para Arduino y ESP32

Listado de funciones para Arduino y ESP32

Muy buenas a todos y todas!!!

Como memorizar todos los comandos con sus variantes en el IDE de Arduino he creado esta lista con los comandos más habituales, las variables que admite la función y si nos retorna algún tipo de dato.

Cada comando aparece en un bloque con un color que indica el periférico al que pertenece GPIO, ADC, I2C, etc.

Empecemos!!!



Descripción de los comandos


🔹 abs(int x)

Periférico: Utilidad / Matemáticas

Variables:

  • int x: Número entero del que se quiere obtener el valor absoluto.

Descripción: Calcula el valor absoluto de un número entero. Por ejemplo, el valor absoluto de -5 es 5.

Devuelve: Valor absoluto de un número entero.

Ejemplo:


x = -42;
Serial.println(abs(x)); // el valor absoluto de -42  
Serial Monitor: 42
  

🔹 int analogRead()

Periférico: ADC

Variables:

  • uint8_t pin: Número del pin ADC.

Descripción: Lee el valor raw (sin calibrar) del convertidor analógico-digital.

Devuelve: Un valor de 12 bits (0 a 4095).

Ejemplo:


int val = analogRead(analogPin);  // Lectura de un pin analógico  
  

🔹 uint32_t analogReadMilliVolts()

Periférico: ADC

Variables: Ninguna

  • uint8_t pin: Número del pin ADC.

Descripción: Lee el valor del ADC y lo convierte directamente a milivoltios (valor calibrado). Ideal para obtener lecturas en unidades de voltaje sin necesidad de hacer cálculos manuales.

Ejemplo:


uint32_t mv  = analogReadMilliVolts(PIN_ADC); // Lee y convierte automáticamente a milivoltios, considerando atenuación y calibración.  
  

🔹 analogReadResolution(uint8_t bits)

Periférico: ADC

Variables:

  • uint8_t bits: Resolución en bits (1 a 16).

Descripción: Cambia la resolución de analogRead(). El valor por defecto es 12 bits. Si se cambia a 10 bits, devolverá valores de 0 a 1023, igual que un Arduino clásico.

Ejemplo:


analogReadResolution(10);  // La lectura del pin ADC con una resolución de 10bits  
  

🔹 constrain()

Periférico: Utilidad / Matemáticas

Variables:

  • int/float amt: El valor a restringir [citation:2].
  • int/float low: Límite mínimo.
  • int/float high: Límite máximo.

Descripción: Restringe un número para que se mantenga dentro de un rango definido.

Devuelve: Si el valor es menor que el límite mínimo, devuelve el mínimo; si es mayor que el máximo, devuelve el limite máximo.

Ejemplo:


int sensVal = 50;
sensVal= constrain(sensVal, 10, 150);  // Limita el rango entre 10 y 150  
  

🔹 dacWrite(uint8_t pin, uint8_t value)

Periférico: DAC (Digital To Analog Converter)

Variables:

  • uint8_t pin: Pin DAC, solo disponibles en algunos ESP32.
  • uint8_t value: Valor de 0 a 255 (8 bits).

Descripción: Genera una salida analógica real en pines DAC (si el modelo lo soporta). Nota: No disponible en ESP32-C3.

Ejemplo:



#define DAC_PIN 25 // GPIO25 corresponde al Canal 1 del DAC

void loop() {
  dacWrite(DAC_PIN, 128); // Escribe un valor de 128 (1.65V)  
}

🔹 digitalWrite(uint8_t pin, uint8_t value)

Periférico: GPIO

Variables:

  • uint8_t pin: Número del pin GPIO.
  • uint8_t value: HIGH (1) o LOW (0).

Descripción: Escribe un valor HIGH o LOW en el pin digital especificado. Si el pin está configurado como salida, establecerá el voltaje correspondiente (3.3V o 0V).

Ejemplo:


digitalWrite(3, HIGH);  

// o

digitalWrite(3, LOW);  
  

🔹 digitalRead(uint8_t pin)

Periférico: GPIO

Variables:

  • uint8_t pin: Número del pin GPIO.

Descripción: Lee el estado de un pin digital.

Devuelve HIGH o LOW.

Ejemplo:


bool estado = digitalRead(3); // Lee el estado del pin 3  
  

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

Periférico: LEDC (PWM)

Variables:

  • uint8_t pin: Número del pin GPIO.
  • uint32_t freq: Frecuencia para PWM.
  • uint8_t resolution: Selecciona la resolución del canal LEDC. Rango 1-14 bits (1-20 bits para ESP32).

Descripción: Esta función se utiliza para configurar el pin LEDC con la frecuencia y resolución especificadas. El canal LEDC se seleccionará automáticamente.

Devuelve: true si la configuración es exitosa. Si se produce un error devuelve false y el canal LEDC no se configura.

Ejemplo:


const int ledPin = 16;
const int ledChannel = 0;
void setup(){
  ledcAttach(ledPin, freq, resolution);  
}
  


🔹 ledcAttachPin(uint8_t pin, uint8_t channel) deprecated

Periférico: LEDC (PWM)

Variables:

  • uint8_t pin: Número del pin GPIO.
  • uint8_t channel: Canal PWM previamente configurado.

Descripción: Asigna un pin GPIO a un canal PWM. El pin empezará a generar la señal PWM con los parámetros configurados en ledcSetup() deprecated.

Ejemplo:


const int ledPin = 16;
const int ledChannel = 0;
void setup(){
  ledcAttachPin(ledPin, ledChannel);  
}
  

🔹 ledcSetup(uint8_t channel, uint32_t freq, uint8_t resolution_bits) deprecated

Periférico: LEDC (PWM)

Variables:

  • uint8_t channel: Canal PWM (0-7 en ESP32-C3).
  • uint32_t freq: Frecuencia en Hz.
  • uint8_t resolution_bits: Resolución en bits (1 a 14 en ESP32-C3).

Descripción: Configura un canal PWM con una frecuencia y resolución determinadas. Debe llamarse antes de usar ledcAttachPin() deprecated.

Ejemplo:


const int ledChannel = 0;
const int freq = 5000; 
const int res = 8;
void setup() {
  ledcSetup(ledChannel, freq, res);  
}
  

🔹 ledcWrite(uint8_t channel, uint32_t duty)

Periférico: LEDC (PWM)

Variables:

  • uint8_t channel: Canal PWM.
  • uint32_t duty: Valor del ciclo de trabajo (0 a 2^resolution-1).

Descripción: Escribe un valor de ciclo de trabajo (duty cycle) en el canal PWM especificado, controlando así el ancho del pulso.

Ejemplo:


// Bucle
for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {  
  ledcWrite(ledPin, dutyCycle);
  delay(15);
}  
  

🔹 long map(long value, long fromLow, long fromHigh, long toLow, long toHigh)

Periférico: Utilidad

Variables:

  • long value: El número a re-mapear.
  • long fromLow: Límite inferior del rango de entrada.
  • long fromHigh: Límite superior del rango de entrada.
  • long toLow: Límite inferior del rango de salida.
  • long toHigh: Límite superior del rango de salida.

Descripción: Re-mapea un número desde un rango de entrada a un rango de salida. Se usa comúnmente para convertir valores de un ADC (0-4095) a un rango útil, como ángulos para un servo (0-180).

Devuelve: Valor long mapeado con un rango entre en limite inferior de salida y el limiter superior de salida.

Ejemplo:


int valor = map(val, 0, 1023, 0, 255); // Entrada 0-1023 ; Salida 0-255  
  

🔹 millis()

Periférico: Tiempo

Variables: Ninguna.

Descripción: Devuelve el número de milisegundos desde que el programa empezó a ejecutarse. Se almacena como un unsigned long, con un valor máximo de 4,294,967,296, lo que equivale a casi 50 días. Es la función recomendada para temporizaciones no bloqueantes.

Devuelve: El número de milisegundos desde que el programa empezó a ejecutarse.

Ejemplo:


unsigned long myTime;
myTime = millis();
Serial.println(myTime); // Imprime el tiempo desde que empezo el programa  

🔹 pinMode(uint8_t pin, uint8_t mode)

Periférico: GPIO

Variables:

  • uint8_t pin: Número del pin GPIO a configurar.
  • uint8_t mode: Modo de funcionamiento (INPUT, OUTPUT, INPUT_PULLUP, etc.).

Descripción: Configura el modo de un pin digital. Es una de las funciones más básicas del Arduino, idéntica en su uso al Arduino original pero adaptada a los pines del ESP32.

Ejemplo:


pinMode(13, OUTPUT);      // Pone el pin 13 como salida
pinMode(8, INPUT);        // Pone el pin 8 como entrada
pinMode(9, INPUT_PULLUP); // Pone el pin 9 como entrada con resistencia interna  
  

🔹 random(long max, long min)

Periférico: Utilidad

Variables:

  • long max: Límite superior exclusivo.
  • long min: Límite inferior inclusivo (opcional).

Descripción: Genera un número pseudo-aleatorio dentro de un rango. En el ESP32, usa el generador de números aleatorios por hardware (HW RNG) para mejorar la aleatoriedad.

Ejemplo:


long randNumber;
randNumber = random(300); // Carga un número random entre 0 y 299

o

randNumber = random(100, 220); // Carga un número random entre 100 y 219  
  

🔹 randomSeed(long seed)

Periférico: Utilidad

Variables:

  • long seed: Número que sirve como semilla para el generador pseudo-aleatorio.

Descripción: Inicializa el generador de números pseudo-aleatorios para que comience en un punto arbitrario de su secuencia. Si se usa una semilla fija, la secuencia de números será siempre la misma.

Devuelve: long con semilla.

Ejemplo:


long randNumber = random(300);  
Serial.println(randNumber);
  

🔹 SPI.begin(int sck, int miso, int mosi, int ss)

Periférico: SPI

Variables:

  • int sck (opcional): Pin de reloj SPI.
  • int miso (opcional): Pin MISO (Master In Slave Out).
  • int mosi (opcional): Pin MOSI (Master Out Slave In).
  • int ss (opcional): Pin SS (Slave Select).

Descripción: Inicializa el periférico SPI en modo maestro. Si no se especifican pines, usa los valores por defecto.

Ejemplo:


const int SCK_PIN = 18;
const int MISO_PIN = 19;
const int MOSI_PIN = 23;
const int SS_PIN = 5;

void setup() {
  // Inicializa SPI con un mapeo de pines personalizado 
  SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN, SS_PIN);
}
  

🔹 SPI.transfer(uint8_t data)

Periférico: SPI

Variables:

  • uint8_t data: Byte a enviar.

Descripción: Envía un byte y recibe simultáneamente otro byte (comunicación full-duplex). Es la función principal para comunicarse con periféricos SPI.

Ejemplo:


uint8_t valor = 0;
SPI.transfer(valor);  
  

🔹 Serial.begin(uint32_t baudrate)

Periférico: UART (Serial)

Variables:

  • uint32_t baudrate: Velocidad de transmisión en baudios (ej. 115200).

Descripción: Inicializa el puerto serie con la velocidad de baudios especificada. Es la función más usada para depuración y comunicación con el PC.

Ejemplo:


Serial.begin(9600);

// O

Serial.begin(38400);

// O

Serial.begin(115200);  

// O

Serial.begin(4800);  
  

🔹 Serial.print(ver descripción)

Periférico: UART (Serial)

Variables:

  • any data: Dato a imprimir (string, int, float, etc.).

Descripción: Envía texto o datos al puerto serie, sin añadir un salto de línea al final. Ideal para construir mensajes de depuración.
Un segundo parámetro opcional especifica la base (formato) a utilizar; los valores permitidos son: BIN (binario o base 2), OCT (octal o base 8), DEC (decimal o base 10), HEX (hexadecimal o base 16).
Para números de coma flotante, este parámetro especifica el número de decimales que se utilizarán.

Ejemplo:


Serial.print(78, BIN); // Imprime "1001110"

Serial.print(78, OCT); // Imprime "116"

Serial.print(78, DEC)  // Imprime "78"

Serial.print(78, HEX)  // Imprime "4E"

Serial.print(1.23456, 0)  // Imprime "1"

Serial.print(1.23456, 2)  // Imprime "1.23"

Serial.print(1.23456, 4)  // Imprime "1.2346"  
  

🔹 Serial.println(ver descripción)

Periférico: UART (Serial)

Variables:

  • any data: Dato a imprimir (string, int, float, etc.).

Descripción: Igual que Serial.print(), pero añade un salto de línea (\r\n) al final. Útil para mensajes en líneas separadas. También le podemos indicar el tipo de dato con:

  • DEC - Un decimal ASCII-encoded
  • HEX - Un hexadecimal ASCII-encoded
  • OCT - Un octal ASCII-encoded
  • BIN - Un binario ASCII-encoded

Ejemplo:


Serial.println(analogValue);       // Imprime un decimal en ASCII-encoded
Serial.println(analogValue, DEC);  // Imprime un decimal en ASCII-encoded
Serial.println(analogValue, HEX);  // Imprime un hexadecimal en ASCII-encoded  
Serial.println(analogValue, OCT);  // Imprime un octal en ASCII-encoded
Serial.println(analogValue, BIN);  // Imprime en binario en ASCII-encoded  
  

🔹 Serial.available()

Periférico: UART (Serial)

Variables: Ninguna.

Devuelve: El número de bytes disponibles para leer en el buffer del puerto serie. Útil para comprobar si ha llegado algún dato.

Ejemplo:


// Cuando los bytes disponibles son mayores que 0
if (Serial.available() > 0) {
  // Leemos el byte de entrada:
  int incomingByte = Serial.read();
}
  

🔹 int Serial.read()

Periférico: UART (Serial)

Variables: Ninguna.

Descripción: Lee un byte del buffer del puerto serie.

Devuelve: El primer byte de los datos seriales entrantes disponibles o -1 si no hay datos disponibles.

Ejemplo:


int incomingByte = Serial.read();  
  

🔹 timerBegin(uint32_t frequency)

Periférico: Timer

Variables:

  • uint32_t frequency: Frecuencia del temporizador en Hz.

Descripción: Configura y pone en marcha un temporizador de hardware con la frecuencia especificada. El temporizador se inicia automáticamente.

Devuelve: La estructura del temporizador si la configuración es correcta. Si se produce un erro devuelve NULL y el temporizador no se configura.

Ejemplo:


int timer = timerBegin(1000000);  // Configuramos la frecuencia a 1Mhz  
  

🔹 timerAttachInterrupt(hw_timer_t * timer, void (*fn)(void))

Periférico: Timer

Variables:

  • hw_timer_t *timer: Función a ejecutar cuando se dispara el temporizador.
  • void (*fn)(void): Función a ejecutar cuando se dispara el temporizador.

Descripción: Asocia una función de interrupción al temporizador. La función se ejecutará cada vez que el temporizador alcance el valor de alarma.

Ejemplo:


hw_timer_t *timer = NULL;

void ARDUINO_ISR_ATTR onTimer() {  
  // Código
}

void setup(){

  timer = timerBegin(1000000);  // Configuramos la frecuencia a 1Mhz

  // Attach onTimer function to our timer.
  timerAttachInterrupt(timer, &onTimer);
}
  

🔹 bool rmtInit(int pin, rmt_ch_dir_t channel_direction, rmt_reserve_memsize_t memsize, uint32_t frequency_Hz);

Periférico: RMT (Remote Control Transceiver)

Variables:

  • int pin: Pin GPIO a configurar.
  • rmt_ch_dir_t channel_direction: true para transmisor, false para receptor. También se puede usar: RMT_RX_MODE o RMT_TX_MODE.
  • rmt_reserve_memsize_t memsize: Número de bloques de memoria reservado para este canal:
    • RMT_MEM_NUM_BLOCKS_1
    • RMT_MEM_NUM_BLOCKS_2
    • RMT_MEM_NUM_BLOCKS_3 - Solo ESP32
    • RMT_MEM_NUM_BLOCKS_4 - Solo ESP32
    • RMT_MEM_NUM_BLOCKS_5 hasta RMT_MEM_NUM_BLOCKS_5 - Solo ESP32
    .
  • uint32_t frequency_Hz: Frecuencia del canal RMT en Hz (frecuencia de tick). Debe estar entre 312,5 kHz y 80 MHz.
    La frecuencia determina la resolución de la duración de los pulsos. Por ejemplo:
    • 10 MHz (tick de 100 ns) - Alta precisión, ideal para LEDs WS2812.
    • 1 MHz (tick de 1 µs) - Adecuado para la mayoría de los protocolos IR
    • 400 kHz (tick de 2,5 µs) - Adecuado para protocolos más lentos.

Devuelve: Esta función devuelve true si la inicialización es exitosa, false en caso contrario.

Descripción: Inicializa el periférico RMT (Remote Control), útil para generar y recibir señales con temporización precisa como infrarrojos o para controlar NeoPixels.

Ejemplo:


#define RMT_FREQ_HZ 10000000  // 10 MHz (tick de 100 ns)
bool isOk = rmtInit(4, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, RMT_FREQ_HZ));  
  

🔹 bool rmtWrite(int pin, rmt_data_t *data, size_t num_rmt_symbols, uint32_t timeout_ms);

Periférico: RMT (Remote Control Transceiver)

Variables:

  • int pin: Pin GPIO configurado en moto TX para RMT TX mode.
  • rmt_data_t * data: punter al array de datos a transmitir.
  • size_t * num_rmt_symbols: Longitud del array.
  • uint32_t timeout_ms: Tiempo de espera en milisegundos. Podemos usar RMT_WAIT_FOR_EVER para una espera indefinida..

Descripción: Transmite datos a través del periférico RMT. Es la función principal para enviar señales codificadas.

Ejemplo:


bool rmtWrite(int pin, rmt_data_t *data, size_t num_rmt_symbols, uint32_t timeout_ms);  
  

🔹 int WiFi.begin(const char* ssid, const char* password)

Periférico: Wi-Fi

Variables:

  • const char* ssid: Nombre de la red Wi-Fi.
  • const char* password: Contraseña de la red.

Descripción: Conecta el ESP32 a una red Wi-Fi como estación (cliente).

Devuelve: WL_CONNECTED cuando la conexión se ha establecido correctamente.

Ejemplo:


int status = WiFi.begin("YOUR_SSID", "YOUR_PASS");  // Configuración para conectar router 
  

🔹 WiFi.localIP()

Periférico: Wi-Fi

Variables: Ninguna.

Descripción: Muestra la dirección IP local asignada al ESP32 en la red Wi-Fi a la que está conectado.

Devuelve: La dirección IP local asignada al ESP32

Ejemplo:


Serial.println(WiFi.localIP());

o

String localip = WiFi.localIP().toString(); // Cambio a String de la IP  
Serial.println(localip);	// Imprime el String "localip"
  

🔹 Wire.begin(int sdaPin, int sclPin, uint32_t frequency)

Periférico: I2C (Wire)

Variables:

  • int sdaPin (opcional): Pin para SDA.
  • int sclPin (opcional): Pin para SCL.
  • uint32_t frequency (opcional): Frecuencia del bus I2C (por defecto 100kHz).

Descripción: Inicializa el periférico I2C en modo maestro. Si no se especifican pines, usa los valores por defecto de la placa.

Ejemplo:


Wire.begin(0x3C); // Inicia el periferico con la dirección hex_address  
  

🔹 Wire.beginTransmission(uint16_t address)

Periférico: I2C (Wire)

Variables:

  • uint16_t address: Dirección del dispositivo esclavo (7 o 10 bits).

Descripción: Inicia una transmisión al dispositivo esclavo con la dirección especificada. Tras llamarla, se pueden escribir datos con Wire.write().

Ejemplo:


Wire.beginTransmission(0x3C);  // Inicia los datos desde el I2C principal al secundario  
  

🔹 int Wire.endTransmission(bool sendStop)

Periférico: I2C (Wire)

Variables:

  • bool sendStop (opcional): Si es true (por defecto), envía la señal STOP.

Descripción: Envía los datos del búfer al esclavo.

Devuelve: 0 si todo es correcto, o un código de error si falla.

Ejemplo:


int isOk = Wire.endTransmission(true);  // Envía la señal de stop y carga la respuesta en la variable "isOk"  
  

🔹 int Wire.requestFrom(uint16_t address, uint8_t size, bool sedStop)

Periférico: I2C (Wire)

Variables:

  • uint16_t address: Dirección del esclavo.
  • uint8_t size: Número de bytes a solicitar.
  • bool sendStop (opcional): Si envía o no la señal STOP.

Descripción: Solicita datos del dispositivo esclavo.

Devuelve: El número de bytes leídos. Los datos se pueden leer con Wire.read().

Ejemplo:


int nBytes = Wire.requestFrom(address, quantity, stop); // Solicita una cantidad de bytes a una dirección y deja libre el canal I2C  
  

Y hasta aquí llegamos, estas son las funciones más comunes que usamos de manera general en el IDE de Arduino


Si te interesa más información sobre Arduino y su IDE, aquí tienes otros articulos relacionados:


Saludos!!!


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: