RELOJ DIGITAL DE CUATRO DÍGITOS CON ESP8266: MULTIPLEXADO Y PUESTA EN HORA CON DOS PULSADORES
RELOJ DIGITAL DE CUATRO DÍGITOS CON ESP8266: MULTIPLEXADO Y PUESTA EN HORA CON DOS PULSADORES
1. Introducción
En la entrada anterior, Control de dos semáforos mediante Arduino UNO y ESP8266, vimos que un mismo programa puede controlar un circuito sin importar la placa que se utilice, siempre que se asignen correctamente los pines. Allí el programa solo encendía y apagaba seis LED con retardos fijos.
En esta práctica damos un paso más. Construiremos un reloj digital de 24 horas con cuatro displays de siete segmentos y dos puntos separadores que parpadean cada segundo. Además, con dos pulsadores (B1 y B2) podremos poner el reloj en hora.
Aparecen varias ideas nuevas respecto al semáforo:
- Multiplexado de displays: cuatro displays comparten las mismas siete líneas de segmentos y se encienden de a uno, tan rápido que el ojo los percibe encendidos a la vez.
- Medición del tiempo con
millis(): el reloj sigue contando aunque el programa esté haciendo otras cosas, algo quedelay()no permite. - Lectura de pulsadores con antirrebote y una pequeña máquina de estados para el modo de configuración.
Igual que en la práctica anterior, para la simulación en Proteus el ESP8266 (módulo NodeMCU V3) se programa como si fuera un Arduino UNO, seleccionando la placa Arduino Uno en el Arduino IDE y cargando el archivo compilado en el modelo de Proteus. Más adelante veremos qué cambios hay que hacer para armar el circuito real en una protoboard con un ESP8266.
2. Descripción del circuito
El circuito está formado por:
- Cuatro displays de 7 segmentos de cátodo común. Los segmentos
aagde los cuatro displays están unidos entre sí (las mismas siete líneas llegan a todos). El cátodo común de cada display va a tierra a través de un transistor. - Cuatro transistores 2N2222 (Q1 a Q4) con una resistencia de 1 kΩ en la base. Cada transistor funciona como interruptor de un display: si su base recibe un nivel alto, el display correspondiente queda conectado a tierra y puede encenderse.
- Dos LED (D1 y D2) con una resistencia de 220 Ω (R5) que forman los dos puntos del reloj.
- Dos pulsadores (B1 y B2) conectados a tierra. Al presionarlos, la entrada correspondiente recibe un nivel bajo.
- El módulo NodeMCU V3 (ESP8266) que controla todo.
La correspondencia entre transistores y dígitos de la hora es:
| Transistor | Etiqueta | Dígito que controla |
|---|---|---|
| Q4 | Dh | Decena de hora |
| Q3 | Uh | Unidad de hora |
| Q2 | Dmin | Decena de minuto |
| Q1 | Umin | Unidad de minuto |
| (sin transistor) | Useg | Dos puntos (D1 y D2) |
Importante: el nombre «Useg» (unidad de segundo) se conserva de una versión anterior del circuito. En este reloj la señal Useg no controla un dígito, sino los dos puntos.
2.1. ¿Por qué multiplexar?
Si cada display tuviera sus propias siete líneas, necesitaríamos 4 × 7 = 28 salidas. Con el multiplexado solo necesitamos 7 líneas de segmentos más 4 líneas de selección de dígito, es decir, 11 salidas.
El principio es el siguiente. En cada instante solo un display está encendido:
- Se apagan todos los segmentos.
- Se colocan en las líneas de segmentos las formas del número que corresponde al primer display.
- Se activa solo el transistor de ese display durante unos 3 milisegundos.
- Se desactiva ese transistor y se repite el proceso con el segundo display, luego el tercero y el cuarto.
Cada display se enciende 3 ms de cada 12 ms, es decir, unas 83 veces por segundo. Esa rapidez, junto con la persistencia de la visión del ojo humano, hace que veamos los cuatro números encendidos y estables.
3. Funcionamiento: cómo se pone en hora
Con el reloj en marcha normal, los dos puntos parpadean cada segundo. Los pulsadores funcionan así:
| Situación | Pulsador | Resultado |
|---|---|---|
| Funcionamiento normal | B1 | Entra al modo de configuración. Parpadea la decena de hora (Dh). |
| Funcionamiento normal | B2 | No hace nada. |
| Configurando (parpadea un dígito) | B2 | Aumenta en 1 el dígito que parpadea. Al llegar a su máximo vuelve a 0. |
| Configurando | B1 | Pasa al siguiente dígito: Dh → Uh → Dmin → Umin. Tras Umin, sale del modo de configuración. |
Durante la configuración, el reloj se detiene y los dos puntos permanecen encendidos fijos, para que el parpadeo del dígito en edición se distinga bien. Al salir, los segundos vuelven a cero y el reloj reanuda la cuenta. Por eso conviene presionar B1 por última vez en el instante exacto en que se desea comenzar.
Los rangos de cada dígito son:
| Dígito | Rango | Observación |
|---|---|---|
| Dh (decena de hora) | 0 a 2 | Si Dh pasa a 2 y Uh era mayor que 3, Uh se corrige a 3 (así nunca aparece 24 a 29). |
| Uh (unidad de hora) | 0 a 9, o 0 a 3 si Dh = 2 | El máximo depende de Dh. |
| Dmin (decena de minuto) | 0 a 9 (ver nota) | Se recomienda limitarla a 0 a 5. Ver la sección 10. |
| Umin (unidad de minuto) | 0 a 9 |
4. Asignación de pines en la simulación (Proteus)
Para la simulación se programa el módulo NodeMCU como un Arduino UNO. En el modelo de Proteus las etiquetas D0 a D9 del módulo responden como los pines digitales 0 a 9 del programa. Los cuatro pines de la fila superior a los que se conectaron Dh, Uh, B1 y B2 responden en el programa como los pines analógicos A0 a A3, usados aquí como entradas y salidas digitales.
| Señal del circuito | Pin en el programa (Arduino UNO) | Función |
|---|---|---|
| a | 9 | Segmento a |
| b | 8 | Segmento b |
| c | 7 | Segmento c |
| d | 6 | Segmento d |
| e | 5 | Segmento e |
| f | 4 | Segmento f |
| g | 3 | Segmento g |
| Dh | A0 | Base de Q4 (decena de hora) |
| Uh | A1 | Base de Q3 (unidad de hora) |
| Dmin | 1 | Base de Q2 (decena de minuto) |
| Umin | 2 | Base de Q1 (unidad de minuto) |
| Useg | 0 | Dos puntos |
| B1 | A2 | Pulsador de configuración |
| B2 | A3 | Pulsador de incremento |
Los pines 0 y 1 de un Arduino UNO real son RX y TX del puerto serie. En la simulación no hay problema porque el programa no usa Serial.
5. Asignación de pines para armar el circuito real con ESP8266
En una protoboard con un NodeMCU real no se pueden usar los números del programa tal cual, por tres motivos:
- El ESP8266 no tiene los pines D0 a D9 del Arduino UNO. Tiene sus propios GPIO, y la etiqueta impresa en la placa (D0, D1, ...) no coincide con el número GPIO.
- El pin A0 del ESP8266 es la entrada del conversor analógico: solo sirve como entrada y no puede entregar un nivel alto o bajo. Por eso el programa de la simulación (que usa A0 como salida para Dh) no puede cargarse sin cambios en la placa real.
- El ESP8266 trabaja a 3,3 V y algunos pines tienen funciones especiales durante el arranque (GPIO0, GPIO2 y GPIO15).
Este circuito necesita 12 salidas digitales y 2 entradas, es decir 14 líneas. El NodeMCU ofrece 13 pines digitales utilizables y el pin analógico A0. La solución que se propone es usar A0 como entrada del pulsador B2 (leyéndolo con analogRead()) y aprovechar todos los demás pines. Esto obliga a usar algunos pines delicados (RX, TX, SD2 y SD3), pero en este circuito resultan seguros: los usamos para las líneas de segmentos, que pasan por una resistencia y un LED y no tienen corriente mientras ningún transistor esté conduciendo.
| Señal | Pin en la simulación (Arduino UNO) | Etiqueta en el NodeMCU | GPIO | Observación |
|---|---|---|---|---|
| a | 9 | D0 | GPIO16 | |
| b | 8 | D4 | GPIO2 | Comparte el LED azul de la placa. Es normal que parpadee al arrancar. |
| c | 7 | D8 | GPIO15 | Debe estar en nivel bajo al arrancar. Con la resistencia y el LED en serie no hay problema. |
| d | 6 | RX | GPIO3 | No usar Serial en este programa. |
| e | 5 | TX | GPIO1 | Puede parpadear un instante al arrancar. No usar Serial. |
| f | 4 | SD2 | GPIO9 | En el Arduino IDE configurar Flash Mode en DIO. |
| g | 3 | SD3 | GPIO10 | En el Arduino IDE configurar Flash Mode en DIO. |
| Dh (base de Q4) | A0 | D5 | GPIO14 | |
| Uh (base de Q3) | A1 | D6 | GPIO12 | |
| Dmin (base de Q2) | 1 | D7 | GPIO13 | |
| Umin (base de Q1) | 2 | D1 | GPIO5 | |
| Useg (dos puntos) | 0 | D2 | GPIO4 | |
| B1 | A2 | D3 | GPIO0 | Pulsador a GND. Ya tiene resistencia de pull-up en la placa. Se puede usar INPUT_PULLUP. |
| B2 | A3 | A0 | ADC | Pulsador a GND con una resistencia externa de 10 kΩ a 3,3 V. Se lee con analogRead(). |
¿Por qué las bases de los transistores no van en D3 ni en D4? Las resistencias de 1 kΩ de las bases llevarían el pin cerca de 0,7 V en el arranque. GPIO0 y GPIO2 deben estar en nivel alto al encender la placa; si no lo están, el ESP8266 entra en modo de grabación o no arranca. Por eso esas dos líneas se reservaron para un segmento y para el pulsador.
Los cambios exactos que deben hacerse al programa se indican en la sección 9.
5.1. Detalles de hardware para la versión real
- Resistencias en los segmentos: en el simulador no se necesitan, pero en el circuito real cada línea de segmento debe llevar una resistencia de 220 Ω en serie antes de llegar a los displays. Sin ellas se dañan los LED y el ESP8266.
- Dos puntos: dos LED rojos en serie necesitan cerca de 3,6 V y el pin del ESP8266 entrega 3,3 V, por lo que no encenderían. En la versión real conecte cada LED con su propia resistencia de 330 Ω, en paralelo entre sí, desde el pin Useg hacia GND.
- Transistores: con 3,3 V y una resistencia de base de 1 kΩ la corriente de base es de unos 2,6 mA, suficiente para que el 2N2222 conduzca la corriente de los segmentos de un display.
- Tierra común: las tierras del ESP8266, los transistores y los pulsadores deben unirse.
6. Flujograma general del programa
El siguiente flujograma muestra el programa completo. Los bloques con doble contorno son subrutinas (funciones) que se detallan en flujogramas independientes. Los colores y las letras (Parte A a F) se mantienen en toda la explicación para ubicar cada sección del código.
Figura 1. Flujograma general del programa.
| Parte | Color | Qué hace | Función en el código |
|---|---|---|---|
| A | Azul | Configura los pines | setup() |
| B | Naranja | Lee los pulsadores | leerBotones() y pulsado() |
| C | Verde | Hace avanzar la hora | actualizarReloj() |
| D | Violeta | Prepara lo que se mostrará | Primera mitad de loop() |
| E | Violeta | Enciende los displays por turnos | Ciclo for final de loop(), mostrarNumero() y apagarSegmentos() |
| F | Rojo | Modifica un dígito en la puesta en hora | incrementarDigito() |
7. Código fuente completo (versión para simulación en Proteus)
Este es el programa que funcionó en el simulador. En el Arduino IDE se selecciona la placa Arduino Uno, se compila, y el archivo .hex generado se carga en las propiedades del módulo NodeMCU de Proteus.
// RELOJ DIGITAL 4 DISPLAYS + DOS PUNTOS + AJUSTE DE HORA
// ESP8266 programado como Arduino UNO - Proteus
// Displays 7 segmentos CATODO COMUN, multiplexados con 2N2222
// ---------- SEGMENTOS ----------
const int SEG_A = 9;
const int SEG_B = 8;
const int SEG_C = 7;
const int SEG_D = 6;
const int SEG_E = 5;
const int SEG_F = 4;
const int SEG_G = 3;
// ---------- DIGITOS (HIGH = encendido) ----------
const int DIG_DH = A0; // Decena de hora (Q4)
const int DIG_UH = A1; // Unidad de hora (Q3)
const int DIG_DMIN = 1; // Decena de minuto (Q2)
const int DIG_UMIN = 2; // Unidad de minuto (Q1)
const int DOTS = 0; // Dos puntos (Useg)
// ---------- BOTONES ----------
const int BTN1 = A2; // B1: entrar / avanzar / salir de configuracion
const int BTN2 = A3; // B2: incrementar el digito que parpadea
// ---------- LIMITES ----------
const int MAX_DH = 2; // Decena de hora: 0..2 (permite 20-23)
const int MAX_DMIN = 9; // Decena de minuto: 0..9 (usa 5 para solo 0..59)
const int segPins[7] = {SEG_A, SEG_B, SEG_C, SEG_D, SEG_E, SEG_F, SEG_G};
const int digPins[4] = {DIG_DH, DIG_UH, DIG_DMIN, DIG_UMIN};
// Tabla 0-9, bits: gfedcba
const byte numeros[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
// ---------- HORA ----------
int horas = 12;
int minutos = 0;
int segundos = 0;
unsigned long ultimoSegundo = 0;
// ---------- MODO CONFIGURACION ----------
// 0 = normal, 1 = Dh, 2 = Uh, 3 = Dmin, 4 = Umin
int modo = 0;
// ---------- ANTIREBOTE ----------
bool b1Previo = HIGH, b2Previo = HIGH;
unsigned long b1Tiempo = 0, b2Tiempo = 0;
const unsigned long REBOTE = 30;
void setup() {
for (int i = 0; i < 7; i++) {
pinMode(segPins[i], OUTPUT);
digitalWrite(segPins[i], LOW);
}
for (int i = 0; i < 4; i++) {
pinMode(digPins[i], OUTPUT);
digitalWrite(digPins[i], LOW);
}
pinMode(DOTS, OUTPUT);
digitalWrite(DOTS, LOW);
pinMode(BTN1, INPUT_PULLUP);
pinMode(BTN2, INPUT_PULLUP);
}
void mostrarNumero(int n) {
byte patron = numeros[n];
for (int i = 0; i < 7; i++) {
digitalWrite(segPins[i], (patron >> i) & 1);
}
}
void apagarSegmentos() {
for (int i = 0; i < 7; i++) digitalWrite(segPins[i], LOW);
}
// Devuelve true una sola vez por cada pulsacion (flanco a LOW)
bool pulsado(int pin, bool &previo, unsigned long &tiempo) {
bool lectura = digitalRead(pin);
bool evento = false;
if (lectura != previo && (millis() - tiempo) > REBOTE) {
tiempo = millis();
previo = lectura;
if (lectura == LOW) evento = true;
}
return evento;
}
void actualizarReloj() {
if (modo != 0) return; // En configuracion el reloj se detiene
if (millis() - ultimoSegundo >= 1000) {
ultimoSegundo += 1000;
segundos++;
if (segundos >= 60) {
segundos = 0;
minutos++;
if (minutos >= 60) {
minutos = 0;
horas++;
if (horas >= 24) horas = 0;
}
}
}
}
void incrementarDigito() {
int dh = horas / 10;
int uh = horas % 10;
int dmin = minutos / 10;
int umin = minutos % 10;
switch (modo) {
case 1: // Decena de hora
dh = (dh + 1) % (MAX_DH + 1);
if (dh == 2 && uh > 3) uh = 3; // evita 24-29
break;
case 2: { // Unidad de hora
int maxUh = (dh == 2) ? 3 : 9;
uh = (uh + 1) % (maxUh + 1);
break;
}
case 3: // Decena de minuto
dmin = (dmin + 1) % (MAX_DMIN + 1);
break;
case 4: // Unidad de minuto
umin = (umin + 1) % 10;
break;
}
horas = dh * 10 + uh;
minutos = dmin * 10 + umin;
}
void leerBotones() {
if (pulsado(BTN1, b1Previo, b1Tiempo)) {
modo++;
if (modo > 4) { // Sale de configuracion
modo = 0;
segundos = 0;
ultimoSegundo = millis(); // El reloj reinicia desde :00
}
}
if (pulsado(BTN2, b2Previo, b2Tiempo)) {
if (modo != 0) incrementarDigito();
}
}
void loop() {
leerBotones();
actualizarReloj();
int digitos[4] = {
horas / 10,
horas % 10,
minutos / 10,
minutos % 10
};
// Dos puntos: parpadean cada segundo en modo normal, fijos en configuracion
if (modo == 0) digitalWrite(DOTS, (segundos % 2 == 0) ? HIGH : LOW);
else digitalWrite(DOTS, HIGH);
// Parpadeo del digito en edicion (~300 ms encendido / 300 ms apagado)
bool parpadeoApagado = (modo != 0) && ((millis() / 300) % 2 == 1);
// Multiplexado
for (int i = 0; i < 4; i++) {
apagarSegmentos();
bool apagarEste = parpadeoApagado && (i == modo - 1);
if (!apagarEste) {
mostrarNumero(digitos[i]);
digitalWrite(digPins[i], HIGH);
}
delay(3);
digitalWrite(digPins[i], LOW);
}
}8. Explicación del código, parte por parte
8.1. Declaración de pines y constantes
Esta sección está al principio del programa y define a qué pin va conectada cada señal.
const int SEG_A = 9;
const int SEG_B = 8;
const int SEG_C = 7;
const int SEG_D = 6;
const int SEG_E = 5;
const int SEG_F = 4;
const int SEG_G = 3;const: indica que el valor no cambiará mientras el programa se ejecuta. Es una buena práctica para los pines porque evita modificarlos por error.int: tipo de dato entero.SEG_A = 9;: crea un nombre (SEG_A) para el número 9. Cuando más adelante el programa diceSEG_A, en realidad se refiere al pin 9, donde está conectado el segmento a.
La ventaja es que si cambia el cableado, solo hay que modificar este número en un lugar y no en todo el programa. Esto es justamente lo que se hará al pasar al ESP8266 real.
const int DIG_DH = A0; // Decena de hora (Q4)
const int DIG_UH = A1; // Unidad de hora (Q3)
const int DIG_DMIN = 1; // Decena de minuto (Q2)
const int DIG_UMIN = 2; // Unidad de minuto (Q1)
const int DOTS = 0; // Dos puntos (Useg)Son los pines que controlan las bases de los transistores (uno por dígito) y el pin de los dos puntos. Un nivel HIGH en la base del transistor lo hace conducir y enciende ese display. A0 y A1 son nombres que Arduino reconoce como pines analógicos, pero aquí se usan como salidas digitales.
const int BTN1 = A2;
const int BTN2 = A3;
const int MAX_DH = 2;
const int MAX_DMIN = 9;BTN1 y BTN2 son los pines de los pulsadores B1 y B2. MAX_DH y MAX_DMIN son el valor máximo que puede tomar la decena de hora y la decena de minuto en la puesta en hora.
8.2. Arreglos de pines y tabla de números
const int segPins[7] = {SEG_A, SEG_B, SEG_C, SEG_D, SEG_E, SEG_F, SEG_G};
const int digPins[4] = {DIG_DH, DIG_UH, DIG_DMIN, DIG_UMIN};Un arreglo es una lista de valores a los que se accede por su posición. Se empieza a contar desde 0:
segPins[0]esSEG_A,segPins[1]esSEG_B...segPins[6]esSEG_G.digPins[0]es la decena de hora,digPins[1]la unidad de hora,digPins[2]la decena de minuto ydigPins[3]la unidad de minuto.
Gracias a los arreglos se pueden recorrer los siete segmentos o los cuatro dígitos con un ciclo for en vez de escribir siete o cuatro líneas casi iguales. Además, el orden de digPins es el mismo orden de izquierda a derecha en que aparecen los dígitos del reloj.
const byte numeros[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F
};Esta es la tabla que dice qué segmentos deben encenderse para dibujar cada número. Cada valor es un número de un byte escrito en hexadecimal (por eso empieza por 0x). Lo importante es verlo en binario: cada bit representa un segmento.
El bit 0 (el de más a la derecha) es el segmento a, el bit 1 es b, y así hasta el bit 6, que es g. El bit 7 no se usa. Por eso se anota «gfedcba»: es el orden de los bits de izquierda a derecha.
| Número | Valor hexadecimal | Binario (g f e d c b a) | Segmentos encendidos |
|---|---|---|---|
| 0 | 0x3F | 0 1 1 1 1 1 1 | a, b, c, d, e, f |
| 1 | 0x06 | 0 0 0 0 1 1 0 | b, c |
| 2 | 0x5B | 1 0 1 1 0 1 1 | a, b, d, e, g |
| 3 | 0x4F | 1 0 0 1 1 1 1 | a, b, c, d, g |
| 4 | 0x66 | 1 1 0 0 1 1 0 | b, c, f, g |
| 5 | 0x6D | 1 1 0 1 1 0 1 | a, c, d, f, g |
| 6 | 0x7D | 1 1 1 1 1 0 1 | a, c, d, e, f, g |
| 7 | 0x07 | 0 0 0 0 1 1 1 | a, b, c |
| 8 | 0x7F | 1 1 1 1 1 1 1 | todos |
| 9 | 0x6F | 1 1 0 1 1 1 1 | a, b, c, d, f, g |
Como la posición 0 de la tabla corresponde al número 0, la posición 7 al número 7, etc., para obtener el patrón del número n basta escribir numeros[n]. Por ejemplo, numeros[1] vale 0x06.
8.3. Variables de la hora, del modo y del antirrebote
int horas = 12;
int minutos = 0;
int segundos = 0;
unsigned long ultimoSegundo = 0;horas,minutosysegundosguardan la hora actual. Se inicializa en 12:00:00. Cambie estos valores si desea otra hora de arranque.unsigned long ultimoSegundoguarda el instante (en milisegundos desde que se encendió el sistema) en que se contó el último segundo. Es de tipounsigned longporquemillis()devuelve números muy grandes que no caben en unint.
int modo = 0;modo es la «memoria» de la máquina de estados:
| modo | Significado |
|---|---|
| 0 | Funcionamiento normal |
| 1 | Se edita Dh (parpadea la decena de hora) |
| 2 | Se edita Uh |
| 3 | Se edita Dmin |
| 4 | Se edita Umin |
bool b1Previo = HIGH, b2Previo = HIGH;
unsigned long b1Tiempo = 0, b2Tiempo = 0;
const unsigned long REBOTE = 30;Son los datos que necesita el antirrebote. b1Previo y b2Previo recuerdan el estado anterior de cada pulsador (HIGH significa suelto, porque se usa pull-up). b1Tiempo y b2Tiempo recuerdan cuándo ocurrió el último cambio, y REBOTE es el tiempo mínimo (30 ms) que debe pasar para aceptar un nuevo cambio.
8.4. Parte A: la función setup()
Figura 2. Parte A del flujograma general: setup().
setup() se ejecuta una sola vez, al encender el sistema o al iniciar la simulación.
void setup() {
for (int i = 0; i < 7; i++) {
pinMode(segPins[i], OUTPUT);
digitalWrite(segPins[i], LOW);
}for (int i = 0; i < 7; i++): repite lo que está entre llaves siete veces, conivaliendo 0, 1, 2, ..., 6. Es decir, recorre los siete segmentos.pinMode(segPins[i], OUTPUT);: configura el pin del segmento númeroicomo salida.digitalWrite(segPins[i], LOW);: lo deja inicialmente en nivel bajo, es decir, el segmento apagado.
for (int i = 0; i < 4; i++) {
pinMode(digPins[i], OUTPUT);
digitalWrite(digPins[i], LOW);
}Lo mismo para los cuatro pines de los transistores. En LOW los transistores no conducen, por lo que ningún display está encendido al iniciar.
pinMode(DOTS, OUTPUT);
digitalWrite(DOTS, LOW);
pinMode(BTN1, INPUT_PULLUP);
pinMode(BTN2, INPUT_PULLUP);
}- El pin de los dos puntos se configura como salida y se apaga.
INPUT_PULLUPconfigura un pin como entrada con una resistencia interna que lo conecta a nivel alto. Así, con el pulsador suelto el pin lee HIGH y al presionarlo (como está conectado a tierra) lee LOW. No se necesita resistencia externa en el circuito.
8.5. Funciones de la pantalla: mostrarNumero() y apagarSegmentos()
void mostrarNumero(int n) {
byte patron = numeros[n];
for (int i = 0; i < 7; i++) {
digitalWrite(segPins[i], (patron >> i) & 1);
}
}Esta función recibe un número n (de 0 a 9) y pone en las siete líneas de segmentos el dibujo de ese número. No enciende ningún display por sí sola: solo prepara las líneas de segmentos. Quien enciende el display es el transistor.
byte patron = numeros[n];: busca en la tabla el patrón del número.(patron >> i): desplaza los bits del patróniposiciones a la derecha, de modo que el bit que interesa queda en la última posición.& 1: se queda solo con ese último bit (un 0 o un 1).digitalWrite(segPins[i], ...): escribe ese 0 o 1 en el pin del segmento. El 1 equivale a HIGH, y el 0 a LOW.
Ejemplo con el número 2 (0x5B = 1011011 en binario). Cuando i vale 0, el bit 0 es 1, entonces el segmento a se enciende. Con i = 1, el bit 1 es 1 y se enciende b. Con i = 2, el bit 2 es 0 y c queda apagado. Así sucesivamente: los segmentos a, b, d, e y g encendidos forman el número 2.
void apagarSegmentos() {
for (int i = 0; i < 7; i++) digitalWrite(segPins[i], LOW);
}Pone las siete líneas de segmentos en LOW. Se usa antes de cambiar de dígito para evitar que el dibujo del número anterior se vea en el display siguiente (el efecto «fantasma»).
8.6. Parte B: lectura de pulsadores con antirrebote, función pulsado()
Un pulsador mecánico no pasa limpiamente de suelto a presionado: durante unos milisegundos «rebota» y genera varios cambios rápidos de nivel. Si el programa los contara todos, una sola pulsación podría interpretarse como varias. Esta función los filtra, y además informa una única vez por cada pulsación, sin repetirse mientras el botón se mantenga presionado.
Figura 3. Detalle de la Parte B: función pulsado() con antirrebote.
bool pulsado(int pin, bool &previo, unsigned long &tiempo) {
bool lectura = digitalRead(pin);
bool evento = false;
if (lectura != previo && (millis() - tiempo) > REBOTE) {
tiempo = millis();
previo = lectura;
if (lectura == LOW) evento = true;
}
return evento;
}bool pulsado(...): es una función que devuelve un valor lógico, verdadero (true) o falso (false). Verdadero significa «ocurrió una nueva pulsación».int pin: número del pin que se va a leer (BTN1 o BTN2).bool &previoyunsigned long &tiempo: el símbolo&significa que la función recibe la variable original y no una copia. Gracias a eso, cuando la función modificaprevioytiempo, los cambios se guardan enb1Previoyb1Tiempo(o en las de B2).bool lectura = digitalRead(pin);: lee el estado actual del pulsador (HIGH suelto, LOW presionado).bool evento = false;: por defecto se asume que no hubo pulsación nueva.if (lectura != previo && (millis() - tiempo) > REBOTE): pregunta dos cosas a la vez. ¿La lectura es distinta de la anterior? Y ¿pasaron más de 30 ms desde el último cambio aceptado? Solo si ambas son ciertas se acepta el cambio.millis()devuelve los milisegundos transcurridos desde el inicio.tiempo = millis();yprevio = lectura;: se guardan el instante del cambio y el nuevo estado.if (lectura == LOW) evento = true;: solo se informa pulsación cuando el cambio fue a LOW, es decir, al presionar. Cuando el botón se suelta, el cambio es a HIGH y no se informa nada.return evento;: entrega el resultado a quien llamó a la función.
8.7. Parte B: función leerBotones()
Figura 4. Parte B del flujograma general: leerBotones().
void leerBotones() {
if (pulsado(BTN1, b1Previo, b1Tiempo)) {
modo++;
if (modo > 4) {
modo = 0;
segundos = 0;
ultimoSegundo = millis();
}
}if (pulsado(BTN1, b1Previo, b1Tiempo)): ejecuta lo que sigue solo si B1 acaba de ser presionado.modo++;: suma 1 amodo. Esta es la acción principal de B1: 0 pasa a 1 (entra a configuración y parpadea Dh), 1 pasa a 2 (parpadea Uh), y así hasta 4.if (modo > 4): si se superó el último dígito, se sale de la configuración. Se vuelve amodo = 0, los segundos se ponen en 0 yultimoSegundose actualiza con el instante actual, para que el reloj reanude su cuenta desde cero de forma limpia.
if (pulsado(BTN2, b2Previo, b2Tiempo)) {
if (modo != 0) incrementarDigito();
}
}- Si B2 fue presionado, solo se hace algo cuando
modo != 0, es decir, cuando se está configurando. Así B2 no tiene ningún efecto en el funcionamiento normal. incrementarDigito()aumenta el dígito que está parpadeando (Parte F).
8.8. Parte C: función actualizarReloj()
Figura 5. Parte C del flujograma general: actualizarReloj().
void actualizarReloj() {
if (modo != 0) return;
if (millis() - ultimoSegundo >= 1000) {
ultimoSegundo += 1000;
segundos++;if (modo != 0) return;: si se está configurando, la función termina inmediatamente. Por eso el reloj se detiene mientras se pone en hora.if (millis() - ultimoSegundo >= 1000): calcula cuánto tiempo pasó desde el último segundo contado. Si pasó 1000 ms o más, corresponde sumar un segundo.ultimoSegundo += 1000;: avanza el «último segundo contado» exactamente 1000 ms. Se suma 1000 y no se copiamillis()para no perder el pequeño retraso acumulado en cada vuelta. Así el reloj no se atrasa.segundos++;: suma un segundo.
Este método reemplaza al delay(1000). Mientras delay() detiene todo el programa, aquí el programa nunca se detiene a esperar: solo consulta la hora de millis() y sigue con sus otras tareas (leer pulsadores y refrescar los displays).
if (segundos >= 60) {
segundos = 0;
minutos++;
if (minutos >= 60) {
minutos = 0;
horas++;
if (horas >= 24) horas = 0;
}
}
}
}Son tres condiciones encadenadas, como las ruedas de un odómetro:
- Si los segundos llegan a 60, se ponen en 0 y se suma un minuto.
- Si los minutos llegan a 60, se ponen en 0 y se suma una hora.
- Si las horas llegan a 24, vuelven a 0. Después de 23:59:59 sigue 00:00:00.
8.9. Parte F: función incrementarDigito()
Figura 6. Parte F del flujograma general: incrementarDigito().
void incrementarDigito() {
int dh = horas / 10;
int uh = horas % 10;
int dmin = minutos / 10;
int umin = minutos % 10;Primero la función separa la hora en sus cuatro dígitos, usando dos operaciones:
/ 10es la división entera: da la decena. Por ejemplo,17 / 10es 1.% 10es el resto de la división (módulo): da la unidad. Por ejemplo,17 % 10es 7.
Con las 17:45, quedan dh = 1, uh = 7, dmin = 4 y umin = 5.
switch (modo) {
case 1:
dh = (dh + 1) % (MAX_DH + 1);
if (dh == 2 && uh > 3) uh = 3;
break;switch (modo) elige qué bloque ejecutar según el valor de modo. Cada case es una opción, y break hace que termine el switch sin seguir con las otras opciones.
Caso 1 (se edita Dh): dh = (dh + 1) % (MAX_DH + 1) suma 1 y vuelve a 0 al pasarse del máximo. Con MAX_DH = 2 el cálculo es % 3, y los valores sucesivos son 0, 1, 2, 0, 1, 2... Si el nuevo Dh es 2 y la unidad de hora era mayor que 3 (por ejemplo, 19 pasaría a 29), se corrige uh a 3 para que la hora siga siendo válida (23).
case 2: {
int maxUh = (dh == 2) ? 3 : 9;
uh = (uh + 1) % (maxUh + 1);
break;
}Caso 2 (se edita Uh): el máximo de Uh depende de Dh. (dh == 2) ? 3 : 9 es una condición abreviada que significa «si dh es 2, el máximo es 3; si no, es 9». Luego se suma 1 con el mismo recurso del módulo. Las llaves { } del caso permiten declarar la variable local maxUh dentro de un case.
case 3:
dmin = (dmin + 1) % (MAX_DMIN + 1);
break;
case 4:
umin = (umin + 1) % 10;
break;
}Casos 3 y 4: lo mismo para la decena y la unidad de minuto. La decena usa el máximo configurado en MAX_DMIN, y la unidad siempre va de 0 a 9 (% 10).
horas = dh * 10 + uh;
minutos = dmin * 10 + umin;
}Finalmente se recomponen las horas y los minutos a partir de los dígitos ya modificados. Por ejemplo, con dh = 2 y uh = 1, horas pasa a valer 21.
8.10. Partes D y E: la función loop()
loop() se ejecuta continuamente. En cada vuelta (que dura unos 12 ms) hace seis cosas, en este orden:
void loop() {
leerBotones();
actualizarReloj();1) Revisa los pulsadores (Parte B) y 2) actualiza la hora si corresponde (Parte C).
int digitos[4] = {
horas / 10,
horas % 10,
minutos / 10,
minutos % 10
};3) Separa la hora actual en cuatro dígitos y los guarda en un arreglo. Este arreglo está ordenado igual que digPins: digitos[0] es la decena de hora y se muestra en el display de digPins[0], etc.
Figura 7. Partes D y E del flujograma general: dos puntos, parpadeo y multiplexado.
if (modo == 0) digitalWrite(DOTS, (segundos % 2 == 0) ? HIGH : LOW);
else digitalWrite(DOTS, HIGH);4) Controla los dos puntos. Si se está en funcionamiento normal (modo == 0), se encienden cuando los segundos son pares (segundos % 2 == 0) y se apagan cuando son impares: parpadean una vez por segundo. En configuración quedan encendidos fijos.
bool parpadeoApagado = (modo != 0) && ((millis() / 300) % 2 == 1);5) Calcula si el dígito en edición debe verse apagado en este instante. La variable es verdadera solo si se está configurando (modo != 0) y si (millis() / 300) % 2 es 1. Esa segunda parte vale 0 durante 300 ms, luego 1 durante 300 ms, y así sucesivamente, lo que produce el parpadeo. Todavía no se apaga nada: solo se guarda el resultado.
for (int i = 0; i < 4; i++) {
apagarSegmentos();
bool apagarEste = parpadeoApagado && (i == modo - 1);
if (!apagarEste) {
mostrarNumero(digitos[i]);
digitalWrite(digPins[i], HIGH);
}
delay(3);
digitalWrite(digPins[i], LOW);
}
}6) Es el multiplexado. El ciclo recorre los cuatro dígitos (i = 0, 1, 2, 3), y para cada uno:
apagarSegmentos();: limpia las líneas de segmentos, para que no quede el dibujo del dígito anterior.bool apagarEste = parpadeoApagado && (i == modo - 1);: este dígito debe quedar apagado solo si estamos en fase de apagado del parpadeo y si es el dígito que se está editando. La relación esmodo - 1porque el modo 1 corresponde a la posición 0 (Dh), el modo 2 a la posición 1 (Uh), y así. En funcionamiento normalmodo - 1vale -1 y ningúnicoincide, y ademásparpadeoApagadoes falso.if (!apagarEste): el signo!significa «no». Si este dígito no debe apagarse, se dibuja su número conmostrarNumero(digitos[i])y se activa su transistor condigitalWrite(digPins[i], HIGH). Si debe apagarse, simplemente no se enciende su transistor y el display queda en blanco.delay(3);: mantiene ese display encendido 3 ms.digitalWrite(digPins[i], LOW);: desactiva el transistor y pasa al siguiente dígito.
Al terminar los cuatro dígitos, loop() vuelve a empezar. Cada vuelta dura unos 12 ms (4 × 3 ms), por lo que los pulsadores se revisan unas 80 veces por segundo, más que suficiente para que respondan sin retraso perceptible.
9. Cambios para el ESP8266 real
Para el circuito armado en protoboard con el NodeMCU V3 y el Arduino IDE (con el paquete de placas ESP8266 instalado y la placa «NodeMCU 1.0 (ESP-12E Module)» seleccionada), se modifican solo las constantes de pines y la lectura del pulsador B2. La lógica restante del programa no cambia.
Cambio 1. Reemplazar los bloques de pines por los números del NodeMCU de la tabla de la sección 5. En el paquete ESP8266 existen los nombres D0, D1, ..., D8, que se corresponden con las etiquetas impresas en la placa. Para RX, TX, SD2 y SD3 se usan los números GPIO directamente:
// ---------- SEGMENTOS ----------
const int SEG_A = D0; // GPIO16
const int SEG_B = D4; // GPIO2
const int SEG_C = D8; // GPIO15
const int SEG_D = 3; // GPIO3 (RX)
const int SEG_E = 1; // GPIO1 (TX)
const int SEG_F = 9; // GPIO9 (SD2)
const int SEG_G = 10; // GPIO10 (SD3)
// ---------- DIGITOS (HIGH = encendido) ----------
const int DIG_DH = D5; // GPIO14
const int DIG_UH = D6; // GPIO12
const int DIG_DMIN = D7; // GPIO13
const int DIG_UMIN = D1; // GPIO5
const int DOTS = D2; // GPIO4
// ---------- BOTONES ----------
const int BTN1 = D3; // GPIO0, pulsador a GND
const int BTN2 = A0; // entrada analogica, pulsador a GND con 10k a 3,3 VCambio 2. El pin A0 no admite INPUT_PULLUP ni digitalRead(). Se agrega una pequeña función que lee un pulsador, sea digital o analógico, y se la usa dentro de pulsado():
// Devuelve HIGH (suelto) o LOW (presionado) para cualquier pulsador
bool leerBoton(int pin) {
if (pin == A0) return analogRead(A0) > 512;
return digitalRead(pin);
}Si el pin es A0, lee el valor analógico (0 a 1023). Con el pulsador suelto, la resistencia de 10 kΩ lleva el pin cerca de 3,3 V y la lectura es alta (mayor que 512, se devuelve true, equivalente a HIGH). Al presionarlo, el pin queda en 0 V (se devuelve false, equivalente a LOW). Así el resto del código sigue pensando que «LOW significa presionado».
Esta función debe escribirse antes de pulsado(), y dentro de pulsado() se cambia una sola línea:
bool lectura = leerBoton(pin); // antes: digitalRead(pin)Cambio 3. En setup(), la línea pinMode(BTN2, INPUT_PULLUP); se elimina (A0 no la necesita). La línea de BTN1 se deja igual.
Cambio 4. En el circuito real, cada línea de segmento lleva una resistencia de 220 Ω, y los dos puntos se conectan como se describe en la sección 5.1.
Para cargar el programa: seleccione el puerto del NodeMCU, verifique que el Flash Mode (menú Herramientas) esté en DIO, y pulse Subir. Durante la carga puede ser necesario desconectar momentáneamente los cables de los segmentos d y e (RX y TX) si observa errores de comunicación.
10. Observaciones y problemas frecuentes
- Rango de la decena de minutos: con
MAX_DMIN = 9, B2 permite escribir minutos de 60 a 99, que no existen. Si se deja así, el reloj los mostrará hasta el siguiente cambio de minuto y entonces pasará a 00 y sumará una hora. Para evitarlo, useMAX_DMIN = 5. - Se ve un solo dígito o dígitos desordenados: revise que cada transistor esté conectado a su pin y que el orden de
digPinscoincida con el orden de los displays. - Segmentos fantasma o brillo desigual: modifique el valor de
delay(3)entre 2 y 5 ms. Con valores mayores aparece parpadeo; con valores menores el brillo disminuye. - Un solo pulsador responde: en la versión real verifique que B2 en A0 tenga su resistencia de 10 kΩ a 3,3 V.
- El reloj se adelanta o atrasa: el reloj se basa en el oscilador del microcontrolador y no tiene el cristal de precisión de un reloj real. Es normal una deriva de algunos segundos por día. Para mayor precisión se debería agregar un módulo RTC (por ejemplo, el DS3231).
- El reloj pierde la hora al apagarse: se reinicia a 12:00 cada vez que se energiza, porque no hay batería ni memoria para guardar la hora.
11. Conclusiones
Este reloj muestra cómo un microcontrolador reemplaza a un conjunto de circuitos integrados (contadores, decodificadores y osciladores) por un programa. Los conceptos clave fueron el multiplexado, que permite controlar cuatro displays con solo once salidas, y la medición del tiempo con millis(), que evita detener el programa y deja libres los tiempos para leer los botones y refrescar la pantalla.
La máquina de estados de la puesta en hora, formada solo por la variable modo, permite que un mismo botón haga cosas distintas según el momento. Esta misma idea es la base de muchos menús y sistemas de control.
Finalmente, la práctica muestra una vez más que lo difícil de pasar de la simulación a la realidad está en el hardware: asignación de pines, niveles de 3,3 V, resistencias limitadoras y pines con funciones especiales de arranque. La lógica del programa se mantiene, y lo que cambia es la forma de conectar el mundo real.








Comentarios
Publicar un comentario