Architektura Komputerów 2 – Laboratorium nr 6 – Jednostka zmiennoprzecinkowa (FPU)

Rejestry jednostki zmiennoprzecinkowej

Koprocesor dysponuje ośmioma 80-bitowymi rejestrami zmiennoprzecinkowymi i trzema 16-bitowymi rejestrami kontrolnymi (control word, status word i tag word). Rejestry zmiennoprzecinkowe połączone są w stos. Możemy wczytywać do nich wartości rozkazami fld (Fpu LoaD) oraz pobierać z nich wartości – fstp (FPU STore and Pop). Mamy do nich dostęp poprzez nazwę ST i numer rejestru – ST(n). Wartości zawsze wstawiane są do rejestru ST(0), a wszystkie pozostałe rejestry zmieniają wtedy swoją numerację – 0 przechodzi na 1, 1 na 2 itd. Analogicznie podczas pobierania wartości z tego “stosu”, pobierana jest wartość z rejestru ST(0), a pozostałe zmieniają numerację w przeciwnym kierunku.

Podstawowe rozkazy jednostki zmiennoprzecinkowej

RozkazOpis
fld REJESTR_STŁaduje zawartość innego podanego rejestru ST do ST(0) i przesuwa numerację wszystkich rejestrów, włącznie z tym z którego pobierana jest wartość, w górę.
fldl ZMIENNA/ADRESPodobnie jak poprzednio jednak ładowany jest float lub double z pamięci określonej przez etykietę zmiennej lub adres w rejestrze (wtedy mnemonik rejestru należy umieścić w nawiasie).
fld1Do ST(0) ładowana jest stała 1.0, a pozostałe rejestry są “przesuwane”.
fldz-||- 0.0 -||-
fldpi-||- liczba PI (3.1415926) -||-
fst REJESTR_STWartość z rejestru ST(0) jest umieszczana w innym rejestrze ST. Numeracja rejestrów nie ulega zmianie.
fstp REJESTR_ST lub nicWartość z rejestru ST(0) jest kopiowana do innego podanego rejestru lub tracona, następnie wszystkie rejestry zmieniają swoją numerację (1->0, 2->1 itd.).
fstpl ZMIENNA/ADRESWartość z rejestru ST(0) kopiowana jest do zmiennej lub do pamięci pod adres sprecyzowany w innym rejestrze ogólnego przeznaczenia podanym w nawiasie. Numeracja rejestrów zmienia się jak poprzednio.
fxch REJESTR_STZawartości rejestru ST(0) i innego podanego jako operand rozkazu są wymieniane.
fabsOblicza wartość bezwzględną z liczby w ST(0) i zapisuje ją do ST(0).
fadd CEL, ŹRÓDŁODodaje zawartość ŹRÓDŁO do CEL i zapisuje wynik do CEL. ŹRÓDŁO i CEL to mnemoniki rejestrów ST.
faddl ZMIENNA/ADRESDodaje do ST(0), liczbę z pamięci.
fsub CEL, ŹRÓDŁOOdejmuje zawartość ŹRÓDŁO od CEL i zapisuje wynik do CEL. ŹRÓDŁO i CEL to mnemoniki rejestrów ST.
fsubl ZMIENNA/ADRESOdejmuje liczbę z pamięci od ST(0).
fmul CEL, ŹRÓDŁOMnoży zawartość CEL przez ŹRÓDŁO i zapisuje wynik do CEL. ŹRÓDŁO i CEL to mnemoniki rejestrów ST.
fmull ZMIENNA/ADRESMnoży zawartość ST(0) przez liczbę z pamięci.
fdiv CEL, ŹRÓDŁODzieli zawartość CEL przez ŹRÓDŁO i zapisuje wynik do CEL. ŹRÓDŁO i CEL to mnemoniki rejestrów ST.
fdivl ZMIENNA/ADRESDzieli zawartość ST(0) przez liczbę z pamięci.
fsqrtOblicza pierwiastek z liczby zapisanej w ST(0) i zapisuje wynik do ST(0).

Rejestr kontrolny

W rejestrze kontrolnym (control word) zapisane są ustawienia jednostki FPU. Przy pomocy rozkazu fstcw można zapisać jego zawartość do pamięci, a następnie instrukcją mov przenieść do rejestru ogólnego przeznaczenia. Poszczególne bity tego rejestru odpowiadają min. za sposób zaokrąglania wartości, czy dokładność obliczeń. Możemy je zmienić, a następnie zapisać do rejestru w analogiczny sposób, umieszczając jego zawartość w pamięci i rozkazem fldcw ładując ustawienia. Zostało to dokładniej przedstawione w zadaniu pierwszym i drugim, gdzie odczytywane i zmieniane są parametry pracy jednostki zmiennoprzecinkowej.

Zadanie pierwsze – Wyświetlanie i zmiana precyzji obliczeń

Kod C:

#include <stdio.h>

// Deklaracja funkcji które zostaną dołączone
// dopiero na etapie linkowania kodu
extern int sprawdz();
extern int ustaw(int precyzja);

int main(void)
{
    // Wyświetlenie menu, pobranie od użytkownika numeru opcji
    // i wyświetlenie lub ustawienie precyzji obliczeń korzystając
    // z funkcji napisanych w Asemblerze
    int nr_opcji=1, precyzja=1;
    do
    {
        printf("=== Wybierz: ===\n");
        printf("1 - aby sprawdzic aktualna precyzje obliczen\n");
        printf("2 - aby zmienic aktualna precyzje obliczen:\n");
        printf("0 - aby zakonczyc program\n");
        scanf("%d", &nr_opcji);

        switch(nr_opcji)
        {
        case 1:
            printf("\nAktualna precyzja: ");
            switch(sprawdz())
            {
                case 0: printf("Single Precision\n\n"); break;
                case 2: printf("Double Precision\n\n"); break;
                case 3: printf("Double Extended Precison\n\n");
                        break;
            }
            break;

        case 2:
            printf("\n1 - Single Precision\n");
            printf("2 - Double Precision\n");
            printf("3 - Double Extended Precision\n");
            scanf("%d", &precyzja);
            if (precyzja>3 || precyzja<1)
                printf("Podano zla wartosc\n");
            else ustaw(precyzja);
            printf("\n");
            break;
        }
    } while(nr_opcji!=0);

    return 0;
}

Kod Asemblerowy:

.data
control_word: .short 0

.text
# Zadeklarowane tutaj funkcje będą możliwe do wykorzystania
# w języku C po zlinkowaniu plików wynikowych kompilacji obu kodów
.global ustaw, sprawdz
.type ustaw, @function
.type sprawdz, @function

ustaw:
    push    %rbp
    mov     %rsp, %rbp
    # Pierwszy i jedyny parametr funkcji umieszczony zostanie
    # w rejestrze RDI. Może on zawierać jedną z trzech wartości:
    # 1 dla single, 2 dla double i 3 dla extended

    # Pobranie zawartości rejestru kontrolnego do rejestru AX
    # przez komórkę w pamięci
    mov     $0, %rax
    fstcw   control_word
    fwait
    mov     control_word, %ax

    # Wyzerowanie bitów kontroli prezycji (00 - początkowo single)
    and     $0xFCFF, %ax # 1111 1100 1111 1111

    # Zamiana na double lub extended w zależności
    # od przesłnego parametru
    cmp     $2, %rdi
    jl      koniec
    je      double

    # Zmiania na extended (11)
    extended:
    xor     $0x300, %ax # 0000 0011 0000 0000
    jmp     koniec # Aby nie zmienić na double

    # Zmiana na double (10)
    double:
    xor     $0x200, %ax # 0000 0010 0000 0000

    koniec:
    # Zapisanie słowa kontrolnego z rejestru AX do rejestru CW
    mov     %ax, control_word
    fldcw   control_word

    mov     %rbp, %rsp
    pop     %rbp
    ret

sprawdz:
    push    %rbp
    mov     %rsp, %rbp

    # Pobranie słowa kontrolnego FPU so rejestru AX
    mov     $0, %rax
    fstcw   control_word
    fwait
    mov     control_word, %ax

    # Wyzerowanie pozostałych bitów poza bitami kontroli precyzji
    # i przesunięcie wygenerowanych bitów w prawo na koniec
    and     $0x300, %ax # 0000 0011 0000 0000
    shr     $8, %ax
    # Wartość zwracana znajduje się już w EAX. Przyjmuje wartości:
    # 0 dla single, 2 dla double i 3 dla extended

    mov     %rbp, %rsp
    pop     %rbp
    ret

Zadanie drugie – Wyświetlanie i zmiana sposobu zaokrąglania

Kod C:

#include <stdio.h>

// Deklaracja funkcji które zostaną dołączone
// dopiero na etapie linkowania kodu
extern int sprawdz();
extern int ustaw(int tryb);

int main(void)
{
    int nr_opcji=1, tryb=1;
    do
    {
        printf("=== Wybierz: ===\n");
        printf("1 - aby sprawdzic aktualny tryb zaokraglania\n");
        printf("2 - aby zmienic aktualny tryb zaokraglania:\n");
        printf("0 - aby zakonczyc program\n");
        scanf("%d", &nr_opcji);

        switch(nr_opcji)
        {
        case 1:
            printf("\nAktualny tryb zaokraglania: ");
            switch(sprawdz())
            {
                case 0: printf("Round to nearest\n\n"); break;
                case 1: printf("Round down\n\n"); break;
                case 2: printf("Round up\n\n"); break;
                case 3: printf("Truncate\n\n"); break;
            }
            break;

        case 2:
            printf("\n0 - Round to nearest\n");
            printf("1 - Round down\n");
            printf("2 - Round up\n");
            printf("3 - Truncate\n");
            scanf("%d", &tryb);
            if (tryb>3 || tryb<0) printf("Podano zla wartosc\n");
            else ustaw(tryb);
            printf("\n");
            break;
        }
    } while(nr_opcji!=0);

    return 0;
}

Kod Asemblerowy:

.data
control_word: .short 0

.text
# Zadeklarowane tutaj funkcje będą możliwe do wykorzystania
# w języku C po zlinkowaniu plików wynikowych kompilacji obu kodów
.global ustaw, sprawdz
.type ustaw, @function
.type sprawdz, @function

ustaw:
    push        %rbp
    mov         %rsp, %rbp
    # Pierwszy i jedyny parametr funkcji umieszczony zostanie
    # w rejestrze RDI. Jego możliwe wartości to:
    # 0 dla nearest, 1 dla round down, 2 dla round up
    # i 3 dla truncate

    # Pobranie zawartości rejestru kontrolnego do rejestru AX
    # przez komórkę w pamięci
    mov         $0, %rax
    fstcw       control_word
    fwait
    mov         control_word, %ax

    # Wyzerowanie bitów kontroli zaokrąglania
    # (00 - początkowo nearest)
    and         $0xF3FF, %ax # 1111 0011 1111 1111

    # Przesunięcie przesłanej jako parametr funkcji
    # wartości w lewo o 10 bitów
    shl         $10, %rdi

    # Ustawienie odpowiednich bitów w zawartości
    # pobranego rejestru CW
    xor         %di, %ax

    # Zapis zmienionej wartości do rejestru CW
    mov         %ax, control_word
    fldcw       control_word

    mov         %rbp, %rsp
    pop         %rbp
    ret

sprawdz:
    push        %rbp
    mov         %rsp, %rbp

    # Pobranie słowa kontrolnego FPU do rejestru AX
    mov         $0, %rax
    fstcw       control_word
    fwait
    mov         control_word, %ax

    # Wyzerowanie pozostałych bitów poza bitami kontroli
    # zaokrąglania i przesunięcie wygenerowanych bitów
    # w prawo na ostatnie pozycje
    and         $0xC00, %ax # 0000 1100 0000 0000
    shr         $10, %ax
    # Wartość zwracana znajduje się już w EAX. Przyjmuje wartości:
    # 0 dla nearest, 1 dla round down, 2 dla round up
    # i 3 dla truncate

    mov         %rbp, %rsp
    pop         %rbp
    ret

Szereg Taylora

Kolejnym zadaniem domowym był program przybliżający wartość funkcji sinus korzystając z rozwinięcia szeregu Taylora. Na zajęciach napisana została modyfikacja tego programu, tak aby przybliżana była wartość cosinusa. Oba programy są niemal identyczne.

Wartość funkcji sinus w zadanym punkcie przybliżana jest ze wzoru x -x3/3! +x6/6! ... +((-1)n * x2n-1)/(2n-1)!. Program nie liczy w każdym obiegu pętli kolejnych wyrazów ciągu, a jedynie je uzupełnia. Pierwszym wyrazem ciągu jest x – wartość kąta w radianach podana przez użytkownika. Następnym: poprzednia wartość ciągu * -x2 / (Y+1)(Y+2) gdzie (Y+1)(Y+2) to 2 kolejne “wyrazy” silni, przy założeniu że początkowo silnia jest równa 1.0. Dla drugiego wyrazu przyjmie ona wtedy wartość 1.0*2.0*3.0, co daje 3!. Przy obliczaniu wartości funkcji cosinus, początkowym wyrazem nie jest kąt podany przez użytkownika, a wartość 1.0. Silnie również zaczynamy liczyć od 0.0, tak aby dla pierwszego wyrazu przyjęła ona wartość 2!, a nie 3! (nie mnożymy wartości przez 0.0, pierwszym wyrazem jest (Y+1)(Y+2)).

Zadanie trzecie – Aproksymacja funkcji Sinus

Kod C:

#include <stdio.h>

// Deklaracja funkcji która zostanie dołączona
// dopiero na etapie linkowania kodu
extern double taylor(double a, int b);

int main(void)
{
    int i;
    double f;
    printf("Kat: ");
    scanf("%lf", &f);
    printf("Liczba wyrazow ciagu: ");
    scanf("%d", &i);
    printf("Twoj wynik to: %lf\n", taylor(f, i));
    return 0;
}

Kod Asemblerowy:

.data
minus: .double -1.0
silnia: .double 1.0

.text
# Zadeklarowana tutaj funkcja będzie możliwa do wykorzystania
# w języku C po zlinkowaniu plików wynikowych kompilacji obu kodów
.global taylor
.type taylor, @function

# Funkcja obliczająca przybliżenie wartości sinusa
# z szeregu Taylora
taylor:
    push    %rbp
    mov     %rsp, %rbp

    # Przyjmowane przez funkcje argumenty:
    # RDI - ilość kroków
    # XMM0 - kąt w radianach

    # Załadowanie wartości z rejestru XMM0
    # do rejestrów ST(0), ST(1) i ST(2) przez stos
    sub     $8, %rsp
    movsd   %xmm0, (%rsp)
    fldl    (%rsp)  # ST(0) = przesłany kąt
    fld     %st     # ST(1) = ST(0); ST(0) = zawartość ST(0)
                    # z poprzedniej linijki
    fld     %st     # -||-

    # Zawartość rejestrów FPU obecnie, na początku i końcu pętli:
    # ST(0) - aktualny wyraz ciągu (początkowo równy x)
    # ST(1) - aktualna suma ciągu (-||-)
    # ST(2) - przesłany kąt (x)

    movq    $0, %rsi # Licznik do pętli
    fwait   # Oczekiwanie na zakończenie wykonywanych przez
            # FPU obliczeń

    petla:
        # Wyskok z pętli po obliczeniu wszystkich wyrazów
        cmp     %rdi, %rsi
        je      koniec
        inc     %rsi # Zwiększenie licznika wykonań pętli

        ###
        ### Obliczenie licznika
        ###
        fmul    %st(2), %st # Aktualny wyraz ciągu (ST(0)) *= x
        fmul    %st(2), %st # Aktualny wyraz ciągu (ST(0)) *= x
        fmull   minus # Aktualny wyraz ciągu (ST(0)) * (-1)
        # Po wykonaniu tych instrukcji uzyskamy:
        # Aktualny wyraz ciągu = poprzedni wyraz ciągu * (-x^2)

        ###
        ### Obliczanie mianownika - silni
        ###
        fld1    # ST(0) -> ST(1)...; ST(0) = 1.0
        fld1    # ST(0) -> ST(1)...; ST(0) = 1.0
        fldl    silnia # ST(0) -> ST(1)...;
                # ST(0) = zawartość komórki pamięci - silnia

        # Aktualne rejestry:
        # ST(0) - numer wyrazu silni (z "silnia")
        # ST(1) - wynik silni (bez uwzględnienia
        #         wcześniejszych części), początkowo 1.0
        # ST(2) - 1.0

        # Obliczenie kolejnych dwóch wyrazów silni:
        # Jeśli początkowa wartość ze zmiennej "silnia" była równa
        # Y, to w tej części obliczane są "wyrazy" (Y+1)*(Y+2).
        # Przez obliczoną wartość dzielny jest poprzedni wyraz
        # ciągu, po uprzednim pomnożeniu przez -x^2.
        # Po tym etapie, obecny wyraz ciągu jest równy:
        # (poprzedni wyraz ciągu * -x^2) / (Y+1)*(Y+2)
        fadd    %st(2), %st
        fmul    %st, %st(1)
        fadd    %st(2), %st
        fmul    %st, %st(1)

        fstpl   silnia # Zapisanie numery ostatniego wyrazu
                       # silni do zmiennej i ściągnięcie go
                       # ze "stosu" FPU

        # Usunięcie niepotrzebnych wartości
        fxch    %st(1) # Zamiana miejscami ST(0) i ST(1)
        fstp    %st    # Ściągnięcie ze "stosu" FPU ostatniej
                       # wartości

        # Aktualne rejestry:
        # ST(0) - silnia (obecny dzielnik)
        # ST(1) - wartość do podzielenia (aktualny wyraz ciągu)
        # ST(2) - aktualna suma ciągu
        # ST(3) - przesłany kąt (x)

        fdivr   %st, %st(1) # Dzielenie obecnego wyrazu przez
                            # dzielnik (silnie) - wynik trafi
                            # do ST(1)

        fstp    %st # Usunięcie obecnego dzielnika ze "stosu" FPU
                    # Zawartość rejestrów jak na początku pętli

        fadd    %st, %st(1) # Dodanie wartości obecnego wyrazu
                            # do wyniku globalnego

        jmp petla # Powrót na początek pętli

    # Przeniesienie wyniku z rejestru ST(0) (sumy ciągu)
    # do rejestru XMM0 przez stos i zakończenie funkcji
    koniec:
    fstp    %st
    fstpl   (%rsp)
    movsd   (%rsp), %xmm0

mov     %rbp, %rsp
pop     %rbp
ret

Zadanie czwarte – Aproksymacja funkcji Cosinus

Kod C:

#include <stdio.h>

// Deklaracja funkcji która zostanie dołączona
// dopiero na etapie linkowania kodu
extern double taylor(double a, int b);

int main(void)
{
    int i;
    double f;
    printf("Kat: ");
    scanf("%lf", &f);
    printf("Liczba wyrazow ciagu: ");
    scanf("%d", &i);
    printf("Twoj wynik to: %lf\n", taylor(f, i));
    return 0;
}

Kod Asemblerowy:

.data
minus: .double -1.0
silnia: .double 0.0

.text
# Zadeklarowana tutaj funkcja będzie możliwa do wykorzystania
# w języku C po zlinkowaniu plików wynikowych kompilacji obu kodów
.global taylor
.type taylor, @function

# Funkcja obliczająca przybliżenie wartości sinusa
# z szeregu Taylora
taylor:
    push    %rbp
    mov     %rsp, %rbp

    # Przyjmowane przez funkcje argumenty:
    # RDI - ilość kroków
    # XMM0 - kąt w radianach

    # Załadowanie wartości z rejestru XMM0
    # do rejestrów ST(0), ST(1) i ST(2) przez stos
    sub     $8, %rsp
    movsd   %xmm0, (%rsp)
    fldl    (%rsp)  # ST(0) = przesłany kąt
    fl1             # ST(1) = ST(0); ST(0) = 1.0
    fl1             # -||-

    # Zawartość rejestrów FPU obecnie, na początku i końcu pętli:
    # ST(0) - aktualny wyraz ciągu (początkowo 1.0)
    # ST(1) - aktualna suma ciągu (początkowo 1.0)
    # ST(2) - przesłany kąt (x)

    movq    $0, %rsi # Licznik do pętli
    fwait   # Oczekiwanie na zakończenie wykonywanych
            # przez FPU obliczeń

    petla:
        # Wyskok z pętli po obliczeniu wszystkich wyrazów
        cmp     %rdi, %rsi
        je      koniec
        inc     %rsi # Zwiększenie licznika wykonań pętli

        ###
        ### Obliczenie licznika
        ###
        fmul    %st(2), %st # Aktualny wyraz ciągu (ST(0)) *= x
        fmul    %st(2), %st # Aktualny wyraz ciągu (ST(0)) *= x
        fmull   minus # Aktualny wyraz ciągu (ST(0)) * (-1)
        # Obecny wyraz ciągu = poprzedni wyraz ciągu * (-x^2)

        ###
        ### Obliczanie mianownika - silni
        ###
        fld1    # ST(0) -> ST(1)...; ST(0) = 1.0
        fld1    # ST(0) -> ST(1)...; ST(0) = 1.0
        fldl    silnia # ST(0) -> ST(1)...;
                # ST(0) = zawartość komórki pamięci - silnia

        # Aktualne rejestry:
        # ST(0) - numer wyrazu silni (z "silnia")
        # ST(1) - wynik silni (bez uwzględnienia
        #         wcześniejszych części), początkowo 1.0
        # ST(2) - 1.0

        # Obliczenie kolejnych dwóch wyrazów silni:
        # Jeśli początkowa wartość ze zmiennej "silnia" była równa
        # Y, to w tej części obliczane są "wyrazy" (Y+1)*(Y+2).
        # Przez obliczoną wartość dzielny jest poprzedni wyraz
        # ciągu, po uprzednim pomnożeniu przez -x^2.
        # Na tym etapie, obecny wyraz ciągu jest równy:
        # (poprzedni wyraz ciągu * -x^2) / (Y+1)*(Y+2)
        fadd    %st(2), %st
        fmul    %st, %st(1)
        fadd    %st(2), %st
        fmul    %st, %st(1)

        fstpl   silnia # Zapisanie numery ostatniego wyrazu silni
                       # do zmiennej i ściągnięcie go
                       # ze "stosu" FPU

        # Usunięcie niepotrzebnych wartości
        fxch    %st(1) # Zamiana miejscami ST(0) i ST(1)
        fstp    %st    # Ściągnięcie ze "stosu" FPU
                       # ostatniej wartości

        # Aktualne rejestry:
        # ST(0) - silnia (obecny dzielnik)
        # ST(1) - wartość do podzielenia (aktualny wyraz ciągu)
        # ST(2) - aktualna suma ciągu
        # ST(3) - przesłany kąt (x)

        fdivr   %st, %st(1) # Dzielenie obecnego wyrazu przez
                            # dzielnik (silnie) - wynik trafi
                            # do ST(1)

        fstp    %st # Usunięcie obecnego dzielnika ze "stosu" FPU
                    # Zawartość rejestrów jak na początku pętli

        fadd    %st, %st(1) # Dodanie wartości obecnego wyrazu
                            # do wyniku globalnego

        jmp petla # Powrót na początek pętli

    # Przeniesienie wyniku z rejestru ST(0) (sumy ciągu)
    # do rejestru XMM0 przez stos i zakończenie funkcji
    koniec:
    fstp    %st
    fstpl   (%rsp)
    movsd   (%rsp), %xmm0

mov     %rbp, %rsp
pop     %rbp
ret

Specjalne podziękowania dla Mateusza Gniewkowskiego za pomoc w przygotowaniu tego wpisu :-)

6 komentarzy do “Architektura Komputerów 2 – Laboratorium nr 6 – Jednostka zmiennoprzecinkowa (FPU)”

  1. przepraszam ze tutaj komentuje, ale nie znalazlem innej drogi

    czy moglbys na nadajniki.olo.ovh zrobic jakas mozliwosc filtrowania po firmie? filtr pozytywny i negatywny (np. zeby mozna bylo odfiltrowac lokalna energetyke ktora ma wiekszosc infrastruktury w okolicach wiejskich i zostawic wszystko inne)

    1. niestety nie jest to możliwe, ale zapraszam na http://http://uke.kurylowicz.info/, to chyba to czego szukasz. moja mapa jest generowana w google fusion tables, jako warstwa mapy google. wykonałem to w ten sposób, ponieważ punktów jest na prawdę dużo, a chcę wyświetlić je wszystkie. jeśli miały by one być nanoszone na mapę w skrypcie js, co pozwoliło by na ich filtrowanie, załadowanie mapy po wejściu na stronę trwało by wieki.

    1. Proszę wyczyścić ciasteczka, tam zapisana jest zgoda. Coś musiało pójść nie tak. Czy mogę prosić o informacje jaki to telefon? Kopanie jest zablokowane na telefonach, a na komputerach nie powinno zużyć więcej jak 25% mocy CPU.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *