MMS 8051 cz4, 8051 programowanie


GÓRNICZO - HUTNICZA

IM. STANISŁAWA STASZICA W KRAKOWIE

0x01 graphic

WYDZIAŁ ELEKTROTECHNIKI, AUTOMATYKI, INFORMATYKI I ELEKTRONIKI

KATEDRA AUTOMATYKI NAPĘDU I URZĄDZEŃ PRZEMYSŁOWYCH

MIKROPROCESOROWE METODY STEROWANIA

Mikrokontrolery rodziny MCS-51

Cz. IV.

Autor:

Dr inż. Zbigniew Waradzyn

Kraków 2005

  1. Kilka pożytecznych instrukcji

30.1. Operacje logiczne na bitach

  1. suma logiczna

Przykładem instrukcji realizującej sumę logiczną jest orl ad, #n - instrukcja wykonująca sumę logiczną, bit po bicie, podanych argumentów i wpisująca wynik do komórki podanej jako pierwszy argument - w powyższym przykładzie do komórki o adresie ad.

Suma logiczna dwóch bitów wynosi 1, jeśli przynajmniej jeden z nich jest w stanie 1; jeśli natomiast oba bity mają stan 0 - ich suma też wynosi 0.

Wniosek: wykonanie sumy logicznej dowolnego bitu i wartości 1 powoduje ustawienie tegoż bitu, natomiast wykonanie sumy logicznej dowolnego bitu i wartości 0 nie zmienia wartości bitu.

Przykład:

Poniższa instrukcja realizuje sumę logiczną zawartości komórki o adresie 30H i liczby 43H:

orl 30H, #43H

A oto efekt jej wykonania (x oznacza dowolny stan bitu, czyli 1 lub 0):

stan komórki 30H przed wykonaniem instrukcji xxxx xxxx

drugi argument instrukcji orl (maska) 0100 0011

stan komórki 30H po wykonaniu instrukcji x1xx xx11

Instrukcja orl może być użyta do ustawiania pojedynczych bitów w komórkach, które nie są adresowalne bitowo lub do ustawienia kilku bitów jednocześnie w dowolnej komórce pamięci. Występująca w powyższej instrukcji liczba 43 określa, które bity w komórce 30H mają być ustawione i jest nazywana maską.

Praktyczny przykład zastosowania instrukcji anl ... jest podany w dalszej części opracowania, przy omawianiu ustawiania trybów pracy liczników.

  1. iloczyn logiczny

Przykładem instrukcji realizującej iloczyn logiczny jest anl ad, #n - instrukcja wykonująca iloczyn logiczny, bit po bicie, podanych argumentów i wpisująca wynik do komórki podanej jako pierwszy argument - w powyższym przykładzie do komórki o adresie ad.

Iloczyn logiczny dwóch bitów wynosi 0, jeśli przynajmniej jeden z nich jest w stanie 0; jeśli natomiast oba bity mają stan 1 - ich iloczyn też wynosi 1.

Wniosek: wykonanie iloczynu logicznego dowolnego bitu przez wartość 0 powoduje jego wyzerowanie, a przez wartość 1 - nie zmienia wartości bitu.

Przykład:

Poniższa instrukcja wykonuje iloczyn logiczny zawartości komórki o adresie 30H i liczby EBH:

anl 30H, #0EBH

A oto efekt jej wykonania (x oznacza dowolny stan bitu):

stan komórki 30H przed wykonaniem instrukcji xxxx xxxx

drugi argument instrukcji anl (maska) 1110 1011

stan komórki 30H po wykonaniu instrukcji xxx0 x0xx

Instrukcja anl może być użyta do zerowania pojedynczych bitów w komórkach, które nie są adresowalne bitowo lub do zerowania kilku bitów jednocześnie w dowolnej komórce pamięci. Praktyczny przykład zastosowania tej instrukcji jest podany w dalszej części opracowania przy omawianiu ustawiania trybów pracy liczników.

  1. suma modulo 2 (EXCLUSIVE-OR, różnica symetryczna)

Przykładem instrukcji realizującej sumę modulo 2 jest xrl ad, #n - instrukcja wykonująca sumę mod 2, bit po bicie, podanych argumentów i wpisująca wynik do komórki podanej jako pierwszy argument - w powyższym przykładzie do komórki o adresie ad.

Suma mod 2 dwóch bitów wynosi 1, jeśli oba bity mają różne stany; jeśli natomiast oba bity mają ten sam stan - suma mod 2 wynosi 0.

Wniosek: wykonanie sumy mod 2 dowolnego bitu i wartości 1 powoduje zmianę stanu bitu, natomiast wykonanie sumy mod 2 dowolnego bitu i wartości 0 - nie zmienia wartości bitu.

Przykład:

Poniższa instrukcja realizuje sumę mod 2 zawartości komórki o adresie 30H i liczby 43H:

xrl 30H, #43H

A oto efekt jej wykonania (0x01 graphic
oznacza dowolny stan bitu, a 0x01 graphic
- stan przeciwny do 0x01 graphic
):

stan komórki 30H przed wykonaniem instrukcji 0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic

drugi argument instrukcji xrl (maska) 0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic

stan komórki 30H po wykonaniu instrukcji 0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic
0x01 graphic

Instrukcja xrl może być użyta do negacji stanu pojedynczych bitów w komórkach, które nie są adresowalne bitowo lub do negacji stanu kilku bitów jednocześnie w dowolnej komórce pamięci. Praktyczny przykład zastosowania tej instrukcji jest podany w następnym punkcie, przy obliczaniu liczby przeciwnej.

  1. kopiowanie bitów

Skopiowanie zawartości 8-bitowej komórki z wewnętrznej pamięci mikrokontrolera do innej komórki z tejże pamięci można zrealizować na przykład instrukcją

mov ad1, ad2

w której ad2 oznacza adres komórki źródłowej (tej, której zawartość kopiujemy), a ad1 - adres komórki docelowej (tej, do której kopiujemy). Przykładowo, za pomocą instrukcji mov 30H, P1 kopiujemy stan wejść portu P1 do komórki o adresie 30H, a za pomocą instrukcji mov 65H, 30H kopiujemy zawartość komórki o adresie 30H do komórki o adresie 65H.

Pytanie: W jaki sposób skopiować stan bitu?

Odpowiedź: Do kopiowania stanu bitu służą także instrukcje mov ... , ale musi w nich występować znacznik przeniesienia CY (Carry), który w tych instrukcjach jest oznaczony przez C. Są dwie instrukcje tego typu i mają one postać (nadmieniano już o tym w p. 14.1):

mov C, bit - skopiowanie wartości bitu o adresie bit do znacznika przeniesienia CY,

mov bit, C - skopiowanie wartości znacznika przeniesienia CY do bitu o adresie bit.

Przykładowo, w wyniku wykonania ciągu instrukcji

mov C, P1.3

mov ACC.0, C

stan linii portu P1.3 zostanie, poprzez znacznik przeniesienia CY, skopiowany do najmłodszego bitu akumulatora, a instrukcje

mov C, 7FH

mov 0H, C

spowodują skopiowanie stanu bitu 7FH (to najstarszy bit komórki 2FH - pamiętasz?) do bitu o adresie 0H (najmłodszy bit komórki 20H).

30.2. Obliczanie liczby przeciwnej - przypomnienie i uzupełnienie

Liczbą przeciwną do danej liczby x1 jest liczba x2 taka, że x2 = - x1; zachodzi więc x1 + x2 = 0.

Jeśli liczbę zapisaną w kodzie dwójkowym (binarnym) interpretujemy jako liczbę ze znakiem w kodzie U2 (kod uzupełnień do dwóch), jak to zostało opisane w p. 20, to obliczenie liczby przeciwnej do danej liczby można wykonać w dwu etapach:

    1. zanegować każdy z bitów z osobna,

    2. do tak otrzymanej liczby dodać 1.

Jeżeli zadana liczba jest ujemna, to liczba przeciwna jest zarazem jej wartością bezwzględną.

Przykład: Weźmy liczbę 5, binarnie 0000 0101b.

liczba zadana 5

0000 0101b

liczba po zanegowania bitów

1111 1010b

suma obu liczb

1111 1111b

Suma obu powyższych liczb daje same jedynki, jeśli więc dodamy jeszcze 1, uzyskamy same zera (pomijając „jeden dalej”). Liczbą przeciwną do 5, czyli -5, będzie więc liczba powstała przez zanegowanie bitów liczby 5 przedstawionej w postaci dwójkowej, a następnie zwiększenie powstałej liczby o 1, czyli liczba 1111 1011b.

Programowa realizacja obliczenia liczby przeciwnej (zakładamy, że liczba zadana znajduje się w komórce pamięci o nazwie Liczba) jest następująca:

xrl Liczba, #0FFh ; negacja wszystkich bitów (p. 30.1c)

inc Liczba ; powiększenie o 1

Po wykonaniu tych instrukcji obliczona liczba przeciwna będzie znajdować się w tej samej komórce pamięci, w której znajdowała się wcześniej liczba zadana; u nas - komórka Liczba.

Negacji wszystkich bitów możemy dokonać też instrukcją cpl A (negacja wszystkich bitów akumulatora). Liczbę przeciwną możemy w takim przypadku obliczyć następująco:

mov A, Liczba ; przeniesienie liczby do akumulatora

cpl A ; negacja wszystkich bitów akumulatora

inc A ; powiększenie zawartości akumulatora o 1

mov Liczba, A ; przeniesienie liczby przeciwnej do komórki Liczba

Takie rozwiązanie ma jednak tę wadę, ze wymaga wykonania dwóch dodatkowych instrukcji kopiowania: najpierw do akumulatora, a by w nim dokonać negacji, a następnie z powrotem do komórki Liczba.

Uwaga: Zastosowana instrukcja cpl A dotyczy akumulatora i realizuje negację wszystkich bitów akumulatora. Wcześniej była omawiana instrukcja cpl bit, negująca stan pojedynczego bitu o adresie bit. Jeśli więc użyjemy instrukcji cpl z argumentem, np. cpl 20H, to ta instrukcja dokona wyzerowania bitu o adresie 20H, a nie komórki 20H.

30.3. Względne adresy skoków w instrukcjach - przypomnienie i uzupełnienie

W wielu instrukcjach argumentem jest względny adres skoku d, np. w poznanych już instrukcjach:

Względny adres skoku d występuje także w instrukcjach:

Działanie obu powyższych instrukcji jest podobne do działania instrukcji jnb bit, d oraz jb bit, d, z tą różnicą, że testowany jest nie stan bitu o zadanym adresie bit, ale znacznik przeniesienia CY. Znacznik ten jest ustawiany lub zerowany w wyniku wykonania niektórych instrukcji - przykłady zostaną podane poniżej.

We wszystkich powyższych instrukcjach następuje w przypadku spełnienia warunku (w instrukcji sjmp d - bezwarunkowo) skok o d pozycji (komórek) w pamięci programu, przy czym liczba d jest traktowana jako liczba ze znakiem. Zakres skoku to maksymalnie 127 komórek do przodu lub 128 komórek do tyłu (gdy d jest liczba ujemną). Skok następuje od miejsca, z którego byłaby pobrana kolejna instrukcja, gdyby nie było skoku. Względny adres skoku został omówiony na konkretnym przykładzie w p. 21.4.4.

Aby zwolnić programistę od żmudnego obliczania o ile komórek należy skoczyć, wprowadzono ułatwienie polegające na tym, że programista zamiast względnego adresu skoku wpisuje etykietę instrukcji, do której należy wykonać skok, np.:

jnc Zwieksz

.........

Zwieksz:

........

Obliczenia o ile komórek należy skoczyć i w którym kierunku, czyli wyliczenia wartości d, dokonuje komputer podczas asemblacji, czyli przekształcania napisanego przez programistę programu źródłowego na kod binarny.

Uwaga1:

Nazwy instrukcji w pliku źródłowym mogą być pisane zarówno dużymi, jak i małymi literami.

Uwaga2:

Od omówionych wcześniej instrukcji (jnb bit, d; jnb bit, d; sjmp d; itp.), w których występuje względny adres skoku d, należy odróżnić instrukcję skoku bezwzględnego ljmp adr16, której argumentem (adr16) jest podany bezpośrednio 16-bitowy adres w pamięci programu, do którego należy wykonać skok. Ten adres jest wyliczany podczas asemblacji, natomiast programista wpisuje tu etykietę (podobnie, jak w przypadku instrukcji z względnym adresem skoku) , np.

A1:

.........

ljmp A1

.........

Uwaga3:

Inne kryterium podziału rozróżnia instrukcje:

30.4. Porównywanie dwu wielkości

W programach często występuje konieczność porównywania ze sobą dwu wielkości. Poniżej omówiono realizację porównania dwu wielkości za pomocą instrukcji cjne oraz instrukcji odejmowania subb.

  1. instrukcja cjne wielkosc1, wielkosc2, d

Instrukcja ta występuje w kilku odmianach (jak i wiele innych instrukcji) i porównuje ze sobą dwie wielkości, po czym realizuje skok względny, jeśli te wielkości różnią się od siebie (compare and jump if not equal - porównaj i skocz, jeśli nie są równe). Symbol d oznacza względny adres skoku (p. 30.3). Dodatkowo, instrukcja ta porównuje obie wielkości traktując je jako liczby bez znaku i odpowiednio ustawia lub zeruje znacznik przeniesienia CY:

Uwaga: Jeśli uważnie przeczytałeś p. 26 „Kody instrukcji i argumenty instrukcji”, to powinieneś teraz mieć słuszną wątpliwość: z zapisu cjne wielkosc1, wielkosc2, d wynikałoby, że instrukcja cjne ... ma 3 argumenty, a tam było napisane, że instrukcja może być maksymalnie 3-bajtowa - kod instrukcji i 2 argumenty. Otóż wielkosc1 występująca w instrukcji cjne ... musi być zawarta w akumulatorze lub jednym z rejestrów R0 ÷ R7 (ewentualnie być wskazywana przez jeden z rejestrów R0 ÷ R7 - instrukcja cjne @Ri, #n, d) i miejsce jej występowania jest już zawarte w kodzie instrukcji, np. cjne A, ad, d (kod B5H) lub cjne R7, #n, d (kod BFH), zmniejszając liczbę argumentów instrukcji w pamięci programu do dwóch. Natomiast nie może istnieć instrukcja postaci cjne ad1, ad2, d, gdyż wymagałaby ona umieszczenia w pamięci programu aż 3 argumentów oprócz kodu instrukcji.

Oto przykład zastosowania instrukcji cjne:

WARTOSC equ 30H ; nadanie nazwy WARTOSC stałej wynoszącej 30H

..............

cjne A, # WARTOSC, TestZnaku

Rowne:

................ ; A = WARTOSC

TestZnaku:

jnc Wiekszy

Mniejszy:

............. ; A < WARTOSC

Wiekszy:

........... ; A > WARTOSC

Przedstawiony fragment programu porównuje zawartość akumulatora z liczbą zadaną w stałej WARTOSC i jeśli te wielkości są różne, testuje znacznik CY. W zależności od tego, która z porównywanych wielkości jest większa następuje przejście do różnych miejsc w programie i wykonanie różnych instrukcji:

    1. jeśli oba argumenty są równe, wykonywane są instrukcje począwszy od etykiety Rowne,

    2. jeśli zawartość akumulatora jest mniejsza od liczby zadanej w stałej WARTOSC, wykonywane są instrukcje począwszy od etykiety Mniejszy,

    3. jeśli zawartość akumulatora jest większa od liczby zadanej w stałej WARTOSC, wykonywane są instrukcje począwszy od etykiety Wiekszy.

Pytanie: W jaki sposób zrealizować skok w określone miejsce w programie przy warunku „mniejszy lub równy” albo „większy lub równy”?

Odpowiedź: Można wykorzystać przedstawiony wyżej fragment programu i w przypadku równości obu argumentów wykonać skok bezwarunkowy w określone miejsce, co jest pokazane w poniższych przykładach:

  1. jeśli chcemy wyróżnić nierówność A WARTOSC, należy w przypadku równości obu argumentów wykonać skok do etykiety Mniejszy - przykład z lewej strony poniżej,

  2. jeśli chcemy wyróżnić nierówność A WARTOSC, należy w przypadku równości obu argumentów wykonać skok do etykiety Wiekszy - przykład z prawej strony poniżej.


WARTOSC equ 30H

..............

cjne A, # WARTOSC, TestZnaku

Rowne:

sjmp Mniejszy

TestZnaku:

jnc Wiekszy

Mniejszy:

............. ; A WARTOSC

Wiekszy:

........... ; A > WARTOSC

WARTOSC equ 30H

..............

cjne A, # WARTOSC, TestZnaku

Rowne:

sjmp Wiekszy

TestZnaku:

jnc Wiekszy

Mniejszy:

............. ; A <WARTOSC

Wiekszy:

........... ; A WARTOSC


W przypadku sprawdzania, czy pierwszy argument jest większy lub równy drugiemu, można też to zrobić krócej, jak w poniższym przykładzie:

WARTOSC equ 30H

..............

cjne A, # WARTOSC, TestZnaku

TestZnaku:

jnc Wiekszy

Mniejszy:

............. ; A < WARTOSC

Wiekszy:

........... ; A WARTOSC

  1. instrukcja subb A, arg

Instrukcja odejmowania odejmuje argument od zawartości akumulatora i dodatkowo odejmuje 1, jeśli znacznik CY jest ustawiony, po czym zapisuje wynik do akumulatora. Poprzednia zawartość akumulatora zostaje więc utracona. Odejmowanie można zapisać tak:

A - arg - CY.

Jeśli znacznik CY ma nie brać udziału w odejmowaniu, należy go wyzerować przed wykonaniem instrukcji odejmowania.

Po wykonaniu odejmowania znacznik CY zostaje:

W poniższym przypadku zostaną wykonane instrukcje począwszy od etykiety Wiekszy, jeśli zawartość akumulatora jest większa lub równa liczbie zadanej w stałej WARTOSC; w przeciwnym wypadku zostaną wykonane instrukcje począwszy od etykiety Mniejszy.

WARTOSC equ 30H

..............

clr C

subb A, # WARTOSC

jnc Wiekszy

Mniejszy:

............. ; A < WARTOSC

Wiekszy:

........... ; A WARTOSC

  1. Symboliczne nazwy komórek i bitów

W programie źródłowym przes_a.s03 podawaliśmy bezpośrednio nazwy linii portu oraz adresy wykorzystywanych komórek (patrz kod źródłowy programu w p.16). Przykładowo, wejście W1 (rys.9.1) testowaliśmy instrukcją jnb P3.4, A2, a do realizacji opóźnienia korzystaliśmy m. in. z komórki 30h, np. w instrukcji djnz 30h, B1. Takie bezpośrednie stosowanie adresów może być bardzo uciążliwe, zwłaszcza w dłuższych programach, gdyż wymaga od programisty ciągłego pamiętania o funkcjach, jakie pełnią poszczególne bity i komórki pamięci. Dużym ułatwieniem może być zastosowanie symbolicznych nazw komórek i bitów, które od razu będą się nam kojarzyć z funkcją pełnioną przez dany bit lub komórkę. W takim przypadku na początku programu jednokrotnie przypisuje się nazwy poszczególnym bitom i komórkom pamięci, a w dalszej części programu używamy już tylko wprowadzonych nazw. Wpisanie właściwych wartości zamiast nazw symbolicznych następuje automatycznie potem, podczas asemblacji programu źródłowego.

Poniżej podano kod źródłowy programu przes_b.s03. Program ten jest dokładnie taki sam, jak program przes_a.s03, z tą różnicą, że zastosowano w nim symboliczne nazwy komórek i linii portów. Przypisanie nazwy do adresu dokonuje się przy użyciu słowa equ, np. w wierszu W1 equ P3.4 dokonano przypisania nazwy W1 linii portu P3.4.

*****************************************************************

; Program przes_b.s03

; autor: dr inz. Zbigniew Waradzyn KANiUP AGH Krakow

;*****************************************************************

; W zaleznosci od stanu wejsc P3.4 i P3.5 nastepuje:

; - stabilne swiecenie dwu diod,

; - swiecenie jednej lub dwu diod z przesuwem w prawo,

; - swiecenie jednej lub dwu diod z przesuwem w lewo.

;-----------------------------------------------------------------

; Nadanie nazw wejsciom, wyjsciom i zmiennym

W1 equ P3.4

W2 equ P3.5

D1 equ P1.1

D2 equ P1.2

D3 equ P1.3

D4 equ P1.4

D5 equ P1.5

D6 equ P1.6

Licznik1 equ 30h

Licznik2 equ 31h

Licznik3 equ 32h

;*****************************************************************

; Poczatek programu od adresu 0000h

;*****************************************************************

; Swieca diody D1 i D4, pozostale zgaszone

clr D1 ; zapalenie diody D1

clr D4 ; zapalenie diody D4

setb D2 ; zgaszenie diody D2

setb D3 ; zgaszenie diody D3

setb D5 ; zgaszenie diody D5

setb D6 ; zgaszenie diody D6

setb P1.0

setb P1.7

;-----------------------------------------------------------------

;*****************************************************************

; POCZATEK PETLI GLOWNEJ PROGRAMU

; Sprawdzanie, czy ma byc przesuw

A1:

jnb W1, A2

;-----------------------------------------------------------------

; Nie ma przesuwu - swieca diody 1 i 4, pozostale zgaszone

A3:

mov P1, #0EDh ; zapalenie i zgaszenie diod jedna instrukcja

sjmp A1

;*****************************************************************

; Jest przesuw - testowanie kierunku przesuwu

A2:

jb W2, A4

;-----------------------------------------------------------------

; Przesuw w prawo

A5:

mov A, P1

rr A

mov P1, A

sjmp A6

;-----------------------------------------------------------------

;-----------------------------------------------------------------

; Przesuw w lewo

A4:

mov A, P1

rl A

mov P1, A

;*****************************************************************

; Blok opozniajacy - ok. 0.5 sek.

;-----------------------------------------------------------------

; Wpis wartosci poczatkowych do komorek pomocn.(liczn. opozn.)

A6:

mov Licznik1, #255 ; licznik opozn. 1

mov Licznik2, #255 ; licznik opozn. 2

mov Licznik3, #4 ; licznik opozn. 3

; Pierwsza petla opozniajaca (ok. 500 mikrosekund)

B1:

djnz Licznik1, B1

; Wejscie w to miejsce w programie co ok. 500 mikrosekund

mov Licznik1, #255 ; ponowne wpisanie wartosci poczatk.

; do licznika opozn.1

; Druga petla opozniajaca

djnz Licznik2, B1

; Wejscie w to miejsce w programie co ok. 0.125 sekundy

mov Licznik2, #255 ; ponowne wpisanie wartosci poczatk.

; do licznika opozn.2

; Trzecia petla opozniajaca

djnz Licznik3, B1

; Wejscie w to miejsce w programie co ok. 0.5 sekundy - koniec

; opoznienia

;-----------------------------------------------------------------

sjmp A1

; Koniec programu

end ; ta instrukcja ZAWSZE konieczna na koncu programu

Uwaga 1:

Przypisywanie nazw odbywa się dokładnie w taki sam sposób, niezależnie od tego, czy nazwa ma zastępować adres bitu, adres komórki, czy też stałą (przykład użycia stałej został podany już w p.30.4). Podczas asemblacji w miejsce nazwy zostanie użyta przypisana jej wielkość, zaś sposób interpretacji będzie zależał od kontekstu. Przykłady z powyższego programu:

Uwaga 2:

Otrzymany plik wynikowy przes_b.hex jest dokładnie taki sam, jak plik przes_a.hex, co można łatwo sprawdzić porównując zawartość obu plików (warto także porównać zawartość pliku przes_b.lst z zawartością pliku przes_a.lst). Sposób uzyskania pliku w postaci *.hex został opisany w p. 21).

Uwaga 3:

0x08 graphic
Przypisania nazw symbolicznych komórkom nie należy podawać w algorytmach w postaci oddzielnych bloczków, jak np.

W algorytmach można posługiwać się zarówno nazwami jak i adresami (porównaj rys. 11.1). Ważne jest użycie takiego zapisu, aby w sposób zrozumiały dla czytelnika pokazywał, jaka operacja jest wykonywana.

32. Układ czasowo - licznikowy

Mikrokontroler 8051 zawiera dwa 16-bitowe liczniki. Jeden z nich oznaczony jest przez T0, a drugi przez oraz T1. Aktualny stan licznika T0 jest przechowywany w rejestrach TH0 (część bardziej znacząca, czyli starszy bajt, czyli starsze osiem bitów) oraz TL0 (część mniej znacząca). Analogicznie, aktualny stan licznika T1 jest przechowywany w rejestrach TH1 i TL1. Wszystkie wymienione rejestry znajdują się w obszarze SFR (rys. 17.2), a dla przykładu można podać, że adres rejestru TL0 to 8AH.

Liczniki te mogą pracować jako liczniki zliczające impulsy zewnętrzne (ang. counter) lub impulsy z wewnętrznego zegara (ang. timer). Liczniki te liczą zawsze „w przód”, czyli impuls wejściowy zwiększa stan licznika.

Licznik T0 może pracować w jednym z trybów 0, 1, 2 lub 3, a licznik T1 - w jednym z trybów 0, 1 lub 2, przy czym w trybach 0, 1 i 2 oba liczniki działają identycznie oraz każdy z nich działa niezależnie od drugiego. Poniżej podane są podstawowe informacje o pracy liczników w trybach 0 i 1.

32.1. Praca liczników w trybie 0 i 1

Schemat blokowy liczników T0 i T1 w trybie 0 i 1 przedstawia rys. 33.1 a). Schemat jest taki sam dla każdego z liczników z tym, że każdemu z nich odpowiadają oddzielne komórki pamięci: np. symbol TLi na schemacie oznacza rejestr TL0 w przypadku licznika T0 lub rejestr TL1 w przypadku licznika T1, zaś symbol TRi - bit TR0 w przypadku licznika T0 lub bit TR1 w przypadku licznika T1.

W zależności od stanu bitu C/T\ (przełącznik programowy) licznik może:

Przy założeniu, że GATE = 0, licznik uruchamia się przez ustawienie bitu TR0 (licznik T0) lub TR1 - licznik T1.

Licznik w trybach 0 i 1 zlicza „w przód” (do „góry”) do osiągnięcia wartości maksymalnej, a następnie po kolejnym impulsie przyjmuje stan 0 (przepełnia się) i zlicza dalej „w kółko”. W wyniku przepełnienia następuje ustawienie specjalnego bitu, tzw. znacznika przepełnienia -

0x01 graphic

0x01 graphic

0x01 graphic

Rys. 32.1. Schematy liczników w poszczególnych trybach pracy [2]

TF0 lub TF1 - te bity są w rejestrze TCON (patrz poniżej). Wartość maksymalna osiągana przez licznik zależy od trybu pracy:

- tryb 0 - licznik „zachowuje się” jak licznik 13-bitowy (złożony z ośmiu bitów rejestru TH0 (TH1) i bitów 3 - 7 rejestru TL0 (TL1) - przepełnia się po zliczeniu 8192 impulsów),

- tryb 1 - licznik „zachowuje się” jak licznik 16-bitowy (zlicza od 0 do 65535).

Jeśli więc nie będziemy do licznika nic wpisywać* to licznik w trybie 0 przepełnia się po każdorazowym zliczeniu 8192 impulsów, zaś licznik w trybie 1 przepełnia się po każdorazowym zliczeniu 65536 impulsów.

* Uwaga: Możliwa jest modyfikacja stanu licznika (nawet podczas jego pracy) - czyli możemy dokonywać zapisu do rejestrów TL0, TH0, TL1 i TH1.

Pytanie.: Jak sterować licznikiem ? (gdzie są bity np. C/T, TR0 i jak ustawić tryb pracy licznika?)

Odp.: Do obsługi liczników przeznaczone są specjalne w rejestry TMOD i TCON, znajdujące się w obszarze SFR (rys. 17.2, 19.1):

0x08 graphic

T1

0x08 graphic

T0

TMOD

GATE

C/T

M1

M0

GATE

C/T

M1

M0

89H

MSB

LSB

TCON

TF1

TR1

TF0

TR0

88H

MSB

LSB

Cztery młodsze bity rejestru TMOD dotyczą licznika T0, a cztery starsze bity - licznika T1. Cztery młodsze bity rejestru TCON dotyczą przerwań, więc nie zostały tu pokazane.

Tryb pracy licznika ustala się przy użyciu bitów M1 i M0:

tryb 0 - M1 = 0, M0 = 0,

tryb 1 - M1 = 0, M0 = 1,

Pytanie.: Czy rejestry związane z licznikami są adresowane bitowo?

Odp.: Bitowo adresowany jest tylko rejestr TCON - ma on adres 88H. Natomiast rejestry TMOD, TL0, TL1, TH0 i TH1 są adresowane tylko bajtowo (końcówka ich adresu jest różna od 0 lub 8 - porównaj rys.17.2).

Pytanie: Chcemy wykorzystać licznik T1 do generacji opóźnienia o czasie trwania ok. 8 ms. Co należy zrobić, aby to uzyskać?

Odp.: a) Warunki:

- tryb 0 (M1 = 0, M0 = 0), wtedy licznik pracuje jak 13-bitowy, więc przepełnienie co 8192 impulsy (przy f XTAL = 12 MHz przepełnienie co ok. 8 ms),

- zliczanie impulsów zegarowych (C/T\ = 0),

- GATE = 0,

- TR1 = 1 - załączenie licznika.

  1. po załączeniu napięcia zasilającego lub zresetowaniu mikrokontrolera wszystkie bity w rejestrach TCON i TMOD (oraz większości innych) są wyzerowane,

  1. wystarczy więc tylko ustawić bit TR1, np. instrukcją setb TR1,

  2. co ok. 8 ms zostanie ustawiony znacznik przepełnienia TF1, co będzie informacją o upływie kolejnych 8 ms (znacznik ten należy następnie programowo wyzerować).

Pytanie.: Co by było, gdyby wybrano tryb 1 pracy licznika, zamiast trybu 0?

Odp.: Licznik przepełniałby się co 65536 impulsów, czyli co ok. 65 ms, zamiast co ok. 8 ms.

Pytanie.: Co należałoby zrobić, aby licznik T0 zliczał impulsy zewnętrzne?

Odp.: Impulsy należy podać na wejście T0 (P3.4) mikrokontrolera oraz dodatkowo:

Pytanie.: Jak ustawić bit C/T\, bez zmieniania stanu pozostałych bitów rejestru TMOD?

Odp.: Można to zrobić instrukcją

orl TMOD, #0000 0100b w pozycji bitu, który ma być ustawiony, wpisuje się 1

Instrukcja orl wykonuje sumę logiczną (bit po bicie) podanych argumentów i wynik wpisuje do komórki podanej jako pierwszy argument - w powyższym przykładzie do rejestru TMOD (p.30.1a). Jeśli więc choć jeden z porównywanych bitów ma stan 1, to wynik wynosi 1 (x oznacza dowolny stan, poza tym, jeśli przed wykonaniem instrukcji i po jej wykonaniu w TMOD jest x, znaczy to, że stan tego bitu się nie zmienił):

stan TMOD przed wykonaniem instrukcji xxxxxxxx

drugi argument instrukcji orl 00000100

stan TMOD po wykonaniu instrukcji xxxxx1xx

Pytanie.: Jak wyzerować bit C/T\ , bez zmieniania stanu pozostałych bitów rejestru TMOD?

Odp.: Można to zrobić instrukcją

anl TMOD, #1111 1011b w pozycji bitu, który ma być wyzerowany, wpisuje się 0

Instrukcja anl wykonuje iloczyn logiczny (bit po bicie) podanych argumentów i wynik wpisuje do komórki podanej jako pierwszy argument - w powyższym przykładzie do rejestru TMOD (p.30.1b). Jeśli więc choć jeden z porównywanych bitów ma stan 0, to wynik wynosi 0.

stan TMOD przed wykonaniem instrukcji xxxxxxxx

drugi argument instrukcji anl 11111011

stan TMOD po wykonaniu instrukcji xxxxx0xx

Pytanie.: Jak skonfigurować licznik T1 do zliczania impulsów wewnętrznych w trybie 0 bez zmiany ustawień licznika T0?

Odp.: Można to zrobić instrukcją

anl TMOD, # 00001111b

lub równoważna jej instrukcją

anl TMOD, #0FH

Działanie tej instrukcji jest następujące:

stan TMOD przed wykonaniem instrukcji xxxxxxxx

drugi argument instrukcji anl 00001111

stan TMOD po wykonaniu instrukcji 0000xxxx

W wyniku wykonania powyższej instrukcji zostaną wyzerowane cztery bardziej znaczące bity rejestru TMOD dotyczące licznika T1 (co odpowiada naszym potrzebom - sprawdź!), zaś cztery mniej znaczące bity tegoż rejestru, dotyczące licznika T0, pozostaną bez zmian.

32.2. Algorytm opóźnienia zrealizowanego przy użyciu licznika

Pytanie.: Jak wykorzystać licznik T1 do generacji zadanego opóźnienia (zakładamy, że ma on przepełniać się co ok. 8 ms)?

Odp.: Wykorzystamy znacznik przepełnienia licznika T1, który nazywa się TF1 (rejestr TCON). W tym celu należy wykonać następujące kroki:

  1. skonfigurować licznik do zliczania czasu - funkcja czasomierza (timera), z przepełnieniami co ok. 8 ms,

  2. zadać wielkość opóźnienia, np. jako krotność czasu 8 ms,

  3. uruchomić licznik,

  4. czekać, aż TF1 zostanie ustawiony,

  5. zarejestrować tę sytuację jako upływ kolejnego odcinka czasu i wyzerować TF1,

  6. sprawdzić, czy minął zadany czas opóźnienia:

Algorytm fragmentu programu realizującego opóźnienie przedstawiono na poniższym rysunku, przy czym założono, że:

  1. przepełnienie licznika T1 (ustawienie znacznika TF1) następuje co ok. 8 ms,

  2. uzyskane opóźnienie będzie wielokrotnością 8 ms, czyli

opóźnienie = n * 8 ms.

Ta liczba n będzie na początku każdego opóźnienia wpisywana do rejestru B.

  1. licznik został odpowiednio skonfigurowany i uruchomiony wcześniej.

0x08 graphic

Poniżej przedstawiono ciąg instrukcji realizujących opóźnienie ok. 480 ms na podstawie powyższego algorytmu - przyjęto n = 60 (480 ms = 60 * 8 ms).

mov B, #60

Opoznienie:

jnb TF1, Opoznienie

clr TF1

djnz B, Opoznienie

Bardziej eleganckim rozwiązaniem będzie zapisanie naszej wartości n jako stałej (dobrym obyczajem jest pisanie stałych wielkimi literami), np.

MNOZNIK_OPOZN equ 60

...................

mov B, # MNOZNIK_OPOZN

Opoznienie:

jnb TF1, Opoznienie

clr TF1

djnz B, Opoznienie

Aby uzyskać lepszą dokładność przy odliczaniu pierwszego odcinka czasu 8 ms, można przed wpisem mnożnika opóźnienia do rejestru B wyzerować licznik T1 oraz znacznik TF1 instrukcjami:

mov TL1, #0

mov TH1, #0

clr TF1

Generację opóźnienia przy wykorzystaniu licznika T1 zademonstrowano w programie przes_c.s03. Program ten realizuje świecenie diod z przesuwem i efekt jego działania jest praktycznie taki sam, jak efekt działania przedstawionych wcześniej programów przes_a.s03 i przes_b.s03 - inna może być jedynie częstotliwość „przesuwu” diod. Jednakże kod wynikowy tego programu jest inny, gdyż zastosowano w nim zupełnie inną metodę realizacji opóźnienia (zawartość pliku przes_c.hex różni się od zawartości pliku przes_a.hex, czy przes_b.hex).

;****************************************************************************

; Program przes_c.s03

; Realizacja opoznienia przy wykorzystaniu timera i testowaniu jego znacznika

; autor: dr inz. Zbigniew Waradzyn KANiUP AGH Krakow

;****************************************************************************

; W zaleznosci od stanu wejsc P3.4 i P3.5 nastepuje:

; - stabilne swiecenie dwu diod,

; - swiecenie jednej lub dwu diod z przesuwem w prawo,

; - swiecenie jednej lub dwu diod z przesuwem w lewo.

;****************************************************************************

; Wprowadzenie stalej

MNOZNIK_OPOZN equ 60

;----------------------------------------------------------------------------

; Nadanie nazw zmiennym

W1 equ P3.4

W2 equ P3.5

D1 equ P1.1

D2 equ P1.2

D3 equ P1.3

D4 equ P1.4

D5 equ P1.5

D6 equ P1.6

;----------------------------------------------------------------------------

;****************************************************************************

; Poczatek programu od adresu 0000h

;****************************************************************************

; Uruchomienie licznika 1 w trybie 0, przepelnienie co ok. 8 ms

anl TMOD, #0FH ; *) patrz następna strona

setb TR1

;----------------------------------------------------------------------------

; Swieca diody D1 i D4, pozostale zgaszone

clr D1 ; zapalenie diody D1

clr D4 ; zapalenie diody D4

setb D2 ; zgaszenie diody D2

setb D3 ; zgaszenie diody D3

setb D5 ; zgaszenie diody D5

setb D6 ; zgaszenie diody D6

setb P1.0

setb P1.7

;****************************************************************************

; POCZATEK PETLI GLOWNEJ PROGRAMU

; Sprawdzanie, czy ma byc przesuw

A1:

jnb W1, A2

;----------------------------------------------------------------------------

; Nie ma przesuwu - swieca diody 1 i 4, pozostale zgaszone

A3:

mov P1, #0EDh ; zapalenie i zgaszenie diod jedna instrukcja

sjmp A1

;****************************************************************************

; Jest przesuw - testowanie kierunku przesuwu

A2:

jb W2, A4

;----------------------------------------------------------------------------

; Przesuw w prawo

A5:

mov A, P1

rr A

mov P1, A

sjmp A6

;----------------------------------------------------------------------------

; Przesuw w lewo

A4:

mov A, P1

rl A

mov P1, A

;****************************************************************************

; Blok opozniajacy - ok. 0.5 sek.

;----------------------------------------------------------------------------

A6:

mov B, #MNOZNIK_OPOZN

Opoznienie:

jnb TF1, Opoznienie

clr TF1

djnz B, Opoznienie

;----------------------------------------------------------------------------

; Wejscie w to miejsce w programie co ok. 0.5 sekundy - koniec opoznienia

;----------------------------------------------------------------------------

sjmp A1

; Koniec programu

end ; ta instrukcja ZAWSZE konieczna na koncu programu

;----------------------------------------------------------------------------

*) Instrukcja anl TMOD, #0FH, zerująca cztery bardziej znaczące bity rejestru TMOD (w celu uzyskania wymaganego skonfigurowania licznika T1 te bity muszą być wyzerowane) bez naruszania stanu czterech mniej znaczących bitów tego rejestru (dotyczą licznika T0, a w nim tutaj nic nie zmieniamy) nie jest tu konieczna, gdyż po załączeniu mikrokontrolera wszystkie bity rejestru TMOD (jak i większości innych rejestrów) są wyzerowane. Jednak można ją tu zamieścić dla porządku, gdyż w bardziej złożonym programie może ona okazać się konieczna, ponieważ np. licznik T1 może być we wcześniejszym fragmencie programu skonfigurowany w inny sposób, a licznik T0 też może być równocześnie wykorzystywany do innych celów.

33. Procedury użytkownika

Często bywa tak, że jakiś fragment programu powtarza się wielokrotnie lub stanowi pewną całość. Wtedy można go zapisać w postaci procedury, a następnie w odpowiednich miejscach tę procedurę wywoływać.

0x08 graphic
Poniższy rysunek przedstawia sytuację, gdy fragment E programu powtarza się trzykrotnie.

0x08 graphic

Innym sposobem realizacji tego programu jest zapisanie fragmentu E jako procedury, a następnie jej wywoływanie wymaganą ilość razy, co skraca kod źródłowy programu. Omawianą sytuację ilustruje poniższy rysunek.

0x08 graphic
0x08 graphic

W przedstawionym powyżej przypadku następuje trzykrotne wywołanie procedury znajdującej się w oddzielnym fragmencie programu E. W ogólnym przypadku każdą procedurę można wywoływać dowolną ilość razy. Do wywołania procedury wykorzystuje się instrukcję lcall Etykieta, gdzie Etykieta (może to być dowolna nazwa dopuszczana przez makroasembler) oznacza pierwszą instrukcję procedury. Procedura użytkownika musi kończyć się instrukcją ret, która oznacza, że należy wrócić do tego miejsca w programie, z którego wywołano procedurę i wykonać instrukcję znajdującą się bezpośrednio za instrukcją lcall.

W ogólnym przypadku korzystanie z procedury wygląda więc następująco:

lcall Etykieta ; wywołanie procedury

...................

Etykieta:

...................... ; zawartość procedury

......................

ret

Poniżej przedstawiono listing programu przes_d.s03, będącego modyfikacją programu przes_c.s03. W przedstawionym programie instrukcje realizujące opóźnienie przy wykorzystaniu licznika T1 tworzą procedurę, której początek wyznacza etykieta Opoznienie.

Aby w demonstrowanym przykładzie uzyskać opóźnienie n * 8 ms, należy przed wywołaniem procedury wpisać mnożnik opóźnienia n do rejestru B:

mov B, #n

lcall Opoznienie

W programie zrealizowano to następująco:

MNOZNIK_OPOZN equ 120

...................

mov B, # MNOZNIK_OPOZN

lcall Opoznienie

co daje opóźnienie ok. 1 s („przesuw” świecenia diod następuje co ok. 1 s).

Natomiast sama procedura generująca opóźnienie ma postać:

Opoznienie:

jnb TF1, Opoznienie

clr TF1

djnz B, Opoznienie

ret

czyli tworzą ją te same instrukcje, których użyto w programie przes_c.s03. Procedury zwykle umieszcza się na końcu programu, jak w programie przes_d.s03.

Po zakończeniu wykonywania procedury następuje powrót do instrukcji znajdującej się bezpośrednio za instrukcją wywołującą procedurę (lcall ...). Adres powrotu zapamiętywany jest na stosie.

Poniżej przedstawiono kod źródłowy programu w wersji przes_d.s03.

;****************************************************************************

; Program przes_d.s03

; Realizacja opoznienia przy wykorzystaniu timera i testowaniu jego znacznika

; przy czym jest to realizowane w oddzielnej procedurze

; autor: dr inz. Zbigniew Waradzyn KANiUP AGH Krakow

;****************************************************************************

; W zaleznosci od stanu wejsc P3.4 i P3.5 nastepuje:

; - stabilne swiecenie dwu diod,

; - swiecenie jednej lub dwu diod z przesuwem w prawo,

; - swiecenie jednej lub dwu diod z przesuwem w lewo.

;****************************************************************************

; Wprowadzenie stalej

MNOZNIK_OPOZN equ 120 ; opoznienie ok. 1 sekunda

;----------------------------------------------------------------------------

; Nadanie nazw zmiennym

W1 equ P3.4

W2 equ P3.5

D1 equ P1.1

D2 equ P1.2

D3 equ P1.3

D4 equ P1.4

D5 equ P1.5

D6 equ P1.6

;****************************************************************************

; Poczatek programu od adresu 0000h

;****************************************************************************

; Uruchomienie licznika 1 w trybie 0, przepelnienie co ok. 8 ms

anl TMOD, #0FH

setb TR1

;----------------------------------------------------------------------------

; Swieca diody D1 i D4, pozostale zgaszone

clr D1 ; zapalenie diody D1

clr D4 ; zapalenie diody D4

setb D2 ; zgaszenie diody D2

setb D3 ; zgaszenie diody D3

setb D5 ; zgaszenie diody D5

setb D6 ; zgaszenie diody D6

setb P1.0

setb P1.7

;****************************************************************************

; POCZATEK PETLI GLOWNEJ PROGRAMU

; Sprawdzanie, czy ma byc przesuw

A1:

jnb W1, A2

;----------------------------------------------------------------------------

; Nie ma przesuwu - swieca diody 1 i 4, pozostale zgaszone

A3:

mov P1, #0EDh ; zapalenie i zgaszenie diod jedna instrukcja

sjmp A1

;****************************************************************************

; Jest przesuw - testowanie kierunku przesuwu

A2:

jb W2, A4

;----------------------------------------------------------------------------

; Przesuw w prawo

A5:

mov A, P1

rr A

mov P1, A

sjmp A6

;----------------------------------------------------------------------------

; Przesuw w lewo

A4:

mov A, P1

rl A

mov P1, A

;****************************************************************************

; Blok opozniajacy - ok. 1 sek.

;----------------------------------------------------------------------------

A6:

mov B, #MNOZNIK_OPOZN

lcall Opoznienie

; Wejscie w to miejsce w programie co ok. 0.5 sekundy - koniec opoznienia

;----------------------------------------------------------------------------

sjmp A1

;****************************************************************************

; Procedura uzytkownika

;----------------------------------------------------------------------------

Opoznienie:

jnb TF1, Opoznienie

clr TF1

djnz B, Opoznienie

ret

;----------------------------------------------------------------------------

; Koniec programu

end ; ta instrukcja ZAWSZE konieczna na koncu programu

;----------------------------------------------------------------------------

34. Przerwania

Przerwanie polega na czasowym zawieszeniu aktualnie wykonywanego fragmentu programu i wykonaniu innego fragmentu programu, zwanego procedurą obsługi przerwania. Po jej wykonaniu następuje powrót do wykonywania przerwanego fragmentu programu.

0x08 graphic

Przerwania mogą nastąpić w wyniku zajścia pewnych ściśle określonych zdarzeń, tzw. źródeł przerwań. Zaleta przerwań polega na tym, że program nie musi w każdej pętli sprawdzać warunku, po spełnieniu którego (np. po zmianie stanu sygnału cyfrowego, naciśnięciu klawisza, upływie zadanego czasu, itp.) należy wykonać pewien ciąg instrukcji. Warunek testowany jest sprzętowo i po jego spełnieniu następuje natychmiastowy skok w to miejsce w programie, gdzie ten ciąg instrukcji jest umieszczony (procedura obsługi przerwania).

Podstawowa wersja mikrokontrolera posiada 5 źródeł przerwań (rys. 34.1):

Z przerwaniami związane są 3 rejestry z obszaru rejestrów specjalnych SFR: IE, IP oraz TCON. Są one przedstawione poniżej.

globalne

port szer.

T1

INT1\

T0

INT0\

IE

EA

-

ES

ET1

EX1

ET0

EX0

A8H

MSB

LSB

IP

-

-

PS

PT1

PX1

PT0

PX0

B8H

MSB

LSB

TCON

TF1

TF0

IE1

IT1

IE0

IT0

88H

MSB

LSB

0x01 graphic

Rejestr IE zawiera bity służące do indywidualnego uaktywniania przerwań i bit EA globalnego uaktywnienia.

Rejestr IP zawiera bity służące do ustalania priorytetu przerwań (nazwy bitów jak w rejestrze IE z tym, że zamiast pierwszej litery E jest litera P),

Rejestr TCON zawiera znaczniki przerwań (TF0 i TF1 - przerwania od układu czasowo-licznikowego, IE0 i IE1 - przerwania od sygnałów zewnętrznych) oraz znaczniki IT0 i IT1 określające sposób zgłoszenia przerwań zewnętrznych (opadającym zboczem lub niskim poziomem sygnału zewnętrznego). Pozostałe dwa bity nie dotyczą systemu przerwań.

Wszystkie trzy przedstawione wyżej rejestry są adresowane bitowo.

Po załączeniu mikrokontrolera lub jego zresetowaniu (wyzerowaniu) wszystkie powyższe bity są wyzerowane, w konsekwencji - wszystkie przerwania są zamaskowane (nieaktywne).

Warunkiem uaktywnienia przerwania jest:

- ustawienie bitu odpowiadającego danemu przerwaniu (indywidualne uaktywnienia przerwania),

- ustawienie bitu globalnego uaktywnienia przerwań EA.

Przykładowo celem uaktywnienia przerwania zewnętrznego INT0\ należy ustawić bity EX0 i EA (rejestr IE) wykonując na przykład instrukcje:

setb EX0

setb EA ; pozostałe bity rejestru IE bez zmiany

lub

mov IE, #1000 0001b ; pozostałe bity rejestru IE wyzerowane

lub też jeszcze inaczej

orl IE, #1000 0001b ; pozostałe bity rejestru IE bez zmiany

Chcąc uruchomić w dalszej części programu przerwanie od licznika T1 wystarczy ustawić bit ET1 (jeśli EA jest już ustawiony). Wyzerowanie bitu EA powoduje zablokowanie (zamaskowanie) wszystkich indywidualnie uaktywnionych przerwań, zaś jego ponowne ustawienie - ich uaktywnienie.

Uwaga: Należy rozróżnić oznaczenia IE0 oraz IE.0. IE0 to bit 1 rejestru TCON, czyli TCON.1. Natomiast IE.0 to bit 0 rejestru IE, bit ten jest oznaczany przez EX0.

Przerwania INT0\ i INT1\ mogą nastąpić przy zerowym poziomie sygnału na wejściu przerwania (bit IT0 lub IT1 wyzerowany - stan domyślny) lub przy opadającym zboczu sygnału (bit IT0 lub IT1 ustawiony).

W chwili wystąpienia sytuacji powodującej przerwanie ustawiony zostaje sprzętowo znacznik przerwania (przykładowo bit IE0 - przerwanie INT0\, bit TF1 - przerwanie od T1). Po przyjęciu przerwania znacznik zostaje wyzerowany automatycznie (nie dotyczy to znaczników przerwania od portu szeregowego).

Uwaga1: Bity TF0 (licznik T0) i TF1 (licznik T1) pełnią podwójną rolę. Po pierwsze są znacznikami przepełnienia liczników odpowiednio T0 i T1 (p. 32) - są więc ustawiane po każdorazowym przepełnieniu licznika, także wtedy, gdy przerwanie od przepełnienia licznika jest zamaskowane. Dodatkowo, jeśli przerwanie od licznika jest odblokowane (aktywne) ustawienie bitu powoduje wystąpienie przerwania, czyli znaczniki te pełnią wtedy rolę znaczników przerwań.

Uwaga2: Należy wiedzieć jeszcze jedno:

Po wystąpieniu przerwania następuje skok do odpowiedniego, ściśle określonego miejsca w pamięci programu gdzie rozpoczyna się procedura obsługi przerwania, zaś po zakończeniu jej wykonywania - powrót do tego miejsca w programie, przy wykonywaniu którego nastąpiło przerwanie. Przed rozpoczęciem wykonywania procedury obsługi przerwania trzeba więc zapamiętać adres powrotu - jest on zapamiętywany na stosie. Zapamiętywanie tego adresu na stosie, a potem ponowne jego przysłanie po licznika rozkazów PC odbywa się automatycznie. Poniżej podano adresy skoku po przyjęciu przerwania (od tych właśnie adresów muszą zaczynać się procedury obsługi przerwań):

Przerwanie

adres skoku

INT0\

0003H

T0

000BH

INT1\

0013H

T1

001BH

port szeregowy

0023H

Przypomnienie: Po załączeniu lub zresetowaniu mikrokontrolera realizacja programu zaczyna się zawsze od adresu 0000H.

Priorytet przerwań:

Przykład wykorzystania przerwań

Na rys. 34.2 podany jest algorytm programu przes_e.s03 realizującego świecenie diod z przesuwem oraz wykorzystującego do realizacji opóźnienia przerwania od przepełnienia licznika.

Program składa się z dwu części, każda jest przedstawiona w postaci oddzielnego algorytmu:

  1. program główny zaczynający się od etykiety Init, działający w zamkniętej pętli,

  2. procedura obsługi przerwania zaczynająca się od etykiety PrzerwT1, a kończąca instrukcją reti.

Po załączeniu mikrokontrolera realizacja programu rozpoczyna się od adresu 0000h. Ponieważ w programie korzystamy z procedury obsługi przerwania od przepełnienia licznika T1, zaczynającej się od adresu 001Bh, pisanie programu głównego w kolejnych komórkach począwszy od adresu 0000h, mogłoby spowodować kolizję - pod adresem 001BH mógłby znaleźć się kod lub argument jakiejś instrukcji z programu głównego, a nie kod pierwszej instrukcji procedury obsługi przerwania. Aby zapobiec takiej kolizji, umieszczono w programie następujące wiersze:

0x08 graphic

0x08 graphic

0x08 graphic

;**************************************************************************

; POCZATEK PROGRAMU

;--------------------------------------------------------------------------

org 0000h ; od adresu 0000h zaczyna sie realizacja programu

; po zalaczenie lub zresetowaniu mikrokontrolera

ljmp Init

org 001Bh ; od adresu 001Bh zaczyna sie realizacja procedury obslugi

; przerwania od przepelnienia licznika T1

ljmp PrzerwT1

;--------------------------------------------------------------------------

Init:

...................................................................................................................................................................

PrzerwT1:

...................................................................................................................................................................

end

Dyrektywa makroasemblera org adres (nie jest to instrukcja mikrokontrolera 8051, zaś o dyrektywach makroasemblera wspominano już w p.21.4) oznacza, że następujący po niej ciąg instrukcji zostanie umieszczony w pamięci programu począwszy od podanego adresu. Rysunek 34.3 przedstawia szkic rozmieszczenia powyższego programu w pamięci programu.

0x08 graphic

  1. Po załączeniu lub wyzerowaniu mikrokontrolera następuje skok (instrukcja ljmp Init - adresy 0000h ... 0002h) do komórki o adresie 001Eh (etykieta Init), od którego zaczyna się program główny.

  2. Po wystąpieniu przerwania od timera T1, realizowany jest program począwszy od adresu 001Bh (rys. 34.1). Instrukcja ljmp PrzerwT1 (adresy 001Bh ... 001Dh) powoduje skok do adresu wskazywanego przez etykietę PrzerwT1, oznaczonego u nas przez AdrT1, od którego rozpoczyna się procedura obsługi przerwania. Procedura ta jest realizowana, aż do napotkania instrukcji reti.

  3. Program główny (adres początkowy 001Eh - etykieta Init) ulokowany jest bezpośrednio za instrukcją ljmp PrzerwT1, zgodnie z kolejnością instrukcji w programie.

  4. Procedura obsługi przerwania (etykieta PrzerwT1) umieszczona jest bezpośrednio za programem głównym. Adres początkowy oznaczony u nas przez AdrT1 zależy od długości programu głównego.

Gdyby w programie korzystano jeszcze dodatkowo z przerwań od sygnału zewnętrznego podawanego na wejście INT0\, to szkielet naszego programu mógłby wyglądać tak:

;**************************************************************************

; POCZATEK PROGRAMU

;--------------------------------------------------------------------------

org 0000h ; od adresu 0000h zaczyna sie realizacja programu

; po zalaczenie lub zresetowaniu mikrokontrolera

ljmp Init

org 0003h ; od adresu 0003h zaczyna sie realizacja procedury obslugi

; przerwania od sygnalu zewnetrznego podanego na we INT0\

ljmp PrzerwINT0:

org 001Bh ; od adresu 001Bh zaczyna sie realizacja procedury obslugi

; przerwania od przepelnienia licznika T1

ljmp PrzerwT1

;--------------------------------------------------------------------------

Init:

...................................................................................................................................................................

PrzerwINT0:

...................................................................................................................................................................

PrzerwT1:

...................................................................................................................................................................

end

Poniżej przedstawiono kod źródłowy programu przes_e, którego algorytm zamieszczono na rys. 34.2.

;**************************************************************************

; Program przes_e.s03

; autor: dr inz. Zbigniew Waradzyn KANiUP AGH Krakow

;**************************************************************************

; W zaleznosci od stanu wejsc P3.4 i P3.5 nastepuje:

; - stabilne swiecenie dwu diod,

; - swiecenie jednej lub dwu diod z przesuwem w prawo,

; - swiecenie jednej lub dwu diod z przesuwem w lewo.

; Generacja opoznienia potrzebnego do uwidocznienia zmiany swiecenia diod

; realizowana jest tym programie w przerwaniu.

; Dzieki temu program glowny nie jest "zaangazowany" przy realizacji

; opoznienia i moze "zajac sie" czyms innym.

;--------------------------------------------------------------------------

; Wprowadzenie stalej

MNOZNIK_OPOZN equ 240 ; przesuw tym razem co 2 sekundy

;--------------------------------------------------------------------------

; Nadanie nazw zmiennym

W1 equ P3.4

W2 equ P3.5

D1 equ P1.1

D2 equ P1.2

D3 equ P1.3

D4 equ P1.4

D5 equ P1.5

D6 equ P1.6

;**************************************************************************

; POCZATEK PROGRAMU GLOWNEGO

;--------------------------------------------------------------------------

org 0000h ; od adresu 0000h zaczyna sie realizacja programu

; po zalaczenie lub zresetowaniu mikrokontrolera

ljmp Init

org 001Bh ; od adresu 0000h zaczyna sie realizacja procedury obslugi

; przerwania od przepelnienia licznika T1

ljmp PrzerwT1

;--------------------------------------------------------------------------

; Instrukcje inicjalizacyjne

Init:

setb TR1 ; uruchomienie licznika 1 w trybie 0, przepeln. co ok. 8 ms

setb EA ; globalne odblokowanie przerwan

mov B, #MNOZNIK_OPOZN ; dotyczy pierwszego opoznienia

;--------------------------------------------------------------------------

; Swieca diody D1 i D4, pozostale zgaszone

clr D1 ; zapalenie diody D1

clr D4 ; zapalenie diody D4

setb D2 ; zgaszenie diody D2

setb D3 ; zgaszenie diody D3

setb D5 ; zgaszenie diody D5

setb D6 ; zgaszenie diody D6

setb P1.0

setb P1.7

;**************************************************************************

; POCZATEK PETLI GLOWNEJ PROGRAMU

; Sprawdzanie, czy ma byc przesuw

A1:

jnb W1, A2

;--------------------------------------------------------------------------

; Nie ma przesuwu - swieca diody 1 i 4, pozostale zgaszone

; Przerwania od timera T1 sa niepotrzebne

A3:

clr ET1 ; zamaskowanie przerwan od przepelnienia T1

mov P1, #0EDh ; zapalenie i zgaszenie diod jedna instrukcja

sjmp A1

;--------------------------------------------------------------------------

;**************************************************************************

; Ma byc przesuw - nalezy uruchomic przerwania od przepelnienia T1

; Sprawdzanie, czy uplynal czas opoznienia, testowanie kierunku przesuwu

; oraz jego realizacja odbywaja sie w procedurze obslugi przerwania

A2:

setb ET1 ; zdjecie maski przerwan od przepelnienia T1

sjmp A1

; KONIEC PROGRAMU GLOWNEGO

;--------------------------------------------------------------------------

;**************************************************************************

; PROCEDURA OBSLUGI PRZERWANIA OD PRZEPELNIENIA LICZNIKA T1

; Procedura wykonywana co ok. 8 ms, jesli ma byc przesuw:

; - sprawdzanie, czy uplynal czas opoznienia

; - testowanie kierunku przesuwu

; - realizacja przesuwu

;--------------------------------------------------------------------------

PrzerwT1:

push ACC

djnz B, PrzerwT1End

; Ponizsze inmstrukcje wykonywane po zakonczeniu opoznienia

mov B, #MNOZNIK_OPOZN ; dotyczy nastepnego opoznienia

; Testowanie kierunku przesuwu

jb W2, A4

;--------------------------------------------------------------------------

; Przesuw w prawo

A5:

mov A, P1

rr A

mov P1, A

sjmp PrzerwT1End

;--------------------------------------------------------------------------

; Przesuw w lewo

A4:

mov A, P1

rl A

mov P1, A

;--------------------------------------------------------------------------

PrzerwT1End:

pop ACC

reti

;--------------------------------------------------------------------------

; Koniec programu

end ; ta instrukcja ZAWSZE konieczna na koncu programu

;--------------------------------------------------------------------------

Z przedstawionego programu wynika z tego, że:

  1. po załączeniu mikrokontrolera wykonywana jest instrukcja zaczynająca się od komórki pamięci o adresie 0000H i w wyniku jej realizacji wykonywany jest skok bezwarunkowy do instrukcji o etykiecie Init,

  2. po wystąpieniu przerwania od przepełnienia licznika T1 wykonywana jest instrukcja zaczynająca się od komórki pamięci o adresie 001BH i w wyniku jej realizacji wykonywany jest skok bezwarunkowy do instrukcji o etykiecie PrzerwT1.

W części inicjalizacyjnej programu głównego (począwszy od miejsca wskazanego etykietą Init) następuje załączenie licznika T1 w trybie 0 (licznik 13-bitowy), co powoduje, że przy zastosowaniu generatora zegarowego o częstotliwości 12 MHz licznik będzie się przepełniał co ok. 8 ms. Następnie dokonywane jest globalne uaktywnienie przerwań, wpisanie do rejestru B odpowiedniej wartości gwarantującej realizację pierwszego opóźnienia oraz wstępne zapalenie i zgaszenie odpowiednich diod.

Uwaga: Czas opóźnienia jest tutaj, tak samo jak w poprzednich przykładach, iloczynem wartości wpisywanej do rejestru B (MNOZNIK_OPOZN) i czasu 8 ms.

Proszę zwrócić uwagę, jak krótka jest główna pętla programu. W zależności od stanu styku W1, decydującego o tym, czy ma być przesuw, czy nie, realizowane jest uaktywnienie (zdjęcie maski) lub zablokowanie (zamaskowanie) przerwania od licznika T1. Program główny nie zajmuje się więc generacją opóźnienia, jak we wcześniej demonstrowanych wersjach i może „zająć się” czymś innym.

Uwaga: W przypadku wykorzystania przerwań program możliwa jest taka struktura programu, że program główny nie „robi” praktycznie nic, a całe działanie odbywa się podczas wykonywania procedur obsługi przerwań. Nie należy jednak zapominać, że także w takim przypadku program główny musi pracować w zamkniętej pętli.

Realizacja opóźnienia oraz wybór kierunku przesuwu odbywają się w procedurze obsługi przerwania od licznika T1 (etykieta PrzerwT1), która przy aktywnym przerwaniu wykonywana jest po każdym przepełnieniu licznika T1, czyli co ok. 8 ms. Procedura obsługi przerwania kończy się instrukcją reti.

Często bardzo ważne jest odłożenie na stosie zawartości akumulatora przed rozpoczęciem wykonywania procedury obsługi przerwania (push ACC) oraz jego odtworzenie na końcu procedury (pop ACC). Wynika to stąd, że zawartość akumulatora jest zmieniana w trakcie realizacji procedury obsługi przerwania i jeśli program główny korzysta z akumulatora może to spowodować błędne działanie programu. Przedstawia to poniższy przykład. Załóżmy, że w programie głównym znajdują się instrukcje:

; ....

mov A, P1

rl A

;

Podczas wykonywania programu przerwanie może mieć miejsce w dowolnej chwili. Jeśli więc przerwanie nastąpi podczas wykonywania instrukcji mov A, P1, to procedura obsługi przerwania zostania wykonana natychmiast po jej zakończeniu, czyli po pomiędzy podanymi wyżej instrukcjami. Jeśli procedura obsługi przerwania zmienia zawartość akumulatora, to jego stan tuż przed wykonaniem instrukcji rl A wcale nie będzie równy zawartości portu P1, jak wynika z programu, co spowoduje niewłaściwą pracę układu. Zapobiegnie temu omówione wyżej zapamiętanie na stosie zawartości akumulatora zrealizowane na samym początku procedury i jej odtworzenie tuż przed zakończeniem wykonywania procedury obsługi przerwania.

W przedstawionym programie przes_e program główny nie korzysta z akumulatora, więc odkładanie go na stosie nie jest konieczne. Jednak w większości przypadków trzeba to zrobić. Ogólna zasada jest taka, że jeśli procedura obsługi przerwania zmienia stan jakiejś komórki, która w tej procedurze wykorzystywana jest jako zmienna pomocnicza, a komórka ta wykorzystywana jest też w programie głównym, to trzeba ją odłożyć na stos. Najczęściej dotyczy to akumulatora ACC i rejestru PSW (zawiera m. in. znacznik CY), stąd w bardzo wielu procedurach obsługi przerwania należy odłożyć je na stos, co zwykle wygląda tak:

ProcObslPrzerw: ; procedura obslugi przerwania

push AC ; zapamietanie na stosie

push PSW

.... ; zawartość procedury

pop PSW ; odtworzenie zawartosci poczatkowej

pop ACC

reti

Należy pamiętać, że zdejmowanie ze stosu musi odbywać się w kolejności odwrotnej, niż odkładanie na niego.

35. Porównanie procedury użytkownika i procedury obsługi przerwania

W p. 33 omówiono procedury użytkownika, czyli fragmenty programu stanowiące pewną zamkniętą całość, wywoływane instrukcją lcall ... . W p. 34, przy omawianiu przerwań, opisano procedury obsługi przerwania. Poniżej zamieszczono porównanie obu typów procedur.

Podobieństwa

  1. każda z wymienionych procedur stanowi ciąg instrukcji zajmujący pewien fragment pamięci programu,

  2. każda z wymienionych procedur może być wywoływana wielokrotnie,

  3. adres powrotu (adres instrukcji, do której należy przejść po zakończeniu wykonywania procedury) zapamiętywany jest na stosie i automatycznie ładowany do licznika rozkazów PC po zakończeniu wykonywania procedury.

Różnice

Procedura użytkownika

Procedura obsługi przerwania

może zaczynać się prawie w dowolnym miejscu w pamięci programu - na jej początek wskazuje etykieta, będąca argumentem instrukcji wywołującej lcall

zaczyna się w ściśle określonym miejscu w pamięci programu

kończy się instrukcją ret

kończy się instrukcją reti

wywoływana jest z miejsc programu wybranych przez programistę (instrukcja lcall)

wywołana może być z dowolnego miejsca w programie w wyniku zajścia zdarzenia generującego przerwanie

36. Prosty program do testowania współpracy mikrokontrolera z przetwornikami A/C i C/A

W niniejszym punkcie zostanie przedstawiony i omówiony prosty program ad_da0.s03 testujący poprawną współpracę przetworników A/C i C/A z mikrokontrolerem 8051. Założono przy tym, że zastosowano przetworniki oraz układ połączeń przedstawiony w p. 28 i 29 (układ „pięterek”).

Działanie programu jest bardzo proste: odczytywany jest stan sygnału analogowego podawanego na wejście IN0 przetwornika A/C, przetworzony sygnał analogowy w postaci cyfrowej przekazywany jest do mikrokontrolera, który natychmiast odsyła go do przetwornika C/A. Sygnał na wyjściu przetwornika C/A jest więc proporcjonalny do sygnału podawanego na wejście przetwornika A/C.

Poniżej przedstawiono kod źródłowy programu ad_da0.s03.

;================================================================

; Program testujacy przetwornik AD i DA

;.................................................................

;Zalozenia :

; AD mierzy wejscie analogowe i przetwarza go na sygnal cyfrowy, ktory

; jest nastepnie przetwarzany przez DA na sygnal analogowy

;==================================================================

;ADRESY PRZETWORNIKOW

;AD = 6000h DA = 8000h

;------------------------------------------------------------------

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

;Inicjalizacja przerwan procesora

;+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

org 0000h

ljmp init

org 0013h

ljmp intpt1

;--------------------------------------------------------------------

;Inicjalizacja

;--------------------------------------------------------------------

init:

mov TCON,#04h ;przerwanie zboczem opadajacym

mov IE,#04h ;odblokowanie przerwania INT1

mov DPTR,#6000h ;adres AD

movx @DPTR,A ;pierwsze uruchomienie przetwornika AD

setb EA ;odblokowanie przerwan

;=======================================================================

;Glowna petla programu

;=======================================================================

main: sjmp main

;=======================================================================

;Obsluga przerwania

;=======================================================================

;.......................................................................

;Przerwanie zewnetrzne INT1\ -----------------

;-przetwornik generuje przerwanie gdy zakonczy przetwarzanie

;-wynik z przetwornika zostaje odczytany do ACC

;-przetwornik AD zostaje uruchomiony kolejny raz

;-wynik z AD zostaje wyslany na przetwornik DA

;.......................................................................

intpt1:

mov DPTR,#6000h

movx A,@DPTR ;odczyt wyniku z AD

movx @DPTR,A ;uruchomienie AD

mov DPTR,#8000h ;adres DA

movx @DPTR,A ;wyslanie wyniku AD na DA

reti

;=======================================================================

end

Program składa się z dwu części: programu głównego i procedury obsługi przerwania od sygnału zewnętrznego podanego na wejście INT1\ mikrokontrolera. Przerwanie to jest generowane przez przetwornik A/C po zakończeniu przetwarzania sygnału (p. 28.3).

  1. Po załączeniu mikrokontrolera realizacja programu rozpoczyna się od wykonania instrukcji, której kod znajduje się w komórce pamięci programu o adresie 0000H. Tą instrukcją jest skok bezwarunkowy do adresu oznaczonego etykietą init. Ten skok jest wykonywany po to, aby „przeskoczyć” adres 0013H, od którego zaczyna się wykorzystywana w tym programie procedura obsługi przerwania od sygnału zewnętrznego podanego na wejście INT1\ (końcówka 13) mikrokontrolera. W przeciwnym wypadku moglibyśmy wpisać coś niewłaściwego do komórki 0013H i następnych. Temat ten został omówiony w p. 34.

  2. od adresu oznaczonego etykietą init zaczynają się instrukcje inicjalizujące pracę układu:

  • po wykonaniu instrukcji inicjalizujących pracę układu program działa w nieskończonej pętli, z której nie ma wyjścia (main: sjmp main). Wszystkie operacje obsługi przetworników wykonywane są w procedurze obsługi przerwania INT1\.

  • po zakończeniu przetwarzania przetwornik A/C generuje na swoim wyjściu EOC impuls, który po zanegowaniu podawany jest na wejście przerwań INT1\ mikrokontrolera (rys. 28.3). Po przyjęciu przerwania mikrokontroler „wyskakuje” ze swojej nieskończonej pętli i wykonuje instrukcje począwszy od adresu 0013H. Pierwszą instrukcją jest skok bezwarunkowy do adresu oznaczonego etykietą intpt1.

  • od adresu oznaczonego etykietą intpt1 rozpoczyna się właściwa procedura obsługi przerwania. W jej ramach wykonywane są następujące czynności: