Resposta curta: não tente “manipular” a rolagem em milissegundos, escreva um código seguro contra rolagem. Seu código de exemplo do tutorial está bom. Se você tentar detectar a sobreposição para implementar medidas corretivas, é provável que esteja fazendo algo errado. A maioria dos programas do Arduino precisa apenas gerenciar eventos que duram relativamente curtos períodos, como rebater um botão por 50 ms ou ligar um aquecedor por 12 horas ... Então, e mesmo se o programa for executado por anos, a rolagem em milissegundos não deve ser uma preocupação.
A maneira correta de gerenciar (ou melhor, evitar a necessidade de gerenciar) o problema de sobreposição é pensar no unsigned long
número retornado
millis()
em termos de aritmética modular . Para os inclinados matematicamente, alguma familiaridade com esse conceito é muito útil na programação. Você pode ver a matemática em ação no artigo millis () overflow de Nick Gammon ... uma coisa ruim? . Para aqueles que não querem passar pelos detalhes computacionais, ofereço aqui uma maneira alternativa (talvez mais simples) de pensar sobre isso. É baseado na distinção simples entre instantes e durações . Contanto que seus testes envolvam apenas comparações de durações, você estará bem.
Nota no micros () : tudo o que foi dito aqui millis()
se aplica igualmente a micros()
, exceto pelo fato de que micros()
rola a cada 71,6 minutos e a setMillis()
função fornecida abaixo não afeta micros()
.
Instantes, carimbos de data e hora e durações
Ao lidar com o tempo, precisamos fazer a distinção entre pelo menos dois conceitos diferentes: instantes e durações . Um instante é um ponto no eixo do tempo. Uma duração é a duração de um intervalo de tempo, ou seja, a distância no tempo entre os instantes que definem o início e o fim do intervalo. A distinção entre esses conceitos nem sempre é muito nítida na linguagem cotidiana. Por exemplo, se eu disser " voltarei em cinco minutos ", " cinco minutos " é a duração estimada
da minha ausência, enquanto " em cinco minutos " é o instante
do meu previsto voltar. Manter a distinção em mente é importante, porque é a maneira mais simples de evitar completamente o problema de rolagem.
O valor de retorno de millis()
pode ser interpretado como uma duração: o tempo decorrido desde o início do programa até agora. Essa interpretação, no entanto, quebra assim que o milissegundo excede. Geralmente é muito mais útil pensar millis()
em retornar um
carimbo de data / hora , ou seja, um "rótulo" identificando um determinado instante. Pode-se argumentar que essa interpretação é ambígua, pois são reutilizados a cada 49,7 dias. No entanto, isso raramente é um problema: na maioria dos aplicativos incorporados, qualquer coisa que aconteceu 49,7 dias atrás é uma história antiga com a qual não nos importamos. Portanto, reciclar os rótulos antigos não deve ser um problema.
Não compare timestamps
Tentar descobrir qual entre dois registros de data e hora é maior que o outro não faz sentido. Exemplo:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Ingenuamente, seria de esperar que a condição do if ()
fosse sempre verdadeira. Mas, na verdade, será falso se o millis exceder o limite durante
delay(3000)
. Pensar em t1 e t2 como etiquetas recicláveis é a maneira mais simples de evitar o erro: a etiqueta t1 foi claramente atribuída a um instante anterior a t2, mas em 49,7 dias será reatribuída para um instante futuro. Assim, t1 acontece antes e depois de t2. Isso deve deixar claro que a expressão t2 > t1
não faz sentido.
Mas, se esses são meros rótulos, a pergunta óbvia é: como podemos fazer cálculos de tempo úteis com eles? A resposta é: restringindo-nos aos únicos dois cálculos que fazem sentido para os registros de data e hora:
later_timestamp - earlier_timestamp
produz uma duração, ou seja, a quantidade de tempo decorrido entre o instante anterior e o instante posterior. Esta é a operação aritmética mais útil que envolve registros de data e hora.
timestamp ± duration
gera um registro de data e hora que demora algum tempo após (se usar +) ou antes (se -) do registro de data e hora inicial. Não é tão útil quanto parece, pois o carimbo de data e hora resultante pode ser usado em apenas dois tipos de cálculos ...
Graças à aritmética modular, é garantido que ambos funcionem bem ao longo da rolagem em milissegundos, pelo menos enquanto os atrasos envolvidos forem menores que 49,7 dias.
Comparar durações é bom
Uma duração é apenas a quantidade de milissegundos decorridos durante algum intervalo de tempo. Desde que não precisemos lidar com durações superiores a 49,7 dias, qualquer operação que faça sentido fisicamente também deve fazer sentido computacionalmente. Podemos, por exemplo, multiplicar uma duração por uma frequência para obter vários períodos. Ou podemos comparar duas durações para saber qual é mais longa. Por exemplo, aqui estão duas implementações alternativas de delay()
. Primeiro, o de buggy:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
unsigned long finished = start + ms; // finished: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
if (now >= finished) // comparing timestamps: BUG!
return;
}
}
E aqui está o correto:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
unsigned long elapsed = now - start; // elapsed: duration
if (elapsed >= ms) // comparing durations: OK
return;
}
}
A maioria dos programadores C escreveria os loops acima em uma forma de terser, como
while (millis() < start + ms) ; // BUGGY version
e
while (millis() - start < ms) ; // CORRECT version
Embora pareçam enganosamente similares, a distinção de carimbo de data / hora deve deixar claro qual é o buggy e qual é o correto.
E se eu realmente precisar comparar registros de data e hora?
Melhor tentar evitar a situação. Se for inevitável, ainda há esperança se se souber que os respectivos instantes estão próximos o suficiente: menos de 24,85 dias. Sim, nosso atraso máximo gerenciável de 49,7 dias foi reduzido pela metade.
A solução óbvia é converter nosso problema de comparação de carimbo de data / hora em um problema de comparação de duração. Digamos que precisamos saber se t1 instantâneo é anterior ou posterior a t2. Escolhemos algum instante de referência em seu passado comum e comparamos as durações dessa referência até t1 e t2. O instante de referência é obtido subtraindo uma duração suficientemente longa de t1 ou t2:
unsigned long reference_instant = t2 - LONG_ENOUGH_DURATION;
unsigned long from_reference_until_t1 = t1 - reference_instant;
unsigned long from_reference_until_t2 = t2 - reference_instant;
if (from_reference_until_t1 < from_reference_until_t2)
// t1 is before t2
Isso pode ser simplificado como:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
É tentador simplificar ainda mais if (t1 - t2 < 0)
. Obviamente, isso não funciona, porque t1 - t2
, sendo calculado como um número não assinado, não pode ser negativo. Isso, no entanto, embora não seja portátil, funciona:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
A palavra-chave signed
acima é redundante (um simples long
sempre é assinado), mas ajuda a tornar clara a intenção. A conversão para um longo assinado é equivalente a uma configuração LONG_ENOUGH_DURATION
igual a 24,85 dias. O truque não é portátil porque, de acordo com o padrão C, o resultado é a implementação definida . Mas como o compilador gcc promete fazer a coisa certa , ele funciona de maneira confiável no Arduino. Se desejamos evitar o comportamento definido pela implementação, a comparação assinada acima é matematicamente equivalente a isso:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
com o único problema que a comparação olha para trás. Também é equivalente, desde que os longos tenham 32 bits, a este teste de bit único:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
Os últimos três testes são realmente compilados pelo gcc no mesmo código de máquina.
Como testo meu esboço em relação à sobreposição de milis
Se você seguir os preceitos acima, deve ser bom. Se você deseja testar, adicione esta função ao seu sketch:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
e agora você pode viajar no tempo com seu programa ligando para
setMillis(destination)
. Se você deseja que ele passe pelo excesso de milésimos repetidamente, como Phil Connors revivendo o Dia da Marmota, você pode colocar isso dentro loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
O carimbo de data / hora negativo acima (-3000) é implicitamente convertido pelo compilador em um comprimento não assinado correspondente a 3000 milissegundos antes do rollover (é convertido em 4294964296).
E se eu realmente precisar rastrear durações muito longas?
Se você precisar ativar um revezamento e desativá-lo três meses depois, será necessário rastrear os estouros de milissegundos. Existem muitas maneiras de fazer isso. A solução mais direta pode ser simplesmente estender millis()
para 64 bits:
uint64_t millis64() {
static uint32_t low32, high32;
uint32_t new_low32 = millis();
if (new_low32 < low32) high32++;
low32 = new_low32;
return (uint64_t) high32 << 32 | low32;
}
Isso basicamente conta os eventos de rolagem e usa essa contagem como os 32 bits mais significativos de uma contagem de milissegundos de 64 bits. Para que essa contagem funcione corretamente, a função precisa ser chamada pelo menos uma vez a cada 49,7 dias. No entanto, se for chamado apenas uma vez a cada 49,7 dias, em alguns casos, é possível que a verificação (new_low32 < low32)
falhe e o código perca uma contagem high32
. Usar millis () para decidir quando fazer a única chamada para esse código em um único "agrupamento" de millis (uma janela específica de 49,7 dias) pode ser muito perigoso, dependendo de como os prazos se alinham. Por segurança, se estiver usando millis () para determinar quando fazer as únicas chamadas para millis64 (), deve haver pelo menos duas chamadas em cada janela de 49,7 dias.
Lembre-se, porém, que a aritmética de 64 bits é cara no Arduino. Pode valer a pena reduzir a resolução do tempo para permanecer em 32 bits.