PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : TTL-Signal am ATmega8



Liquidator
20.03.2013, 22:29
Gute Tageszeit liebe Gemeinde,

nach einer erfolgreichen Konstruktion eines Frequenzzählers, würde ich gern versuchen das Offset von einer Sekunde zu umgehen,
indem ich bei bekannter uC-Frequenz die Impulsanzahl zwischen zwei steigenden Flanken messe. So die Theorie.
Praktisch bin ich mir nicht sicher und wende mich deshalb an euch.


TCCR0 |= (1<<CS02) | (1<<CS01) | (1<<CS00);
Diese Zeile bewirkt das Starten des Timer0 bei steigender Flanke am T0 Eingang. Beim Überlauf wird eine Zählvariable inkrementiert usw, die übliche Geschichte.

Meine Frage wäre folgende: Ich starte zwar den Timer, aber wie soll ich ihm sagen, dass er auch bei der nächsten Flanke aufhören sollte?
Denn so scheint mir das Ganze noch nicht vollständig zu sein ;)

Anbei die relevanten Codestellen:

Ich bedanke mich im Voraus,
Nik


ISR(TIMER0_OVF_vect)
{
z++;
TIFR = (1<<TOV0); //Beim Überlauf von Timer0 und dem anschließenden Interrupt erfolgt eine Zurücksetzung auf Null
}

int main(void)
{
// Definition von Ein- und Ausgängen des uC Boards
DDRC = 0xFF; // PORTC als Ausgang - über PORTC werden die jeweiligen Segmente einer 7-Seg. Anzeige gesteuert
DDRD = 0xEF; // außer dem PD4 für den TTL-Pegel sind alle Pins als Ausgänge definiert
PORTC = 0b00000000;


TCCR0 |= (1<<CS02) | (1<<CS01) | (1<<CS00); // Extern anliegendes Signal an T0, Interrupt bei steigender Flanke, Starten des Timer0

TIMSK |= (1<<TOIE0); // Aktiviere den Interrupt von Timer0 beim Überlauf


sei(); // Interruptbehandlung ein



while(1) // unendliche Schleife
{

zaehler0_aktuell = TCNT0; // der aktuelle Timer0 Wert wird in count kopiert
zaehler0_ovf = z; // Anzahl der Überläufe wird in zaehler0_ovf kopiert
z= 0;
TCNT0 = 0;

if ((zaehler0_ovf==0)&&(zaehler0_aktuell==0))
{
freq = 0;
}
else
{
// Die abschließende Summierung aller Anteile samt Kompensation von Abweichungen
freq = (uint16_t) (F_CPU/(0.5+((256.0*zaehler0_ovf + zaehler0_aktuell)*korrekturfaktor)));
}

zahl_ausgeben(freq);

}
return 0;
}

markusj
21.03.2013, 00:32
Gar nicht. Diese Betriebsart ist für den Betrieb mit einer externen Taktquelle, zum Zählen von Ereignissen oder zum Messen von Frequenzen mit niedriger Wiederholrate gedacht. Wenn du sehr schnelle Frequenzmessungen durchführen möchtest, hast du im wesentlichen zwei Möglichkeiten:
1. Input Capture. Diese Funktion größerer Timer ist genau für den von dir genannten Zweck gedacht, sie erfasst die Zeit zwischen zwei Ereignissen.
2. Manuelles Capture: Du pollst den Pin oder verwendest einen der Externen Interrupts um den Zeitstempel eines kontinuierlich laufenden (wie bei 1) Timers einzulesen.

mfG
Markus

Liquidator
21.03.2013, 11:06
Als Erstes vielen Dank für die Tipps markusj, eine Verständnisfrage hätte ich allerdings trotzdem. Den Code habe ich soweit auf den ICP1 abgestimmt (s.u.), doch verstehe ich nicht die Abfrage einzusetzen, wann denn
nun die zweite Flanke eintraf.



TIMSK |= (1<<TICIE1); // ICR1 aktiviert
TCCR1B |= (1<<ICES1); // Trigger bei steigender Flanke


Der Codeausschnitt startet den Timer1 sobald eine steigende Flanke am ICR1-Pin eintrifft und ich kann wie gewöhnlich die Anzahl der Überläufe und aktuellen Zähler1-Stand kopieren und auswerten.
Doch wann tritt die nächste Flanke ein?



if (ICF == 1) {};


Das ist das Einzige, was ich als Option fand, doch steht in der Doku, dass das Bit bereits gesetzt ist, sobald das erste Ereignis,also die erste Flanke, stattfand.
Ich hoffe, dass es verständlich war ;)

MfG Nik

markusj
21.03.2013, 11:18
Gibt es einen speziellen Grund, warum du Interrupts vermeidest, Nik?

Einfache Variante: Letzten Zählerstand in einer Variablen speichern, Interrupt anschalten und jedes Mal wenn der Input-Capture-Interrupt kommt, kannst du durch Neu-Alt die Periodendauer (in Zählerticks) ausrechnen.
Umständliche Variante: Du pollst ICF (übrigens mit if (TIFR & (1 << ICF1), nicht mit deinem Codeschnipsel) und machst das was der Interrupt tun würde manuell. Und was das gesetzte Bit angeht:


ICF1 is automatically cleared when the Input Capture Interrupt Vector is executed. Alternatively, ICF1 can be cleared by writing a logic one to its bit location.

mfG
Markus

Liquidator
21.03.2013, 12:35
Peinlicherweise, weil mir ein Ansatz fehlt - wenn du den (TIMER1_CAPTURE_vect)-Ansatz meinst :)



ISR(TIMER1_OVF_vect)
{
z++;
TIFR = (1<<TOV1); //Beim Überlauf von Timer1 und dem anschließenden Interrupt erfolgt eine Zurücksetzung auf Null
}

ISR(TIMER1_CAPTURE_vect)
{


}



int main(void)
{
// Definition von Ein- und Ausgängen des uC Boards
DDRB = 0xFE; // Mit Ausnahme des ICP1-Pins alles als Ausgang
DDRC = 0xFF; // PORTC als Ausgang - über PORTC werden die jeweiligen Segmente einer 7-Seg. Anzeige gesteuert
DDRD = 0xFF; // PORTD als Ausgang
PORTC = 0b00000000;


TIMSK = (1<<TICIE1) | (1<<TOIE1); // ICR1 und Ovf aktiviert
TCCR1B = (1<<ICES1) | (1<<CS10) // Trigger bei steigender Flanke, Vorteiler = 1



sei(); // Interruptbehandlung ein



while(1) // unendliche Schleife
{

zaehler1_aktuell = TCNT1; // der aktuelle Timer1 Wert wird in zaehler1_aktuell kopiert
zaehler1_ovf = z; // Anzahl der Überläufe wird in zaehler1_ovf kopiert
z= 0;
TCNT1 = 0;

if ((zaehler1_ovf==0)&&(zaehler1_aktuell==0))
{
freq = 0;
}
else
{
// Die abschließende Summierung aller Anteile samt Kompensation von Abweichungen
freq = (uint16_t) (F_CPU/(0.5+(65535.0*zaehler1_ovf + zaehler1_aktuell)));
}

zahl_ausgeben(freq);


}

return 0;
}


Mit einem Interrupt sichere ich den Timer1 beim Überlauf.
Wenn das TIMER1-Capture Ereignis eintritt,k nann ich doch nicht den Zählerstand auswerten, weil es möglicherwise erst die erste Flanke war?

MfG Nik

- - - Aktualisiert - - -

So, habe hier mal was eingefügt - einfache Zählvariable, die inkrementiert wird, sobald ein Timer1-Capture Ereignis eintrifft. Erst beim zweiten Durchlauf (Zähvariable=2) werden die Timerwerte gesichert und angewendet.



#define F_CPU 16000000 // uC läuft mit 16MHz


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


volatile unsigned short z=0;
volatile unsigned short c=0;

int zaehler1_aktuell = 0; // aktueller Timer0 Wert
int zaehler1_ovf = 0; // Anzahl der Überläufe des Timer0

uint16_t freq = 0;


ISR(TIMER1_OVF_vect)
{
z++;
TIFR = (1<<TOV1); //Beim Überlauf von Timer1 und dem anschließenden Interrupt erfolgt eine Zurücksetzung auf Null
}

ISR(TIMER1_CAPTURE_vect)
{
//if(TIFR&(1<<ICF1))
c++;


}



int main(void)
{
// Definition von Ein- und Ausgängen des uC Boards
DDRB = 0xFE; // Mit Ausnahme des ICP1-Pins alles als Ausgang
DDRC = 0xFF; // PORTC als Ausgang - über PORTC werden die jeweiligen Segmente einer 7-Seg. Anzeige gesteuert
DDRD = 0xFF; // PORTD als Ausgang
PORTC = 0b00000000;


TIMSK = (1<<TICIE1) | (1<<TOIE1); // Interrupts akivieren, Capture und Overflow
TCCR1B = (1<<ICES1) | (1<<CS10); // Input Capture Edge, kein Prescaler


sei(); // Interruptbehandlung ein



while(1) // unendliche Schleife
{
if (C==2)
{
zaehler1_aktuell = TCNT1; // der aktuelle Timer1 Wert wird in count kopiert
zaehler1_ovf = z; // Anzahl der Überläufe wird in zaehler1_ovf kopiert
z= 0;
TCNT1 = 0;

if ((zaehler1_ovf==0)&&(zaehler1_aktuell==0))
{
freq = 0;
}
else
{
// Die abschließende Summierung aller Anteile samt Kompensation von Abweichungen
freq = (uint16_t) (F_CPU/(0.5+(65535.0*zaehler1_ovf + zaehler1_aktuell)));
}

zahl_ausgeben(freq);

c=0;
}


}

return 0;
}



Würde das eine optimale Lösung darbieten?
MfG Nik

markusj
21.03.2013, 13:00
Nein. Hast du dir Mal die Erläuterung zum Input-Capture im Datenblatt durchgelesen? Da wird fast alles für dich automatisch gemacht!

Kurzbeschreibung: Die Input-Capture-Einheit besteht im wesentlichen aus einem zusätzlichen Register, das ICR. Jedes Mal wenn ein Ereignis auftritt, kopiert die Input-Capture-Einheit den aktuellen Wert aus dem Zählerregister TCNT in das ICR.

Was kannst du damit Anfangen? Du kannst die Zeit für eine komplette Periode (High -> Low -> High ->Low) sehr einfach messen. Die Dauer ist nämlich genau der alte Wert vom ICR (den du dir in eine Variable sicherst) Minus den neuen Wert. Damit bekommst du die Anahl der Zählertakte die der Periodendauer entsprechen. Anhand der von dir gewählten Timer-Konfiguration (Prescaler, CPU-Takt) kannst du ausrechnen, wie lange ein Zählertakt dauert. Und schon hast du die Periodendauer in Sekunden, und die Frequenz ist genau 1/Periodendauer.

mfG
Markus

Liquidator
21.03.2013, 15:44
Gut, wenn es einen T0-Vektor auf Flanken gäbe, würde ich ohne auskommen...
Nach zahlreichen Recherchen scheint mir langsam die Bedeutung klar zu werden - sorri for maj inglisch pliiz :D

Das scheint wirklich die genauste Methode zu sein, deshalb hier noch einmal der Code :) Dürfte diesmal nicht viel falsch sein,
da ich diesmal bisschen von anderen abgeschaut habe ;)



#define F_CPU 16000000 // uC läuft mit 16MHz

#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif


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


volatile unsigned short z=0;
volatile unsigned short UpdateDisplay; // Verzögerung
volatile unsigned int Startzeit = 0; // ICR-Wert bei 1. steigender Flanke sichern
volatile unsigned int Endzeit = 0; // ICR-Wert bei 2. steigenden Flanke sichern

int zaehler1_ovf = 0; // Anzahl der Überläufe des Timer1

uint16_t freq = 0;
uint16_t zaehlschritte = 0;


ISR(TIMER1_OVF_vect)
{
z++;
TIFR = (1<<TOV1); //Beim Überlauf von Timer1 und dem anschließenden Interrupt erfolgt eine Zurücksetzung auf Null
}

ISR(TIMER1_CAPT_vect)
{

static unsigned short ErsteFlanke = TRUE;

if(UpdateDisplay) // Das Display wurde mit den Ergebnissen der vorhergehenden
{ // Messung noch nicht aktualisiert. Die nächste Messung
return; // verzögern, bis die Start- und EndTime-Variablen wieder
} // gefahrlos beschrieben werden können


// Bei der ersten Flanke beginnt die Messung, es wird der momentane
// Timer beim Input Capture als Startwert gesichert

if(ErsteFlanke)
{
Startzeit = ICR1;
z = 0;
ErsteFlanke = FALSE; // Nach der nächsten Flanke beginnt die Ausgabe
}


// das ist die zweite Flanke im Messzyklus. Die Messung wird gestoppt

else
{
Endzeit = ICR1;
UpdateDisplay = TRUE; // Eine vollständige Messung. Sie kann ausgewertet werden
ErsteFlanke = TRUE; // Bei der nächsten Flanke beginnt der nächste Messzyklus
}

}



int main(void)
{
// Definition von Ein- und Ausgängen des uC Boards
DDRB = 0xFE; // Mit Ausnahme des ICP1-Pins alles als Ausgang
DDRC = 0xFF; // PORTC als Ausgang - über PORTC werden die jeweiligen Segmente einer 7-Seg. Anzeige gesteuert
DDRD = 0xFF; // PORTD als Ausgang
PORTC = 0b00000000;


TIMSK = (1<<TICIE1) | (1<<TOIE1); // Interrupts akivieren, Capture und Overflow
TCCR1B = (1<<ICES1) | (1<<CS10); // Input Capture Edge, kein Prescaler


sei(); // Interruptbehandlung ein



while(1) // unendliche Schleife
{
if(UpdateDisplay) // Erst nach zweiter Flanke ausführen
{
zaehler1_ovf = z; // Anzahl der Überläufe wird in zaehler1_ovf kopiert
z= 0;


zaehlschritte = (uint16_t)(0.5+(65536.0*zaehler1_ovf) + Endzeit - Startzeit);


if (zaehlschritte==0)
{
freq = 0;
}
else
{
freq = (uint16_t) (F_CPU/(zaehlschritte));
}

zahl_ausgeben(freq);

UpdateDisplay = FALSE;
}


}

return 0;
}



Ich möchte mich bei dir bedanken für deine geduldigen Erklärungen Markus :)
MfG Nik

Besserwessi
21.03.2013, 17:00
So im Groben könnte es funktionieren, wenn auch noch relativ unübersichtlich. Bei den Überläufen gibt es aber noch ein paar Tücken - das wird so wie gezeigt nur meistens funktionieren, je nach Laufzeit für die Dispayroutinen ggf. auch weniger oft. Das erste Problem dabei ist es, das der Überlauf erst im Hauptprogramm ausgewertet wird - da sind also noch ein paar Verzögerungen möglich, so dass noch ein Überlauf dazu kommen kann bis das Hauptprogramm so weit ist.

Dazu kommt hier noch, das in dem Code oben das Ergebnis in einer 16 Bit variable gespeichert wird. In dem Fall könnte man die Behandlung der Überläufe auch gleich ganz weglassen. Die Überläufe braucht man erst, wenn die Zeiten länger werden.

Das 2. etwas schwierige Problem ist, dass der ICP-interrupt und der Overflow interrupt fast gleichzeitig kommen können - das kommt zwar nur selten vor (etwa alle 50000 Zyklen), das Problem lässt sich aber auch lösen. Im RN-Wissen Bereich unter Timer/Counter(AVR) (http://www.rn-wissen.de/index.php/Timer/Counter_(Avr)) steht wie es geht, incl. einem fertigen Programm dazu.

Liquidator
21.03.2013, 18:38
Danke Besserwessi, dass du mich auf diesen Umstand aufmerksam gemacht hast. Die Variable zaehlschritte soll natürlich größer sein, werd' wohl 'nen unsigned long Datentyp deklarieren - bei F_CPU von 16Mio wird es schwer mit 16 bit :)
Den zweiten von dir angesprochenen Punkt habe ich zwar befürchtet, dachte mir doch, dass der eh nie eintreten wird. Werde das aber doch berücksichtigen, danke für den Hinweis.

Mal eine Frage als Anfänger - Quelltext unübersichtlich: Ist das Ästhetik oder nicht optimierter Code?

MfG Nik

markusj
21.03.2013, 20:02
Hi Nik,

ein paar Dinge:
1. Du brauchst im Überlauf-Interrupt das Overflow-Flag nicht löschen. Jeder Interrupt löscht automatisch sein eigenes Flag bei Aufruf der ISR.
2. Du wählst möglicherweise einen zu komplexen Ansatz für die Zeitmessung. Welche Frequenz willst du denn ermitteln? Wenn du den Prescaler entsprechend wählst, solltest du ohne den Zusatz-Zähler auskommen.
3. Ich würde die Anzahl der vergangenen Ticks in der ISR ermitteln und direkt in eine Variable schreiben. Du brauchst also nur Startzeit und Ticks als Variable. Ticks errechnet sich durch ICR1 - Startzeit, fertig. Zusätzlich muss Startzeit dann auch nicht mehr volatile sein.
4. Du solltest die Ergebnisse mehrere Messzyklen mitteln, um stabilere Werte zu bekommen. Das hängt natürlich auch ein Stück von den Eigenschaften des vermessenen Signals ab.
5. Warum machst du deine Multiplikation mit Gleitkommazahlen?

mfG
Markus

@Unübersichtlich: Beides. "Schöner Code" ist zumindest weniger Fehleranfällig und oft auch noch schneller.

Liquidator
21.03.2013, 20:20
Guten Abend Markus - ersteinmal danke für die Rückmeldung.
Zu den paar Dingern:
1. Das war mir nicht völlig bewusst, deshalb mal nach dem Motto " vorsichthalber, man weiß ja nie" vorgegangen :)
2. Eigentlich war mein Ziel den Ansatz zu vereinfachen - meine erste Version benötigt zwei Timer zur Zeitmessung und einen für Multiplexing. Damit hätte ich keine Timer mehr zur Verügung :D
Diese Version funktioniert auch wirklich gut - benötigt wird der Ganzzahlenbereich von 1-9999 Hz. Wurde alles perfekt angezeigt. Verbessern wollte ich das wegen des Offsets von einer Sekunde, die mit einem Timer gemessen wurde, während mit dem anderen Timer Flanken registriert wurden.
3. Du hast Recht, diese Optimierungen kommen nur nicht sofort in den Sinn, da braucht man die Erfahrung :)
4. Da ich keine Nachkommastellen brauche, gehe ich von aus, dass es mit einem Zyklus geht. Wenn nicht, habe ich da diese Option des Noise Canceler gefunden...
5. Falls eine lineare Abweichung aufgrund des Quarzes eintritt brauche ich da nur einen korrekturfaktor einzusetzen und nicht mit datentypen spielen - so der Gedanke :)

Danke für die Tipps - die Sache mit überflüssigen Variablen werde ich so weit es geht zu klären versuchen.
Anbei mein "Fortschritt" (mit noch nicht optimierten Variablen) ;)

MfG Nik



#define F_CPU 16000000 // uC läuft mit 16MHz

#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif


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


volatile unsigned short z=0;
volatile unsigned short UpdateDisplay; // Verzögerung
volatile unsigned int Startzeit = 0; // ICR-Wert bei 1. steigender Flanke sichern
volatile unsigned int Endzeit = 0; // ICR-Wert bei 2. steigenden Flanke sichern
volatile unsigned int LowByte = 0;
volatile unsigned int HighByte = 0;

int zaehler1_ovf = 0; // Anzahl der Überläufe des Timer1

uint16_t freq = 0;
unsigned long zaehlschritte = 0;


ISR(TIMER1_OVF_vect)
{
z++;
//TIFR = (1<<TOV1); //Beim Überlauf von Timer1 und dem anschließenden Interrupt erfolgt eine Zurücksetzung auf Null
}


ISR(TIMER1_CAPT_vect)
{
static unsigned short ErsteFlanke = TRUE;

if(UpdateDisplay) // Das Display wurde mit den Ergebnissen der vorhergehenden
{ // Messung noch nicht aktualisiert. Die nächste Messung
return; // verzögern, bis die Start- und EndTime-Variablen wieder
} // gefahrlos beschrieben werden können


LowByte = ICR1L; // low Byte zuerst, high Byte wird gepuffert
HighByte = ICR1H;

// overflow verpasst, wenn ICR1H klein und wartender Overflow Interrupt

if ((HighByte < 128) && (TIFR & (1<<TOV1)))
{ // wartenden timer overflow Interrupt vorziehen
++z;
TIFR = (1<<TOV1); // timer overflow int. löschen, da schon hier ausgeführt
}


// Bei der ersten Flanke beginnt die Messung, es wird der momentane
// Timer beim Input Capture als Startwert gesichert

if(ErsteFlanke)
{
Startzeit = ICR1;
z = 0;
ErsteFlanke = FALSE; // Nach der nächsten Flanke beginnt die Ausgabe
}

// das ist die zweite Flanke im Messzyklus. Die Messung wird gestoppt

else
{
Endzeit = ICR1;
UpdateDisplay = TRUE; // Eine vollständige Messung. Sie kann ausgewertet werden
ErsteFlanke = TRUE; // Bei der nächsten Flanke beginnt der nächste Messzyklus
}

}



int main(void)
{
// Definition von Ein- und Ausgängen des uC Boards
DDRB = 0xFE; // Mit Ausnahme des ICP1-Pins alles als Ausgang
DDRC = 0xFF; // PORTC als Ausgang - über PORTC werden die jeweiligen Segmente einer 7-Seg. Anzeige gesteuert
DDRD = 0xFF; // PORTD als Ausgang
PORTC = 0b00000000;


TIMSK = (1<<TICIE1) | (1<<TOIE1); // Interrupts akivieren, Capture und Overflow
TCCR1B = (1<<ICES1) | (1<<CS10); // Input Capture Edge, kein Prescaler


sei(); // Interruptbehandlung ein



while(1) // unendliche Schleife
{
if(UpdateDisplay) // Erst nach zweiter Flanke ausführen
{
zaehler1_ovf = z; // Anzahl der Überläufe wird in zaehler1_ovf kopiert
z= 0;


zaehlschritte = (unsigned long)(0.5+(65536.0*zaehler1_ovf) + Endzeit - Startzeit);


if (zaehlschritte==0)
{
freq = 0;
}
else
{
// Die abschließende Summierung aller Anteile samt Kompensation von Abweichungen
freq = (uint16_t) (F_CPU/(zaehlschritte));
}

zahl_ausgeben(freq);

UpdateDisplay = FALSE;
}


}

return 0;
}



EDIT: Wieso ich keinen Prescaler verwende? Der Gedanke war, dass je mehr Signale pro Zeiteinheit gemessen werden können, desto genauer auch die Anzeige sein wird. :)


EDIT 2:

Änderungen:



volatile unsigned long Zeitdifferenz = 0;

ISR(TIMER1_CAPT_vect)
{

static unsigned short ErsteFlanke = TRUE;
static unsigned long Startzeit = 0; // Startzeit als static deklariert
...
{
Zeitdifferenz = ICR1 - Startzeit; // Jetzt wird
UpdateDisplay = TRUE; // Eine vollständige Messung. Sie kann ausgewertet werden
ErsteFlanke = TRUE; // Bei der nächsten Flanke beginnt der nächste Messzyklus
}
}
...
int main(void)
{
...

zaehlschritte = (unsigned long)(0.5+(65536.0*zaehler1_ovf) + Zeitdifferenz);
...
}

Besserwessi
21.03.2013, 21:58
Die Auswertung der Zahl der Überläufe muss man schon in der ICP ISR machen. Im Hauptprogramm kann man da schon um 1 oder gar mehr zu hoch liegen.

Bei 10 kHz als maximale Frequenz hat man nur 1600 Zyklen - das gibt mit nur einer Periode noch keine gute Auflösung. Das reicht noch nicht ganz für 1 Hz Auflösung. Für genaue Werte wäre es da schon gut die Zeit über mehr Perioden zu messen. Für 1 Hz Auflösung auch bei der oberen Frequenzgrenze sollten es schon wenigstens 10000 Timerschritte sein. Das macht auch nur eine Messzeit von etwa 1/1000 Sekunde als Minimum.

Das Zusammenfügen der Zahlen geht wirklich einfacher mit ganzen Zahlen. Da braucht man keine Fließkommazahlen. Wenn man dann für die genaue Quarzfrequenz noch einen krummen Faktor rein bekommt, kann man das danach machen.

Der Noise Cancler Modus dient zum Abfangen von ganz kurzen Glitches, also Störungen die 1-3 Zyklen lang sind, z.B. durch Reflexion in einem Kabel oder Überschwinger an der Eingangsstufe. Das wird in den meisten Fällen keinen Unterschied machen.

markusj
21.03.2013, 22:24
EDIT: Wieso ich keinen Prescaler verwende? Der Gedanke war, dass je mehr Signale pro Zeiteinheit gemessen werden können, desto genauer auch die Anzeige sein wird. :)

Du misst aber nicht Signale je Zeiteinheit sondern Zeiteinheiten (Ticks) je Signal/Ereignis.

@Frequenzbereich: Das ist etwas unangenehm. Für niedrige Frequenzen brauchst du tatsächlich einen zusätzlichen Zähler, da du ja für 1 Hz die Ticks einer ganzen Sekunde zählen musst. Die 10kHz schöpfen den Wertebereich dagegen nicht Mal ansatzweise aus. Eine clevere/faule Lösung wäre eine Bereichsumschaltung die je nach Eingangssignal den günstigsten Prescaler wählt. Einfacher ist vermutlich die Erweiterung des Zählbereichs auf 32 Bit ;)

mfG
Markus

Liquidator
21.03.2013, 22:45
Jetzt wird's wirklich interessant :D

Ich befürchte es auch, dass es bei kleineren Frequenzen nicht ausreichen wird. Nach dem Fertigstellen der ersten Version hatte ich bereits einen ähnlichen Gedanken wie eben Markus. Auch eine Bereichsumschaltung, aber nicht unter Verwendung des Vorteilers, sondern der zwei Modi - einmal der alten Version und der aktuellen.
Die alte Version startet gleichzeitig zwei Timer, lässt einen genau eine Sekunde zählen und sichert sofort den Stand des zweiten Counters. Auf diese Weise wird die völlig richtige Frequenz gemessen - nur eben mit einem Offset von einer Sekunde.
Und die eigentliche Idee ist jetzt - rauszufinden bis welcher Frequenz meine aktuelle Methode ungenau ist. Und den Bereich mit meiner alten Programmversion abdecken.

@Markus: Da ich ja alle Überläufe in einer Zählvariable sichere, hoffe ich ohne 32 Bit-Erweiterungen auszukommen (wenigstens noch). Meine Sorge ist wirklich nur die Ungenauigkeit, die eintreffen könnte...

@Besserwessi: Mit den Gleitkommazahlen steht noch die alte Version - die Überreste des Codes lassen sich ohne Probleme beseitigen :)
Zu dem Noise Canceler habe ich mich wohl geirrt, da ich dachte, dass ich mit seiner Hilfe feststellen kann, dass die nächsten HIGH-Pegel identisch sind und man diesen Wert auch verwerten kann. (bzw. Mittelwert bilden)

MfG Nik

EDIT: Wenn ich es mir so recht überlege, reichen mir für beide Modi nicht die 3 Timer :D Also doch über mehrere Takte zu messen versuchen :(

Besserwessi
21.03.2013, 23:10
Solange man nur 1 Periode vermisst, gibt es einen Punkt wo man mit dem vermessen einer Periode gleich gute Ergebnisse wie dem Zählen für 1 Sekunde bekommt. Das sollte bei etwa 4000 Hz liegen. Der Übergang zur "alten" Variante mit dem Zählen der Impulse in 1 Sekunde wird nur für hohe Frequenzen gebraucht, so ab etwa 200 kHz - dann wird es irgendwann zu schnell für das Auswerten der ICP Interrupts.

Darunter ist die Methode der Wahl einfach mehr als eine Periode zu messen: also mit dem Messen der "Endzeit" so lange zu warten bis die gewünschte Mindestzeit und damit Auflösung erreicht ist, also z.B. 160000 Counts = 1/100 s oder auch einfach 3-10 Überläufe des Timers. In der Zeit zählt man einfach in Software die Pulse und misst dann die Endzeit und die Zahl der Perioden.

Für eine besonders hohe Auflösung bei Signalen wie etwa der Netzfrequenz gibt es dann noch ein paar andere Tricks für weniger Störungen und mehr Auflösung.

markusj
21.03.2013, 23:31
Ich sehe gerade nicht ganz wie ihr auf Probleme bei den 10kHz kommt? Die Zeitbasis ist bei T_CLK = F_CPU gleich 1/16 µs. Du misst also jede Periodendauer mit (um konservativ zu rechnen) 1/4µs Ungenauigkeit. Was dann 10kHz +-25Hz ergeben würde, und das sind nur die Rohwerte. Wenn du darüber einen Mittelwert bildest, werden die Ergebnisse ähnlich genau werden wie die Messung der Impulsanzahl.

Ach ja, tatsächlich liegt der Fehler vermutlich eher bei +- 3,125Hz. Ich würde das Jammern auf höchstem Niveau nennen ...

Und drei Timer? Nik, du kannst die Timer recyclen. Mit etwas Kreativität bleiben sogar noch Ressourcen für andere Programmteile frei ... (und die Timer 0 und 2 sowieso).

mfG
Markus

Liquidator
21.03.2013, 23:31
Vielen Dank Besserwessi :)

Wäre es zu fehleranfällig, wenn ich nach 3 Perioden (6 Flanken) die Endvariable (zaehlschritte) einfach durch drei teilen würde?
Ein Problem hätte ich mit 1Hz - bei Messung von mehreren Perioden könnte ich >1sek warten oO

Markus, solange ich kein totales Differential bilden muss, um die Ungenauigkeiten zu glätten... :D
Bei geringen Frequenzen sind 3 Hz ziemlich viel - oder meintest du das nur auf den 10 KHz bezogen?
3 Timer... Einen bräuchte ich für die genaue Sekunde, somit unberührbar, was Vergleichswert und Prescaler betrifft. (Timer2)
Multiplex muss ich nicht sehr genau sein, daher kein CTC Mode benötigt (also Timer0)
Timer1 wäre für die Taktanzahlen zuständig, da 16 Bit - müsste sowohl nach einer Sekunde als auch vom ICR abhängig funktionieren...
Bisschen Kreativität erlangt man, wenn man sich im Stoff frei fühlt :)

MfG Nik

Besserwessi
22.03.2013, 00:12
Die Größte Unsicherheit hätte man schon bei der hohen Frequenz, also etwa +- 7 Hz bei 10 kHz, +-1 Hz bei 4 kHz oder +-0,1 Hz bei 400 Hz.

Einfach nach 3 Periode durch 3 zu teilen geht. Das kommt auf das gleiche hinaus, als würde man 3 Periode hintereinander mitteln.
Mit der ICP Funktion erkennt man aber immer nur Flanken der einen Polarität, also entweder H->L oder L -> H. Für 3 Perioden braucht man da nur 4 Flanken: eine erste und dann 3 weitere.

Wie schon erkannt, sind aber fest immer 3 Periode keine gute Lösung für sehr niedrige Frequenzen.
Bei dem großen Frequenzbereich muss die Zahl der Perioden nicht fest sein: bei sehr niedriger Frequenz (z.B. unter 100 Hz) reicht eine Periode, darüber nimmt man dann halt mehr Perioden, so das die Messzeit z.B. mindestens 1/100s beträgt. Das macht nur die Abfrage der "Endzeit" etwas komplizierter:
- ist die Zeit seit dem Start unter etwa z.B. 10 ms zählt man die Periodenzahl hoch
- ist die Zeit über 10 ms, hat man die "Endzeit" und ist fertig mit der Messung.
Je nach Frequenz ist die Zahl der Perioden dann halt 1 (bei weniger als 100 Hz) oder auch mal 100 bei 10 kHz.

Zu den Timern: den Timer für die genaue Sekunde könnte man z.B. auch für das Multiplexing nutzen. alternativ gibt es für weniger hohe Anforderungen auch noch eine Timing über z.B. USI, den Watchdogtimer, oder den Ad Wandler. Außerdem gibt es für einen nicht genutzten Timer kein Geld zurück.

Liquidator
22.03.2013, 00:27
Damit kann ich leben :)

Dann werde ich das nun wie geplant basteln und mal berichten, wie gut die Messung war. Ein Paar Tücken sind vorhanden - beispielsweise eben dieses gleichzeitiges Auslösen von Ereignissen verschiedener Prioritäten...
Man lernt auf alle Fälle ordentlich dazu. Wie kann man eigentlich den 1-Sekunde-Vergleichstimer für Multiplexing nutzen, ohne Vergleichswert oder Prescaler zu ändern?

MfG Nik

Besserwessi
22.03.2013, 11:42
Wenn man das Multiplexing über einen Timer im CTC Mode macht, kann man den Timer auch für das 1 s Signal nutzen. Es muss nur der Takt des Timers eine genau passende Frequenz (z.B.100 Hz) haben. Die genaue Sekunde ergibt sich dann als die Zeit für 100 der Interrupts. Das Problem mit einer kleinen Unsicherheit durch die Laufzeit bis der Timer aktiviert oder gestoppt wird hat man ja in fast jedem Fall, es ist aber auch ohne ASM weitgehend lösbar, wenn auch nicht so einfach.

Das größere Problem ist eher, wenn noch andere Interrupts aktiv sind, denn die können eine schwer zu berechnende Verzögerung bringen. Von daher ist es sogar ein Vorteil, wenn man so etwas wie das Multiplexing nicht mit einem anderen Interrupts macht.

Mit dem Zählen der ICP Ereignisse erübrigt sich die Methode mit der festen 1 s Torzeit aber ohnehin weitgehend, bis zu einer Frequenz von etwa 200 kHz.

Liquidator
22.03.2013, 12:18
Das stimmt, die eine-Sekunde-Vergleichsmessung wird wohl (vorerst) nicht benötigt :)
Doch würde mich das wirklich interessieren, wie man gleichzeitig eine Sekunde und beispielsweise 5ms-Takte gleichzeitig mit einem Timer zu zählen vermag. Im Moment brauche ich das nicht besonders, doch die Anzahl der Timer ist immer zu wenig und man lernt immer mehr dazu ;)
In kürzester Zeit spiele ich den Code drauf und melde mich wieder zu Wort. Versuchen werde ich das wohl erst einmal mit Messung einer Periode - nur im Notfall ändere ich das in mehrere.

Anbei poste ich den Code für die Nachfolger und würde natürlich dankbar sein, wenn ihr auch Optimierungstipps für mich hättet - so ein Microcontroller hat nicht allzuviel Toleranz (sprich:Speicher), was nicht-optimierte Programmierung betrifft :)

MfG Nik


/************************************************** ***************

Frequenzzähler, im Bereich von 1Hz bis mind. benötigte 1400Hz.

Unter Zuhilfenahme eines externen Quarzes (16MHz) wird das an
PPB0(ICP1) des ATmega8 anliegende TTL-Signal ausgewertet und anschließend
auf einer vierstelligen 7-Segment-Anzeige im Multiplexbetrieb
dargestellt.

Für etwaige kleinere Abweichungen wurde der Korrekturfaktor eingeführt,
welcher durch Abgleich die Ungenauigkeit der Messung arithmetisch "glättet".

************************************************** ****************/

#define F_CPU 16000000UL // uC läuft mit 16MHz

#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif


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


volatile unsigned short z=0; // Zählvariable des Timer1
volatile unsigned short UpdateDisplay; // Verzögerung
volatile unsigned long Zeitdifferenz = 0;

volatile uint8_t tausender, hunderter, zehner, einer; // Speicher für die eizelnen Digits
volatile uint8_t position = 0;

unsigned int zaehler1_ovf = 0; // Anzahl der Überläufe des Timer1
unsigned long zaehlschritte = 0;

uint8_t bitmaske = 0b00010000; // Funktion: Keinen Strom auf PortD4 geben, da TTL

uint16_t freq = 0;




float korrekturfaktor = 1; // für Abgleich anpassen (z.B. 0.9997)

const int8_t numbers[11] = // Array zur Darstellung der Zeichen auf der 7-Seg-Anz.
{

0b01101111, // 0
0b00000110, // 1
0b10101011, // 2
0b10001111, // 3
0b11000110, // 4
0b11001101, // 5
0b11101101, // 6
0b00000111, // 7
0b11101111, // 8
0b11001111, // 9
0b00000000 // leere Anzeige

};


ISR(TIMER1_OVF_vect)
{
z++;
TIFR = (1<<TOV1); //Beim Überlauf von Timer1 und dem anschließenden Interrupt erfolgt eine Zurücksetzung auf Null
//Braucht nicht gelöscht zu werden. Jeder Interrupt löscht automatisch sein eigenes Flag beim Aufruf der ISR.
}


ISR(TIMER1_CAPT_vect)
{
// static bedeutet, dass auf die Funktion/Variable nur in der Datei, in der sie steht, zugegriffen werden kann.

static unsigned short ErsteFlanke = TRUE;
static unsigned long Startzeit = 0;
static unsigned int LowByte = 0;
static unsigned int HighByte = 0;


if(UpdateDisplay) // Das Display wurde mit den Ergebnissen der vorhergehenden
{ // Messung noch nicht aktualisiert. Die nächste Messung
return; // verzögern, bis die Start- und EndTime-Variablen wieder
} // gefahrlos beschrieben werden können


LowByte = ICR1L; // LowByte zuerst, HighByte später gepuffert
HighByte = ICR1H;

// Overflow verpasst, wenn ICR1H klein und wartender Overflow Interrupt

if ((HighByte < 128) && (TIFR & (1<<TOV1)))
{
// wartenden Timer Overflow Interrupt vorziehen
++z;
TIFR = (1<<TOV1); // Timer Overflow int. löschen, da schon hier ausgeführt
}


// Bei der ersten Flanke beginnt die Messung, es wird der momentane
// Timer beim Input Capture als Startwert gesichert

if(ErsteFlanke)
{
Startzeit = ICR1;
z = 0;
ErsteFlanke = FALSE; // Nach der nächsten Flanke beginnt die Ausgabe
}

// das ist die zweite Flanke im Messzyklus. Die Messung wird gestoppt

else
{
Zeitdifferenz = ICR1 - Startzeit;
UpdateDisplay = TRUE; // Eine vollständige Messung. Sie kann ausgewertet werden
ErsteFlanke = TRUE; // Bei der nächsten Flanke beginnt der nächste Messzyklus
}

}


ISR (TIMER0_OVF_vect) // timer0 overflow interrupt
{
TCNT0 += 6; // WorkAround, CTC-Mode-"Simulation"

PORTC = 0b00000000; // alle Digits aus

/************************************************** ***************
Die Prozedur ist gesondert zu schildern. Die Funktion switch(position)
enthält nicht nur den eigentlichen Multiplex-Vorgang zum
timer1-abhängigen Wechsel der jeweiligen 7-Segment-Anzeige, sondern
entfernt, mithilfe verknüpfter case-if-Abfragen auch die unnötigen
Nullen, die vor der eigentlichen Frequenzanzeige angezeigt werden würden.

************************************************** ****************/


switch (position) // aktuelle Stelle ausgeben
{
case 0 : if (tausender==0)
{digit(10, PC0);}
else
{digit(tausender, PC0);}
break;

case 1 : if ((tausender==0)&&(hunderter==0))
{digit(10, PC1);}
else
{digit(hunderter, PC1);}
break;

case 2 : if ((tausender==0)&&(hunderter==0)&&(zehner==0))
{digit(10, PC2);}
else
{digit(zehner, PC2);}
break;

case 3 : {digit(einer, PC3);}
break;
}

position++; // beim nächsten Mal nächstes Digit
if(position == 4) // wenn alle Digits durch
{
position = 0; // von vorne beginnen
}
}


/************************************************** *******************
Die Funktion digit() sorgt für die eigentliche Darstellung der Zahlen
an der jeweiligen Stelle. Sie filtert unter Zuhilfenahme der Bitmaske
den für das TTL-Signal freigehaltenen Pin (T0) und holt sich aus dem
numbers[]-Array die Portzuweisungen.

************************************************** *******************/

void digit(uint8_t wert, uint8_t pin)
{

PORTD = (PORTD & bitmaske) | numbers[wert];
PORTC |= (1 << pin); // entsprechendes Digit an

}

void zahl_ausgeben(uint16_t zahl)
{
tausender = zahl/1000; // Tausender Dezimalstelle
zahl = zahl % 1000;

hunderter = zahl/100; // Hunderter Dezimalstelle
zahl = zahl % 100;

zehner = zahl/10; // Zehner Dezimalstelle
zahl = zahl % 10;

einer = zahl; // Einer Dezimalstelle
}



int main(void)
{
// Definition von Ein- und Ausgängen des uC Boards
DDRB = 0xFE; // Mit Ausnahme des ICP1-Pins alles als Ausgang
DDRC = 0xFF; // PORTC als Ausgang - über PORTC werden die jeweiligen Segmente einer 7-Seg. Anzeige gesteuert
DDRD = 0xFF; // PORTD als Ausgang
PORTC = 0b00000000;


TIMSK = (1<<TICIE1) | (1<<TOIE1); // Interrupts akivieren, Capture und Overflow
TCCR1B = (1<<ICES1) | (1<<CS10); // Input Capture Edge, kein Prescaler

TIMSK |= (1 << TOIE0); // Interrupt aktivieren, Overflow
TCCR0 |= (1 << CS01) | (1 << CS00); // Vorteiler auf 64


sei(); // Interruptbehandlung ein



while(1) // unendliche Schleife
{
if(UpdateDisplay) // Erst nach zweiter Flanke ausführen
{
zaehler1_ovf = z; // Anzahl der Überläufe wird in zaehler1_ovf kopiert
z= 0;


zaehlschritte = (unsigned long)(0.5+(65536.0*zaehler1_ovf) + Zeitdifferenz);


if (zaehlschritte==0)
{
freq = 0;
}
else
{
// Die abschließende Berechnung der Schritte pro Sekunde
freq = (uint16_t) (F_CPU/(zaehlschritte));
}

zahl_ausgeben(freq);

UpdateDisplay = FALSE;
}


}

return 0;
}

Besserwessi
22.03.2013, 12:48
Der Overflow Zähler muss in der ICP ISR gesichert werden - der Fehler ist immer noch drin.

Auch das unnötige zurücksetzen des Overflow interrupt bits ist immer noch drin - das sollte raus.

Das Multiplexing zur Darstellung geht auch ohne switch, einfach nur über ein Array. Den Test auf Führende Nullen macht man dann halt einmal beim Schreiben der Zahl in den Puffer. Die Laufzeit der ISR hat auch einen Einfluss auf die maximale Frequenz die per ICP noch gemessen werden kann - für maximal 10 KHz reicht es auch so noch locker, nur das Array wäre aber deutlich schneller.
Die Frequenz für die Darstellung ist auch recht hoch. Das würde vor allem die Messung mit der festen Torzeit von 1s stören.

Es ist nicht gut in einer ISR ein Unterprogramm aufzurufen - dafür müssen unnötig viel Register gerettet werden. Die Minimallösung wäre es die routine digit als INLINE zu kennzeichnen - mit etwas Glück macht das der Compiler auch von alleine, muss aber nicht.

Liquidator
22.03.2013, 13:15
Den Overflow Zähler sichern... Meinst du damit, in zaehler1_ofv soll am Ende von ISR(TIMER1_CAPT_vect) aus z kopiert werden?

Overflow bit ist entfernt. Für switch finde ich keinen anderen Ansatz, doch werde ich mir darüber Gedanken machen. Die ICP ISR kann ich nicht kleiner gestalten, da dort alle Überprüfungen stattfinden...
Der Multiplex-Betrieb läuft mit 1 ms. (anhand von Experimenten werden zur sauberen Anzeige 4-5 ms benötigt) Auf eine Millisekunde habe ich den umgestellt, um damit später evtl. auch gleich eine Sekunde messen zu können :)

INLINE wäre mir in diesem Kontext neu, ist das eine Art von Makro? Oder reicht es folgendermaßen zur Kennzeichnung:


inline void digit(uint8_t wert, uint8_t pin)
{
PORTD = (PORTD & bitmaske) | numbers[wert];
PORTC |= (1 << pin);
}


MfG Nik

Besserwessi
23.03.2013, 08:30
Das Kopieren von Z nach zaehler1_ofv wäre eine Möglichkeit den Stand in der ISP ISR zu sichern.

Der Zusatz INLINE sorgt dafür das der Code direkt eingesetzt wird, und kein echtes Unterprogramm Erzeugt wird. Je nach Einstellung des Compilers wird das zur Optimierung ggf. auch automatisch gemacht. Zumindest die älteren GCC Versionen erzeugen noch zusätzlich ein nie benutztes Unterprogramm, denn der Compiler unterscheidet nicht nach einem kompletten Programm und einem Teil, der ggf. noch eingebunden wird. Um das zu verhindern wäre STATIC INLINE die richtige Kennzeichnung - damit weiss der Compiler, dass das Unterprogramm nicht noch extern gebraucht wird.
Das Gleiche könnte man auch erreichen indem an einen Makro (mit #Define ) nutzt.

Die ISR zum Multiplexing kriegt man kürzer, indem man ein Puffer direkt für die Werte anlegt, die ausgegeben werden sollen. Also hier numbers[einer],numbers[zener],... .Es müssen dann nur noch die Werte aus diesem Array ausgegeben werden. Die Überprüfung auf führende Nullen und die Umrechnung von Ziffern in die Segmente erfolgt dann im nicht zeitkritischen Hauptprogramm.

Die ISR sähe dann etwa so aus, gleich ohne die extra Funktion Digit:

PORTD = (PORTD & bitmaske) | puffer[position];

position++; // beim nächsten Mal nächstes Digit
if(position == 4) // wenn alle Digits durch
{
position = 0; // von vorne beginnen
}

Liquidator
24.03.2013, 14:53
Guten Tag Besserwessi,

alles klar, soweit verstanden, werde es umsetzen.


STATIC INLINE void digit(uint8_t wert, uint8_t pin)
{
PORTD = (PORTD & bitmaske) | numbers[wert];
PORTC |= (1 << pin);
}


Zur Optimierungdes Multiplex werde ich mir das Ganze nochmal anschauen, aber noch versteh ich nicht so ganz, wieso die z-variable nicht in main(), sondern am Ende der CAPT_ISR in zahler1_ovf kopiert werden soll? Dann müsste auch z zum Ende der ISR hin annulliert werden, nehm' ich an...

MfG Nik

Besserwessi
24.03.2013, 15:37
Das hochzählen von z in der Overflow ISR geht auch nach dem ICP Interrupt weiter. Bei der Startzeit ist das kein Problem, weil da z zum richtigen Zeitpunkt in der ISR auf 0 gesetzt wird. Aber beim Ende der Periode vergeht zwischen dem ICP Interrupt und der Ausgabe im Hauptprogramm noch etwas Zeit, und in der Zeit kann noch 1 Timer Überlauf stattfinden. Das ist nicht viel Zeit, aber das reicht damit mit einer gewissen Wahrscheinlichkeit (etwa 0,1-1%) ein Überlauf zu viel angezeigt wird.

Liquidator
24.03.2013, 15:56
Klingt plausibel, danke für die Erklärung :)
Anmerkung zu den static inline Funktionen: Je nach Linker/Compiler müssen sie auch extra oben deklariert werden, sonst meckern die Ersteren.


static inline void digit(uint8_t wert, uint8_t pin);


MfG Nik

EDIT: Eine andere Möglichkeit in der Optimierung der Multiplex-Routine wäre wohl ein Multiplexer-Baustein, ähnlich dem MAX7219 - spart auch einiges an Ports.

EDIT 2: Heute die Möglichkeit gehabt das Ganze am Funktionsgenerator zu testen - nach langem Suchen habe ich auf die inline-Deklarierung verzichtet und zumindest wurde die Anzeige aktiviert.
Zur Messgenauigkeit: sie ist bei einer ganzen Zahl wirklich gut - schwankt +-1 Hz, lässt sich einfach korrigieren. Aktuell suche ich nach einem Codefehler, der die Anzeige flimmern lässt und zwar ganz unregelmäßig - mal eine Anzeige, mal alle.

EDIT3: Lasse die Platine mit ISP-Buchse fertigen und werde dann nach dem Fehler suchen, ich melde mich :)

Liquidator
25.04.2013, 16:30
Hallo nochmal,

die Platine wurde angefertigt, doch flackert die Anzeige, als ob ein falsches Multiplexing vorliegt. Zwischendurch sieht man die völlig korrekt angezeigte Frequenz.
Leider finde ich auf Anhieb keine Fehler im Quelltext...
Ob jemand einen Tipp hätte, mir gehen die Möglichkeiten aus ;)

Bedanke mich im Voraus,
MfG Nik