millis()-Funktion Fehler: 1ms/1s feintuning möglich?

Hallo,

habe gerade meine millis()-Funktion an einem Osszi(LeCroy WaveSurfer 44Xs) und im Vergleich dazu eine DS3231 RTC vermessen.

Dabei fällt mir auf, dass die millis()-Funktion des öfteren einen Tick zu lange dauert ( ca. 1001,24ms) die RTC im direkten Vergleich (gibt ein Impuls jede Sec aus) laut Osszi ganz regelmäßig 999,84ms ausgibt.

Prinzipielle Frage:
Kann man das entsprechende Timer-Register paar Ticks ändern um die millis() zu "feintunen"?

Vielen Dank und schöne Grüße

PrinzipCodeSchnipsel

unsigned long jetzt = 0; // global
unsigned long last = 0;
unsigned int periode = 1000;

//.. in leerer loop()

jetzt = millis();
if(jetzt - last >= periode){
last = jetzt;
LED13on();  //Macro LED13  ein

delay(5);

LED13off();
}

Ist ein Fehler der von allem möglichen abhängen kann. Wäre daher fraglich, ob eine Änderung im Arduino Core optimal wäre.

Kannst du doch auch in deinem Sketch machen:
unsigned int periode = 999; // ist z.B. für meinen Resonator-Arduino genauer als 1000

rein theoretisch musst du übrigens last += periode; statt last = jetzt; schreiben...

Wir reden hier von Ungenauigkeiten um 0,1%. Wie genau ist Dein Osszi ?

Die Genauigkeit von Resonatoren wie Arduino UNO einen hat liegt bei 0,1 bis 0,5% (das sind 1mS bzw 5mS fehler auf einen Sekunde).
Diese Ungenuigkeit ist Temperaturabhängig, Alterungbedingt und Exemplarstreuung.
Das ist fast unmöglich zu korrigieren.
Wenn Du sehr genaue intervalle brauchst mußt Du auf einen Arduino mit Quarz oder externen genauen Oszillator verwenden.

Grüße Uwe

Du brauchst einen Quarzofen

jim_beam:
PrinzipCodeSchnipsel

Das sieht mir unsauber programmiert aus, insbesondere weil die millis() Funktion unrund läuft.

Und zwar kann die millis() Funktion entweder um 1 hochzählen (meistens) oder um 2 (manchmal).

Wenn nun kurz vor dem Umspringen eine Zählung um 2 gemacht wurde, dann setzt Du mit "last = jetzt;" die Startzeit der neuen Periode um fast eine Millisekunde zu spät an.

Ändere Deinen Codeschnipsel mal von:

last = jetzt;

auf:

last += periode;

Änderung?

Eigentlich sollten sich Zeitscheiben von 1000 ms einwandfrei zählen lassen, wenn ich mich nicht täusche.

Abgesehen von einem kleinen 4µs (0.004ms) Jitter, der durch Timer0 Interrupts bedingt wird plus den Jitter durch die Laufzeit Deiner loop() Funktion.

Vielen Dank für die schnelle Rückmeldungen.
Mir ist klar, dass es "jammern auf Hohem Niveau" ist, bzw. ich es gar nicht besser haben will/muss, es Interessiert mich und ich habe damit etwas "rum gespielt" und wollte es nun genauer wissen. :slight_smile:

Das Osszi mag auch seine Fehler habe, klar, daher auch zum Vergleich die RTC (die auch nicht Perfekt ist).

Habe beides Probiert, sowohl last = jetzt; und last += periode; Meine Erkenntnis bisher: micros() erkennt theoretisch in 4us Schritten eine Änderung.

Hier mein TestCode:

/*
 * Speed Test with non-bloking Tasks and comparsion of millis() vs micros()
 * 
 * Result: micros() is more accurate (a change in one bit is recodnises faster), 
 * milllis() is late by 1ms from time to time.
 * 
 * micros(1000000) = 1000'840.7us
 * millis(1000)    = 1001'299.8us toggles between 
 *                   1000'259.6us
 */

#define MICROS 1        // 1: use micros() instead of millis()

const byte cnt = 2;     // Number of Tasks in "Scheduler"

const int pin1 = 13;    // Indication Pin (LED)
const int pin2 = 30;

unsigned long jetzt = 0;        // global var. to store time now from millis()/micros()
unsigned long last[cnt] = {0};  // saves Task last called time [ms/us]

bool state[cnt] = {0};          // just an "inner" state of the Task


#if (MICROS == 1)
unsigned long periode[cnt] = {1000000}; // Nominal periode [us]
const unsigned long t_kurz  =  100000;  // short periode   [us]
const unsigned long t_lang  =  900000;  // long periode     [us]
#else
unsigned long periode[cnt] = {1000};    // ...same but [ms]
const unsigned long t_kurz  =  100;
const unsigned long t_lang  =  900;
#endif



void setup() {
 
  pinMode(pin1, OUTPUT);
  pinMode(pin2, OUTPUT);
  
  #if (MICROS == 1)
  last[0] = 100000; // Offset Task0 [us]
  #else
  last[0] = 100;    // Offset Task0 [ms]
  #endif
}

void loop() {


  
  // get current "time"
  #if (MICROS == 1)
  jetzt = micros()+10000000UL;    // Add offset to millis for Starting-Offset of Task0 (to avoid neg. overflow) 
  #else
  jetzt = millis()+10000UL;
  #endif
  
  //---Task0-----------------------
  if(jetzt - last[0] >= periode[0]){
    //last[0] = jetzt;        // weather use "real now time" or..
    last[0] += periode[0];    // ..unse fixed periodes between Task executiuon
    
   if(state[0]){              // On, short
    digitalWrite(pin1,HIGH);
    periode[0] = t_kurz;
   }
   
   else{                      // Off, long
    digitalWrite(pin1,LOW);
    periode[0] = t_lang;
   }
   state[0] = !state[0];      // swap state
  }



  // update current "time"
  #if (MICROS == 1)
  jetzt = micros()+10000000UL;
  #else
  jetzt = millis()+10000UL;
  #endif

  //---Task1-----------------------
  if(jetzt - last[1] >= periode[1]){
    //last[1] = jetzt;
    last[1] += periode[1];
    
   if(state[1]){                // On, short
    digitalWrite(pin2,HIGH);
    periode[1] = t_kurz;
   }//if
   
   else{                        // Off, long
    digitalWrite(pin2,LOW);
    periode[1] = t_lang;
   }//else
   
   state[1] = !state[1];        // swap state
        
  }//if



}//loop

Jetzt fehlt mir leider die Zeit, werde mich morgen noch mal etwas damit beschäftigen und entsprechend antworten.
Schöne Grüße

Mal eine andere Frage zum Thema icht ganz sauber programmiert...

Um den Tasks einen Zeitlichen Versatz zueinander oder später auch ein Absoluten Offset zu geben (z.B. erst Foo1 und 100ms später Foo, natürlich ohne delay) muss ich meinem Code
jetzt = millis + 10000 (Code s. oben) als Offset mit dazu geben, sonst bekomme ich am Programmanfang, wenn millis() noch sehr klein sind (kleiner als mein gewünschtes Offset) einen neg. Überlauf bei der Abfrage
if(jetzt - last >= periode) Bsp: 10 - 500 >= 1000 ... ist ja immer wahr.

Hilft mir dann if((long)(jetzt-last) >= periode) weil zu signed gecasted?

Ist das "sauber" oder wie würde man das Problem angehen? (Kann es leider erst morgen wieder testen.)

Vielen Dank und schönen Abend.

einen neg. Überlauf bei der Abfrage

Bei unsigned long gibts keine negativen Überläufe.

if(jetzt - last >= periode) Bsp: 10 - 500 >= 1000 ... ist ja immer wahr.

Wie kann den last 500 sein, wenn jetzt erst 10 ist?

jim_beam:
muss ich meinem Code
jetzt = millis + 10000

Du darfst NIE zu einem millis() Wert irgendwas draufaddieren, bevor Du es vergleichst.

Du mußt in einem zuverlässig laufenden Code immer nur mit DIFFERENZEN von Zeiten hantieren.

combie:
Bei unsigned long gibts keine negativen Überläufe.

Da habe ich mich falsch ausgedrückt, meinte in negative Richtung, von 0 zu 2^16.

combie:
Wie kann den last 500 sein, wenn jetzt erst 10 ist?

jim_beam:
Um den Tasks einen Zeitlichen Versatz zueinander oder später auch ein Absoluten Offset zu geben (z.B. erst Foo1 und 100ms später Foo, natürlich ohne delay)

Habe es mal getestet und es tut dass was ich mir wünsche und erwarte, aber ist es auch "sauber" oder wie gibt man sonst noch ein Start-OFFSET für eine Task mit?

Test-Code:

#define SP(x); Serial.print(x);
#define OFFSET 500    // Start-Offset

unsigned long last = OFFSET;
long periode = 1000;
unsigned long jetzt = 0;
signed long dt = 0;

int id = 0;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(230400);
  SP("Starting...")
}

void loop() {
  // put your main code here, to run repeatedly:

  jetzt = millis();           // Akt. Zeit
  dt = (long)(jetzt -last);   // Delta t (signed!)
  if(dt >= periode){          // Task schon wieder drann?
    if(abs(dt) > periode*2){  // bei zu großen Abweichung ab jetzt
      last = jetzt;
    }
    else{
      last += periode;        // sonst konst. Perioden
    }
        
    SP("\n");
    SP(jetzt);
    SP("\t ID: ");
    id++;
    SP(id);
    
  }

}

Vielen Dank für die Anregungen! :slight_smile:

jim_beam:
Habe es mal getestet und es tut dass was ich mir wünsche und erwarte, aber ist es auch "sauber" oder wie gibt man sonst noch ein Start-OFFSET für eine Task mit?

Wenn Du einen OFFSET mitgeben möchtest, dann machst Du es so:
last ==> unsigned long ==> auf OFFSET setzen
jetzt ==> long
Dann ist die Differenz als Wert vorzeichenbehaftet. Du kannst sie direkt verarbeiten, oder wenn Du möchtest die Differenz auch als Variable deklarieren, und zwar als vorzeichenbehaftete Variable:
dt ==> long

Mit dem Start-OFFSET für 'last', der in der Zukunft liegt, ergibt sich zu Anfang des Programms eine negative Zeitdifferenz, und eine neue Periode startet erst, wenn die Zeitdifferenz im positiven Bereich erreicht wird.

Deklarationen und loop:

unsigned long last = OFFSET;
long periode = 1000;
long jetzt = 0;
long dt = 0;


void loop() {
  jetzt = millis();           // Akt. Zeit
  dt = jetzt-last;           // Delta t (signed!)
  if(dt >= periode)       // Beim Programmstart ist dt negativ: 0-500= -500
  {
    last += periode;  // neue Periode
    Serial.print("\nTime: ");Serial.print(last/1000.0,6);
  }
  SP("\n");
  SP(jetzt);
  SP("\t ID: ");
  id++;
  SP(id);
}

Die Periodenstarts liegen dann bei 500, 1500, 2500, 3500 etc.

Übrigens kann ein 16MHz-Arduino diese Baudrate nicht erzeugen: Serial.begin(230400);

Wenn Du eine höhere serielle Baudrate benötigst als die höchste Standardbaudrate (115200), dann solltest Du nur Baudraten verwenden, die der Arduiono auch tatsächlich erzeugen kann, das wären:
Serial.begin(250000);
oder
Serial.begin(500000);

Es geht ganz einfach:

const unsigned int PERIODE= 10000;

unsigned long last = -5000L;  // das geht schon

void loop() {
   unsigned long now = millis(); 
   if ( now - last > PERIODE) {
       last += PERIODE;
       // Hier der 10-Sekundentakt, zuerst 5 sec nach Start

   }
}

Die ersten Differenzen ergeben:
0 - (-5000) = 5000
...
1000 - (-5000) = 6000
5000 - (-5000) = 10000 --> Periode erreicht
6000 - (5000) = 1000

Dass last als unsigned deklariert ist, macht für die eigentliche Berechnung nichts aus,
würde nur in einem println nicht als -5000 sondern als 4294962296 angezeigt. (Was nur blöde vorher auszurechnen und so einzutippen wäre)

Vielen Dank für die Rückmeldungen!

michael_x:
Es geht ganz einfach:

const unsigned int PERIODE= 10000;

unsigned long last = -5000L;  // das geht schon

void loop() {
  unsigned long now = millis();
  if ( now - last > PERIODE) {
      last += PERIODE;
      // Hier der 10-Sekundentakt, zuerst 5 sec nach Start

}
}





Die ersten Differenzen ergeben: 
0 - (-5000) = 5000
...
1000 - (-5000) = 6000
5000 - (-5000) = 10000 --> Periode erreicht
6000 - (5000) = 1000

Dass last als unsigned deklariert ist, macht für die eigentliche Berechnung nichts aus, 
würde nur in einem println nicht als -5000 sondern als 4294962296 angezeigt. (Was nur blöde vorher auszurechnen und so einzutippen wäre)

Das ist tatsächlich "ganz einfach" und scheint gut zu funktionieren, allerdings nur, wenn OFFSET <(=?) Periode ist, was bei mir auch anders sein kann.

Mir scheint die vermischte Variante von jurs und michael_x mit dem long dt und long Periode sowie -Werten gut für alle erdenklichen Fälle zu funktionieren. Dabei scheint "jetzt" unsigned long bleiben zu dürfen (sonst gibt es doch bei großen Zahlen Probleme mit der Zuweisung unsigned zu signed!?)

* /*
 * Offset für Task:
 * Vor Start wird last mit Offset initialisiert:
 * Soll die Task x [ms] nach Start zum ersten Mal erfolgen:  last = x - Periode; [ms] 
 *  
 * Bsp: Periode = 1000; [ms]
 *  x[ms]   last[ms]
 * ------------------------  
 *     0    -1000*         *(last darf NICHT  last < (-Periode)  sein!
 *   900     -100
 *  1000        0
 *  1100      100
 *  5000     4000
 *   ...      ...
 *   
 * unsigned long jetzt;
 * unsigned long last;
 * long          periode;
 * long          dt;   oder ohne extra variable dt: if( (long)(millis()-last) >= periode )
 */
 *

Sehr schön, so wollte ich es gerne haben! Vielen Dank! :slight_smile:

Hier noch mein Test-Code (aber nicht hauhen, habe mir da ein sicherlich ganz böses Macro gebastlet um Tipparbeit zu sparen und for Fun!)

/*
 * 150820 V003 Test_Offset_signed_V003.ino
 * 
 * Test Sketsch für Task-Scheduller Berechnungen mit Start-Offste 
 * 
 * Offset für Task:
 * Vor Start wird last mit Offset initialisiert:
 * Soll die Task x [ms] nach Start zum ersten Mal erfolgen:  last = x - Periode; [ms] 
 *  
 * Bsp: Periode = 1000; [ms]
 *  x[ms]   last[ms]
 * ------------------------  
 *     0    -1000*         *(last darf NICHT  last < (-Periode)  sein!
 *   900     -100
 *  1000        0
 *  1100      100
 *  5000     4000
 *   ...      ...
 *   
 * unsigned long jetzt;
 * unsigned long last;
 * long          periode;
 * long          dt;   oder ohne extra variable dt: if( (long)(millis()-last) >= periode )
 */



//### Setable Defines ################################################################
#define DEBUG_TASK 2                                // Set Task Debug Leve /Info (0~2)
#define USE_MICROS 0                                // use micros() instead of millis() [NOT INSTALLED]
//************************************************************************************       


//### Edit Task Array ###############################################################
//const int     CNT         = 3;                    // Task Count
#define CNT (sizeof(last)/sizeof(last[0]))          // Task/Array Count
unsigned long last[]   = {-500,    0,      0   };   // last exec. / Start Offset
int           periode[]= {1000,  1000,   1000  };   // Periode
bool          state[]  = {0,     0,      0     };   // inner LED State
const byte    pin[]    = {13,    12,     11    };   // LED Pin
//************************************************************************************       



//### Global Var. for Task exec. #####################################################
unsigned long jetzt       = 0;                       // akt. Zeitmerker
//long          dt          = 0;                       // Differenzzeit (signed!)
//************************************************************************************    

//### Define TASK(x) Exec. Test and Service Macro ####################################
/*
#define TASK(x)    if((long)(millis()-last[x]) >= periode[x]){\
                    jetzt = millis();\
                    SerialDebug(x);                 \
                    last[x] += periode[x];          \
                    byte _tID = x; 
 */                   
 
#define TASK(x)   jetzt = millis();                 \         // Ok geht! ohne long dt
                  if((long)(jetzt-last[x]) >= periode[x]){\
                    SerialDebug(x);                 \
                    last[x] += periode[x];          \
                    byte _tID = x; 

/*
#define TASK(x)   jetzt = millis();                 \
                  dt    = (jetzt - last[x]);  \
                  if(dt >= periode[x]){             \
                    SerialDebug(x);                 \
                    if(dt > (periode[x]*2)){        \  // ändern des last gewollt??
                      last[x] = jetzt;              \
                    }                               \
                    else{                           \
                      last[x] += periode[x];        \
                    }                               \
                    byte _tID = x; 
*/
//************************************************************************************       

//### Debug Leve für Task Exec. Info ##################################################
#if (DEBUG_TASK == 1)                               // Get Task exec. Info
void SerialDebug(byte x){
  Serial.println();              
  Serial.print(jetzt);           
  Serial.print(F("\tTask: "));   
  Serial.print(x);
}
#elif (DEBUG_TASK == 2)                             // +additional Task Parameter
void SerialDebug(byte x){
  Serial.println();              
  Serial.print(jetzt);           
  Serial.print(F("\tTask: "));   
  Serial.print(x);
  Serial.print(F("\tLast: "));
  Serial.print(last[x]);
  Serial.print(F("\tPeriod: "));
  Serial.print(periode[x]); 
}
#else
void SerialDebug(byte x){
}
#endif
//************************************************************************************



//####################################################################################
void setup() {
  // put your setup code here, to run once:
  
  for (byte y=0;y<CNT;y++){                               // Init LED Pins
    pinMode(pin[y], OUTPUT);
  }
  
  Serial.begin(230400);                                   // Start Serial
  Serial.println(F("\n\nStarting...\n\n" __FILE__ "\n" __DATE__ "\n" __TIME__ "\n"));

}//void setup()
//************************************************************************************       



//####################################################################################
//### main loop() ####################################################################
void loop() {
  // put your main code here, to run repeatedly:


  //-----------------------------------------------
  TASK(0)                             // Task(_tID) (NO "{" after "Task()" !) 
    
    if(state[_tID]){                  // LED is on
      digitalWrite(pin[_tID],LOW);  
    }
    else{                             // LED is off
    digitalWrite(pin[_tID],HIGH);
    }
    state[_tID] = !state[_tID];
    
  }//Task[0] --------------------------------------



  //-----------------------------------------------
  TASK(1)                             // Task(_tID) (NO "{" after "Task()" !)
    
    if(state[_tID]){                  // LED is on
      digitalWrite(pin[_tID],LOW);
    }
    else{                             // LED is off
    digitalWrite(pin[_tID],HIGH);
    }
    state[_tID] = !state[_tID];
    
  }//Task[1] --------------------------------------
  

}//void loop()************************************************************************     
//************************************************************************************

jurs:
Übrigens kann ein 16MHz-Arduino diese Baudrate nicht erzeugen: Serial.begin(230400);

Wenn Du eine höhere serielle Baudrate benötigst als die höchste Standardbaudrate (115200), dann solltest Du nur Baudraten verwenden, die der Arduiono auch tatsächlich erzeugen kann, das wären:
Serial.begin(250000);
oder
Serial.begin(500000);

Funktioniert super hier mit meinen Megas. :slight_smile: 250'000 wollte nicht laufen und 500'000 unterstützt mein Terminal der IDE (1.6.5 Ubuntu) nicht (und mein H-Term läuft unter Ubuntu 14.04 leider nicht mehr :cry: )

scheint gut zu funktionieren, allerdings nur, wenn OFFSET <(=?) Periode

Das stimmt natürlich:
Bei last = 0 dauert es genau eine Periode, bis es los geht. Bei last < 0 etwas weniger
Beim Beispiel PERIODE=10000 last = -15000L ist sofort die Startbedingung erfüllt:
0 - (-15000) > PERIODE

Bei last= +5000L ergibt sich eine Differenz (now - last) kleiner als Null.
Bei unsigned Arithmetik ist "kleiner null" das gleiche wie "sehr groß".

Die Lösung ist also jurs' Vorschlag mit signed long Arithmetik.

Die sollte auch nach 24,x Tagen, sogar im Übergang, wenn jetzt und kurz darauf last negativ wird, funktionieren, da die Differenz immer im passenden Bereich bleibt. ( -x ... PERIODE )

michael_x:
Die Lösung ist also jurs' Vorschlag mit signed long Arithmetik.

Was mich son bisschen wundert, wie machen das denn alle anderen so? Also die zeitliche Steuerung im uC.

Ist da denn noch keiner drüber gestolpert? Denn bei meinen Hobby-Anwendungen (im wesentlichen Data-Logging und Steuern/Regeln) brauche ich eigentlich immer einen Task-Scheduler für zeitliche Abläufe (also Endlicher Automat).

Dazu kommt, dass ich den Anspruch habe, den Controller schlafen zu legen und fast alle (ext./int) Hardware aus zu schalten (bis auf WDT), bis die nächste Task dran ist. Wüsste nicht, wies ohne diese "minimalst Betriebssystem" geht. Aber ich hab'e auch nicht viel Ahnung davon :slight_smile:

wie machen das denn alle anderen so

Wie es alle machen, weiss keiner :wink:

Dass du am Anfang der Zeit in loop() ein anderes Verhalten willst als später, ist eher speziell ...

Wer stur in "Endlichen Automaten" denkt, baut dafür natürlich einen extra Zustand (WarteBisEsLosGeht).

if ( millis() - last > PERIODE)

ist schon der Standard- Ansatz, bei dem in der Regel unsigned long die richtigen Datentypen sind (wie millis() es ja auch zurückliefert).

Wer stur in "Endlichen Automaten" denkt, baut dafür natürlich einen extra Zustand (WarteBisEsLosGeht).

Danke für die Blumen....!
Ich könnte mich ja jetzt aufregen, aber das macht wenig Sinn weil du damit vollkommen recht hast.
:wink:

Aber auf diese Idee würde ich nicht kommen:

jetzt = millis + 10000

Oder:

last = OFFSET

Last ist last!
Das vergangene....
Und Offset ist offset.

Z.B. kann Jetzt nur millis() sein.
Denn millis() ist die Arduino Entsprechung von Jetzt.

Auch kann last niemals ein Offset sein.
Das passt sprachlich nicht.
Last ist ein vergangenes Jetzt!

Die Zuweisungen sind aus meiner bescheidenen Sicht defekt.
Bzw. fehlt ihnen die sprachliche Logik.

Die Zuweisungen sind aus meiner bescheidenen Sicht defekt

.
Statt mich über den harten Ausdruck "defekt" aufzuregen, geb ich dir lieber recht :wink:

Guter Code ist etwas, das man auch ein halbes Jahr später noch sofort versteht und nicht über die seltsame signed Arithmetik und Initialisierung der Variablen ins Grübeln kommt.

Allerdings gibt es, speziell für unsere Tiny und Uno Lieblinge, den sportlichen Ehrgeiz, hier und da ein Byte RAM oder Code zu sparen. Wer weiss, ob man's noch findet, wenn der Sketch an seine Grenzen kommt und man sparen muss.

Hallo,

ich verstehe ehrlich gesagt Dein eigentliches Problem noch nicht und den genauen Anwendungsfall nicht.
Kann aus den Infos nur so viel sagen. Wenn Du millis verwendest bist Du abhängig davon die schnell oder wie konstant deine loop durchlaufen wird. Wenn in der loop hin und wieder irgendwelche Sachen entschieden oder berechnet werden in Abhängigkeit von anderen, dauert die loop unterschiedlich lange.

Wenn Du aber eine RTC schon hast, dann verwende diese und lass den µC mittels RTC Alarm jede Sekunde, oder aller 30sec oder aller einer Minute aufwecken. Das ist dann wirklich konstant. Erfordert jedoch mehr Codeaufwand.