Mesure de 0 à 1 MHz (résolution de 0,25 Hz) Squarewave à l'aide d'un MCU


8

J'ai besoin de mesurer la fréquence de l'onde carrée qui peut varier entre 0 et 1 MHz, et a une résolution de 0,25 Hz.

Je n'ai pas encore décidé sur quel contrôleur mais ce sera probablement l'un des 20pin Attiny.

Normalement, la façon dont je mesurerais les signaux de fréquence inférieure serait d'utiliser deux temporisateurs, l'un configuré en mode de capture de temporisation pour interrompre, disons, les fronts montants du signal externe et un autre temporisateur configuré pour interrompre toutes les secondes, donc l'ancienne valeur de registre du compteur de temporisateurs après 1 seconde serait égal à la fréquence du signal.

Cependant, cette méthode ne fonctionnera évidemment pas pour capturer des signaux compris entre 0 et 1 MHz avec une résolution de 0,25 Hz. Pour cela, j'aurais besoin d'un compteur 22 bits (les micros AFAIK 8 bits n'ont que des compteurs 8/16 bits).

Une idée que j'avais était de diviser le signal avant de l'appliquer au micro mais cela ne serait pas pratique car le signal devrait être divisé par 61 donc la fréquence ne pourrait être mise à jour que toutes les 61 secondes où je voudrais qu'il soit toutes les quelques secondes .

Existe-t-il une autre méthode qui permettrait de mettre à jour la fréquence, disons toutes les 4 secondes?


Mise à jour:

La solution la plus simple consiste à utiliser une interruption externe ou une capture temporisée pour interrompre sur le front montant du signal et faire de l' isrincrément une variable de type long int. Lisez la variable toutes les 4 secondes (pour permettre des fréquences jusqu'à 0,25 Hz à mesurer).


Mise à jour 2:

Comme l'a souligné JustJeff, un microcontrôleur 8 bits ne pourra pas suivre un signal de 1 MHz, ce qui exclut l'interruption à chaque front montant et l'incrémentation d'un long int...

J'ai choisi la méthode proposée par timororr. Une fois que j'arriverai à le mettre en œuvre, je posterai et partagerai les résultats. Merci pour toutes vos suggestions.


Rapport d'étape:

Iv'e a commencé à tester certaines des idées présentées ici. J'ai d'abord essayé le code de vicatcu. Il y avait un problème évident de TCNT1 non résolu après que la fréquence ait été calculée - pas un gros problème ...

Ensuite, j'ai remarqué lors du débogage du code qu'environ toutes les 2 à 7 fois la fréquence était calculée, le nombre de dépassements du temporisateur 1 (le temporisateur configuré pour compter les événements externes) serait court de deux. J'ai mis cela à la latence du minuteur 0 ISR et j'ai décidé de déplacer le bloc d'instructions if du ISR vers le principal (voir l'extrait ci-dessous) et de simplement définir un indicateur dans l'ISR. Un débogage a montré que la première mesure serait correcte, mais à chaque lecture suivante, le nombre de débordements du temporisateur 1 serait dépassé de 2, ce que je ne peux pas expliquer - je m'attendais à ce qu'il ne soit pas inférieur à ...

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

        .....
    }
}

Ensuite, j'ai décidé d'essayer de mettre en œuvre la suggestion de timrorrs. Pour générer l'intervalle nécessaire (d'environ 15 ms entre chaque interruption timer_isr), je devrais mettre en cascade les deux temporisateurs 8 bits car le seul temporisateur 16 bits de l'Atmega16 est utilisé pour capturer les fronts montants du signal externe.

Je pensais que cette solution fonctionnerait et serait beaucoup plus efficace car la plupart des frais généraux sont décalés vers les temporisateurs et un seul isr court est laissé pour le processeur à gérer. Cependant, ce n'était pas aussi précis que je l'espérais, les mesures se déplaçaient d'avant en arrière d'environ 70 Hz, ce qui ne me dérangerait pas aux hautes fréquences, mais ce n'est certainement pas acceptable aux basses fréquences. Je n'ai pas passé beaucoup de temps à analyser le problème, mais je suppose que l'arrangement en cascade de la minuterie n'est pas aussi précis que j'ai mis en place une disposition similaire à la suggestion de timrorrs sur un contrôleur 8051 beaucoup plus lent qui avait 2 temporisateurs 16 bits et les résultats étaient assez précis.

Je suis maintenant revenu à la suggestion de vicatcu, mais j'ai déplacé le calcul de fréquence dans le temporisateur 0 isr (voir l'extrait ci-dessous ), ce code a produit des mesures cohérentes et raisonnablement précises. Avec un peu de précision de calibrage, il devrait être d'environ +/- 10 Hz.

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;
    }                                                 
}       

Si quelqu'un a d'autres suggestions, je suis ouvert à eux, mais je n'ai pas à utiliser de plages ... Je n'ai plus non plus l'intention d'obtenir une résolution de 0,25%, il ne semble pas très utile avec le niveau de précision que j'ai en ce moment .


Il existe un moyen relativement facile de le faire en utilisant une interruption de capture sur un PIC et un temporisateur 1 fonctionnant à une vitesse très élevée. Si vous êtes toujours intéressé par d'autres méthodes, faites-le moi savoir et je peux le décrire dans une réponse.
Kortuk

Je n'ai pas encore commencé à travailler sur ce sujet, donc oui, je suis toujours intéressé.
volting du

Pour une raison quelconque, il ne m'a jamais fait savoir que vous aviez commenté mon commentaire.
Kortuk

@Kortuk: Le logiciel ne vous avertit que si je laisse un commentaire à l'une de vos réponses ou questions. Il pourrait également vous informer de ce commentaire, car j'ai mis @Kortuk devant. Mais c'est une modification du logiciel StackOverflow, et je ne sais pas si cela s'est infiltré dans la base de code StackExchange ou non.
Robert Harvey

non, il ne m'a pas fait savoir que vous aviez répondu, même avec le @kortuk. Pas de soucis. Il semble qu'une réponse ait été trouvée.
Kortuk

Réponses:


4

Si possible, je suggère de sélectionner un microcontrôleur qui prend en charge une opération de compteur à l'aide des entrées de minuterie; plutôt que d'incrémenter manuellement un compteur à l'intérieur d'un ISR (qui à des fréquences élevées finit rapidement par saturer l'activité du microcontrôleur), vous autorisez le matériel à gérer le comptage. À ce stade, votre code devient simplement une question d'attendre votre interruption périodique, puis de calculer la fréquence.

Pour étendre la plage et rendre le compteur de fréquence plus généralisé (en supprimant le besoin de plusieurs plages au détriment d'un peu plus de travail pour le MCU), vous pouvez utiliser la technique suivante.

Sélectionnez un taux d'interruption périodique qui permet une précision de mesure à la fréquence d'entrée la plus élevée; cela devrait prendre en compte la taille de votre compteur (vous devez sélectionner la période de la minuterie de sorte que le compteur de la minuterie ne déborde pas à la fréquence d'entrée maximale). Pour cet exemple, je suppose que la valeur du compteur d'entrée peut être lue à partir de la variable "timer_input_ctr".

Inclure une variable pour compter les interruptions périodiques (doit être initialisée à 0 au démarrage); pour cet exemple, je ferai référence à cette variable comme "isr_count". La période d'interruption est contenue dans la constante "isr_period".

Votre interruption périodique doit être implémentée en tant que (pseudo-code C):

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;
  }
}

Évidemment, cet exemple grossier repose sur des calculs en virgule flottante qui peuvent ne pas être compatibles avec les microcontrôleurs bas de gamme, il existe des techniques pour surmonter cela, mais elles sortent du cadre de cette réponse.


1
Excellent timororr, cela fera exactement ce que je veux sans frais de circuits intégrés supplémentaires, ce qui est toujours bon, je pense que j'ai été trop rapide pour écarter la possibilité de résoudre le problème dans le logiciel. Merci
volts

@timrorr, je suis intéressé par vos réflexions sur ma réponse ci-dessous si vous avez envie de la lire
vicatcu

7

Vous voudrez peut-être envisager d'avoir deux (ou plus) plages. Les problèmes de capture de fréquences très basses sont quelque peu différents de ceux des fréquences supérieures. Comme vous l'avez déjà remarqué, dans le haut de gamme, vous avez des problèmes de débordement de compteur.

Mais pensez au bas de votre plage, votre précision souffrira de ne pas avoir suffisamment de comptes dans le registre. Je ne sais pas si vous voulez vraiment faire la différence entre 0,25 Hz et 0,5 Hz, mais si vous le faites, vous devrez en fait compter quatre secondes pour le faire.

En outre, la spécification d'une résolution plate de 0,25 Hz, strictement interprétée, signifie que vous seriez en mesure de discerner 500 000,00 Hz de 500 000,25 Hz, ce qui est un degré de précision assez élevé.

Pour ces raisons, la conception pour des plages distinctes pourrait atténuer le problème de taille de compteur. Tirer des nombres au hasard, par exemple, pour le bas de gamme, disons 0 à 100 Hz, comptez pour des intervalles de 10 secondes, et vous obtenez une résolution de 0,1 Hz, et votre compteur n'a besoin que d'aller jusqu'à 1000, pas même 10 bits. Ensuite, de 100 Hz à 10 kHz, comptez pour des intervalles de 1 seconde; vous n'obtenez qu'une résolution de 1 Hz, mais votre compteur n'a besoin que de fonctionner jusqu'à 10 000 encore plus petit que 16 bits. La plage supérieure de 10 kHz à 1 MHz pourrait compter pour seulement 0,01 s, et le nombre maximal ne serait toujours que de 10 000 et bien que votre résolution soit de 100 Hz, ce serait une précision raisonnable.


Oui, j'ai mentionné que dans la mise à jour de ma question (plus tôt), je devrais compter jusqu'à 4 secondes pour ... et oui, je voudrais pouvoir faire la différence entre, disons, 500 000,00 Hz et 500 000,25 Hz. J'avais pensé à utiliser différentes gammes, je pourrais facilement lier cela au reste du matériel car le signal a 6 gammes sélectionnables, donc je pourrais probablement concevoir un simple encodeur 6 à 3 pour indiquer quelle gamme ... mais je ne suis pas si vous serait nécessaire si j'utilise un compteur matériel couplé à 4 secondes de temps de mise à jour, cela devrait prendre soin des problèmes à chaque extrémité du spectre
volting

5

Vous pouvez mélanger un compteur matériel et logiciel en comptant les débordements du compteur matériel dans un ISR.

Le comptage de chaque front du signal dans un ISR sera trop lent pour un signal à 1 MHz. Je pense que vous pourriez faire jusqu'à environ 50 kHz de cette façon.


Oui, vous avez probablement raison - il sera trop lent pour 1 MHz, mais j'imagine qu'un processeur RISC 20MIPS pourrait faire mieux que 50 kHz. Quoi qu'il en soit, j'envisageais également de synchroniser un compteur binaire 8 bits avec le signal et de connecter le report du compteur à la broche d'interruption externe du MCU, puis de lire la fréquence du signal comme la somme des interruptions du bit de report plus le nombre d'o / p valeur du compteur toutes les n secondes, je suppose que c'est ce que vous vouliez dire lorsque vous avez dit une combinaison de compteurs matériels et logiciels.
volte

Je pense que l'OP faisait référence au compteur matériel intégré. Ils ont tous des interruptions de débordement qui peuvent être utilisées pour améliorer la plage de comptage.
jpc

@starblue, est-ce le code que j'ai écrit ci-dessous que vous aviez en tête avec votre réponse?
vicatcu

2

Au lieu de faire un compteur de 1 seconde, faites-en un compteur de 0,1 seconde et multipliez le compte par 10?

S'il s'agit simplement de stocker le numéro de compteur, ne pouvez-vous pas utiliser un code supplémentaire pour savoir quand le compteur est sur le point de déborder et écrire dans un autre emplacement mémoire pour conserver le décompte?


2
Je pense que je dois avoir eu un gel du cerveau .. la solution la plus simple, je pense, est juste d'incrémenter une variable de type long int à chaque fois qu'un front montant est détecté. Lisez cette valeur une fois par seconde, puis remettez-la à zéro.
volte

2
En fait, je devrai lire la valeur toutes les 4 secondes pour mesurer jusqu'à 0,25 Hz
2010

2

Ne pouvez-vous pas simplement utiliser la capture d'entrée d'un temporisateur 16 bits et les interruptions de débordement (plus une variable) pour effectuer la mesure? Voici comment je le ferais avec l'ATTiny24A avec AVR-GCC (non testé et potentiellement buggé bien sûr):

#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;
} 

... en tout cas, ça compile :)


EDIT J'ai regardé la sortie du fichier lss de mon code, et le code généré a trop d'instructions pour ne pas se déclencher à 1 MHz avec une horloge de 8 MHz ... même l'incrémentation simple d'une ligne dans le TIM1_OVF_vect génère 19 instructions! Donc, pour gérer les événements à 1 MHz, vous devez certainement optimiser, enregistrer probablement allouer des trucs (probablement num_overflows et capture_value_ticks), utiliser l'assembleur en ligne (voler les trucs importants du fichier lss), et déplacer le traitement hors des interruptions et dans le principal boucle autant que possible.


La mesure d'une fréquence à l'aide de la période fonctionne assez bien avec des formes d'onde lentes (vous comptez sur la minuterie interne étant beaucoup plus rapide que le signal externe) mais atteint rapidement une limite à mesure que la fréquence du signal d'entrée augmente. Fondamentalement, comme vous l'avez constaté, le temps passé à l'intérieur de l'interruption de capture du minuteur devient dominant; il n'y a plus de temps pour l'exécution d'autres parties du code. Bien que je ne sois pas très familier avec ATTiny, un rapide coup d'œil à la fiche technique montre que timer / counter1 prend en charge le comptage d'événements externes, alors laissez le matériel gérer le comptage.
timrorr

@timrorr, wow oui c'est une façon beaucoup plus intelligente de le faire :) J'ai posté le code AVR-GCC mis à jour dans un article séparé. Vous voulez jeter un œil et voir ce que vous en pensez?
vicatcu

2

Publier ce code comme alternative par la suggestion de @ timrorr à mon post précédent. Cela compile pour l'ATTiny24A en utilisant la norme de langage c99, mais je ne l'ai pas testé de quelque manière que ce soit.

#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;
}

C'est une belle petite utilisation des capacités matérielles du Timer1 et libère une tonne de cycles de traitement par rapport à mon article d'origine.


2
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.