Werkzeugkoffer
Erstellt: 2009-12-20 • Letzte Änderung: 2009-12-20 [vor 14 Jahren, 9 Monaten]
Der Werkzeugkoffer beinhaltet viele nützliche und unentbehrliche Methoden, Codeschnipsel und Rechenbeispiele
der Elektronik und Programmierung. Viele von mir verfasste Artikel verweisen an den entsprechenden Stellen auf den Werkzeugkoffer, um nicht jedes mal alles erneut erklären zu müssen. |
Zeitbasis
Beschreibung
Oftmals ist in Mikrocontrollerprojekten eine genaue Zeitbasis von Nöten, z.B. um eine interne Uhr weiterzuzählen oder eine Zeitverzögerung zu erzeugen.Im Folgenden soll eine sehr einfache Implementierung in Software (AVC-GCC) vorgestellt werden.
Funktionsweise
Folgender Code benutzt den Output Compare Match Interrupt des TIMER2 (8 Bit) eines ATmega16, der auf einem 2 MHz externen Quarz läuft. Der Timer ist so konfiguriert, dass der Interrupt 1000 mal pro Sekunde aufgerufen wird. Maßgeblich dafür sind der Prescaler und der Vergleichswert des Timers.Aus gewünschter Periodendauer (in diesem Fall 1000 µS) und Inputfrequenz des Timers (in diesem Fall 2 MHz / 8 (Prescaler) = 250 kHz) kann man mithilfe des AVR-Timer-Calculator den Compare-Wert (in diesem Fall 249) berechnen.
In der Interruptroutine können dann einfach beliebig viele Zählervariablen kaskadiert werden, um große Zeitspannen zählen zu können. Im Folgenden Code werden Millisekunden, Sekunden und Minuten gezählt.
Code
Zuerst müssen die benötigten Includes für I/O und Interrupts eingebunden werden.#include <avr/interrupt.h> #include <avr/io.h>
Wie oben beschrieben, werden in diesem Beispielcode drei Zeitwerte erfasst. Diese werden in globalen Variablen gespeichert. Die Direktive
volatile
ist hier wichtig und darf nicht weggelassen werden.volatile int mseconds, seconds, minutes;
In der main wird zuerst der TIMER2 konfiguriert.
int main(void) { // TIMER2-Setup TCCR2 = (1 << WGM21) | 0b010; // CTC-Mode, Prescaler 8 OCR2 = 249; // Vergleichswert set_bit(TIMSK, OCIE2); // Output Compare Match Interrupt Enable sei(); // Interrupts einschalten
Nun wird der Interrupt aktiviert. Die Interruptroutine, zu der wir gleich kommen werden, wird ab jetzt bereits 1000 mal pro Sekunde aufgerufen und der Zeitzähler beginnt seine Arbeit.
sei(); // Interrupts einschalten
Möchte man z.B. die Uptime (also die vergangene Zeit seit dem letzten Reset) des AVR messen, so muss der Zeitzähler kurz vor der Verwendung auf Null gesetzt werden - folgende Zeile bewirkt dies.
// Zeitzähler Reset mseconds = seconds = minutes = 0;
Der Zeitzähler läuft nun und kann z.B. in der Hauptschleife des Programms verwendet werden.
// Hauptschleife while (1) { // Die Zeit kann nun z.B. hier verwendet werden } }
Nun kommen wir zur Interruptroutine an sich. Da diese im Abstand von jeweils einer Millisekunde aufgerufen wird, können wir einfach bei jedem Aufruf einen Millisekundenzähler
mseconds
inkrementieren (erhöhen) - allerdings nur, wenn der aktuelle Millisekundenzählerwert unter 999 liegt.// TIMER2 Output Compare Match Interrupt ISR(TIMER2_COMP_vect) { // Millisekunden Zählen if (mseconds < 999) { mseconds++; }
Würde der Zähler nach erneutem Inkrementieren den Wert 1000 erreichen, setzen wir ihn auf Null. Eine Sekunde ist nun vergangen und wir können den Sekundenzähler inkrementieren. Auch hier wird wieder sichergestellt, dass der Wert von
seconds
nicht über 59 steigt, ansonsten wird
ein Minutenzähler minutes
inkrementiert. Dieses Spiel kann man endlos weiterführen und auch
Stunden, Tage, Monate und Jahre zählen.else { mseconds = 0; // Sekunden zählen if (seconds < 59) { seconds++; } else { seconds = 0; // Minuten zählen if (minutes < 59) { minutes++; } else { minutes = 0; // Hier kann beliebig weiter gezählt werden } } } }
Download
Den obigen Code gibt es hier zum Download.Download: werkzeugkoffer_zeitbasis_1.c.zip [625.00 Bytes] | |
[1802 Downloads] |
Genauigkeit
Eine mit obiger Methode gezählte Zeitspanne ist natürlich nur so akkurat wie die Taktquelle des Mikrocontrollers. Der interne RC-Oszillator von AVRs ist in diesem Zusammenhang höchstens für einfache, zeitunkritische Verzögerungsschleifen zu gebrauchen. Soll obiger Code z.B. in einer Uhr Anwendung finden, so sollte auf jeden Fall ein externer Quarz oder Resonator verwendet werden.Nach oben
Taster entprellen
Das Problem
Wenn Taster oder Schalter in digitalen Schaltungen Anwendung finden sollen, so ist das meist nicht ohne zusätzlichen Entstöraufwand möglich. Warum? Taster und Schalter sind mechanische Bauteile, bei denen während der Betätigung zwei Metallkontakte aufeinandertreffen bzw. sich voneinander entfernen. Dabei tritt ein besonderer Effekt, das sog. Prellen auf. Dabei "hüpfen" die Kontakte für kurze Zeit aufeinander herum, ehe ein dauerhafter Kontakt besteht. Im Signal äußert sich das wie folgt (ich habe mal zwei besonders gruselige Fälle des Tastenprellens herausgesucht):Um dieses Tastenprellen, welches eine Digitale Schaltung gehörig aus dem Konzept bringen kann, zu eliminieren, ist ein Tiefpass von Nöten, der die kurzen Spikes herausfiltert. Im Folgenden soll hier eine Implementierung in Software (AVR-GCC) vorgestellt werden, um unnötigen Hardwareaufwand zu vermeiden.
Die Lösung
In diesem Beispiel wird davon ausgegangen, dass ein Taster mit einem Pol am Pin PA0 eines AVR-Mikrocontrollers und mit dem anderen Pol an GND hängt. Der interne PullUp-Widerstand von PA0 ist eingeschaltet und hält den Pin so lange auf HIGH, bis ein Tastendruck erfolgt.In jedem Aufruf eines periodisch auftretenden Interrupts - siehe - wird der aktuelle Status des Tasters abgefragt. Dabei wird eine Variable - der sog. Tastenzähler bei vorhandenem Tastendruck inkrementiert und bei nicht gedrückter Taste dekrementiert. Der Wert des Tastenzählers darf dabei nicht auf unter 0 sind und nicht über den größtmöglichen Wert der Variable (255 bei
unsigned char
,
65535 bei unsigned int
steigen.In der Hauptschleife des Programms wird der Wert des Tastenzählers mit einem vorher fest definierten Schwellwert verglichen. Ist der Wert des Tastenzählers größer oder gleich dem Schwellwert, so kann der Tastendruck erfasst werden und der gewünschte Code ausgeführt werden. Nach Ausführung des Codes muss dann gewartet werden, bis der Wert des Tastenzählers wieder unter den Schwellwert sinkt (der User hat die Taste wieder losgelassen) und dann der Tastenzähler auf Null gesetzt werden. So ist der Timer wieder "scharf" für den nächsten Tastendruck.
Innerhalb der Routine, die bei einem detektierten Tastendruck aufgerufen wird, kann der Tastenzähler natürlich weiter überwacht werden und so ein weiteres Ereignis getriggert werden, wenn die Taste eine bestimmte Zeit lang gehalten wurde. Dafür kann wird dann einfach ein zweiter Schwellwert definiert.
Code
Der Tastenzählerbutton_counter
wird als globale Variable deklariert. Die Direktive volatile
ist wichtig und darf nicht weggelassen werden.volatile unsigned int button_counter = 0;
Es werden zwei Schwellwerte definiert; einer für einen normalen Tastendruck und einer für einen gehaltenen Tastendruck. Beide Angaben sind in Millisekunden, da davon ausgegangen wird, dass ein Interrupt mit 1 ms Periodendauer verwendet wird.
#define THRESHOLD_PRESS 20 #define THRESHOLD_HOLD 600
Folgende Codeblock wird in den Interrupt eingefügt:
if (bit_is_clear(PINA, 0)) { // Taster gedrückt if (button_counter < 65535) button_counter++; } else { // Taster nicht gedrückt if (button_counter > 0) button_counter--; }
In der Hauptschleife kann der Tastenzähler dann ausgewertet werden und normale sowie gehaltene Tastendrücke detektiert werden.
// Hauptschleife while (1) { if (button_counter >= THRESHOLD_PRESS) { // Hier wurde der Taster gedrückt //[DO_SOMETHING] // Hier wird gewartet, bis der Taster wieder losgelassen wurde // und der Tastenzähler letztendlich wieder auf Null gesetzt. while (button_counter >= THRESHOLD_PRESS) { // Währenddessen wird auch geprüft, ob der Taster gehalten wird. if (button_counter >= THRESHOLD_HOLD) { // Hier wird der Taster gehalten //[DO_SOMETHING_ELSE] // Warten, bis der Taster nicht mehr gehalten wird while (button_counter >= THRESHOLD_HOLD); } } button_counter = 0; } }
Je nach Bedarf und Anwendung kann obiger Code in vielerlei Weise abgeändert werden, um das gewünschte Verhalten zu erreichen.
Nach oben
LED-Multiplexing
Wenn große Mengen an LEDs angesteuert werden sollen (wie z.B. bei den meisten Projekten der Sektion "Optoelektronik", siehe links im Menü), die Steuereinheit (meist ein Mikrocontroller) dafür aber nicht genügend Pins zur Verfügung stellen kann, bedient man sich gerne eines "Tricks", dem Multiplexing.Hier wird die Gesamtheit der LEDs in eine bestimmte Anzahl
n
kleinerer Abschnitte eingeteilt, die die
Steuereinheit einzeln noch ansteuern kann. Praktisch kann so ein Abschnitt z.B. eine einzelne Siebensegmentanzeige
(siehe z.B. Funkwecker), eine einzelne Ebene eines LED-Würfels
oder eine LED-Zeile einer Binäruhr sein.$$n = Anzahl\ Abschnitte$$
Die gewünschten LEDs des ersten Abschnittes werden für einen bestimmten Zeitraum eingeschaltet, danach wieder ausgeschaltet, der nächste Abschnitt wird ausgewählt und es wird genauso verfahren. Ist der letzte Abschnitt angesteuert worden, wird wieder mit dem ersten Abschnitt begonnen.
Passiert dieses Durchlaufen der einzelnen Abschnitte schnell genug, nimmt man das Ein- und Ausschalten jedes einzelnen Abschnittes nicht mehr als Aufblitzen, sondern als durchgehendes Leuchten wahr. Durch den sog. Phi-Effekt, sozusagen die "Trägheit" des menschlichen Auges ist dies möglich.
Folgendes Video veranschaulicht dies anhand von vier Siebensegmentanzeigen, die mit steigender Frequenz gemultiplext werden:
Weitere Informationen und Rechenbeispiele gibt es beim nächsten Update!
Nach oben
PWM
Folgt in Kürze.Nach oben
DCF77-Signalfilter
Der Aufbau des DCF77-Signals dürfte bekannt sein: Es werden nacheinander im Abstand von jeweils 1 Sekunde 59 Datenbits gesendet, welche die aktuelle Zeitinformation speichern (Informationen zur Dekodierung dieser Bits folgen weiter unten). Das Signal ist generell HIGH (5V), wird aber für jedes übertragende Bit für eine bestimmte Zeit auf LOW (GND, 0V) gezogen. Diese Zeit, in der das Signal LOW ist, bestimmt den Pegel des aktuellen Bits; eine Absenkung von 100 ms Dauer ist eine logische Null, 200 ms eine logische Eins. Nach 59 so übertragenden Bits folgt eine Synchronisationssekunde, in der keine Absenkung stattfindet.Der Trick ist nun, in einem derartigen Signalverlauf zuerst das Synchronisationssignal zu finden und danach die heißbegehrten Zeitbits herauszufiltern. Soweit so gut, das lässt sich doch ganz einfach machen, indem wir die Länge der HIGH-Abschnitte messen: Bei jeder steigenden Flanke des Signales einen Timer starten und ihn bei der nächsten fallenden Flanke wieder stoppen. Aus der gestoppten Zeit können wir dann schließen, ob es sich um einen kurzen (0), langen (1) oder sehr langen (Sync) Impuls handelt.
Doch leider funktioniert das nur mit einem ganz perfekten Signal, welches man in der Praxis nicht bekommt. Alte Computermonitore, Netzteile, per Multiplexing betriebene Displays, ICs etc. sind große Störenfriede, die uns wunderbar das Signal verhunzen können. Konkret bedeutet das, dass sich Spikes, also kurze, unerwünschte Nadelimpulse ins Signal schleichen; z.B. ein kurzer LOW-Pegel in einer eigentlich langen HIGH-Periode.
Daher muss eine Möglichkeit gefunden werden, die Spikes zu ignorieren und nur die wirklich breiten Impulse zur Signalauswertung anzuerkennen. Ich habe mir Gedanken gemacht, wie man dies am einfachsten lösen kann und bin auf eine einfache Lösung gekommen, die im Folgenden vorgestellt werden soll.
Algorithmus
Wir gehen im Folgenden zuerst einmal von einem perfekten Signal ohne Störspikes - in der Grafik weiter unten schwarz gezeichnet - aus.Die Abtastung dieses Signals soll über einen Interrupt mit 100 Hz erfolgen. Dieser Interrupt prüft also alle 10 ms den aktuellen Ausgangspegel des DCF77-Moduls.
Ist ein HIGH-Pegel vorhanden, so wird ein Zähler
counter
dekrementiert (erniedrigt).
Er kann dabei allerdings nicht unter den Wert 0 sinken.Ist ein LOW-Pegel vorhanden, so wird
counter
inkrementiert (erhöht).In der untenstehenden Grafik ist neben dem Signal (schwarz) auch der aktuelle Zählerstand von
counter
(blau) im Zeitlichen Verlauf aufgetragen.Erreicht
counter
einen bestimmten Schwellwert counter_threshold
,
so wird der Zähler counter
auf 0 gesetzt - dies nennen wir einen
Reset. Resets sind als grüne Punkte in der Grafik vermerkt.Durch sinnvolle Wahl des Schwellwertes
counter_threshold
lässt sich erreichen,
dass in einer LOW-Periode von 100 ms genau einer, in einer LOW-Periode von 200 ms genau zwei Resets auftreten (siehe Grafik).Wir haben nun also bereits den wichtigen Anhaltspunkt gefunden, mit dem wir auf eine logische 0 (1 Reset) oder eine logische Eins (2 Resets) im Signal schließen können. Ein weiterer Zähler
reset_counter
merkt sich die Anzahl der aufgetretenen Resets, wird also im Resetfall inkrementiert.Bei jedem Interruptaufruf wird, unabhängig von alldem, ein weiterer Zähler
lastreset_counter
inkrementiert.
Dieser Zähler wird bei jedem auftretenden Reset ebenfalls geresetted, also auf 0 gesetzt. lastreset_counter
hält also stets die genaue Zeit (in 10 ms-Schritten), die seit dem letzten Reset von counter
vergangen ist.Nun müssen wir einfach nur nach einer bestimmten Zeit - diese ist abgelaufen, wenn
lastreset_counter
dem Zeitschwellwert level_threshold
entspricht - den Zählerstand von reset_counter
checken
und haben damit unser Bit erfolgreich ermittelt. Danach muss reset_counter
wieder auf 0 gesetzt
werden und das Spiel kann von vorn beginnen.Die Zeitpunkte der Signalauswertung sind in folgender Grafik als rote Kreise eingezeichnet. Man erkennt, dass diese etwas hinter der steigenden Signalflanke liegen, also nicht ganz Synchron zum DCF77-Signal sind. Diese geringe Abweichung liegt meiner Meinung nach und für meine Zwecke jedoch noch absolut im Rahmen.
Natürlich darf
level_threshold
nicht zu klein gewählt werden, ansonsten erfolgt die Auswertung
immer nach nur einem Reset, auch wenn noch ein zweiter folgen würde.Das Aufspüren der Synchronisationssekunde ist nun auch sehr einfach, denn in dieser Sekunde ohne LOW-Periode treten keine Resets auf,
lastreset_counter
läuft also ungehindert weiter.Erreicht
lastreset_counter
nun also den sehr hohen, fest vordefinierten Wert sync_threshold
,
so ist dies das Indiz für das Synchronisationssignal. lastreset_counter
wird nun auf 0 gesetzt,
und wir haben zunächst einmal erreicht, was wir wollten.So weit, so gut. Doch wie verhält sich obiges Filterkonstrukt nun bei Störungen im Signal? Es folgen einige Screenshots meiner Quick'n'Dirty-Simulation, dich ich zu Testzwecken in FreeBASIC schrieb. (Bitte auf die Bilder klicken, um sie in voller Größe darzustellen.)
Den Sourcecode der Simulation gibt es hier zum Download.
Download: dcf77signalfilter.bas.zip [1.22 kiB] | |
[1777 Downloads] |
Zuerst habe ich nur in die HIGH-Phasen des Signalverlaufs übertrieben viele, mehr oder minder lange Spikes eingebaut (rot markiert). Man sieht, dass diese gut kompensiert werden, sofern sie im Mittel nicht länger als
counter_threshold - 1
, also 70 ms, sind. Ansonsten würde ein Reset auftreten und die Störung als
Datenbit gewertet werden.Parameter:
counter_threshold = 8
, level_threshold = 11
, sync_threshold = 107
Spikes in den LOW-Phasen sind da natürlich weitaus kritischer. Fehlerhafte Einstreuungen dürfen bei Verwendung der oben angegebenen Parameter pro 100 ms-Abschnitt nicht länger als 10 ms sein.
Treten mehr (längere) Spikes auf, so kann man zwar den Schwellwert
counter_threshold
herabsetzen,
sodass auch bei kürzeren LOW-Phasen Resets auftreten, jedoch führt das zu unerwünschten Ergebnissen in
den perfekten (ungestörten) LOW-Phasen. Ist also Quatsch...Parameter:
counter_threshold = 6
, level_threshold = 11
, sync_threshold = 107
Die tatsächliche Auswertung der in den empfangenen Bits gespeicherten Zeitinformation ist nach der Signalfilterung nicht sonderlich schwer. Für nähere Informationen zur Dekodierung bitte ich, Google zu bemühen.
Nach oben