Meine eigene Funktion millis () ist im Arduino nicht genau

Ich habe selbst ein Programm geschrieben, um die Funktionen millis () und delay () ohne die Arduino-Bibliothek zu implementieren. Ich habe eine Zählvariable eingefügt, die jede Sekunde zählt und ihren Wert jede Sekunde über die serielle Schnittstelle sendet. Was ich fand, ist, dass sein Wert alle 3 Minuten um fast 2 Sekunden vom wahren Wert abweicht. Stimmt etwas mit meinem Code nicht? Oder ist das Serial.print() der Schuldige, der diese Routine verzögern kann? Wie lange dauert die Ausführung von Serial.print()?

Hier ist der Code:

Bearbeiten: Ich habe den Code so bearbeitet, aber die Zählung auf dem Arduino verzögert sich nach 4 Minuten immer noch um 4 Sekunden. Es verzögert sich nach 10 Minuten um 13 Sekunden, dh es zählt nur 587 Sekunden nach tatsächlichen 600 Sekunden.

Bearbeiten 2: Hier ist mein aktualisierter Code. Dennoch gibt es zeitliche Verzögerungen. Ich bekomme eine Verzögerung von ungefähr 6 Sekunden in 5 Minuten.

#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);
}
Bin kein Experte für AVR (oder eingebettete Programmierung), habe mich aber gefragt, ob Sie Ihr toggle_led() nicht effizienter machen könnten, indem Sie den Status vermeiden und einfach Folgendes tun: PORTB ^= 1<<5; Außerdem habe ich kürzlich in einem Atmel-Dokument gelesen, dass es am besten ist, das Aufrufen von Funktionen innerhalb einer ISR zu vermeiden, also könnten wir besser sein, stattdessen toggle_led() zu einem Makro zu machen. Immerhin ist es sehr klein, wird an sehr wenigen Stellen genannt, also das perfekte Rezept.
Ach noch was. Wenn Sie einen Logikanalysator haben, können Sie in einigen Fällen vollständig auf die Verwendung der seriellen Konsole zum Debuggen von Programmen verzichten, dh auf das Umschalten der Pins. Die serielle Bibliothek ist sowohl in Bezug auf die Laufzeitleistung als auch auf die Codegröße (~ 550 B) ziemlich schwer. Dies ist besonders wichtig, wenn Sie dies innerhalb von ISR tun. Mit einer langsamen Abtastrate und RLE-Komprimierung von Samples könnten Sie das Pin-Toggling (zum Debuggen) recht effektiv und mit sehr geringen Leistungskosten (im Gegensatz zur seriellen Bibliothek) verwenden.
Sie sollten so wenig Code wie möglich in einer Interrupt-Service-Routine haben. Vor allem keine schweren Routinen wie Serial.println().
Der Fehler war, dass ich den CTC-Modus in den Timer-Einstellungen nicht aktiviert habe. Der interne Timer-Zähler ist also immer auf 255 übergelaufen. Hinzufügen von "TCCR0A = 0b10;" to the main() hat das Problem gelöst. Danke an alle für die Unterstützung.

Antworten (4)

Ihre Verzögerung ist die Länge der Funktion newdelay() plus die Zeit, die zum Senden der seriellen Daten benötigt wird.

Du hast:

  1. Senden Sie die Zählung seriell
  2. 1000ms warten
  3. LED umschalten.

Jeder dieser Schritte braucht Zeit.

Um eine genaue Zeit von 1000 ms zu erhalten, können Sie entweder das serielle Senden von einem 1-Sekunden-Interrupt auslösen oder die Millisekundenzahl in Ihrer Schleife untersuchen und die seriellen Daten senden, wenn die Millisekunden 1000 durchlaufen:

unsigned long lastmil;

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

(Eine andere Möglichkeit, die mehrfache Schleife für eine einzelne Millisekunde zu stoppen, wäre, eine kurze Verzögerung in das if (..) einzufügen, um sicherzustellen, dass die Routine mindestens 1 ms dauert.)

Die Funktion Serial.println() benötigt je nach Folgendem eine unterschiedliche Zeitdauer:

  1. Die verwendete Baudrate
  2. Die Anzahl der gesendeten Zeichen

Bei 9600 Baud senden Sie 9600 Symbole pro Sekunde. Beim "8N1"-Format sind das 10 Symbole pro Byte. Für eine Zeichenfolge mit 3 Ziffern plus Wagenrücklauf und Zeilenvorschub sind das also 10 Symbole × 5 Zeichen, also 50 Symbole.

9600 Baud sind 0,0001041670,000104167 s pro Symbol (oder 104 µS pro Symbol), also sind 50 Symbole 0,00520835 s (oder 5,20835 ms).

Das ist die tatsächliche Sendezeit. Hinzu kommt dann noch der Zeitaufwand für die eigentliche Formatierung der Daten und den Aufruf der seriellen Routinen. All dies dauert eine unbestimmte Zeit. Um dies herauszufinden, müssen Sie den Assembler-Code kennen, in den die Routine kompiliert wird, und dann die Anzahl der Taktzyklen ermitteln, die jeder Befehl benötigt, und sie zusammenzählen.

Können Sie mir sagen, wie viel Zeit Serial.print() verbraucht?
Siehe meine Bearbeitung für Details.
8n1 benötigt zehn Bitzeiten, nicht neun: ein Startbit, 8 Daten und ein Stoppbit.
Natürlich tut es das - ich habe das Stoppbit vergessen
@majenko der von Ihnen bereitgestellte Code funktioniert nicht, da die if-Bedingung für mehrere 'for'-Schleifenzyklen innerhalb einer Sekunde wahr ist.
WAHR. Es ist nur etwas, das ich schnell rausgehauen habe. Sie müssen eine Aufzeichnung darüber führen, wann Sie den seriellen Teil das letzte Mal ausgeführt haben. Lass mich nur schnell modifizieren ...

Majenko hat ausführlich erklärt, warum Sie Zeitdrift sehen. Die Frage sollte jetzt sein, wie man es richtig macht. Sie sind bereits auf einen der vielen Gründe gestoßen, warum geschäftige Wartezeiten eine schlechte Methode für langfristiges Timing sind.

Ich kenne die AVR-Hardware nicht, auf der das Arduino basiert, aber ich bin sicher, dass es Timer hat. Es muss eine Möglichkeit geben, einen periodischen Interrupt vollständig in der Hardware einzurichten, indem einer dieser Timer verwendet wird. Ein Interrupt von 1 kHz (1 ms Periode) ist oft ein guter Kompromiss zwischen der zeitlichen Auflösung, nicht zu viel CPU-Auslastung und einem ausreichend kleinen Wert, den die Hardware nativ ausführen kann. Diese Interrupt-Routine wird regelmäßig 1000 Mal pro Sekunde von der Hardware aufgerufen, unabhängig davon, was Ihre Vordergrundroutine sonst tut. Es kann problemlos mehrere 1-ms-Ticks zählen, um Ticks mit längeren Perioden wie 1 Sekunde zu erzeugen. Da 1 Sekunde ziemlich langsam ist, kann die Interrupt-Routine jede Sekunde ein Flag setzen, das die Foregrond-Routine in ihrer Hauptereignisschleife überprüft und dann zurücksetzt.

Je nachdem, was sonst noch passiert, könnten Sie möglicherweise keine Interrupt-Routine verwenden, sondern die Hardware einfach bei jedem Takt ein Flag setzen lassen. In diesem Fall möchten Sie die Uhr wahrscheinlich länger ticken lassen, z. B. alle 10 ms. Der Vordergrundcode behandelt das in der Hauptereignisschleife und teilt es auf den eigentlich gewünschten 1er-Tick.

Weiter zu Olins Antwort ... so wird die Arduino-Funktion millis () tatsächlich implementiert ... Es gibt eine flüchtige globale 32-Bit-Zählervariable namens timer0_millis, die von der ISR TIMER0_OVF_vect verwaltet / aktualisiert wird, und die Millis-Funktion selbst nur stoppt Interrupts kurz, um diesen Wert in eine lokale Variable einzulesen und zurückzugeben. Sie können die Implementierung in der Datei „wiring.c“ in der Arduino-Kernbibliothek überprüfen, um zu sehen, wie sie es tatsächlich gemacht haben.

Neben den genannten Antworten gibt es noch eine weitere Möglichkeit der Zeitverschiebung. Die zum Takten des Arduino verwendeten Kristalle haben auch einen ziemlich weiten Toleranzbereich, so dass der Kristall selbst eine Drift hervorrufen kann. Wenn Sie dies über einen längeren Zeitraum verwenden möchten und bei Ihren Zeitmessungen präzise sein müssen, würde ich nach einem externen Taktchip suchen, z. B. dem DS1307, der sich einfach mit dem Arduino verbinden lässt. Diese Chips verwenden normalerweise einen "hochpräzisen" 32-kHz-Kristall, um die Zeit anzuzeigen (derselbe Kristall, der in gewöhnlichen Wanduhren verwendet wird).