Messung von 0 - 1 MHz (0,25 Hz Auflösung) Rechteckwelle mit einer MCU


8

Ich muss die Frequenz der Rechteckwelle messen, die zwischen 0 und 1 MHz variieren kann und eine Auflösung von 0,25 Hz hat.

Ich habe mich noch nicht für einen Controller entschieden, aber es wird höchstwahrscheinlich einer der 20-Pin-Attiny sein.

Normalerweise würde ich Signale mit niedrigerer Frequenz messen, indem ich zwei Timer verwende, von denen einer im Timer-Erfassungsmodus konfiguriert ist, um beispielsweise die ansteigenden Flanken des externen Signals zu unterbrechen, und ein anderer Timer, der so eingestellt ist, dass er jede Sekunde unterbricht, daher der frühere Timer-Zählerregisterwert nach 1 Sekunde wäre gleich der Frequenz des Signals.

Diese Methode funktioniert jedoch offensichtlich nicht für die Erfassung von Signalen zwischen 0 und 1 MHz mit einer Auflösung von 0,25 Hz. Ich würde einen 22-Bit-Zähler benötigen (AFAIK 8-Bit-Mikros haben nur 8/16-Bit-Zähler).

Eine Idee, die ich hatte, war, das Signal vor dem Anlegen an das Mikro zu teilen, aber dies wäre unpraktisch, da das Signal durch 61 geteilt werden müsste, daher könnte die Frequenz nur alle 61 Sekunden aktualisiert werden, wo ich es alle paar Sekunden haben möchte .

Gibt es eine andere Methode, mit der die Frequenz etwa alle 4 Sekunden aktualisiert werden kann?


Aktualisieren:

Die einfachste Lösung besteht darin, einen externen Interrupt oder eine Timer-Erfassung zu verwenden, um die ansteigende Flanke des Signals zu unterbrechen und das isrInkrement als Variable vom Typ zu verwenden long int. Lesen Sie die Variable alle 4 Sekunden ab (um Frequenzen bis zu 0,25 Hz zu messen).


Update 2:

Wie JustJeff hervorhob, kann eine 8-Bit-MCU nicht mit einem 1-MHz-Signal mithalten, sodass eine Unterbrechung bei jeder ansteigenden Flanke und ein Inkrementieren einer long int...

Ich habe die von timororr vorgeschlagene Methode gewählt. Sobald ich mit der Implementierung fertig bin, werde ich zurückschicken und die Ergebnisse teilen. Vielen Dank an alle für Ihre Vorschläge.


Fortschrittsbericht:

Ich habe begonnen, einige der hier vorgestellten Ideen zu testen. Zuerst habe ich den Code von vicatcu ausprobiert. Es gab ein offensichtliches Problem, dass TCNT1 nach der Berechnung der Frequenz nicht behoben wurde - keine große Sache ...

Dann bemerkte ich beim Debuggen des Codes, dass etwa alle 2 bis 7 Mal, wenn die Frequenz berechnet wurde, die Überlaufzahl von Timer 1 (der Timer, der zum Zählen externer Ereignisse konfiguriert ist) um zwei kurz war. Ich habe dies auf die Latenz des Timer 0 ISR zurückgeführt und beschlossen, den if-Anweisungsblock vom ISR in den Hauptblock zu verschieben (siehe Snippet unten) und einfach ein Flag im ISR zu setzen. Einige Fehlerbehebungen haben gezeigt, dass die erste Messung in Ordnung ist, aber bei jedem weiteren Ablesen ist die Überlaufzahl von Timer 1 um 2 überschritten. Was ich nicht erklären kann. Ich hätte erwartet, dass sie nicht über ... liegt.

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

        .....
    }
}

Als nächstes beschloss ich, den Timrorrs-Vorschlag umzusetzen. Um das erforderliche Intervall (von ca. 15 ms zwischen jedem timer_isr-Interrupt) zu erzeugen, müsste ich die beiden 8-Bit-Timer kaskadieren, da der einzige 16-Bit-Timer auf dem Atmega16 zum Erfassen der ansteigenden Flanken des externen Signals verwendet wird.

Ich dachte, diese Lösung würde funktionieren und viel effizienter sein, da der größte Teil des Overheads auf die Timer verlagert wird und nur noch eine kurze ISR für die CPU übrig ist. Es war jedoch nicht so genau wie ich gehofft hatte, die Messungen wurden um ca. 70 Hz hin und her verschoben, was mir bei hohen Frequenzen nichts ausmachen würde, aber bei niedrigeren Frequenzen definitiv nicht akzeptabel ist. Ich habe nicht viel Zeit damit verbracht, das Problem zu analysieren, aber ich vermute, dass die Timer-Kaskadenanordnung nicht so genau ist, da ich eine ähnliche Anordnung wie der Timrorrs-Vorschlag auf einem weitaus langsameren 8051-Controller implementiert habe, der 2 16-Bit-Timer hatte und die Ergebnisse ziemlich genau waren.

Ich bin jetzt zu Vicatcus Vorschlag zurückgekehrt, aber ich habe die Frequenzberechnung in den Timer 0 isr verschoben (siehe Ausschnitt unten ). Dieser Code hat konsistente und einigermaßen genaue Messungen erzeugt. Mit ein wenig Kalibaration sollte die Genauigkeit ungefähr +/- 10 Hz betragen.

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

Wenn jemand andere Vorschläge hat, bin ich offen für sie, aber ich muss lieber keine Bereiche verwenden ... Ich bin auch nicht mehr darauf bedacht, eine Auflösung von 0,25% zu erreichen, es scheint nicht viel Sinn mit der Genauigkeit zu haben, die ich im Moment habe .


Es gibt eine relativ einfache Möglichkeit, dies mit einem Capture-Interrupt auf einem PIC und Timer 1 zu tun, die mit einer sehr hohen Geschwindigkeit laufen. Wenn Sie noch an anderen Methoden interessiert sind, lassen Sie es mich wissen und ich kann es in einer Antwort skizzieren.
Kortuk

Ich habe noch nicht damit begonnen, also bin ich immer noch interessiert.
Volting

Aus irgendeinem Grund ließ es mich nie wissen, dass Sie meinen Kommentar kommentiert hatten.
Kortuk

@Kortuk: Die Software benachrichtigt Sie nur, wenn ich eine Ihrer Antworten oder Fragen kommentiere. Es könnte Sie auch über diesen Kommentar informieren, weil ich @Kortuk davor gestellt habe. Aber das ist eine StackOverflow-Softwareänderung, und ich weiß nicht, ob sie in die StackExchange-Codebasis gelangt ist oder nicht.
Robert Harvey

nein, es hat mich nicht wissen lassen, dass du geantwortet hast, auch nicht mit dem @kortuk. Keine Sorge. Es sieht so aus, als ob eine Antwort gefunden wurde.
Kortuk

Antworten:


4

Wenn möglich, würde ich vorschlagen, einen Mikrocontroller auszuwählen, der eine Zähleroperation unter Verwendung der Timereingänge unterstützt. Anstatt einen Zähler innerhalb eines ISR manuell zu erhöhen (was bei hohen Frequenzen schnell dazu führt, dass die Mikrocontroller-Aktivität gesättigt wird), können die Hardware die Zählung durchführen. An diesem Punkt muss Ihr Code einfach auf Ihren periodischen Interrupt warten und dann die Häufigkeit berechnen.

Um den Bereich zu erweitern und den Frequenzzähler allgemeiner zu gestalten (wodurch die Notwendigkeit mehrerer Bereiche auf Kosten von etwas mehr Arbeit für die MCU entfällt), können Sie die folgende Technik verwenden.

Wählen Sie eine periodische Interruptrate, die Messgenauigkeit bei der höchsten Eingangsfrequenz ermöglicht. Dies sollte Ihre Zählergröße berücksichtigen (Sie müssen die Timer-Periode so auswählen, dass der Timer-Zähler bei der maximalen Eingangsfrequenz nicht überläuft). In diesem Beispiel gehe ich davon aus, dass der Eingangszählerwert aus der Variablen "timer_input_ctr" gelesen werden kann.

Fügen Sie eine Variable zum Zählen periodischer Interrupts hinzu (sollte beim Start auf 0 initialisiert werden). In diesem Beispiel werde ich diese Variable als "isr_count" bezeichnen. Die Unterbrechungsperiode ist in der Konstanten "isr_period" enthalten.

Ihr periodischer Interrupt sollte wie folgt implementiert werden: (C-Pseudocode):

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

Offensichtlich basiert dieses grobe Beispiel auf einer Gleitkomma-Mathematik, die möglicherweise nicht mit Low-End-Mikrocontrollern kompatibel ist. Es gibt Techniken, um dies zu überwinden, aber sie liegen außerhalb des Rahmens dieser Antwort.


1
Ausgezeichneter Timororr, der genau das macht, was ich will, ohne zusätzliche ICs zu kosten, was immer gut ist. Ich denke, ich war zu schnell, um die Möglichkeit der Lösung des Problems in der Software auszuschließen. Vielen Dank
Volt

@ Timrorr, ich bin an Ihren Gedanken zu meiner Antwort unten interessiert, wenn Sie Lust haben, sie zu lesen
vicatcu

7

Möglicherweise möchten Sie zwei (oder mehr) Bereiche verwenden. Die Probleme bei der Erfassung sehr niedriger Frequenzen unterscheiden sich etwas von den Problemen bei den höheren. Wie Sie bereits bemerkt haben, treten am oberen Ende Ihres Bereichs Probleme mit dem Gegenüberlauf auf.

Bedenken Sie jedoch, dass Ihre Genauigkeit am unteren Ende Ihres Bereichs darunter leidet, dass nicht genügend Zählungen im Register vorhanden sind. Sie sind sich nicht sicher, ob Sie wirklich zwischen 0,25 Hz und 0,5 Hz unterscheiden möchten, aber wenn Sie dies tun, müssen Sie tatsächlich vier Sekunden zählen, um dies zu tun.

Wenn Sie eine flache Auflösung von 0,25 Hz angeben, die streng interpretiert wird, können Sie 500.000,00 Hz von 500.000,25 Hz unterscheiden, was ein ziemlich hohes Maß an Präzision darstellt.

Aus diesen Gründen könnte das Entwerfen für unterschiedliche Bereiche das Problem der Zählergröße verringern. Wenn Sie beispielsweise für das untere Ende zufällig Zahlen ziehen, z. B. 0 bis 100 Hz, für Intervalle von 10 Sekunden zählen, erhalten Sie eine Auflösung von 0,1 Hz, und Ihr Zähler muss nur auf 1000 steigen, nicht einmal auf 10 Bit. Zählen Sie dann von 100 Hz bis 10 kHz für Intervalle von 1 Sekunde. Sie erhalten nur eine Auflösung von 1 Hz, aber Ihr Zähler muss nur bis zu 10.000 laufen, die noch kleiner als 16 Bit sind. Der obere Bereich von 10 kHz bis 1 MHz könnte nur 0,01 Sekunden lang zählen, und die maximale Anzahl würde immer noch nur 10.000 betragen, und obwohl Ihre Auflösung 100 Hz betragen würde, wäre dies eine angemessene Genauigkeit.


Ja, ich habe in der Aktualisierung meiner Frage (früher) erwähnt, dass ich bis zu 4 Sekunden zählen müsste ... und ich möchte in der Lage sein, zwischen etwa 500.000,00 Hz und 500.000,25 Hz zu unterscheiden. Ich hatte überlegt, verschiedene Bereiche zu verwenden. Ich könnte dies leicht mit dem Rest der Hardware verknüpfen, da das Signal 6 wählbare Bereiche hat, sodass ich wahrscheinlich einen einfachen 6 bis 3-Encoder entwerfen könnte, um anzuzeigen, welcher Bereich ... aber ich bin es nicht Sicher, wenn es notwendig wäre, wenn ich einen Hardware-Zähler mit einer Aktualisierungszeit von 4 Sekunden verwende, sollte dies die Probleme an beiden Enden des Spektrums
beheben

5

Sie können einen Hardware- und einen Software-Zähler mischen, indem Sie die Überläufe des Hardware-Zählers in einem ISR zählen.

Das Zählen jeder Flanke des Signals in einem ISR ist für ein 1-MHz-Signal zu langsam. Ich denke, Sie könnten auf diese Weise bis zu 50 kHz erzeugen.


Ja, Sie haben wahrscheinlich Recht - es wird für 1 MHz zu langsam sein, aber ich könnte mir vorstellen, dass ein 20MIPS-RISC-Prozessor besser als 50 kHz sein könnte. Wie auch immer, ich dachte auch darüber nach, einen 8-Bit-Binärzähler mit dem Signal zu takten und den Übertrag des Zählers mit dem externen Interrupt-Pin der MCU zu verbinden und dann die Frequenz des Signals als die Summe der Übertrag-Bit-Interrupts plus der O / P-Anzahl zu lesen Wert des Zählers alle n Sekunden. Ich vermute, das ist es, worauf Sie gekommen sind, als Sie eine Kombination aus Hardware- und Software-Zählern sagten.
Volt

Ich denke, das OP bezog sich auf den eingebauten Hardware-Zähler. Sie haben alle Überlauf-Interrupts, mit denen der Zählbereich verbessert werden kann.
JPC

@starblue, ist der Code, den ich unten geschrieben habe, das, was Sie mit Ihrer Antwort im Sinn hatten?
Vicatcu

2

Anstatt einen 1-Sekunden-Zähler zu erstellen, machen Sie ihn zu einem 0,1-Sekunden-Zähler und multiplizieren Sie den Zähler mit 10?

Wenn es nur darum geht, die Zählernummer zu speichern, können Sie dann keinen zusätzlichen Code verwenden, um zu verfolgen, wann der Zähler überläuft, und an einen anderen Speicherort schreiben, um die Zählung zu führen?


2
Ich denke, ich muss das Gehirn einfrieren lassen. Die einfachste Lösung, die ich denke, besteht darin, jedes Mal, wenn eine steigende Flanke erkannt wird, eine Variable vom Typ long int zu erhöhen. Lesen Sie diesen Wert einmal pro Sekunde und setzen Sie ihn dann auf Null zurück.
Volting

2
Eigentlich muss ich den Wert alle 4 Sekunden ablesen, um auf 0,25
Hz

2

Können Sie nicht einfach die Eingangserfassung und die Überlaufinterrupts eines 16-Bit-Timers (plus eine Variable) verwenden, um die Messung durchzuführen? So würde ich es mit dem ATTiny24A mit AVR-GCC machen (ungetestet und möglicherweise fehlerhaft natürlich):

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

... jedenfalls kompiliert es :)


BEARBEITEN Ich habe mir die von meinem Code ausgegebene LSS-Datei angesehen, und der generierte Code enthält zu viele Anweisungen, um bei 1 MHz mit einem 8-MHz-Takt nicht über sich selbst zu stolpern ... Selbst das einfache Inkrementieren um eine Zeile im TIM1_OVF_vect generiert 19 Anweisungen! Um 1-MHz-Ereignisse zu verarbeiten, müssten Sie auf jeden Fall optimieren, wahrscheinlich einige Dinge registrieren (wahrscheinlich num_overflows und capture_value_ticks), den Inline-Assembler verwenden (wichtige Dinge aus der lss-Datei stehlen) und die Verarbeitung aus den Interrupts in die Hauptdatei verschieben Schleife wo immer möglich.


Das Messen einer Frequenz unter Verwendung der Periode funktioniert recht gut mit langsamen Wellenformen (Sie verlassen sich darauf, dass der interne Timer viel schneller als das externe Signal ist), stößt jedoch mit zunehmender Frequenz des Eingangssignals schnell an eine Grenze. Grundsätzlich wird, wie Sie festgestellt haben, die Zeit, die innerhalb des Timer-Erfassungsinterrupts verbracht wird, dominant. Es bleibt keine Zeit mehr, um andere Teile des Codes auszuführen. Obwohl ich mit dem ATTiny nicht so vertraut bin, zeigt ein kurzer Blick auf das Datenblatt, dass timer / counter1 die externe Ereigniszählung unterstützt. Lassen Sie also die Hardware die Zählung übernehmen.
Timrorr

@ Timrorr, wow ja, das ist viel schlauer :) Ich habe den aktualisierten AVR-GCC-Code in einem separaten Beitrag veröffentlicht. Möchten Sie einen Blick darauf werfen und sehen, was Sie denken?
Vicatcu

2

Das Posten dieses Codes als Alternative gemäß dem Vorschlag von @ timrorr zu meinem vorherigen Beitrag. Dies wird für den ATTiny24A unter Verwendung des c99-Sprachstandards kompiliert, aber ich habe es darüber hinaus in keiner Weise getestet.

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

Dies ist eine nette kleine Nutzung der Hardwarefunktionen des Timer1 und spart eine Menge Verarbeitungszyklen im Vergleich zu meinem ursprünglichen Beitrag.


2
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.