Necesito leer un sensor cada cinco minutos, pero como mi boceto también tiene otras tareas que hacer, no puedo simplemente delay()
entre las lecturas. Existe el tutorial de Blink sin demora que sugiere que codifique a lo largo de estas líneas:
void loop()
{
unsigned long currentMillis = millis();
// Read the sensor when needed.
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
readSensor();
}
// Do other stuff...
}
El problema es que millis()
volverá a cero después de aproximadamente 49.7 días. Dado que mi boceto está diseñado para ejecutarse por más tiempo, necesito asegurarme de que el rollover no haga que mi boceto falle. Puedo detectar fácilmente la condición de vuelco ( currentMillis < previousMillis
), pero no estoy seguro de qué hacer entonces.
Por lo tanto, mi pregunta: ¿cuál sería la forma correcta / más simple de manejar el
millis()
rollover?
programming
time
millis
Edgar Bonet
fuente
fuente
previousMillis += interval
lugar depreviousMillis = currentMillis
si quisiera una cierta frecuencia de resultados.previousMillis += interval
si desea una frecuencia constante y está seguro de que su procesamiento tarda menosinterval
, peropreviousMillis = currentMillis
para garantizar un retraso mínimo deinterval
.uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
Respuestas:
Respuesta corta: no intente "manejar" el rollover millis, en su lugar escriba un código seguro para rollover. Su código de ejemplo del tutorial está bien. Si intenta detectar el vuelco para implementar medidas correctivas, es probable que esté haciendo algo mal. La mayoría de los programas de Arduino solo tienen que gestionar eventos que abarcan duraciones relativamente cortas, como eliminar el botón de un botón durante 50 ms o encender un calentador durante 12 horas ... Entonces, e incluso si el programa está destinado a funcionar durante años, el vuelco del millis no debería ser una preocupación.
La forma correcta de gestionar (o más bien, evitar tener que gestionar) el problema de rollover es pensar en el
unsigned long
número devueltomillis()
en términos de aritmética modular . Para los matemáticamente inclinados, cierta familiaridad con este concepto es muy útil cuando se programa. Puedes ver las matemáticas en acción en el desbordamiento millis () del artículo de Nick Gammon ... ¿algo malo? . Para aquellos que no quieren pasar por los detalles computacionales, les ofrezco aquí una forma alternativa (ojalá más simple) de pensar al respecto. Se basa en la simple distinción entre instantes y duraciones . Siempre que sus pruebas solo incluyan la comparación de duraciones, debería estar bien.Nota sobre micros () : todo lo que se menciona aquí se
millis()
aplica igualmentemicros()
, excepto por el hecho de que semicros()
transfiere cada 71.6 minutos, y lasetMillis()
función proporcionada a continuación no afectamicros()
.Instantes, marcas de tiempo y duraciones
Cuando se trata del tiempo, tenemos que hacer una distinción entre al menos dos conceptos diferentes: instantes y duraciones . Un instante es un punto en el eje del tiempo. Una duración es la duración de un intervalo de tiempo, es decir, la distancia en el tiempo entre los instantes que definen el inicio y el final del intervalo. La distinción entre estos conceptos no siempre es muy clara en el lenguaje cotidiano. Por ejemplo, si digo " Volveré en cinco minutos ", entonces " cinco minutos " es la duración estimada de mi ausencia, mientras que " en cinco minutos " es el instante de mi regreso previsto. Es importante tener en cuenta la distinción, ya que es la forma más sencilla de evitar por completo el problema del vuelco.
El valor de retorno de
millis()
podría interpretarse como una duración: el tiempo transcurrido desde el inicio del programa hasta ahora. Esta interpretación, sin embargo, se rompe tan pronto como se desborda Millis. En general, es mucho más útil pensarmillis()
que devuelve una marca de tiempo , es decir, una "etiqueta" que identifica un instante en particular. Se podría argumentar que esta interpretación adolece de que estas etiquetas sean ambiguas, ya que se reutilizan cada 49,7 días. Sin embargo, esto rara vez es un problema: en la mayoría de las aplicaciones integradas, cualquier cosa que sucedió hace 49.7 días es una historia antigua que no nos importa. Por lo tanto, reciclar las etiquetas antiguas no debería ser un problema.No compare las marcas de tiempo
Intentar averiguar cuál de las dos marcas de tiempo es mayor que la otra no tiene sentido. Ejemplo:
Ingenuamente, uno esperaría que la condición de la
if ()
sea siempre cierta. Pero en realidad será falso si Millis se desborda durantedelay(3000)
. Pensar en t1 y t2 como etiquetas reciclables es la forma más sencilla de evitar el error: la etiqueta t1 se ha asignado claramente a un instante anterior a t2, pero en 49.7 días se reasignará a un instante futuro. Por lo tanto, t1 ocurre tanto antes como después de t2. Esto debería dejar en claro que la expresiónt2 > t1
no tiene sentido.Pero, si se trata de simples etiquetas, la pregunta obvia es: ¿cómo podemos hacer cálculos de tiempo útiles con ellas? La respuesta es: restringiéndonos a los únicos dos cálculos que tienen sentido para las marcas de tiempo:
later_timestamp - earlier_timestamp
produce una duración, es decir, la cantidad de tiempo transcurrido entre el instante anterior y el instante posterior. Esta es la operación aritmética más útil que implica marcas de tiempo.timestamp ± duration
produce una marca de tiempo que es un tiempo después (si usa +) o antes (si -) de la marca de tiempo inicial. No es tan útil como parece, ya que la marca de tiempo resultante se puede usar en solo dos tipos de cálculos ...Gracias a la aritmética modular, se garantiza que ambos funcionarán bien en el rollo de Millis, al menos siempre que los retrasos involucrados sean más cortos que 49.7 días.
Comparar duraciones está bien
Una duración es solo la cantidad de milisegundos transcurridos durante un intervalo de tiempo. Mientras no necesitemos manejar duraciones superiores a 49.7 días, cualquier operación que tenga sentido físicamente también debería tener sentido computacionalmente. Podemos, por ejemplo, multiplicar una duración por una frecuencia para obtener varios períodos. O podemos comparar dos duraciones para saber cuál es más largo. Por ejemplo, aquí hay dos implementaciones alternativas de
delay()
. Primero, el buggy:Y aquí está el correcto:
La mayoría de los programadores de C escribirían los bucles anteriores en forma terser, como
y
Aunque se ven engañosamente similares, la distinción de marca de tiempo / duración debe dejar en claro cuál tiene errores y cuál es la correcta.
¿Qué sucede si realmente necesito comparar marcas de tiempo?
Mejor trata de evitar la situación. Si es inevitable, todavía hay esperanza si se sabe que los instantes respectivos están lo suficientemente cerca: menos de 24.85 días. Sí, nuestro retraso manejable máximo de 49.7 días se redujo a la mitad.
La solución obvia es convertir nuestro problema de comparación de marca de tiempo en un problema de comparación de duración. Digamos que necesitamos saber si el instante t1 es antes o después de t2. Elegimos algún instante de referencia en su pasado común, y comparamos las duraciones de esta referencia hasta t1 y t2. El instante de referencia se obtiene restando una duración suficientemente larga de t1 o t2:
Esto se puede simplificar como:
Es tentador simplificar aún más
if (t1 - t2 < 0)
. Obviamente, esto no funciona porquet1 - t2
, al ser calculado como un número sin signo, no puede ser negativo. Esto, sin embargo, aunque no es portátil, funciona:La palabra clave
signed
anterior es redundante (una llanuralong
siempre está firmada), pero ayuda a aclarar la intención. La conversión a un largo firmado es equivalente a una configuraciónLONG_ENOUGH_DURATION
igual a 24.85 días. El truco no es portátil porque, de acuerdo con el estándar C, el resultado es la implementación definida . Pero dado que el compilador gcc promete hacer lo correcto , funciona de manera confiable en Arduino. Si deseamos evitar el comportamiento definido de implementación, la comparación firmada anterior es matemáticamente equivalente a esto:con el único problema de que la comparación se ve al revés. También es equivalente, siempre que los largos sean de 32 bits, a esta prueba de un solo bit:
Las tres últimas pruebas son compiladas por gcc en el mismo código de máquina.
¿Cómo pruebo mi boceto contra el rollo de Millis?
Si sigues los preceptos anteriores, deberías estar bien. Sin embargo, si desea probar, agregue esta función a su boceto:
y ahora puede viajar en el tiempo su programa llamando
setMillis(destination)
. Si quieres que pase por el desbordamiento de los milis una y otra vez, como Phil Connors reviviendo el Día de la Marmota, puedes poner esto dentroloop()
:La marca de tiempo negativa anterior (-3000) es implícitamente convertida por el compilador a un largo sin signo correspondiente a 3000 milisegundos antes del rollover (se convierte a 4294964296).
¿Qué sucede si realmente necesito rastrear duraciones muy largas?
Si necesita encender un relé y apagarlo tres meses después, entonces realmente necesita rastrear los desbordamientos del millis. Hay muchas maneras de hacerlo. La solución más sencilla puede ser simplemente extender
millis()
a 64 bits:Esto es esencialmente contar los eventos de reinversión y usar este recuento como los 32 bits más significativos de un recuento de milisegundos de 64 bits. Para que este conteo funcione correctamente, la función debe llamarse al menos una vez cada 49.7 días. Sin embargo, si solo se llama una vez cada 49.7 días, en algunos casos es posible que la verificación
(new_low32 < low32)
falle y el código no cuentehigh32
. Usar millis () para decidir cuándo hacer la única llamada a este código en una sola "envoltura" de millis (una ventana específica de 49.7 días) podría ser muy peligroso, dependiendo de cómo se alineen los plazos. Por seguridad, si usa millis () para determinar cuándo hacer las únicas llamadas a millis64 (), debe haber al menos dos llamadas en cada ventana de 49.7 días.Sin embargo, tenga en cuenta que la aritmética de 64 bits es costosa en el Arduino. Puede valer la pena reducir la resolución de tiempo para permanecer en 32 bits.
fuente
TL; DR Versión corta:
An
unsigned long
es de 0 a 4,294,967,295 (2 ^ 32 - 1).Entonces, digamos que
previousMillis
es 4,294,967,290 (5 ms antes del rollover), ycurrentMillis
es 10 (10ms después del rollover). EntoncescurrentMillis - previousMillis
es real 16 (no -4,294,967,280) ya que el resultado se calculará como un largo sin signo (que no puede ser negativo, por lo que se rodará). Puede verificar esto simplemente:Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16
Entonces el código anterior funcionará perfectamente bien. El truco consiste en calcular siempre la diferencia de tiempo y no comparar los dos valores de tiempo.
fuente
unsigned
lógica, por lo que no sirve de nada aquí!millis()
vuelcan dos veces, pero es muy poco probable que ocurra con el código en cuestión.previousMillis
tiene que haberse medido antescurrentMillis
, por lo que sicurrentMillis
es más pequeño que sepreviousMillis
produjo un vuelco. Lo matemático resulta que, a menos que se hayan producido dos vuelcos, ni siquiera necesita pensar en ello.t2-t1
, y si puede garantizar quet1
se mide antes,t2
entonces es equivalente a firmado(t2-t1)% 4,294,967,295
, de ahí el ajuste automático. ¡Agradable!. Pero, ¿quéinterval
pasa si hay dos vuelcos, o es> 4,294,967,295?Envuelva el
millis()
en una clase!Lógica:
millis()
directamente.Seguimiento de las reversiones:
millis()
. Esto lo ayudará a descubrir si semillis()
ha desbordado.Temporizador de créditos .
fuente
get_stamp()
51 veces. Comparar demoras en lugar de marcas de tiempo ciertamente será más eficiente.Me encantó esta pregunta y las excelentes respuestas que generó. Primero, un comentario rápido sobre una respuesta anterior (lo sé, lo sé, pero todavía no tengo el representante para comentar. :-).
La respuesta de Edgar Bonet fue asombrosa. Llevo 35 años codificando, y hoy aprendí algo nuevo. Gracias. Dicho esto, creo que el código para "¿Qué pasa si realmente necesito rastrear duraciones muy largas?" se rompe a menos que llame a millis64 () al menos una vez por período de reinversión. Realmente quisquilloso, y es poco probable que sea un problema en una implementación del mundo real, pero ahí lo tienes.
Ahora, si realmente quería marcas de tiempo que cubrieran cualquier rango de tiempo razonable (64 bits de milisegundos es aproximadamente medio billón de años según mi cálculo), parece simple extender la implementación de millis () existente a 64 bits.
Parece que estos cambios en attinycore / cableado.c (estoy trabajando con el ATTiny85) funcionan (supongo que el código para otros AVR es muy similar). Vea las líneas con los comentarios // BFB y la nueva función millis64 (). Claramente será más grande (98 bytes de código, 4 bytes de datos) y más lento, y como señaló Edgar, es casi seguro que puede lograr sus objetivos con una mejor comprensión de las matemáticas de enteros sin signo, pero fue un ejercicio interesante .
fuente
millis64()
solo funciona si se llama con más frecuencia que el período de reinversión. Edité mi respuesta para señalar esta limitación. Su versión no tiene este problema, pero tiene otro inconveniente: hace aritmética de 64 bits en el contexto de interrupción , lo que ocasionalmente aumenta la latencia para responder a otras interrupciones.