AVR Asembler. Prosty licznik impulsów.
Podczas lektury poprzedniego odcinka kursu poznaliśmy podstawy organizacji pamięci oraz wykorzystania zasobów mikrokontrolera AT90S8515. Nauczyliśmy się w jaki sposób skonfigurować linie portów mikrokontrolera, jak odczytać stan portu wejściowego i jak zaświecić diodę LED dołączoną do wyprowadzenia mikrokontrolera. Poznaliśmy też sposób użycia przerwania zewnętrznego do realizacji licznika. Dziś posuniemy się o krok dalej. Do mikrokontrolera dołączymy cyfrę LED i nauczymy się nią sterować. Wszystko na przykładzie prostej aplikacji licznika.
Przykład programowania: wyświetlanie stanu licznika
Zbudowany poprzednio licznik wyświetlał wartość zliczonej liczby impulsów w postaci zmieniającego się stanu diod LED dołączonych do portu A. Informację w takiej postaci jest mało czytelna dla przeciętnego obserwatora. Zastąpmy diody LED wyświetlaczem 7-segmentowym dołączonym do portu A. Niech na początek będzie to pojedyncza cyfra o wspólnej anodzie. Nie może to być zbyt duży wyświetlacz, ponieważ nie będą używane żadne układy buforów a prąd segmentów może uszkodzić porty wyjściowe mikrokontrolera. Taki o wysokości około 8..10 mm będzie najbardziej odpowiedni. Model zbudowałem z użyciem TDSY-3150 firmy Vishay. Anodę dołączyłem bezpośrednio do +5V, natomiast katody do wyprowadzeń portu A mikrokontrolera, ale z użyciem rezystorów szeregowych o wartości 470Ω. Sposób w jaki podłączyłem swój wyświetlacz przedstawiono na rysunku 1.
Rys. 1. Sposób dołączenia wyświetlacza LED do mikrokontrolera dla programu z artykułu.
Na listingu 1 przedstawiono przykład realizacji zmodyfikowanego programu licznika z odcinka 1-go kursu. W przykładzie tym mikrokontroler zlicza impulsy od 0 do 9 i wyświetla stan licznika na dołączonej pojedynczej cyfrze wyświetlacza LED.
W programie (identycznie jak w poprzednim odcinku) ustawiany jest adres stosu mikrokontrolera oraz sposób reakcji na zewnętrzne przerwanie INT0. Port D ustawiany jest jako wejściowy, port A w całości jako wyjściowy. Interesująca nas w tym momencie część aplikacji zaczyna się od polecenia załadowania rejestru ZL.
Funkcja konwersji liczby (stanu licznika) na jej reprezentację na wyświetlaczu działa w następujący sposób. W pamięci programu mikrokontrolera zdefiniowano tablicę stałych (wzorce), uszeregowanych w taki sposób, że wzorzec na początku tablicy odpowiada liczbie „0”, następny liczbie „1”, następny liczbie „2” i tak aż do „9”. Tablicę umieszcza się w pamięci przy pomocy dyrektywy kompilatora .org i definiuje jej elementy po dyrektywie .db <lista bajtów rozdzielonych przecinkami>. Polecenia:
ldi ZL,low(wzorce<<1)
ldi ZH,high(wzorce<<1)
powodują załadowanie do rejestru Z (jest to rejestr o długości 2-bajtów) adresu początku tablicy wzorce w pamięci programu mikrokontrolera. W czasie ładowania, ze względu na sposób działania używanej dalej instrukcji lpm, niezbędne jest przesunięcie adresu o jeden bit w lewo. Powoduje to zapis wzorce<<1.
Listing 1. Realizacja licznika dekadowego z wyświetlaniem wartości na wyświetlaczu LED
;Licznik z wyświetleniem wartości na wyświetlaczu
;7-segmentowym LED TDSY3150 (użycie pojedynczej cyfry)
.include "8515def.inc"
.def temp = R16
.org 0
;---------------------------------------------
;wektory obsługi przerwań
;---------------------------------------------
rjmp RESET ;po Reset
rjmp IRQ_Int0 ;External Interrupt 0
reti ;External Interrupt 1
reti ;T/C1 Capture Event
reti ;T/C1 Compare Match A
reti ;T/C1 Compare Match B
reti ;T/C1 Overflow
reti ;T/C0 Overflow
reti ;SPI Transfer Complete
reti ;UART Rx Complete
reti ;UART Data Register Empty
reti ;UART Tx Complete
reti ;Analog Comparator
;---------------------------------------------
;program główny
;---------------------------------------------
RESET:
ldi temp,low(RAMEND) ;ustawienie wskaźnika stosu
out SPL,temp
ldi temp,high(RAMEND)
out SPH,temp
;konfigurowanie reakcji na przerwanie od wejścia INT0
ldi temp,(1<<INT0) ;ustawienie bitu odpowiedzialnego za akceptcję
out GIMSK,temp ;przerwania INT0 w rejestrze GIMSK
;nastawa "wrażliwości" przerwania
ldi temp,$03 ;opadające zbocze na INT0 będzie generować
out MCUCR,temp ;przerwanie
;konfiguracja portu D
clr temp ;w tym przykładzie wszystkie linie portu D
out DDRD,temp ;pracują jako wejściowe
ser temp ;teraz do linii portu D dołączamy rezystory
out PORTD,temp ;zasilające pull-up (wewnętrzne)
;konfigurowanie portu A
out DDRA,temp ;port A jako wyjściowy (temp zawiera $FF)
;wyświetlenie cyfry "0"
ldi ZL,low(wzorce<<1) ;do rejestru Z adresu tablica w ROM
ldi ZH,high(wzorce<<1) ;adres może być 2-bajtowy
lpm ;załadowanie do R0 wzorca cyfry z ROM (0)
out PORTA,R0 ;wyprowadzenie wzorca przez port A
;temp będzie licznikiem
clr temp
;zezwolenie na przyjmowanie przerwań
sei
;główna pętla programu
loop:
rjmp loop ;program główny "kręci się" w pętli i oczekuje
;na przerwania od INT0
;---------------------------------------------
;obsługa przerwania od INT0
;---------------------------------------------
IRQ_Int0:
inc temp ;zwiększenie wartości temp o 1
cpi temp,10 ;porównaj zawartość zmiennej temp z liczbą 10
breq IRQ_Int0_Skip_Reti ;skocz, jeśli wartość jest równa 10
rjmp IRQ_Int0_Disp ;powrót z obsługi przerwania
IRQ_Int0_Skip_Reti:
ldi temp,0 ;zerowanie zmiennej temp
IRQ_Int0_Disp:
;rejestr Z jest indeksem do wzorców cyfr w pamięci ROM
clr R0 ;zerowanie R0-posłuży do dodania przeniesienia
ldi ZL,low(wzorce<<1) ;adres w ROM może być 2-bajtowy, należy
ldi ZH,high(wzorce<<1) ;uwzględnić młodszy i starszy bajt adresu
add ZL,temp ;do indeksu dodajemy wartość temp-to będzie
adc ZH,R0 ;wartość przesunięcia w tablicy
lpm ;załadowanie do R0 bajtu (wzorca cyfry) z ROM
out PORTA,R0 ;wyprowadzenie wzorac przez port A
reti
;definicje wzorców cyfr dla wyświetlacza LED, "0" na pozycji bitu odpowiada
;zaświeceniu segmentu, "1" jego zgaszeniu
wzorce:
.db $82,$E7,$2A,$23,$47,$13,$12,$A7,$02,$03
Rozkaz lpm służy do odczytu struktur zawierających stałe i umieszczonych w pamięci programu. Adres bajtu stałej musi być umieszczony w rejestrze Z. Wybierany jest przez 15 najstarszych bitów adresu, bit najmłodszy decyduje o tym czy odczytywany jest młodszy, czy starszy bajt słowa programu. Instrukcja lpm umieszcza odczytany bajt stałej w rejestrze r0. Wartość licznika (zmiennej temp) wykorzystywana jest jednocześnie jako offset indeksu tablicy. Dodanie do adresu zawartego w Z wartości 0, 1, 2, ...9 powoduje, że rejestr Z będzie zawierać adres wzorca do wyświetlenia dla kolejno wymienionych liczb. Wybieraniem wzorców z tablicy zajmuje się ciąg poleceń umieszczonych w obsłudze przerwania INT0. Ponieważ adres stałej z pamięci ROM jest dwubajtowy, operacja dodania do adresu początku wartości licznika (zmiennej temp) również wykonywana jest dla 2 bajtów. W zestawie rozkazów AT90S8515 brak jest takiego, który dodaje stałą z tzw. przeniesieniem do wartości rejestru. W związku z tym funkcję stałej pełni rejestr R0, wyzerowany przed wykonaniem operacji dodawania tak, aby dodane zostało wyłącznie przeniesienie. To ważne, aby zasadę tą zrozumieć przy nauce programowania w języku asembler. Dlatego też jeszcze kilka słów wyjaśnienia.
Mikrokontroler dodając dwie 1-bajtowe wartości może przeprowadzić operację arytmetyczną również wówczas, gdy długość wyniku przekracza rozmiar rejestru. Jest to możliwe dzięki istnieniu wskaźnika przeniesienia, który przyjmuje wartość logiczną 1 wówczas, gdy wynik przekracza pojemność rejestru przechowującego wynik operacji arytmetycznej. Dla przykładu przytoczę jeden ze skrajnych przypadków: dodawane są do siebie dwie wartości $FF szesnastkowo (255 dziesiętnie): $FF + $FF = $1FE (dziesiętnie: 255 + 255 = 510).
Wynik operacji nie jest jednym bajtem – jest dwubajtowy. Nie sposób więc zapisać go do rejestru o pojemności 1 bajtu. Jak łatwo jednak zauważyć, wartość starszego bajtu będzie co najwyżej równa 1. Potrzebny jest więc faktycznie pojedynczy bit na sygnalizację przepełnienia. W większości mikrokontrolerów nosi on oznaczenie C lub CY (z ang. Carry) i nazywa się flagą przeniesienia (również flagą C).
Aplikacja licznika jest bardzo mała i umieszczona w pamięci od adresu 0. Adres tablicy będącej wykazem wzorców, umieszczonej za kodem programu, ma więc wartość jednobajtową. Praktycznie nie zdarzy się sytuacja, w której będzie istnieć konieczność wykonania działania na 2 bajtach adresu. Ale teoretycznie może zdarzyć się sytuacja, w której adres tablicy po dodaniu offsetu przekroczy rozmiar jednego bajtu lub tablica zostanie przesunięta na koniec pamięci programu. Może więc zaistnieć konieczność wykonania działania na wartości dwubajtowej.
Rozkaz dodawania add zawsze ustawia lub zeruje bit przeniesienia w zależności od długości liczby – wyniku. Bit przeniesienia dodawany jest przez polecenie adc tworzące kompletną operację dodawania liczby dwubajtowej. Najlepszą ilustracją będzie poniższy fragment aplikacji.
clr R0 ;załadowanie wartości 0 do rejestru R0
ldi ZL,low(wzorce<<1) ;załadowanie do Z 2-bajowego adresu tablicy
ldi ZH,high(wzorce<<1);stałej (programu) mikrokontrolera
add ZL,temp ;do młodszego bajtu adresu dodaję stan licznika będący
;jednocześnie wskazaniem na wzorzec do wyświetlenia
adc ZH,R0 ;dodanie do starszego bajtu adresu wartości bitu
;przeniesienia
Myślę, że komentarze umieszczone obok instrukcji są wystarczające do zrozumienia zasady działania. Prześledźmy jeszcze jak zachowa się program, gdy adres tablicy w ROM będzie miał wartość na przykład $0FA a wartość zmiennej temp będzie równa 7.
clr R0 → R0 = $00
add ZL,temp → ZL = $FA+7 = $01 + (C = 1)
adc ZH,R0 → ZH = $00 + (C=1) = $01
Po realizacji wyżej wymienionej sekwencji rozkazów, rejestr Z będzie zawierać wartość $0101, co jest prawidłową wartością adresu dla cyfry 7 i początku tablicy pod adresem $00FA. Warto wspomnieć, że dla potrzeb pewnych mniej zaawansowanych operacji, istnieje rozkaz adiw, dodający wartość bezpośrednią do wartości rejestru dwubajtowego. Polecenie to automatycznie uwzględnia przeniesienie.
Jacek Bogusz
j.bogusz@easy-soft.net.pl
Odpowiedzi
Lol
Jeśli to przykład pokazania jak nie programuje się w asemblerze to jest idealny.
Po co używać przerwań skoro główny program tylko marnuje energie?
Czemu główny program bez sensu marnuje energię a nie tylko czeka na przerwanie?
W przerwaniu zmieniany jest wielokrotnie rejestr SREG, co będzie jeśli w pętli głównej zamiast marnowania czasu będzie jakiś kod?
Nieznajomość asemblera RISC pomijam, to przychodzi z czasem.
Dodaj nowy komentarz