Im Rahmen der Mikrocontroller-Programmierung in Assembler habe ich mir als Mini-Projekt eine Uhr ausgedacht, die die Uhrzeit binär anzeigt. Okay…das ist sicher nicht die neueste Idee 🙂
Ich wollte mal die Timer/Counter des ATmega 2560 einsetzen und habe den TC1 im CTC-Modus programmiert, so dass dieser alle 500ms einen Compare-Match-Interrupt auslöst und dabei den entsprechenden Pin toggelt (das ist die LED zur Sekundenanzeige).
In der ISR wird dann die Zeit in Minuten und Stunden aktualisiert und durch LEDs angezeigt.
Außerdem werden in der ISR die beiden Taster abgefragt, mit denen die Uhrzeit gestellt werden kann.

Zur Programmierung habe ich das Atmel-Studio verwendet.

Nachfolgend ein Bild und ein kurzes Video der Uhr. Im Anschluss daran befindet sich mein Assemblerprogramm.
Hinsichtlich der Genauigkeit der Uhr musste ich den Compare-Match-Wert im Register OCR1A korrigieren. Der Taktgeber (Quarz) auf meinem Funduino-Board läuft nicht genau auf 16 MHz, sondern etwas schneller. Dadurch ist die Uhr auch nach relativ kurzer Zeit deutlich vorgegangen.
Durch mehrere Messungen konnte ich eine Ungenauigkeit des Quarzes von ca. 450ppm ermitteln. Deshalb habe ich den Vergleichswert in OCR1A etwas angehoben, bis die Uhrzeit über mehrere Tage dann zumindest bei einem Vergleich mit einer Funkuhr ziemlich genau ging.

Platine zur Nerd-Uhr
Nerd-Uhr in Aktion 🙂

In dem nachfolgenden Bild sind die Anschlüsse der LEDs an den Arduino dargestellt. Für die Widerstände habe ich den Wert 220 Ohm verwendet.

; Nerd-Uhr.asm
;=========================================================================
; Nerd-Uhr für ATmega 2560
; Timer/Counter 1 im CTC-Modus. TC1 ist ein 16 Bit Zähler.
; Compare Output Mode ist COM1A1=0/COM1A0=1: Toggle OC1A on compare match
; Die LED ist am Port B an Bit 5 (OC1A) angeschlossen (Digital Pin 11)
; Taktfrequenz von TC1 ist in diesem Programm 62,5 kHz (Vorteiler 256)
; Alle 31250/62,5 kHz --> t_ein = t_aus = 0,5 s --> f = 1 Hz
;=========================================================================

.equ HALF_SECONDS = 0x0200
.equ SECONDS = 0x0201
.equ MINUTES = 0x0202
.equ HOURS = 0x0203

;=======Start des Assembler-Programms=====================================
.org 0x0000
; Sprung zur Initialisierung
	rjmp INIT
;=========================================================================

;=======Interrupt-Sprungtabelle (Beispiel)================================
.org 0x0022 ; Timer/Counter1 Compare Match A ISR
	rjmp TIM1_COMPAREMATCH_A
;=======Interrupt-Sprungtabelle (Ende)====================================

;=======Interrupt Service Routinen (ISR)==================================
; Timer/Counter1 Compare Match A ISR
TIM1_COMPAREMATCH_A:

; alle Interrupts sperren
	cli
; Register R16 und R17 sichern
	push R16
	push R17

; Wurde der Taster zur Minuteneinstellung (PORTB1) gedrückt?
; Wenn ja, rufe das Unterprogramm hierzu auf
	sbis PINB, PORTB1
	rcall ADJUST_MINUTES

; Wurde der Taster zur Stundeneinstellung (PORTB0) gedrückt?
; Wenn ja, rufe das Unterprogramm hierzu auf
	sbis PINB, PORTB0
	rcall ADJUST_HOURS

; Halbe Sekunden lesen
	lds R16, HALF_SECONDS
	inc R16
	cpi R16, 2
	breq INCREMENT_SECONDS
	sts HALF_SECONDS, R16
	rjmp BACK

INCREMENT_SECONDS:
; Sekunden lesen
	ldi R16, 0
	sts HALF_SECONDS, R16

	lds R16, SECONDS
	inc R16
	cpi R16, 60
	breq INCREMENT_MINUTES
	sts SECONDS, R16
	rjmp BACK

INCREMENT_MINUTES:
; Minuten lesen
	ldi R16, 0
	sts SECONDS, R16

	lds R16, MINUTES
	inc R16
	cpi R16, 60
	breq INCREMENT_HOURS
	sts MINUTES, R16
	rjmp BACK


INCREMENT_HOURS:
; Stunden lesen
	ldi R16, 0
	sts MINUTES, R16

	lds R16, HOURS
	inc R16
	cpi R16, 24
	breq SET_HOURS_ON_ZERO
	sts HOURS, R16
	rjmp BACK

SET_HOURS_ON_ZERO:
	ldi R16, 0
	sts HOURS, R16

BACK:
; Aktuelle Zeit ausgeben
	lds R16, MINUTES
	out PORTC, R16

	lds R16, HOURS
	out PORTA, R16

; Register R16 und R17 zurückladen
	pop R16
	pop R17
; alle Interrupts "scharf" schalten
	sei
; Rücksprung
	reti
;=======Interrupt Service Routinen (ISR) (Ende)===========================


;=======Initialisierung (Anfang)==========================================
INIT:
; alle Interrupts sperren
	cli

; Stackpointer initialisieren
	ldi	R16, HIGH(RAMEND)
	out	SPH, R16
	ldi	R16, LOW(RAMEND)     
	out	SPL, R16

; LED für Sekunde
; Port B Bit 5 als Ausgang schalten
	sbi DDRB, DDB5

; LEDs für Minuten
; Port C Bit 0..5 als Ausgang schalten
	ldi R16, 0b00111111
	out DDRC, R16
	out PORTC, R16

; LEDs für Stunden
; Port A Bit 0..4 als Ausgang schalten
	ldi R16, 0b00011111
	out DDRA, R16
	out PORTA, R16

; Taster für Stunden- und Minuten-Einstellung
; Pull Up einschalten
; Taster für Stunden
	sbi PORTB, PORTB0
; Taster für Minuten	
	sbi PORTB, PORTB1

; Speicherstellen für Uhrzeit initialisieren auf 12:30:00
	ldi R16, 0x00
	sts HALF_SECONDS, R16
	sts SECONDS, R16

	ldi R16, 30
	sts MINUTES, R16

	ldi R16, 12
	sts HOURS, R16

; Initialisieren der TC1-Register A und B
; Compare Output Mode ist COM1A1=0/COM1A0=1
; Toggle OC1A on compare match

; TCCR1A
	ldi R16, 0x00
	ldi R17, (1<<COM1A0)
	eor R16, R17
; Das Register TCCR1A kann nicht mit "out" beschrieben werden, da es
; außerhalb des mit "out" möglichen Adressbereichs liegt
	sts TCCR1A, R16

; TCCR1B (CS12:0 = 4)
; Hier wird mit CS00=0, CS01=0 und CS02=1 der Vorteiler für den
; Takt des TC1 auf den Wert 256 eingestellt.
; Die Taktfrequenz 16 MHz des Mikrocontrollers wird durch 256 dividiert
; Mit dieser neuen Taktfrequenz (62,5 kHz) wird der TC1 getaktet.
; Sobald dieser 16 Bit-Zähler den Wert im OCR1A erreicht, wird der
; Output Compare Match A-Interrupt ausgelöst und es wird die 
; zuständige ISR ausgeführt.
	ldi R16, 0x00
	ldi R17, (1<<CS12)
	eor R16, R17

; Toggle OC1A erfordert das Setzen der Wave Form Generation Bits
; Für Toggle OC1A ist WGM12 auf 1 zu setzen
	ldi R17, (1<<WGM12)
	eor	R16, R17

; Das Register TCCR1B kann nicht mit "out" beschrieben werden, da es
; außerhalb des mit "out" möglichen Adressbereichs liegt
	sts TCCR1B, R16

; Das OCR1A wird beschrieben. Dieses Register ist ein 16 Bit Register.
; Zuerst wird der HIGH-Teil beschrieben, dann der LOW-Teil
; To do a 16-bit write, the high byte must be written before the low byte.
; For a 16-bit read, the low byte must be read before the high byte.
; Geschrieben wird hierzu der Wert 31250 - 1 = 31249 = 0x7A11
; Messungen bei meinem Arduino haben eine Abweichung ~8s auf ~5h ergeben,
; die die Uhr zu schnell läuft. Das sind 8/(5*3600s) = ~440ppm
; Der errechnte Wert wird also um 440ppm erhöht --> 13,7 --> 14 --> 0x7A1F
	ldi R16, 0x7A
	sts OCR1AH, R16
	ldi R16, 0x21
	sts OCR1AL, R16

; Timer/Counter1 Compare Match A Interrupt Enable auf 1 setzen
	ldi R16, (1<<OCIE1A)
	sts TIMSK1, R16

	rcall LED_TEST

; alle Interrupts "scharf" schalten
	sei
;=======Initialisierung (Ende)============================================


;===Haupt-Programm in Endlosschleife (Anfang)=============================
MAIN:
; hier passiert nichts
	rjmp MAIN
;===Haupt-Programm in Endlosschleife (Ende)===============================

;===Unterprogramme (Anfang)===============================================
;===LED_Test==============================================================
LED_TEST:
	push R16
	
; LEDs für Minuten	
	ldi R16, 0x01
LOOP_MINUTES:
	out PORTC, R16
	rcall DELAY_250MS
	lsl R16
	cpi R16, 0b01000000
	brne LOOP_MINUTES

	ldi R16, 0x00
	out PORTC, R16

; LEDs für Stunden
	ldi R16, 0x01
LOOP_HOURS:
	out PORTA, R16
	rcall DELAY_250MS
	lsl R16
	cpi R16, 0b00100000
	brne LOOP_HOURS

	ldi R16, 0x00
	out PORTA, R16

	pop R16
	ret
;===LED-Test==============================================================


;===ADJUST_MINUTES========================================================
ADJUST_MINUTES:
	push R16

; Wert in Speicherstelle für Minuten holen, inkrementieren
; und auf Überlauf testen; bei Überlauf (60) Minuten auf 0
	lds R16, MINUTES
	inc R16
	cpi R16, 60
	brne NOT_ZERO_MINUTES
	ldi	R16,0

NOT_ZERO_MINUTES:
	sts	MINUTES, R16

; Sekunden und halbe Sekunden auf 0 stellen
	ldi R16, 0
	sts	HALF_SECONDS, R16
	sts	SECONDS, R16

	pop R16
	ret
;===ADJUST_MINUTES (Ende)=================================================


;===ADJUST_HOURS==========================================================
ADJUST_HOURS:
	push R16

; Wert in Speicherstelle für Stunden holen, inkrementieren
; und auf Überlauf testen; bei Überlauf (24) Stunden auf 0
	lds R16, HOURS
	inc R16
	cpi R16, 24
	brne NOT_ZERO_HOURS
	ldi	R16,0

NOT_ZERO_HOURS:
	sts	HOURS, R16

; Sekunden und halbe Sekunden auf 0 stellen
	ldi R16, 0
	sts	HALF_SECONDS, R16
	sts	SECONDS, R16

	pop R16

	ret
;===ADJUST_HOURS (Ende)===================================================


;===Warteschleife (Anfang)================================================
; Warteschleife für 250ms
; f_takt = 16 MHz --> T_takt = 62,5 ns
; für 250ms sind 4.000.000 Taktzyklen erforderlich
; Feste Maschinenzyklen durch push, pop, ret und Initialisierung
; (2 + 2 + 2) + (2 + 2 + 2) + 5 + 1 = 18 MZ
; Notwendige Gesamtdauer durch Schleifen: 3.999.982 MZ
; Tatsächliche Gesamtdauer durch Schleifen: 4.000.047 MZ
DELAY_250MS:
; Befehle des Unterprogramms
	push	R16				; Sichern der Register auf dem Stack
	push	R17				; push sind 2 Maschinenzyklen (MZ)
	push	R18				; --> 6 MZ

; Initialisiere Register für Herunterzählen
	ldi		R18, 16			; Lade R18 mit 16 dezimal (1 MZ)

; LOOP_3 dauert (1 MZ + LOOP_2 + 3 MZ) * 16  - 1 MZ = (249.999 MZ + 4 MZ) * 16 - 1 MZ =  4.000.047 MZ
LOOP_3:
	ldi		R17, 250		; Lade R17 mit 250 dezimal (1 MZ)

; LOOP_2 dauert (1 MZ + LOOP_1 + 8 MZ) * 250 - 1 MZ = (991 MZ + 9 MZ) * 250 - 1 MZ = 249.999 MZ
LOOP_2:
	ldi		R16, 248		; Lade R16 mit 248 dezimal (1 MZ)

; LOOP_1 dauert 4 MZ * 248 - 1 MZ = 992 MZ - 1 MZ (bei nicht erfüllter Bedingung) = 991 MZ
LOOP_1:
	dec		R16				; Dekrementiere R16 (1 MZ)
	nop						; (1 MZ)
	brne	LOOP_1			; Verzweige bei Zero-Bit = 0 (R16 <> 0)
							; Bei Verzweigung: 2 MZ, sonst 1 MZ

	dec		R17				; Dekrementiere R17 (1 MZ)
	nop						; (1 MZ)
	nop						; (1 MZ)
	nop						; (1 MZ)
	nop						; (1 MZ)
	nop						; (1 MZ)
	brne	LOOP_2			; R16 neu laden und dekrementieren
							; Bei Verzweigung: 2 MZ, sonst 1 MZ

	dec		R18				; Dekrementiere R17 (1 MZ)
	brne	LOOP_3			; R16 neu laden und dekrementieren
							; Bedingung falsch: 1 MZ, sonst 2 MZ

	pop		R18				; Register aus Stack wiederherstellen
	pop		R17				; pop sind 2 MZ
	pop		R16				; --> 6 MZ
	ret						; Rücksprung (5 MZ)
;===Warteschleife (Ende)==================================================
;===Unterprogramme (Ende)=================================================

Anmerkung: Da im originalen Programm $-Zeichen für die Hexdarstellung verwendet wurden, habe ich diese ausgetauscht gegen 0x, da die Code-Darstellung nicht einwandfrei funktioniert hat.
Falls doch etwas bei der Darstellung oben durch das Code-Plugin „gefressen“ wurde, nachfolgend das Original-Programm (main.asm) als 7z-Datei.