Nie zawsze jest uzasadnione, by powoływać do życia jakąś funkcję implementowaną w języku asembler by wykonać jakąś mało skompilowaną operację. Wręcz klasycznym przykładem jest pusta instrukcja (w asm jest to nop). Jest uzasadnione by taką wygenerować wprost w ściśle określonym miejscu w programie (w języku C). Tak prostą operację można zapisać jako:
__asm__ __volatile__ ("nop") ;
Wstawka zaczyna się od __asm__ (dwa znaki podkreślenia, wyraz asm i dwa znaki podkreślenia pisane bez odstępów), więcej o syntaktyce w dalszej części. Jednak okazuje się, że jeżeli instrukcja nie korzysta z informacji wejściowych, nie generuje wyników (a takim przypadkiem właśnie jest instrukcja pusta – nie wnosi w algorytmie programu żadnych skutków), to kompilator w ramach akcji optymalizacji kodu może takie fragmenty (tak zwane martwe kody) usunąć z programu. Nie po to programista (człowiek) trudził się by maszyna (kompilator) niszczył jego pracę i uważała, że „wie lepiej” (w końcu kilka instrukcji nop wykonanych w jednym ciągu oznacza ściśle określony czas ich realizacji i można tego chwytu używać do odmierzania bardzo krótkich odcinków czasu). By zapobiec takim sytuacjom, dodawane jest zaklęcie __volatile__ (dwa znaki podkreślenia, słowo volatile i dwa znaki podkreślenia).
Reasumując, powyższy zapis oznacza nieusuwalną wstawkę zawierającą instrukcję pustą. Bardzo podobnie sprawa wygląda przykładowo z instrukcją blokady lub zezwolenia na przyjmowanie przerwań.
__asm__ __volatile__ ("cli") ;
Patrząc na program z punktu widzenia algorytmu, to ta instrukcja (pozornie) nic nie wnosi w programie: nie zmieniły się żadne wartości zmiennych, nie wpłynęło to na stan portów. Pomimo tego istnienie tej instrukcji ma głęboki sens.
Ogólna postać instrukcji jest następująca (podaję z dodatkowym zaklęciem __volatile__ choć nie jest ono obowiązkowe):
__asm__ __volatile__ ( "kod w asm" : operandy wejściowe : operandy wyjściowe : używane rejestry ) ;
gdzie „kod w asm” jest ciągiem instrukcji rozdzielonym znakiem LF (znak nowej linii to \n) często łączy go się ze znakiem tabulacji (daje to „ładniejszy” wydruk z kompilacji, znak tabulacji to \t). Przykładowo ciąg kilku powyższych nop'ów można zapisać następująco:
__asm__ __volatile__ ( "nop \n\t nop \n\t nop \n\t nop” ) ;
Jednak taka postać zapisu jest mało czytelna, znacząco lepiej wygląda:
Kod: Zaznacz cały
__asm__ __volatile__ ( "nop \n\t"
"nop \n\t"
"nop \n\t "
"nop" ) ;
Z ogólnej postaci zapisu instrukcji podanej powyżej, jeżeli któraś część nie występuje może zostać pominięta (co skutkuje przykładowo dwoma znakami ::). W sytuacji, gdy wszystkie części (te po dwukropku) nie występują, to mogą być pominięte w całości razem ze znakami „:”).
Jako ilustrację powyższych rozważań rozpatrzmy następujący przykład:
Kod: Zaznacz cały
#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#define nop() __asm__ __volatile__ ("nop")
uint8_t GVar ;
int main ( void )
{
uint8_t LVar ;
/* ------------------- */
LVar = 0xC3 ; //135
GVar = 0x3C ; //120
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
"bst r0 , 7" "\n\t"
"lsl r0" "\n\t"
"bld r0 , 0" "\n\t"
"mov %0 , r0" "\n\t"
: "=r" (LVar)
: "0" (LVar ) ) ;
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
"bst r0 , 7" "\n\t"
"lsl r0" "\n\t"
"bld r0 , 0" "\n\t"
"mov %0 , r0" "\n\t"
: "=r" (GVar)
: "0" (GVar ) ) ;
for ( ; ; )
{
nop();
} /* for */ ;
} /* main */
Wiadomo, że rejestr R0, jest jest ogólnofilozoficznym rejestrem roboczym i nie należy specjalnie dbać o jego zawartość. W powyższym przykładzie do rejestru roboczego jest wpisana zmienna. Rejestr jest „obrócony cyklicznie” o jeden bit w lewo (w jaki sposób to działa to każdy czytelnik może sam nad tym się zastanowić: ciąg instrukcji bst, lsl i bld). Na końcu instrukcji __asm__ są zapisane następujące zaklęcia: "=r" (LVar): i "0" (LVar). Pierwsze z nich określa operandy wyjściowe (tu konkretnie zmienną lokalną LVar). Drugie z nich określa operandy wejściowe (również zmienna LVar). Wszędzie tam, gdzie występuje %0 zostanie wstawione odwołanie do zmiennej. Jednak warto się zapytać, w jaki sposób to działa, że działa. W zależności od stopnia optymalizacji działa poprawnie zawsze. W zależności od „klasy” zmiennej (zmienna lokalna czy zmienna globalna) działa poprawnie. Warto zadać sobie pytanie: dlaczego?
Otóż, cała wstawka posiada własny prolog i epilog. Prolog to pobranie do jakiegoś rejestru czegoś, co jest opisane jako operandy wejściowe (może być ich wiele). Epilog, to zapis wyniku do zmiennej (w tym wypadku LVar) wyniku operacji. Oczywiście to my musimy wiedzieć, co jest wynikiem.
LVar = C3 hex po rotacji to 87 hex = 135 dec.
Podobnie GVar = 3C hex po rotacji to 78 hex = 120 dec. Wyniki są poprawne.
W przypadku braku optymalizacji (opcje O0), to daje następujący kod:
Kod: Zaznacz cały
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
58: 89 81 ldd r24, Y+1 ; 0x01
5a: 08 2e mov r0, r24
5c: 07 fa bst r0, 7
5e: 00 0c add r0, r0
60: 00 f8 bld r0, 0
62: 80 2d mov r24, r0
64: 89 83 std Y+1, r24 ; 0x01
"lsl r0" "\n\t"
"bld r0 , 0" "\n\t"
"mov %0 , r0" "\n\t"
: "=r" (LVar)
: "0" (LVar ) ) ;
Zmienna lokalna jest lokowana na stosie i ma adres (Y+1). Prolog to: ldd r24,Y+1 oraz epilog to std Y+1,r24.
Kod: Zaznacz cały
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
66: 80 91 60 00 lds r24, 0x0060
6a: 08 2e mov r0, r24
6c: 07 fa bst r0, 7
6e: 00 0c add r0, r0
70: 00 f8 bld r0, 0
72: 80 2d mov r24, r0
74: 80 93 60 00 sts 0x0060, r24
"mov %0 , r0" "\n\t"
: "=r" (GVar)
: "0" (GVar ) ) ;
Zmienna globalna na stały adres w pamięci RAM (60 hex). W tym przypadku prolog to lds r24,0x60 oraz epilog to sts 0x60,r24.
W przypadku włączenia optymalizacji, zmienia się sam prolog oraz epilog.
Kod: Zaznacz cały
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
46: 83 ec ldi r24, 0xC3 ; 195
48: 08 2e mov r0, r24
4a: 07 fa bst r0, 7
4c: 00 0c add r0, r0
4e: 00 f8 bld r0, 0
50: 80 2d mov r24, r0
"lsl r0" "\n\t"
"bld r0 , 0" "\n\t"
"mov %0 , r0" "\n\t"
: "=r" (LVar)
: "0" (LVar ) ) ;
W tym konkretnym przypadku, to kompiler do tego stopnia wszystko wyoptymalizował, że gdyby nie zaklęcie __volatile__, to cały fragment zostałby usunięty.
Trochę inaczej rzecz ma się ze zmienną globalną. Tak sobie to wszystko wykombinował, że doszedł do wniosku, że zmienną globalną zaalokuje w rejestrze r25.
Kod: Zaznacz cały
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
52: 09 2e mov r0, r25
54: 07 fa bst r0, 7
56: 00 0c add r0, r0
58: 00 f8 bld r0, 0
5a: 90 2d mov r25, r0
"mov %0 , r0" "\n\t"
: "=r" (GVar)
: "0" (GVar ) ) ;
Jak się nad tym chwilę zastanowić, to się okazuje, że rozwiązanie nie jest bardzo optymalne. Wróćmy do stanu początkowego (brak optymalizacji):
Kod: Zaznacz cały
#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#define nop() __asm__ __volatile__ ("nop")
uint8_t GVar ;
int main ( void )
{
uint8_t LVar ;
/* ------------------- */
LVar = 0xC3 ; //135
GVar = 0x3C ; //120
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
"bst r0 , 7" "\n\t"
"lsl r0" "\n\t"
"bld r0 , 0" "\n\t"
"mov %0 , r0" "\n\t"
: "=r" (LVar)
: "0" (LVar ) ) ;
__asm__ __volatile__ ( "mov r0, %0" "\n\t"
"bst r0 , 7" "\n\t"
"lsl r0" "\n\t"
"bld r0 , 0" "\n\t"
"mov %0 , r0" "\n\t"
: "=r" (GVar)
: "0" (GVar ) ) ;
for ( ; ; )
{
nop();
} /* for */ ;
} /* main */
Skoro prolog to załadowanie operandu do rejestru %0, to dlaczego nie można wykorzystać go do końca (zamiast operować na r0)? Czasem warto popatrzeć na własne działania tak trochę z boku. Kolejna iteracja rozwiązania to:
Kod: Zaznacz cały
#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#define nop() __asm__ __volatile__ ("nop")
uint8_t GVar ;
int main ( void )
{
uint8_t LVar ;
/* ------------------- */
LVar = 0xC3 ; //135
GVar = 0x3C ; //120
__asm__ __volatile__ ( "bst %0 , 7" "\n\t"
"lsl %0" "\n\t"
"bld %0 , 0" "\n\t"
: "=r" (LVar)
: "0" (LVar ) ) ;
__asm__ __volatile__ ( "bst %0 , 7" "\n\t"
"lsl %0" "\n\t"
"bld %0 , 0" "\n\t"
: "=r" (GVar)
: "0" (GVar ) ) ;
for ( ; ; )
{
nop();
} /* for */ ;
} /* main */
Wyszło o kilka instrukcji maszynowych mniej.
Kod: Zaznacz cały
__asm__ __volatile__ ( "bst %0 , 7" "\n\t"
58: 89 81 ldd r24, Y+1 ; 0x01
5a: 87 fb bst r24, 7
5c: 88 0f add r24, r24
5e: 80 f9 bld r24, 0
60: 89 83 std Y+1, r24 ; 0x01
"lsl %0" "\n\t"
"bld %0 , 0" "\n\t"
: "=r" (LVar)
: "0" (LVar ) ) ;
__asm__ __volatile__ ( "bst %0 , 7" "\n\t"
62: 80 91 60 00 lds r24, 0x0060
66: 87 fb bst r24, 7
68: 88 0f add r24, r24
6a: 80 f9 bld r24, 0
6c: 80 93 60 00 sts 0x0060, r24
"bld %0 , 0" "\n\t"
: "=r" (GVar)
: "0" (GVar ) ) ;
I to ma sens.