AVR-GCC_cz7.pdf
(
289 KB
)
Pobierz
ep_09_097-099_avr_gcc_cz7.indd
K U R S
AVR–GCC: kompilator C
mikrokontrolerów AVR,
część 7
Obsługa przerwań
Przechodzimy do omówienia obsługi przerwań
za pomocą programów napisanych w AVR–GCC.
Jak się okazuje, jest to bardzo skuteczne
narzędzie do ich obsługi.
AVR–GCC pozwala na skuteczną
kontrolę obsługi sprzętowych prze-
rwań. Jest jednak kilka szczegółów
często sprawiających na początku kło-
poty – zobaczymy jak sobie z nimi
poradzić. Na wstępie dla krótkiego
przypomnienia spójrzmy na
rys. 18
,
na którym skrótowo pokazano typowy
przebieg operacji podczas przerwania.
Wystąpienie sprzętowego prze-
rwania powoduje kolejno:
– zapisanie na stosie stanu licznika
rozkazów PC (zauważmy, że stos
musi być wcześniej prawidłowo
zainicjalizowany – AVR–GCC robi
to automatycznie, musimy jedy-
nie uważać w przypadku ATmega
128 aby wyłączyć zaprogramowa-
ny fabrycznie
fuse bit
kompaty-
bilności z ATmega 103; w trybie
kompatybilności ustawiany przez
kompilator na adres RAMEND
początek stosu jest fizycznie nie-
dostępny gdyż ATmega 103 ma
mniejszy RAM),
– zablokowanie wszelkich następ-
nych przerwań,
– skok programu do odpowiedniej
dla przerwania pozycji w tablicy
wektorów,
– na pozycji tej musi być wpisana
instrukcja skoku do właściwej pro-
cedury obsługi przerwania,
– zakończenie obsługi instrukcją
reti
powoduje odblokowanie przerwań
i przywrócenie ze stosu stanu licz-
nika PC (czyli wznowienie wy-
konywania programu od miejsca,
w którym wystąpiło przerwanie).
Część z tych operacji jest auto-
matyczna ale napisanie odpowiedniej
procedury obsługi i wprowadzenie jej
adresu do tablicy wektorów spoczy-
wa na programiście. Popatrzmy jakie
wsparcie oferuje w tym zakresie AVR–
–GCC.
Korzystając z już omówionych ele-
mentów rozpocznijmy w subfolderze
[Przyklad–04] nowy projekt
test04
zawierający na początek pojedyn-
czy plik
main.c
z szablonem pro-
gramu głównego. Po skompilowaniu
zajrzyjmy jeszcze raz (było to już
wstępnie omawiane) do wygenerowa-
nego kodu assemblera (
CTRL+F7
).
Na początku znajdziemy wektory
przerwań, które na razie oczywiście
nie wskazują na żadne konkretne pro-
cedury i ograniczają się do skoku pod
wspólny adres
__bad_interrupt
obsługi
błędnego przerwania:
test04.elf: file format elf32–avr
Disassembly of section.text:
00000000 <__vectors>:
0: 12 c0 rjmp .+36 ; 0x26
2: 2b c0 rjmp .+86 ; 0x5a
4: 2a c0 rjmp .+84 ; 0x5a
6: 29 c0 rjmp .+82 ; 0x5a
8: 28 c0 rjmp .+80 ; 0x5a
a: 27 c0 rjmp .+78 ; 0x5a
c: 26 c0 rjmp .+76 ; 0x5a
e: 25 c0 rjmp .+74 ; 0x5a
10: 24 c0 rjmp .+72 ; 0x5a
12: 23 c0 rjmp .+70 ; 0x5a
14: 22 c0 rjmp .+68 ; 0x5a
16: 21 c0 rjmp .+66 ; 0x5a
18: 20 c0 rjmp .+64 ; 0x5a
1a: 1f c0 rjmp .+62 ; 0x5a
1c: 1e c0 rjmp .+60 ; 0x5a
1e: 1d c0 rjmp .+58 ; 0x5a
20: 1c c0 rjmp .+56 ; 0x5a
22: 1b c0 rjmp .+54 ; 0x5a
24: 1a c0 rjmp .+52 ; 0x5a
[....]
0000005a <__bad_interrupt>:
5a: d2 cf rjmp .–92 ; 0x0
AVR–GCC oferuje makra, które au-
tomatyzują proces tworzenia procedur
obsługi przerwań (
handlerów
): SIGNAL
(signame) oraz INTERRUPT (signame)
(znajdziemy je w pliku nagłówkowym
signal.h
).
Signame
jest nazwą potrzeb-
nego wektora. Generalnie nazwa ta
może mieć uniwersalną postać _VEC-
TOR (numer przerwania). Jednak jest
to mało czytelne i dlatego plik nagłów-
kowy
ioxxx.h
dla danego typu kostki
zawiera dużo łatwiejsze w użyciu na-
zwy opisowe, np. w
io8.h
(ATmega 8)
znajdziemy następujące definicje:
#define SIG_INTERRUPT0 _VECTOR(1)
#define SIG_INTERRUPT1 _VECTOR(2)
#define SIG_OUTPUT_COMPARE2 _VECTOR(3)
#define SIG_OVERFLOW2 _VECTOR(4)
#define SIG_INPUT_CAPTURE1_VECTOR(5)
#define SIG_OUTPUT_COMPARE1A _VECTOR(6)
#define SIG_OUTPUT_COMPARE1B _VECTOR(7)
#define SIG_OVERFLOW1 _VECTOR(8)
#define SIG_OVERFLOW0 _VECTOR(9)
#define SIG_SPI _VECTOR(10)
#define SIG_UART_RECV _VECTOR(11)
#define SIG_UART_DATA _VECTOR(12)
#define SIG_UART_TRANS _VECTOR(13)
#define SIG_ADC _VECTOR(14)
#define SIG_EEPROM_READY _VECTOR(15)
#define SIG_COMPARATOR _VECTOR(16)
#define SIG_2WIRE_SERIAL _VECTOR(17)
#define SIG_SPM_READY _VECTOR(18)
Wystarczy zdefiniować potrzebne
makro, aby kompilator wygenerował
podstawowy szablon kodu obsługi
oraz umieścił w tablicy wektorów od-
powiedni skok. Wypróbujmy to zaraz
dopisując w naszym
main.c
obsługę
np. dla pierwszego z brzegu przerwa-
nia zewnętrznego INT0, która na ra-
zie nie robi nic konkretnego (
SIGNAL
(SIG_INTERRUPT0) {}
;). Koniecznie
musimy też dołączyć nagłówki
avr/
signal.h
oraz
avr/io.h
(zwłaszcza brak
signal.h
może wprawić w zakłopotanie
kilkoma mało czytelnymi w pierwszej
chwili ostrzeżeniami). Wygenerowany
kod wygląda następująco:
0000005c <__vector_1>:
SIGNAL (SIG_INTERRUPT0)
{
5c: 1f 92 push r1
5e: 0f 92 push r0
60: 0f b6 in r0, 0x3f ; 63
62: 0f 92 push r0
64: 11 24 eor r1, r1
6e: 0f 90 pop r0
70: 0f be out 0x3f, r0 ; 63
72: 0f 90 pop r0
74: 1f 90 pop r1
76: 18 95 reti
}
W prologu obsługi znajdujemy
zapamiętanie na stosie wykorzysty-
wanych przez kompilator rejestrów
r0
(rejestr tymczasowy
__tmp_reg__
)
oraz
r1
(rejestr zerowy
__zero_reg__
).
Następnie zachowany zostaje rejestr
stanu SREG (0x3f) a rejestr
r1
zostaje
wyzerowany (AVR–GCC wymaga aby
był on równy zeru przy każdym wy-
wołaniu funkcji a nie wiadomo jaka
jest jego wartość w momencie wystą-
pienia przerwania).
Zakończenie
handlera
odtwarza
poprzedni stan rejestrów oraz za-
Rys. 18. Przebieg obsługi przerwania
Elektronika Praktyczna 9/2005
97
K U R S
Rys. 19. Okienko autokompletacji
przerwania w AvrSide
jącego kodu obsługi przerwania (np.
taką niespodziankę miałem po sko-
piowaniu kodu obsługi TWI z pro-
jektu ATmega8 do ATmega88: cały
interfejs działa i jest opisany iden-
tycznie z wyjątkiem właśnie zmie-
nionej – z
SIG_2WIRE_SERIAL
na
SIG_TWI
– nazwy wektora).
Pojawia się od razu pytanie kie-
dy stosować
SIGNAL
a kiedy
INTER-
RUPT
. Zawsze będzie to oczywiście
zależeć głównie od potrzeb konkret-
nego programu, jednak można sfor-
mułować kilka podstawowych reguł:
A. W niektórych przypadkach
IN-
TERRUPT
nie możemy używać
w ogóle. Przypomnijmy sobie, że
przerwanie w AVR może być wy-
wołane zdarzeniem (ustawiającym
odpowiednią flagę we właściwym
rejestrze) – np. przepełnieniem
licznika; albo warunkiem – prze-
rwanie jest aktywne cały czas
dopóki zachodzi określona sytu-
acja – np. w rejestrze odbiornika
USART znajduje się nie odczytany
znak. Dodatkowa komplikacja to
fakt, że chociaż zazwyczaj rozpo-
częcie obsługi przerwania zdarze-
niowego powoduje w chwili skoku
do wektora przerwania samoczyn-
ne (sprzętowe) zgaszenie flagi to
jednak są od tego wyjątki. np.
przerwanie magistrali TWI (i2c).
W obu ostatnich przypadkach usu-
nięcie przyczyny przerwania (eli-
minacja warunku albo zgaszenie
flagi) musi byc wykonane progra-
mowo wewnątrz funkcji obsługi.
Na przykład dla wspomnianego
odbiornika USART będzie to od-
czyt rejestru UDR. W tych właśnie
przypadkach generowane przez
INTERRUPT
odblokowanie prze-
rwań na samym początku
han-
dlera
spowoduje natychmiastowe
wywołanie tego samego przerwa-
nia – program jeszcze nie dotarł
i nigdy nie będzie mógł dotrzeć
do fragmentu kodu wyłączającego
warunek wyzwalający (spójrzmy
jeszcze raz na rys. 18 – zaraz po
wejściu do funkcji obsługi i wyko-
naniu
sei
nastąpi ponowny skok
do wektora). Taka aplikacja nie
ma szans na poprawne działanie.
Pomyłka ta pojawia się na tyle
często, że autorzy
avr–libc
zaczęli
nawet rozważać ewentualną zmianę
wprowadzającego w błąd nazewnic-
twa – ale to na razie tylko wstęp-
ne propozycje.
B. Jak łatwo się domyśleć,
INTER-
RUPT
ma służyć do zastąpienia
nieobecnej w AVR kontroli priory-
tetu przerwań. Pozwala na obsłu-
żenie krytycznego czasowo prze-
rwania niezależnie od faktu czy
program wykonuje pętlę główną
czy też już obsługuje zgłoszone
wcześniej przerwanie o mniejszym
dla nas znaczeniu. Sprawa jest
prosta jeśli mamy do czynienia
z dwoma przerwaniami: dla pod-
rzędnego używamy makra
INTER-
RUPT
co pozwala na praktycznie
natychmiastową obsługę drugiego
– ważniejszego. Gorzej jeśli prze-
rwań jest kilka – globalne odblo-
kowanie umożliwia wykonywanie
wszystkich pozostałych co nie za-
wsze jest pożądane. W takim przy-
padku selektywne podwyższenie
priorytetu tylko jednego wybranego
przerwania wymaga każdorazowo
szczegółowego przełączania konfi-
guracji zezwoleń na poszczególne
przerwania w kodzie
handlerów
.
C. Z powyższych ograniczeń wynika,
że na ogół domyślnym sposobem
obsługi będzie
SIGNAL
, natomiast
INTERRUPT
użyjemy w specyficz-
nych przypadkach, dokładnie roz-
ważając potrzeby, korzyści i możli-
wości wystąpienia niepożądanych
efektów.
D. Ponieważ makro
SIGNAL
blokuje
wszystkie inne przerwania (zgod-
myka obsługę instrukcją
reti
.
Adres
handlera
(w tym przypad-
ku 0x5c) pojawia się samoczynnie
w tablicy wektorów:
00000000 <__vectors>:
0: 12 c0 rjmp .+36 ; 0x26
2: 2c c0 rjmp .+88 ; 0x5c
Zauważmy, że kod zachowu-
je “naturalny” dla AVR przebieg
obsługi – z wszystkimi pozostały-
mi przerwaniami zablokowanymi
do momentu wykonania instrukcji
reti
. Czasem jednak chcemy aby na
inne, krytyczne czasowo przerwania
reakcja następowała natychmiast
– wtedy musimy w naszej obsłudze
samodzielnie je ponownie włączyć.
Robi to samoczynnie drugie makro.
Sprawdźmy, że wywołanie:
INTERRUPT (SIG_INTERRUPT0) {} ;
generuje taki sam kod ale rozpo-
czynający się włączającą przerwania
instrukcją
sei
. Jednak trzeba ten spo-
sób stosować w odpowiednią uwagą
gdyż może spowodować kilka niespo-
dzianek (do czego zaraz wrócimy).
Makra
SIGNAL
oraz
INTERRUPT
zadziałają nawet w przypadku wsta-
wienia dowolnej nazwy nie odpo-
wiadającej żadnemu z rzeczywistych
wektorów przerwań. Kod zostanie
wygenerowany i umieszczony w pro-
gramie ale oczywiście kompila-
tor nie będzie mógł mu przypisać
żadnej pozycji w tablicy wektorów.
W niektórych przypadkach (o czym
za chwilę) zrobimy tak celowo.
Niestety zazwyczaj ta cecha jest ra-
czej źródłem zaskakujących błędów
wynikających np. z drobnej pomyłki
literowej w nazwie wektora albo ze
skopiowania
handlera
z programu
dla innej kostki. Kompilator nie
zgłasza w takim przypadku żadnych
wątpliwości a przerwanie pozostaje
nie obsługiwane. (W niektórych wer-
sjach AVR–GCC są zdaje się łatki
powodujące poinformowanie o wy-
stępującej rozbieżności ale general-
nie lepiej na to nie liczyć).
AvrSide wyposażono w dynamicz-
ną podpowiedź właściwych nazw
(
CTRL+L
) co pozwala wyeliminować
taki błąd (
rys. 19
) jednakże w razie
jakichś kłopotów z działaniem pro-
gramu zajrzyjmy zawsze do tablicy
wektorów i upewnijmy się czy za-
wiera ona właściwy skok do istnie-
List. 3. Plik z programem obsługi
timerów
// obsługa timerów
#include „projdat.h”
#include <avr/io.h>
#include <avr/signal.h>
volatile uchar T2_counter;
/* Licznik T2 posłuży nam jako podsta-
wowy timer systemowy, na bazie którego
będziemy
realizować cykliczne akcje, odliczać
timeouty oraz uruchomimy prototyp ze-
gara.
Wykorzystamy tryb pracy CTC – wygod-
ny ze względu na samoczynne zerowanie
licznika.
*/
void InitT2(void)
// atmega 8 pracuje z wewnętrznym
oscylatorem 8 MHz – pojedynczy cykl ma
długość 0.125 us
// (1 / 8000000)
// Jeśli ustawimy preskaler = 64 po-
jedynczy tick licznika ma 64 * 0.125
= 8 us
// Dla uzyskania przerwania licznika
co 1 ms ustawimy jego wartość przełado-
wania na 124
// (8 * (124+1) = 1000 us = 1 ms)
{
OCR2 = 124; // wartość
przeładowania w trybie CTC
TCCR2 = _BV(WGM21) | _BV(CS22); //
tryb CTC bez zewnętrznego wyjścia,
preskaler 64
TIMSK |= _BV(OCIE2); // włącze-
nie przerwania CTC
}
SIGNAL (SIG_OUTPUT_COMPARE2)
{
if (++T2_counter == 100);
{
T2_counter = 0;
MS100_FLAG = true;
}
}
98
Elektronika Praktyczna 9/2005
K U R S
nie ze sprzętowym działaniem
mikrokontrolera) zazwyczaj powin-
niśmy zadbać aby funkcje obsługi
były jak najkrótsze: nie umiesz-
czać w nich skomplikowanych
przeliczeń lub konwersji, obsługi
zewnętrznych urządzeń itp. a już
w żadnym przypadku nie wstawiać
do nich programowych pętli opóź-
niających. Takie poczynania mogą
doprowadzić do utraty jakichś in-
nych przerwań, których mikrokon-
troler nie zdąży obsłużyć.
W wielu typowych zastosowa-
niach mikrokontrolera (jak różne
transmisje, pomiary, akwizycja da-
nych itp.) dobrze sprawdza się na-
stępująca recepta:
– stosujemy wyłącznie makra SI-
GNAL,
– funkcje obsługi skracamy do nie-
zbędnego minimum i przekazu-
jemy z nich, za pośrednictwem
zmiennych logicznych lub flag
bitowych, do pętli głównej in-
formację o konieczności realizacji
czynności związanych z wystąpie-
niem przerwania,
– pętla główna sprawdza stan takich
flag awmomencie ich ustawienia
wykonuje potrzebne działania, nie-
kiedy mocno pracochłonne, bez
blokowania dostępu do przerwań.
W ten sposób żadne z przerwań
nie przejmuje sterowania na zbyt
długi czas a wszystkie zadania są wy-
konywane mniej więcej równomiernie
(zauważmy, że jest to bardzo uprosz-
czony model znanego z dużych syste-
mów programowania zdarzeniowego).
Zróbmy sobie od razu tego typu przy-
kład wykorzystujący wiele wcześniej
omawianych technik. Do projektu do-
dajmy plik
timers.c
o zawartości poka-
zanej na
list. 3
oraz plik nagłówkowy
projdat.h
gromadzący globalne zmien-
ne, deklaracje funkcji i różne definicje
(
list. 4
). Do pliku
main.c
dodamy kod
pokazany na
list. 5
.
Plik nagłówkowy
projdat.h
korzy-
sta również z ogólnego, wspólnego
nagłówka
mynames.h
zawierającego
ulubione typy i definicje (umieściłem
go w folderze
\AvrSide\Myinc
podając
odpowiednią ścieżkę w opcjach projek-
tu) –
list. 6
.
Jak widać działanie programu
sprowadza się do kilku podstawo-
wych operacji:
– inicjalizacja ustawia potrzebne nam
linie I/O (PORTB jako wyjściowy),
konfiguruje
timer
T2 (niestety nie
dopisałem jeszcze w AvrSide kre-
atora automatycznej konfiguracji
timerów
, jest ona wykonana ręcz-
nie ale została dosyć szczegółowo
opisana w komentarzu) i na koniec
uruchamia system przerwań (do-
datkowo znajdujemy tu ustawienie
kalibracji
OSCCAL
dla pracy z we-
wnętrznym oscylatorem 8 MHz,
potrzebna wartość jest przechowa-
na w ostatnim bajcie eeprom; taką
metodą posługuje się wbudowany
w AvrSide programator usb ale
każdy użytkownik prawdopodobnie
zastosuje jakiś własny sposób pa-
sujący do posiadanego sprzętu);
– 1 ms przerwanie
timera
odmie-
rza (przy pomocy dodatkowego
lokalnego programowego licznika
T2_counter
) okresy 100 ms i usta-
wia flagę bitową
MS100_FLAG
zdefiniowaną globalnie w
projdat.
h
(zwróćmy jeszcze raz uwagę na
niezbędne klasyfikatory
volatile
dla
zmiennych współużytkowanych
przez program główny oraz
han-
dler
przerwania);
– pętla główna śledzi nieustannie
stan flagi
MS100_FLAG
, po jej
ustawieniu przystępuje do wykona-
nia przypisanych fladze procedur:
– kasuje flagę (co jest konieczne
aby wykonanie było tylko jedno-
krotne);
– przy pomocy lokalnego programo-
wego licznika
Ms100_counter
od-
mierza interwał czasowy określony
stałą
MS100_DELAY
(w przykładzie
jest to 0,5 s);
– co 0,5 s przesuwa okrężnie poje-
dynczy ustawiony bit w lokalnej
zmiennej stanu portu
LedState
oraz przepisuje ją na wyjście
portu B.
Jerzy Szczesiul, EP
jerzy.szczesiul@ep.com.pl
List. 4. Listing pliku nagłówkowego
projdat.h
// plik nagłówkowy globalnych danych
projektu
#ifndef _PROJ_DAT_H_
#define _PROJ_DAT_H_
// #include:
#include „mynames.h”
// #define:
// definicje typów typedef
// dane globalne
volatile Flags SysFlags;
#define MS100_FLAG SysFlags.Bits.Flag1
#ifdef _MAIN_MOD_
// definicje danych – tylko w module
main()
// char x;
#else
// deklaracje danych jako importowanych
– w każdym innym module
// extern char x;
#endif
// deklaracje funkcji
// extern char Myfunc(int,char);
extern void InitT2(void);
#endif
List. 5. Główny moduł przykładowego
projektu
// główny moduł projektu
#define _MAIN_MOD_ 1
// pliki dołączone (include):
#include „projdat.h”
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/eeprom.h>
#include <avr/signal.h>
#include <string.h>
#define MS100_DELAY 5
// dane:
static char Ms100_counter;
static volatile uchar LedState = 1;
// funkcje:
INTERRUPT (SIG_INTERRUPT0)
{
}
//====================
// funkcja main()
int main(void)
{
// inicjalizacja
OSCCAL=eeprom_read_byte((uchar*)E-
2END); // zapis kalibracji w ostat-
niej komórce eeprom
DDRB=0xff;
List. 6. Plik nagłówkowy z deklara-
cjiami ulubionycyh typów i definicji
// ulubione oznaczenia
#ifndef _MY_NAMES_H
#define _MY_NAMES_H
#include <stdbool.h>
#define uint unsigned int
#define uchar unsigned char
#define ulong unsigned long
#define forever while(1)
#define EEPROM __attribute__ ((sec-
tion(„.eeprom”)))
#define NOINIT __attribute__ ((sec-
tion(„.noinit”)))
#define NAKED __attribute__ ((naked))
typedef struct
{
uchar Flag1:1;
uchar Flag2:1;
uchar Flag3:1;
uchar Flag4:1;
uchar Flag5:1;
uchar Flag6:1;
uchar Flag7:1;
uchar Flag8:1;
} FlagBits;
typedef union
{
FlagBits Bits;
uchar Byte;
} Flags;
#endif
InitT2();
sei();
// pętla główna
while (1)
{
if (MS100_FLAG)
{
MS100_FLAG = false;
if (++Ms100_counter == MS100_DELAY)
{
Ms100_counter = 0;
// nasza okresowa akcja (przełą-
czenie wyjścia) uruchamiana
// zegarem systemowym co 100ms *
MS100_DELAY (0,5s)
PORTB=LedState;
if(LedState==128) LedState=1;
else LedState = LedState<<1;
}
}
}
}
UWAGA!
Środowisko IDE dla AVR-GCC opracowane
przez autora artykułu można pobrać ze
strony http://avrside.ep.com.pl.
Elektronika Praktyczna 9/2005
99
Plik z chomika:
mzg1
Inne pliki z tego folderu:
AVR-GCC_cz9.pdf
(502 KB)
AVR-GCC_cz8.pdf
(617 KB)
AVR-GCC_cz7.pdf
(289 KB)
AVR-GCC_cz6.pdf
(160 KB)
AVR-GCC_cz5.pdf
(447 KB)
Inne foldery tego chomika:
• Wielka baza układów scalonych
_Katalogi
⇒ Schematy do wykrywaczy
1-wire
1-wire(1)
Zgłoś jeśli
naruszono regulamin