Trucos que aprendí sobre el uso de un reloj en tiempo real DS3231 para interrumpir un Arduino
Desea determinar los momentos precisos en los que Arduino ejecutará segmentos de código especiales en un boceto, sin utilizar los temporizadores y contadores del hardware Arduino. Tiene cierta habilidad y experiencia escribiendo código con el IDE de Arduino y le gustaría evitar el uso de declaraciones de código como delay()
.
En su lugar, le gustaría conectar un módulo de reloj en tiempo real DS3231 muy preciso como fuente de interrupciones externas. Puede ser importante para usted que el DS3231 pueda usar una batería para mantener la hora exacta incluso si el Arduino pierde energía temporalmente.
Finalmente, desea aprender a utilizar la biblioteca DS3231.h de Andrew Wickert a la que se hace referencia en la referencia en línea de Arduino: https://www.arduino.cc/reference/en/libraries/ds3231/. Contiene algunos trucos que bien vale la pena dominar. Puede importar esta biblioteca a su IDE de Arduino utilizando el Administrador de bibliotecas (Herramientas > Administrar bibliotecas...).
Vaya paso a paso. Este tutorial demuestra los siguientes pasos:
Si desea que el código especial se ejecute más de una vez, en los intervalos que especifique, su código puede realizar el Paso 3 nuevamente y configurar una nueva alarma. El boceto de ejemplo que acompaña a este tutorial interrumpe el Arduino repetidamente, en intervalos de 10 segundos.
Este tutorial se basa en referencias "oficiales", que incluyen:
attachInterrupt()
: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/. El código especial en este ejemplo es trivial: solo imprime la hora actual del DS3231. Un ejemplo de la vida real podría hacer algo útil como regar una planta o registrar una medición de temperatura. Cualquiera que sea la tarea, el código debe ir a su propio bloque especial para ejecutarse sólo cuando la alarma DS3231 interrumpa el Arduino.
Creo que es una buena práctica dividir el código en funciones cortas, donde cada función maneja solo una tarea o un conjunto de tareas relacionadas. El nombre de una función puede ser cualquier cosa; ¿Por qué no hacer que describa lo que hace la función? Aquí hay parte de mi función que ejecuta el código especial en este ejemplo.
void runTheSpecialCode() {
// get the current time
// using the DateTime and RTClib classes
// defined in DS3231.h
DateTime dt = RTClib::now();
// print the current time
Serial.print(dt.hour()); Serial.print(":");
if (dt.minute() < 10) Serial.print("0");
Serial.print(dt.minute()); Serial.print(":");
if (dt.second() < 10) Serial.print("0");
Serial.println(dt.second());
// There will be more to do here, as you will see.
// This is enough, for now, to illustrate the idea:
// put special code in its own, special function
}
Pasará cables entre cinco pares de pines. Cada par realiza un propósito eléctrico y hace coincidir un pin del Arduino con un pin correspondiente del DS3231. Tómelo con calma, conecte cada par y luego verifique ambos extremos para asegurarse de que cada cable vaya donde debe. La tabla enumera los pares en orden a medida que se conectan, de izquierda a derecha, al DS3231 desde un Arduino Uno.
Objetivo | DS3231 Pasador | pin arduino |
---|---|---|
Alarma | SQW | 3* |
SCL | SCL | SCL** |
ASD | ASD | ASD** |
potencia de 5 voltios | VCC | 5V |
Suelo | Tierra | Tierra |
Como dije, tómate tu tiempo para hacer estas conexiones. Lento y seguro suele ser la forma más rápida de completar cualquier cosa correctamente.
Hablaremos con el módulo DS3231 mediante un "objeto" DS3231, una especie de caja de herramientas de software con un nombre. La biblioteca DS3231 define muchas funciones (considérelas como herramientas) dentro de la caja. Cuando queremos utilizar una función, escribimos el nombre de la caja de herramientas, seguido de un punto, luego el nombre de la herramienta. El boceto de ejemplo crea una variable "reloj" para este propósito. Luego, el boceto puede acceder a las herramientas en el cuadro "reloj". Todas las herramientas se declaran en el archivo DS3231.h, mencionado anteriormente.
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
Usaremos herramientas en nuestro objeto "reloj" para configurar la alarma. Pero primero necesitamos calcular la hora de la alarma.
Este paso supone que usted ha configurado previamente la hora real en el DS3231. El boceto de ejemplo contiene código que puede utilizar para configurar la hora en el reloj, en caso de que lo necesite. Simplemente elimine los delimitadores de comentarios, /* y */, que lo rodean.
El boceto de ejemplo de este tutorial calcula una hora de alarma en el futuro agregando un intervalo, en forma de segundos, a la hora actual. El ejemplo añade diez segundos. Un minuto sumaría 60 segundos. Una hora sumaría 3.600 segundos. Un día, 86.400 segundos. Y así sucesivamente.
La biblioteca DS3231 tiene un "truco" oculto que facilita agregar tiempo en forma de segundos. No encontrará este truco entre las funciones disponibles en la página README de la biblioteca DS3231. Tampoco es del todo obvio al mirar el archivo DS3231.h. Algunos de los detalles esperan ser encontrados en el archivo de código DS3231.cpp. Estos son los pasos para realizar el cálculo.
now()
, a la que se debe acceder de una manera especial.Los pasos 4 y 5 se pueden combinar.
const Uint32_t interval = 10; // number of seconds to add
DateTime currentTime; // default declaration
currentTime = RTClib::now(); // RTClib is defined in DS3231.h
uint32_t currentSeconds = currentTime.unixtime(); // express the date in seconds
DateTime alarmTime(currentSeconds + interval); // add 10 seconds and create a new date
Aunque el objeto alarmTime se crea en función de una cantidad de segundos, proporciona herramientas en su caja de herramientas para expresar su año, mes, día, hora, minuto y segundo. Configuramos la hora de la alarma en el DS3231, como se describe a continuación, utilizando el objeto alarmTime como fuente de los valores que necesitaremos.
Supongamos, por ejemplo, que la hora actual informada por el módulo DS3231 fue 7 segundos después de las 10:42 de la mañana del miércoles 27 de octubre de 2021. La hora de alarma calculada anteriormente sería las 10:42:17 de ese mismo día, diez segundos después.
Un DS3231 pone a disposición dos alarmas diferentes: Alarma n.º 1 (A1) y Alarma n.º 2 (A2). Ambas alarmas se pueden especificar en un día y hora, hasta un minuto. La diferencia es que A1 se puede especificar más hasta un segundo. Cada alarma tiene su propio par de funciones en la biblioteca DS3231 para configurar la hora de la alarma y leer esa hora. Se accede a todas las funciones a través de un objeto DS3231, por ejemplo, el que llamamos "reloj":
reloj.setA1Time(), reloj.getA1Time(), reloj.setA2Time() y reloj.getA2Time()
La función setA1Time() toma ocho parámetros, como se enumeran en esta cita del archivo DS3231.h:
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
Una lectura minuciosa del archivo de encabezado DS3231.h puede explicar los parámetros. Lo que sigue aquí son mis intentos de volver a explicármelos a mí mismo con mis propias palabras. Si el lector encuentra alguna discrepancia entre mi versión y el archivo de encabezado, asuma que el encabezado es correcto.
Los primeros cinco parámetros son de tipo "byte". El sitio web de cppreference define el tipo de byte de esta manera https://en.cppreference.com/w/cpp/types/byte:
std::byte es un tipo distinto que implementa el concepto de byte como se especifica en la definición del lenguaje C++.
Al igual que char y unsigned char, se puede utilizar para acceder a la memoria sin procesar ocupada por otros objetos (representación de objetos), pero a diferencia de esos tipos, no es un tipo de carácter ni un tipo aritmético. Un byte es sólo una colección de bits, y los únicos operadores definidos para él son los bit a bit.
Podemos permitirnos pensar en las variables de tipo byte para el día y la hora como si fueran enteros sin signo, en esta situación particular. Pueden contener un valor entero entre 0 y 255. PRECAUCIÓN: el escritor del código debe evitar valores sin sentido. Por ejemplo, un valor de 102 no tiene sentido para ninguno de estos parámetros. Es su trabajo como escritor de código proporcionar valores sensatos.
Sigamos con la hora de alarma creada en el paso anterior: el día 27 del mes, a las 10:42 de la mañana, 17 segundos. La siguiente lista muestra cómo puede proporcionar esos valores en la función. Enumero cada parámetro en su propia línea, para hacerlos más legibles para los humanos y dejar espacio para comentarios. El ejemplo aquí está incompleto; muestra solo los valores de tipo byte para fecha y hora. La función requiere más parámetros, como se describe a continuación, y no se ejecutará en el formulario que se muestra aquí.
Por cierto, observe que las variables "reloj" y "hora de alarma" son objetos, es decir, son cajas de herramientas de software. Como puede ver, utilizamos herramientas desde el interior de las respectivas cajas de herramientas para acceder a la información que contienen los objetos.
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
// ... the remaining parameters are explained below
);
El siguiente parámetro de tipo byte, denominado AlarmBits, en realidad es sólo una colección de bits. Los bits tienen nombres tal como se definen en la hoja de datos DS3231 (en la página 11).
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
Juntos, los bits forman una "máscara" o patrón, que le indica al DS3231 cuándo y con qué frecuencia señalar una alarma. Una tabla en la página 12 de la hoja de datos proporciona el significado de diferentes colecciones de bits. Según esa tabla, el boceto de ejemplo de este tutorial utiliza la siguiente colección de bits:
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
Esta disposición de bits se puede expresar explícitamente en el código: 0x00001110
. Le indica al DS3231 que señale la alarma siempre que "los segundos coincidan", es decir, cuando el valor de "segundos" de la configuración de la alarma coincida con el valor de "segundos" de la hora actual.
Los últimos tres parámetros de la función setA1Time()
son valores booleanos o verdadero/falso. Le dan al DS3231 más información sobre cómo evaluar la configuración de la alarma. El siguiente segmento de código muestra la llamada completa a setA1Time(), continuando con el ejemplo iniciado anteriormente:
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
0x00001110, // AlarmBits = signal when the seconds match
false, // A1Dy false = A1Day means the date in the month;
// true = A1Day means the day of the week
false, // A1h12 false = A1Hour in range 0..23;
// true = A1Hour in range 1..12 AM or PM
false // A1PM false = A1Hour is a.m.;
// true = A1Hour is p.m.
);
En el boceto de ejemplo, donde configuramos la alarma para que se interrumpa cada 10 segundos, solo importan los parámetros A1Second y AlarmBits. Sin embargo, debemos proporcionarlos todos cuando llamamos a la función setA1Time()
. Los valores correctos no son más difíciles de proporcionar que los valores basura; También podríamos tener cuidado con ellos.
La función setA2Time()
funciona de manera similar, pero sin un parámetro para los segundos. Tómese un tiempo para revisar las líneas 119 a 145 del archivo DS3231.h en la biblioteca y las páginas 11 y 12 de la hoja de datos. Siéntate pacientemente con estas referencias hasta que hayas encontrado en ellas la información que necesitas para poner una hora de alarma.
Después de configurar la hora, el boceto debe tomar medidas adicionales para habilitar la alarma en el DS3231. Animo al lector a seguir constantemente una secuencia de tres pasos, incluso si algún paso puede parecer menos necesario en algún momento por alguna razón. Si su código debe errar, déjelo errar por el lado de la certeza.
Para la alarma A1, las instrucciones en la biblioteca DS3231 serían:
turnOffAlarm(1); // clear the A1 enable bit in register 0Eh
checkIfAlarm(1); // clear the A1 alarm flag bit in register 0Fh
turnOnAlarm(1); // set the A1 enable bit in register 0Eh
Para la alarma A2, simplemente cambie el parámetro a 2. Por ejemplo: checkIfAlarm(2); // clear A2 flag bit in register 0Fh
.
Un problema descrito en este repositorio por @flowmeter enfatiza que ambos indicadores de alarma deben borrarse antes de que el DS3231 pueda señalar una alarma. Para estar seguro, considere llamar a checkIfAlarm() dos veces, una para cada alarma, incluso si está usando solo una de las alarmas :
checkIfAlarm(1);
checkIfAlarm(2);
¿Por qué los escritores de códigos elegirían "verificar" una alarma que creen que no está enviando una señal actualmente? La razón es que la función checkIfAlarm()
tiene un efecto secundario no obvio. Borra el bit del indicador de alarma. Usamos la función checkIfAlarm()
, porque es la única en la biblioteca DS3231 que realiza la operación necesaria.
Piénselo. Por razones que se explicarán a continuación, el hardware de detección de interrupciones Arduino necesita que el voltaje en el pin SQW del DS3231 esté ALTO antes del momento en que ocurre la alarma. El evento de alarma cambia dos cosas dentro del DS3231:
El pin SQW permanecerá BAJO mientras cualquiera de esos bits de bandera de alarma permanezca configurado. Mientras un bit de indicador de alarma dentro del DS3231 mantenga el pin SQW BAJO, el Arduino no podrá detectar más alarmas. Los bits del indicador de alarma deben borrarse para que el DS3231 restablezca un voltaje ALTO en su pin de alarma SQW. Consulte la discusión sobre los bits 1 y 0 en el "Registro de estado (0Fh)", en la página 14 de la hoja de datos DS3231.
Cada alarma tiene su propio bit de indicador de alarma dentro del DS3231. Cualquiera de los bits del indicador de alarma puede mantener el pin SQW en BAJO. El DS3231 no borrará un bit de indicador de alarma por iniciativa propia. Es trabajo del escritor del código borrar los bits del indicador de alarma después de que ocurre la alarma .
Su bucle principal no necesita medir el tiempo. Sólo necesita comprobar una bandera para ver si se ha producido una alarma. En el boceto de ejemplo, este indicador es una variable booleana denominada "alarmEventFlag":
if (alarmEventFlag == true) {
// run the special code
}
La mayoría de las veces, la bandera será falsa y el bucle omitirá el código especial. ¿Cómo coloca el boceto la bandera? Tres pasos:
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
proporcionada por el IDE de Arduino lo reúne todo. El siguiente ejemplo le indica al hardware Arduino que ejecute la función rtcISR()
inmediatamente cada vez que detecte una señal de "CAÍDA" en un pin digital designado.attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
Por razones profundas y ocultas, utilice siempre la función especial, digitalPinToInterrupt()
, al especificar el número de pin para una interrupción. Lo dejo como ejercicio para que el lector descubra por qué necesitamos esa función.
¿Qué es una señal de CAÍDA? Significa un cambio de voltaje, de BAJO a ALTO, según lo detectado por el pin digital del Arduino. ¿De dónde viene el cambio de voltaje? Se origina en el pin de alarma del módulo DS3231. Ese pin está etiquetado como SQW y emite un voltaje ALTO, cerca del nivel de suministro de VCC (es decir, 5 voltios en el Uno) la mayor parte del tiempo. Una alarma hace que el DS3231 cambie el voltaje en el pin SQW a BAJO. El Arduino detecta el voltaje proveniente del pin SQW y nota el cambio. Le decimos al Arduino que note una CAÍDA porque ese evento solo ocurre una vez por alarma, mientras que el nivel BAJO puede persistir y confundir al Arduino y provocar muchas interrupciones.
¿Qué pin puede detectar un cambio de voltaje DESCENDENTE? Para Unos, puedes elegir entre los pines 2 o 3. Para Leonardo, puede ser cualquiera de los pines 0, 1 o 7. (Sí, lo sé, Leonardo también detecta interrupciones en los pines 2 y 3. Sin embargo, esas son Los pines I2C de Leonardo, lo que significa que el módulo DS3231 los usaría. Empiezo con el pin 7 para interrupciones en un Leonardo). El boceto de ejemplo define una variable dataPin e inicializa su valor. 3 para ejecutar en un Uno de esta manera:
int dataPin = 3;
El código especial también puede establecer una nueva hora de alarma, si desea repetir el ciclo. Comience calculando la nueva hora de la alarma, como se describe en el Paso 3, y siga la secuencia de pasos a partir de ahí.
Esperaría que el boceto de ejemplo produzca un resultado similar a la ilustración a continuación, cuando se ejecuta en un Arduino Uno correctamente conectado a un módulo DS3231, como lo describo en este tutorial. No se sorprenda si los horarios mostrados son diferentes. De todos modos, deberías trabajar con tus propios tiempos.