Misura 0 - 1 MHz (risoluzione 0,25 Hz) Squarewave usando un MCU


8

Devo misurare la frequenza dell'onda quadra che può variare tra 0 e 1 MHz e ha una risoluzione di 0,25 Hz.

Non ho ancora deciso quale controller, ma molto probabilmente sarà uno dei 20 pin di Attiny.

Normalmente come misurare i segnali a bassa frequenza sarebbe utilizzando due timer uno configurato in modalità di acquisizione timer per interrompere su diciamo i fronti di salita del segnale esterno e un altro timer impostato per interrompere ogni secondo, quindi il valore del registro contatore dei timer precedenti dopo 1 secondo sarebbe uguale alla frequenza del segnale.

Tuttavia questo metodo ovviamente non funzionerà per catturare segnali che vanno da 0 a 1 MHz con una risoluzione di 0,25 Hz per questo avrei bisogno di un contatore a 22 bit (i micro AFAIK a 8 bit hanno solo contatori a 8/16 bit).

Un'idea che avevo era quella di dividere il segnale prima di applicarlo al micro ma questo sarebbe impraticabile in quanto il segnale dovrebbe essere diviso per 61 quindi la frequenza potrebbe essere aggiornata solo ogni 61 secondi dove vorrei che fosse ogni pochi secondi .

Esiste un altro metodo che consentirebbe di aggiornare la frequenza ogni 4 secondi?


Aggiornare:

La soluzione più semplice consiste nell'utilizzare un interrupt esterno o una cattura con timer per interrompere sul fronte di salita del segnale e ottenere l' isrincremento di una variabile di tipo long int. Leggi la variabile ogni 4 secondi (per consentire frequenze fino a 0,25Hz da misurare).


Aggiornamento 2:

Come sottolineato da JustJeff, una MCU a 8 bit non sarà in grado di tenere il passo con un segnale da 1 MHz in modo da escludere l'interruzione su ogni fronte di salita e incrementare un long int...

Ho scelto il metodo suggerito da Timororr. Una volta che riesco a implementarlo, invio post e condivido i risultati. Grazie a tutti per i vostri suggerimenti.


Rapporto sullo stato di avanzamento:

Iv ha iniziato a testare alcune delle idee presentate qui. In primo luogo ho provato il codice di Vicatcu. Si è verificato un evidente problema di TCNT1 non risolto dopo che la frequenza è stata calcolata, non un grosso problema ...

Poi ho notato durante il debug del codice che circa ogni 2 a 7 volte la frequenza è stata calcolata il conteggio di overflow del timer 1 (il timer configurato per contare gli eventi esterni) sarebbe breve di due. Ho messo questo alla latenza del Timer 0 ISR e ho deciso di spostare il blocco dell'istruzione if dall'ISR al principale (vedi frammento di seguito) e ho appena impostato un flag nell'ISR. Alcuni debug hanno mostrato che la prima misurazione sarebbe ok, ma con ogni lettura successiva il conteggio di overflow del Timer 1 sarebbe finito di 2. che non posso spiegare - mi sarei aspettato che non fosse finita ...

int main()
{
    while(1)
    {
        if(global_task_timer_ms > 0 && (T0_overflow == 1))
        {
            global_task_timer_ms--;
            T0_overflow = 0;
        }

        .....
    }
}

Successivamente ho deciso di provare a implementare il suggerimento di Timrorrs. Per generare l'intervallo necessario (di circa 15 ms tra ogni interruzione timer_isr) dovrei mettere in cascata i due timer a 8 bit poiché l'unico timer a 16 bit su Atmega16 viene utilizzato per catturare i fronti di salita del segnale esterno.

Ho pensato che questa soluzione avrebbe funzionato e sarebbe stata molto più efficiente in quanto la maggior parte delle spese generali è stata spostata sui timer e solo un breve tempo rimanente per la CPU. Tuttavia non era accurato come avevo sperato, le misurazioni si spostavano avanti e indietro di circa 70Hz, cosa che non mi dispiacerebbe alle alte frequenze, ma sicuramente non è accettabile alle frequenze più basse. Non ho trascorso molto tempo ad analizzare il problema, ma immagino che la disposizione a cascata del timer non sia così accurata in quanto ho implementato una disposizione simile alla proposta di timrorrs su un controller 8051 molto più lento che aveva 2 timer a 16 bit e i risultati erano abbastanza precisi.

Sono ora tornato al suggerimento di Vicatcu, ma ho spostato il calcolo della frequenza in Timer 0 isr (vedi frammento di seguito ), questo codice ha prodotto misurazioni coerenti e ragionevolmente accurate. Con un po 'di precisione, la precisione della calibrazione dovrebbe essere di circa +/- 10Hz.

ISR(TIMER0_OVF_vect)
{            

    TCNT0 = TIMER0_PRELOAD;         //Reload timer for 1KHz overflow rate

    if(task_timer_ms > 0)
    {
        task_timer_ms--;
    }
    else
    {     
        frequency_hz = 1.0 * TCNT1;
        TCNT1 = 0;
        frequency_hz += global_num_overflows * 65536.0;
        global_num_overflows  = 0;
        frequency_hz /= (TASK_PERIOD_MS / 1000.0);
        task_timer_ms = TASK_PERIOD_MS;
    }                                                 
}       

Se qualcuno ha altri suggerimenti, sono aperto a loro, ma preferisco non usare i range ... Inoltre, non sono più intenzionato a ottenere una risoluzione dello 0,25%, non sembra molto utile il livello di precisione che ho al momento .


Esiste un modo relativamente semplice per farlo utilizzando un interrupt di acquisizione su un PIC e Timer 1 in esecuzione a una velocità molto elevata. Se sei ancora interessato ad altri metodi fammi sapere e posso delinearlo in una risposta.
Kortuk,

Non ho ancora iniziato a lavorare su questo, quindi sì, sono ancora interessato.
Volting

Per qualche motivo non mi ha mai fatto sapere che avevi commentato il mio commento.
Kortuk,

@Kortuk: il software ti avvisa solo se lascio un commento a una delle tue risposte o domande. Potrebbe anche avvisarti di questo commento, perché ho messo @Kortuk di fronte. Ma si tratta di una modifica del software StackOverflow e non so se sia stato inserito nella base di codice StackExchange o meno.
Robert Harvey,

no, non mi ha fatto sapere che avevi risposto, anche con @kortuk. Nessun problema. Sembra che sia stata trovata una risposta.
Kortuk,

Risposte:


4

Se possibile, suggerirei di selezionare un microcontrollore che supporti un'operazione di contatore usando gli ingressi del timer; piuttosto che incrementare manualmente un contatore all'interno di un ISR (che alle alte frequenze finisce rapidamente per saturare l'attività del microcontrollore) si consente all'hardware di gestire il conteggio. A questo punto il tuo codice diventa semplicemente una questione di attesa per l'interruzione periodica e quindi di calcolo della frequenza.

Per estendere l'intervallo e rendere il contatore di frequenza più generalizzato (eliminando la necessità di intervalli multipli a scapito di un po 'più di lavoro per l'MCU) è possibile utilizzare la seguente tecnica.

Selezionare una frequenza di interruzione periodica che consenta l'accuratezza della misurazione alla massima frequenza di ingresso; questo dovrebbe tenere conto delle dimensioni del contatore (è necessario selezionare il periodo del timer in modo tale che il contatore del timer non trabocchi alla massima frequenza di ingresso). Per questo esempio suppongo che il valore del contatore di input possa essere letto dalla variabile "timer_input_ctr".

Includere una variabile per il conteggio degli interrupt periodici (deve essere inizializzato su 0 all'avvio); per questo esempio mi riferirò a questa variabile come "isr_count". Il periodo di interruzione è contenuto nella costante "isr_period".

L'interruzione periodica deve essere implementata come (C pseudo-codice):

void timer_isr()
{
  isr_count++;
  if (timer_input_ctr > 0)
  {
    frequency = timer_input_ctr / (isr_count * isr_period).
    timer_input_ctr = 0;
    isr_count = 0;
  }
}

Ovviamente questo esempio approssimativo si basa su alcuni calcoli in virgola mobile che potrebbero non essere compatibili per i microcontrollori di fascia bassa, esistono tecniche per superarlo ma non rientrano nell'ambito di questa risposta.


1
Timororr eccellente, che farò esattamente quello che voglio senza costi di IC extra che è sempre buono, penso di essere stato troppo veloce per respingere la possibilità di risolvere il problema nel software. Grazie
volting

@timrorr, sono interessato ai tuoi pensieri sulla mia risposta qui sotto se hai voglia di leggerlo
vicatcu

7

Potresti voler considerare di avere due (o più) intervalli. I problemi con l'acquisizione di frequenze molto basse sono leggermente diversi da quelli con le frequenze più alte. Come hai già notato, nella fascia alta della gamma si verificano problemi di overflow.

Ma considera nella fascia bassa del tuo intervallo, la tua precisione soffrirà di non avere abbastanza conteggi nel registro. Non sono sicuro se vuoi davvero discriminare tra 0,25Hz e 0,5Hz, ma se lo fai, dovrai effettivamente contare per quattro secondi per farlo.

Inoltre, specificare una risoluzione piatta di 0,25Hz, interpretata rigorosamente, significa che sarai in grado di discernere 500.000,00Hz da 500.000,25Hz, che è un grado di precisione piuttosto elevato.

Per questi motivi, la progettazione per intervalli distinti potrebbe alleviare il problema delle dimensioni del contatore. Tirare i numeri a caso per esempio, per la fascia bassa, dire da 0 a 100Hz, contare per intervalli di 10 secondi e ottenere una risoluzione di 0,1Hz e il contatore deve solo salire a 1000, nemmeno a 10 bit. Quindi da 100Hz a 10kHz, conta per intervalli di 1 secondo; ottieni solo una risoluzione di 1Hz, ma il tuo contatore deve solo eseguire fino a 10.000 ancora più piccoli di 16 bit. L'intervallo superiore da 10kHz a 1MHz potrebbe contare per soli 0,01 secondi e il conteggio massimo sarebbe comunque solo di 10.000 e sebbene la risoluzione sia di 100Hz, questa sarebbe una precisione ragionevole.


Sì, ho detto che nell'aggiornamento alla mia domanda (precedente) avrei dovuto contare fino a 4 secondi per ... e sì, vorrei poter distinguere tra 500.000,00Hz e 500.000,25Hz. Avevo pensato di utilizzare intervalli diversi, potrei facilmente collegarlo al resto dell'hardware poiché il segnale ha 6 intervalli selezionabili, quindi probabilmente potrei progettare un semplice encoder da 6 a 3 per indicare quale intervallo ... ma non lo sono certo se fosse necessario se uso un contatore hardware accoppiato con un tempo di aggiornamento di 4 secondi, questo dovrebbe occuparsi dei problemi alle due estremità dello spettro
voltaggio del

5

È possibile combinare un contatore hardware e un software contando gli overflow del contatore hardware in un ISR.

Il conteggio di ogni fronte del segnale in un ISR sarà troppo lento per un segnale da 1 MHz. Penso che potresti fare fino a circa 50kHz in quel modo.


Sì, probabilmente hai ragione, sarà troppo lento per 1MHz ma immagino che un processore RISC 20MIPS potrebbe fare meglio di 50KHz. In ogni caso stavo anche considerando di sincronizzare un contatore binario a 8 bit con il segnale e collegare il carry out del contatore al pin di interruzione esterno dell'MCU, quindi leggere la frequenza del segnale mentre la somma del bit di carry si interrompe più il conteggio o / p valore del contatore ogni n secondi, immagino che sia quello che stavi ottenendo quando hai detto una combinazione di contatori hardware e software.
Volting

Penso che l'OP si riferisse al contatore hardware integrato. Tutti hanno interruzioni di overflow che possono essere utilizzate per migliorare l'intervallo di conteggio.
jpc

@starblue, il codice che ho scritto sotto è quello che avevi in ​​mente con la tua risposta?
Vicatcu,

2

Invece di fare un contatore da 1 secondo, trasformalo in un contatore da 0,1 secondi e moltiplica il conteggio per 10?

Se si tratta solo di memorizzare il numero del contatore, non è possibile utilizzare un codice aggiuntivo per tenere traccia di quando il contatore sta per traboccare e scrivere in un'altra posizione di memoria per mantenere il conteggio?


2
Penso che dovrei avere il congelamento del cervello .. la soluzione più semplice che penso è solo quella di incrementare una variabile di tipo long int ogni volta che viene rilevato un fronte di salita. Leggere quel valore una volta al secondo, quindi reimpostarlo su zero.
Volting

2
In realtà dovrò leggere il valore ogni 4 secondi per misurare fino a 0,25Hz
voltando

2

Non puoi semplicemente utilizzare l'acquisizione in ingresso di un timer a 16 bit e gli interrupt di overflow (più una variabile) per eseguire la misurazione? Ecco come lo farei con ATTiny24A con AVR-GCC (ovviamente non testato e potenzialmente difettoso):

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define TIMER1_BITS           16    // 16 bit timer
#define TIMER1_HZ             8.0e6 // 8MHz crystal
#define TIMER1_OVF_PERIOD_SEC (1.0 * (1 << TIMER1_BITS) / TIMER1_HZ)
#define TIMER1_SEC_PER_TICK   (1.0 / TIMER1_HZ)

//global variables for time keeping
double total_period_sec = 0.0;
uint16_t  num_overflows = 0;

void setup_timer1_capture(){
   // set the ICP (input caputure pin) to a floating input
   DDRA  &= ~_BV(7); // it's A7 on the ATTiny24A...
   PORTA &= ~_BV(7);

   TIMSK1 =   _BV(ICIE1)  // enable input pin capture interrupt
            | _BV(TOIE1); // enable overflow interrupt

   TCCR1B =   _BV(ICNC1)  // activate the input noise canceller
            | _BV(ICES1)  // capture on rising edge of ICP
            | _BV(CS10);  // run the timer at full speed

}

ISR(TIM1_CAPT_vect, ISR_NOBLOCK){ //pin capture interrupt
  uint16_t capture_value_ticks = ICR1; // grab the captured value
  // do some floating point math
  total_period_sec =   1.0 * num_overflows * TIMER1_OVF_PERIOD_SEC
                     + 1.0 * capture_value_ticks / TIMER1_SEC_PER_TICK; 

  num_overflows = 0; // clear helper variable to be ready for next time
}

ISR(TIM1_OVF_vect){   //timer overflow interrupt
    num_overflows++;
}

int main(int argc, char *argv[]){
   setup_timer1_capture();

   sei(); // enable interrupts!

   for(;;){ //forever
      // do whatever you want...
      // the most recently calculated period is available in the 
      // total_period_sec variable 
      // (obviously 1.0 / total_period_sec is the frequency in Hz)
   }

   return 0;
} 

... in ogni caso, si compila :)


EDIT Ho guardato l'output del file lss dal mio codice e il codice generato ha troppe istruzioni per non inciampare su se stesso a 1MHz con un clock a 8MHz ... anche il semplice incremento di una riga in TIM1_OVF_vect genera 19 istruzioni! Quindi per gestire gli eventi a 1MHz, avresti sicuramente bisogno di ottimizzare, probabilmente registrare allocare alcune cose (probabilmente num_overflows e capture_value_ticks), usare l'assemblatore inline (rubare le cose importanti dal file lss) e spostare l'elaborazione fuori dagli interrupt e nel principale loop ove possibile.


Misurare una frequenza usando il periodo funziona abbastanza bene con le forme d'onda lente (stai facendo affidamento sul fatto che il timer interno è molto più veloce del segnale esterno) ma raggiunge rapidamente un limite all'aumentare della frequenza del segnale di ingresso. Fondamentalmente, come hai scoperto, il tempo trascorso all'interno dell'interrupt di acquisizione del timer diventa dominante; non c'è più tempo per l'esecuzione di altre parti del codice. Sebbene non abbia familiarità con ATTiny, una rapida occhiata al foglio dati mostra che timer / counter1 supporta il conteggio di eventi esterni, quindi lascia che l'hardware gestisca il conteggio.
Timrorr

@timrorr, wow sì, è un modo molto più intelligente di farlo :) Ho pubblicato il codice AVR-GCC aggiornato in un post separato. Ti va di dare un'occhiata e vedere cosa ne pensi?
Vicatcu,

2

Pubblicando questo codice come alternativa al suggerimento di @ timrorr al mio post precedente. Questo si compila per ATTiny24A usando lo standard del linguaggio c99, ma in realtà non l'ho testato in alcun modo oltre.

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#define TIMER0_PRELOAD   0x83 // for 8MHz crystal and overflow @ 1kHz
#define TIMER0_PRESCALE 0x03  // divide by 64
#define TASK_PERIOD_MS 4000   // execute task every 4 seconds

//global variables for time keeping
volatile uint16_t  global_num_overflows = 0;
volatile uint16_t  global_task_timer_ms = TASK_PERIOD_MS;

void setup_timers(){
    // set the T1 pin (PA.4) to a floating input (external event)
    DDRA  &= ~_BV(4);
    PORTA &= ~_BV(4);

    // set Timer1 to count external events
    TIMSK1 = _BV(TOIE1);      // enable overflow interrupt
    TCCR1B =   _BV(CS10)      // clock on external positive edge of T1 pin
        | _BV(CS11)
        | _BV(CS12);

    // set Timer0 for task timing (overflow once per ms)
    TCCR0B = TIMER0_PRESCALE;
    TCNT0  = TIMER0_PRELOAD;  // setup appropriate timeout
    TIMSK0 = _BV(TOIE0);      // enable timer0 overflow interrupt
}


ISR(TIM1_OVF_vect){   //timer1 overflow interrupt
    global_num_overflows++;
}

ISR(TIM0_OVF_vect){            //timer0 overflow interrupt @ 1kHz
    TCNT0 = TIMER0_PRELOAD;   // preload timer for 1kHz overflow rate
    if(global_task_timer_ms > 0){
        global_task_timer_ms--;
    }
}

int main(int argc, char *argv[]){
    double frequency_hz = 0;
    uint16_t num_overflows = 0;
    uint16_t num_positive_edges  = 0;
    setup_timers();
    sei(); // enable interrupts!
    for(;;){ //forever
        if(global_task_timer_ms == 0){ // wait for task to be scheduled
            ATOMIC_BLOCK(ATOMIC_FORCEON){
                num_overflows        = global_num_overflows; // copy the volatile variable into a local variable
                global_num_overflows = 0;                    // clear it for next time
                num_positive_edges   = TCNT1;                // copy num positive edge events to local variable
            }

            // calculate the 'average' frequency during this task period
            frequency_hz  = 1.0 * num_positive_edges;  // num edges since last overflow
            frequency_hz += num_overflows * 65536.0;   // edges per overflow of 16 bit timer
            frequency_hz /= (TASK_PERIOD_MS / 1000.0); // over the task interval in seconds

            global_task_timer_ms = TASK_PERIOD_MS;     // reschedule task
        }

        // use frequency_hz for whatever other processing you want to do
    }
    return 0;
}

Questo è un piccolo uso delle funzionalità hardware di Timer1 e libera un sacco di cicli di elaborazione rispetto al mio post originale.


2
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.