RTC DS3231

Módulo de reloj RTC DS3231 – Avanzado

El módulo de reloj RTC DS3231 que ya presentamos en una página anterior (puede acceder a ella haciendo click aquí), posee otras características en su funcionalidad de reloj de tiempo real que nos gustaría presentarles en ésta página. Algunas de ellas serán de muy poco uso, y otras tal vez los sorprenda, por lo que iremos una a una presentándolas y compartiendo un Sketch de ejemplo para que ponga a su módulo a prueba.

Manejo de centuria

El módulo RTC DS3231 por definición no maneja el año con 4 dígitos. Algunos dirán que es una limitación (estoy de acuerdo) pero los que los defienden dirán que posee una manera de manejar la centuria y eso es lo que les mostraremos a continuación. Si usted carga el Sketch que compartimos en la primera presentación del módulo y en lugar de configurar la fecha y hora con datos actuales usa una fecha y hora que muestre el cambio de centuria, verá lo limitado de nuestro ejemplo. Les dejo el Sketch modificado acá para que lo pruebe:

// Librería para convertir de BCD a decimal y viceversa
#include <bcdlib.h>

// Incluimos la librería del proyecto Arduino para I2C
#include "Wire.h"

// Esta es la dirección de nuestro módulo
#define DS3231_I2C_ADDRESS 0x68

void setup() {
  // Inicializamos el Monitor Serial para nuestras pruebas
  Serial.begin(9600);

  // Inicializamos nuestra librería
  Wire.begin();
  
  // Opcional: configuramos la hora
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  
  Wire.write(0x00); // El primer byte es la dirección en la que escribiremos
  
  // Ejemplo de cambio de centuria: 31 Dic 99, 23:59:40
  Wire.write(bcdlib::dec2bcd(40)); // Segundos 0-59
  Wire.write(bcdlib::dec2bcd(59)); // Minutos 0-59
  Wire.write(bcdlib::dec2bcd(23)); // Hora 0-23 en formato de 24 horas (aunque soporta formato 12h no lo veremos)
  Wire.write(bcdlib::dec2bcd(6));  // Día de la semana 1-7
  Wire.write(bcdlib::dec2bcd(31));  // Dia 1-31
  Wire.write(bcdlib::dec2bcd(12));  // Mes 1-12
  Wire.write(bcdlib::dec2bcd(99)); // Anio 0-99
  Wire.endTransmission();
}

void loop() {
  int x;
  byte buffer[7];
  
  // Para leer el reloj simplemente iniciamos una transmision y enviamos un 0 solamente
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set DS3231 register pointer to 00h
  Wire.endTransmission();
  
  // Solo leeremos 7 bytes ya que nos alcanza para la fecha y hora
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7); 
  
  // Leemos 7 bytes
  for (x=0; x<7; x++) {
    buffer[x] = Wire.read();
  }

  // Mostramos la fecha y hora leida
  Serial.println(String("Fecha (D/M/Y) ") + bcdlib::bcd2dec(buffer[4]) + "/" + bcdlib::bcd2dec(buffer[5])
    + "/20" + bcdlib::bcd2dec(buffer[6]) + ", Hora " + bcdlib::bcd2dec(buffer[2])
    + ":" + bcdlib::bcd2dec(buffer[1]) + ":" + bcdlib::bcd2dec(buffer[0]));
  Serial.println("");
  
  // Leeremos la hora cada 5 segundos
  delay(5000);  
}

Ahora si observa el cambio de centuria (ocurrirá en 20 segundos luego de abrir el Monitor Serial), verá que no solo el año aparecerá raro, sino que el més ahora es ridículo (81). El año aparece como 200 debido a que nosotros le agregábamos ’20’ al principio del año (por eso al principio muestra 2099 en lugar de 1999 ya que no entiende cual es la centuria), y al pasar de 99 a 0, ahora dice 200. Si hubieramos manejado el 0 de la izquierda y en lugar de 0 apareciera 00, entonces quedaría 2000, pero aun así sería raro, de 2099 pasaría a 2000.

El problema con el mes es diferente, el mes aparece bien, con el 1 de Enero, mes que sigue a Diciembre (12) pero ese 8 significa algo más. Si nos vamos a la hoja de datos (datasheet) veremos que para el registro del mes dice:

DIRECCIONBIT 7BIT 6BIT 5BIT 4BIT 3BIT 2BIT 1BIT 0
05hCenturia00MesMesMesMesMes

Entonces vemos que el bit 7 de dicho registro (así se les llama a cada uno de los bytes que leemos), se usa para marcar la centuria, y eso no afecta al mes ya que el mes va del 1 al 12 y el 12 en BCD usa solo 5 bits (4 para el 2 de la derecha, y solo 1 para el de la izquierda) con lo que le sobran 3 bits. Para trabajar correctamente con este registro y el Módulo de reloj RTC DS3231 deberíamos tratar a ese bit de forma diferenciada. Lo correcto sería sacarlo del mes, y si está seteado, usarlo para incrementar la centuria que deberíamos mantener fija nosotros. Una versión optimizada de del Sketch que les compartí pero teniendo en cuenta este concepto sería asi:

// Librería para convertir de BCD a decimal y viceversa
#include <bcdlib.h>

// Incluimos la librería del proyecto Arduino para I2C
#include "Wire.h"

// Esta es la dirección de nuestro módulo
#define DS3231_I2C_ADDRESS 0x68

// Centuria, comenzamos como 1900
unsigned char centuria = 19; 

void setup() {
  // Inicializamos el Monitor Serial para nuestras pruebas
  Serial.begin(9600);

  // Inicializamos nuestra librería
  Wire.begin();
  
  // Opcional: configuramos la hora
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  
  Wire.write(0x00); // El primer byte es la dirección en la que escribiremos
  
  // Prueba de la centuria, usamos 31 Dic 1999, 23:59:40
  Wire.write(bcdlib::dec2bcd(40)); // Segundos 0-59
  Wire.write(bcdlib::dec2bcd(59)); // Minutos 0-59
  Wire.write(bcdlib::dec2bcd(23)); // Hora 0-23 en formato de 24 horas (aunque soporta formato 12h no lo veremos)
  Wire.write(bcdlib::dec2bcd(6));  // Día de la semana 1-7
  Wire.write(bcdlib::dec2bcd(31));  // Dia 1-31
  Wire.write(bcdlib::dec2bcd(12));  // Mes 1-12
  Wire.write(bcdlib::dec2bcd(99)); // Anio 0-99
  Wire.endTransmission();
}

void loop() {
  int x;
  byte buffer[7];
  unsigned char centuriaCorregida; 
  
  // Para leer el reloj simplemente iniciamos una transmision y enviamos un 0 solamente
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set DS3231 register pointer to 00h
  Wire.endTransmission();
  
  // Solo leeremos 7 bytes ya que nos alcanza para la fecha y hora
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7); 
  
  // Leemos 7 bytes
  for (x=0; x<7; x++) {
    buffer[x] = Wire.read();
  }

  // Controlamos la centuria acá
  if (buffer[5]&0x80) {
    centuriaCorregida = centuria+1;
  }
  else {
    centuriaCorregida = centuria;
  }
  
  buffer[5] &= 0x7f;
  
  // Mostramos la fecha y hora leida
  // Esta version corrige los problemas de los simple 0 y doble 00 también
  Serial.println(String("Fecha (D/M/Y) ") + ((buffer[4] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[4]) + "/" 
    + ((buffer[5] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[5]) + "/" 
    + centuriaCorregida + ((buffer[6] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[6]) + ", Hora " 
    + ((buffer[2] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[2]) + ":" 
    + ((buffer[1] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[1]) + ":" 
    + ((buffer[0] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[0]));
  Serial.println("");
  
  // Leeremos la hora cada 5 segundos
  delay(5000);  
}

Formato de hora 12h vs 24h

Es cierto que el programador experimentado podría convertir el formato por defecto de 24h del Módulo RTC DS3231 en formato de 12h (con AM y PM), pero el módulo puede manejar los formatos en forma nativa y les explicaré como funciona este tema.

El registro de la hora está estratégicamente diseñado para que al inicio, en su formato 24h que es el formato por defecto, todo funcione sin que usted deba hacer nada al respecto. El registro de la hora extraído de la hoja de datos se ve así:

DIRECCIONBIT 7BIT 6BIT 5BIT 4BIT 3BIT 2BIT 1BIT 0
02h012/24AM/PM
2x Horas
1x HorasHorasHorasHorasHoras

Como ven el bit 6 solo se usa para determinar el formato, y por defecto está en 0, que significa 24h (1 significa 12h). El bit 5 tiene 2 propósitos o significados dependiendo del bit 6. Si el bit 6 está en formato 24h, el bit 5 se usa para cuando la hora supera los 19 (el 2 de las horas 20-23 necesita de este bit en BCD), pero cuando el bit 6 está en formato 12h, el bit 5 nos dice si es AM o PM mediante un 0 o un 1 respectivamente. A continuación les dejo el Sketch base modificado para que trabaje en formato 12h:

// Librería para convertir de BCD a decimal y viceversa
#include <bcdlib.h>

// Incluimos la librería del proyecto Arduino para I2C
#include "Wire.h"

// Esta es la dirección de nuestro módulo RTC DS3231
#define DS3231_I2C_ADDRESS 0x68

// Centuria, comenzamos como 1900
unsigned char centuria = 20; 

void setup() {
  // Inicializamos el Monitor Serial para nuestras pruebas
  Serial.begin(9600);

  // Inicializamos nuestra librería
  Wire.begin();
  
  // Opcional: configuramos la hora
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  
  Wire.write(0x00); // El primer byte es la dirección en la que escribiremos
  
  // Prueba de la centuria, usamos 31 Dic 1999, 23:59:40
  Wire.write(bcdlib::dec2bcd(00)); // Segundos 0-59
  Wire.write(bcdlib::dec2bcd(57)); // Minutos 0-59
  Wire.write(bcdlib::dec2bcd(9) | 0x20 | 0x40); // 9, 0x20 = PM, 0x40 = formato 12h
  Wire.write(bcdlib::dec2bcd(4));  // Día de la semana 1-7
  Wire.write(bcdlib::dec2bcd(20));  // Dia 1-31
  Wire.write(bcdlib::dec2bcd(6));  // Mes 1-12
  Wire.write(bcdlib::dec2bcd(17)); // Anio 0-99
  Wire.endTransmission();
}

void loop() {
  int x;
  byte buffer[7];
  unsigned char centuriaCorregida; 
  
  // Para leer el reloj simplemente iniciamos una transmision y enviamos un 0 solamente
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set DS3231 register pointer to 00h
  Wire.endTransmission();
  
  // Solo leeremos 7 bytes ya que nos alcanza para la fecha y hora
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7); 
  
  // Leemos 7 bytes
  for (x=0; x<7; x++) {
    buffer[x] = Wire.read();
  }

  // Mostramos la fecha y hora leida
  Serial.println(String("Fecha (D/M/Y) ") + ((buffer[4] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[4]) + "/" 
    + ((buffer[5] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[5]) + "/" 
    + centuria + ((buffer[6] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[6]) + ", Hora " 
    + (((buffer[2] & 0x1f) < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[2] & ((buffer[2] & 0x40) ? 0x1f : 0x3f)) + ":" 
    + ((buffer[1] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[1]) + ":" 
    + ((buffer[0] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[0]) 
    + ((buffer[2] & 0x40) ? ((buffer[2] & 0x20) ? "PM" : "AM") : ""));
    
  Serial.println("");
  
  // Leeremos la hora cada 5 segundos
  delay(5000);  
}

Alarmas con el RTC DS3231

Este módulo posee 2 alarmas que son levemente diferentes en cuanto a su capacidad de configuración, pero que en le resto funcionan de igual manera e incluso comparten algunos recursos. La alarma 1 es la más completa y es posible configurarla para que se active desde cada segundo hasta en un día del mes determinado a una hora específica. La alarma 2 es menos potente y permite configurarla para que se active desde cada minuto hasta en un día del mes determinado a una hora específica (a los 00 segundos del minuto configurado).

La siguiente tabla muestra los registros 07h a 0Ah que corresponden a la alarma 1:

DIRECCIONBIT 7BIT 6BIT 5BIT 4BIT 3BIT 2BIT 1BIT 0
07hA1M1SegundosSegundosSegundosSegundosSegundosSegundosSegundos
08hA1M2MinutosMinutosMinutosMinutosMinutosMinutosMinutos
09hA1M312/24AM/PM
2x Horas
1x HorasHorasHorasHorasHoras
0AhA1M4DY/DT2x/3x Día del mes1x Día del mesDía de la semana /
Día del mes
Día de la semana /
Día del mes
Día de la semana /
Día del mes
Día de la semana /
Día del mes

Como pueden ver, los bit 7 de los 4 registros son especiales, denominados A1M1 a A1M4 donde A1 corresponde a Alarma 1, y M1 a M4 son los modos en que la alarma funcionará segun la siguiente tabla:

DY/DTA1M4A1M3A1M2A1M1Modo
X1111Una vez por segundo
X1110Cuando los segundos coinciden
X1100Cuando los minutos y segundos coinciden
X1000Cuando la hora, minutos y segundos coinciden
00000Cuando el día del mes y hora completa coinciden
10000Cuando el día de la semana y hora completa coinciden

Entonces por ejemplo si queremos que una alarma 1 se active todos los dias a la misma hora, debemos poner A1M1 a A1M3 en 0, y A1M4 en 1, como en el siguiente ejemplo que les dejo en el Sketch a continuación. Los 2 casos especiales son cuando queremos configurar la alarma 1 para que se active en un día de la semana especifico o en un día del mes específico, para lo cual debemos poner A1M1 a A1M4 en 0, y solo configurar DY/DT en 0 o 1 dependiendo de la opción.

Un detalle antes de que prueben el Sketch en su RTC DS3231, es que la alarma cuando se activa, pone el bit 0 del registro 0Fh en 1 pero no lo vuelve a poner en 0 nunca más, por lo que nos toca a nosotros volverlo a 0 cuando terminamos de detectar la activación de la alarma (vea ese detalle en el código del Sketch).

// Librería para convertir de BCD a decimal y viceversa
#include <bcdlib.h>

// Incluimos la librería del proyecto Arduino para I2C
#include "Wire.h"

// Esta es la dirección de nuestro módulo
#define DS3231_I2C_ADDRESS 0x68
#define DS3231_REG_SIZE 19 // 19 registros en el RTC DS3231

// Centuria, comenzamos como 2000
unsigned char centuria = 20; 

void setup() {
  // Inicializamos el Monitor Serial para nuestras pruebas
  Serial.begin(9600);

  // Inicializamos nuestra librería
  Wire.begin();
  
  // Opcional: configuramos la hora
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  
  Wire.write(0x00); // El primer byte es la dirección en la que escribiremos
  
  // Primero seteamos el dia y hora actual
  Wire.write(bcdlib::dec2bcd(00)); // Segundos 0-59
  Wire.write(bcdlib::dec2bcd(30)); // Minutos 0-59
  Wire.write(bcdlib::dec2bcd(19)); // Hora 0-23 en formato de 24 horas (aunque soporta formato 12h no lo veremos)
  Wire.write(bcdlib::dec2bcd(4));  // Día de la semana 1-7
  Wire.write(bcdlib::dec2bcd(21));  // Dia 1-31
  Wire.write(bcdlib::dec2bcd(6));  // Mes 1-12
  Wire.write(bcdlib::dec2bcd(17)); // Anio 0-99
  
  // Ahora seteamos una alarma para que se active en 10 segundos
  // Solo activaremos A1M4 = 1 que significa cada dia, a la misma hora exacta
  Wire.write(bcdlib::dec2bcd(10)); // Segundos = 10 
  Wire.write(bcdlib::dec2bcd(30)); // Minutos = igual que la hora seteada arriba 
  Wire.write(bcdlib::dec2bcd(19)); // Hora = igual que la hora seteada arriba
  Wire.write(0x80); // No necesitamos setear nada acá más que A1M4
  
  Wire.endTransmission();
}

void loop() {
  int x;
  byte buffer[DS3231_REG_SIZE];
  
  // Para leer el reloj simplemente iniciamos una transmision y enviamos un 0 solamente
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set DS3231 register pointer to 00h
  Wire.endTransmission();
  
  // Leemos todos los registros para verificar la alarma que está en 0x0F
  Wire.requestFrom(DS3231_I2C_ADDRESS, DS3231_REG_SIZE); 
  
  // Leemos de a un byte a la vez
  for (x=0; x<DS3231_REG_SIZE; x++) {
    buffer[x] = Wire.read();
  }

  // Mostramos la fecha y hora leida
  Serial.println(String("Fecha (D/M/Y) ") + ((buffer[4] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[4]) + "/" 
    + ((buffer[5] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[5]) + "/" 
    + centuria + ((buffer[6] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[6]) + ", Hora " 
    + ((buffer[2] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[2]) + ":" 
    + ((buffer[1] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[1]) + ":" 
    + ((buffer[0] < 0x10) ? "0" : "") + bcdlib::bcd2dec(buffer[0]));
  
  if (buffer[0x0F] & 1) {
    Serial.println("Alarma activada!");
    
    // Desactivamos la alarma sino seguirá ahi
    Wire.beginTransmission(DS3231_I2C_ADDRESS);
    Wire.write(0x0F); // Escribimos directo en el registro del estado de la alarma
    Wire.write((buffer[0x0F] & 0xFE)); // Esto es solo para apagar el bit de la alarma 1 dejando el resto intacto
    Wire.endTransmission();
  }
  
  // Leeremos la hora cada 1 segundo
  delay(1000);  
}

La alarma 2

La alarma 2 es similar a la anterior y para su referencias les dejamos las tablas respectivas que tienen menos opciones dadas las limitaciones que ya explicamos. Entonces la primera tabla de del seteo de la alarma y corresponde a los registros del RTC DS3231 0Bh a 0Dh.

DIRECCIONBIT 7BIT 6BIT 5BIT 4BIT 3BIT 2BIT 1BIT 0
0BhA2M2MinutosMinutosMinutosMinutosMinutosMinutosMinutos
0ChA2M312/24AM/PM
2x Horas
1x HorasHorasHorasHorasHoras
0DhA2M4DY/DT2x/3x Día del mes1x Día del mesDía de la semana /
Día del mes
Día de la semana /
Día del mes
Día de la semana /
Día del mes
Día de la semana /
Día del mes

Y la tabla correspondiente al modo seteado en la Alarma 2 es la siguiente:

DY/DTA2M4A2M3A2M2Modo
X111Una vez por minuto (al segundo 00)
X110Cuando los minutos coinciden
X100Cuando las horas y minutos coinciden
0000Cuando el día del mes, hora y minutos coinciden
1000Cuando el día de la semana, hora y minutos coinciden

Y las consideraciones son similares. El bit que la Alarma 2 pone en 1 cuando se activa es el bit 1 del mismo registro 0Fh, lo que le dá independencia de la Alarma 1 en ese sentido.

Recursos comunes entre las 2 alarmas

Las alarmas tienen una función de interrupción. La misma permite al módulo, dependiendo de unos seteos en unos registros del RTC DS3231, activar una interrupción cuando una alarma, o la otra, o ambas se activan dependiendo de la configuración. Para configurar las interrupciones (que son activas en LOW, osea que se ponen a 0v o GND cuando está activa), se usa el registro 0Eh que para el propósito de este explicativo solo nos preocuparemos de los bits 0 a 2:

DIRECCIONBIT 7BIT 6BIT 5BIT 4BIT 3BIT 2BIT 1BIT 0
0EhEOSCBBSQWCONVRS2RS1INTCNA2IEA1IE

Entonces para activas las interrupciones ponemos INTCN en 1, y luego ponemos A2IE y/o A1IE en 1 también según activemos la alarma 1, 2 o ambas para que activen la interrupción. Esta función no modifica las funciones anteriores de las alarmas del RTC DS3231. La interrupción se activa en el pin SQW del módulo (que tiene otro propósito que escapa el propósito de éste instructivo).

4 comentarios en “Módulo de reloj RTC DS3231 – Avanzado”

  1. Hola una consulta respecto al formato de 12/24 Horas.
    Cuando colocas el valor de horas haces dos operaciones OR para agregar el formato a 12H y PM en la linea:
    Wire.write(bcdlib::dec2bcd(9) | 0x20 | 0x40); // 9, 0x20 = PM, 0x40 = formato 12h
    Y esto queda escrito en el registro de Horas osea la posición: 0x2 del reloj.

    Luego lees los 7 primeros registros del reloj y guardas en el buffer:
    for (x=0; x<7; x++) {
    buffer[x] = Wire.read();
    }
    La posición 2 del buffer contiene los datos de la hora (9) pero con las opciones de PM y 12H agregadas.
    La parte que no logro entender es cuando mandas imprimir las horas haces un If para agregar un CERO para números menores a 10.
    Mi consulta es si el valor contenido en el buffer[2] no es exactamente 9 , sino 9 + los bits para PM y el modo 12H . como es que puede cumplir con esta condición if.
    Saludos.

    Responder
    • Hola Luis, creo que simplemente encontraste un error en el codigo. Es un error inofensivo porque simplemente no agrega el cero nunca, pero para que cumpla con esa condicion deberia primer eliminar los bits de AM/PM y de 12h/24h y luego evaluar. Muy buen allazgo! en breve lo corrijo. Saludos!!

      Responder
  2. Hola, gracias por tu proyecto es muy bueno. Tengo una consulta porque para borrar el estatus de la alarma 1 activada usas:
    Wire.write((buffer[0x0F&0xFE]));

    Si el valor que deseamos enviar al registro: 0xF es 0000 1110 ( ó en Hex: 0xE ) entonces esta codigo sería:
    Wire.write(0xE); ó es que esto significa solo posicionarse en la posición de registro 0xE

    Gracias por tu respuesta.

    Responder
    • Hola Luis, creo que encontraste otro error en el código, excelente! Dejame explicarte (ya corregi el error en la pagina) como es esto: el registro 0x0f es el que tiene el flag de que la alarma se ha activado y nos toca resetearla. El bit 1 (0x01) es el que nos interesa y dejar el resto intacto. Entonces, lo que debemos hacer para apagar solo un bit es unsar una mascara de bits para apagar ese bit que en nuestro caso es 0xFE (con el bit 1 apagado) y hacerle una operacion and “&” pero al valor y no a la direccion, y ese fue mi error. Entonces, en lugar de buffer[0x0F&0xFE] me tocaba hacer buffer[0x0F] & 0xFE. Espero se entienda pero el cogigo ya quedo corregido. Gracias por interes y tu aporte! Saludos.

      Responder

Deja un comentario