pusty pusty pusty
pusty
pusty pusty forum szukaj książki linki artykuły
home ASM IDE c c c c c c c c c
teoria dla początkujących schematy elektronika retro mikrokontrolery pusty
na dół

Mikrokontrolery


Kurs asemblera dla AVR w przykładach

Pierwszy program - zapalanie i gaszenie diody LED

Pierwszy program jaki wszyscy najczęściej piszą, to zapalanie diody LED podłączonej do któregoś z portów mikrokontrolera. Ja nie byłem więc oryginalny i również taki program tutaj zamieściłem, a poza tym to dobre ćwiczenie przy poznawaniu konfiguracji portów.
   Dioda LED podłączona będzie do wyprowadzenia PB0 portu B. Do wyprowadzeń PD0 i PD1 portu D podłączone będą przełączniki, których zadaniem będzie odpowiednio włączanie i wyłączanie świecenia diody LED. Naciśnięcie przełącznika podłączonego do PD0 powinno spowodować zaświecenie diody, natomiast naciśnięcie przełącznika podłączonego do PD1 powinno spowodować zgaszenie diody.
   Zapewne każdy kto używał różnego rodzaju przełączników wie, że podczas naciśnięcia, a następnie zwolnienia przycisku przełącznika występuje zjawisko drgania styków powodujące na przemian zwarcie i rozwarcie styków. W tym konkretnym przypadku nie będziemy zajmować się programową eliminacją wpływu drgania styków przełączników gdyż nie ma to znaczenia dla poprawnego działania układu.
   Opisywane działanie programu ma być zrealizowane w układzie przedstawionym na rys. 1. Jest to fragment schematu ideowego zestawu ZL1AVR.
   W tabelce poniżej znajduje się plik do ściągnięcia z omawianym programem.

Pliki do pobrania
Wersja programu Nazwa pliku do pobrania
[4] Pierwszy program - wariant I asm001.asm asm001.hex
[4] Pierwszy program - wariant II asm001b.asm asm001b.hex
rys. 1
   Aby schemat widoczny na rys. 1 był zgodny z połączeniami w ZL1AVR, to w zestawie należy zewrzeć następujące złącza:
   - JP1 - podłączenie +5V do zasilania LED,
   - JP6 i JP7 - podłączają SW1 do PD0 i SW4 do PD1
   - JP4 zewrzeć 2 z 3 - podłączy to masę do SW1 i SW4
   - J4 - 2 z 3, J3 - 1 z 2 - zostanie dołączony kwarc X1
   - ZW_PORTB - 15 z 16 - podłączenie LED1 do PB0
   - do JP13 podłączyć zasilanie.
   Dioda LED1 zaświeci się gdy PB0 (pin12 AT90S2313) będzie skonfigurowany jako wyjście i jego stan przyjmie wartość "0", to umożliwi przepływ prądu przez diodę LED1, prąd ten ograniczony jest rezystorem R9 do wartości ok. 3,5mA (zależy to również od wartości spadku napięcia na diodzie LED). Aby świecenie diody uzależnić od stanu przełączników SW1 i SW4 to PD0 (pin 2) i PD1 (pin 3) muszą być skonfigurowane jako wejścia z wejściem typu pull-up wymuszającym początkowy stan 1. W takim przypadku naciśnięcie jednego z przełączników spowoduje, że na odpowiednim wejściu pojawi się stan "0". Pozostałe nie wykorzystywane wyprowadzenia zarówno portu B jak i D mogą być skonfigurowane dowolnie, można je więc ustawić np. jako wyjścia.

W procesorach AVR na początku zawsze należy skonfigurować porty określając dla każdego wyprowadzenia dwa parametry:
- funkcję jaką ma pełnić - wejścia czy wyjścia
- stan spoczynkowy jaki ma przyjąć - "0" czy "1"
Jeżeli porty nie zostaną skonfigurowane, to przy starcie mikrokontrolera (po wyzerowaniu) rejestry PORTx i DDRx (x w zależności od portu będzie zastąpiony literką A, B, C lub D) zostaną wyzerowane co oznacza, że wszystkie wyprowadzenia portów będą wejściami w stanie wysokiej impedancji (wejścia pływające)

   Teraz przechodzimy do sedna sprawy, czyli jak to co opisałem wyżej zamienić na program zrozumiały dla naszego AVR-a.
   W przykładach poniżej stosuję kolorystykę składni poleceń taką jaką mam ustawioną w AVR Studio 4. Kolorem zielonym wyróżniam komentarze. Warto przyzwyczaić się do opatrywania swoich programów komentarzami gdyż to po pewnym czasie ułatwia analizę wcześniej napisanych programów. Również warto stosować nazwy i etykiety w języku angielskim gdyż ułatwi to również innym korzystać z twoich programów, gdy już będziesz je publikował   . Co do nazw i etykiet to można je pisać małymi jak i dużymi literami, spotkasz się tutaj z nazwami np. acc, ResetProcessor itp. i możesz spokojnie je zmienić na inne gdyż to od Ciebie zależy, ma Ci to pomóc w poruszaniu się po kodzie programu. Spotkasz się również z nazwami takimi jak UTXCaddr i tu musisz uważać gdyż jest to nazwa zdefiniowana w pliku 2313def.inc i nie możesz jej zmienić chyba że zmienisz również definicję w tym pliku.
   Zgodnie z tym co jest napisane powyżej (w ramce) należy najpierw skonfigurować porty AT90S2313. Za stan portów w mikrokontrolerach AVR odpowiedzialne są trzy rejestry DDRx, PORTx i PINx (x to: A, B, C lub D). Jeżeli do każdego bitu rejestru DDRx wpiszemy "1" to wszystkie wyprowadzenia portu będą wyjściami, natomiast jeżeli do każdego bitu rejestru DDRx wpiszemy "0" to wszystkie wyprowadzenia będą wejściami. Jeżeli każdy bit rejestru PORTx ustawiony będzie miał stan "1", to wyjścia lub wejścia (w zależności od stanu DDRx) będą podciągnięte do stanu logicznej jedynki (pull-up). Jeżeli wszystkie bity PORTx będą miały ustawione "0" to w przypadku wyjść będzie to oznaczało stan logicznego zera, a w przypadku wejść stan wysokiej impedancji czyli wejście "pływające".
   Ale jak to zrobić w asemblerze? Tu konieczna jest znajomość instrukcji (rozkazów) dla procesorów AVR oraz znajomość działania mikrokontrolera z punktu widzenia piszącego program, a w szczególności jak odbywa się komunikacja mikrokontrolera ze "światem zewnętrznym". Szczegółowo jak działa mikrokontroler AT90S2313 napisałem w Budowa i działanie AT90S2313, można również korzystać z literatury i zasobów sieciowych producenta czyli firmy ATMEL.
   Mikrokontroler realizując swój program zawarty w pamięci programu (pamięci FLASH) operuje na zasobach zawartych wewnątrz (czasami na zewnątrz – zewnętrzna pamięć RAM, zewnętrzne porty i kontrolery) układu scalonego. Do tych zasobów zaliczamy pamięć statyczną SRAM, pamięć nieulotną EEPROM, zbiór rejestrów roboczych od R0 do R31 oraz zbiór rejestrów w przestrzeni I/O. W najprostszym wariancie "łączność ze światem zewnętrznym" realizowana jest poprzez dostępne porty. Zbiór możliwych do użycia portów jest zależny od modelu mikrokontrolera. W AT90S2313 są dwa porty PB i PD, w mikrokontrolerze AT90S8515 lub jego ulepszonej wersji ATMEGA8515 możliwe jest użycie czterech portów ośmiobitowych (porty PA, PB, PC i PD), co daje 32 linie o funkcji wejścia lub wyjścia. Każdy bit portu może być skonfigurowany jako wejściowy lub wyjściowy dla mikrokontrolera. Za pomocą linii wejściowych portów możliwe jest pobieranie przez mikrokontroler informacji z zewnątrz oraz poprzez linie wyjściowe mikrokontroler może wpływać na urządzenia do niego przyłączone. Bardzo ważnym elementem w tym procesie są rejestry robocze R0 do R31 (w literaturze nazywane są również rejestrami ogólnego przeznaczenia), które mają bezpośredni dostęp do jednostki arytmetyczno logicznej ALU. Instrukcje wykonywane przez jednostkę ALU wykonywane są właśnie na tych na rejestrach roboczych i dlatego bardzo często będziesz się nimi posługiwał.
   Dobrze opisana lista rozkazów dla AT90S2313 znajduje się w książce "Mikrokontrolery AVR w praktyce" [1], oczywiście na mojej stronie również ją znajdziesz. Ale wszystko po kolei, nie trzeba przecież od razu uczyć się wszystkich instrukcji na pamięć. W trakcie pisania kolejnych programów będziesz poznawał kolejne instrukcje.

Na początek konfiguracja całego portu B jako wyjście:

  ldi r16,0xFF ;
  out DDRB,r16 ; cały port B jako wyjściowy

i gotowe, tylko jak to działa?
Do skonfigurowania portu B zostały użyte dwie instrukcje: ldi oraz out. Opis instrukcji znajdziesz "klikając w nią" - odnośniki są tutaj wyróżniane (jak zresztą w całym serwisie) takim kolorem. Ponieważ nie można wpisać bezpośrednio do rejestru z obszaru we/wy jakim jest rejestr DDRB liczby 0xFF (zapis szesnastkowy), która odpowiada samym jedynkom na ośmiu bitach, trzeba się posłużyć jakimś "pośrednikiem" czyli rejestrem roboczym r16. Dlaczego akurat r16? Instrukcja ldi ładująca bezpośrednio do rejestru stałą może pracować tylko na górnej połówce rejestrów roboczych czyli na rejestrach r16 do r31, użyty został więc pierwszy dozwolony (możesz użyć inny np. r18). Następnym krokiem jest przeniesienie danej umieszczonej w rejestrze roboczym do rejestru we/wy czyli w naszym przypadku do DDRB. Do tego celu trzeba się posłużyć instrukcją out używając jako argumentów źródło z jakiego pobierana jest dana czyli r16 i miejsce docelowe czyli rejestr DDRB, oba argumenty rozdzielone są przecinkiem. Prawda, że to jest proste?

Teraz kolej aby wszystkie wyjścia portu B były w stanie jedynki logicznej. Trzeba więc zapisać do rejestru PORTB same jedynki. Jak to zrobić to już wiesz:

  ldi r16,0xFF ;
  out PORTB,r16 ; wszystkie wyjścia portu B
; w stanie wysokim

Warto zauważyć, że stan rejestru r16 nie zmieniał się, można więc połączyć razem konfigurację portu i ustawianie jego wyjść w stan wysoki, i skrócić zapis o jedną linię:

  ldi r16,0xFF ;
  out DDRB,r16 ; cały port B jako wyjściowy
  out PORTB,r16 ; wszystkie wyjścia portu B
; w stanie wysokim

Popatrz teraz na schemat, skoro na wyjściu PB0 jest stan wysoki, to dioda LED1 nie może świecić i tak miało być.

Teraz trzeba się zająć portem D, do którego wyprowadzeń PD0 i PD1 podłączone są odpowiednio SW1 i SW4. Te dwa wyprowadzenia muszą pełnić więc rolę wejść, pozostałe z portu D podobnie jak w porcie B będą wyjściami, a cały port D będzie miał wyprowadzenia podciągnięte do "1". Jak to zrobić? Teraz to już jest bardzo proste:

  ldi r16,0xFC ;
  out DDRD,r16 ; PD0 i PD1 jako wejścia,
; reszta jako wyjścia
  ldi r16,0xFF ;
  out PORTD,r16 ; wszystkie wyprowadzenia
; portu D w stanie wysokim

Jak widać nie ma nic prostszego. Zapewne zauważyłeś, że instrukcja ldi została użyta dwa razy - jest to spowodowane tym, że najpierw rejestr roboczy miał wpisaną wartość "11111100" (0xFC) aby było możliwe ustawienie pinów PD0 i PD1 jako wejścia (zamiennie można używać nazw: wyprowadzenia, końcówki), a następnie konieczna była zmiana zawartości rejestru r16 na "11111111" (0xFF) tak aby wszystkie piny portu D miały stan "1". Zbierzmy teraz to razem, tak aby otrzymać pełną konfigurację portów dla naszego zadania:

  ldi r16,0xFF ;
  out DDRB,r16 ; cały port B jako wyjściowy
  out PORTB,r16 ; wszystkie wyjścia portu B
; w stanie wysokim
  ldi r16,0xFC ;
  out DDRD,r16 ; PD0 i PD1 jako wejścia,
; reszta jako wyjścia
  ldi r16,0xFF ;
  out PORTD,r16 ; wszystkie wyprowadzenia
; portu D w stanie wysokim

Oczywiście to dopiero początek, a do tego jeszcze nie całkiem kompletny. Czego brakuje aby uznać, że program został właściwie rozpoczęty? Otóż na początku programu musimy umieścić trochę informacji istotnych dla kompilatora:
   • dla jakiego mikrokontrolera pisany jest program
   • czy jest potrzebny raport z kompilacji
   • od którego miejsca zaczyna się kod programu
   • definicja obszaru tabeli wektorów przerwań
   • zainicjowanie stosu programowego
to wszystko można osiągnąć korzystając z dyrektyw asemblera. A oto jak będzie w naszym przypadku wyglądał początek zapisu programu:

  .nolist    
  .include  "2313def.inc"  
  .list    
  .cseg    
  .org 0  
  rjmp ResetProcessor  

Powyższe zapisy oznaczają, polecenia dla kompilatora aby do programu dołączył plik o nazwie wymienionej w dyrektywie include, który zawiera podstawowe definicje zasobów danego modelu mikrokontrolera. Dyrektywy nolist, list oraz listmac sterują generowaniem raportu z procesu kompilacji (tu do raportu nie będzie przeniesiony dołączany przez dyrektywę include zbiór, a jedynie nasz program). Dyrektywa cseg oznacza, że dalsze zapisy dotyczą kodu programu, to znaczy, że w wyniku kompilacji będą umieszczone w pamięci FLASH. Teraz kolej już na segment kodu programu ale najpierw należy jeszcze wskazać od jakiego adresu będzie umieszczany w pamięci programu generowany przez kompilator kod wynikowy. Dyrektywa org z argumentem "0" występująca za dyrektywą cseg powoduje, że generowany przez kompilator kod wynikowy będzie umieszczany w pamięci programu zaczynając od adresu o wartości zero. No i teraz pierwsza instrukcja w programie czyli rjmp. Jest to instrukcja skoku, a jej argumentem jest adres skoku, a ściślej rzecz biorąc przemieszczenie względem adresu bieżącego, najczęściej stosuje się symboliczny zapis adresu w postaci etykiety. Tak więc zapis "rjmp  ResetProcessor" oznacza, że w pamięci programu na adresie zero będzie umieszczony kod instrukcji skoku do etykiety o nazwie "ResetProcessor". Ta etykieta musi znaleźć się w obrębie programu. Procesor rozpoczynając pracę po sygnale zerowania wykona powyższą instrukcję jako pierwszą.
   Ponieważ w programie nie jest przewidywane używanie przerwań, można byłoby nie wypełniać początkowego obszaru pamięci programu związanego z tabelą wektorów przerwań ale w ramach wyrabiania właściwych nawyków zrobimy to:

  .org INT0addr ; Przerwanie INT0
  reti    
  .org INT1addr ; Przerwanie INT1
  reti    
  .org ICP1addr ; Przerwanie
  reti    
  .org OC1addr ; Przerwanie
  reti    
  .org OVF1addr ; Przerwanie
  reti    
  .org OVF0addr ; Przerwanie
  reti    
  .org URXCaddr ; Przerwanie
  reti    
  .org UDREaddr ; Przerwanie
  reti    
  .org UTXCaddr ; Przerwanie
  reti    
  .org ACIaddr ; Przerwanie
  reti    

W powyższych zapisach wszystkie stałe występujące w dyrektywie org pochodzą z odpowiedniego zbioru (tu konkretnie z 2313def.inc). Jeżeli zaistniałoby przerwanie przykładowo od UART odbiornika, to µC (będę używał tego skrótu zamiast nazwy "mikrokontroler") rozpocznie obsługę od adresu, który jest określony przez wartość stałej "URXCaddr", czyli wykona instrukcję spod tego adresu w pamięci flash - tu będzie to instrukcja reti ale w przypadku gdyby w programie było przewidywane faktycznie obsługiwanie przerwań byłaby to np. instrukcja skoku "rjmp  UARTRXCInterrupt", a pod etykietą "UARTRXCInterrupt" byłaby napisana procedura obsługi tego przerwania. Powyższe zapisy definiują więc obszar tabeli wektorów przerwań. Ponieważ nasz program nie używa przerwań cała tabela zawiera "plomby" w postaci instrukcji reti, która to instrukcja oznacza powrót z obsługi przerwania.
   Wróćmy teraz do pierwszej instrukcji jaką wykona mikrokontroler rozpoczynając pracę po sygnale zerowania. Wykonany jest więc skok do etykiety "ResetProcessor" - od tej etykiety rozpoczynają się zapisy inicjujące stos programowy, co wygląda jak poniżej:

ResetProcessor :   ;
  cli   ;
  ldi r16,LOW(RAMEND) ;
  out SPL,r16 ;

Inicjacja stosu programowego odbywa się poprzez wskazanie w rejestrze SPL na fizyczny koniec pamięci. Poprzedzone to jest zablokowaniem przerwań rozkazem cli. Rozkaz ten zeruje wskaźnik I w rejestrze SREG, przez co blokuje przerwania. Następnie opisywanymi już wcześniej instrukcjami ldi oraz out zostaje wskazany fizyczny koniec pamięci w rejestrze SPL czyli początek stosu (jest to wskaźnik stosu). Ze względu, że nasz program nie używa żadnych zmiennych, to stos zajmie całą dostępną pamięć.
Komentarza wymaga jeszcze zapis "ldi  r16,LOW(RAMEND)". Zapis ten oznacza, że do rejestru r16 należy wpisać liczbę, która jest młodszą częścią stałej RAMEND (dlatego użyta jest funkcja LOW). W przypadku AT90S2313 można napisać "ldi  r16,RAMEND", ponieważ stała RAMEND ma wartość $DF (w zapisie szesnastkowym) lub 233 (w zapisie dziesiętnym), czyli mieści się w jednym bajcie. A dlaczego nazwa stałej brzmi RAMEND? I tu należy zerknąć do zbioru 2323def.inc gdzie ta stała jest zdefiniowana w następujący sposób: ".equ  RAMEND=$DF". Żeby całkiem problem wyjaśnić załóżmy, że mamy przykładowo AT90S8515 (zamiast AT90S2313), gdzie stała RAMEND ma wartość $25F (lub dziesiętnie 607), to zapis: "ldi  r16,607" jest niedopuszczalny, ponieważ liczba 607 nie mieści się w jednym bajcie. Dodatkowo wskaźnik stosu jest zawarty w dwóch 8-bitowych rejestrach (SPL i SPH), które są traktowane jako jeden rejestr 16-bitowy. Zainicjowanie wskaźnika stosu w µC z większym stosem (mających SPL i SPH) polega na wpisaniu do "rejestru 16-bitowego SPL:SPH" liczby 16-bitowej. Można to zrobić jedynie w ten sposób, że do SPL wpisze się młodszą część 16-bitowej stałej i do SPH wpisze się starszą część liczby 16-bitowej, czyli:

  ldi r16,LOW(RAMEND) ;
  out SPL,r16 ;
  ldi r16,HIGH(RAMEND) ;
  out SPH,r16 ;

dla AT90S2313 redukuje się to do:

  ldi r16,LOW(RAMEND) ;
  out SPL,r16 ;

a ponieważ RAMEND < 256, to można to zredukować do:

  ldi r16,RAMEND ;
  out SPL,r16 ;

   Ze względu na to, że program nie używa przerwań nie używa więc stosu, to inicjację stosu można byłoby pominąć, ale ze względu na wyrabianie właściwych nawyków proponuję ją pozostawić.
   Po resecie oprócz inicjacji stosu należy ustawić stan początkowy całego układu czyli należy skonfigurować we właściwy sposób porty. Wcześniej zostało to już opisane dlatego poniżej zbierzemy cały początkowy zapis naszego programu:

Pierwszy program - wariant I
;----------------------------------- Pierwszy program ---------------------------------

  .nolist   ;
  .include  "2313def.inc" ;
  .list   ;
  .listmac   ;

;-------------------- Początek segmentu kodu (Code Segment) -------------------

  .cseg   ;
  .org 0 ;
  rjmp ResetProcessor ;

;----------------- Tabela wektorów przerwań (Interrupt Vectors) ----------------

  .org INT0addr ; External Interrupt0 Vector
  reti   ; Address
  .org INT1addr ; External Interrupt1 Vector
  reti   ; Address
  .org ICP1addr ; Input Capture1 Interrupt
  reti   ; Vector Address
  .org OC1addr ; Output Compare1A
  reti   ; Interrupt Vector Address
  .org OVF1addr ; Overflow1 Interrupt Vector
  reti   ; Address
  .org OVF0addr ; Overflow0 Interrupt Vector
  reti   ; Address
  .org URXCaddr ; UART Receive Complete
  reti   ; Interrupt Vector Address
  .org UDREaddr ; UART Data Register Empty
  reti   ; Interrupt Vector Address
  .org UTXCaddr ; UART Transmit Complete
  reti   ; Interrupt Vector Address
  .org ACIaddr ; Analog Comparator
  reti   ; Interrupt Vector Address
;---------------------------------------------------------------------------------------------
ResetProcessor :   ;
  cli   ; zablokowanie przerwań
  ldi r16,LOW(RAMEND) ; inicjacja stosu
  out SPL,r16 ; programowego
;
  ldi r16,0xFF ; cały port B jako wyjściowy
  out DDRB,r16 ; wszystkie wyjścia portu B
  out PORTB,r16 ; w stanie wysokim
;
  ldi r16,0xFC ; PD0 i PD1 jako wejścia,
  out DDRD,r16 ; reszta jako wyjścia
;
  ldi r16,0xFF ; wszystkie wyprowadzenia
  out PORTD,r16 ; portu D w stanie wysokim
;---------------------------------------------------------------------------------------------

Początek został zrobiony, wszystko zostało pięknie zainicjowane, porty są właściwie skonfigurowane. Teraz należy przejść do zasadniczej części programu czyli odczytywać stan przełączników SW1, SW4 i w zależności od ich stanów zapalać lub gasić diodę LED1. To co pozostało do zrobienia można podzielić na trzy części:
 - sprawdzenie, który przycisk jest naciśnięty - pętla główna "Main_0"
 - zapalenie diody LED1 - pętla "Main_1"
 - zgaszenie diody LED1 - pętla "Main_2"
Zacznijmy więc od pętli głównej czyli sprawdzenia stanu przełączników SW1 i SW4:

Main_0 :   ; początek pętli głównej
  in r16,PIND ; wczytanie PIND do r16
  andi r16,0x03 ; iloczyn logiczny r16 i 3

Jak widać pojawiły się dwie nowe instrukcje: in, która powoduje odczytanie zawartości rejestru PIND i przepisanie jego zawartości do rejestru roboczego r16 oraz instrukcja andi, która wykonuje iloczyn logiczny rejestru r16 i stałej 0x03, a wynik umieszcza w rejestrze roboczym r16. Co uzyskamy przy takiej operacji? Wczytanie do rejestru roboczego r16 stanu panującego na porcie D (wszystkich pinów) oraz wyzerowanie w tym rejestrze wszystkich bitów, które nie są istotne (ważne są bity 0 i 1 tego portu). Stała o wartości 3 ma jedynki na bicie 0 i 1 oraz zera na pozostałych bitach. W wyniku iloczynu logicznego (logiczne AND) zostaną wyzerowane wszystkie bity z wyjątkiem dwóch najmłodszych (bit 0 i 1), których stan się nie zmieni. Teraz wystarczy tylko rozpoznać ich stan i odpowiednio do niego zareagować. Najpierw zbadamy czy SW1 jest naciśnięty:

  cpi r16,0x02 ; czy jest przyciśnięty SW1
  breq Main_1 ; tak - to skocz do Main_1

I znów dwie nowe instrukcje: cpi, która porównuje zawartości rejestru roboczego r16 ze stałą 2 i ustawia znaczniki Z, N, V, C, H rejestru stanu SREG, oraz breq, która jeśli znacznik Z jest równy 1 wykonuje skok do etykiety Main_1. Co w efekcie uzyskujemy? Porównanie zawartości rejestru r16 ze stałą o wartości 2 (w rozwinięciu binarnym ma postać 00000010). Jeżeli zawartość r16 jest równa 2, to oznacza, że na bicie 0 jest stan logicznego zera (przycisk SW1 jest zwarty do masy co oznacza, że jest naciśnięty) oraz na bicie 1 jest stan logicznej jedynki (przycisk SW4 nie jest naciśnięty). Jeżeli tak jest to należy wykonać skok do etykiety Main_1, jeżeli nie to należy przejść do wykonania następnej instrukcji czyli rozpoznanie czy SW4 jest naciśnięty:

  cpi r16,0x01 ; czy jest przyciśnięty SW4
  breq Main_2 ; tak - to skocz do Main_2

Porównanie zawartości rejestru r16 ze stałą o wartości 1 (w rozwinięciu binarnym ma postać 00000001). Jeżeli zawartość r16 jest równa 1, to oznacza, że na bicie 1 jest stan logicznego zera (przycisk SW4 jest zwarty do masy co oznacza, że jest naciśnięty) oraz na bicie 0 jest stan logicznej jedynki (przycisk SW1 nie jest naciśnięty). Jeżeli tak jest to należy wykonać skok do etykiety Main_2, jeżeli nie to należy przejść do wykonania następnej instrukcji czyli powrotu do początku pętli głównej:

  rjmp Main_0 ; powrót do pętli głównej

Jest to przypadek pasywny gdy nie jest przyciśnięty żaden przycisk. Naciśnięcie obu przycisków jednocześnie (praktycznie jest to niemożliwe) zostanie rozpoznane jako naciśnięcie przycisku SW1 przyłączonego do PD0.
   Pętla główna został już napisana, kolej teraz na napisanie fragmentów programu odpowiedzialnych za zaświecenie i zgaszenie diody LED1:

Main_1 :   ;
  cbi PORTB,0 ; PB0 = 0 to dioda świeci
  rjmp Main_0 ; powrót do pętli głównej

W tym miejscu rozpoznane jest już, że został naciśnięty przycisk SW1, który ma spowodować zaświecenie diody LED1. Dzięki instrukcji cbi, która zeruje bit 0 w rejestrze PORTB stan pinu PB0 przyjmuje wartość zera logicznego co daje w efekcie zaświecenie diody. Po tej czynności program wraca do pętli głównej czyli do badania stanu przycisków.

Main_2 :   ;
  sbi PORTB,0 ; PB0 = 1 to dioda nie świeci
  rjmp Main_0 ; powrót do pętli głównej

W tym miejscu rozpoznane jest już, że został naciśnięty przycisk SW4, który ma spowodować zgaszenie diody LED1. Dzięki instrukcji sbi, która ustawia bit 0 w rejestrze PORTB stan pinu PB0 przyjmuje wartość jedynki logicznej co daje w efekcie zgaszenie diody. Po tej czynności program wraca do pętli głównej czyli do badania stanu przycisków.
   I tak oto został napisany pierwszy program w asemblerze, prawda że proste? Zbierzmy więc poszczególne fragmenty razem:

;----------------------------------- Pierwszy program ---------------------------------

  .nolist   ;
  .include  "2313def.inc" ;
  .list   ;
  .listmac   ;

;-------------------- Początek segmentu kodu (Code Segment) -------------------

  .cseg   ;
  .org 0 ;
  rjmp ResetProcessor ;

;----------------- Tabela wektorów przerwań (Interrupt Vectors) ----------------

  .org INT0addr ; External Interrupt0
  reti   ; Vector Address
  .org INT1addr ; External Interrupt1
  reti   ; Vector Address
  .org ICP1addr ; Input Capture1 Interrupt
  reti   ; Vector Address
  .org OC1addr ; Output Compare1A
  reti   ; Interrupt Vector Address
  .org OVF1addr ; Overflow1 Interrupt
  reti   ; Vector Address
  .org OVF0addr ; Overflow0 Interrupt
  reti   ; Vector Address
  .org URXCaddr ; UART Receive Complete
  reti   ; Interrupt Vector Address
  .org UDREaddr ; UART Data Register Empty
  reti   ; Interrupt Vector Address
  .org UTXCaddr ; UART Transmit Complete
  reti   ; Interrupt Vector Address
  .org ACIaddr ; Analog Comparator
  reti   ; Interrupt Vector Address
;---------------------------------------------------------------------------------------------
ResetProcessor :   ;
  cli   ; zablokowanie przerwań
  ldi r16,LOW(RAMEND) ; inicjacja stosu
  out SPL,r16 ; programowego
;
  ldi r16,0xFF ; cały port B jako wyjściowy
  out DDRB,r16 ; wszystkie wyjścia portu B
  out PORTB,r16 ; w stanie wysokim
;
  ldi r16,0xFC ; PD0 i PD1 jako wejścia,
  out DDRD,r16 ; reszta jako wyjścia
;
  ldi r16,0xFF ; wszystkie wyprowadzenia
  out PORTD,r16 ; portu D w stanie wysokim
;---------------------------------------------------------------------------------------------
Main_0 :   ; początek pętli głównej
  in r16,PIND ; wczytanie PIND do r16
  andi r16,0x03 ; iloczyn logiczny r16 i 3
  cpi r16,0x02 ; czy jest przyciśnięty SW1
  breq Main_1 ; tak - to skocz do Main_1
  cpi r16,0x01 ; czy jest przyciśnięty SW4
  breq Main_2 ; tak - to skocz do Main_2
  rjmp Main_0 ; powrót do pętli głównej
;---------------------------------------------------------------------------------------------
Main_1 :   ;
  cbi PORTB,0 ; PB0 = 0 to dioda świeci
  rjmp Main_0 ; powrót do pętli głównej
;---------------------------------------------------------------------------------------------
Main_2 :   ;
  sbi PORTB,0 ; PB0 = 1 - dioda nie świeci
  rjmp Main_0 ; powrót do pętli głównej
;---------------------------------------------------------------------------------------------
.exit     ; koniec kompilacji
;---------------------------------------------------------------------------------------------

   Jak widać asembler wcale nie jest taki trudny. Teraz wystarczy zaprogramować AT90S2313 i sprawdzić działanie programu   .

Pierwszy program - wariant II    Jeśli myślałeś, że to już koniec, to się myliłeś! Teraz ten sam program ale w bardziej uniwersalnym wydaniu. Jakie są różnice? Pierwszy wariant jest wariantem usztywnionym, w którym zmiana położenia przycisków oraz diody LED wymaga ingerencji z tekst programu (jako zmiana położenia należy rozumieć przyłączenie do innego portu lub innego pinu). W drugim wariancie program jest w pełni sparametryzowany. Oznacza to, że zmiana położenia przycisków lub diody LED wymaga zmiany wartości parametrów i ponownego przekompilowania programu bez konieczności modyfikacji jakichkolwiek instrukcji w programie. można więc zmienić typ µC lub podpiąć w inne miejsca przyciski lub diodę i nie trzeba wszystkiego przepisywać od nowa. Poniżej ten właśnie program z niezbędnymi wyjaśnieniami - warto go przeanalizować i poeksperymentować na nim. Wyjaśnienia do różnic między wariantami tego programu wpiszę po każdym różniącym się fragmencie jako komentarz. Pojawią się tutaj również dwie do tej pory nie używane dyrektywy def i equ oraz jedna nowa instrukcja sbrs, która wykonuje skok, jeśli określony bit w rejestrze roboczym jest ustawiony tzn. ma wartość jeden, w przeciwnym przypadku wykonywana jest następna instrukcja, czyli inaczej mówiąc omija lub nie rozkaz umieszczony bezpośrednio za rozkazem skoku.

;--------------------------- Pierwszy program - wariant II --------------------------

  .nolist   ;
  .include  "2313def.inc" ;
  .list   ;
  .listmac   ;

;---------------------------------------------------------------------------------------------
.def acc   =  r16 ; r16 teraz ma nazwę acc
;---------------------------------------------------------------------------------------------
.equ KeyPort   =  PORTD ;
.equ LEDPort   =  PORTB ;
.equ Key0Pin   =  0 ;
.equ Key1Pin   =  1 ;
.equ LEDPin   =  0 ;
; Powyżej definiujemy podstawowe parametry programu, to jest
; określamy stałą o nazwie KeyPort (ze wskazaniem na port D) jako port
; przycisków. Analogicznie określona jest stała LEDPort na oznaczenie
; portu, do którego jest przyłączona dioda LED. Dalej uszczegółowione są
; położenia przycisków oraz diody LED jako numerów bitów w portach.
; Tylko te stałe należy modyfikować w przypadku innej parametryzacji.
;---------------------------------------------------------------------------------------------
.equ KeyPDirection   =  KeyPort - 1 ;
.equ LedPDirection   =  LEDPort - 1 ;
.equ KeyPins   =  KeyPort - 2 ;
; Dodatkowo określone są rejestry związane z konfiguracją pinów
; (wejście/wyjście) użytych portów (adres portu kierunku jest o 1 mniejszy
; od portu danych: DDRB ma adres $17, a PORTB ma adres $18, DDRD ma
; adres $11, a POTRD ma adres $12. Względne przesunięcie adresowe
; rejestru kierunku (DDRx) oraz rejestru stanu pinów (PINx) w stosunku
; do rejestru danych (PORTx) jest stałe. Zmiana wartości stałych LEDPort
; lub KeyPort spowoduje, że wymienione stałe będą nadążały same za
; tymi zmianami.
;
;-------------------- Początek segmentu kodu (Code Segment) -------------------

  .cseg   ;
  .org 0 ;
  rjmp ResetProcessor ;

;----------------- Tabela wektorów przerwań (Interrupt Vectors) ----------------

  .org INT0addr ; External Interrupt0 Vector
  reti   ; Address
  .org INT1addr ; External Interrupt1 Vector
  reti   ; Address
  .org ICP1addr ; Input Capture1 Interrupt
  reti   ; Vector Address
  .org OC1addr ; Output Compare1A
  reti   ; Interrupt Vector Address
  .org OVF1addr ; Overflow1 Interrupt Vector
  reti   ; Address
  .org OVF0addr ; Overflow0 Interrupt Vector
  reti   ; Address
  .org URXCaddr ; UART Receive Complete
  reti   ; Interrupt Vector Address
  .org UDREaddr ; UART Data Register Empty
  reti   ; Interrupt Vector Address
  .org UTXCaddr ; UART Transmit Complete
  reti   ; Interrupt Vector Address
  .org ACIaddr ; Analog Comparator
  reti   ; Interrupt Vector Address
;---------------------------------------------------------------------------------------------
ResetProcessor :   ;
  cli   ; zablokowanie przerwań
  ldi acc,LOW(RAMEND) ; inicjacja stosu
  out SPL,acc ; programowego
;
  cbi KeyPDirection,Key0Pin ; Key0Pin - jako wejście
  sbi KeyPort,Key0Pin ; podciągnięte do 1
; Powyższe dwie instrukcje ustawiają pin związany z pierwszym
; przyciskiem do właściwego stanu - określają kierunek i wstępne
; spolaryzowanie pinu poprzez zerowanie oraz ustawienie odpowiednich
; (sparametryzowanych) pinów w odpowiednich (sparametryzowanych)
; portach. Wymienione dwie instrukcje "obrabiają" jeden pin.
;---------------------------------------------------------------------------------------------
  cbi KeyPDirection,Key1Pin ; Key1Pin - jako wejście
  sbi KeyPort,Key1Pin ; podciągnięte do 1
; Analogicznie jak wyżej następuje parametryzacja drugiego pinu.
;---------------------------------------------------------------------------------------------
  sbi LedPDirection,LEDPin ; LEDPin jako wyjście
  sbi LEDPort,LEDPin ; w stanie wysokim
; Parametryzacja pinu związanego z diodą LED.
;---------------------------------------------------------------------------------------------
Main_0 :   ; początek pętli głównej
  in acc,KeyPins ; acc = stan portu KeyPort
  sbrs acc,Key0Pin ; czy Key0Pin = 0 - jeżeli tak
; Jeżeli na bicie odpowiadającym pierwszemu przyciskowi jest stan wysoki
; (przycisk nie jest naciśnięty), to w wyniku wykonania instrukcji sbrs
; zostanie pominięta instrukcja następna (rjmp). W przeciwnym wypadku
; program wykona tę instrukcję (skok do etykiety Main_1).
;---------------------------------------------------------------------------------------------
  rjmp Main_1 ; to skok do Main_1
  sbrs acc,Key1Pin ; czy Key1Pin = 0 - jeżeli tak
  rjmp Main_2 ; to skok do Main_2
  rjmp Main_0 ; powrót do pętli głównej
;---------------------------------------------------------------------------------------------
Main_1 :   ;
  cbi LEDPort,LEDPin ; LEDPin=0 - LED świeci
  rjmp Main_0 ; powrót do pętli głównej
;---------------------------------------------------------------------------------------------
Main_2 :   ;
  sbi LEDPort,LEDPin ; LEDPin=1 - LED nie świeci
  rjmp Main_0 ; powrót do pętli głównej
;---------------------------------------------------------------------------------------------
.exit     ; koniec kompilacji
;---------------------------------------------------------------------------------------------

Teraz to już naprawdę koniec pierwszego programu!

Teraz możesz przejść do następnej części, czyli do drugiego programu - naciśnij ikonkę dalej...

Literatura:

[1] "Mikrokontrolery AVR w praktyce" - J. Doliński
[2] www.atmel.com - strona producenta mikrokontrolerów AVR
[3] AVR-Assembler-Tutorial - www.avr-asm-tutorial.net by Gerhard Schmidt
[4] materiały od użytkownika forum o nicku "gaweł"
pusty
do góry
WsteczMenuDalej
pusty

UWAGA: Wszystkie umieszczone schematy, informacje i przykłady mają służyć tylko do własnych celów edukacyjnych i nie należy ich wykorzystywać do żadnych konkretnych zastosowań bez przeprowadzenia własnych prób i doświadczeń, gdyż nie udzielam żadnych gwarancji, że podane informacje są całkowicie wolne od błędów i nie biorę odpowiedzialności za ewentualne szkody wynikające z zastosowania podanych informacji, schematów i przykładów.


Wszystkie nazwy handlowe, nazwy produktów oraz znaki towarowe umieszczone na tej stronie są zastrzeżone dla ich właścicieli.
Używanie ich tutaj nie powinno być uważane za naruszenie praw właściciela, jest tylko potwierdzeniem ich dobrej jakości.

All trademarks mentioned herein belong to their respective owners.
They aren't intended to infringe on ownership but only to confirm a good quality.


Strona wygląda równie dobrze w rozdzielczości 1024x768, jak i 800x600.
Optymalizowana była pod IE dlatego polecam przeglądanie jej w IE5.5 lub nowszych przy rozdzielczości 1024x768.


© Copyright 2001-2005   Elektronika analogowa
pusty
pusty pusty pusty