Buon Giorno,
prendendo spunto da un antico progetto "ardutester", ho pensato di scrivere una funzione per la lettura dell'adc direttamente con registri del micro. Questo per ottenere i risultati molto piu' velocemente in modo da applicare una sorta di oversampling velocemente.
Nella fattispecie la funzione calcola una media di valori mediani, parametrabile.
I risultati sono davvero soddisfacenti sia sul 328P che sul 2560, Eliminando quasi del tutto le fastidiosissime oscillazioni delle letture con analogread, soprattutto sul 328, aumentando virtualmente la risoluzione. I risultati migliori che ho ottenuto sono stati con 40 campionature di mediani (120 letture). In firma la funzione porta un parametro - int discards - questo parametro serve per regolare la scarica del condensatore posto sull'adc di arduino, di solto sono sufficienti 1-2 discards con riferimento a 5v e resistenza del partitore 10 kohm.
se di interesse ne possiamo discutere
float smartAverage(int pin, int readings, int discards) {
// 1. Uniforma l'input: trasforma A0..A15 nei numeri puri 0..15
if (pin >= A0) {
pin -= A0;
}
// Protezione per ATmega328P: se l'utente inserisce un pin inesistente (es. 8..15), lo limita a 7
#if !defined(__AVR_ATmega2560__) && !defined(__AVR_ATmega1280__)
if (pin > 7) pin = 7;
#endif
int pinOriginale = pin;
// 2. DIDR: Disabilita il buffer digitale in modo sicuro per l'architettura
#if defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__)
if (pinOriginale < 8) {
DIDR0 |= (1 << pinOriginale);
} else {
DIDR2 |= (1 << (pinOriginale - 8));
}
#else
// Su ATmega328P (Uno/Nano) esiste solo DIDR0
if (pinOriginale < 8) {
DIDR0 |= (1 << pinOriginale);
}
#endif
// 3. Configurazione hardware dell'ADC
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
#ifdef MUX5
if (pin >= 8) {
ADCSRB |= (1 << MUX5);
pin -= 8; // Scala il pin nel range 0..7 per ADMUX
} else {
ADCSRB &= ~(1 << MUX5);
}
#endif
// Applica le modifiche hardware scrivendo ADMUX
ADMUX = (1 << REFS0) | (pin & 0x07);
// 4. Ciclo di svuotamento/stabilizzazione del MUX
for(uint8_t i = 0; i < discards; i++) {
ADCSRA |= (1 << ADSC);
while (ADCSRA & (1 << ADSC));
delayMicroseconds(5);
volatile uint16_t svuota = ADCW; // Impedisce al compilatore di ottimizzare e rimuovere la riga
}
delayMicroseconds(25); // Breve pausa stabilizzatrice
// 5. Lettura e filtraggio dei campioni
uint32_t sommaMediane = 0;
for (uint16_t m = 0; m < readings; m++) {
// Campione A
ADCSRA |= (1 << ADSC); while (ADCSRA & (1 << ADSC)); uint16_t a = ADCW;
delayMicroseconds(15);
// Campione B
ADCSRA |= (1 << ADSC); while (ADCSRA & (1 << ADSC)); uint16_t b = ADCW;
delayMicroseconds(15);
// Campione C
ADCSRA |= (1 << ADSC); while (ADCSRA & (1 << ADSC)); uint16_t c = ADCW;
delayMicroseconds(15);
// Calcolo rapido della mediana
uint16_t mediana;
if ((a <= b && b <= c) || (c <= b && b <= a)) mediana = b;
else if ((b <= a && a <= c) || (c <= a && a <= b)) mediana = a;
else mediana = c;
sommaMediane += mediana;
}
// 6. Ripristino dei registri allo stato iniziale
#ifdef MUX5
ADCSRB &= ~(1 << MUX5);
#endif
#if defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__)
if (pinOriginale < 8) {
DIDR0 &= ~(1 << pinOriginale);
} else {
DIDR2 &= ~(1 << (pinOriginale - 8));
}
#else
if (pinOriginale < 8) {
DIDR0 &= ~(1 << pinOriginale);
}
#endif
// 7. Ritorna la media matematica delle mediane calcolate
return static_cast<float>(sommaMediane) / readings;
}
Grazie per la condivisione. L'approccio del filtraggio mediano combinato all'oversampling è efficace e ben scelto, gestisce bene sia i disturbi impulsivi isolati sia il rumore a banda larga, aumentando la risoluzione effettiva senza distorcere il segnale.
Riguardo alla firma della funzione,
non converrebbe usare uint8_t per pin e discards, e uint16_t per readings, al posto di int per tutti e tre. Nel corpo della funzione pin viene comunque trattato come un intero piccolo compreso tra 0 e 15, discards viene usato in un ciclo con contatore uint8_t e readings in un ciclo con contatore uint16_t, quindi il tipo int lascia aperta la possibilità di passare valori negativi che non vengono gestiti dalla funzione.
Usare tipi senza segno coerenti con quelli utilizzati internamente renderebbe l'interfaccia più chiara.
Detto questo, questo non impedisce a readings di valere 0, il che provocherebbe una divisione per zero nel calcolo finale della media, un controllo esplicito su questo valore resterebbe comunque necessario.
Il tempo di esecuzione è piuttosto lungo, con 14 15 millisecondi di blocco completo della CPU per una chiamata con questa configurazione. Avete considerato un approccio asincrono, usando l'interruzione ADC_vect o un polling non bloccante sul bit ADIF, per liberare la CPU durante l'acquisizione, oppure questa scelta bloccante è voluta nel vostro caso d'uso ?
ti ringrazio della risposta, hai perfettamente ragione, ll motivo per cui ho usato gli int in firma, e' per una sostituzione dei una vecchia funzione che portava in firma gli int, per nn correggere il firmware nel quale sono istanziati quei parametri in una struct, poi come spesso accade le cose provvisorie diventano definitive
Il tempo di esecuzione è piuttosto lungo, e' vero, e potrebbe essere ridotto, nel senso che 25 microsecondi potrebbero essere 10 - 15. Pero' considerato che la funzione sostituisce una media trimmata che era di gran lunga piu' lenta, ho pensato di che mantenere questo compromesso sarebbe stato comunque vantaggioso.
Massimo
una ulteriore riflessione sul tempo di elaborazione, c'e' da considerare anche che in fase di implemetazione, soprattutto nel caso ci siano piu' porte da leggere, conviene leggere le porte con una fsm in modo da avere un ritardo di pochi millisecondi ogni loop, con analogread sarebbe molto piu' lenta la lettura, inoltre, io sono un neofita del mondo arduino, e la semplita' del codice per me e' una necessita'.
Per essere un neofita, aver usato direttamente i registri e il basso livello invece di restare su analogRead è davvero notevole!
La fsm proposta per leggere più porte è una buona idea, ma opera a livello applicativo, non a livello dell'ADC, quindi ogni singola chiamata di smartAverage continua a bloccare la CPU per la propria durata. Detto questo, la semplicità del codice resta un argomento legittimo per non spingersi verso una vera architettura a interruzioni.
ho provato a correggere la funzione seguendo i tuoi consigli:
float smartAverage(uint8_t pin, uint16_t readings, uint8_t discards) {
// Protezione contro la divisione per zero se l'utente passa 0 letture
if (readings == 0) readings = 1;
// 1. Uniforma l'input: trasforma A0..A15 nei numeri puri 0..15
if (pin >= A0) {
pin -= A0;
}
// Protezione per ATmega328P (Uno/Nano): limita il pin a 7 se fuori range hardware
#if !defined(__AVR_ATmega2560__) && !defined(__AVR_ATmega1280__)
if (pin > 7) pin = 7;
#endif
uint8_t pinOriginale = pin;
// 2. DIDR: Disabilita il buffer digitale in modo sicuro per l'architettura
#if defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__)
if (pinOriginale < 8) {
DIDR0 |= (1 << pinOriginale);
} else {
DIDR2 |= (1 << (pinOriginale - 8));
}
#else
if (pinOriginale < 8) {
DIDR0 |= (1 << pinOriginale);
}
#endif
// 3. Configurazione hardware dell'ADC
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
#ifdef MUX5
if (pin >= 8) {
ADCSRB |= (1 << MUX5);
pin -= 8; // Scala il pin nel range 0..7 per ADMUX
} else {
ADCSRB &= ~(1 << MUX5);
}
#endif
// Applica le modifiche hardware scrivendo ADMUX (Riferimento a AVCC / 5V)
ADMUX = (1 << REFS0) | (pin & 0x07);
// 4. Ciclo di svuotamento/stabilizzazione del MUX (eseguito solo se richiesto)
if (discards > 0) {
for(uint8_t i = 0; i < discards; i++) {
ADCSRA |= (1 << ADSC);
while (ADCSRA & (1 << ADSC));
delayMicroseconds(5);
volatile uint16_t svuota = ADCW; // Impedisce l'ottimizzazione del compilatore
}
delayMicroseconds(25); // Breve pausa stabilizzatrice
}
// 5. Lettura e filtraggio dei campioni tramite triplette di mediane
uint32_t sommaMediane = 0;
for (uint16_t m = 0; m < readings; m++) {
// Campione A
ADCSRA |= (1 << ADSC); while (ADCSRA & (1 << ADSC)); uint16_t a = ADCW;
delayMicroseconds(15); // Diversità temporale sul rumore
// Campione B
ADCSRA |= (1 << ADSC); while (ADCSRA & (1 << ADSC)); uint16_t b = ADCW;
delayMicroseconds(15);
// Campione C
ADCSRA |= (1 << ADSC); while (ADCSRA & (1 << ADSC)); uint16_t c = ADCW;
delayMicroseconds(15);
// Calcolo rapido della mediana tra le tre letture consecutive
uint16_t mediana;
if ((a <= b && b <= c) || (c <= b && b <= a)) mediana = b;
else if ((b <= a && a <= c) || (c <= a && a <= b)) mediana = a;
else mediana = c;
sommaMediane += mediana;
}
// 6. Ripristino dei registri allo stato iniziale
#ifdef MUX5
ADCSRB &= ~(1 << MUX5);
#endif
#if defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__)
if (pinOriginale < 8) {
DIDR0 &= ~(1 << pinOriginale);
} else {
DIDR2 &= ~(1 << (pinOriginale - 8));
}
#else
if (pinOriginale < 8) {
DIDR0 &= ~(1 << pinOriginale);
}
#endif
// 7. Ritorna la media matematica delle mediane calcolate
return static_cast<float>(sommaMediane) / readings;
}
per quanto riguarda il tempo di elaborazione, ho fatto mente locale sulla logica
"L'approccio asincrono (tramite ADC_vect o polling su ADIF) è una valida obiezione in senso assoluto, ma è stata scartata per due motivi pratici nel mio caso d'uso:
La funzione esegue un filtraggio a triplette di mediane ravvicinate (con delay di 15µs tra i campioni). Renderla asincrona richiederebbe di spostare la logica matematica e il conteggio dei campioni dentro l'interrupt, complicando il codice in modo sproporzionato rispetto ai benefici.
La funzione viene chiamata all'interno di una FSM generale che scansiona un solo canale ogni 300ms. Il tempo di blocco della CPU per calcolare le mediane è di pochissimi microsecondi, totalmente irrilevante sulla fluidità del ciclo principale, mantenendo però il codice atomico, pulito e facilmente portabile da chiunque."
Ti ringrazio per il complimento, in effetti per me non e' stato facile studiare nella selva dei registri degli avr. Come ho scritto, questa funzione deriva dalla revisione di un antichissimo software del 2013 (credo) e si trattava di un component tester, da qui ho cominciato a studiare i registri. In quella discussione di alcuni mesi fa, qualche utente si e' dimostrato interessato alla lettura dell'adc direttamente coi registri, per questo ho pubblicato questa funzione. Comunque ti ringrazio ancora per l'attenzione che hai applicato alla lettura del codice
Grazie per i dettagli sul ragionamento. Una cosa però va segnalata: la cifra di "pochi microsecondi" sottostima il tempo di blocco reale. Con il prescaler dell'ADC impostato a 128, ogni conversione richiede circa 104 microsecondi, e considerando i cicli di discard più i tre campioni per ogni mediana moltiplicati per il numero di letture, una chiamata completa con parametri tipici come 40 readings e 2 o 3 discards finisce per bloccare la CPU per circa 15ms in totale, non pochi microsecondi. Si tratta comunque della funzione nel suo insieme, non solo del calcolo delle mediane.
Detto questo, la conclusione di fondo può comunque reggere, solo per un motivo diverso. Se la FSM scansiona un solo canale ogni 300ms e nient'altro nel programma richiede timing stretti durante quella finestra, bloccare per 15ms rappresenta circa il 5 percento del ciclo, il che è probabilmente accettabile per questo tipo di applicazione.
➜ Il compromesso diventa un problema reale solo se lo sketch deve anche rimanere reattivo a qualcos'altro durante quella finestra, come comunicazione seriale, controllo motori, o altri sensori da scansionare, nel qual caso stalli ripetuti di 15ms potrebbero cominciare a pesare.
scusa, ho confuso i microsecondi con i millisecondi, il mio ragionalento e' stato questo
secondo quanto letto sul datasheet, e' consigliato, quando si cambia il canale di fare una lettura a vuoto con analogread,ed attendere 5 millisecondi.
Ora con 5 milliseondi fissi, e il tempo che impiega anlogread per 120 letture, siamo ampiamente al di sopra del tempo impiegato da questa funzione
e' chiaro che qui parliamo di 120 letture totali, che sono moltissime, ottimi risultati si ottengono anche con 10 readings (30 letture), dipende moltissimo da come viene alimentato arduino. In questa applicazione gestisco sia una comunicazione seriale che una comunicazione lan udp, l'intero loop compresa la gestione di un paio di fsm arrva intorno ai 150 millisecondi, che non sono pochi, ma siamo anche su un micro a 16 Mhz
Salvare lo stato iniziale: Mantiene in memoria il vero numero del pin (es. 10) prima che la variabile "pin" venga alterata e scalata a 2.
Garantire la simmetria del DIDR: Permette al Punto 6 di sapere con assoluta certezza se l'algoritmo deve andare a toccare DIDR0 (pin 0-7) o DIDR2 (pin 8-15) per ripristinare i buffer digitali esattamente come erano prima della lettura.
Senza quella variabile, la funzione avrebbe configurato la lettura sul pin corretto, ma avrebbe "acceso" il pin sbagliato nel registro di ripristino, causando potenziali letture fantasma o comportamenti anomali sui pin digitali associati.
Questo perche' possa funzionare sia sul 328 che sul 2560