Precisione Millis()

Fonte di quanto segue

https://www.gammon.com.au/forum/?id=12127&reply=1#reply1

A causa del modo in cui funziona il codice della libreria, il numero restituito da millis () andrà lentamente alla deriva.

Supponendo un orologio da 16 MHz:

C’è un interrupt (Timer 0 overflow interrupt) chiamato ogni 1024 μs che aggiorna la variabile usata da millis (). Pertanto millis () uscirà di 24 μs dopo il primo interrupt, 48 μs dopo il secondo interrupt e così via. Il codice alla fine compensa quindi questa inesattezza non è cumulabile nel tempo.

In altre parole, millis () funzionerà lentamente (dovrebbe aggiornarsi ogni 1000 μs ma in realtà aggiorna ogni 1024 μs).

Tuttavia l’interrupt di overflow, che viene chiamato ogni 1024 μs, tiene traccia della quantità che è fuori, e alla fine aggiunge uno al millis () conteggio per recuperare (e riduce l’importo di overflow per compensare). Ciò avverrà all’incirca ogni 42 interruzioni di overflow. A questo punto, ovviamente, il conteggio restituito da millis () “salterà” quando viene aggiunta questa quantità di compensazione aggiuntiva.

In dettaglio, aggiunge 3 (FRACT_INC) a una variabile chiamata timer0_fract ogni overflow. Continua a farlo finché timer0_fract è> = 125 (FRACT_MAX). Quando ciò accade aggiunge 1 al contatore millis (timer0_millis) e sottrae 125 da timer0_fract. Poiché 125/3 è 41,67 questo succede ogni 42 overflow o giù di lì. (E poiché stiamo aggiungendo uno ogni 42 overflow, significa che ne stiamo aggiungendo uno ogni 42 * 24 μs, ovvero ogni 1008 μs).

Se si eseguono piccoli intervalli di sincronizzazione, micros () sarà molto più accurato, dato che legge direttamente dall’hardware e non soffre di questo errore di scorrimento. Tuttavia si avvolge dopo circa 71 minuti. Inoltre, micros ha una risoluzione di 4 μs (non 1 μs) a causa del modo in cui è configurato il timer. (Conta fino a 256, utilizzando un prescaler di 64. Un ciclo di clock è 62,5 nS, quindi “spunta” ogni 4 μs e overflow ogni 256 * 4 μs).

Non vorrei essere troppo semplicistico, ma mi sembra molto più semplice ed accurato inizzializzare il Timer0 a 6 cosichè possa contare 250 x 4uS = 1000uS giusti.

Che ne pensate ?

Non conosco i dettagli di funzionamento dei timer sull'ATmega, ma sui PIC famiglie 12/16 ogni precaricamento del registro timer azzera il prescaler, causando un errore ancora maggiore e non deterministicamente prevedibile. L'unico modo per ottenere un tempo periodico è lasciarli conteggiare in free running secondo una qualche potenza di due.

Ciao Claudio

Conosco i Pic e so che la scrittura del timer comporta l’ azzeramento del prescaler ma non sembra che questo accada sull’ ATMEGA fra l’ altro tutti i timer hanno anche il contatore di comparazione.

Quindi si potrebbe usare anche quello per avere un conteggio preciso sino a 250.

http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega328_P%20AVR%20MCU%20with%20picoPower%20Technology%20Data%20Sheet%2040001984A.pdf

Features
• Two Independent Output Compare Units
• Double Buffered Output Compare Registers
• Clear Timer on Compare Match (Auto Reload)
• Glitch Free, Phase Correct Pulse Width Modulator (PWM)
• Variable PWM Period
• Frequency Generator
• Three Independent Interrupt Sources (TOV0, OCF0A, and OCF0B)

Overview
Timer/Counter0 (TC0) is a general purpose 8-bit timer/counter module, with two independent output
compare units, and PWM support. It allows accurate program execution timing (event management) and
wave generation.

Allego diagramma a blocchi

Prescaler Reset
The prescaler is free-running, i.e., it operates independently of the clock select logic of the timer/counter,
and it is shared by timer/counter1 and timer/counter0. Since the prescaler is not affected by the timer/
counter’s clock select, the state of the prescaler will have implications for situations where a prescaled
clock is used. One example of prescaling artifacts occurs when the timer is enabled and clocked by the
prescaler (0x06 > CSn[2:0] > 0x01). The number of system clock cycles from when the timer is enabled to
the first count occurs can be from 1 to N+1 system clock cycles, where N equals the prescaler divisor (8,
64, 256, or 1024).
It is possible to use the prescaler Reset for synchronizing the timer/counter to program execution.
However, care must be taken if the other timer/counter that shares the same prescaler also uses
prescaling. A prescaler Reset will affect the prescaler period for all timer/counters it is connected to.

Allego Diagramma Prescaler

Devo dire che mi sembra un bel micro fra l’ altro il clock non viene diviso per quattro
come nei pic quindi con clock a 16 MHZ gira a 16 non a 4 e la maggior parte delle istruzioni
è single cycle alcune a due come per i pic.

NON mi sono messo a verificare cosa succede cambiando le impostazioni (... non ho molto tempo :confused: ), ma ...
... stai tenendo conto che in realtà millis() è solo una cosa ricavata dal vero uso principale del timer 0 il cui compito è generare il corretto segnale PWM (fast hardware PWM a 976.5625 Hz) sui pin 5 e 6 ?

Come timer 1 viene usato per il 8-bit phase correct pwm, a 488.28125 Hz, sui pin 9 e 10 e come timer 2 viene usato, per la stessa cosa, sui pin 3 e 11.

Guglielmo

Proprio perchè ne tengo conto se millis fosse corretta avresti un interrupt dal timer 0 ogni 1000uS
e non ogni 1024uS

In questo modo il Fast hardware pwm avrebbe una frequenza di 1000 HZ
In questo modo la frequenza del pwm del timer 1 sarebbe a 500 HZ

la delay sarebbe di 1ms e non di 1,024ms
la delayMicrosecond sarebbe di 1uS e non di 1,024uS

E' vero che sono tempi irrisori e le variazioni di frequenza da quella teorica sono ininfluenti
ma se è possibile non introdurre a priori errori generati dal software tanto meglio.
Ci sono già quelli generati dalle tolleranze dei componenti, perchè aggiungerne altri ?

Fra l' altro anche la gestione dell' interruzione del timer 0 sarebbe più semplice e più veloce
e le modifiche da apportare sarebbero minime in quanto serve solo inizializzare il timer0 al valore giusto
sia nella routine di interruzzione che nella preparazione dei registri del timer0

Andrebbero messi a posto tutte le situazioni che tengono conto del valore attuale di 1024.

... hai provato a segnalare la cosa su GitHub proponendo una modifica del "core"? Ti hanno dato qualche spiegazione del perché sia stata fatta la scelta che è stata fatta? ... sarebbe interessante indagare e non dovrebbe essere difficile modificare wiring.c per implementarle la cosa :wink:

Guglielmo

P.S.: ... relativamente a :

PaoloF_60:
Andrebbero messi a posto tutte le situazioni che tengono conto del valore attuale di 1024.

... credo che tutto sia, appunto, dentro a wiring.c qundi non dovrebbe essere complesso ::slight_smile:

No non ho provato perchè non sapevo neanche a chi chiedere e tantomeno a chi chiedere una eventuale
modifica ho trovato alcune informazioni sul core di arduino ma ne vorrei avere di più. Mi sono anche comprato il libro di Leonardo Milani perchè ho creduto che ne parlasse ma non è così. Il libro sono comunque contento di averlo comprato. Ho visto la wiring.c ed in effetti la maggior parte delle modifiche
sarebbero da fare li. Oggi non ho tempo devo andare via ma stasera quando torno provo a fare due misure
serie ed eventualmente provo una modifica della sola routine di interruzione per vedere come cambia.

Dispongo di un Tektronix 224 con interfaccia seriale parallela e non ricordo che altro. Ho letto GPIB
dispone di un connettore Centronics come quello usato sulle stampanti parallele di una volta.
Altrimenti posso sempre fargli una foto col cellulare.

Se hai link da segnalarmi sul core di arduino te ne sono grato.

PaoloF_60:
Se hai link da segnalarmi sul core di arduino te ne sono grato.

Arduino è su Github ... QUI ...
... puoi aprire issues e pull-requests :wink:

Guglielmo

Sono 2 cose separate che convogliano nella precisione dei millis()
Per primo la parte codice.
per seconod (cosa che hai ignorato finora) é la precisione del clock. Questa é pessima se viene usato un risonatore come sui Arduino UNO e MEGA e si aggira intorno ai 0,5% contro un 0,01% se hai uno quarzo.

Ciao Uwe

Non è vero che lo ignorata, ma non vedo perchè aggiungerne altre nel software.

E' vero che sono tempi irrisori e le variazioni di frequenza da quella teorica sono ininfluenti
ma se è possibile non introdurre a priori errori generati dal software tanto meglio.
Ci sono già quelli generati dalle tolleranze dei componenti, perchè aggiungerne altri ?

Io sul mio Arduino UNO R3 e sulla Mega 2650 R3 vedo un quarzo.

... in ogni caso, credo che la ragione fondamentale della scelta originale, fatta a suo tempo da chi ha scritto il "core", sia comunque legata al PWM (... e come esso è utilizzato in Arduino) ... appena trovo il tempo, mi metto a verificare.

Guglielmo

Anche perché se si necessitano temporizzazioni "serie" conviene usare altre sorgenti.

PaoloF_60:
Non è vero che lo ignorata, ma non vedo perchè aggiungerne altre nel software.

Io sul mio Arduino UNO R3 e sulla Mega 2650 R3 vedo un quarzo.

Anch io li vedo ma sono attacati al ATmega16U2. Il ATmega328 e il ATmega2560 hanno un risuonatore.

L’ errore dato dal inprecisione del clock non puoi correggere via Software perché non sai quantificarlo. Ok potresti misurare una frequenza molto precisa di quanto si scosta dalla frequenza nominale, ma a questo punto derivi la frequenza di clock da questa fonte molto precisa ( per esempio la frequenza del segale DCF77 sono precisamente 77500Hz derivate dal orologio atomare).

Ciao Uwe

Sono stato un po’ impegnato … innanzi tutto ho verificato lo schema elettrico di Arduino Uno e effettivamente il quarzo non è per il 328 per lui c’è un risuonatore.

Ma torniamo al discorso ho effettuato prove e misure sulle temporizzazioni e sul pwm pin 5 e 6.

la delayMicroseconds(uS) per tempi molto bassi da 1 a 20 uS introduce errori di circa +3,2uS
che si riducono a 2uS intorno ai 50uS, per tempi più lunghi l’ errore rimane invariato.
Col valore 1 si hanno 4,4uS daltra parte il prescaler da impulsi al timer0 ogni 4uS.

La delay(mS) introduce da 10uS a 24uS di errore per valori da 1 a 100 rimangono invariati a salire.

Il pwm sul pin 5 con duty cicle di 128 ha un periodo di 1024uS dato dalla temporizzazzione del timer0

Ho modificato la wiring.c in questo modo

Ho cercato di evidenziare in grassetto le parti modificate ma non ci sono riuscito sono solo
rimasti i tag come mai non funzionano all’ interno dei tag code

// 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 * [b]250[/b]))

// 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(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
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;

[b]
        unsigned char oldtifr;
	unsigned char oldtimsk;
	
#ifdef TIFR0
	oldtifr=TIFR0;
	oldtimsk=TIMSK0;
#else
	oldtifr=TIFR;
	oldtimsk=TIMSK;
#endif	

#if defined(TCNT0)
	TCNT0=6;
#else
	TCNT0L=6;
#endif	
[/b]
	
	m += MILLIS_INC;
	
[b]
#ifdef TIFR0
	TIFR0=oldtifr;
	TIMSK0=oldtimsk;
#else
	TIFR=oldtifr;
	TIMSK=oldtimsk;
#endif

//	m += MILLIS_INC;
//	f += FRACT_INC;
//	if (f >= FRACT_MAX) {
//		f -= FRACT_MAX;
//		m += 1;
[/b]
	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;
}

Ho fatto le varie verifiche

l’ unico inconveniente è il duty cicle del pwm.

analogWrite(pin,0) genera un segnale basso

analogWrite(pin,1 to 6) genera un segnale alto perchè il timer0 viene reinizializzato a 6

analogWrite(pin,7 to 254) genera un segnale pwm con duty cicle variabile.

quindi si potrebbe correggere solo la analogWrite utilizzando come valore da passare per il duty cycle
l’ intervallo 1 to 248 e nella funzione gli si somma 6.

Allego le forme d’ onda dell’ oscilloscopio del pwm prima e dopo

Ok, allora parliamo della precisione del oscilloscopio. Quanto preciso é nella misura di tempi é il Tuo oscilloscopio? Come scrivi lo dai preciso al 1000%
Ciao Uwe

Ciao wuefed che devo dire la prcisione è quella di uno strumento di laboratorio, il data sheet Tektronics specifica Horizontal Accuracy - ±0.01% parliamo di uno strumento digitale da 100MHZ a 4 canali.

ormai fuori produzione ma il suo successore ha solo il display a colori invece che bianco e nero.

http://it.farnell.com/tektronix/tbs1104/oscilloscope-4ch-100mhz-1gsps/dp/2347634?mckv=s991Icpge_dc|pcrid|74363451738|kword|tbs1104|match|p|plid|&CMP=KNC-GIT-GEN-SKU-MDC&gclid=EAIaIQobChMIoc7m2c-E2gIVQkQYCh0YuQaiEAAYASAAEgLRGfD_BwE

ho scritto due righe di codice per generare un segnale col timer2 e metterlo a confronto con un segnale
temporizzato con delay per lo stesso periodo e il risultato da un segnale dal timer 2 di 1000uS precisi
con la delay 1012uS. SE l’ oscillo scoppio da corretti i 1000 sono corretti anche i 1012.

Allego forme d’onda

// include the library code:
#include <LiquidCrystal.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

int retroPin = 10;//digital 10

int led = 13;//fisicamente collegato a rled
int rled = 12;//fisicamente collegato a led

int cout = 11;//fisicamente collegato a rcout uscita pwm t2
int rcout = 3;//fisicamente collegato a cout

int mout = 5;//uscita con delay


void setup() {
  // LED Display connected to digital pin 10:
  // sets the digital pin as output:
  pinMode(retroPin, OUTPUT);  
     
  // sets retro off:
  digitalWrite(retroPin,LOW);
  // set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  //delay(1000);
  lcd.clear();
  // Print a message to the LCD.
  //display("0123456789012345")  
  lcd.print("     Ready      ");
  //delay(1000);  
  digitalWrite(retroPin,HIGH);

  pinMode(led,OUTPUT);
  pinMode(rled,INPUT);//collegato a led
  pinMode(cout,OUTPUT);
  pinMode(rcout,INPUT);//collegato a cout uscita di pwm t2
  pinMode(mout,OUTPUT);//uscita delay
  
  TCCR2B=0;//Blocco t2
  TIFR2=0b00000011;//Azzero int flag
  //TIMSK2=0b00000011;//Abilito if ov e cp solo dopo creazione vettori interruzioni altrimenti non parte
  TCNT2=0;//Inizializzo t2
  OCR2A=249;//Imposto 250 x 2uS (16000000/32) Fclk/prescaler
  ASSR=0b01000000;//Clock di sistema al t2
  TCCR2A=0b01000011;//Mode 7 e compare togle oc2a
  TCCR2B=0b00001011;//Mode 7 e Prescaler a 32
  DDRB=DDRB | 0b00001000;//Abilito uscita
  while(rcout==0) {//aspetto salita per sinconizzare mout a cout   
 }
}

void loop() {
  
  digitalWrite(mout,HIGH);
  delayMicroseconds(500);
  digitalWrite(mout,LOW);
  delayMicroseconds(500);
}