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 devuelto
millis()
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 igualmente micros()
, excepto por el hecho de que se micros()
transfiere cada 71.6 minutos, y la setMillis()
función proporcionada a continuación no afecta micros()
.
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 pensar millis()
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:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Ingenuamente, uno esperaría que la condición de la if ()
sea siempre cierta. Pero en realidad será falso si Millis se desborda durante
delay(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ón t2 > 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:
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;
}
}
Y aquí está el correcto:
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;
}
}
La mayoría de los programadores de C escribirían los bucles anteriores en forma terser, como
while (millis() < start + ms) ; // BUGGY version
y
while (millis() - start < ms) ; // CORRECT version
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:
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
Esto se puede simplificar como:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
Es tentador simplificar aún más if (t1 - t2 < 0)
. Obviamente, esto no funciona porque t1 - t2
, al ser calculado como un número sin signo, no puede ser negativo. Esto, sin embargo, aunque no es portátil, funciona:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
La palabra clave signed
anterior es redundante (una llanura long
siempre está firmada), pero ayuda a aclarar la intención. La conversión a un largo firmado es equivalente a una configuración LONG_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:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
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:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
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:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
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 dentro loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
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:
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;
}
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 cuente high32
. 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.