¿Cual es la probabilidad de recibir un evento justo cuando millis() se desborda?

Hola a todos. De tanto responder a preguntas del foro, acabo teniendo dudas de si el código que realizo es correcto.

Por ejemplo, acabo de responder a un chaval que cuando recibe un caracter determinado por el puerto serie, se encienda un led durante determinado tiempo. La verdad es que el chaval ha puesto un código sin usar delays que no le funciona del todo, bravo por él, por no usar delays.

Mi consejo ha sido usar una variable de control para indicar que debe contar o que se ha iniciado el proceso. El código ha sido este:

/*
 * Este código espera a recibir un caracter 'a' por el puerto serie. Cuando este
 * se recibe enciende el led de la placa del Arduino durante 5 segundos.
 */
unsigned long tinicio;  // En esta variable indicamos el tiempo cuando se inicia.
bool          iniciado; // En esta otra indicamos que se inicia el proceso.
void setup() {
  Serial.begin(9600);
  pinMode(13,OUTPUT);
  iniciado=false;
}

void loop() {
  // Comprobamos si ha sido iniciado.
  if ( iniciado ) {
    // Si lo ha sido comprobamos que el tiempo transcurrido es mayor del tiempo
    // que queremos.
    if ( millis() - tinicio > 5000 ) {
      iniciado=false; // El tiempo ha transcurrido con lo que iniciado es falso
      digitalWrite(13, LOW); // Apagamos el led.
    }
  }

  // Comprobamos si hay datos en el puerto serie.
  if ( Serial.available() ) {
    // Leemos un caracter.
    char c = Serial.read();
    // Si el caracter es 'a' iniciamos el contador tinicio e indicamos que ya
    // se ha iniciado. Además encendemos el led.
    if ( c=='a' ) {
      tinicio=millis();
      iniciado=true;
      digitalWrite(13,HIGH);
    }
  }
}

El código por supuesto funciona. Pero no es lo que yo haría. Yo utilizaria la misma variable tinicio como variable de control. Así cuando la variable tinicio vale 0 no cuento, pero si es distinto cuento.

Así que sin usar otra variable queda:

/*
 * Este código espera a recibir un caracter 'a' por el puerto serie. Cuando este
 * se recibe enciende el led de la placa del Arduino durante 5 segundos.
 * 
 * No se utiliza variable de control!!!
 */

unsigned long tinicio;  // En esta variable indicamos el tiempo cuando se inicia.
                        // Si la variable vale 0, el proceso no cuenta.
void setup() {
  Serial.begin(9600); 
  pinMode(13,OUTPUT);
  tinicio=0; // Iniciamos el tiempo de inicio a 0.
}

void loop() {
  // Si el tiempo de inicio es distinto de 0 debemos comprobar que haya 
  // transcurrido el tiempo.
  if ( tinicio!=0 ) {
    if ( millis() - tinicio > 5000 ) {
      digitalWrite(13, LOW); // Apagamos el led.
      tinicio=0; // Debemos poner el tiempo a 0, para que no vuelva a contar.
    }
  }
  if ( Serial.available() ) {
    char c = Serial.read();
    if ( c=='a' ) {
      tinicio=millis(); // Indicamos que queremos iniciar.
      digitalWrite(13,HIGH);
    }
  }
}

Que efectivamente funciona. Salvo por un detalle: si recibo el caracter justo cuando millis se desborda (es decir, millis() devuelve 0) NO se ejecuta.

Ahora bien, y aquí es donde me doy cuenta de que no tengo ni idea de estadistica/probabilidad, ¿Cual es la probabilidad de recibir un evento justo cuando millis() se desborda?

PD. Lo dejo en la sección de humor y debate, ya que aunque se relaciona con software es más una duda de caracter matemático que de programación en si... el código funciona... solo que quiero saber la probabilidad de que falle. Si el moderador cree o considera oportuno que deba ir en software, lo dejo a su elección.

Una entre 4294967296.

Para mi era para software pero por la curiosidad y sabiendo que solo opinaremos nosotros que se quede aquí, total no muchos van a opinar.

Que opinas de esto?

if ( Serial.available() ) {
   char c = Serial.read();
   if ( c=='a' ) {
      unsigned long tmp = millis();
      if (tmp > 0)
         tinicio=tmp; // Indicamos que queremos iniciar.
      else 
         tinicio = 1UL;  // se ejecturá con la probabilidad indicada x IA (Ignorante Absoluto).        
     digitalWrite(13,HIGH);
   }
 }

si lo moví 1 mseg pero afecta mucho empezar corrido 1 mseg? Yo creo que no y salvo el problema.

o sea 1 vez cada 136 años 2 meses 8 dias 5 horas, etc etc

Eso fue lo primero que pense: 1 entre 2^32 (tamaño del unsigned long) es decir es casi imposible que eso ocurra, aunque no imposible del todo.

Pero mi mente siempre va mas allá y busco donde por donde puede fallar (y asi me pasa, que cuando tengo un fallo no lo encuentro…) y pensé que para eventos asincronos o aleatorios esa es la probabilidad, pero ¿qué pasa cuando el evento es sincrono?

Imaginad que el caracter ‘a’ lo envia otro arduino cada X milisegundos, de manera periodica. Entonces ya la probabilidad deja de ser 1/2^32,

Para facilitar la compresión de lo que digo, vamos a imaginar que millis() solo cuenta 10 y que cuando llega a 9 se desborda a 0; entonces:

Para X=3

millis(): 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
              X     X     X     X     X     X     X <--- Aqui ocurre.


Para X=2

millis(): 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
              X   X   X   X   X <--- Aquí ocurre.

Aquí si depende del valor de X con lo que la probabilidad de que ocurra es mas alta.

Aunque después de analizar el código de Surbyte, creo que se evita el problema del todo. No se me había ocurrido esa solución, muy bien pensado!.

Tengo mi estadística muy oxidada, pero lo que recuerdo es que hay que ser muy especifico con lo que se pregunta y supone.
por ejemplo, la probabilidad de mandar una "a" ,de manera aleatoria, y que coincida con el desborde es 1/2^32 (probabilidad individual del suceso)
La probabilidad al mandar una serie N de "a" ,de manera aleatoria, y que coincida con el desborde es N/2^32
(probabilidad del suceso)
Ahora si al mandar una "a" no es aleatorio, sino una secuencia discreta, los cálculos cambian.
¿cuando se desborda millis() se reinicia el programa, como cuando se desborda una variable ?

Pd me contesto yo mismo; según la documentación vuelve a cero.

¿cuando se desborda millis() se reinicia el programa, como cuando se desborda una variable ?

Peter me dejas perplejo. Si una variable se desborda no reinicia el programa, simplemente cambia a cero. .Da igual el tipo que sea.

uint16_t i;

void setup() {
  Serial.begin(9600);
  Serial.println("start...");
  i = 1; // Lo pongo a 1 para que lo primero que salga no sea "Me desborde...".
}

void loop() {
  i++;
  if ( i==0xFFFF ) { Serial.println(F("Me voy a desbordar")); delay(1000); }
  if ( i==0x0000 ) { Serial.println(F("Me desborde...")); delay(1000); }
}

Mira ese ejemplo. Jamás se ejecutará el println(“start”) salvo que resetees el Arduino y la variable i se desborda constantemente.

Utilizo esta técnica de usar la misma variable de conteo como control en mis automatas, generalmente en debounce de las entradas y timers y no he tenido nunca un problema de reinicio indeseado. Y creeme, alguno lleva años funcionando las 24 horas del día, los 365 dias del año. El único problema que he tenido con ellos y me ha costado resolver ha sido el tema del ModBus, pero creeme, nada de software.

Prueba declarar i como int

Declarada como int, salida:

start...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...
Me voy a desbordar
Me desborde...

No el cambiado lo del desborde que ahora ya no asi, pero la variable sigue contando y no se reinicia.

Me han asaltado las dudas (ves, me liais...) y he comprobado lo que ocurre.

Un entero con signo de 16 bits va desde -32768 a 32767. Así que ocurre cuando llegamos a 32767 y seguimos contando, se desborda, pero no va a 0, si no a -32768.

He modificado el código que lo veas:

int i;

void setup() {
  Serial.begin(9600);
  Serial.println("start...");
  i = 32000;
}

void loop() {
  i++;
  Serial.println(i);
  

}

Jaja me acusan muy seguido de liar a la gente, seguro que alguna vez genere un error y lo achaque equivocadamente a un desborde.

y que tal con mi arreglo?

La probabilidad al mandar una serie N de "a" ,de manera aleatoria, y que coincida con el desborde es N/2^32
(probabilidad del suceso)

Esto no es estrictamente cierto en este caso.
Supongamos que tonamos un espacio muestral igual a 2^32 segundos y enviamos aleatoriamente una serie de "a".
la primera vez que enviemos una "a" la probabilidad de éxito (que coincida con el desborde ) es igual 1/2^32, pero la probabilidad de segunda "a" sera 2/(2^32-1) ya que no puede coincidir con el mismo segundo que enviamos la primera "a". En estadística esto se llama "sin reposición ".
Si la cantidad de "a" envidas es baja prácticamente no hay diferencia con N/2^32.

y que tal con mi arreglo?

Perfecto. Ya lo comenté :slight_smile:

En estadística esto se llama "sin reposición ".

Ves. No tengo ni idea de estadistica. :slight_smile:

(Parte 1 de 2)

@surbyte, lamento decirte que tu solución también puede fallar.

Personalmente prefiero no ahorrar en variables y utilizar la solución de usar una “variable de control”. Esto hace más fiable el programa al ser más claro y más mantenible. Pero, si no se quiere usar la otra variable y no importa mucho si el tiempo de espera es un milisegundo más o un milisegundo menos, se puede hacer "el apaño" que propone @surbyte y asegurarte que tinicio no valga cero cuando millis() devuelve cero. Pero para asegurarnos de que no falle sería mejor asignar -1 a tinicio cuando se dé esa situación. Sí, menos uno:

  ...

  if ( Serial.available() ) {
    char c = Serial.read();
    if ( c=='a' ) {
      tinicio=millis(); // Indicamos que queremos iniciar.
      if (tinicio == 0) tinicio = -1; // tinicio tendrá un valor distinto de cero y el valor que tiene millis() antes de desbordarse y pasar a valer cero
      digitalWrite(13,HIGH);
    }
  }

  ...

Yo, como @surbyte, pensé primeramente en asignarle 1 a tinicio cuando millis() fuera cero. Pero me di cuenta de que esto podría hacer que no esperase ni un milisegundo... ¿Por qué? Pues porque estamos tratando con enteros sin signo.

Si se pone a uno tinicio y en la siguiente iteración de loop() resulta que millis() vuelve a dar como valor el cero, nos encontramos con que en la comparación:

    if ( millis() - tinicio > 5000 ) {

La expresión que se evalúa es algo así como…

    if ( 0 - 1 > 5000 ) {

Podríamos pensar que no se cumple la condición… Pero en este caso sí se cumple. Les recuerdo que estamos tratando con enteros largos sin signo y el resultado de de la resta 0 - 1 no puede ser un número negativo (-1) ya que estamos tratando con enteros sin signo (se supone que no hay números negativos). Se produce un “desbordamiento por debajo” que da como resultado el número positivo más grande que puede almacenar… ese es justo el valor que tenía millis() hace un milisegundo, justo antes de desbordarse y pasar a valer cero. ¿No me creen? Ejecuten el siguiente código en su Arduino y vean qué muestra el monitor serie.

// Este programa muestra "(cero - uno > 5000) es verdadero"
void setup() {
    Serial.begin(9600);
    unsigned long uno = 1;
    unsigned long cero = 0;
    if (cero - uno > 5000) {
        Serial.println(F("(cero - uno > 5000) es verdadero"));
    }
    else {
        Serial.println(F("(cero - uno > 5000) es falso"));
    }
}

void loop() {
    // Vacío adrede
}

Sí que ocurre lo esperado si ambas variables son con signo. Prueben a quitar el unsigned (sin signo) de la declaración de las dos variables. Repito: quitar de la declaración de las dos variables.

Ojo, desde que una de las variables sea sin signo, las operaciones “interpretarán” los enteros con signo como enteros sin signo. Pueden probar, tras quitar los dos unsigned, comparar con 5000UL en lugar de con 5000 y verán que vuelve a dar un resultado “verdadero”. El sufijo UL le indica al compilador que ha de tratar el 5000 como un unsigned long. Y desde que uno de los operadores es sin signo, la operación se evalúa como si todos fueran sin signo.

Cuando hablamos de un entero con signo o sin signo, estamos hablando básicamente de cómo vamos a “interpretar” lo que significa que el bit más significativo esté a uno. Si hablamos de “sin signo”, el valor que “interpretamos” es el que sería de esperar de un número binario positivo. Mientras que si estamos hablando de “con signo”, el valor que “interpretamos” es que es negativo en complemento a dos. Si no saben de qué estoy hablando buscar “bit más significativo” y “complemento a dos” en Google.

Supongamos que tenemos una variable entera sin signo de cuatro bits. Entonces sus 16 posibles valores serían “interpretadas” así (binario y valor “interpretado” en decimal):

0000  0
0001  1
0010  2
0011  3
0100  4
0101  5
0110  6
0111  7
1000  8
1001  9
1010  10
1011  11
1100  12
1101  13
1110  14
1111  15

Mientras que si esa misma variable es con signo entonces sus valores los “interpretaremos” así (ordenado por el valor que “interpretamos”):

1000  -8
1001  -7
1010  -6
1011  -5
1100  -4
1101  -3
1110  -2
1111  -1
0000  0
0001  1
0010  2
0011  3
0100  4
0101  5
0110  6
0111  7

En la segunda lista, los que tienen el bit más significativo a uno son negativos y los que los tienen a cero son positivos. En ambas listas todos los valores que “interpretamos” son consecutivos y en ambas listas si al último valor lo incrementamos en uno el nuevo valor es el primero de la lista. En esto consiste el famoso “desbordamiento”. Asimismo, si al primer valor de cada lista lo decrementamos en uno, obtenemos el último valor de la lista.

Recordemos que, por ejemplo, sumar 3 a 4 es incrementar en uno tres veces el número 4. Y, como es obvio, restar 3 a 4 es decrementar en uno tres veces el 4. Efectivamente, si buscamos el 4 (0100) en alguna de las dos listas y le sumamos 3 (0011) avanzamos tres posiciones en la lista y nos encontramos con el 7 (0111). Y si al 4 (0100) le restamos 3 (0011) retrocedemos tres posiciones en la lista y nos encontramos con el 1 (0001).

¿Pero qué pasa si a 3 (0011) le restamos 4 (0100)? Fácil: localicemos el 3 (0011) en ambas listas y “retrocedamos” cuatro elementos. ¡Cáspita! En la primera lista si retrocedo cuatro “me salgo fuera”. Bueno, pues si hemos de posicionarnos antes del primer elemento, nos situamos en el último. Es como un cuentakilómetros de cuatro dígitos que, si lo hacemos retroceder: 0003; 0002; 0001; 0000; 9999; 9998, cuando hemos llegado al cero pasamos al 9999. Se nos “desborda”. Así que en ambos casos, al restarle 4 al 3, nos encontramos en ambas listas que “llegamos” al 1111. En la segunda lista el 1111 lo “interpretamos” como el -1, lo que era de esperar. Pero en la segunda lista lo interpretamos como un 15. Algo raro, ¿no?

Eso de restar 4 al 3 y que nos de 15 parece algo contra natura, pero es algo que nos puede resultar familiar. Pensemos en el reloj de agujas del abuelo. Si el reloj marca las cuatro y lo retrocedemos tres horas se nos quedará marcando la una… pero si el reloj marca las tres y lo retrocedemos cuatro horas nos marcará las once y no las menos una. ¿Ven que nos resulta familiar? Con un reloj de agujas siempre “interpretamos” horas positivas, nunca negativas. Pues con los enteros sin signo hacemos igual. Así que lo que con los enteros sin signo “interpretamos” como menos uno, en un entero sin signo es el mayor valor que podemos “interpretar”.

Compilar y ejecutar el siguiente programa para ver qué muestra:

void setup() {
    unsigned long x = -1;
    Serial.begin(9600);
    Serial.println(x);
}

void loop() {
    // Vacío adrede
}

¿Alguien esperaba que mostrase un -1?

(Parte 2 de 2)

Volvamos al “problema principal”. Para simplificar un poco, en lugar de 5000 milisegundos, vamos a suponer que son 5 milisegundos. Y en lugar de con variables enteras de 32 bits vamos a simular que trabajamos con variables enteras de 4 bits. El comportamiento de los cálculos y comparaciones básicamente es el mismo, pero nos será más fácil verlo en las listas que he puesto.

Recordemos que el problema del que hablo se da cuando millis() resulta ser cero a la hora de asignar la variable tinicio, forzamos en este caso que tinicio valga uno, y antes de que millis() deje de ser cero se hace la comparación:

    if ( millis() - tinicio > 5 ) { // He cambiado el 5000 por un 5 para “simplificar” el ejemplo

La expresión que se evalúa, en decimal, sería:

    if ( 0 - 1 > 5 ) {

Y si lo ponemos con nuestra codificación de cuatro bits (mirar cualquiera de las dos listas) quedaría una cosa tal que así:

    if ( 0000 - 0001 > 0101 ) {

Para restar 1 (0001) a 0 (0000) hemos de localizar el 0 (0000) en las listas y “retroceder” un elemento. En la primera lista nos encontramos que no hay elemento anterior, así que nos “iremos” al último. En ambos casos nos da como resultado el 1111.

La comparación en el “formato de cuatro bits”, una vez realizada la resta, quedaría:

    if ( 1111 > 0101 ) {

Aquí está el quid de la cuestión. Según qué lista usemos para interpretar el 1111 la condición será verdadera o falsa. Si utilizamos la primera lista, la de sin signo, la comparación se “interpreta” como ¿15 es mayor que 5? Siendo “verdadero”. Mientras que si utilizamos la segunda lista, la de con signo, la comparación se “interpreta” como ¿-1 es mayor que 5? Siendo “falso”. Esta es la pequeña gran diferencia que hay entre usar números enteros con signo o sin signo y el problema de la “solución” de asignar un uno a tinicio.

¿Y si volvemos al ejemplo del reloj de agujas? Al asignar un uno a tinicio cuando millis() es cero, es como si dijéramos que: como son la doce en punto “anotamos” que comenzamos a esperar a las doce y un minuto. Supongamos que vamos a esperar cinco minutos. OK, esperemos entonces cinco minutos. ¿Qué hora es? Son las doce. ¿A qué hora se empezó a esperar? A las doce y un minuto. ¿Llevamos menos un minuto esperando? No, imposible, no se puede esperar menos un minuto. Entonces hemos de llevar esperando, como mínimo, once horas y 59 minutos (tengamos en cuenta que no sabemos si ahora son las doce AM o PM. Ni sabemos de qué día. Así como tampoco sabemos si estamos esperando desde las doce y un minuto AM o PM, ni desde qué día estamos esperando). Así que si me dicen que son las doce en punto y que llevamos esperando desde las doce y un minuto, sin duda alguna ya han pasado los cinco minutos de espera, por lo que ya no esperamos más. ¿Realmente hemos esperado cinco minutos “más o menos”? No, no hemos esperado prácticamente nada.

Mi propuesta, la de indicar que se ha comenzado a esperar “antes de ahora”, soluciona ese problema (el de indicar que se ha comenzado a esperar “después de ahora”, en el futuro). Al asignar el -1, en realidad le estoy asignando el máximo valor que puede tener la variable entera con signo.

Volviendo al ejemplo anterior: la expresión que se evalúa, en decimal, sería:

    if ( 0 - (-1) > 5 ) {

Al sustituir los valores con nuestra codificación de cuatro bits quedaría una cosa tal que así:

    if ( 0000 - 1111 > 0101 ) {

Aviso que en enteros de cuatro bits -1 es 1111. Independientemente de si es con signo o sin signo. Digamos que si a una variable entera sin signo se le asigna un valor negativo, lo que se hace es el asignarle el valor binario que tendría si la variable fuera con signo. Por eso, para “ver” qué valor le asigna tendremos que mirar la lista de los enteros de cuatro bit con signo, y el valor que “interpretamos” al leerlo es el de la lista de los sin signo. Ejemplo: si asignamos un -4, el valor en binario obtenido en la lista de los con signo (segunda lista) es el 1100 y ese es el valor que se guarda. Que al “leerlo” lo “interpretamos” como un 12 (en este caso porque estamos trabajando con enteros sin signo de cuatro bits. Si fueran de ocho bits, el valor “interpretado” sería otro).

Así que el -1 se convierte, en este caso, en un 15. Podemos ver en la lista de cuatro bits sin signo que el 1111 lo “interpretamos” como un 15. Si a un entero de cuatro bits sin signo que valga cero le restamos quince, obtenemos un uno. Para “verlo” más gráficamente, localicemos el 0000 en la lista de los enteros de cuatro bits sin signo (es el primer elemento de la lista) y “retrocedamos” quince elementos. Nada más empezar a “retroceder” nos encontramos que antes del elemento 0000 no hay ninguno, con lo que hemos de “pasar” al último elemento (el 1111). Retrocedemos las catorce veces restantes y “vamos a parar” al 0001. Después de todo, es lógico que (0 - (-1)) nos de 1.

Así que la comparación en el “formato de cuatro bits” nos quedaría:

    if ( 0001 > 0101 ) {

Efectivamente: la condición esta vez no es cierta ya que 1 no es mayor que 5. Está comprobando que ha pasado un milisegundo y que ese tiempo no es superior a los cinco milisegundos que queremos que espere (recuerden que hemos sustituído los 5000 milisegundos por 5 para el ejemplo).

Volviendo al ejemplo del reloj del abuelo. Cuando se dé el caso a las doce en punto: “anotamos” que ha sido un minuto antes, a las once y cincuenta y nueve. ¿Qué hora es ahora? Las doce. Entonces lleva un minuto esperando. ¿Han pasado los cinco minutos de espera? No. Pues que siga esperando. Cierto es que como mínimo puede llegar a esperar realmente cuatro minutos, en lugar de cinco, porque “anotamos” que ha empezado un minuto antes de lo que en realidad lo hizo. Pero es mejor esperar un minuto menos que no esperar casi nada.

Dudo que les quede claro qué es lo que he tratado de explicar. Pero tal vez a alguien le haya servido de algo.

P.D.: Llevo mucho tiempo pensando crear un post sobre el uso de millis(). Para tratar de explicar cómo funciona; cómo se ha de trabajar con millis() y, lo más importante y complicado, porqué. Pero lo complicado y extenso que puede llegar a ser, así como la falta de tiempo, no me ha permitido hacerlo. He aprovechado este post para explicar una parte de ello: la diferencia de los enteros con signo y sin signo.

Interesante aclaración, a ninguno se nos ocurrió.

Pero claro, estamos en la situación que nuestro loop se ejecute en menos de 1 ms para que en la siguiente iteración millis() valga todavia 0, cosa que no medimos casi nunca.

Por ejemplo, en este código mido lo que tarda un simple Serial.print con una cadena:

uint32_t start;
uint32_t stop;
uint32_t m;
void setup() {
  Serial.begin(9600);
  start = micros();
  Serial.println(F("Imprimo una cadena bastante larga y veo lo que tarda."));
  stop = micros()-start;
  Serial.println(stop);

}

void loop() {

}

Y el resultado es 344 microsegundos.

Así que si añadimos algunas instrucciones mas brincaremos seguramente del milisegundo, con lo que en la siguiente iteración millis ya valdrá 1.

En uno de los automatas se me ocurrió medir el tiempo de ciclo: leer entradas, ejecutar código, escribir salidas y la velocidad de ejecución era de 8-10 ms (dependia del modbus).

Aún así efectivamente, habría que tenerlo en cuenta.