Mi propia función millis() no es precisa en el arduino

Yo mismo escribí un programa para implementar la función millis() y delay() sin la biblioteca arduino. Incluí una variable de conteo que cuenta cada segundo y envía su valor cada segundo a través del puerto serie. Lo que encontré es que su valor se aleja del valor real en casi 2 segundos cada 3 minutos. ¿Hay algo mal con mi código? ¿O es Serial.print() el culpable que puede retrasar esa rutina? ¿Cuánto tiempo tarda en ejecutarse Serial.print()?

Aquí está el código:

Editar: edité el código de esta manera, pero el conteo en el arduino aún se retrasa alrededor de 4 segundos después de 4 minutos. Tiene un retraso de 13 segundos después de 10 minutos, es decir, cuenta solo 587 segundos después de los 600 segundos reales.

Edición 2: Aquí está mi código actualizado. Todavía hay retraso en el tiempo. Tengo un retraso de alrededor de 6 segundos en 5 minutos.

#include "Arduino.h"
#include <avr/io.h>
#include <avr/interrupt.h>


void toggle_led(void);

unsigned long volatile millis_count = 0;
volatile char state = 1;
unsigned long volatile current_count = 0;
unsigned int volatile count = 0;

ISR(TIMER0_COMPA_vect) {        //Timer interrupt ISR

    millis_count++;
    if (millis_count - current_count == 1000) {
        current_count = millis_count;
        toggle_led();
        Serial.println(count++);
    }

}


int main(void) {

    init();

    TCCR0B = 0b11;  //Timer settings for interrupt at every millisecond
    OCR0A = 249;
    TIMSK0 |= 0b10;

    sei();
    Serial.begin(9600);
    DDRB |= 1<<5;

    for (;;) {



    }
}

void toggle_led(void) {

    PORTB ^= (1<<5);
}
No soy un experto en AVR (o programación integrada), pero me preguntaba si no podría hacer que su toggle_led() sea más eficiente evitando el estado, y simplemente haga: PORTB ^= 1<<5; Además, estuve leyendo recientemente en un documento de Atmel que es mejor evitar llamar a funciones desde dentro de un ISR, por lo que podría ser mejor hacer que toggle_led() sea una macro en su lugar. Después de todo, es muy pequeño, llamado en muy pocos lugares, por lo que es la receta perfecta.
Ah, una cosa más. Si tiene un analizador lógico, en algunos casos puede eliminar por completo el uso de la consola Serial para la depuración de programas, es decir, usar la alternancia de pines. La biblioteca en serie es bastante pesada tanto en términos de rendimiento en tiempo de ejecución como en el impacto en el tamaño del código (~ 550B). Esto es especialmente importante cuando lo hace dentro de ISR. Con una tasa de muestreo lenta y compresión RLE de muestras, puede usar el cambio de pines (para la depuración), de manera bastante efectiva, con un costo de rendimiento muy bajo (a diferencia de la biblioteca Serial).
Debe tener la menor cantidad de código posible dentro de una rutina de servicio de interrupción. Especialmente no rutinas pesadas como Serial.println().
El error fue que no habilité el modo CTC en la configuración del temporizador. Entonces, el contador del temporizador interno siempre se desbordó a 255. Agregar "TCCR0A = 0b10;" to the main() resolvió el problema. Gracias a todos por el apoyo.

Respuestas (4)

Su demora será la duración de la función newdelay() más el tiempo que lleva enviar los datos en serie.

Tienes:

  1. Enviar el conteo a través de serie
  2. espera 1000ms
  3. alternar LED.

Cada uno de esos pasos lleva tiempo.

Para obtener un tiempo exacto de 1000 ms, puede activar el envío en serie desde una interrupción de 1 segundo, o puede examinar el conteo de milisegundos dentro de su bucle y enviar los datos en serie cuando los milisegundos pasan por 1000:

unsigned long lastmil;

lastmil == newmillis();
for(;;)
{
    if(((newmillis()%1000) == 0) && (lastmil != newmillis()))
    {
        lastmil = newmillis();
        Serial.println(count++);
        toggle_led();
    }
}

(Otra forma de evitar que se reproduzca varias veces durante un solo milisegundo sería agregar un breve retraso dentro del if (...) para garantizar que la rutina tarde al menos 1 ms)

La función Serial.println() tomará una cantidad variable de tiempo dependiendo de:

  1. La tasa de baudios en uso
  2. El número de caracteres enviados

A 9600 baudios estás enviando 9600 símbolos por segundo. Con formato "8N1" que son 10 símbolos por byte. Entonces, para una cadena de 3 dígitos, más el retorno de carro y el avance de línea, serán 10 símbolos × 5 caracteres, que son 50 símbolos.

9600 baudios son 0,0001041670,000104167 s por símbolo (o 104 µS por símbolo), por lo que 50 símbolos serán 0,00520835 s (o 5,20835 ms).

Ese es el tiempo de transmisión real. Luego, también debe agregarle el tiempo necesario para formatear los datos y llamar a las rutinas en serie. Todos estos toman una cantidad de tiempo no especificada. Para averiguarlo, necesitará conocer el código ensamblador en el que se compila la rutina, luego encontrar el número de ciclos de reloj que toma cada instrucción y sumarlos.

¿Puede decirme cuánto tiempo consume Serial.print ()?
Vea mi edición para más detalles.
8n1 toma diez tiempos de bit, no nueve: un bit de inicio, 8 datos y un bit de parada.
Por supuesto que sí, estaba olvidando el bit de parada
@majenko, el código que proporcionó no funcionará porque la condición if será verdadera para varios ciclos de bucle 'for' en un segundo.
Verdadero. Es solo algo que resolví rápidamente. Deberá mantener un registro de la última vez que ejecutó la parte serial. Déjame modificarlo rápidamente...

Majenko ha explicado en detalle por qué estás viendo la deriva del tiempo. La pregunta ahora debería ser cómo hacerlo bien. Ya se ha encontrado con una de las varias razones por las que las esperas ocupadas son una mala manera de medir el tiempo a largo plazo.

No conozco el hardware AVR en el que se basa el arduino, pero estoy seguro de que tiene temporizadores. Debe haber una manera de configurar una interrupción periódica completamente en hardware utilizando uno de estos temporizadores. Una interrupción de 1 kHz (período de 1 ms) suele ser una buena compensación entre la resolución de tiempo, no usar demasiado la CPU y un valor lo suficientemente pequeño que el hardware puede hacer de forma nativa. El hardware llamará regularmente a esta rutina de interrupción 1000 veces por segundo, independientemente de cualquier otra cosa que esté haciendo su rutina principal. Puede contar fácilmente múltiples tics de 1 ms para hacer tics con períodos más largos, como 1 segundo. Dado que 1 segundo es bastante lento, la rutina de interrupción puede establecer una bandera cada segundo que la rutina de primer plano verifica en su ciclo de eventos principal y luego se reinicia.

Dependiendo de qué más esté sucediendo, es posible que no use una rutina de interrupción, sino que el hardware solo establezca una bandera cada tic del reloj. En ese caso, probablemente desee hacer que el reloj marque más tiempo, como cada 10 ms. El código de primer plano maneja eso en el bucle de eventos principal y lo divide en el tic de 1 que realmente desea.

Continuando con la respuesta de Olin... así es como se implementa la función Arduino millis()... Hay una variable de contador global volátil de 32 bits llamada timer0_millis que es mantenida/actualizada por TIMER0_OVF_vect ISR, y la función millis en sí misma detiene las interrupciones brevemente para leer ese valor en una variable local y devolverlo. Puede revisar la implementación en el archivo "wiring.c" en la biblioteca central de Arduino para ver cómo lo hicieron realmente.

Además de las respuestas mencionadas, existe otra posibilidad de deriva temporal. Los cristales utilizados para cronometrar el arduino también tienen un rango de tolerancia bastante amplio, por lo que el cristal puede inducir una deriva. Si planea usar esto durante un largo período de tiempo y debe ser preciso en sus mediciones de tiempo, buscaría un chip de reloj externo, por ejemplo, el DS1307, que es fácil de conectar al arduino. Estos chips generalmente usan un cristal de 32khz de "alta precisión" para mantener el tiempo (el mismo cristal que se usa en los relojes de pared comunes).