Construye un reloj preciso con Arduino Nano y LCD 16×2
Componentes y suministros
| Arduino Nano R3 (funciona con cualquier Arduino) | × | 1 |
| Pantalla LCD alfanumérica 16×2 | × | 1 |
| Pulsadores táctiles | × | 3 |
| Potenciómetro recortable 10 kΩ | × | 1 |
| Cables de conexión | × | 1 |
Sobre este proyecto
Empecé esto como ejercicio académico, pero acabé con un reloj muy preciso. Tras 5 días funcionando, no había perdido ni ganado tiempo.
El problema principal de usar solo un Arduino es que su reloj interno no es 100 % exacto. Si confías solo en él, el conteo de milisegundos se desvía un pequeño porcentaje y el reloj adelanta o retrasa. Mi enfoque fue medir la inexactitud del Arduino usado y determinar cuántos milisegundos ganaba/perdía por hora. Luego programé un ajuste de velocidad para añadir/restar esa diferencia cada hora.
Mi otra preocupación era si la inexactitud era consistente; como el reloj programado mantuvo hora exacta 5 días, parece que la inexactitud es consistente.
El segundo problema es que millis() se reinicia cada ~50 días y no se puede manipular. La solución fue reemplazar la interrupción de millis() por un contador propio que pueda manipular y que cuente milisegundos desde medianoche, reiniciándose cada día, eliminando límites de tiempo de ejecución.
Evaluar la inexactitud
Asumí que el reloj de mi ordenador (y millis() en Processing) era exacto. Creé un programa para Arduino que envía los milisegundos transcurridos desde el handshake a Processing cada 2 segundos, y un script en Processing que lo lee y compara con sus propios milisegundos, mostrando resultado en tiempo real y la diferencia tras una hora. Esto dio los milisegundos ganados/perdidos por hora, valor para el ajuste de velocidad en el reloj.
Los códigos del programa Arduino y script Processing están abajo. Si no tienes Processing, visita https://processing.org para descargarlo y aprender.
El código del reloj
Las áreas clave son la configuración de la interrupción, su uso y cómo se guarda y manipula la fecha.
La interrupción
El siguiente código configura una interrupción que salta cada milisegundo. Desvía la interrupción usada por millis(), por lo que millis() y delay() dejarán de funcionar.
// Configurar interrupción de tiempo - millis() desborda a los 50 días
// Usamos nuestro propio contador de milisegundos que reiniciamos cada día
TCCR0A = (1 << WGM01); // Modo CTC
OCR0A = 0xF9; // Valor para 1 ms = 249 (8 bits, máx 256)
// [(16 000 000/64) * 0.001] - 1 = 249 = 1 milisegundo
TIMSK0 |= (1 << OCIE0A); // Habilitar interrupción por comparación
TCCR0B |= (1 << CS01) | (1 << CS00); // Prescaler 1/64 (bits 110)
TCNT0 = 0; // Inicializar contador
sei(); // Habilitar interrupciones globales
Este es el código que se ejecuta cada milisegundo:
// ISR llamada cuando se alcanza el tiempo de comparación
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
currentTime y elapsed son unsigned long y se declaran volatile porque se modifican en la ISR y en el código principal. Esto obliga al sistema a leer el valor real cada vez.
currentTime almacena milisegundos desde medianoche; hay rutinas para convertirlo a HH:MM:SS y reiniciarlo al ajustar la hora.
Al pasar 24 horas, el sistema resta los milisegundos de un día e incrementa la fecha en 1 día. El reloj no se ve afectado por el valor máximo de la variable, a diferencia de millis().
if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
noInterrupts();
currentTime -= millisecondsInADay;
interrupts();
currentDate++;
}
Se deshabilitan interrupciones mientras se manipula currentTime para evitar corrupción si la ISR salta en medio.
Cada hora, el sistema ajusta los milisegundos transcurridos por la corrección de velocidad calculada, compensando el reloj interno rápido/lento.
if (elapsed >= millisecondsInHour) {
noInterrupts();
currentTime += speedCorrection;
elapsed = 0;
interrupts();
}
Almacenamiento y cálculo de fecha
La fecha se guarda como fecha juliana (días desde el 1 enero 4713 a.C.). Incluyen rutinas para calcular la fecha juliana y convertirla al calendario gregoriano.
float JulianDate(int iday, int imonth, int iyear) {
unsigned long d = iday, m = imonth, y = iyear;
if (m < 3) { m += 12; y -= 1; }
unsigned long t1 = (153 * m - 457) / 5;
unsigned long t2 = 365 * y + (y / 4) - (y / 100) + (y / 400);
return 1721118.5 + d + t1 + t2;
}
void GregorianDate(float jd, int &iday, int &imonth, int &iyear) {
unsigned long f = jd + 68569.5;
unsigned long e = (4.0 * f) / 146097;
unsigned long g = f - (146097 * e + 3) / 4;
unsigned long h = 4000ul * (g + 1) / 1461001;
unsigned long t = g - (1461 * h / 4) + 31;
unsigned long u = (80ul * t) / 2447;
unsigned long v = u / 11;
iyear = 100 * (e - 49) + h + v;
imonth = u + 2 - 12 * v;
iday = t - 2447 * u / 80;
}
Botones de ajuste
El botón Mode avanza el modo: Mostrar Hora → Ajustar Hora → Ajustar Año → Ajustar Día/Mes → Ajustar Corrección Velocidad → Mostrar Hora. Cada modo usa los otros 2 botones para incrementar/decrementar.
Una vez en marcha, si el reloj adelanta/retrasa, puedes cambiar el ajuste de velocidad en el modo Set Speed Adjustment subiendo/bajando de 5 en 5 segundos.
Código
Programa del reloj
// Paul Brace - Feb 2021
// Reloj simple con fecha solo con Arduino - sin módulo RTC
// Incorpora ajuste de corrección de velocidad para compensar inexactitud del reloj interno.
// Una vez ajustado, el reloj es sorprendentemente preciso. En mi test no perdió/ganó tiempo en 5 días.
// Muestra hora en LCD 16x2
// Botones para ajustar hora
// Mode (pin 2) alterna: mostrar, ajustar hora, año, fecha, corrección velocidad
// Botón 1 (pin 3) Incrementa Minutos/Mes, decrementa Año/ajuste vel.
// Botón 2 (pin 4) Incrementa Hora/Día, incrementa Año/ajuste vel.
// Display 24 h
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 13, 6, 7, 8, 9); // RS, EN, D4, D5, D6, D7
#define MODE_BUTTON 2
#define HOUR_BUTTON 3
#define UP_BUTTON 3
#define DAY_BUTTON 3
#define MINUTE_BUTTON 4
#define DOWN_BUTTON 4
#define MONTH_BUTTON 4
#define SHOW_TIME 1
#define SET_TIME 2
#define SET_YEAR 3
#define SET_DATE 4
#define SET_SPEED_ADJ 5
int speedCorrection = 3545; // ms que mi Nano va lento por hora (negativo si va rápido)
volatile unsigned long currentTime;
unsigned long lastTime = -1000;
volatile unsigned long elapsed;
unsigned long millisecondsInADay = 24ul * 60 * 60 * 1000;
unsigned long millisecondsInHour = 60ul * 60 * 1000;
int currentMode;
float currentDate;
float lastDate = 0.0;
int currentDay, currentMonth, currentYear;
char *dayArray[] = { "Tue. ", "Wed. ", "Thur. ", "Fri. ", "Sat. ", "Sun. ", "Mon. " };
void setup() {
TCCR0A = (1 << WGM01);
OCR0A = 0xF9;
TIMSK0 |= (1 << OCIE0A);
TCCR0B |= (1 << CS01) | (1 << CS00);
TCNT0 = 0;
sei();
pinMode(MINUTE_BUTTON, INPUT_PULLUP);
pinMode(HOUR_BUTTON, INPUT_PULLUP);
pinMode(MODE_BUTTON, INPUT_PULLUP);
currentTime = 0;
currentDate = JulianDate(1, 1, 2021);
elapsed = 0;
currentMode = SHOW_TIME;
lcd.begin(16, 2);
lcd.noAutoscroll();
lcd.display();
lcd.clear();
ShowTime(currentTime);
}
void loop() {
if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
noInterrupts();
currentTime -= millisecondsInADay;
interrupts();
currentDate++;
}
if (elapsed >= millisecondsInHour) {
noInterrupts();
currentTime += speedCorrection;
elapsed = 0;
interrupts();
}
CheckButtons();
switch (currentMode) {
case SHOW_TIME: ShowTime(currentTime); ShowDate(currentDate); break;
case SET_TIME: ShowTimeSet(currentTime); break;
case SET_YEAR: ShowYearSet(currentDate); break;
case SET_DATE: ShowDDMMSet(currentDate); break;
case SET_SPEED_ADJ: ShowSpeedSet(); break;
}
Wait(150);
}
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
// ... (funciones JulianDate, GregorianDate, SplitTime, SetTime, Wait, CheckButtons,
// FormatNumber, ShowTime, ShowDate, ShowDDMMSet, ShowYearSet, ShowTimeSet, ShowSpeedSet)
// Ver código completo en el original.
Programa Arduino para test de temporizador
// Paul Brace Feb 2021
// Para usar con script Processing correspondiente
// Compara millis() de aquí con millis() en Processing (reloj PC)
int inByte = 0;
unsigned long firstReading = 100000;
void setup() {
Serial.begin(9600);
sayHello();
}
void loop() {
if (Serial.available() > 0) {
inByte = Serial.read();
Serial.print(millis() - firstReading);
Serial.print('E');
delay(2000);
}
}
void sayHello() {
while (Serial.available() <= 0) {
Serial.print('Z');
delay(200);
}
firstReading = millis();
}
Script Processing para test de temporizador
// Paul Brace Feb 2021
// Script para recibir millis() de Arduino y comparar con millis() interno
// Asume reloj PC exacto
// -ve = Arduino lento → ajuste +ve en reloj
// +ve = Arduino rápido → ajuste -ve
import processing.serial.*;
Serial theSerialPort;
int[] serialBytesArray = new int[15];
int bytesCount = 0;
boolean init = false;
int fillColor = 255;
long mills = 0, first = 0, now, firstReading = 100000, DiffPerHour = 0;
int inByte;
void setup() {
size(500, 500);
background(70);
noStroke();
printArray(Serial.list());
String thePortName = Serial.list()[1];
theSerialPort = new Serial(this, thePortName, 9600);
}
void draw() {
background(70);
fill(fillColor);
textSize(25);
text(hour() + ":" + minute() + ":" + second(), 50, 50);
text("Incoming elapsed: " + mills, 50, 100);
text("Local elapsed: " + (now - firstReading), 50, 150);
text("Diff: " + (mills - (now - firstReading)), 50, 200);
if (((now - firstReading) >= 3600000) && (DiffPerHour == 0)) {
DiffPerHour = mills - (now - firstReading);
}
text("Diff after 1 hour: " + DiffPerHour, 50, 300);
}
void serialEvent(Serial myPort) {
inByte = myPort.read();
if (!init) {
if (inByte == 'Z') {
myPort.clear();
init = true;
myPort.write('Z');
if (first == 0) first = millis();
}
} else {
if (inByte != 69) { // 'E'
if (bytesCount < 14) {
serialBytesArray[bytesCount] = inByte;
bytesCount++;
}
}
if (inByte == 69) {
now = millis();
mills = 0;
for (int i = 1; i <= bytesCount; i++) {
mills += (serialBytesArray[i-1] - 48) * pow(10, bytesCount - i);
}
if (firstReading == 100000) firstReading = now;
myPort.write('Z');
bytesCount = 0;
}
}
}
Esquemáticos


Proceso de manufactura
- Reloj POV con Arduino: Construye una pantalla LED de persistencia de visión
- Construye un Decodificador DTMF Solo con Arduino – Sin CI Externos
- Crea un Impresionante Sistema Ambilight para Monitor con Arduino
- Construye un Reloj de Pared Elegante con Anillo Neopixel 60 LEDs Adafruit y Arduino
- Reloj de Palabras DIY con Arduino: Construye una Pantalla Elegante en Tiempo Real
- Construye un reloj en tiempo real con Arduino para horarios de oración islámica
- Construye un Reloj Maestro LED Personalizado con Alarma – Visible desde 12 Metros
- Construye un Voltímetro Inteligente con Arduino y Smartphone – Proyecto DIY Fácil
- Monitor de Frecuencia Cardíaca IoT en Tiempo Real con Arduino y MAX30100
- Servidor Web Arduino Uno WiFi: Controla un LED desde el Navegador
- Reloj Preciso de 7 Segmentos con Arduino Pro Mini y RTC DS1302