Skip to Content

Cómo sincronizar el reloj del ESP32 con un servidor SNTP

Cómo sincronizar el reloj del ESP32 con un servidor SNTP

En este tutorial aprenderás cómo sincronizar el reloj del ESP32 con un servidor SNTP para tener siempre la hora exacta y no tener que ajustar el reloj nunca más.

Al sincronizar el reloj vía internet con un servidor de tiempo SNTP, podemos asegurarnos de que nuestro reloj siempre muestre la hora correcta, independientemente de la precisión del reloj interno del ESP32 o de los cambios por horario de verano.

Comencemos con las piezas necesarias.

Piezas necesarias

Solo necesitarás un ESP32, un LCD y algunos cables. En lugar de la placa de desarrollo ESP32-C3 Mini que se menciona a continuación, también podrías usar cualquier otro ESP32.

ESP32-C3 Mini

Cable USB C

LCD 16×2

Dupont wire set

Juego de cables Dupont

Half_breadboard56a

Protoboard

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Servidor de tiempo NTP

NTP significa Network Time Protocol, y un servidor NTP es un sistema informático especializado que proporciona información de tiempo precisa a otros dispositivos en internet.

Un cliente NTP consulta regularmente uno o más servidores NTP para recibir la hora actual y sincroniza su reloj interno según la marca de tiempo recibida. En el caso del ESP32 como cliente, este se conecta a internet vía router y Wi-Fi e inicia una conexión a un grupo de servidores NTP. Uno de los servidores responde con una marca de tiempo que el ESP32 recibe y usa para ajustar su reloj interno.

ESP32 polls NTP Servers
El ESP32 consulta servidores NTP

Los servidores NTP están organizados en un sistema jerárquico y por capas con dispositivos de alta precisión como relojes atómicos, GNSS (incluyendo GPS) u otros relojes de radio como origen (fuente de reloj autorizada). Las capas individuales se llaman Stratum.

Hierarchy of the NTP server service
Jerarquía del servicio de servidores NTP (source)

Normalmente, no te conectas a un servidor NTP específico (dirección IP), sino a un grupo de muchos servidores NTP. Más sobre esto en la siguiente sección.

Grupo NTP

El NTP Pool es una colección dinámica de servidores NTP en red. La siguiente tabla muestra el número de servidores NTP en el grupo ntp.org en diferentes ubicaciones geográficas.

Active NTP servers as of Aug 2024
Servidores NTP activos a agosto de 2024 (source)

Solicitas información de tiempo a un servidor NTP del grupo usando el nombre «pool.ntp.org». El sistema intentará encontrar el servidor disponible más cercano para ti. Esta es la forma recomendada.

Sin embargo, también puedes solicitar la hora a servidores NTP en ubicaciones o países específicos. Por ejemplo, la dirección «de.pool.ntp.org» se refiere a un grupo de servidores NTP en Alemania (DE). También puedes usar continental zones, como europenorth-americaoceania o asia.pool.ntp.org. Finalmente, puedes añadir un prefijo numérico si necesitas varios nombres de servidor, por ejemplo «0.de.pool.ntp.org», «1.de.pool.ntp.org».

STNP vs NTP

Existen dos protocolos para sincronización de tiempo. NTP (Network Time Protocol) y SNTP (Simple Network Time Protocol). NTP es más preciso y complejo, mientras que SNTP es una versión simplificada de NTP.

SNTP fue desarrollado específicamente para computadoras pequeñas y microcontroladores. Está diseñado para requerir menos memoria y potencia de procesamiento que NTP. Aunque mucho menos preciso que NTP, SNTP aún proporciona la hora con una precisión de 100 milisegundos respecto al tiempo exacto.

Normalmente, es usado por dispositivos de red pequeños, como cámaras IP, DVR, teléfonos IP, routers, dispositivos de consumo y también por microcontroladores como el ESP32.

Sincronizando el reloj del ESP32 con un servidor SNTP

El siguiente código muestra cómo sincronizar el reloj interno de un ESP32 con la hora recibida de un servidor SNTP. Esto permite que el ESP32 mantenga la hora exacta actualizándola periódicamente desde el servidor. Echa un vistazo rápido al código completo primero, luego discutiremos los detalles.

//  makerguides.com: synchronizing ESP32 time with SNTP server

#include "WiFi.h"
#include "esp_sntp.h"

const char* ssid = "SSID";
const char* password = "PASSWORD";

void initWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
}

void notify(struct timeval* t) {
  Serial.println("synchronized");
}

void initSNTP() {  
  sntp_set_sync_interval(1 * 60 * 60 * 1000UL);  // 1 hour
  sntp_set_time_sync_notification_cb(notify);
  esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
  esp_sntp_setservername(0, "pool.ntp.org");
  esp_sntp_init();
  setTimezone();
}

void wait4SNTP() {
  while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) {
    delay(100);
    Serial.println("waiting ...");
  }
}

void setTimezone() {  
  setenv("TZ", "AEST-10AEDT,M10.1.0,M4.1.0/3", 1);
  tzset();
}

void printTime() {
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

void setup() {
  Serial.begin(115200);
  initWiFi();
  initSNTP();
  wait4SNTP();
}

void loop() {
  printTime();
  delay(10000);
}

Librerías

Comenzamos incluyendo las librerías necesarias. WiFi.h para conectarse a internet, y esp_sntp.h para la comunicación con un servidor SNTP. Ambas librerías son parte del núcleo del ESP32 y no necesitan instalación.

#include "WiFi.h"
#include "esp_sntp.h"

Constantes y variables

Luego definimos las constantes y variables necesarias para el programa. Aquí especificamos el SSID y la contraseña de la red Wi-Fi a la que se conectará el ESP32. Obviamente, deberás reemplazarlos por tus propias credenciales Wi-Fi.

const char* ssid = "SSID";
const char* password = "PASSWORD";

Inicialización de WiFi

La función initWiFi() es responsable de conectar el ESP32 a la red WiFi especificada. Verifica continuamente el estado de la conexión hasta que se conecta con éxito.

void initWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
}

Inicialización SNTP

En la función initSNTP() configuramos los ajustes SNTP para la sincronización de tiempo. Esto incluye establecer el intervalo de sincronización, el nombre del servidor, el modo de operación y la zona horaria.

void notify(struct timeval* t) {
  Serial.println("synchronized");
}

void initSNTP() {
  sntp_set_sync_interval(60 * 60 * 1000UL); // 1 hour
  sntp_set_time_sync_notification_cb(notify);
  esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
  esp_sntp_setservername(0, "pool.ntp.org");
  esp_sntp_init();
  setTimezone();
}

Veamos más de cerca las llamadas a funciones individuales y qué hacen.

sntp_set_sync_interval(interval_ms) determina con qué frecuencia sincronizamos el reloj interno del ESP32 con el servidor SNTP. El intervalo de tiempo se especifica en microsegundos y un intervalo de 60 * 60 * 1000UL microsegundos significa que sincronizamos cada hora. No envíes consultas excesivamente frecuentes. Los intervalos razonables suelen ser de una o dos veces al día hasta 5 veces por hora. 

sntp_set_time_sync_notification_cb(callback) te permite especificar una función de notificación (callback) que se llama cada vez que ocurre una sincronización. En el código de ejemplo, definimos la función notify() para este propósito, que simplemente imprime «synchronized«. En la mayoría de las aplicaciones no necesitarás esto, ¡pero es genial para pruebas y depuración!

esp_sntp_setoperatingmode(operating_mode) establece el operating mode. Normalmente es ESP_SNTP_OPMODE_POLL – estamos consultando el servidor SNTP. Pero también existe ESP_SNTP_OPMODE_LISTENONLY.

esp_sntp_setservername(idx, server) se usa para establecer el nombre/dirección del servidor. Puedes especificar varios servidores si quieres. Por ejemplo:

esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_setservername(1, "de.pool.ntp.org");
esp_sntp_setservername(2, "time.nist.gov");

esp_sntp_init() inicia el servicio SNTP con los parámetros anteriores. También existe una función para detenerlo: esp_sntp_stop() para detener el servicio.

Finalmente, necesitamos establecer la zona horaria ya que el servidor SNTP devuelve la hora en UTC. Esa parte está implementada en una función separada que discutiremos en la siguiente sección.

Si quieres aprender más sobre funciones de temporizador del ESP, echa un vistazo a ESP32 Programming Guide for System Time. Ten en cuenta que también existe una función configTime() como parte del núcleo de Arduino que hace esencialmente lo mismo que nuestra función initSNTP(). Sin embargo, toma un gmtOffset_sec y daylightOffset_sec como parámetros, pero no pude hacer que funcionara correctamente con la zona horaria australiana y no permite establecer el intervalo de sincronización.

Configuración de la zona horaria

La función setTimezone() establece la zona horaria para el ESP32 usando la variable de entorno TZ. Como actualmente vivo en Australia, la configuré como «AEST-10AEDT,M10.1.0,M4.1.0/3», que corresponde a la Hora Estándar del Este de Australia (AEST) con ajustes por horario de verano.

void setTimezone() {
  setenv("TZ", "AEST-10AEDT,M10.1.0,M4.1.0/3", 1);
  tzset();
}

Las partes de esta definición de zona horaria son las siguientes

  • AEST: Hora Estándar del Este de Australia
  • -10: Desfase UTC de 10 horas adelantado respecto al Tiempo Universal Coordinado (UTC)
  • AEDT: Hora de Verano del Este de Australia
  • M10.1.0: La transición al horario de verano ocurre el primer domingo de octubre
  • M4.1.0/3: La transición de regreso al horario estándar ocurre el primer domingo de abril, con una diferencia de 3 horas respecto a UTC.

Para otras definiciones de zona horaria, consulta Posix Timezones Database. Simplemente copia y pega la cadena que encuentres allí para tu zona horaria en la función setenv().

Esperando la sincronización

La función wait4SNTP() verifica continuamente el estado de sincronización con el servidor SNTP hasta que se completa. Imprime un mensaje mientras espera la sincronización.

void wait4SNTP() {
  while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) {
    delay(100);
    Serial.println("waiting ...");
  }
}

Imprimiendo la hora

La función printTime() obtiene la información de la hora actual y la imprime de forma formateada usando la estructura de datos struct tm.

void printTime() {
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

"%A, %B %d %Y %H:%M:%S" son format specifiers que determinan cómo se formatea la timeinfo y los miembros de la tm struct son los siguientes:

  Member    Type  Meaning                   Range
  tm_sec    int   seconds after the minute  0-61*
  tm_min    int   minutes after the hour    0-59
  tm_hour   int   hours since midnight      0-23
  tm_mday   int   day of the month          1-31
  tm_mon    int   months since January      0-11
  tm_year   int   years since 1900
  tm_wday   int   days since Sunday         0-6
  tm_yday   int   days since January 1      0-365
  tm_isdst  int   Daylight Saving Time flag

Función Setup

En la función setup() inicializamos la comunicación serial, conectamos a Wi-Fi, inicializamos SNTP y esperamos a que la sincronización de la hora se complete.

void setup() {
  Serial.begin(115200);
  initWiFi();
  initSNTP();
  wait4SNTP();
}

Ten en cuenta que no puedes apagar la conexión Wi-Fi después de completar el setup. Algunos tutoriales se equivocan en esto. Sin Wi-Fi, el ESP32 no puede sincronizarse con el servidor SNTP. Apagar el Wi-Fi solo tiene sentido si pones el ESP32 en deep sleep y lo reconectas a un servidor SNTP al arrancar.

Función Loop

La función loop() imprime repetidamente la hora actual cada 10 segundos.

void loop() {
  printTime();
  delay(10000);
}

Ten en cuenta que la sincronización ocurre en segundo plano (hilo separado). No hay ninguna función que debas llamar regularmente en la función loop para consultar el servidor SNTP.

Ejemplo de salida

Para probar el código, te sugiero cambiar temporalmente el intervalo de sincronización a 30 segundos (30 * 1000UL). En el Monitor Serial deberías ver la siguiente secuencia de salidas:

SNTP synchronized time output on Serial Monitor
Salida de sincronización de tiempo en el Monitor Serial

Primero un mensaje «waiting» hasta que se establezca la conexión con el servidor SNTP. Luego impresiones de la hora cada 10 segundos, seguidas de una notificación «synchronized» cada 30 segundos.

En la siguiente sección usaremos esencialmente el mismo código para construir un reloj sincronizado por internet que muestre la hora y fecha en un LCD.

Mostrando la hora de internet en un LCD

Conectar un LCD con interfaz I2C al ESP32 para mostrar la hora es fácil. Primero conecta 5V a VCC y G a tierra. Luego conecta SDA al pin 8 (cable amarillo) y SCL al pin 9 (cable naranja) en el ESP32.

Connecting LCD display via I2C to ESP32
Conexión del LCD vía I2C al ESP32

Si necesitas información más detallada sobre pantallas LCD, consulta nuestros tutoriales sobre How to use a 16×2 character LCD with Arduino y Character I2C LCD with Arduino Tutorial (8 Examples).

Asegúrate de conectar correctamente SDA y SCL. Si usas otra placa, puede que quieras cambiar a los pines que soportan la interfaz I2C en tu placa. Solo busca los pines marcados como SDA y SCL en el pinout. Aquí está el pinout para el ESP32-C3 Supermini, que uso en este proyecto:

Pinout for the ESP32-C3 Supermini
Pinout para el ESP32-C3 Supermini

Aunque los pines GPIO8 y GPIO9 se supone que son los nativos para la interfaz I2C, tuve que configurarlos explícitamente en el código (Wire.begin(sda, scl)) para que funcionara. Para más información sobre la placa SuperMini, consulta el tutorial ESP32-C3 SuperMini Board.

La siguiente imagen muestra el cableado completo y el reloj funcionando en el LCD. Como puedes ver, son solo unos pocos cables y el ESP32-C3 Supermini es realmente «mini» comparado con el LCD.

ESP32 synchronized clock on LCD
Reloj sincronizado por internet en LCD

Código para el reloj sincronizado por internet

Aquí está el código completo para sincronizar el reloj interno de un ESP32 con un servidor SNTP y mostrar la hora y fecha actual en un LCD.

//  makerguides.com: synchronizing ESP32 time with SNTP server

#include "WiFi.h"
#include "esp_sntp.h"
#include "LiquidCrystal_I2C.h"

const int sdaPin = 8;
const int sclPin = 9;

const char* ssid = "SSID";
const char* password = "PASSWORD";

// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
const char *timezone = "AEST-10AEDT,M10.1.0,M4.1.0/3"

LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x3F, 16, 2);

void initWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
}

void initSNTP() {
  sntp_set_sync_interval(60 * 60 * 1000UL);  // 1 h
  esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
  esp_sntp_setservername(0, "pool.ntp.org");  
  esp_sntp_init();
  setenv("TZ", timezone, 1);
  tzset();
}

void wait4SNTP() {
  lcd.setCursor(0, 0);
  lcd.print("synchronizing...");
  while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) {
    delay(100);
  }
  lcd.clear();
}

void initLCD() {
  Wire.begin(sdaPin, sclPin);  // I2C for ESP32-C3 Supermini
  lcd.init();
  lcd.backlight();
  lcd.clear();
}

void lcdPrintTime() {
  // https://cplusplus.com/reference/ctime/tm/
  char buff[24];
  struct tm t;
  getLocalTime(&t);  
  sprintf(buff, "%02d:%02d:%02d ", t.tm_hour, t.tm_min, t.tm_sec);
  lcd.setCursor(4, 0);
  lcd.print(buff);
  sprintf(buff, "%02d-%02d-%04d ", t.tm_mday, t.tm_mon+1, t.tm_year+1900);
  lcd.setCursor(3, 1);
  lcd.print(buff);
}

void setup() {
  initLCD();
  initWiFi();
  initSNTP();
  wait4SNTP();
}

void loop() {
  lcdPrintTime();
  delay(1000);
}

Como ves, es esencialmente el mismo código que antes con la adición de la función initLCD() que prepara el LCD y la función lcdPrintTime() que muestra la hora y fecha en el LCD.

void lcdPrintTime() {
  char buff[24];
  struct tm t;
  getLocalTime(&t);  
  sprintf(buff, "%02d:%02d:%02d ", t.tm_hour, t.tm_min, t.tm_sec);
  lcd.setCursor(4, 0);
  lcd.print(buff);
  sprintf(buff, "%02d-%02d-%04d ", t.tm_mday, t.tm_mon, t.tm_year+1900);
  lcd.setCursor(3, 1);
  lcd.print(buff);
}

La función lcdPrintTime() primero llama a getLocalTime() para obtener la hora local del reloj interno del ESP32, que sincronizamos con el servidor SNTP. De struct tm extraemos las piezas necesarias de información de tiempo y fecha (hora, minutos, …, año) y las escribimos formateadas en el buffer de cadena buff. Finalmente, colocamos el cursor en el LCD y mostramos el contenido del buffer en el LCD. La salida en tu LCD debería verse así:

Time output on LCD
Salida de la hora en el LCD

¡Y eso es todo!
Ahora tienes un reloj siempre exacto en un ESP32 que sincroniza su hora con un servidor SNTP.

Conclusiones

En este tutorial aprendiste cómo sincronizar el reloj interno de un ESP32 con la señal horaria de un servidor SNTP. Esto asegura que el reloj del ESP32 siempre sea exacto y tiene la ventaja adicional de que nunca tendrás que ajustar el reloj.

Usamos funciones específicas del ESP32 como sntp_set_sync_interval, esp_sntp_setoperatingmode y esp_sntp_setservername para configurar la comunicación con un servidor SNTP.

Sin embargo, vale la pena saber que la conexión Arduino core also provides two functions to set up an SNTP server. Estas son configTime y configTzTime. Son envoltorios más abstractos alrededor de las mismas funciones que usamos en initSNTP pero funcionarán sin cambios para ESP32 o ESP8266, por ejemplo. Así que podrías reemplazar initSNTP llamando a configTzTime(timezone, "pool.ntp.org"). Pero initSNTP te da un poco más de control, como establecer el intervalo de sincronización.

Finalmente, si quieres ejecutar el ESP32 en modo deep sleep y sincronizar su reloj al despertar, o necesitas información horaria adicional, puede que prefieras usar un proveedor de tiempo por internet como WorldTimeAPI. Para más información, consulta el Automatic Daylight Savings Time Clock, el Digital Clock with CrowPanel 3.5″ ESP32 Display o el tutorial LED Ring Clock with WS2812.

¡Feliz bricolaje ; )