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.
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.