Genauigkeit Millis?

Hallo,
Wieder ein mal eine Frage “nur zum Verstehen weshalb”:

Ich habe einen Sketch mit welchem ich alle 1000 Millis einen Messwert auf eine SD Karte schreibe.
Im ganzen Sketch gibt es kein Delay, deshalb sollte das doch auch wirklich alle Sekunde passieren.
Wenn ich die Messergebnise auslese, dann sehe ich allerdings, dass etwa alle 7 Minuten eine Sekunde fehlt. Das führt letzten Endes dazu, dass ich nach einem Tag nicht wie erwartet 86400 Messwerte auf der Karte habe sondern nur rund 86000 (± 2).

Die Frage ist jetzt: Ist das einfach eine Toleranz mit der man leben (oder welche man herausrechnen) muß, oder habe ich in den Sketch doch etwas eingebaut wo ich Zeit verbummle?

Hier der Sketch “zum mal drübergucken”:

/*
  Data logging to the Deek-Robot shields with RTC for the Arduino Nano.
  Btw.: Because i found no informations which card size can be used: i  use a Fat32 formated 8GB noname.

  The sketch reads the value of 2 current sensors, writes the result as mA to an SD card und shows it at an 4 lines
  i2C display. Current sensors are INA282 modules (unidirectional, Ref1 andRef2 to GND) with replaced shunt, new shunt has 0.0167 Ohm, so it's possible to measure
  current up to 4,4 A .

  The base of the storge and RTC part i found at https://publiclab.org/notes/cfastie/04-30-2017/data-logger-shield-for-nano .

  created 2021
  by Herbert Kozuschnik

  This code is in the public domain.
*/



#include <SdFat.h>
#include  <SPI.h>
#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>                  // LiquidCrystal_I2C Bibliothek einbinden
LiquidCrystal_I2C lcd(0x27, 20, 4);             // Display Adresse und Größe deffinieren.


float mAps = 4.3;                              // Miliampere per analogRead Step, muss an jeden Sensor angepasst werden
float korrekturwert = 0.975;                    // zum Angleichen der beiden Sensoren

int adwert = 0;                                 // für Displayausgabe des durchschnittlichen AD Wertes, nur zur Kontrolle

long sensorsumme1 = 0;
long sensorsumme2 = 0;

long mAmp1 = 0;
long mAmp2 = 0;

long mAhmin1 = 0;
long mAhmin2 = 0;

unsigned int j = 0;
unsigned int k = 0;

unsigned long currentMillis;
unsigned long previousMillis = 0;
const int interval = 1000;


SdFat SD;
const byte MOSIpin {11};
const byte MISOpin {12};

RTC_DS1307 RTC;
#define DS1307_I2C_ADDRESS 0x68

char TmeStrng[20];
char mAsStrng[16];
char mAmStrng[16];



void setup() {

  lcd.init();                                       // LCD aktivieren
  lcd.backlight();                                  // Hintergrundbeleuchtung einschalten (lcd.noBacklight(); schaltet die Beleuchtung aus).

  Wire.begin();                                    // start the i2c interface
  RTC.begin();                                     // start the RTC
  //  RTC.adjust(DateTime((__DATE__), (__TIME__)));    // !! Zeile nach erstem Upload auskommentieren und Sketch noch einmal hochladen, - setzt die Zeit auf dem RTC
  SD.begin();

}                                                  // end of setup




void loop() {

  read_ampere();

  currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    ausgabe();
  }


}


void ausgabe()  {

  get_time();
  calc_strom();
  calc_mAhmin();
  displayausgabe();
  write_data();

  j = 0;

}


void calc_strom()  {                                       // Strom aus Sensorwerten berechnen

  sensorsumme1 = sensorsumme1 / j;
  adwert = sensorsumme1;
  mAmp1 = sensorsumme1 * mAps;
  sensorsumme1 = 0;

  sensorsumme2 = sensorsumme2 / j;
  mAmp2 = sensorsumme2 * (mAps * korrekturwert);
  sensorsumme2 = 0;

}


void calc_mAhmin()  {                                      // Berechnen wie viele mAh in einer Minute geflossen sind.

  mAhmin1 = mAhmin1 + mAmp1;
  mAhmin2 = mAhmin2 + mAmp2;

  k = k + 1;

  if (k == 60) {
    mAhmin1 = mAhmin1 / 3600;
    mAhmin2 = mAhmin2 / 3600;
    write_mAhmin();
  }

}




void displayausgabe() {

  lcd.setCursor(0, 0);                                      //Display leeren
  lcd.print("                   ");
  lcd.setCursor(0, 1);
  lcd.print("                   ");
  lcd.setCursor(0, 2);
  lcd.print("                   ");
  lcd.setCursor(0, 3);
  lcd.print("                   ");

  lcd.setCursor(0, 0);                                      // Messwerte Anzeigen
  lcd.print(TmeStrng);
  lcd.setCursor(0, 1);
  lcd.print(mAmp1);
  lcd.print(" mA");
  lcd.setCursor(0, 2);
  lcd.print(mAmp2);
  lcd.print(" mA");
  lcd.setCursor(0, 3);
  lcd.print(j);
  lcd.setCursor(10, 3);
  lcd.print(adwert);


}




void get_time() {                                            //Datum und Zeit auslesen Zeit auslesen

  DateTime now = RTC.now();
  sprintf(TmeStrng, "%02d.%02d.%04d,%02d:%02d:%02d", now.day(), now.month(), now.year(), now.hour(), now.minute(), now.second());
  sprintf(mAsStrng, "%04d%02d%02dAs.txt", now.year(), now.month(), now.day());
  sprintf(mAmStrng, "%04d%02d%02dAm.txt", now.year(), now.month(), now.day());

}



void write_data() {                                        //auf SD Karte schreiben

  String dataString = "";                                  //erst mal vorherigen string löschen

  dataString += TmeStrng;                                  //und neuen string zusammensetzen
  dataString += ",";
  dataString += String(mAmp1);
  dataString += ",";
  dataString += " mA";
  dataString += ",";
  dataString += String(mAmp2);
  dataString += ",";
  dataString += " mA";


  File dataFile = SD.open(mAsStrng, FILE_WRITE);
  if (dataFile) {
    dataFile.println(dataString);
    dataFile.close();
  }
  else {
    // Displayausgabe wenn schreiben nicht möglich
    lcd.setCursor(0, 0);                                      // Zeile löschen
    lcd.print("                   ");
    lcd.setCursor(0, 1);
    lcd.print("                   ");
    lcd.setCursor(0, 2);
    lcd.print("                   ");
    lcd.setCursor(0, 3);
    lcd.print("                   ");

    lcd.setCursor(0, 0);                                      // Messwerte mit Fehlermeldung anzeigen
    lcd.print(TmeStrng);
    lcd.setCursor(0, 1);
    lcd.print(mAmp1);
    lcd.print(" mA");
    lcd.setCursor(0, 2);
    lcd.print(mAmp2);
    lcd.print(" mA");
    lcd.setCursor(0, 3);
    lcd.print("SD Schreibfehler");

  }

}


void write_mAhmin() {                                      //auf SD Karte schreiben

  String dataString = "";                                  //erst mal vorherigen string löschen

  dataString += TmeStrng;                                  //und neuen string zusammensetzen
  dataString += ",";
  dataString += String(mAhmin1);
  dataString += ",";
  dataString += " mAh";
  dataString += ",";
  dataString += String(mAhmin2);
  dataString += ",";
  dataString += " mAh";



  File dataFile = SD.open(mAmStrng, FILE_WRITE);
  if (dataFile) {
    dataFile.println(dataString);
    dataFile.close();
  }
  else {

    lcd.setCursor(0, 3);
    lcd.print("SD Schreibfehler mAhmin");

  }

  k = 0;
  mAhmin1 = 0;
  mAhmin2 = 0;


}





void read_ampere()  {

  int  sensorwert1 = 0;                                                   // Sensorwert rücksetzen
  int  sensorwert2 = 0;


  sensorwert1 = analogRead(A1);
  sensorsumme1 = sensorsumme1 + sensorwert1;

  sensorwert2 = analogRead(A2);
  sensorsumme2 = sensorsumme2 + sensorwert2;

  //  delay (1);
  j = j + 1;

}

Beim Code einfügen ist mir noch etwas aufgefallen:
Wenn man in der IDE die Funktion “für Forum kopieren” verwendet, werden massenhaft Leerzeilen eingefügt, - ich denke das ist nicht so gewollt…

Der RTC kann dir einen genauen Sekunden Puls geben, benutze den.

millis() driftet immer und ist auch grundsätzlich nicht 100%ig genau.

Die Genauigkeit von millis() hängt von dem Taktgeber ( Quarz/Resonator ) auf deinem Arduino-Board ab. Da gibt es große Unterschiede, aber wirklich präzise sind sie alle nicht. Die größten Toleranzen hat ein Resonator, ein Quarz ist schon besser.
Wenn Due es genauer haben willst, musst Du ein gutes Uhrenmodul z.B. mit dem DS3231, einsetzen, und deine Zeit von diesem Uhrenmodul ableiten.

Edit: hab' jetzt erst gesehen, dass Du ja schon ein Uhrenmodul verbaut hast. Das dürfte auf jeden Fall genauer sein, wie die millis().

herbk:
Die Frage ist jetzt: Ist das einfach eine Toleranz mit der man leben (oder welche man herausrechnen) muß, oder habe ich in den Sketch doch etwas eingebaut wo ich Zeit verbummle?

Du vergisst, das die Laufzeit zu einer Ungenauigkeit führt.
Darüber hinaus addierst Du die Abweichung bei jedem

    previousMillis = currentMillis;

Besser wäre, das intervall aufzuaddieren. Damit vermeidest Du den additionsfehler.

Ja das ist ein berechtigter Einwand ( hätte ich auch drauf kommen können :wink: ). Wenn Du möglichst gleichmäßige Intervalle haben wilst, musst Du

previousMillis += intervall;

schreiben.
Das ändert allerdings nicht daran, dass millis() prinzipbedingt nicht besonders genau ist. Aber vielleicht taugt es dann für dich ja schon. 100%ig exakt gibt es eh nicht. Kommt immer auf die Anforderungen an.

Da schon eine Antwort auf meinen Einwand drin ist, hier der gerne genuztzte VorführSketch:

unsigned long lastMillis = 0;
const unsigned long interval = 1000;

void setup() {
  Serial.begin(115200);
  Serial.println(F("Start"));
}

void loop() {
  if (millis() - lastMillis >= interval) {
    Serial.println(lastMillis);
    lastMillis=lastMillis+interval;
  }
}

@ TO: Schau mal in die Ausgabe auf dem SerMon - beachte die Stellen in der Uhrzeit nach dem .

11:04:02.808 -> Start
11:04:03.807 -> 0
11:04:04.805 -> 1000
11:04:05.835 -> 2000
11:04:06.831 -> 3000
11:04:07.829 -> 4000
11:04:08.827 -> 5000
11:04:09.824 -> 6000
11:04:10.822 -> 7000
11:04:11.818 -> 8000
11:04:12.827 -> 9000
11:04:13.824 -> 10000
11:04:14.820 -> 11000
11:04:15.816 -> 12000
11:04:16.813 -> 13000
11:04:17.808 -> 14000
11:04:18.806 -> 15000
11:04:19.807 -> 16000
11:04:20.806 -> 17000
11:04:21.802 -> 18000
11:04:22.833 -> 19000
11:04:23.829 -> 20000
11:04:24.825 -> 21000
11:04:25.822 -> 22000
11:04:26.817 -> 23000
11:04:27.813 -> 24000
11:04:28.808 -> 25000
11:04:29.804 -> 26000
11:04:30.833 -> 27000
11:04:31.830 -> 28000
11:04:32.843 -> 29000
11:04:33.827 -> 30000
11:04:34.823 -> 31000
11:04:35.818 -> 32000
11:04:36.814 -> 33000
11:04:37.810 -> 34000
11:04:38.807 -> 35000
11:04:39.803 -> 36000
11:04:40.832 -> 37000
11:04:41.827 -> 38000
11:04:42.823 -> 39000
11:04:43.819 -> 40000

Die Genauigkeit lässt sich damit über den Tag signifikant verbessern. Vorausgesetzt, das der zeitgebende Takt das hergibt.
Ansonsten bin ich ebenfalls bei einer genauen RTC.

Wenn Du die Zeit möglichst genau haben willst dann nimm eine DS3231 und den INT/SQW Ausgang. Dieser liefert richtig programmiert ein 1Hz Signal:

https://datasheets.maximintegrated.com/en/ds/DS3231.pdf:
Active-Low Interrupt or Square-Wave Output. This open-drain pin requires an external pullup resistor connected to a supply at 5.5V or less. This multifunction pin is determined by the state of the INTCN bit in the Control Register (0Eh). When INTCN is set to logic 0, this pin outputs a square wave and its frequency is determined by RS2 and RS1 bits. When INTCN is set to logic 1, then a match between the timekeeping registers and either of the alarm registers activates the INT/SQW pin (if the alarm is enabled). Because the INTCN bit is set to logic 1 when power is first applied, the pin defaults to an interrupt output with alarms disabled. The pullup voltage can be up to 5.5V, regardless of the voltage on VCC. If not used, this pin can be left unconnected.

Mit dem 1Hz Signal steuerst Du eine Interruproutine an, die dann die Messung macht.

Grüße UWe

Hallo,

Das liegt an der Systemfrequenz, zudem ist die auch noch oft von der Tempearatur abhängig. Wenn Du schon eine RTC drin hast , dann solltest Du auch die dazu benuzten. Du kannst mit now ja auch die aktuellen sekunden abfragen, tust Du ja auch eigendlich schon. Immer wenn sich die gerade geändert haben ist das dein Ereigniss.

Heinz

Nachtrag: muss ich so zurücknehmen. Geht so nicht, solange du now abgeleitet von millis() alle 1000ms aktualisierst wird das nicht besser. Man musste dann eventuell now alle 200ms akualisieren.

Danke
habe ich also doch etwas eingebaut wo Zeit verbummelt wird...

Ich werde als erstes auf "den Interval addieren" umstellen und dann mal ein paar Tage laufen lassen.
Das Zeitmodul das ich verwende ist noch eine DS1307 Micro SD Kombination, ich habe aber schon DS3231 Module bestellt. Wenn die da sind probiere ich dann mal wie viel es noch genauer wird.

Wie eingangs gesagt: war eigentlich mehr eine Frage zum Verständnis, in dieser Anwendung könnte ich selbst mit den 400 Sekunden Abweichung leben....

Hallo,
hier mal eine kleine Rückmeldung zu dem Thema nach ein paar Tagen messen:

Die Umstellung auf

  currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
  previousMillis = previousMillis + interval;
  ausgabe();
  }

hat gebracht, dass etwa 50 Sekunden weniger verloren gehen, beim Messen im Freien mit einer Temperaturspanne von - 5 bis + 15° C. Beim Messen unter konstant 20° C gehen weitere 20 Sekunden weniger verloren.

Wie oben schon gesagt, ich kann damit leben, - hat mich halt interessiert…

Wollte schon ganz am Anfang antworten, weil ich mit millis() kein Problem habe.

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    ausgabe();
  }

austauschen durch:

  if (currentMillis - previousMillis >= interval) {
    ausgabe();
    previousMillis = currentMillis;
  }

erst ausgabe() machen, dann millis() speichern. Da sollte der Drift minimal bleiben.

freddy64:
erst ausgabe() machen, dann millis() speichern. Da sollte der Drift minimal bleiben.

Nö.
Das macht noch mehr Drift.
Das merkst nur nicht, weil Du Dich selbst bescheisst.

erst ausgabe() machen, dann millis() speichern. Da sollte der Drift minimal bleiben.

Die Drift kann man auch noch weiter gehend unterbinden!
Dann spielt auch die Reihenfolge keine Rolle.

  if (currentMillis - previousMillis >= interval) {
    previousMillis +=  interval;
    ausgabe();
  }

Ich nenne das Pattern "Die Aufholjagt"

Auge an Großhirn!
previousMillis += interval;
speichern!

Das habe ich auch noch nicht gewusst.

Hallo combie,

ist

previousMillis +=  interval;

nicht nur eine kürzere Schreibweise von

 previousMillis = previousMillis + interval;

?
Verursacht das tatsächlich weniger verlorene Sekunden?
OK, ich probiere es aus, - das Ergebnis kommt dann Mittwoch...

Das habe ich auch noch nicht gewusst.

:o

Mindestens drei mal, alleine in diesem Thread.
Aber gut, dass der Groschen jetzt gefallen ist.

ist

previousMillis +=  interval;

nicht nur eine kürzere Schreibweise von

Vollkommen korrekt!

Du hattest es ja schon geschluckt.....

Verursacht das tatsächlich weniger verlorene Sekunden?

Es kann nicht exakter sein, als der Quarz/Resonator vorgibt.

combie:
Es kann nicht exakter sein, als der Quarz/Resonator vorgibt.

OK, Danke, - das hätte mich jetzt auch echt gewundert...

Mindestens drei mal, alleine in diesem Thread.
Aber gut, dass der Groschen jetzt gefallen ist.

ich wollte erst mal abwarten, bis hier die beste Lösung angeboten wird. :slight_smile:

Aber funktioniert top. Die Zeitangaben in der mySQLDB, in der alle 20 min. die Sensordaten gespeichert werden sind jetzt immer auf die Sekunde genau.

freddy64:
ich wollte erst mal abwarten, bis hier die beste Lösung angeboten wird. :slight_smile:

Alles klar!
Da habe ich keine Fragen mehr!
Alles klar.

@combie
Sei doch nicht gleich eingeschnappt!
20 Erfahrungen von anderen zu lesen ist wertvoll.
Man könnte es auch Schwarmintelligenz nennen.