Częstościomierz cyfrowy to nie tylko odpowiednio połączone obwody elektroniczne, ale także program. Do tego program na tyle prosty, że warto go na spokojnie przeanalizować. Możemy go także potraktować jako punkt wyjścia do oprogramowania innych funkcjonalności. Niestety, program będzie w assemblerze, języku uznawanym za trudny bo niskopoziomowy. Ale jak inaczej zrozumieć działalnie procesora?
Na początek warto opisać zastosowanie poszczególnych rejestrów, oraz wejść i wyjść. To ważne, bo bez tego trudno się będzie zorientować co do czego służy. Taki komentarz przyda się przy analizie kodu a i ewentualne modyfikacje nie powinny być potem problemem.
;-------------------------------------------------------------------------------
.include "m8adef.inc" ; Atmega8
;-------------------------------------------------------------------------------
; ------------------------------------
; register usage:
; ----------------
; r16, z, - common purpose registers, to be used for current calculation and
; passing parameters to subroutines. Usually changed after return
; r17-18 - common use registers
; r0-5 - display content
; r6 - currently displayed digit
; r7-9 - number to dispaly
; ------------------------------------
; pinout:
; ----------------
; PB0 -
; PB1 - display 4
; PB2 - time gate output
; PB3 - programmer
; PB4 - programmer
; PB5 - programmer
; PB6 - xtal
; PB7 - xtal
; PC0 - display A
; PC1 - display B
; PC2 - display C
; PC3 - display D
; PC4 - display E
; PC5 - display F
; PC6 - reset - resistor 10k to Vcc
; PD0 - display 3
; PD1 - display 2
; PD2 - display 1
; PD3 - display 0
; PD4 - pulse input
; PD5 - display 5
; PD6 - display G
; PD7 - display H
;-------------------------------------------------------------------------------
W pamięci RAM –czyli pamięci przeznaczonej na dane, rezerwujemy miejsce na stos wywołań podprogramów. Stos będzie też używany do zapamiętywania stanu rejestrów. To ważne, bo w przerwaniach nie powinniśmy zmodyfikować żadnych rejestrów które mogą być używane.
.dseg
.equ stack_size = 128
stack: .byte stack_size
Kolejnym elementem programu jest wektor przerwań. Ta tablica skoków określa instrukcje które są wykonane w momencie w którym zostaje wywołane odpowiednie przerwanie. Oczywiście jedyną sensowną instrukcją jest instrukcja skoku w miejsce w którym odpowiedni podprogram jest zapisane. Skoki takie są konieczne tylko dla tych przerwań, które są używane. W naszym przypadku – używamy tylko trzech przerwań pochodzących od wewnętrznych zegarów
;-------------------------------------------------------------------------------
.cseg
.org 0
; interupt vector
rjmp i_reset
reti ; rjmpi_ext_int0
reti ; rjmpi_ext_int1
reti ; rjmpi_tim2_comp
rjmp out_next_digit
reti ; rjmpi_tim1_capt
reti ; rjmpi_tim1_compa
rjmp timer_int
reti ; rjmpi_tim1_ovf
rjmp counter_int
reti ; rjmpi_spi_stc
reti ; rjmpi_usart_rxc
reti ; rjmpi_usart_udre
reti ; rjmpi_usart_txc
reti ; rjmpi_adc
reti ; rjmpi_ee_rdy
reti ; rjmpi_ana_comp
reti ; rjmpi_twsi
reti ; rjmpi_spm_rdy
;-------------------------------------------------------------------------------
Główna część programu składa się z inicjalizacji stosu, licznika oraz wyświetlacza. Po zakończeniu inicjalizacji – program wpada w nieskończoną pętlę – wszystko co program będzie robił – będzie robił w przerwaniach.
i_reset: ; Initialize whole program. Setup all internal periferials
;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cli
; init stack
ldi zh, high(stack+stack_size-1)
ldi zl, low(stack+stack_size-1)
out SPH, zh
out SPL, zl
rcall init_display
rcall init_counter
rcall init_timer
sei
loop: ; Main loop of processor - just doing nothing because everything
; is handled in interuptes
;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
nop
nop
nop
l1: dec r18
brne l1
dec r17
brne l1
rjmp loop
;-------------------------------------------------------------------------------
Teraz pora na przygotowanie wyświetlania znaków na wyświetlaczach siedmiosegmentowych. Ze względu na małą liczbę wyprowadzeń procesora, katody wszystkich wyświetlaczy są połączone razem i przyłączone do ośmiu wyprowadzeń. Anody – są sterowane przez tranzystory włączane przez sześć innych wyjść procesora. W jednym momencie świecą segmenty tylko jednego wyświetlacza, a program dba o to by przełączać aktywny wyświetlacz co 1ms. Zajmuje się tym podprogram wyzwalany przerwaniem związanym z przepełnieniem licznika T2.
Inicjując wyświetlacz musimy przełączyć odpowiednie porty tak by pracowały jako wyjścia, włączyć przerwania i ustawić licznik T2 tak by pracował z 1/64 częstotliwości zegara procesora.
Podczas wyświetlania niestety musimy wykonać parę dodatkowych operacji. Oprócz sprawdzenia który znak powinien być obecnie wyświetlany, i oprócz włączenia i wyłączenia sygnału na odpowiedniej anodzie, sam znak musimy przesłać na wyjścia które są częściami dwóch różnych portów. Niestety wyjścia procesora mają dodatkowe punkcje których używamy i nie dysponujemy całym żadnym całym portem.
;-------------------------------------------------------------------------------
init_display: ; changes r16
ldi r16, DDRC
ori r16, $3f
out DDRC, r16
ldi r16, DDRD
ori r16, $c0
out DDRD, r16
sbi DDRD, 3
sbi DDRD, 2
sbi DDRD, 1
sbi DDRD, 0
sbi DDRB, 1
sbi DDRD, 5
sbi PORTD, 3
sbi PORTD, 2
sbi PORTD, 1
sbi PORTD, 0
sbi PORTB, 1
sbi PORTD, 5
ldi r16, $04
out TCCR2, r16
in r16, TIMSK
andi r16, $3f
ori r16, $40
out TIMSK, r16
ret
out_next_digit: ; changes noting - interupt
push r16
in r16, SREG
push r16
push r17
push r18
mov r16, r6
cpi r16, 0
breq out_1_digit
cpi r16, 1
breq out_2_digit
cpi r16, 2
breq out_3_digit
cpi r16, 3
breq out_4_digit
cpi r16, 4
breq out_5_digit
out_6_digit: sbi PORTB, 1
mov r16, r5
rcall out_digit
cbi PORTD, 5
ldi r16, $ff
mov r6, r16
rjmp out_end
out_5_digit: sbi PORTD, 0
mov r16, r4
rcall out_digit
cbi PORTB, 1
rjmp out_end
out_4_digit: sbi PORTD, 1
mov r16, r3
rcall out_digit
cbi PORTD, 0
rjmp out_end
out_3_digit: sbi PORTD, 2
mov r16, r2
rcall out_digit
cbi PORTD, 1
rjmp out_end
out_2_digit: sbi PORTD, 3
mov r16, r1
rcall out_digit
cbi PORTD, 2
rjmp out_end
out_1_digit: sbi PORTD, 5
mov r16, r0
rcall out_digit
cbi PORTD, 3
out_end: inc r6
pop r18
pop r17
pop r16
out SREG, r16
pop r16
reti
out_digit: ; input r16 - bit image; active: 0
; changes r17,r18
in r17, PORTC
andi r17, $c0
mov r18, r16
andi r18, $3f
or r18, r17
out PORTC, r18
in r17, PORTD
andi r17, $3f
mov r18, r16
andi r18, $c0
or r18, r17
out PORTD, r18
ret
;-------------------------------------------------------------------------------
Wyświetlanie wymaga wartości przygotowanych dla wyświetlacza siedmiosegmentowego. Dlatego warto przygotować procedurę która zamieni numer znaku na odpowiednie zapalone i zgaszone segmenty wyświetlacza
char_views: .db 0b00010010, 0b11010111, 0b00011100, 0b01010100 ; 0123
.db 0b11010001, 0b01110000, 0b00110000, 0b11010110 ; 4567
.db 0b00010000, 0b01010000, 0b11101111, 0b11111101 ; 89.-
.db 0b00111000, 0b10111101, 0b00110101, 0b11111111 ; Ero
.db 0b00111011, 0b10010001 ; LH
get_char_view: ; changes z(r30,r31)
; input r16 - char number
; output r16 - bit image; active:0
ldi zl, low(char_views * 2)
ldi zh, high(char_views * 2)
add zl, r16
ldi r16, 0
adc zh, r16
lpm r16, z
ret
Teraz trzeba przygotować procedurę która zmienili liczbę na ciąg cyfr. Nie jest to wbrew pozorom prosta operacja, bo procesor z jakim mamy do czynienia nie pozwala na wykonywanie dzielenia, a ponadto będziemy mieli do czynienia ze stosunkowo długimi liczbami. Jeśli chcemy wyświetlać liczby sześciocyfrowe – do ich zapisy będziemy używali 20 bitów, a więc aż trzy rejestry.
Operacja konwersji jest skomplikowana jeśli chodzi o ilość kodu, ale tak naprawdę jest bardzo prosta. Zamiast dzielić, będziemy sprawdzali czy liczba jest większa od pewnej stałej, i jeśli jest, to ową stałą będziemy odejmowali. Jeśli wykonamy ta operację dla stałej wynoszącej odpowiednio 800000, 400000, 200000, 10000, to uzyskamy cztery bity cyfry oznaczającej setki tysięcy. Musimy tylko taką liczbę rozłożyć na bajty, i sprawa wydaje się prosta. Jeśli wynik będzie mały – wystarczy operować dwoma bajtami, a dla dwóch najmniej znaczących liczb – będziemy operowali pojedynczym bajtem.
Ta procedura jest skuteczna, pod warunkiem że wejściowa liczba jest mniejsza niż milion. Dlatego warto sprawdzić to na początki i jeśli nie jest – wyświetlić informację o błędzie
;-------------------------------------------------------------------------------
convert_number: ; converts number from r7,8,9 to digits from r0,1,2,3,4,5
; changes r16-23, z(r30,31)
mov r21, r7
mov r22, r8
mov r23, r9
ldi r18, 64
ldi r19, 66
ldi r20, 15
rcall compare_3
brlo convert_3
show_error: ; changes r16,z(r30,r31)
ldi r16, $0f
rcall get_char_view
mov r5, r16
ldi r16, $0c
rcall get_char_view
mov r4, r16
ldi r16, $0d
rcall get_char_view
mov r3, r16
ldi r16, $0d
rcall get_char_view
mov r2, r16
ldi r16, $0e
rcall get_char_view
mov r1, r16
ldi r16, $0d
rcall get_char_view
mov r0, r16
ret
convert_3: ldi r16, 0 ; digit 6
ldi r17, 8
ldi r18, 0
ldi r19, 53
ldi r20, 12
rcall check_bit_3
ldi r17, 4
ldi r18, 128
ldi r19, 26
ldi r20, 6
rcall check_bit_3
ldi r17, 2
ldi r18, 64
ldi r19, 13
ldi r20, 3
rcall check_bit_3
ldi r17, 1
ldi r18, 160
ldi r19, 134
ldi r20, 1
rcall check_bit_3
cpi r16, 0
brne convert_a
ldi r16, $0f
convert_a: rcall get_char_view
mov r5, r16
ldi r16, 0 ; digit 5
ldi r17, 8
ldi r18, 128
ldi r19, 56
ldi r20, 1
rcall check_bit_3
ldi r17, 4
ldi r18, 64
ldi r19, 156
ldi r20, 0
rcall check_bit_3
ldi r17, 2
ldi r18, 32
ldi r19, 78
rcall check_bit_2
ldi r17, 1
ldi r18, 16
ldi r19, 39
rcall check_bit_2
rcall get_char_view
rcall add_dot
mov r4, r16
ldi r16, 0 ; digit 4
ldi r17, 8
ldi r18, 64
ldi r19, 31
rcall check_bit_2
ldi r17, 4
ldi r18, 160
ldi r19, 15
rcall check_bit_2
ldi r17, 2
ldi r18, 208
ldi r19, 7
rcall check_bit_2
ldi r17, 1
ldi r18, 232
ldi r19, 3
rcall check_bit_2
rcall get_char_view
mov r3, r16
ldi r16, 0 ; digit 3
ldi r17, 8
ldi r18, 32
ldi r19, 3
rcall check_bit_2
ldi r17, 4
ldi r18, 144
ldi r19, 1
rcall check_bit_2
ldi r17, 2
ldi r18, 200
ldi r19, 0
rcall check_bit_2
ldi r17, 1
ldi r18, 100
rcall check_bit_1
rcall get_char_view
mov r2, r16
ldi r16, 0 ; digit 2
ldi r17, 8
ldi r18, 80
rcall check_bit_1
ldi r17, 4
ldi r18, 40
rcall check_bit_1
ldi r17, 2
ldi r18, 20
rcall check_bit_1
ldi r17, 1
ldi r18, 10
rcall check_bit_1
rcall get_char_view
rcall add_dot
mov r1, r16
mov r16, r21
rcall get_char_view
mov r0, r16
ret
add_dot: mov r17, r16
ldi r16, 10
rcall get_char_view
and r16, r17
ret
check_bit_3: ; compares r18,19,20 with r21,22,23 and if not less -
; subtracts r16.. from r19... and adds r17 to r16
rcall compare_3
brlo cmp_end
sub r21, r18
sbc r22, r19
sbc r23, r20
add r16, r17
ret
check_bit_2: ; compares r18,19 with r21,22 and if not less -
; subtracts r16.. from r19... and adds r17 to r16
rcall compare_2
brlo cmp_end
sub r21, r18
sbc r22, r19
add r16, r17
ret
check_bit_1: ; compares r18 with r21 and if not less -
; subtracts r16.. from r19... and adds r17 to r16
cp r21, r18
brlo cmp_end
sub r21, r18
add r16, r17
ret
compare_3: ; compares r18,19,20 with r21,22,23
cp r23, r20
brne cmp_end
compare_2: cp r22, r19
brne cmp_end
cp r21, r18
cmp_end: ret
;-------------------------------------------------------------------------------
Zamo zliczanie impulsów zrealizujemy na liczniku T0, którego wejście jest dostępne jako alternatywna funkcja jednego z portów. Licznik ten potrafi liczyć do 256, ale przy przepełnieniu może zgłaszać przepełnienie jako przerwanie. W tym przerwaniu wystarczy zwiększać rejestry które potraktujemy jako kolejne bajty licznika. Musimy tylko pamiętać, że w przerwaniu nie wolno nam zmienić stanu procesora, a operacje zwiększania wartości rejestru – modyfikują znaczniki stanu. Dlatego musimy je na początku zapamiętać na stosie.
init_counter: ; initialises external counter realized on timer/counter T0
; storing upper bytes in r10,11
; changes r16
cbi DDRD, 4
cbi PORTD, 4
ldi r16, $06
out TCCR0, r16
in r16, TIMSK
ori r16, $01
out TIMSK, r16
counter_reset: ; changes r16, resets all bytes of count
ldi r16, 0
out TCNT0, r16
mov r10, r16
mov r11, r16
ret
counter_int: ; nothing changes - interupt
push r16
in r16, SREG
push r16
inc r10
brne cint_01
inc r11
cint_01: pop r16
out SREG, r16
pop r16
reti
Odczyt licznika i zamiana jego wartości na liczbę jest tylko złożeniem procedur które są gotowe. Pamiętajmy tylko, że pierwszy bajt – jest w rejestrze licznika.
counter_read: ; changes r16-23, z(r30,31)
; sets r7..9 acording to counted value
; sets r0..5 to display apropriate digits
in r16, TCNT0
mov r17, r10
or r17, r11
brne cr_01
cpi r16, 200
brsh cr_01
ldi r16, 0
mov r7, r16
mov r8, r16
mov r9, r16
rjmp convert_number
cr_01: mov r7, r16
mov r8, r10
mov r9, r11
rjmp convert_number
Teraz wystarczy odliczenie odpowiedniego czasu na liczniku T1. Zastosowano tu tryb generowania fali PWM, przy czym dla nas istotny jest czas trwania stanu wysokiego – użytego do bramkowania sygnału wejściowego. Przy zmianie stanu – zgłaszane przerwanie czyta stan licznika, przekazuje go to wyświetlania, oraz kasuje licznik przygotowując go do kolejnego cyklu zliczania.
;-------------------------------------------------------------------------------
init_timer: ; changes r16
ldi r16, $23
out TCCR1A, r16
ldi r16, $1d
out TCCR1B, r16
ldi r16, 9
push r16
out OCR1BH, r16
ldi r16, 196
out OCR1BL, r16
pop r16
inc r16
inc r16
out OCR1AH, r16
ldi r16, 0
out OCR1AL, r16
ldi r16, $ff
out TCNT1H, r16
out TCNT1H, r16
sbi DDRB, 2
in r16, TIMSK
ori r16, $08
out TIMSK, r16
ret
timer_int: ; nothing changes - interupt
push r16
in r16, SREG
push r16
push r17
push r18
push r19
push r20
push r21
push r22
push r23
rcall counter_read
rcall counter_reset
pop r23
pop r22
pop r21
pop r20
pop r19
pop r18
pop r17
pop r16
out SREG, r16
pop r16
reti
;-------------------------------------------------------------------------------