Zeit ist relativ

Wir wollen jetzt hier keine Diskussion zur Relativitätstheorie anfangen. Ich hatte ja in der letzten Folge versprochen, daß wir uns das Webradio mal in der Praxis ansehen. In dieser Folge wollen wir uns die komplette Uhrenschaltung ansehen und auch darüber sprechen, wie denn nun die Firmware für den ATMega aussieht. Insbesondere die Implementierung der Uhr und das Multiplexing sollen uns hier interessieren. Wie versprochen hier also zunächst die gesamte Schaltung der Uhrenplatine:

Schaltung der Uhrenplatine

Schaltung der Uhrenplatine

Ich setzte an dieser Stelle voraus, daß sich der interessierte Leser bereits mit Mikrocontrollern, insbesondere denen aus der ATMega Familie auskennt. Wer hier noch keinen Überblick hat, sollte sich hier mal ansehen um was es geht. Vielleicht schreibe ich auch hierzu irgendwann mal einen Artikel. Interessant ist dieses Gebiet auf jeden Fall.

Die Beschaltung des Mikrocontrollers entspricht ziemlich genau dem, was notwendig ist, um den Baustein zu betreiben. So verwende ich ier einen normalen Quarz zur Taktung, ISP ist der EInfachheit halber nicht angeschlossen. Praktisch alles, was sich links vom Controller befindet, ist Standard um den Baustein zu betreiben. auf der rechten Seite kann man die Leitungen sehen, die die 7-Segmentanzeigen steuern. Der Übersicht halber habe ich das hier über einen Bus geführt. Mit den Pins PB2-PB5 steuere ich die Transistoren, die die jeweilige Anzeige aktivieren. Was hier nicht zu sehen ist, ist der UART, den ich direkt mit dem Raspberry verbunden habe, um die Uhrzeit vom Raspberry an den ATMega zu übertragen.  Es stellt sich nun die Frage, wie man den ATMega dazu bewegt, die Uhrzeit anzuzeigen. Und hier sind wir auch beim Kern der Sache.

Ich habe das ganz einfach über einen Timer gelöst, der im CTC-Modus läuft. Im Code sieht das dann so aus:

void timer1_start_ctc(uint16_t cmp) {
	OCR1A = cmp; // set value to output compare register
	TCCR1B = (1 << WGM12) | (1 << CS12) | (0 << CS11) | (0 << CS10); // ctc, 256 prescale
	//Enable the Output Compare A interrupt
	TIMSK1 |= (1 << OCIE1A);
}

Hier wird das Register OCR1A mit einem Wert geladen, der bestimmt, wann die Timer ISR ausgelöst wird. Dies geschieht eben dann, wenn der Timer den Wert erreicht, der im OCR1A Register hinterlegt ist. Dann wird der Timer so eingestellt, daß er mit 1/256 der Taktfrequenz  läuft. Anschliessend  laden wir das OCR1A Register mit dem Wert 31250 und starten den Timer. Nun wird die ISR-Routine für den Timer genau einmal in der Sekunde ausgelöst. Aber wie komme ich denn nun genau auf 31250?

Ganz einfach, die Taktfrequenz beträgt in meinem Fall 8MHz. Also wird, wenn im OCR1A der Wert 0 steht, die Timer ISR mit einer Frequenz von 8MHz/256 ausgelöst, also 31250Hz. Somit müssen wir das Register genau mit diesem Wert vorladen, um eine Frequenz von 1Hz zu bekommen. Für andere Taktfrequenzen muss man das dann entsprechend umrechnen.

Jetzt ist es einfach, wir definieren uns eine Struktur:

typedef struct { uint8_t hour; uint8_t minute; uint8_t second; uint8_t day; uint8_t day_of_week; uint8_t month; uint8_t year; } date_t;

Dann definieren wir den entsprechenden Typ mit

volatile date_t current_date;

Und können die ISR implementieren:

ISR (TIMER1_COMPA_vect) {

	if (current_date.second < 59) {
		current_date.second++;
	}
	else {
		current_date.second = 0;

		if (current_date.minute < 59) {
			current_date.minute++;
		}
		else {
			current_date.minute = 0;

			if (current_date.hour < 23) {
				current_date.hour++;
			}
			else {
				current_date.hour = 0;
			}
		}
	}

}

In der Hauptschleife aktualisieren wir dann immer die aktuelle Zeit. Nun fehlt noch die Ausgabe an die 7-Segment anzeigen.

Es werden jetzt einige Definitionen für die Anzeigemodule benötigt. Da die Module über eine gemeinsame Anode verfügen, müssen die Segmente zur Anzeige auf LOW geschaltet werden.

#define setPin(PORT,PIN) PORT |= (1 << PIN)
#define clearPin(PORT,PIN) PORT &= ~(1 << PIN)

#define SEG_A clearPin(PORTB, 4)
#define SEG_B clearPin(PORTC, 5)
#define SEG_C clearPin(PORTC, 3)
#define SEG_D clearPin(PORTC, 2)
#define SEG_E clearPin(PORTB, 2)
#define SEG_F clearPin(PORTB, 3)
#define SEG_G clearPin(PORTC, 4)

Jetzt können wir die Methoden für die Zeitanzeige implementieren:

void displayNumber(uint8_t num, boolean withDot) {

	// turn all segments off
	setPin(PORTC, 5);
	setPin(PORTC, 4);
	setPin(PORTC, 3);
	setPin(PORTC, 2);
	setPin(PORTB, 4);
	setPin(PORTB, 3);
	setPin(PORTB, 2);
	setPin(PORTB, 1);

	if (withDot) {
		clearPin(PORTB, 1);
	}
	else {
		setPin(PORTB,1);
	}

	if (num == 0) {
		SEG_A;
		SEG_B;
		SEG_C;
		SEG_D;
		SEG_E;
		SEG_F;
	}
	else if (num == 1) {
		SEG_B;
		SEG_C;
	}
	else if (num == 2) {
		SEG_A;
		SEG_B;
		SEG_G;
		SEG_D;
		SEG_E;
	}
	else if (num == 3) {
		SEG_A;
		SEG_B;
		SEG_C;
		SEG_D;
		SEG_G;
	}
	else if (num == 4) {
		SEG_B;
		SEG_C;
		SEG_F;
		SEG_G;
	}
	else if (num == 5) {
		SEG_A;
		SEG_F;
		SEG_G;
		SEG_C;
		SEG_D;
	}
	else if (num == 6) {
		SEG_A;
		SEG_F;
		SEG_G;
		SEG_C;
		SEG_D;
		SEG_E;
	}
	else if (num == 7) {
		SEG_A;
		SEG_B;
		SEG_C;
	}
	else if (num == 8) {
		SEG_A;
		SEG_B;
		SEG_C;
		SEG_D;
		SEG_E;
		SEG_F;
		SEG_G;
	}
	else if (num == 9) {
		SEG_A;
		SEG_B;
		SEG_C;
		SEG_D;
		SEG_F;
		SEG_G;
	}

}

void displayTime(int hours, int minutes, int seconds, boolean dot) {

	int s_tenths = seconds / 10;
	int s_ones = seconds - s_tenths * 10;

	int m_tenths = minutes / 10;
	int m_ones = minutes - m_tenths * 10;

	int h_tenths = hours / 10;
	int h_ones = hours - h_tenths * 10;

	clearPin(PORTD,5);
	clearPin(PORTD,6);
	clearPin(PORTB,6);
	setPin(PORTB,7);
	displayNumber(m_ones,false);
	_delay_ms(SW_DELAY);

	clearPin(PORTD,5);
	clearPin(PORTD,6);
	setPin(PORTB,6);
	clearPin(PORTB,7);
	displayNumber(m_tenths,false);
	_delay_ms(SW_DELAY);

	clearPin(PORTB,6);
	clearPin(PORTB,7);
	clearPin(PORTD,5);
	setPin(PORTD,6);
	displayNumber(h_ones,dot);
	_delay_ms(SW_DELAY);

	clearPin(PORTB,6);
	clearPin(PORTB,7);
	setPin(PORTD,5);
	clearPin(PORTD,6);
	displayNumber(h_tenths,false);
	_delay_ms(SW_DELAY);
}

Hier kann man gut sehen, wie die einzelnen Anzeigeelemente umgeschaltet werden, indem der entprechende Pin geschaltet wird, bevor die jeweilge Ziffer angezeigt wird. Schauen wir uns das ganze mal auf dem Logic Analyzer an:

Multiplexing

Multiplexing

Hier sehen wir von oben nach unten die einzelnen Ausgänge des MIkrocontrollers für die Ansteuerung der Transistoren. Hier lassen sich noch zwei Messgrößen ablesen: Zum einen können wir sehen, daß jedes einzelne Segment mit einer Frequenz von knapp 118 Hz angesteuert wird und somit kein Flimmern mehr zu sehen ist, zum andern kann man sehen, daß jedes einzelne Segment für knapp 2ms aktiv ist, bevor zum nächsten umgeschaltet wird.

Im Grunde ist das die gesamte "Magie", die benötigt wird, um die Zeit anzuzeigen. In der nächsten Folge beschäftigen wir uns damit, wie wir über den UART die aktuelle Zeit vom Raspberry an das Uhrenmodul übertragen.

Der Code für das Projekt kann wie immer auf Github eingesehen und heruntergeladen werden. Hier ist auch die Schaltung im EAGLE Format enthalten.