Rozszerzone deklaracje funkcji
Kompilator SDCC umożliwia deklarowanie funkcji z pewnymi rozszerzeniami w stosunku do standardu ANSI C, aby wykorzystać różne możliwości mikrokontrolera. Rozszerzenia te umożliwiają:
- deklarację funkcji jako procedur przerwań:
interrupt n
, gdzie n jest numerem przerwania, - wybór banku rejestrów do użycia przez funkcję:
using n
, gdzie n jest numerem banku rejestrów, - deklarację funkcji, która może być wywołana jednocześnie z dwóch lub większej liczby miejsc:
reentrant
.
Rozszerzenia te mogą być wykorzystywane samodzielnie lub w połączeniu ze sobą.
Określenie prywatnego banku rejestrów dla funkcji
Fragment wewnętrznej pamięci danych o adresach od 0 do 31 każdego z mikrokontrolerów rodziny MCS-51 zajmują 4 banki rejestrów grupowane po 8 każdy. Z poziomu programu dostępne są jako rejestry R0 do R7. Aktualnie wykorzystywany bank rejestrów jest wybierany za pomocą dwóch bitów rejestru PSW.
Domyślnie instrukcje asemblerowe, jakie kompilator SDCC przydzieli do realizacji poszczególnych linii kodu programu źródłowego, używają zawsze rejestrów banku o numerze 0. Możliwe jest jednek użycie przez daną funkcję rejestrów innego banku poprzez użycie słowa kluczowego using
. Wymaga ono jednocześnie wyspecyfikowania stałej z zakresu 0 do 3, która określa numer banku rejestru do wykorzystania przez funkcję. Atrybut using
jest również wymagany w prototypie takiej funkcji.
void NazwaFunkcji (void) using 3 { ... ... ... }
W powyższym przykładzie funkcja będzie używała trzeciego banku rejestrów. Użycie tego atrybutu powoduje:
- przed realizacją kodu funkcji zapamiętanie na stosie aktualnego banku rejestrów, co jest realizowane przez zapamiętanie rejestru PSW,
- przełączenie na wyspecyfikowany po słowie kluczowym
using
bank rejestrów, - przed wyjściem z funkcji przywrócenie poprzedniego banku rejestrów, przez odczyt ze stosu ustawień rejestru PSW.
Przełączanie banków rejestrów przy użyciu słowa kluczowego using
jest szczególnie użyteczne przy deklaracji funkcji obsługi przerwań (funkcje deklarowane z atrybutem interrupt
). Funkcje te są wywoływane niezależnie od pozostałego kodu programu, więc w chwili ich wywołania aktualna zawartość wykorzystywanych rejestrów musi być zapamiętana, ponieważ funkcja przerwania może je zmienić. Zamiast zapamiętywać na stosie 8 rejestrów danego banku, wystarczy przełączyć się na inny bank, co trwa o wiele krócej.
Zgodnie z dokumentacją kompilatora SDCC, użycie tego atrybutu w innym przypadku niż do funkcji obsługi przerwań nie ma wpływu na generowany kod funkcji, jednak w niektórych przypadkach może być użyteczne. Mianowicie, jeśli dana funkcja jest wywoływana tylko w funkcji obsługi przerwania, która używa innego banku rejestrów niż zerowy, to powinna być ona również deklarowana z użyciem tego samego banku rejestrów. Dla przykładu, jeśli w programie zadeklarowano kilka funkcji obsługi przerwań wykorzystujacych bank 1 i wszystkie z nich wywołują funkcję memcpy() to ma sens stworzenie specjalizowanej wejsji tej funkcji używającej pierwszego banku rejestrów, gdyż uchroni to funkcje obsługi przerwań przed odkładaniem na stosie rejestrów banku 0 przy wejściu do funkcji i przełączeniu na bank 0 przed wywołaniem funkcji memcpy() . |
Funkcje przerwań
Mikrokontrolery rodziny MCS-51 i ich klony posiadają pewną ilość przerwań sprzętowych, których źródłami są m.in.: przepełnienie liczników-czasomierzy, odbiór lub nadanie znaku informacji przez port szeregowy, odpowiedni stan na wejściach INT0 lub INT1. Standardowe źródła przerwań, jakie można spotkać we wszystkich odmianach tych mikrokontrolerów zebrano w poniższej tabeli.
Tabela 6.3. Standardowe źródła przerwań występujące w mikrokontrolerach rodziny MCS-51
Nr przerwania | Adres | Źródło przerwania |
---|---|---|
0 | 0003H | Stan niski lub przejście ze stanu wysokiego w niski na wejściu INT0 |
1 | 000BH | Przepełnienie licznika T0 |
2 | 0013H | Stan niski lub przejście ze stanu wysokiego w niski na wejściu INT1 |
3 | 001BH | Przepełnienie licznika T1 |
4 | 0023H | Odebranie lub nadanie znaku informacji przez port szeregowy |
Wraz z tworzeniem nowych odmian mikrokontrolerów tej rodziny, producenci dodawali coraz to nowe źródła przerwań. Kompilator SDCC wspomaga obsługę właściwie nieskończonej ilości przerwań (podczas testów sprawdzono możliwość poprawnego umieszczania odwołań do funkcji przerwań aż do setnego numeru). Poniższa tabela przedstawia powiązania pomiędzy adresem wektora przerwania a jego numerem aż do numeru 31 i może być pomocna do określenia numeru przerwania w zależności od adresu wektora przerwania i odwrotnie.
Tabela 6.4. Powiązania pomiedzy numerem przerwania i jego adresem
Nr przerwania | Adres | Nr przerwania | Adres | |
---|---|---|---|---|
0 | 0003H | 16 | 0083H | |
1 | 000BH | 17 | 008BH | |
2 | 0013H | 18 | 0093H | |
3 | 001BH | 19 | 009BH | |
4 | 0023H | 20 | 00A3H | |
5 | 002BH | 21 | 00ABH | |
6 | 0033H | 22 | 00B3H | |
7 | 003BH | 23 | 00BBH | |
8 | 0043H | 24 | 00C3H | |
9 | 004BH | 25 | 00CBH | |
10 | 0053H | 26 | 00D3H | |
11 | 005BH | 27 | 00DBH | |
12 | 0063H | 28 | 00E3H | |
13 | 006BH | 29 | 00EBH | |
14 | 0073H | 30 | 00F3H | |
15 | 007BH | 31 | 00FBH |
Funkcja, która ma realizować obsługę danego przerwania powinna być oznaczona słowem kluczowym interrupt
umieszczonym tuż za nawiasem zamykającym definiującym parametry wejściowe funkcji. Dodatkowo należy określić numeru przerwania, który funkcja ta ma obsługiwać poprzez zapis odpowiedniej wartości bezpośrednio za słowem kluczowym interrupt
. Kompilator sam umieszcza pod odpowiednim adresem w tablicy wektorów przerwań instrukcję skoku do funkcji przerwania (zgodnie z przedstawioną powyżej tabelą) i odpowiedni kod niezbędny przy wyjściu z tej funkcji. Poniżej przedstawiono przykładową deklarację funkcji obsługi przerwania od licznika-czasomierza T0.
unsigned int licznik; ... ... ... void Timer0 (void) interrupt 1 { if (++licznik == 4000) // gdy zmienna licznik jest równy 4000 { licznik = 0; // zerowanie zmiennej licznik ... // wykonaj inne ewentualne zadania } }
Słowo kluczowe interrupt
wywiera następujący wpływ na generowany przez kompilator kod asemblerowy funkcji:
- jeśli jest to konieczne na początku funkcji zawartość niektórych rejestrów specjalnych, jak ACC, B, DPH, DPL i PSW jest odkładana na stos,
- jeśli nie określono nowego banku rejestrów dla funkcji za pomocą słowa kluczowego
using
lub wybrano bank 0, to wszystkie rejestry robocze od R0 do R7, które będą użyte w funkcji przerwania są również odkładane na stos, - przed wyjściem z funkcji wszystkie zapamiętane na stosie rejestry są z niego ściągane,
- funkcja przerwania zawsze kończona jest automatycznie przez kompilator instrukcją RETI.
Poniżej przedstawiono kod asemblerowy powyższej funkcji obsługi przerwania wygenerowany przez kompilator. Pogrubioną czcionką wyróżniono instrukcje asemblerowe.
;------------------------------------------------------------ ;proba.c:12: void Timer0 (void) interrupt 1 ; ----------------------------------------- ; function Timer0 ; ----------------------------------------- _Timer0: ar2 = 0x02 ar3 = 0x03 ar4 = 0x04 ar5 = 0x05 ar6 = 0x06 ar7 = 0x07 ar0 = 0x00 ar1 = 0x01 push acc push dpl push dph push ar2 push ar3 push psw mov psw,#0x00 ;proba.c:14: if (++licznik == 4000) // gdy zmienna licznik jest równy 4000 ; genPlus mov dptr,#_licznik movx a,@dptr add a,#0x01 movx @dptr,a inc dptr movx a,@dptr addc a,#0x00 movx @dptr,a ; genAssign mov dptr,#_licznik movx a,@dptr mov r2,a inc dptr movx a,@dptr mov r3,a ; genCmpEq ; Peephole 112.b changed ljmp to sjmp ; Peephole 198 optimized misc jump sequence cjne r2,#0xA0,00103$ cjne r3,#0x0F,00103$ ; Peephole 201 removed redundant sjmp ; Peephole 300 removed redundant label 00106$ 00107$: ;proba.c:16: licznik = 0; // zerowanie zmiennej licznik ; genAssign mov dptr,#_licznik clr a movx @dptr,a inc dptr movx @dptr,a 00103$: pop psw pop ar3 pop ar2 pop dph pop dpl pop acc reti ; eliminated unneeded push/pop b
Jak można zauważyć, ze względu na brak słowa kluczowego using
zmieniającego bank rejestrów dla funkcji, oprócz odłożenia na stos rejestrów specjalnych również nastąpiło odłożenie na stos rejestrów R2 i R3, gdyż są wykorzystywane wewnątrz funkcji. Tylko rejestr B nie został odłożony na stos, gdyż nie jest używany w żadnej instrukcji wygenerowanej dla kodu tejże funkcji.
Dodatkowe zastosowanie słowa kluczowego using
z numerem banku rejestrów inym niż 0 pozwoli na zmniejszenie liczby instrukcji asemblerowych, gdyż rejestry R2 i R3 nie będą już musiały być odkładane na stos.
unsigned int licznik; ... ... ... void Timer0 (void) interrupt 1 using 2 { if (++licznik == 4000) // gdy zmienna licznik jest równy 4000 { licznik = 0; // zerowanie zmiennej licznik ... // wykonaj inne ewentualne zadania } }
Dla takiej deklaracji funkcji jej kod asemblerowy wygenerowany przez kompilator ma następująca postać.
;------------------------------------------------------------ ;proba.c:12: void Timer0 (void) interrupt 1 using 2 ; ----------------------------------------- ; function Timer0 ; ----------------------------------------- _Timer0: ar2 = 0x12 ar3 = 0x13 ar4 = 0x14 ar5 = 0x15 ar6 = 0x16 ar7 = 0x17 ar0 = 0x10 ar1 = 0x11 push acc push dpl push dph push psw mov psw,#0x10 ;proba.c:14: if (++licznik == 4000) // gdy zmienna licznik jest równy 4000 ; genPlus mov dptr,#_licznik movx a,@dptr add a,#0x01 movx @dptr,a inc dptr movx a,@dptr addc a,#0x00 movx @dptr,a ; genAssign mov dptr,#_licznik movx a,@dptr mov r2,a inc dptr movx a,@dptr mov r3,a ; genCmpEq ; Peephole 112.b changed ljmp to sjmp ; Peephole 198 optimized misc jump sequence cjne r2,#0xA0,00103$ cjne r3,#0x0F,00103$ ; Peephole 201 removed redundant sjmp ; Peephole 300 removed redundant label 00106$ 00107$: ;proba.c:16: licznik = 0; // zerowanie zmiennej licznik ; genAssign mov dptr,#_licznik clr a movx @dptr,a inc dptr movx @dptr,a 00103$: pop psw pop dph pop dpl pop acc reti ; eliminated unneeded push/pop b
Jak widać w tym przypadku na stos odkładane są tylko rejestry: ACC, DPL, DPH oraz PSW. Natomiast zmiana banku rejestrów odbywa są za pomoca instrukcji MOV PSW,#0x10
.
Do funkcji przerwań stosuje się poniższe reguły.
- Funkcje obsługi przerwań muszą być bezargumentowe. Jeśli tego typu funkcja zostanie zadeklarowana z argumentami, to kompilator pokaże błąd.
- Deklaracja funkcji obsługi przerwania również nie może zwracać wartości. W tym wypadku kompilator nie pokaże błędu, ale ze względu na fakt, że funkcje obsługi przerwań nie są wywoływane w kodzie programu, nie będzie możliwości przypisania zwracanej wartości do jakiejś zmiennej.
- Chociaż kompilator SDCC dopuszcza bezpośrednie wywołanie w kodzie programu funkcji obsługi przerwania nie wskazując błędu, to nie powinno być to nigdy realizowane.
- Jeśli program składa się z wielu plików z kodem źródłowym, to funkcja obsługi przerwania może być umieszczona w dowolnym z tych plików, ale jej prototyp musi być umieszczony lub dołączony poprzez plik nagłówkowy w pliku, w którym znajduje się funkcja
main
. - Jeśli funkcja obsługi przerwania dokonuje zmiany wartości zmiennej, która również jest wykorzystywana przez inną funkcję w programie, to zmienna ta powinna być deklarowana z atrybutem
volatile
. - Chociaż jest to możliwe, to jednak nie jest zalecane wywoływanie innych funkcji wewnątrz funkcji obsługi przerwań. Jeśli już jakaś funkcja jest wywoływana wewnątrz funkcji obsługi przerwania i nie jest deklarowana ze słowem kluczowym
reentrant
, to jej deklaracja powinna być poprzedzona dyrektywą#pragma nooverlay
. Ponadto funkcje deklarowane bez słowa kluczowegoreentrant
, a wywoływane wewnątrz funkcji obsługi przerwania nie powinny być wywoływane wewnątrz funkcjimain
przy aktywowanym przerwaniu obsługiwanym przez daną funkcję.
Generacja instrukcji skoku do funkcji obsługi przerwania umieszczanej pod odpowiednim adresem może być zakazana przez użycie identyfikatora noiv
w dyrektywie #pragma
kompilatora. W takim wypadku ewentualne umieszczenie instrukcji skoku do danej funkcji obsługi przerwania musi być realizowane samodzielnie np. w osobnym module asemblerowym.
... ... ... // Funkcja obsługi przerwania od licznika T0 void Timer0 (void) interrupt 1 using 2 { ... ... ... } #pragma noiv // Funkcja obsługi przerwania od wejścia INT0 void InInt0 (void) interrupt 0 using 1 { ... ... ... }
W powyższym przykładzie brak generacji kodu związanego ze skokiem do funkcji obsługi przerwania dotyczy tylko drugiej funkcji obsługi przerwania od wejścia INT0, gdyż funkcja obsługi przerwania od licznika T0 występuje w kodzie przed dyrektywą #pragma
.
Ze względu na fakt, że funkcje obsługi przerwań o wyższym priorytecie mogą przerywać działanie funkcji obsługi przerwań o niższym priorytecie, przypisanie tego samego banku rejestrów dla funkcji obsługi przerwań o priorytetach z różnych poziomów może spowodować będne działanie aplikacji. Dlatego te same banki rejestrów mogą współdzielić tylko funkcje obsługujące przerwania z tego samego poziomu priorytetu, poniważ nie ma możliwości, aby przerywały się one nawzajem nadpisując przez to zawartość rejestrów wspólnego banku. |
Funkcje uproszczone
Słowo kluczowe _neked
może być dołączone do deklaracji funkcji w celu zablokowania generowania przez kompilator prologu i epilogu w jej kodzie, tj. odkładania na stos najczęściej używanych rejestrów i ściągania ich przy wyjściu z funkcji. Takie funkcje zostały określone w niniejszym opracowaniu jako funkcje uproszczone (ang. naked functios). W takim wypadku to programista jest odpowiedzialny za ewentualne zachowanie zawartości rejestrów, które mogą zostać zmienione wewnątrz funkcji, wybór pożądanego banku rejestrów, generację kodu odpowiadającego instrukcji return
, itp. Praktycznie oznacza to, że zawartość funkcji musi być napisane w asemblerze. Szczególnie jest to przydatne dla funkcji przerwań, które mają duży i często niepotrzebny prolog i epilog. Dla przykładu poniżej porównano kod wygenerowany przez dwie różne deklaracje tak samo działającej funkcji obsługi przerwania.
data unsigned char licznik; void FunkcjaPrzerwania(void) interrupt 1 { licznik++; }
data unsigned char licznik; void FunkcjaProsta(void) interrupt 1 _naked { _asm inc _licznik reti ;funkcja ta musi jawnie zawierać instrukcję powrotu _endasm; }
Kod wynikowy wygenerowany dla funkcji FunkcjaPrzerwania
wygląda następująco
_FunkcjaPrzerwania: push acc push b push dpl push dph push psw mov psw ,#0 x00 inc _counter pop psw pop dph pop dpl pop b pop acc reti
podczas gdy kod wynikowy funkcji NakedInterrupt
wygląda jak poniżej.
_FunkcjaProsta: inc _counter reti ;funkcja ta musi jawnie zawierać instrukcję powrotu
Chociaż od strony technicznej nie ma żadnych ograniczeń,aby kod funkcji uproszczonej był pisany w języku C,to jednak może to spowodować wiele nieprzewidzianych kłopotów. Dlatego zaleca się jednak pisanie kodu takich funkcji w asemblerze.
Do sterowania odkładaniem i ściąganiem rejestrów w fukcji można również wykorzystać
dyrektywę #pragma exclude
.