Werkzeugkoffer


Erstellt: 2009-12-20Letzte Änderung: 2009-12-20 [vor 14 Jahren, 3 Monaten, 28 Tagen]

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]
[1740 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):

Tastenprellen Tastenprellen
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ähler button_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]
[1716 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