Preciso ler um sensor a cada cinco minutos, mas como meu esboço também tem outras tarefas, não posso apenas delay()
entre as leituras. Existe o tutorial Blink sem demora, sugerindo que eu codifique ao longo destas linhas:
void loop()
{
unsigned long currentMillis = millis();
// Read the sensor when needed.
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
readSensor();
}
// Do other stuff...
}
O problema é que millis()
ele voltará a zero depois de aproximadamente 49,7 dias. Como meu esboço deve ser executado por mais tempo, preciso garantir que a sobreposição não faça com que meu esboço falhe. Posso detectar facilmente a condição de sobreposição ( currentMillis < previousMillis
), mas não tenho certeza do que fazer.
Assim, minha pergunta: qual seria a maneira correta / mais simples de lidar com a
millis()
rolagem?
programming
time
millis
Edgar Bonet
fonte
fonte
previousMillis += interval
vez depreviousMillis = currentMillis
se eu quisesse uma certa frequência de resultados.previousMillis += interval
se você deseja frequência constante e tem certeza de que seu processamento leva menos deinterval
, maspreviousMillis = currentMillis
para garantir um atraso mínimo deinterval
.uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
Respostas:
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 retornadomillis()
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 amicros()
, exceto pelo fato de quemicros()
rola a cada 71,6 minutos e asetMillis()
função fornecida abaixo não afetamicros()
.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 pensarmillis()
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:
Ingenuamente, seria de esperar que a condição do
if ()
fosse sempre verdadeira. Mas, na verdade, será falso se o millis exceder o limite durantedelay(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ãot2 > 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:E aqui está o correto:
A maioria dos programadores C escreveria os loops acima em uma forma de terser, como
e
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:
Isso pode ser simplificado como:
É tentador simplificar ainda mais
if (t1 - t2 < 0)
. Obviamente, isso não funciona, porquet1 - 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:A palavra-chave
signed
acima é redundante (um simpleslong
sempre é assinado), mas ajuda a tornar clara a intenção. A conversão para um longo assinado é equivalente a uma configuraçãoLONG_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: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:
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:
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 dentroloop()
: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: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 contagemhigh32
. 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.
fonte
TL; DR Versão curta:
An
unsigned long
é de 0 a 4.294.967.295 (2 ^ 32 - 1).Digamos que
previousMillis
seja 4.294.967.290 (5 ms antes da sobreposição) ecurrentMillis
é 10 (10ms após a sobreposição). Em seguida,currentMillis - previousMillis
é 16 real (não -4.294.967.280), pois o resultado será calculado como um longo sem sinal (que não pode ser negativo, portanto, será revertido). Você pode verificar isso simplesmente por:Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16
Portanto, o código acima funcionará perfeitamente bem. O truque é sempre calcular a diferença de tempo e não comparar os dois valores de tempo.
fonte
unsigned
lógica, de modo que não adianta aqui!millis()
rolar duas vezes, mas é muito improvável que isso ocorra no código em questão.previousMillis
deve ter sido medido antescurrentMillis
, portanto, securrentMillis
for menor do quepreviousMillis
ocorreu uma sobreposição. A matemática acontece que, a menos que duas rolagens ocorram, você nem precisa pensar nisso.t2-t1
, e se puder garantir que at1
medição é feita antest2
, é equivalente a assinado(t2-t1)% 4,294,967,295
, daí a envolvente automática. Agradável!. Mas e se houver duas rolagens ouinterval
houver> 4.294.967.295?Envolva o
millis()
em uma classe!Lógica:
millis()
diretamente.Acompanhamento de reversões:
millis()
. Isso ajudará você a descobrir se omillis()
excesso foi excedido.Créditos do temporizador .
fonte
get_stamp()
51 vezes. Comparar atrasos em vez de timestamps certamente será mais eficiente.Adorei essa pergunta e as ótimas respostas que ela gerou. Primeiro, um comentário rápido sobre uma resposta anterior (eu sei, eu sei, mas ainda não tenho o representante para comentar. :-).
A resposta de Edgar Bonet foi incrível. Eu codigo há 35 anos e aprendi algo novo hoje. Obrigado. Dito isto, acredito no código para "E se eu realmente precisar rastrear durações muito longas?" a menos que você chame millis64 () pelo menos uma vez por período de rolagem. Realmente exigente e improvável que seja um problema em uma implementação do mundo real, mas aí está.
Agora, se você realmente deseja carimbos de data / hora cobrindo qualquer intervalo de tempo são (64 bits de milissegundos é de cerca de meio bilhão de anos, pelo meu acerto de contas), parece simples estender a implementação millis () existente para 64 bits.
Essas alterações no attinycore / fiação.c (estou trabalhando com o ATTiny85) parecem funcionar (estou assumindo que o código para outros AVRs seja muito semelhante). Veja as linhas com os comentários // BFB e a nova função millis64 (). Claramente, será tanto maior (98 bytes de código, 4 bytes de dados) quanto mais lento, e, como Edgar apontou, você quase certamente pode alcançar seus objetivos com apenas uma melhor compreensão da matemática inteira não assinada, mas foi um exercício interessante .
fonte
millis64()
só funciona se for chamado com mais frequência do que o período de rolagem. Eu editei minha resposta para apontar essa limitação. Sua versão não possui esse problema, mas apresenta outra desvantagem: faz aritmética de 64 bits no contexto de interrupção , o que ocasionalmente aumenta a latência na resposta a outras interrupções.