Millis non è "a tempo"

Salve
Ho un piccolo problema con la programmazione di un metronomo (eccessivamente complicato per quello che deve fare un metronomo ma vabbè).

Quando richiamo la funzione millis() per contare l'intervallo di tempo tra una battuta e la successiva, se associo un metronomo di un app sul telefono (affidabile), il metronomo realizzato con arduino perde "un po' il conto del tempo" in modo lineare:

più semplicemente mantiene un conteggio costante del tempo, ma un millisecondo contato dall'arduino è più lento di un millisecondo reale. Volevo sapere se è un errore dovuto al modo in cui è realizzato il codice (funzioni che in qualche modo "rallentano arduino") o se è la scheda arduino originale ad essere non perfettamente funzionante.

premetto che non sono proprio per niente un esperto in programmazione, e che l'ho testato anche su una scheda arduino nano non originale e dava lo stesso problema.

allego il codice, non è ultimato e per realizzarlo voglio integrare tutti i "void" all'interno del void loop

//LIBRERIE**************************************************************************
#include<Wire.h>     
#include<LiquidCrystal_I2C.h>  
//IMPOSTAZIONI INIZIALI E VARIABILI GLOBALI********************************************
LiquidCrystal_I2C LCD(0x27, 16, 2);
int BPM = 25;                       
int prevBPM = 25;                   
int BEAT = 1;                     
int currentBEAT = 1;                  
int DIVISION = 1;                    
long unsigned currentTime = 0;       
long unsigned previousTime = 0;    
//PIN MAPPING***********************************************************************
const int BUZZER = 2;             
const int RLED = 5;               
const int GLED = 3;               
const int BLED = 4;               
const int POTENZIOMETRO = A0;    
const int PULSANTE1 = 6;         
const int PULSANTE2 = 7;          
const int PULSANTE3 = 8;         
const int PULSANTE4 = 9;          
const int PULSANTE5 = 10;        
const int PULSANTE6 = 11;         
const int PULSANTE7 = 12;        
//SETUP*****************************************************************************
void setup() {
  LCD.begin();               
  LCD.backlight();          
  pinMode(PULSANTE1, INPUT); 
  pinMode(PULSANTE2, INPUT);  
  pinMode(PULSANTE3, INPUT);  
  pinMode(PULSANTE4, INPUT); 
  pinMode(PULSANTE5, INPUT);  
  pinMode(PULSANTE6, INPUT); 
  pinMode(PULSANTE7, INPUT); 
  pinMode(RLED, OUTPUT);     
  pinMode(BLED, OUTPUT);    
  pinMode(GLED, OUTPUT);     
  pinMode(BUZZER, OUTPUT);  
}
//LOOP*****************************************************************************
void loop() {
  LEGGI();      //legge gli ingressi (pulsanti e potenziometro)
  ATTUA();      //esegue le azioni (suoni e luci)
  STAMPA();     //stampa un output (schermo)
}
//STAMPA***************************************************************************
void STAMPA() {
  LCD.setCursor(0, 0);                                                                                           
  LCD.print(BPM); LCD.print(" BPM in "); LCD.print(BEAT); LCD.print("/"); LCD.print(DIVISION); LCD.print("  "); 
  LCD.setCursor((currentBEAT -1), 1); LCD.print(currentBEAT); LCD.print("               ");                       
}
//ATTUA*****************************************************************************
void ATTUA() {
  float Intervallo = 60000 / BPM;                                           
  currentTime = millis();                                                    
  if(currentTime - previousTime >= Intervallo){                              
    digitalWrite(GLED, HIGH);                                                
    previousTime = currentTime;                                              
  }
  else {
    digitalWrite(GLED, LOW);                                                 
  }
}
//LEGGI*****************************************************************************
void LEGGI() {
  int StateBPMa1 = digitalRead(PULSANTE1);      
  int StateBPMa5 = digitalRead(PULSANTE2);      
  int StateBPMt1 = digitalRead(PULSANTE3);       
  int StateBPMt5 = digitalRead(PULSANTE4);      
  int StateABILITA = digitalRead(PULSANTE5);    
  int StateDIVISION = digitalRead(PULSANTE6);   
  int StateBEAT = digitalRead(PULSANTE7);       
  if(StateABILITA == HIGH) {
    int Cursore = analogRead(POTENZIOMETRO);      
    BPM = map(Cursore, 5, 1020, 25, 260);       
  }
  else {
    BPM = prevBPM;                                
  }
  if(StateBPMa1 == HIGH) {
    BPM = BPM + 1;                                
    delay(100);                                  
  }
  if(StateBPMa5 == HIGH) {
    BPM = BPM + 5;                                
    delay(100);                                   
  }
  if(StateBPMt1 == HIGH) {
    BPM = BPM - 1;                                
    delay(100);                                  
  }
  if(StateBPMt5 == HIGH) {
    BPM = BPM - 5;                                
    delay(100);                                   
  }
  if(BPM >= 261) {
    BPM = 260;                                     
  }
  if(BPM <= 25) {
    BPM = 25;                                    
  }
  prevBPM = BPM;                                  
  if(StateDIVISION == HIGH) {
    DIVISION = DIVISION + 1;                      
    delay(100);                                   
  }
  if(StateBEAT == HIGH) {
    BEAT = BEAT + 1;                            
    delay(100);                                  
  }
  if(DIVISION >= 17 || DIVISION <= 0) {
    DIVISION = 1;                                
  }
  if(BEAT <= 0 || BEAT >= DIVISION+1) {
    BEAT = 1;                                     
  }
}

Probabilmente è questo:
https://forum.arduino.cc/index.php?topic=500503.msg3414558#msg3414558

Per due grandi motivi

Nessuno dei due facilmente risolvibile

1
A causa del fatto che noi ragioniamo in base decimale usiamo i millesimi di secondo, ma siccome arduino ragiona in base 2 il conto non sarà mai giusto

Tipicamente un millesimo di secondo di arduino dura 1024 microsecondi invece che 1000

2
Comunque arduino (almeno le versioni più diffuse) non usa un oscillatore a cristallo, ma un semplice ristoratore ceramico
Questo significa che la precisione della misura dei tempi è scarsa, tanto da far sì che si possa ignorare il problema 1

Comunque anche un oscillatore a cristallo se non è fatto come si deve con componenti non selezionati e non termostatato non è poi così preciso

E quindi che fare?
O rinunciare e prendere un attrezzo specifico oppure prendere un rtc che abbia un uscita di clock da usare per sincronizzare arduino

Grazie mille a entrambi.
Sempre per mantenerlo più complicato del necessario e prendendo spunto da un accenno presente nell'altro topic, se faccio rilevazioni multiple sulla differenza di tempo e ci tiro fuori un'equazione semplice che mi riporti il valore del tempo "arduino" al tempo "reale", potrebbe ipoteticamente funzionare? Mi servono 5 minuti di precisione molto alta (minimo 2 minuti di precisione) dopo si può resettare/spegnere tranquillamente.

Salvorhardin:
A causa del fatto che noi ragioniamo in base decimale usiamo i millesimi di secondo, ma siccome arduino ragiona in base 2 il conto non sarà mai giusto Tipicamente un millesimo di secondo di arduino dura 1024 microsecondi invece che 1000

:o
Ma sicuro? Dove lo hai visto?

arduino (almeno le versioni più diffuse) non usa un oscillatore a cristallo, ma un semplice ristoratore ceramico Questo significa che la precisione della misura dei tempi è scarsa

Il problema è sicuramente questo. Va usato un RTC se si vogliono tempi precisi al millisecondo.

("ristoratore ceramico"? :o ;D )

alla fine ho deciso che è meglio comprarsi un rtc esterno: andare a modificare il valore di millis() in una variabile esterna è piuttosto inutile anche se corregge parzialmente l'errore (di un 20% a spanne, troppo basso), e ridurre il valore dell'intervallo non va a risolvere in nessun modo l'errore in questione.

ho riscritto la parte interessata in questo modo, sembra più corretto e funzionale

void ATTUA() {
  float Intervallo = 60000 / BPM;                                            
  if(millis() - previousTime >= Intervallo){                                  
    digitalWrite(GLED, HIGH);                                               
    previousTime = millis();
  }
  else {
    digitalWrite(GLED, LOW);                                              
}

non so come non ho fatto a non vedere l'altro topic, forse cercavo con parole chiavi sbagliate -.-

Con un misero oscillatore ceramico, usando millis in modo corretto si ha un errore di circa cinque secondi all'ora... 0,4 secondi in cinque minuti.

docdoc:
:o
Ma sicuro? Dove lo hai visto?

("ristoratore ceramico"? :o ;D )

ristoratore ceramico è un "piatto da portata" ovviamente
in inverno anche una "zuppiera" va bene

che millis durasse 1024 microsecondi me lo sono inventato

o magari no, lo ricordavo

Massimo invece dice che basta leggere la documentazione
io però ho trovato solo:

circa vero metà pagina dove dice che il clock a 16MHz viene diviso da un prescaler impostato a 64
che fa girare un contatore HW da 8 bit
che ogni 256 gruppi di 64 16milionesimi di secondo scatena un interrupt
che conta i millis
che a casa mia fa 1024 microsecondi
non so da te......

PS ne ho altre tre pagine che dicono cose simili

Magari studiarsi il "core" per capire come funziona la cosa no? :grin:

// the prescaler is set so that timer0 ticks every 64 clock cycles, and the
// the overflow handler is called every 256 ticks.
#define MICROSECONDS_PER_TIMER0_OVERFLOW (clockCyclesToMicroseconds(64 * 256))

// the whole number of milliseconds per timer0 overflow
#define MILLIS_INC (MICROSECONDS_PER_TIMER0_OVERFLOW / 1000)

// the fractional number of milliseconds per timer0 overflow. we shift right
// by three to fit these numbers into a byte. (for the clock speeds we care
// about - 8 and 16 MHz - this doesn't lose precision.)
#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)
#define FRACT_MAX (1000 >> 3)
...
...
ISR(TIMER0_OVF_vect)
{
	// copy these to local variables so they can be stored in registers
	// (volatile variables must be read from memory on every access)
	unsigned long m = timer0_millis;
	unsigned char f = timer0_fract;

	m += MILLIS_INC;
	f += FRACT_INC;
	if (f >= FRACT_MAX) {
		f -= FRACT_MAX;
		m += 1;
	}

	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;
}

Guglielmo

64 clock cycle x 256 = 16384
Se il clock è 16MHz, allora 16 000 000 / 16384 = 976,5625us
1024 salta fuori così:

1/16 000 000 = 0,000000063 (nella realtà 0625 e no 063)
0,0000000625 x 64 = 0,000004 (62.5 ns e 4us)
0,000004 x 256 = 0,001024 (4us x 256 = 1024us)

Grazie, interessante!

Quindi se millis() in realtà non conta 1ms ma 1,024 ms, ha un errore in eccesso del 2.4% il che si somma all'imprecisione del clock interno (che se magari ci va bene che è inferiore di pari quantità alla frequenza base potrebbe accidentalmente essere quasi preciso :wink: ).
Trascurando quindi l'eventuale errore dell'oscillatore (e la soluzione RTC, che è sicuramente la migliore), per migliorare un poco la precisione basterebbe dividere per 1,024 gli intervalli richiesti? Ossia per intenderci fare "if (millis()-prevMillis > (intervallo*1000UL)/1024)"? :wink:

docdoc:
Grazie, interessante!

Quindi se millis() in realtà non conta 1ms ma 1,024 ms, ha un errore in eccesso del 2.4% il che si somma ...

Vedo che non vi siete dati la briga di guardare BENE il sorgente che ho messo ... ed in particolare l'uso della variabile 'f' che è utilizzata appunto per correggere l'errore ... ::slight_smile:

Guglielmo

@lionell
ma tu come fai a dire che l'app nel telefono è affidabile?

Guglielmo: se non ricordo male (non ho la rivista sottomano qui), sull'articolo del 1284P citavi la presenza di un circuito RTC all'interno, a cui bastava "dare in pasto" un quarzettino da orologio ... la precisione di quello, per quanto difficilmente potrebbe arrivare ad eguagliare un sistema professionale termostatato, dovrebbe essere comunque molto piu alta del semplice risuonatore, giusto ? ... solo come idea ... :wink:

So che usarlo per un banale metronomo puo sembrare esagerato, ma magari come esercizio didattico, e pure per incoraggiarne la diffusione ...

Poi lo stesso OP parla di sistema esagerato per un metronomo, quindi magari gli viene voglia di sfruttare la potenza extra per un display o qualche altra cosa ancora piu esagerata ... magai un direttore d'orchestra robotizzato "stile Gundam" :smiley: (scherzo, qui :wink: )

Etemenanki:

Guglielmo: se non ricordo male (non ho la rivista sottomano qui), sull'articolo del 1284P ...

Si, esatto, su due pin del ATmega1284P (TOSC1 e TOSC2, ovvero il 22 e 23 della schedina) puoi mettere il classico quarzo da 32'768KHz ed utilizzare il RTC (Real Timer Counter) del Timer 2 che opera anche in "power-save" ed avere un contatore piuttosto preciso.

Comunque quella schedina, come sicuramente ben ricordi, monta un bel quarzo anche per il clock di sistema e millis() è piuttosto precisa! :slight_smile:

Guglielmo

Grazie mille per la disponibilità
proverò ad utilizzare un quarzo esterno da 12Mhz, utilizzato per un vecchio progetto riguardante un atmega8p standalone.

nella scheda arduino che possiedo è montato un' atmega328-PU, ho letto che questa atmel può funzionare fino a 20Mhz dato che è alimentata a 5V, in futuro se avrò quarzi simili proverò.

Comunque, per collegarlo in modo poco cristiano, gli "infilo i piedini" del quarzo nelle fessure dov'è montata la scheda con i rispettivi condensatori o è illegale? :slight_smile:
non trovo pin TOSC1 e TOSC2 / XTAL1 e XTAL2 che si interfaccino "facilmente" con l'esterno, e non credo ci siano, per quello che ho cercato (rimuovere l'atmel dal socket è un'idea forse sbagliata che mi è venuta in mente).