W styczniowym i lutowym numerze Nowego Olimpu przedstawiliśmy panel pomiarowy który może być użyty zarówno na lekcjach jak i na pokazach. Część elektroniczna i mechaniczna została mniej więcej omówiona – teraz czas na oprogramowanie.
Panel pomiarowy zbudowano w oparciu o procesor Atmega8. Procesor ten ma wystarczająca moc obliczeniową by mierzyć napięcia oraz czas pomiędzy zdarzeniami jednocześnie obsługując wyświetlacz i mógłby się zajmować jeszcze wieloma innymi zadaniami, jednak trudno o tańszy i prostszy procesor a niewykorzystanie wszystkich możliwości nie jest specjalnym problemem. Zastosowany mikrokontroler posiada 1KB pamięci RAM oraz 8KB pamięci programu. Dodatkowo procesor ma 32 rejestry które mogą być użyte w większości poleceń. Ponieważ program obsługi naszego miernika jest stosunkowo prosty, większość informacji możemy umieścić bezpośrednio w rejestrach.
Nasz miernik będzie przystosowany do współpracy z wieloma przystawkami pomiarowymi. W zależności od podłączonego detektora, program powinien dostosować swoje zachowanie wywołując odpowiednie podprogramy i sterując procesem pomiaru i wyświetlania. Jednak niektóre procedury są wspólne i warto się nad nimi na początek zastanowić. Po pierwsze, będziemy potrzebowali programu sterującego wyświetlaczami. Pamiętajmy, że wyświetlacze podłączone są w taki sposób, że można zaświecić tylko jeden z nich, i aby panować nad wyświetlaniem wszystkich cyfr, trzeba cały czas przełączać informacje sterujące zarówno poszczególnymi segmentami jak i tym który wyświetlacz powinien być w danej chwili włączony. Odpowiednia procedura powinna być wołana kilkaset razy na sekundę.
Warto przygotować sobie kawałek programu który będzie wyświetlał zawartość trzech rejestrów i przy okazji powoli na wywołanie zadanej procedury 1000 razy na sekundę. Lub jeszcze lepiej – pozwoli na ustawienie jak często to procedura będzie wywoływana i od tego zaczniemy pisanie naszego programu. Warto przy okazji usystematyzować trochę kod i założyć, że każdy fragment naszego systemu będzie posiadał część inicjująca oraz wykonawczą. Procedury inicjujące będą miały etykiety zaczynające się od init. Procedury wykonujące pomiary będą zazwyczaj wykonywane w przerwaniach – na przykład zegarowych lub przerwań zewnętrznych będą miały także odpowiednie nazwy.
Program jest dość złożony i w chwili obecnej zawiera ponad 500 linii kodu. Dlatego będziemy omawiali go prezentując w jednym odcinku po jednej funkcjonalności.
Obsługę wewnętrznego timera oprzemy o wewnętrzny zegar nr 2. Będzie on zliczał dzieląc częstotliwość zegara procesora przez 2000, co przy zegarze 2MHz da nam jedno przerwanie na 1ms.:
initCron: ldi r16, 0b10001010 ; CNT, fosc/8
out TCCR2, r16
ldi r16, 125 ; divider
out OCR2, r16
ldi r16, 0
out TCNT2, r16
in r16, TIMSK
ori r16, 0b10000000 ; interupt on compare
out TIMSK, r16
Po zainicjowaniu przerwań i ustawieniu częstotliwości warto zastanowić się nas ustawieniem dokładności przerwań które będą wywoływane. Licznik który będzie liczył co które przerwanie wołać procedurę pomiarową. Jego wartość umieścimy w parze rejestrów r11:r10. Ponieważ jednak wartość tego licznika będziemy ponownie ustawić po odliczeniu do 0 – musimy gdzieś zapamiętać wartość którą należy ustawić. Nie musi to być cała wartość – lecz wartość która pozwoli ją odtworzyć. Ponieważ potrzebujemy podzielniki 1, 10, 100 i 1000 (do uzyskania dokładności 1/1000s, 1/100s, 1/10s i 1s). Wartość podzielnika możemy więc zapamiętać na 2 bitach które umieścimy w rejestrze r8 na pozycjach 3 i 4.
Przygotujemy sobie procedurę która ustawi zawartości licznika:
cronResetAccuracy: clr r11
mov r16, r8
andi r16, 0b00011000
breq os_crac_00
cpi r16, 0b00001000
breq os_crac_01
cpi r16, 0b00010000
breq os_crac_10
os_crac_11: ldi r16, HIGH(1000)
mov r11, r16
ldi r16, LOW(1000)
mov r10, r16
ret
os_crac_10: ldi r16, 100
rjmp os_crac_0A
os_crac_01: ldi r16, 10
rjmp os_crac_0A
os_crac_00: ldi r16, 1
os_crac_0A: clr r11
mov r10, r16
ret
Program obsługi przerwania musi zapamiętać rejestry z których korzysta. Ba pewno musimy zapamiętać rejestr znaczników oraz rejestr r16 który używamy jako rejestr tymczasowy. Ponadto będzie nam potrzebny rejestr z służący do adresowania.
Poza pokazaniem kolejnego segmentu wyświetlacza, w procedurze zegarowej musimy jeszcze zmniejszyć licznik r11:r10 i gdy osiągnie zero – wywołać procedurę związana z obsługa aktualnie podłączonego urządzenia. Adres tej procedury będziemy pamiętać w rejestrach r0:r1. Przyda się nam też obsługa migania wyświetlacza. Czasem by wskazać błąd, będziemy migać całą zawartością wyświetlacza. Maskę która będziemy nakładać na wartość do wyświetlenia – umieścimy w rejestrze r7, a pierwszy bit rejestry r8 użyjemy jako flagę zezwalającą lub zabraniająca migotania:
timerTick: push r16
in r16, SREG
push r16
push zl
push zh
; show next digit
rcall showSegment
; increament tick counter
inc r9
brne os_ttick_01
; each 256ms check blinking bit
ldi r16, $1
and r16, r8
brne os_ttick_bln
ldi r16, $ff
cp r16, r7
brne os_ttick_01
; if set - change display state
os_ttick_bln: mov r16, r7
clr r7
cpi r16, $ff
breq os_ttick_01
com r7
os_ttick_01: ldi r16, 3
dec r10
brne os_ttick_02
and r11, r11
breq os_ttick_03
dec r11
rjmp os_ttick_02
os_ttick_03: mov zl, r0
mov zh, r1
icall
rcall cronResetAccuracy
os_ttick_02: pop zh
pop zl
pop r16
out SREG, r16
pop r16
reti
Pozostała nam jeszcze do napisania procedura wyświetlająca zawartość pamięci na wyświetlaczu. Na początek warto zainicjować wyświetlacz ustawiając kierunki na portach sterujących wyświetlaczem:
initDisplay: ; setup or port used by display
ldi r16, $ff
out DDRB, r16
ldi r16, 0
out PORTB, r16
in r16, DDRD
ori r16, $e0
out DDRD, r16
in r16, PORTD
andi r16, $1f
out PORTD, r16
ret
Samo wyświetlanie pobiera wartości z rejestrów. Na początek sprawdzamy czy wyświetlacz został wygaszony sprawdzając zawartość rejestru r7. Jeśli tak – to wyłączamy wszystkie segmenty. Jeśli nie jest wygaszony – rejestr r7 zawiera informacje o tym który segment powinien zostać w danej chwili wyświetlony. Włączany wtedy odpowiedni bit portu D i ustawia na porcie B wartość z jednego z rejestrów r4:r6 zawierających stan wyświetlacza:
showSegment: ; shows the next segment
push r16
mov r16, r7
cpi r16, $ff
brne os_shseg_03
clr r16
cbi PORTD, 5
cbi PORTD, 6
cbi PORTD, 7
rjmp os_shseg_01
os_shseg_03: andi r16, 3
breq os_shseg_A
cpi r16, 1
breq os_shseg_B
os_shseg_C: cbi PORTD, 7
out PORTB, r6
sbi PORTD, 6
clr r7
rjmp os_shseg_01
os_shseg_B: cbi PORTD, 5
out PORTB, r5
sbi PORTD, 7
rjmp os_shseg_02
os_shseg_A: cbi PORTD, 6
out PORTB, r4
sbi PORTD, 5
os_shseg_02: inc r7
os_shseg_01: pop r16
ret