AVR Asembler. Odczyt stanu przycisku i licznik zdarzeń.

AVR Asembler. Odczyt stanu przycisku i licznik zdarzeń.

Początki nauki nie są zbyt pasjonujące. Ale tak to już jest, że aby zacząć biegać, trzeba się nauczyć chodzić. Niestety, początki są na „czworaka”. W tym odcinku kursu dołożymy kolejny klocek: odczyt stanu przycisku. Następnie połączymy tę funkcję z poznaną wcześniej, służącą do wyświetlenia stanu licznika. Utworzona w ten sposób aplikacja będzie najprostszym licznikiem zdarzeń.

Przykład programowania: pętla opóźnienia czasowego i odczyt przycisku

Podczas zwierania przełącznika, na skutek jego właściwości mechanicznych, może pojawić się zjawisko tak zwanego drgania styków. Ilustruje je rysunek 1. Jak łatwo zauważyć, stan niski na wejściu portu pojawi się co najmniej kilkakrotnie i jeśli mikrokontroler będzie wystarczająco szybki a zadanie klawisza polegać ma np. na zwiększeniu wartości zmiennej o 1 przy jego naciśnięciu, to może się okazać, że pojedyncze naciśnięcie klawisza owocuje przyrostem wartości o kilka czy nawet kilkanaście kroków. Myślę, że było to dobrze widoczne we wcześniej opisywanych przykładach zliczania impulsów pochodzących z INT1.
Współcześnie używane miniaturowe przełączniki mają wyeliminowane prawie całkowicie zjawisko drgania styków, jednak może ono występowanie zależnie od konstrukcji mechanicznej przełącznika. A ponieważ tej nigdy nie możemy być pewni, lepiej się przed nim zabezpieczyć.

Rysunek 1. Drgania styków klawisza mogą być źródłem błędów odczytu.

Lekarstwem na jest prosta metoda: reakcja na wciśnięty klawisz (czy to opadające zbocze, czy poziom niski  sygnału), odczekanie przez czas 20..50ms i ponowny odczyt stanu portu. Jeśli nie zmienił się, to oznacza, że klawisz nadal jest wciśnięty i użytkownik żąda reakcji. Jeśli natomiast stan jest różny od niskiego, to oznaczać może, że albo odebrano zakłócenie, albo przypadkowo naciśnięto klawisz. Tę prostą zasadę działania wykorzystuje przykładowy program. Oczywiście metodzie tej można zarzucić, że jest to czyste marnowanie czasu mikrokontrolera. Jest jednak prosta i w większości zastosowań wystarczająca. Inaczej trzeba by wykorzystać przerwania któregoś z timerów i synchroniczny podział czasu pomiędzy różne realizowane zadania.
Do wyprowadzenia portu mikrokontrolera PORTD.2 dołączono przycisk, który po naciśnięciu podaje na to wyprowadzenie stan niski. Wyświetlacz dołączony do wyprowadzeń portu A pokazuje cyfrę „0”, gdy przycisk jest zwolniony i „1”, gdy naciśnięty. Praktyczną realizację aplikacji pokazano na listingu 1.
Początek programu to deklaracja wektorów przerwań oraz ustawienie adresu stosu. Tym razem nasz program zawiera oprócz używanej we wcześniejszych przykładach zmiennej temp, również deklaracje zmiennych pomocniczych dla podprogramu realizującego opóźnienie czasowe. Podobnie jak w poprzednich przykładach, port D pracuje jako wejściowy a port A jako wyjściowy. Rejestrowi Z nadawana jest wartość adresu tablicy zawierającej wzorce cyfr „0” i „1” w pamięci ROM.
Pętla główna rozpoczyna się od etykiety loop. Podzielona została praktycznie na dwie części: pierwsza z nich wyświetla cyfrę „0” lub „1” w zależności od tego czy klawisz jest wciśnięty („1”), czy nie („0”). Druga odczytuje stan bitu 2 portu D i w zależności od niego zmienia zwartość rejestru Z.
Przyjrzyjmy się realizowanym przez mikrokontroler rozkazom. Podobnie jak w poprzednim przykładzie, adres tablicy z wzorcami cyfr „0” i „1” ładowany jest przez rozkazy ldi do młodszej (ZL) i starszej (ZH) części rejestru Z. Następnie polecenie sbic testuje wartość doprowadzenia PIND.2. Działa ono w taki sposób, że jeśli bit 2 doprowadzeń PIND ma wartość logiczną „0”, to instrukcja leżąca bezpośrednio po sbic jest omijana. Poleceniem „odwrotnym”, reagującym na stan wysoki bitu w obszarze przestrzeni I/O jest sbis.
Jeśli bit doprowadzenia PIND.2 jest zerem, to może oznaczać, że klawisz został wciśnięty. W takim przypadku program wywołuje pętlę opóźnienia czasowego trwającą 20 milisekund a następnie bit testowany jest powtórnie. Program „upewnia się” w ten sposób, że stan logiczny niski nadal trwa i nie było to przypadkowe zadziałanie klawisza lub jakieś zakłócenie. Oczywiście taka prosta metoda nie jest w stanie wyeliminować wszystkich przypadkowych załączeń w trudnych warunkach pracy mikrokontrolera.
Jeśli stan niski nadal trwa, to do adresu tablicy zawartego w rejestrze Z dodawana jest 1 powodując tym samym, że instrukcja lpm załaduje do rejestru R0 wzorzec cyfry „1”. Po wykonaniu tych instrukcji, jak również w przypadku gdy na wyprowadzeniu PIND.2 jest stan wysoki, program wykonuje skok na początek pętli do etykiety loop i cały cykl jest powtarzany. Pętla opóźnienia czasowego jest wywoływana tylko wówczas, gdy na PIND.2 jest stan logiczny niski.

List. 1. Odczyt przycisku dołączonego do PORTD.2
;Odczyt stanu klawisza dołączonego do PORTD.2
;Uwaga: czasy podano dla rezonatora kwarcowego 7,3728MHz
.include "8515def.inc"
.def    temp = R16          ;zmienna ogólnego przeznaczena
.def    delay = R17         ;tu parametr dla pętli opóźnienia

.def    delay1 = R18        ;zmienne pomocnicza
.def    delay2 = R19

.org      0
;---------------------------------------------
;wektory obsługi przerwań
;---------------------------------------------
    rjmp    RESET            ;po Reset
    reti                     ;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 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)
    out    PORTA,temp        ;wyprowadzenie stanu wysokiego
;wartość inicjująca port PORTA (wyświetlenie 0)
    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
;główna pętla programu
loop:
    lpm
    out    PORTA,R0
    ldi    ZL,low(wzorce<<1) ;adres w ROM może być 2-bajtowy
    ldi    ZH,high(wzorce<<1)
    sbic   PIND,2            ;sprawdzenie bitu 2 i skok,jeśli ustawiony
    rjmp   loop              ;instrukcja jest omijana,jeśli bit 4 jest 0
    ldi    delay,20          ;parametr dla opóźnienia 20ms
    rcall  delayms           ;wywołanie pętli opóźnienia
    sbic   PIND,2            ;ponowne sprawdzenie bitu 2 portu D
    rjmp   loop              ;instrukcja jest omijana,jeśli bit 4 jest 0
    adiw   ZL,1              ;zwiększenie rejestru Z o 1
    rjmp   loop
;---------------------------------------------
;realizacja opóźnienia delay x 1ms
;---------------------------------------------
;procedura angażuje cpu na czas około delay x 1ms
;czas w milisekundach w "delay"
delayms:
    ldi    delay1,$0A
dloop2:
    ldi    delay2,$F6
dloop1:
    dec    delay2            ;pierwsza pętla 1/7.3728MHz x 127
    brne   dloop1
    dec    delay1            ;druga pętla 1/7.3728MHz x 127 x 29
    brne   dloop2
    dec    delay             ;testowanie parametru wywołania
    brne   delayms
    ret

;defincje wzorców cyfr dla wyświetlacza LED, "0" na pozycji bitu odpowiada
;zaświeceniu segmentu, "1" jego zgaszeniu-tu definicja tylko "0" i "1"
wzorce:   .db    $82,$E7

Od etykiety delayms rozpoczyna się podprogram. Jest to konstrukcja, która jak dotąd nie była używana. To bardzo wygodny mechanizm umożliwiający podzielenie całej aplikacji na mniejsze fragmenty, które znacznie łatwiej jest uruchomić. Krótko mówiąc są to pewne fragmenty napisane w celu realizacji określonych czynności, zaczynające się od etykiety będącej najczęściej nazwą podprogramu (aczkolwiek nie musi tak być) i zakończone instrukcją ret. Do wywołania podprogramu używane są najczęściej rozmaite odmiany instrukcji call. Przy jej realizacji na stosie mikrokontrolera odłożony zostaje 2 lub 3-bajtowy adres powrotu wykorzystywany i „zdejmowany” ze stosu po realizacji polecenia ret, którego realizacja kończy się powrotem do następnej instrukcji następującej po rozkazie call.
Podprogram absorbujący mikrokontroler na czas wartość zmiennej delay x 1 milisekunda to zwykłe pętle wykonujące arytmetyczne operacje odejmowania do momentu aż zawartość rejestru osiągnie wartość 0. Wykorzystywany jest fakt, że wykonanie każdej instrukcji zajmuje czas około 1 cyklu zegarowego. Prześledźmy przykład praktycznej realizacji.

delayms:             ;etykieta delayms to początek podprogramu
    ldi    delay1,$0A ;druga pętla wykonywana jest 10 razy
dloop2:
    ldi    delay2,$F6 ;pierwsza pętla wykonywana jest 246 razy
dloop1:
    dec    delay2    ;pierwsza pętla: czas wykonania to 1/7.3728MHz x 246
    brne   dloop1    ;wykonywana do momentu aż spełniony będzie warunek
                     ;delay2 = 0
    dec    delay1    ;druga pętla 1/7.3728MHz x 246 x 10
    brne   dloop2    ;wykonywana do momentu aż spełniony będzie warunek
                     ;delay1 = 0
    dec    delay     ;trzecia pętla określana przez parametr wywołania delay
    brne   delayms   ;wykonywana do momentu aż spełniony będzie warunek
                     ;delay = 0
    ret              ;powrót do miejsca wywołania podprogramu

Pętla opóźnienia czasowego w postaci jak wyżej, nie nadaj się do precyzyjnego odmierzania czasu. Dzieje się tak z co najmniej dwóch powodów. Po pierwsze bardzo trudno jest dobrać czas wykonywania rozkazów w taki sposób, aby dokładnie odmierzyć czas. Po drugie realizacja podprogramu może być przerywana przez sygnały przerwań. O ile więc może być używana w tego rodzaju zastosowaniach, o tyle nie nadaje się do precyzyjnego odmierzania czasu i na przykład budowy zegara.
Na koniec wspomnę o jeszcze jednym bicie wskaźnika. Używa go na przykład instrukcja brne i inne – mowa tu o tzw. bicie wskaźnika zera otrzymanego w wyniku operacji (z ang. Zero Flag). Bit ten przyjmuje wartość logiczną „1”, jeśli w wyniku operacji arytmetycznej lub logicznej rejestr wyniku będzie zawierał 0. Cechę tę doskonale wykorzystuje się do wszelkiego rodzaju porównań w dowolnym języku asemblera. Wykorzystując rozkaz odejmowania oraz badając stan wskaźników Z i C po operacji można w prosty sposób stwierdzić czy wartość odejmowana jest większa, mniejsza czy też równa. Jest to zagadnienie, z którym często spotyka się programista wykonujący aplikacje dla mikrokontrolerów.

 

Jacek Bogusz

j.bogusz@easy-soft.net.pl

http://www.tomaszbogusz.blox.pl/

Dodaj nowy komentarz

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