Troppo discostamento Tempi 1284P + Quarzo 20ppm + RTC DS3231

Salve a tutto, ho un problema e chiedo aiuto a voi esperti.
Ho assemblato uno StandAlone con MCU ATMEGA1284P, monta un quarzo da 20MHz (con core adeguato) con margine di errore di +/- 20ppm e un RTC DS3231.
Ho bisogno di gestire dei cronometri accurati e mi aspettavo una accuratezza molto più alta sulla gestione del tempo da parte dell' MCU (tramite il millis()).
Facendo alcune prove mi trovo a perdere circa 15 secondi per ogni ora....credo sia tantissimo considerando il quarzo che sto usando.
Sto confrontando il tempo che passa con quello letto dall' RTC e mi ritrovo con questi valori:
Millis() / RTC
01:27:00 / 01:27:20
04:00:00 / 04:00:57

Vi posto il codice che sto usando, vorrei capire dove è il problema, magari mi sbaglio e quelle sono le tempistiche giuste, anche se come detto me lo sarei aspettato da un arduino che monta un risuonatore, ma non da uno StandAlone con quarzo 20ppm.

//------TEST PER VERIFICARE LA SINCRONIZZAZIONE DEI TEMPI TRA L' RTC E I TEMPI INTERNI VIA SOFTWARE ---------
#include <LiquidCrystal.h>
#include <DS3231.h>                                                                                       //Libreria per il RTC
#include <Wire.h>                                                                                         //Libreria per utilizzare la I2C, usata per il RTC e altro eventuale
//-------------------NOMENCLATURA PIN INPUT / OUTPUT--------------------------
const byte rs = 14, d4 = 0, d5 = 1, d6 = 2, d7 = 3;                                                       //Assegnazione pin per la gestione dei display
const byte enableDisplay1 = 29, enableDisplay2 = 28, enableDisplay3 = 27;                                 //Pin utilizzati per gli Enable dei 3 Display
const byte contrastPin = 12;                                                                              //è il pin dedicato alla gestione del costrasto, attraverso filtro RC : R=1K, C=100uF Elettrolitico
const byte lightPin = 13;                                                                                 //è il pin dedicato alla gestione della luminosità
//-------------------NOMENCLATURA PIN INPUT / OUTPUT--------------------------

//-------------------VARIABILI USATE DA RTC--------------------------
bool h12Flag = false;                                                                                     //Per settare il formato ora h12 o h24 (default)
bool pmFlag = false;                                                                                      //Nel caso di formato h12 per sapere se è AM o PM
bool century = false;

byte Anno = 0, Mese = 0, Giorno = 0, GiornoDellaSettimana = 0, Ore = 0, Minuti = 0, Secondi = 0;          //Variabili di Data e Orario gestito dall' RTC HardWare
byte OreSW = 0, MinutiSW = 0, SecondiSW = 0, MillisecondiSW = 0;                                          //Variabili di Orario gestito via Software

float TemperaturaInterna = 0;                                                                             //Temperatura interna dell' RTC 3231
char Data[10];                                                                                            //Stringa Stampabile che Rappresenta la Data completa
char Ora[10];                                                                                             //Stringa Stampabile che Rappresenta l' Orario completo
char OraSW[10];                                                                                           //Stringa Stampabile che Rappresenta l' Orario Software completo

//-------------------VARIABILI USATE DA RTC--------------------------

//-------------------VARIABILI DI GESTIONE --------------------------
word livelloContrasto = 0;                                                                                //è il livello del contrasto, PWM da 0 = Massimo Contrasto a 255 = Minimo Contrasto
word livelloLuminosita = 255;                                                                             //è il livello dela luminosità, PWM da 0 = Minima luminosità a 255 = Massima Luminosità
//-------------------VARIABILI DI GESTIONE --------------------------

//-------------------  CREAZIONE OGGETTI   --------------------------
DS3231 Clock;                                                                                             //Inizializza una procedura per il RTC
RTClib Clock2;                                                                                            //Inizializza un altra procedure, sempre per il RTC, con altre funzioni avanzate
DateTime Adesso;                                                                                          //Creo l' Oggetto "Adesso" con il formato di "DateTime" impostato nella libreria, conterrà la data e orario dalla funzione "now"
LiquidCrystal Display1(rs, enableDisplay1, d4, d5, d6, d7);                                               //4 BIT SENZA ASSEGNAZIONE DEL PIN R/W (A GND GLI ALTRI 4)
LiquidCrystal Display2(rs, enableDisplay2, d4, d5, d6, d7);                                               //4 BIT SENZA ASSEGNAZIONE DEL PIN R/W (A GND GLI ALTRI 4)
LiquidCrystal Display3(rs, enableDisplay3, d4, d5, d6, d7);                                               //4 BIT SENZA ASSEGNAZIONE DEL PIN R/W (A GND GLI ALTRI 4)
//-------------------  CREAZIONE OGGETTI   --------------------------

void setup() {      //------------ SETUP ------------ SETUP ------------ SETUP ------------ SETUP ------------ SETUP ------------ SETUP ----

  //-------------------  ASSEGNAZIONE PIN INPUT / OUTPUT   --------------------------
  pinMode(contrastPin, OUTPUT);                                                                           //è il pin dedicato alla gestione del costrasto, attraverso filtro RC : R=1K, C=100uF Elettrolitico
  pinMode(lightPin, OUTPUT);                                                                              //è il pin dedicato alla gestione della luminosità, attraverso R330 e LS7407
  //-------------------  ASSEGNAZIONE PIN INPUT / OUTPUT   --------------------------
  Wire.begin();                                                                                           //Inizializza la comunicazione I2C per il RTC ed altro eventuale
  Display1.begin(8, 2);
  Display2.begin(8, 2);
  Display3.begin(8, 2);
  analogWrite(contrastPin, livelloContrasto);                                                             //Setta il CONTRASTO
  analogWrite(lightPin, livelloLuminosita);                                                               //Setta la LUMINOSITA'
  //--------- SINCRONIZZAZIONE DELL' RTC CON IL TEMPO INTERNO SOFTWARE (LEGGERMENTE DISCOSTATO) ----------
  Clock.setHour(0);
  Clock.setMinute(0);
  Clock.setSecond(millis() / 1000);
/*
  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
  */
}

void loop() {       //--------------- LOOP --------------- LOOP --------------- LOOP --------------- LOOP --------------- LOOP --------------- LOOP ----
  long istante = millis();                                                                              //Legge l' orario Software
  Adesso = Clock2.now();                                                                                //Legge l' orario Hardware

//--------------GESTIONE ORARIO SOFTWARE---------------------
  long SecondiTemp = istante / 1000;                                                                    //Rappresenta il numero TOTALE dei Secondi Passati
  long MinutiTemp = SecondiTemp / 60;                                                                   //Rappresenta il numero TOTALE dei Minuti Passati
  long OreTemp = MinutiTemp / 60;                                                                       //Rappresenta il numero TOTALE delle Ore Passate
  MillisecondiSW = istante % 1000;                                                                      //Millisecondi SW
  SecondiSW = SecondiTemp % 60;                                                                         //Secondi SW
  MinutiSW = MinutiTemp % 60;                                                                           //Minuti SW
  OreSW = OreTemp % 24;                                                                                 //Ore SW
  sprintf(OraSW, "%02d:%02d:%02d", OreSW, MinutiSW, SecondiSW);                                         //Inserisce in una stringa l' orario per poi visualizzarlo
  Display2.setCursor(0, 1);
  Display2.print(OraSW);

//--------------GESTIONE ORARIO HARDWARE DA RTC---------------------
  sprintf(Data,   "%02d/%02d/%d", Adesso.day(), Adesso.month(), Adesso.year());                         //Inserisce in una stringa la data per poi visualizzarla
  sprintf(Ora,  "%02d:%02d:%02d", Adesso.hour(), Adesso.minute(), Adesso.second());                     //Inserisce in una stringa l' orario per poi visualizzarlo
  Display3.setCursor(0, 0);
  Display3.print( Data );
  Display3.setCursor(0, 1);
  Display3.print( Ora );
}

Prova ad usare micros()

con micros() perdo 4 secondi al minuto :astonished:
mi sta sfuggendo qualcosa, forse nel formato delle variabili o dei calcoli..

EDIT: no anche escludendo tutti i calcoli e trasformazione in string non cambia nulla.
Semplicemente visualizzando i "micros()" e i secondi dell' RTC vedo un discostamento notevole

Forse non è il problema, ma le variabili per millis vanno dichiarate unsigned long.

No non è quello il problema, ho anche provato a impostarle come "unsigned long" ma non cambia nulla.
c'è proprio un errore di frequenza.
Ho provato, giusto per fare delle prove, a ingannare l' MCU:
impostando il ".build.f_cpu=20000000L" ho che il millis mi perde 4 millisecondi reali al secondo (quindi è più lento)..
impostando il ".build.f_cpu=19999999L" ho che il millis mi recupera 45 millisecondi reali in più al secondo(quindi è più veloce)..
Non potendo mettere valori con decimali non posso ingannarlo in questo modo (ma nemmeno vorrei risolvere in questo modo, era solo una prova).
Non so se ci sono comandi a livello di compilazione che possano modificare parametri interni per variare la frequenza di quel poco che serve, facendo quindi una taratura.
Allora facendo i calcoli giusti ho provato a moltiplicare la variabile che richiama il "millis()" per lo scostamento, quindi : millis() * 1.004.
In questo modo sono riuscito a sincronizzarlo quasi alla perfezione (basterebbe raffinare con i decimillesimi facendo test lunghi....)..
Però lo vedo come un paliativo.... vorrei capire perchè con questo hardware e con con un quarzo da +/-20 ppm ho così tanta differenza dalla realtà :relieved:

Per essere sicuro che il problema non sia di natura software, invece di usare milllis(), perché non provi ad impostare un timer hardware per i secondi?
L'ideale però sarebbe usare un oscillatore che consente di ottenere 1Hz preciso tipo da 19.6608MHz.

volatile unsigned long SecondiTemp = 0;


void setupTimer1() {
  noInterrupts();
  // Clear registers
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;

  // Con 20Mhz, questo è il valore più vicino ad 1 Hz
  // 1.0000128001638422 Hz (20000000/((19530+1)*1024))
  OCR1A = 19530;   
  TCCR1B |= (1 << WGM12);               // CTC 
  TCCR1B |= (1 << CS12) | (1 << CS10);  // Prescaler 1024  
  TIMSK1 |= (1 << OCIE1A);              // Output Compare Match A Interrupt Enable
  interrupts();
}

void setup() {
  setupTimer1();
}

void loop() {
......
.....
}

ISR(TIMER1_COMPA_vect) {
  SecondiTemp++;
}

ok, faccio questa prova, spero di capirne bene il funzionamento.
ora provo, se ho difficoltà chiedo aiuto :slight_smile:

OK, implementata la procedura, contemporaneamente controllo il RTC, il Timer Hardware e il millis, il risultato è questo:
Dopo 1000 secondi (ho fatto una prova rapida, ne farò una altra più lunga) mi ritrovo il Timer Hardware perfettamente (per quello che posso notare su display) sincronizzato con l' RTC (che segna 1000 e apparentemente nello stesso istante del Timer Hardware), mentre il millis mi segna 996 (e mi ritrovo con lo stesso 4 su 1000 con i precedenti calcoli).
Quindi mi pare di intuire che a livello Hardware l' MCU lavora perfettamente sincronizzato e il quarzo da 20MHz sta facendo egregiamente il suo lavoro, giusto?
E a questo punto perchè il millis mi sballa così tanto? cosa posso fare per sistemare questa cosa?
Il Timer Hardware mi è servito come prova ma penso non risolva il mio problema inquanto ho bisogno dei millisecondi, ho paura che mi interferisca con altre librerie, con altri interrupt etc... e ad ogni modo ho già l' RTC che mi scandisce i secondi.
A questo punto dovrei incrociare il millis con la lettura dell' RTC e sincronizzarli, ma sarebbe comunque un ripiego e piuttosto articolato.
Vorrei capire perchè il millis si comporta in questo modo e risolverlo :sweat_smile:

Ciao contestatnt, intanto ti ringrazio per i consigli, come ho detto implementando il Timer Hardware la sincronizzazione è quasi perfetta, dopo 40000 secondi sono ancora allineati, mentre il millis ne ha persi ben 155. Ho aggiunto un moltiplicatore a una variabile che legge il millis e anche questa è piuttosto allineata dopo le 40000 ore.

Vorrei capire perchè la millis è così imprecisa (molto di più da quello che mi aspettavo con il Quarzo usato) rispetto alla funzione con il Timer Hardware (anche la millis dovrebbe lavorare con un Timer Hardware, il T0 se non erro).

Per quanto riguarda quello che voglio fare, timer e countdown, a questo punto penso che le soluzioni sono 2, usare il millis ricalibrato via software con la costante di correzione, oppure integrare l' RTC (da cui leggo i secondi) con la funzione millis per calcolare i millisecondi all' interno dei secondi letti dall' RTC.
Cosa mi consigliate?
Grazie ancora

Considerando che hai bisogno di accuratezza, io rimarrei sui Timer hardware e come già detto oscillatore con la frequenza di 19.6608MHz.

Il micro che hai scelto ne ha 3 disponibili se non sbaglio e il Timer2 ad esempio non dovrebbe influenzare il funzionamento di altro.

A quel punto hai due opzioni

  • impostare il timer in modo che ti dia la base tempi con cui poi effettuare i conteggi e quindi nel tuo caso 1 millisecondo,
  • oppure leggere il timer quando serve e fare la differenza con la lettura precedente (gestendo l'eventuale overflow del timer con il relativo interrupt).

Gentilissimo :+1:
Ok, faccio delle prove con il Timer 2 come mi hai consigliato, con risoluzione al millesimo.
I miei dubbi sul suo uso erano in merito a eventuali interferenza con altri componenti che uso (è un dispositivo complesso), interferenze con altre librerie, interferenze con gli altri interrupt che utilizzo etc. Ad ogni modo faccio dei test e verifico la funzionalità del tutto, a questo punto l' RTC lo utilizzo solo come calendario.

Ribadisco però la mia perplessità sulla funzione millis, spero che qualcuno mi illumini sulla possibile causa.
La millis dovrebbe appoggiarsi al Timer0 se non erro e visto che le prove fatte con il Timer1 (come sopra) hanno dato risultati eccellenti non capisco perchè la millis (che dovrebbe lavorare in maniera anagola con il timer Hardware) da un errore così grande.
Ho paura che ci sia qualche errore nel settaggio dell' Hardware, dell' MCU.

Vorrei capirne il motivo, anche se risolvo in maniera differente sono comunque testardo e voglio capire il perchè non si comporta come invece dovrebbe :sweat_smile:
Intanto grazie mille

... basta che vai a leggerti i sorgenti nel "core" per capire che comunque è una cosa approssimata.

// 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)

volatile unsigned long timer0_overflow_count = 0;
volatile unsigned long timer0_millis = 0;
static unsigned char timer0_fract = 0;

#if defined(TIM0_OVF_vect)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
	// 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++;
}

unsigned long millis()
{
	unsigned long m;
	uint8_t oldSREG = SREG;

	// disable interrupts while we read timer0_millis or we might get an
	// inconsistent value (e.g. in the middle of a write to timer0_millis)
	cli();
	m = timer0_millis;
	SREG = oldSREG;

	return m;
}
...
...

Guglielmo

Grazie Guglielmo, so bene che è approssimata, ma a me è esageratamente errata, in realtà è quello che ho fatto stamattina, sono proprio andato a vedere come era articolata per capirne il funzionamento e le differenze con la funzione del Timer che ho usato ma sono onesto, non ho capito bene come funziona, non conosco bene l' uso diretto dei registri interni. In realtà anche la funzione del Timer 1 che mi ha passato @cotestatnt non l' ha riesco a decifrare del tutto, non conoscendo nello specifico i registri e le loro funzioni.
Anche usare il timer2 (o forse ancor meglio il 3) non mi è così facile, sto cercando di documentarmi sul Datasheet dell' MCU, cerco di approfondire le conoscenze studiando anche quello che trovo pronto perchè mi piace capire come funzionano le cose, ma ovviamente ho delle limitazioni.

Volevo proprio capire perchè con 4 righe usando il Timer1 riesco ad avere una precisione ottimale mentre la millis che sempre usa un Timer Hardware e che ideologicamente parlando dovrebbe dare il meglio che può (nelle sue limitazioni) invece sfarfalla alla grandissima, più di quello che dovrebbe.
E' questo quello che mi sfugge :grin:
Grazie sempre dell' aiuto

EDIT1: Domanda stupida, ma meglio chiedere :sweat_smile:, Considerando che i Timer condividono i pin fisici dell' MCU... ad esempio il Timer 3 condivide gli I/O del bus SPI sui pin digitali 5-6-7, posso usare altro su quei pin? posso usare il bus SPI per esempio per altri dispositivi? o potrei avere conflitti?

EDIT2 (mentre studio il Datasheet): La differenza di comportamento può dipendere dal fatto che il Timer0 (che usa il millis) e il Timer2 sono a 8bit mentre il Timer1 (e il Timer 3 che sto provando ora) sono a 16bit?

Se non riesci a risolvere in altro modo, potresti anche incrementare di un secondo ogni 4 minuti, oppure copiare millis() in un'altra variabile incrementandola di 100 ogni 24 secondi. In questo modo puoi anche fare aggiustamenti molto precisi.

Si in realtà avevo fatto una cosa similare ma in tempo reale, perchè la correzione deve essere continua e non ogni un tot di tempo, come scritto precedentemente, ho calcolato il fattore di correzione su un arco di tempo di circa 24 ore, avendo un risultato di 1,0039718.
Ad ogni lettura del millis moltiplicavo la variabile per questo fattore di correzione. Devo dire che la cosa è molto precisa, ma non mi piace assolutamente come soluzione, sarebbe solo un brutto ripiego.
A questo punto ho utilizzato il Timer 3, settato il prescaler a 8 per avere massima accuratezza per una risoluzione ottimale di 100Hz (1 centesimo di secondo che mi basta come risoluzione).
Testando solo con uno sketch per valutare i tempi sembra funzionare alla perfezione, molto preciso, ma devo fare dei test più a lungo termine, vi posto il settaggio del Timer così mi date conferma se tutto è corretto.

void setupTimer3() {                                                                      //Timer 3 HardWare per ottenere i centesimi di secondo
  noInterrupts();                                                                         //Interrompo gli Interrupt
                                // Azzero i Registri del Timer 3
  TCCR3A = 0;                                                                             //TCCRnA, la "n" identifica quale Timer viene usato (sul 1284 ci sono 4 Timero (0-3)
  TCCR3B = 0;                                                                             //TCCRnB, la "n" identifica quale Timer viene usato (sul 1284 ci sono 4 Timero (0-3)
  TCNT3 = 0;                                                                              //TCNTn,  la "n" identifica quale Timer viene usato (sul 1284 ci sono 4 Timero (0-3)

  //Formula OCR3A : [(Frequenza Quarzo / Prescaler) -1] / Herz Conteggio 
  // Con 20Mhz, per avere 100Hz : ((20.000.000 / 8) - 1) / 100 = 24.999,99 (approssimato a 25.000)

  TCCR3B |= (1 << WGM32);                 // CT                                           //WGMn2, la "n" identifica quale Timer viene usato (sul 1284 ci sono 4 Timero (0-3)
  OCR3A = 25000;                          //x 100 Hz, ris. 1 cent. di secondo             //OCRnA, la "n" identifica quale Timer viene usato (sul 1284 ci sono 4 Timero (0-3)   
  TCCR3B |= (1 << CS31);                  // Prescaler 8                                  //CSn2 - CSn1 - CSn0, la "n" identifica quale Timer viene usato (sul 1284 ci sono 4 Timero (0-3)  

  TIMSK3 |= (1 << OCIE3A);                // Output Compare Match A Interrupt Enable      //OCIEnA, la "n" identifica quale Timer viene usato (sul 1284 ci sono 4 Timero (0-3)
  interrupts();
}
//----------------------------------------------------------------------------------------------
ISR(TIMER3_COMPA_vect) {                                                                  //Funzione richiamata dal Timer 3 in funzione del Setup del Timer
  cantesimiSecondiTimer3Hardware = cantesimiSecondiTimer3Hardware+ 10;                    //lo incremento di 10 per portarlo a "millesimi" e compararlo con la millis
}

Sto facendo test con vari settaggi dei Timer.
Al momento sto facendo test lunghi con 2 timer contemporanei (T1 e T3 che sono a 16bit) con settaggi di prescaler differenti, per avere una risoluzione di 1 millesimo e 1 centesimo di secondo.
Ho sviluppato una semplice tabella in Excel dove basta inserire la frequenza del QUARZO e la frequenza che si desidera ottenere dal Timer e si ottengono i settaggi da utilizzare per le impostazioni del Timer per ogni Prescaler disponibile, visualizzando la frequenza reale ottenuta e la precisione (più è prossima a 1 e migliore è) .
In questo modo si può scegliere il settaggio ottimale.
Magari può servire a qualcuno :wink:
Calcolo solo per PRESCALER MCU.zip (8.4 KB)

Su un Arduino con risuonatore ceramico, millis sbaglia di circa 5 secondi all'ora (anche se la singola lettura è da considerare affetta da un jitter di +/- 2ms non cumulativo). Con un oscillatore quarzato dovrebbe fare molto meglio.

Ciao
Stavo giocando un po' con il file di Excel per il calcolo e ho notato che se desidero 1000Hz ottengo 999,95, ma se desidero 999,95 ottengo 999,90! C'è qualcosa che non va...

Calcolo solo per PRESCALER MCU.zip (8.6 KB)

Il calcolo è giusto.
Il fatto è che non sempre (quasi mai) puoi ottenere nella realtà la frequenza che desideri.
Le configurazioni possibili dei registri non sono molte e di conseguenza non puoi ottenere un ventaglio di valori troppo grande.
Se vuoi avere una frequenza di 1000Hz la migliore precisione la ottieni con il Prescaler settato a "1" e OCRnA a 20000, ma hai comunque un margine di errore, infatti otterrai una frequenza REALE di 999,95000250Hz, con un errore di circa -50ppm.
Se imposti la frequenza desiderata a 999,95Hz otterrai la migliore precisione sempre con Prescaler a "1" ma con OCRnA a 20001, ottenendo una frequenza REALE di 999,90001000Hz, sempre con un errore di circa -50ppm.
Tutto è corretto, non devi ottenere il contrario se inverti i valori.

Uhmm... Se impostando 999,95 ottenessi 999,95000250Hz non sarebbe meglio?... E' solo questione di approssimazioni! :slight_smile: