MCS-51
Opis rozszerzeń języka C w kompilatorze SDCC

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.

Uwaga 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 kluczowego reentrant, a wywoływane wewnątrz funkcji obsługi przerwania nie powinny być wywoływane wewnątrz funkcji main 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.

Uwaga 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.