Misura velocità ruota - Frequenza campionamento

Vorrei misurare la velocità delle ruote di una monoposto (prototipo).
Attualmente sono montati dei sensori di prossimità induttivi di tipo NPN.
La ruota fonica ha 30 fori.
Considerata una velocità massima di 100 km/h e un diametro di rotolamento di 0,5 m, mi aspetto una frequenza del segnale di circa 530 Hz.

Ho utilizzato il seguente codice:

const int pin = 2;

volatile byte rpmcount;
unsigned int rpm;
unsigned int v;
unsigned long timeold;

void contaImpulsi()
{
rpmcount++;
}

void setup() {
  pinMode(pin, INPUT_PULLUP);

  Serial.begin(9600);

  attachInterrupt(digitalPinToInterrupt(pin), contaImpulsi, FALLING);
  
  rpmcount = 0;
  rpm = 0;
  timeold = 0;
}

void loop() {
  
    delay(1000);

    detachInterrupt(0);
    
    rpm = (60/30)*1000/(millis() - timeold)*rpmcount;
    v = ((rpm*6.28*0.25)/60)*3.6;
    
    timeold = millis();

    rpmcount = 0;

    Serial.print(v);

    attachInterrupt(0, contaImpulsi, FALLING);
}

Il delay l'ho messo per poter leggere il valore sul monitor seriale.
Il valore letto (ruota destra condotta) l'ho confrontato con quello visualizzato sul monitor di un sistema di acquisizione dati professionale (ruota sinistra condotta). I due valori paiono comparabili anche se per confrontarli sarebbe opportuno salvarli. Non ho idea di come farlo per i valori letti da arduino.

Quello che ho riscontrato è che a volte, tra una lettura e l'altra, con arduino si nota una variazione di velocità non congruente.
Esempio: a velocità costante in un istante il valore è 12 km/h, quello successivo 6 e quello dopo ancora 12. Potrebbe essere un problema di campionamento?

Non ho capito come si setta quest'ultimo per acquisire correttamente il segnale.
Ho letto che arduino ha una velocità di clock di 16 MHz e che ha tre timer. Che la funzione millis() è gestita col timer0 che è a 8 bit e che ci sono dei registri nei quali sono settati dei prescaler. Dopo aver letto diverse discussioni ho ancora le idee confuse.

millis() - timeold dovrebbe darmi già un'idea del tempo intercorso tra due esecuzioni consecutive delle istruzioni nel blocco loop. E' corretto?

Ti invitiamo a presentarti (dicci quali conoscenze hai di elettronica e di programmazione) qui: Presentazioni
e a leggere il regolamento se non lo hai già fatto: Regolamento
Qui una serie di link utili, non inerenti al tuo problema:

Prova con questa alternativa, che non utilizza delay. Io l'ho testato con frequenze oltre i 50kHz senza alcun problema.
Anche se è nato come frequezimetro, basta la modifica alla base dei tempi nella variabile periodo e moltiplicare freq x il diametro ruota ed il gioco è fatto

#define pinInput 2                         // pin input impulsi
#define Interrupt 0                        // interrupt associato

volatile word impulsi = 0;                 // contatore impulsi
word impulsiOld =0;
word freq =0;                              // valore frequenza

const unsigned long periodo = 1000;        // costante unità di tempo = 1" ritardo
unsigned long millisOld;                   // variabile supporto per calcolo periodo 

  
// -----------------------------------------------------------------------------------------------

void setup() {

  pinMode (pinInput,INPUT_PULLUP);           // imposta porta ingresso (opzionale PULLUP)
  Serial.begin(57600);                       // attiva comunicazione
   
  attachInterrupt(Interrupt,somma,RISING);   // iniziailizza interrupt  
  impulsiOld = impulsi;                     // allinea variabile
  
  millisOld = millis();                     // inizializza timer
 }


void somma() {
  impulsi ++;                                            // incrementa contatore
}


void loop() {
  if ((millis() - millisOld) >= periodo){                 // trascorso 1"
    millisOld += periodo;                                 // aggiorna variabile
    freq = impulsi - impulsiOld;                          // calcola frequenza
    impulsiOld = impulsi;                                 // allinea variabile

    Serial.println (freq);
  }//if
}//void loop

Ciao,
i problemi potrebbero diversi:

  • un problema di cast: il primo conto è fatto in interi e potrebbe perdere risoluzione. Fallo tutto in float (più probabile)
  • la variabile è un byte, ma se poi vai a 512 Hz in un secondo hai sicuramente un wraparound
  • meglio se salvi tutto appena prima ed appena dopo il delay per avere i contatori giusto
  • meglio evitare di fare l'attach e il detach in continuazione: lascia sempre l'interrupt acceso, azzeri solo la variabile
  • potresti avere segnali sporchi: hai provato ad acquisirli con un oscillo?
  • potresti aver bisogno di mettere un filtro digitale

Per aver massima risoluzione lo farei cosi (mettendo anche una piccola media mobile come filtro):

const int pin = 2;

volatile int rpmcount;
int rpmcounttemp;
unsigned float rpm;
unsigned int v = 0;
unsigned long timeold;
unsigned long timenew;

void contaImpulsi()
{
rpmcount++;
}

void setup() {
  pinMode(pin, INPUT_PULLUP);

  Serial.begin(9600);

  attachInterrupt(digitalPinToInterrupt(pin), contaImpulsi, FALLING);
  
  rpmcount = 0;
  rpm = 0;
  timeold = 0;
}

void loop() {
  
    timeold = millis(); 
    rpmcount = 0;
    delay(1000);
    rpmcounttemp = rpmcount;
    timenew = millis(); 

    rpm = (60.0/30.0)*1000.0/(float)((timenew  - timeold)*rpmcounttemp);
    v = (int)((((rpm*6.28*0.25)/60.0)*3.6 + (float)v)/2.0);

    Serial.print(v);
}

Non ho modo di provare, spero ti sia d'aiuto

Ma al fine di semplificare i calcoli, e rendere il codice presente nel loop più efficente, non sarebbe il caso di accorpare le vari costanti (60 - 30 - 6.28 ecc.) con un'unica espressione in una unica variabile definita nel setup?

Qualcosa di analogo lo propongo per la mia proposta di "frequenzimetro":
nota la corrispondenza 100km/h = 530 hz, sarebbe sufficiente definire nel setup " k = 100/530;" e nel loop " vel = freq * k; ". Lascio a te il compito di sistemare la definizione delle variabili.

Grazie a tutti per le indicazioni e i suggerimenti.
Ho ancora dei dubbi. L'utilizzo della funzione interrupt non necessita quindi l'impostazione di un tempo di campionamento per il segnale?
C'è la sicurezza che tutti gli interrupt esterni siano catturati dal contatore anche per frequenze elevate come 50 kHz?

Perchè evitare l'utilizzo del detach?
Se durante l'esecuzione delle istruzioni nel blocco loop arriva un interrupt esterno questo non blocca l'esecuzione del programma?

Putroppo non credo di avere l'opportunità di controllare il segnale con l'oscilloscopio.

Non potendo provare, per il momento, sulla macchina ho testato il codice di lelebum per la lettura dei giri/min di una ventola del pc. Nonostante la

velocità non fosse controllata da segnale pwm le letture hanno oscillato parecchio da 1380 fino a 1920.

Si sicuramente è una buona idea definire prima le variabili e magari calcolare già una costante per la quale debba essere moltiplicato il valore misurato e

ridurre al minimo i calcoli. La formula l'avevo scritta così per fare velocemente la prova sul campo dopo aver verificato il funzionamento della lettura del numero di giri.

Ciao,
un interrupt blocca il programma nel loop, ma se non devi fare qualcosa di critico nel loop non è un problema: si interrompe e si riprende dov'era. In generale, per abitudine mia, a meno che non sia necessario, preferisco non continuare a a modificare le impostazioni base del micro (es. staccando e riattacando l'interrupt), ma non c'è nessun particolare rischio: solo una buona regola che mi ha aiutato spesso con HW magari un po' "deboli". Moltiplicare prima le costanti non ha alcun effetto: il compilatore in realtà lo fa già da solo durante la compilazione (programmo da una vita micro: te lo posso dire con assoluta certezza). es se scrivi: a = 1010b, il compilatore farà in realtà a=100*b. A 50KHz il micro non ha problemi ad intercettare l'interrupt: devi però esser sicuro che abbia tempo di fare tutti i conti prima che arrivi il successivo (e sono pochi microsecondi.. l'arduino è veloce, ma non velocissimo). Se hai il dubbio che il segnale sia sporco, un filtro dovrebbe aiutarti

Lo sketch che ho proposto era nato come contagiri con soglie di controllo e per frequenze al di sotto di 1kHz.

Solo per capire i limiti del 328 e del software ho spinto con un generatore di funzioni la frequenza sino ai fatidici 65kHz, limite massimo permesso dalla variabile "word" e il tutto mi ha sorpreso per precisione e stabilità.

Per quanto riguarda il tempo di campionamento nella mia proposta è affidata alla variabile "periodo" mentre nella tua è affidato al comando delay(1000).

Se aumenti il baud rate dovresti notare un miglioramento.
9600 bit/secondo equivalgono a (2bit start/stop + 8 data bit = 10 bit) 9600/10 = 960 byte per secondo.
Per ogni carattere ci vuole un tempo di 1/960 = 0,001041667 secondi (1.04ms).

Anche secondo me il calcolo di 'v' con gli operandi in virgola mobile per poi castare ad intero con segno andrebbe ripensato, as esempio si può trasformare tutto in interi senza segno per velocizzare il calcolo, perché il micro non ha fpu, quindi il calcolo in virgola mobile viene fatto con il software.

Riguardo agli interrupt, non c'è certezza che una richiesta di interrupt venga onorata, cioè ci sono buone probabilità di perdere una IRQ. Nota che millis() fa ricorso ad IRQ, cioè ogni tot di tempo si solleva una IRQ.
Quando il micro esegue una ISR, gli interrupt sono disabilitati, e se nel frattempo si sollevano più di una IRQ, solo una viene onorata ma ciò avviene dopo con un ritardo legato al tempo di permanenza nella ISR.

Trovi maggiori info all'interno del datasheet della MCU scaricabile dal sito di Atmel.
Visita anche: avr-libc

Ciao.

Ciao,
da che sapevo gli interrupt sono serviti a priorità: se due avvengono contemporaneamente, quello più prioritario entra per primo, l'altro semplicemente rimane in attesa. Una volta finito il primo, il secondo viene eseguito, quindi nulla viene perso a meno che nel frattempo non ne sia già arrivato un altro (overrun). Solo in questo caso rischi di perderne qualcuno visto che per due fronti ne viene contato solo uno.
Comunque se nessuna delle soluzioni attuali funziona, posso vedere se riesco ad estrapolarti la parte di un sistema simile che avevo fatto tempo fa: so per certo che va perchè è attaccato da un macchinario da almeno due anni e non ha mai dato problemi.

Questa la mia ultima idea per semplificare i calcoli: modificare il tempo di campionamento.
Visto che hai una frequenza in ingresso relativamente alta, potresti tentare questa soluzione.
Sostanzialmente è il tuo codice riveduto e corretto a:

  • corretto ordine attivazione interrupt
  • velocità seriale
  • tipo var rpmcount
  • delay()
    ma senza calcoli.
const int pin = 2;

volatile word rpmcount;

void setup() {
  pinMode(pin, INPUT_PULLUP);

  Serial.begin(57600);

}

void loop() {
 
    rpmcount=0;  // resetta counter

    // avvia ricezione impulsi
    attachInterrupt(digitalPinToInterrupt(pin), contaImpulsi, FALLING); 

    delay(188);         // periodo di tempo di accumulo impulsi (oppure 189)
    detachInterrupt(0); // blocca ricezione impulsi
 
    Serial.print(rpmcount);   // si ottiene direttamente il valore dei giri senza ulteriori calcoli
    
    delay (500);  // per rallentare la stampa dei valori
}

Anche se sono contrario all'uso del delay() la cosa dovrebbe funzionare.
Avrai campionamenti più rapidi, ma a scapito della precisione del risultato, ma sufficiente in questo ambito.
In realta il valore teorico da inserire nel delay dovrebbe essere 100/530*1000 (188,6792), quindi scegli un pò te.
Il secondo delay(500) serve per rendere leggibile le varie letture e potresti anche ommetterlo.

Scusate ma a me ancora non è chiaro il meccanismo di acquisizione. Portate pazienza.
Pensavo di poter impostare qualche parametro per dire ad Arduino ogni quanto tempo andare a acquisire il segnale invece mi pare di capire che Arduino il segnale lo acquisisce e incrementa il contatore e io posso, scegliendo il delay, solo andare a modificare il calcolo della frequenza scegliendo un tempo sul quale rapportare i passaggi di stato high/low che lui ha già acquisito e memorizzato nel contatore. Ma qual è il limite fisico di acquisizione? Come posso controllare la velocità con cui Arduino sente gli interrupt sul pin? Non so se sono riuscito a spiegare bene il mio dubbio.

Se ho un segnale di 500 Hz il tempo di campionamento non dovrebbe essere almeno 1/(2*500) cioè 1 millisecondo?

Non penso che arduino possa acquisire con la sua frequenza di clock e cioè ogni 62.5 ns.

Per quanto riguarda il delay per la lettura per me si può benissimo togliere nel senso che lo stavo usando solo per controllare che i valori letti fossero realistici. Fatto questo lo tolgo perchè dovrei inviare il segnale ad un'altra scheda di acquisizione che dovrà attuare dei controlli in base ai valori di velocità letti.

Spero di poter provare lunedì.

Scusate ma a me ancora non è chiaro il meccanismo di acquisizione. Portate pazienza.

Il termine 'acquisizione' si usa quando si parla di conversione Analogico Digitale (ADC), usato qui porta confusione, infatti stavo per scrivere che il convertitore impiega 13 cicli di ADC clock.

Come ha scritto @Ulixxes le IRQ hanno delle priorità fisse, la maggiore priorità parte da INT0, INT1 e poi bisogna leggere il datasheet. Se il micro non sta eseguendo alcuna richiesta di interrupt (cioè eseguire la ISR) il tempo di reazione dovrebbe essere di circa 4 cicli di main clock, quindi 62.5n x 4, quindi è più rapido di quanto ti serva.

Il problema nel caso specifico riguarda il calcolo che avviene nella funzione loop e in particolare all'uso di millis che si fa per ricavare il tempo che è influenzato dalla chiamata a funzione loop(), da Serial.print e in generale da tutto quel codice che si inserisce nel loop().

530Hz vuol dire che:
La ISR viene eseguita ogni 1/530=0,001886792s (1.886792ms) a cui aggiungiamo (4 * F_CPU) 0,00025s, totale
0,002136792s (2,1ms). 530 cambi si stato dovrebbero essere conteggiati dal contatore in 1 secondo, ma abbiamo un ritardo cumulato di 0.00025s * 530 = 0,1325s (132.5ms) che corrispondo ad un errore in frequenza di 1/0.1325 = 7,547169811Hz circa.

Su Arduino ci sono timer che una volta configurati possono essere usati con maggiore profitto rispetto a quanto si sta facendo con il codice.

Ora devo andare, quindi tronco il post qui.

Ciao.

MauroTec:
Come ha scritto @Ulixxes le IRQ hanno delle priorità fisse, la maggiore priorità parte da INT0, INT1 e poi bisogna leggere il datasheet. Se il micro non sta eseguendo alcuna richiesta di interrupt (cioè eseguire la ISR) il tempo di reazione dovrebbe essere di circa 4 cicli di main clock, quindi 62.5n x 4, quindi è più rapido di quanto ti serva.

Ti riferisci al tempo di reazione di interrupt?
Sono andato a guardare sul datasheet dell'ATmega328. Ho trovato il paragrafo "Interrupt Response Time".
Se non ho capito male la latenza di interrupt è 11 o 15 cicli( se il controllore è in modalità sleep):
4 per salvare il PC nello stack per ricordarsi da dove riprendere il programma
3 per trovare l'indirizzo della ISR nell'interrupt vector
4 per riprendere il programma

E' corretto quindi se concludo che arduino riesce a sentire gli interrupt che si verificano in un tempo inferiore o uguale a 0.6785 microsecondi?

530Hz vuol dire che:
La ISR viene eseguita ogni 1/530=0,001886792s (1.886792ms) a cui aggiungiamo (4 * F_CPU) 0,00025s, totale
0,002136792s (2,1ms). 530 cambi si stato dovrebbero essere conteggiati dal contatore in 1 secondo, ma abbiamo un ritardo cumulato di 0.00025s * 530 = 0,1325s (132.5ms) che corrispondo ad un errore in frequenza di 1/0.1325 = 7,547169811Hz circa.

F_CPU è 16 Mhz o qualche altro valore? Perchè altrimenti il ritardo è 0.00000025 s.

Su Arduino ci sono timer che una volta configurati possono essere usati con maggiore profitto rispetto a quanto si sta facendo con il codice.

L'utilizzo degli interrupt interni relativi ai timer dici che porta ad un calcolo più accurato rispetto all'utilizzo degli interrupt esterni?

F_CPU è 16 Mhz o qualche altro valore? Perchè altrimenti il ritardo è 0.00000025 s.

Purtroppo la premura è cattiva consigliera, comunque c'è un errore in quello che ho scritto, correggo.

Correzione {
a cui aggiungiamo (4 * 1/F_CPU) 0,00025s
}

Perché un ciclo di clock vale 1/F_CPU = (1/16.000.000) * 10^9 = 62.5ns (^ elevamento a potenza)

Certamente la velocità di risposta del circuito che fa capo ad un pin di ingresso e relativo interrupt è molto più rapida di 62.5ns, ma ciò che conta è proprio il tempo di risposta in cicli di clock, proprio quello che hai trovato tu nel datasheet.

E' corretto quindi se concludo che arduino riesce a sentire gli interrupt che si verificano in un tempo inferiore o uguale a 0.6785 microsecondi?

Certamente si, il risultato corrisponde a (IRT * 1/F_CPU) = 11 * 62.5ns = 687,5ns (0.6785us).

Riguardo al calcolo dell'errore in frequenza ho qualche dubbio su come l'ho conteggiato, per adesso non tenerne conto.

Mi fa piacere constatare che hai letto il datasheet, li trovi tutto non sempre spiegato in modo chiaro è lampante, ma è l'unica risorsa di informazione attendibile.

PS: anche il codice per essere eseguito richiede un numero di cicli cpu e in alcuni casi bisogna mettere in conto anche questo.

L'utilizzo degli interrupt interni relativi ai timer dici che porta ad un calcolo più accurato rispetto all'utilizzo degli interrupt esterni?

Non te lo do per certo, (per la certezza c'è il datasheet) però ho visto molti frequenzimetri basati sul l'uso del timer.

In teoria puoi configurare un timer in modo che quando il contatore raggiunge un determinato valore avviene l'esecuzione di una ISR, quindi nel tuo caso basterebbe che il timer chiamasse la ISR ogni secondo così
da fare il calcolo e azzerare il contatore degli impulsi ricevuti. Tuttavia c'è da controllare nel datasheet se si può configurare il timer per tempi così lunghi (1 secondo è tanto se viaggi a 16MHz). Di sicuro si può configurare per chiamare una ISR ogni millesimo di secono (lo fa già arduino), perchè millis() funziona cosi.

Ciao.