AVR Asembler. Prosty licznik impulsów.

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.


cyfra led

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

http://www.tomaszbogusz.blox.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

Zawartość pola nie będzie udostępniana publicznie.