Truques que aprendi sobre como usar um relógio de tempo real DS3231 para interromper um Arduino
Você deseja determinar momentos precisos em que o Arduino executará segmentos de código especiais em um esboço, sem usar os temporizadores e contadores do hardware do Arduino. Você tem alguma habilidade e experiência em escrever código com o IDE do Arduino e gostaria de evitar o uso de instruções de código como delay()
.
Em vez disso, você gostaria de conectar um módulo DS3231 Real Time Clock muito preciso como fonte de interrupções externas. Pode ser importante para você que o DS3231 possa usar uma bateria para manter a hora precisa, mesmo se o Arduino ficar temporariamente sem energia.
Finalmente, você deseja aprender como usar a biblioteca DS3231.h de Andrew Wickert, mencionada na referência online do Arduino: https://www.arduino.cc/reference/en/libraries/ds3231/. Ele contém alguns truques que podem valer a pena o esforço para dominar. Você pode importar esta biblioteca para o seu Arduino IDE usando o Gerenciador de Bibliotecas (Ferramentas > Gerenciar Bibliotecas...).
Vá passo a passo. Este tutorial demonstra as seguintes etapas:
Se desejar que o código especial seja executado mais de uma vez, em intervalos especificados, seu código poderá executar a Etapa 3 novamente e definir um novo alarme. O esboço de exemplo que acompanha este tutorial interrompe o Arduino repetidamente, em intervalos de 10 segundos.
Este tutorial baseia-se em referências “oficiais”, incluindo:
attachInterrupt()
: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/. O código especial neste exemplo é trivial: ele imprime apenas a hora atual do DS3231. Um exemplo da vida real pode fazer algo útil, como regar uma planta ou registrar uma medição de temperatura. Qualquer que seja a tarefa, o código deve entrar em um bloco próprio e especial para ser executado somente quando o alarme do DS3231 interromper o Arduino.
Acredito que seja uma boa prática dividir o código em funções curtas, onde cada função lida com apenas uma tarefa ou um conjunto de tarefas relacionadas. O nome de uma função pode ser qualquer coisa; por que não descrever o que a função faz? Aqui está parte da minha função que executa o código especial neste exemplo.
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
}
Você passará fios entre cinco pares de pinos. Cada par executa uma finalidade elétrica e combina um pino no Arduino com um pino correspondente no DS3231. Vá devagar, conecte cada par e verifique ambas as extremidades para ter certeza de que cada fio vai onde deveria. A tabela lista os pares em ordem à medida que eles são anexados, da esquerda para a direita, ao DS3231 a partir de um Arduino Uno.
Propósito | Pino DS3231 | Pino Arduino |
---|---|---|
Alarme | SQW | 3* |
SCL | SCL | SCL** |
IASD | IASD | IASD** |
Potência de 5 volts | CCV | 5V |
Chão | GND | GND |
Como eu disse, reserve um tempo para fazer essas conexões. Lento e seguro costuma ser a maneira mais rápida de concluir qualquer coisa corretamente.
Falaremos com o módulo DS3231 por meio de um "objeto" DS3231, uma espécie de caixa de ferramentas de software com um nome. A biblioteca DS3231 define muitas funções – pense nelas como ferramentas – dentro da caixa. Quando queremos utilizar uma função, escrevemos o nome da caixa de ferramentas, seguido de um ponto e depois o nome da ferramenta. O esboço de exemplo cria uma variável “clock” para essa finalidade. Então o esboço pode acessar as ferramentas na caixa “relógio”. Todas as ferramentas são declaradas no arquivo DS3231.h mencionado acima.
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
Usaremos ferramentas em nosso objeto “relógio” para definir o alarme. Mas primeiro precisamos calcular a hora do alarme.
Esta etapa pressupõe que você tenha configurado anteriormente a hora real no DS3231. O esboço de exemplo contém código que você pode usar para definir a hora no relógio, caso precise. Simplesmente remova os delimitadores de comentário, /* e */, que o cercam.
O esboço de exemplo neste tutorial calcula a hora do alarme no futuro adicionando um intervalo, como um número de segundos, à hora atual. O exemplo adiciona dez segundos. Um minuto acrescentaria 60 segundos. Uma hora acrescentaria 3.600 segundos. Um dia, 86.400 segundos. E assim por diante.
A biblioteca DS3231 possui um "truque" oculto que facilita a adição de tempo em segundos. Você não encontrará esse truque listado entre as funções disponíveis na página README da biblioteca DS3231. Também não é totalmente óbvio olhando o arquivo DS3231.h. Alguns dos detalhes aguardam para serem encontrados no arquivo de código DS3231.cpp. Aqui estão as etapas para realizar o cálculo.
now()
, que precisa ser acessada de forma especial.As etapas 4 e 5 podem ser combinadas.
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
Embora o objeto alarmTime seja criado com base em um número de segundos, ele fornece ferramentas em sua caixa de ferramentas para expressar ano, mês, dia, hora, minuto e segundo. Definimos a hora do alarme no DS3231, conforme descrito a seguir, usando o objeto alarmTime como fonte dos valores que precisaremos.
Suponha, por exemplo, que o currentTime relatado pelo módulo DS3231 fosse 7 segundos depois das 10h42 da manhã de quarta-feira, 27 de outubro de 2021. O alarmTime calculado acima seria 10h42:17 daquele mesmo dia, dez segundos depois.
Um DS3231 disponibiliza dois alarmes diferentes: Alarme nº 1 (A1) e Alarme nº 2 (A2). Ambos os alarmes podem ser especificados para um dia e hora, até um minuto. A diferença é que A1 pode ser especificado em até um segundo. Cada alarme possui seu próprio par de funções na biblioteca DS3231 para definir a hora do alarme e para ler essa hora. As funções são todas acessadas através de um objeto DS3231, por exemplo, aquele que chamamos de "relógio":
clock.setA1Time(), clock.getA1Time(), clock.setA2Time() e clock.getA2Time()
A função setA1Time() utiliza oito parâmetros, conforme listado nesta citação do arquivo DS3231.h:
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
Uma leitura atenta do arquivo de cabeçalho DS3231.h pode explicar os parâmetros. O que se segue aqui são minhas tentativas de reexplicá-los para mim mesmo com minhas próprias palavras. Se o leitor encontrar alguma discrepância entre minha versão e o arquivo de cabeçalho, presuma que o cabeçalho está correto.
Os primeiros cinco parâmetros são do tipo “byte”. O site cppreference define o tipo de byte desta forma https://en.cppreference.com/w/cpp/types/byte:
std::byte é um tipo distinto que implementa o conceito de byte conforme especificado na definição da linguagem C++.
Assim como char e unsigned char, ele pode ser usado para acessar a memória bruta ocupada por outros objetos (representação de objetos), mas diferentemente desses tipos, não é um tipo de caractere e não é um tipo aritmético. Um byte é apenas uma coleção de bits, e os únicos operadores definidos para ele são os bit a bit.
Podemos pensar nas variáveis do tipo byte para dia e hora como se fossem inteiros sem sinal, nesta situação específica. Eles podem conter um valor inteiro entre 0 e 255. CUIDADO: o escritor do código deve evitar valores absurdos. Por exemplo, um valor de 102 não faz sentido para nenhum destes parâmetros. É seu trabalho como escritor de código fornecer valores sensatos.
Vamos continuar com o alarmTime criado na etapa anterior: dia 27 do mês, aos 17 segundos depois das 10h42 da manhã. A listagem abaixo mostra como você pode fornecer esses valores na função. Listo cada parâmetro em sua própria linha, para torná-los mais legíveis por humanos e para permitir espaço para comentários. O exemplo aqui está incompleto; demonstra apenas os valores do tipo byte para data e hora. A função requer mais parâmetros, conforme descrito abaixo, e não será executada da forma mostrada aqui.
Aliás, observe que as variáveis “clock” e “alarmTime” são objetos, ou seja, são caixas de ferramentas de software. Como você pode ver, usamos ferramentas de dentro das respectivas caixas de ferramentas para acessar as informações contidas nos 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
);
O próximo parâmetro do tipo byte, denominado AlarmBits, é na verdade apenas uma coleção de bits. Os bits têm nomes definidos na folha de dados do DS3231 (na página 11).
Parte 7 | Parte 6 | Parte 5 | Parte 4 | Parte 3 | Parte 2 | Parte 1 | Bit 0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
Juntos, os bits formam uma “máscara”, ou padrão, que informa ao DS3231 quando e com que frequência sinalizar um alarme. Uma tabela na página 12 da folha de dados fornece o significado para diferentes coleções de bits. Com base nessa tabela, o esboço de exemplo neste tutorial usa a seguinte coleção de bits:
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
Este arranjo de bits pode ser expresso explicitamente no código: 0x00001110
. Diz ao DS3231 para sinalizar o alarme sempre que "os segundos coincidirem", ou seja, quando o valor de "segundos" da configuração do alarme corresponder ao valor de "segundos" da hora atual.
Os três parâmetros finais da função setA1Time()
são valores booleanos ou verdadeiro/falso. Eles fornecem ao DS3231 mais informações sobre como avaliar a configuração do alarme. O segmento de código a seguir mostra a chamada concluída para setA1Time(), continuando o exemplo iniciado acima:
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.
);
No esboço de exemplo, onde configuramos o alarme para interromper a cada 10 segundos, apenas os parâmetros A1Second e AlarmBits são importantes. No entanto, precisamos fornecer todos eles quando chamarmos a função setA1Time()
. Valores corretos não são mais difíceis de fornecer do que valores inúteis; podemos muito bem ter cuidado com eles.
A função setA2Time()
funciona de forma semelhante, mas sem parâmetro para segundos. Reserve algum tempo para revisar as linhas 119 a 145 do arquivo DS3231.h na biblioteca e as páginas 11 a 12 na folha de dados. Aguarde pacientemente essas referências até encontrar nelas as informações necessárias para definir a hora do alarme.
Após definir a hora, o sketch deve realizar ações adicionais para habilitar o alarme no DS3231. Encorajo o leitor a seguir consistentemente uma sequência de três passos, mesmo que algum passo possa parecer menos necessário em algum momento por algum motivo. Se o seu código deve errar, deixe-o errar pelo lado da certeza.
Para o alarme A1, as instruções da biblioteca DS3231 seriam:
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 o alarme A2 basta alterar o parâmetro para 2. Por exemplo: checkIfAlarm(2); // clear A2 flag bit in register 0Fh
.
Um problema descrito neste repositório por @flowmeter enfatiza que ambos os sinalizadores de alarme devem ser apagados antes que o DS3231 possa sinalizar um alarme. Para ter certeza, considere chamar checkIfAlarm() duas vezes, uma para cada alarme, mesmo se você estiver usando apenas um dos alarmes :
checkIfAlarm(1);
checkIfAlarm(2);
Por que os criadores de código escolheriam “verificar” um alarme que eles acreditam não estar enviando um sinal no momento? A razão é que a função checkIfAlarm()
tem um efeito colateral não óbvio. Limpa o bit do sinalizador de alarme. Usamos a função checkIfAlarm()
, porque é a única na biblioteca DS3231 que executa a operação necessária.
Pense nisso. Por motivos que serão explicados abaixo, o hardware de detecção de interrupção do Arduino precisa que a tensão no pino SQW do DS3231 seja ALTA antes do momento em que o alarme ocorre. O evento de alarme altera duas coisas dentro do DS3231:
O pino SQW permanecerá em nível BAIXO enquanto um desses bits de sinalização de alarme permanecer definido. Enquanto um bit de sinalização de alarme dentro do DS3231 mantiver o pino SQW em nível BAIXO, o Arduino não poderá detectar mais nenhum alarme. Os bits do sinalizador de alarme devem ser apagados para que o DS3231 restaure uma tensão ALTA em seu pino de alarme SQW. Consulte a discussão dos bits 1 e 0 no "Registro de status (0Fh)", na página 14 da folha de dados do DS3231.
Cada alarme possui seu próprio bit de sinalização de alarme dentro do DS3231. Qualquer um dos bits do sinalizador de alarme pode manter o pino SQW em nível BAIXO. O DS3231 não limpará um bit de sinalização de alarme por sua própria iniciativa. É função do redator do código limpar os bits do sinalizador de alarme após a ocorrência do alarme .
Seu loop principal não precisa medir o tempo. Basta verificar um sinalizador para ver se ocorreu um alarme. No esboço de exemplo, esse sinalizador é uma variável booleana chamada "alarmEventFlag":
if (alarmEventFlag == true) {
// run the special code
}
Na maioria das vezes, o sinalizador será false e o loop ignorará o código especial. Como o esboço configura a bandeira? Três etapas:
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
fornecida pelo Arduino IDE reúne tudo. O exemplo a seguir diz ao hardware Arduino para executar a função rtcISR()
imediatamente sempre que detectar um sinal “FALLING” em um pino digital designado.attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
Por razões profundas e ocultas, sempre use a função especial digitalPinToInterrupt()
, ao especificar o número do pino para uma interrupção. Deixo como exercício para o leitor descobrir porque precisamos dessa função.
O que é um sinal de QUEDA? Significa uma mudança na tensão de ALTO para BAIXO, conforme detectado pelo pino digital do Arduino. De onde vem a mudança de tensão? Origina-se no pino de alarme do módulo DS3231. Esse pino é rotulado como SQW e emite uma tensão ALTA, próxima ao nível de alimentação VCC (ou seja, 5 volts no Uno) na maioria das vezes. Um alarme faz com que o DS3231 altere a tensão no pino SQW para BAIXO. O Arduino detecta a tensão proveniente do pino SQW e percebe a mudança. Dizemos ao Arduino para perceber QUEDA porque esse evento só acontece uma vez por alarme, enquanto o nível BAIXO pode persistir e confundir o Arduino, fazendo com que ele acione muitas interrupções.
Qual pino pode detectar uma mudança QUEDA na tensão? Para Unos, você pode escolher entre os pinos 2 ou 3. Para Leonardo, pode ser qualquer um dos pinos 0, 1 ou 7. (Sim, eu sei, Leonardo também detecta interrupções nos pinos 2 e 3. No entanto, esses são Pinos I2C do Leonardo, o que significa que o módulo DS3231 os usaria. Começo com o pino 7 para interrupções em um Leonardo.) O esboço de exemplo define uma variável dataPin e inicializa seu valor para. 3 para rodar em um Uno desta forma:
int dataPin = 3;
O código especial também pode definir um novo horário de alarme, se você quiser repetir o ciclo. Comece calculando a nova hora do alarme, conforme descrito na Etapa 3, e siga a sequência de etapas a partir daí.
Eu esperaria que o esboço de exemplo produzisse uma saída semelhante à ilustração abaixo, quando executado em um Arduino Uno conectado corretamente a um módulo DS3231, da maneira que descrevo neste tutorial. Não se surpreenda se os horários exibidos forem diferentes. Você deveria trabalhar com seu próprio tempo, de qualquer maneira.