El desarrollo de un reloj digital mediante el microcontrolador PIC18F4550 constituye un ejercicio práctico que permite integrar conceptos fundamentales de electrónica digital, programación embebida y control de periféricos. El proyecto utiliza un cristal externo de 8 MHz y está diseñado para representar la hora en formato de 24 horas mediante cuatro displays de siete segmentos, incorporando además botones de control para ajustar manualmente las horas y los minutos.
Desde el punto de vista de la programación, el sistema está desarrollado en lenguaje C utilizando el compilador XC8. La configuración inicial establece la frecuencia de operación del microcontrolador y define los puertos que serán utilizados como entradas y salidas. También se configura el conversor analógico-digital para trabajar con los pines correspondientes en modo digital, debido a que las entradas destinadas a los botones no requieren conversión analógica.
La visualización de la hora se realiza mediante cuatro displays de siete segmentos. Para simplificar el control de los segmentos se utiliza una tabla que contiene los códigos correspondientes a los números del 0 al 9. De esta manera, el programa no necesita determinar individualmente qué segmentos deben activarse cada vez que se presenta un número, sino que puede acceder directamente al patrón almacenado en la tabla.
Uno de los elementos centrales del proyecto es la técnica conocida como multiplexeo. Los cuatro displays no permanecen activados simultáneamente; el microcontrolador selecciona cada uno de ellos de manera secuencial y a una velocidad suficientemente elevada para que el usuario perciba una visualización continua. La variable mux determina cuál de los cuatro dígitos se encuentra activo en cada instante. Este procedimiento permite reducir la cantidad de recursos necesarios para controlar los displays y constituye una técnica ampliamente utilizada en sistemas digitales.

DESCARGAR EL PLANO ELECTRÓNICO AQUI
El control temporal se realiza mediante el Timer0 del PIC18F4550. El temporizador se configura en modo de 16 bits, utilizando como fuente de reloj la frecuencia interna derivada del oscilador y un prescaler de 1:8. Cuando el temporizador alcanza su condición de desbordamiento, genera una interrupción que es atendida mediante la rutina isr(). Dentro de esta rutina se recarga el Timer0 y se ejecutan las tareas relacionadas con la actualización de los displays y el conteo del tiempo.
La variable ticks funciona como contador de interrupciones. En cada interrupción se incrementa su valor y, al alcanzar el límite establecido en el programa, se actualizan los segundos. Posteriormente, el algoritmo controla los cambios de minuto y hora, manteniendo los rangos correspondientes de 0 a 59 segundos, 0 a 59 minutos y 0 a 23 horas. Cuando el reloj alcanza las 23:59:59, el siguiente ciclo devuelve el sistema a 00:00:00.
Es importante señalar que el valor utilizado para ticks está planteado específicamente para facilitar la simulación en Proteus. Para implementar el proyecto físicamente, este valor debe calcularse con precisión a partir de la frecuencia del oscilador, la frecuencia de instrucción, la configuración del Timer0 y el prescaler seleccionado. Esta consideración es fundamental, ya que una configuración incorrecta puede provocar que el reloj adelante o atrase con respecto al tiempo real.
El sistema incorpora además cuatro botones conectados a las entradas RA0, RA1, RA2 y RA3. Dos de ellos permiten incrementar o disminuir las horas, mientras que los otros dos realizan la misma función sobre los minutos. El programa emplea variables de estado anterior para detectar el cambio producido al presionar cada botón. Este método evita que una pulsación mantenida sea interpretada como múltiples órdenes consecutivas en cada ciclo del programa principal.
En conjunto, este proyecto representa una aplicación concreta de los principios de programación de microcontroladores. Su importancia no radica únicamente en mostrar una hora en cuatro displays, sino en integrar de manera coordinada temporización, interrupciones, multiplexeo, lectura de entradas digitales, control de salidas y manejo de variables compartidas.

El reloj digital desarrollado con el PIC18F4550 constituye, por tanto, una práctica relevante para comprender la relación entre hardware y software en sistemas embebidos. Asimismo, proporciona una base sólida para desarrollar aplicaciones más complejas que requieran medición de tiempo, interacción con el usuario y control eficiente de diferentes dispositivos electrónicos.
LISTA DE MATERIALES
1 Microcontrolador PIC18F4550
2 condensadores de 22pF cerámicos
20 resistencias de 330 ohms o 220 ohms a 1/2W o 1/4W
4 pulsadores normalmente abiertos NA
1 Cristal de 8MHz
4 transistores 2N2222
8 resistencias de 1k
cable UTP o par telefónico
1 protoboard
pinza de punta plana
pinza pelacables
1 fuente de voltaje de 5V
1 Pickit 3
software MPLAB y PICKIT3
PROGRAMA
// ================================================================
// RELOJ DIGITAL - PIC18F4550 @ cristal externo 8MHz
// ================================================================
#define _XTAL_FREQ 8000000
#include <xc.h>
// -------- TABLA 7 SEGMENTOS --------
const unsigned char tabla[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
// -------- VARIABLES --------
volatile unsigned char horas = 0;
volatile unsigned char minutos = 0;
volatile unsigned char segundos = 0;
volatile unsigned int ticks = 0;
volatile unsigned char mux = 0;
volatile unsigned char estado_led = 0;
// ================================================================
// INTERRUPCIÓN - TIMER0
// ================================================================
void __interrupt() isr(void) {
if (INTCONbits.TMR0IF) {
INTCONbits.TMR0IF = 0;
TMR0H = 0xFF;
TMR0L = 0x06;
// Apagar todos los segmentos y enables
PORTE = 0x00;
PORTD = 0x00;
PORTB = 0x00;
PORTCbits.RC0 = 0;
PORTCbits.RC1 = 0;
// LEDs dos puntos ( : )
PORTCbits.RC2 = estado_led;
PORTCbits.RC4 = estado_led;
// Descomponer dígitos
unsigned char d_h = horas / 10;
unsigned char u_h = horas % 10;
unsigned char d_m = minutos / 10;
unsigned char u_m = minutos % 10;
// Multiplexeo
switch (mux) {
case 0:
PORTD = tabla[d_h];
PORTEbits.RE1 = 1;
break;
case 1:
PORTD = tabla[u_h];
PORTEbits.RE0 = 1;
break;
case 2:
PORTB = tabla[d_m];
PORTCbits.RC1 = 1;
break;
case 3:
PORTB = tabla[u_m];
PORTCbits.RC0 = 1;
break;
}
if (++mux > 3) mux = 0;
// -------- CONTEO DE TIEMPO --------
// NOTA: ticks >= 10 solo para simulacion Proteus
// cambiar a 1000 para hardware real
if (++ticks >= 10) {
ticks = 0;
estado_led = !estado_led;
if (++segundos > 59) {
segundos = 0;
if (++minutos > 59) {
minutos = 0;
if (++horas > 23)
horas = 0;
}
}
}
}
}
// ================================================================
// MAIN
// ================================================================
void main(void) {
ADCON1 = 0x0F; // Todos los pines digitales
TRISA = 0xFF;
TRISB = 0x00;
TRISC = 0x00;
TRISD = 0x00;
TRISE = 0x00;
PORTB = 0x00;
PORTC = 0x00;
PORTD = 0x00;
PORTE = 0x00;
unsigned char prev_ra0 = 0;
unsigned char prev_ra1 = 0;
unsigned char prev_ra2 = 0;
unsigned char prev_ra3 = 0;
// Timer0: 16 bits, Fosc/4, prescaler 1:8
T0CON = 0b00000010;
TMR0H = 0xFF;
TMR0L = 0x06;
INTCONbits.TMR0IE = 1;
INTCONbits.TMR0IF = 0;
INTCONbits.PEIE = 1;
INTCONbits.GIE = 1;
T0CONbits.TMR0ON = 1;
while (1) {
if (PORTAbits.RA0 == 1 && prev_ra0 == 0) {
if (++horas > 23) horas = 0;
}
prev_ra0 = PORTAbits.RA0;
if (PORTAbits.RA1 == 1 && prev_ra1 == 0) {
horas = (horas == 0) ? 23 : horas - 1;
}
prev_ra1 = PORTAbits.RA1;
if (PORTAbits.RA2 == 1 && prev_ra2 == 0) {
if (++minutos > 59) minutos = 0;
}
prev_ra2 = PORTAbits.RA2;
if (PORTAbits.RA3 == 1 && prev_ra3 == 0) {
minutos = (minutos == 0) ? 59 : minutos - 1;
}
prev_ra3 = PORTAbits.RA3;
__delay_ms(20);
}
}
EXPLICACIÓN DEL PROGRAMA
El programa corresponde a un reloj digital realizado con un microcontrolador PIC18F4550, utilizando un cristal externo de 8 MHz. El objetivo principal es mostrar la hora en cuatro displays de siete segmentos, en formato de horas y minutos, además de permitir modificar estos valores mediante cuatro botones.
El programa está escrito en lenguaje C utilizando el compilador XC8. Una de las características principales es que se utiliza el Timer0 del PIC para generar interrupciones periódicas. Estas interrupciones son las que permiten realizar el multiplexeo de los displays y llevar el conteo del tiempo.
Al comienzo del programa aparece:
#define _XTAL_FREQ 8000000
#include <xc.h>
La primera línea le indica al compilador que el microcontrolador está trabajando con una frecuencia de 8 MHz. Esto es importante principalmente para que los retardos, como __delay_ms(20), sean calculados correctamente. La segunda línea incluye la librería propia del compilador XC8 para poder trabajar con los registros internos del PIC, como los puertos, el Timer0 y los registros de interrupciones.
Después se encuentra la tabla de los displays de siete segmentos:
const unsigned char tabla[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
Esta tabla permite representar los números del 0 al 9. Cada valor hexadecimal corresponde a una combinación de segmentos encendidos. Por ejemplo, cuando el programa necesita mostrar un número 5, utiliza tabla[5], que corresponde al valor 0x6D. De esta manera, no es necesario escribir individualmente qué segmentos deben encenderse para cada número.
Luego se declaran las variables principales:
volatile unsigned char horas = 0;
volatile unsigned char minutos = 0;
volatile unsigned char segundos = 0;
volatile unsigned int ticks = 0;
volatile unsigned char mux = 0;
volatile unsigned char estado_led = 0;
horas, minutos y segundos almacenan la hora actual. Las horas trabajan entre 0 y 23, mientras que los minutos y segundos trabajan entre 0 y 59.
La variable ticks se utiliza para contar las interrupciones producidas por el Timer0. La variable mux sirve para seleccionar cuál de los cuatro displays se debe activar. Finalmente, estado_led controla el encendido y apagado de los dos puntos que aparecen entre las horas y los minutos.
La palabra volatile se utiliza porque estas variables pueden modificarse dentro de la interrupción y también utilizarse desde el programa principal. Esto le indica al compilador que no debe asumir que su valor permanece constante.
Una de las partes más importantes es la función:
void __interrupt() isr(void)
Esta función es la rutina de atención de interrupciones. No es necesario llamarla manualmente. El PIC entra automáticamente en esta función cuando ocurre una interrupción que ha sido habilitada.
Lo primero que hace es comprobar:
if (INTCONbits.TMR0IF)
Esta condición verifica si la interrupción fue provocada por el Timer0. Cuando Timer0 se desborda, la bandera TMR0IF se coloca en 1. Por eso el programa la vuelve a colocar en cero:
INTCONbits.TMR0IF = 0;
Esto permite preparar el sistema para la siguiente interrupción.
Después se vuelve a cargar el Timer0:
TMR0H = 0xFF;
TMR0L = 0x06;
Timer0 está trabajando como un temporizador de 16 bits, por lo que necesita dos registros para almacenar su valor: TMR0H para la parte alta y TMR0L para la parte baja. En este caso se carga el valor 0xFF06.
A continuación, el programa apaga todos los displays antes de seleccionar el siguiente:
PORTE = 0x00;
PORTD = 0x00;
PORTB = 0x00;
PORTCbits.RC0 = 0;
PORTCbits.RC1 = 0;
Esto es necesario porque se está utilizando multiplexeo. El multiplexeo consiste en activar los displays uno por uno a una velocidad muy alta. Aunque en un instante determinado solamente hay un display activado, el cambio es tan rápido que el ojo humano percibe los cuatro números simultáneamente.
Por ejemplo, si la hora es:
23:45
el programa realmente va mostrando:
2
3
4
5
uno después de otro. Como este proceso se repite rápidamente, nosotros vemos 23:45.
Para poder hacer esto, primero se separan las horas y los minutos en decenas y unidades:
unsigned char d_h = horas / 10;
unsigned char u_h = horas % 10;
unsigned char d_m = minutos / 10;
unsigned char u_m = minutos % 10;
Si horas vale 23, entonces:
23 / 10 = 2
23 % 10 = 3
Por lo tanto, d_h vale 2 y u_h vale 3.
Si minutos vale 45:
45 / 10 = 4
45 % 10 = 5
Así se obtienen los cuatro valores que necesitan los displays.
Después se utiliza:
switch (mux)
para determinar qué display se activa.
Cuando mux vale 0, se muestran las decenas de las horas:
PORTD = tabla[d_h];
PORTEbits.RE1 = 1;
Cuando mux vale 1, se muestran las unidades de las horas:
PORTD = tabla[u_h];
PORTEbits.RE0 = 1;
Cuando mux vale 2, se muestran las decenas de los minutos:
PORTB = tabla[d_m];
PORTCbits.RC1 = 1;
Y cuando mux vale 3, se muestran las unidades de los minutos:
PORTB = tabla[u_m];
PORTCbits.RC0 = 1;
Después de mostrar cada posición, el programa aumenta mux:
if (++mux > 3)
mux = 0;
Esto hace que la secuencia sea 0, 1, 2, 3 y nuevamente 0.
La misma interrupción también se utiliza para controlar el paso del tiempo. Para ello se utiliza:
if (++ticks >= 10)
Cada vez que se produce una interrupción, ticks aumenta en uno. Cuando llega a 10, se reinicia y se incrementan los segundos.
En ese momento también se cambia el estado de los dos puntos:
estado_led = !estado_led;
Si estaba en cero pasa a uno, y si estaba en uno pasa a cero. Esto produce el efecto de parpadeo de los dos puntos.
Después se incrementan los segundos:
if (++segundos > 59)
Cuando los segundos pasan de 59, vuelven a cero y se incrementan los minutos. Lo mismo sucede con las horas. Cuando los minutos pasan de 59, vuelven a cero y se incrementa la hora. Finalmente, cuando las horas pasan de 23, regresan a cero.
Por ejemplo:
23:59:59
después pasa a:
00:00:00
De esta manera se mantiene el funcionamiento de un reloj de 24 horas.
En la función main() se realiza inicialmente la configuración del microcontrolador. La instrucción:
ADCON1 = 0x0F;
configura los pines correspondientes para trabajar como entradas y salidas digitales en lugar de utilizarlos como entradas analógicas.
Después se configuran los puertos mediante los registros TRIS:
TRISA = 0xFF;
TRISB = 0x00;
TRISC = 0x00;
TRISD = 0x00;
TRISE = 0x00;
En estos registros, un 1 significa entrada y un 0 significa salida. Por eso PORTA queda configurado como entrada, ya que allí están conectados los botones. Los demás puertos se configuran como salidas para controlar los displays y los LEDs.
También se ponen inicialmente todos los puertos en cero:
PORTB = 0x00;
PORTC = 0x00;
PORTD = 0x00;
PORTE = 0x00;
Después aparecen las variables:
prev_ra0
prev_ra1
prev_ra2
prev_ra3
Estas variables son necesarias para saber si un botón acaba de ser presionado. No basta con comprobar si un botón está en uno, porque mientras permanezca presionado el programa podría aumentar o disminuir el valor continuamente.
Por ejemplo:
if (PORTAbits.RA0 == 1 && prev_ra0 == 0)
significa que RA0 está presionado actualmente, pero anteriormente estaba en cero. Por lo tanto, se considera una nueva pulsación.
RA0 aumenta las horas, mientras que RA1 las disminuye. RA2 aumenta los minutos y RA3 los disminuye.
Finalmente:
__delay_ms(20);
hace que el programa espere 20 milisegundos antes de volver a revisar los botones. Esto también ayuda a reducir el efecto del rebote mecánico de los pulsadores.
Un aspecto que debe tenerse en cuenta es el valor de ticks. En el código se utiliza 10 porque está pensado para facilitar la simulación en Proteus. Para utilizar el circuito físicamente, no es correcto asumir simplemente que cambiar ese valor por 1000 hará que el reloj funcione exactamente a un segundo. El tiempo debe calcularse de acuerdo con la frecuencia del cristal, la frecuencia de instrucción, el prescaler y el valor inicial cargado en Timer0.