Język C dla mikrokontrolerów 8051. Komunikacja mikrokontrolera z otoczeniem.

Rzadko zdarza się, aby mikrokontroler, którego zamierzamy użyć, oferował nam wszystkie potrzebne układy peryferyjne. Jeżeli jednak już tak jest, to albo ma również szereg innych, zupełnie niepotrzebnych, albo też przeraża jego cena. Dobrym rozwiązaniem tak przedstawionego problemu jest użycie taniego mikrokontrolera i dołączenie do niego w możliwie jak najtańszy sposób możliwie jak najtańszych układów peryferyjnych. Tu jednak pojawia się pewien problem – dołączenie układów zewnętrznych...

„Problem” ten został rozwiązany różnie przez różnych autorów języków programowania.Najprostszy w użyciu pod tym względem jest Bascom, który zawiera biblioteki gotowych rozwiązań. Inaczej jest w wypadku C. Tu musimy o wszystko zadbać sami. No może o prawie wszystko, ponieważ obsługę sprzętowego portu UART (RS232) większość kompilatorów oferuje jako standard. Jest to związane ze specyfikacją ANSI dla standardu języka C, która mówi, że instrukcje printf, getchar, putchar wysyłają znak (pobierają z) do standardowego urządzenia wyjściowego (wejściowego). W przypadku komputera PC jest to monitor (i klawiatura). Trudno jednak wyobrazić sobie prosty sterownik zbudowany z użyciem mikrokontrolera podłączony do monitora. Oczywiście jest to możliwe, ale raczej nieopłacalne. W związku z tym standardowym urządzeniem wejścia/wyjścia dla mikrokontrolera jest port UART. Od niego też zaczniemy opis implementacji interfejsów.

UART, kojarzy się z RS232 – funkcje stdio.h

Niestety – w związku ze specyfiką podawanych w tym opisie informacji, będą one dotyczyć pakietu Raisonance. Instrukcje printf, getchar, putchar będą zapewne działać identycznie w programach napisanych przy pomocy kompilatorów pochodzących od różnych producentów, ale nastawy dotyczące szybkości przesyłanych danych mogą być wykonywane zupełnie inaczej i jeśli ktoś używa na przykład Keil, to musi sięgnąć do dokumentacji tego pakietu.
W asynchroniczny port UART spełniający wymogi standardu transmisji RS232 wyposażony jest prawie każdy mikrokontroler. Oczywiście podłączenie UART do linii transmisyjnej wymaga układu dopasowującego zbudowanego z zastosowaniem elementów dyskretnych lub układów scalonych np. typu MAX232. Standard transmisji wymaga bowiem aby poziomy logiczne napięć były równe: 5...12 V dla logicznej jedynki, -12...–5V dla logicznego zera.
Wymaga to zasilania układów dopasowujących z symetrycznego źródła napięcia, czyli najczęściej budowy przetwornicy. Wspomniany układ MAX232 zawiera wbudowane pompy ładunkowe wytwarzające z jednego napięcia zasilającego wymagane napięcia zarówno dodatnie, jak i ujemne. Uwalnia nas tym samym od konieczności stosowania symetrycznego zasilacza. Przytoczone tu zagadnienia techniczne dotyczą jednak samego układu mikrokontrolera oraz jego otoczenia – zazwyczaj nie wpływają na kształt programu napisanego w języku C.
Podobnie jak w przypadku rozwiązań innych problemów, również i tutaj mamy co najmniej dwie drogi poprawnego wykorzystania układu UART. Możemy na przykład skorzystać z systemu przerwań oferowanego przez mikrokontroler. Wówczas UART pracuje niejako w tle i dopiero skompletowanie słowa danych spowoduje, że zgłoszone zostanie przerwanie – w jego obsłudze możemy opróżnić bufor, odebrać dane itp. Inaczej jest (i według mnie jest to prostsze rozwiązanie), jeśli możemy oczekiwać na odbiór bajtu w pętli tworzonej przez kod instrukcji getchar(). Wówczas proste przypisanie znak= getchar() rozwiązuje problem odbioru bajtu. Wykorzystanie procesora nie jest jednak w tym przypadku optymalne. Może on sporą część czasu tracić bezproduktywnie na oczekiwanie znaku. Wykorzystanie przerwań pozwala mu zająć się innymi zadaniami w przerwach między odbieranymi danymi. Jedna i druga metoda jest prawidłowa, trzeba wybrać tę, która będzie bardziej adekwatna do naszych potrzeb.
Zacznijmy opis wykorzystania UART od moim zdaniem prostszej metody, tej która nie wykorzystuje przerwań. Funkcje obsługujące wysyłanie i odbiór znaków zdefiniowane są w bibliotece stdio.h. Aby ich użyć, musimy tę bibliotekę dołączyć dyrektywą #include. UART wykorzystuje Timer 1 do ustalenia szybkości transmisji. Timer pracuje w trybie 2, czyli jako ośmiobitowy z automatycznym odświeżaniem zawartości przy przepełnieniu. Szybkość pracy UART można więc ustalić wpływając na wartość bajtu ładowanego do rejestru TH1. Poniżej przytaczam wzór zaczerpnięty z instrukcji programowania mikrokontrolera 80C51 pozwalający wyliczyć wartość TH1 odpowiednią do danej szybkości transmisji:

TH1 = 256 – (k x częstotliwość kwarcu / (384 x szybkość transmisji))

„k” to mnożnik prędkości transmisji – dla bitu SMOD równego 0, wynosi on 1, natomiast dla SMOD ustawionego na 1, wynosi on 2. Spróbujmy wyliczyć zawartość TH1 dla kwarcu 11,0592 MHz, bitu SMOD = 0 oraz prędkości transmisji 9600 bodów:

TH1 = 256 – (1 x 11059200 / (384 x 9600)) = 253 (0xFD)

Jak teraz ustalić wartość rejestru TH1 dla procedur transmisji danych tak, aby funkcje zawarte w stdio.h mogły poprawnie ją odczytywać i interpretować?
I znowu można to zrobić na kilka sposobów. Można napisać samodzielnie procedurę inicjalizacji. Można również w parametrach kompilatora ustawić potrzebną wartość. Można też zmienić ją przy pomocy dyrektywy defj umożliwiającej modyfikację stałych systemowych. Jeśli zdecydowaliśmy się na zmianę ustawienia stałej systemowej bez pisania własnej procedury inicjalizacji, zdecydowanie nie polecam jej wykonywania korzystając z okienka opcji. Może się zdarzyć, że wartość ustawiona dla jednego programu nie będzie właściwą dla innego, natomiast system zapamięta ją jako domyślną. Jeśli zapomnimy o okienku opcji, nowy program po skompilowaniu nie będzie działał prawidłowo. Będziemy szukać błędu, który jest tym trudniejszy do lokalizacji, że nie znajduje się w kodzie źródłowym programu. Tak więc pozornie wszystko będzie w porządku, ale wartość TH1 ustawiona w opcjach kompilatora skutecznie uniemożliwi nam prawidłową pracę programu. Ten sam efekt, co przez zmianę opcji kompilatora, można osiągnąć używając dyrektywy defj. Postać jej użycia jest następująca:

#pragma DEFJ(TIM1_INIT=wartość)

Czyli dla przykładu:

#pragma DEFJ(TIM1_INIT=0xFD)

Zdecydowanie polecam właśnie to polecenie, jeśli nie chcesz pisać procedur do inicjalizacji UART. Uzbrojeni już w tę zupełnie podstawową wiedzę, możemy przejść do przykładów wykorzystania UART.
Listing 1 podaje przykład programu wysyłającego znaki do urządzenia podłączonego do UART. Na samym początku dołączane są zbiory biblioteczne niezbędne do prawidłowego jego funkcjonowania oraz inicjowana jest wartość TH1 przy pomocy defj. Znaki (bajty) wysyłane są przez funkcję putchar(). Domyślnie bit SMOD ma wartość „0”. Wydaje mi się, że rzut oka wystarczy, aby przesyłanie danych przez UART nie stanowiło już żadnego problemu.
 

List. 1.Tak można wysyłać znaki używając funkcji putchar(). Wyróżniono funkcje związane z UART.

/*****************************************
wysyłanie kodów ASCII przez UART
mikrokontroler AT89S8252, kwarc 11,0592 MHz
Raisonance RC-51
******************************************/
#include <reg52.h>    //definicje rejestrów
#include <stdio.h>    //dołączenie funkcji wejścia – wyjścia
#pragma DEFJ(TIM1_INIT=0xFD) //ustalenie szybkości transmisji

//funkcja realizuje opóźnienie około k*1ms
//dla rezonatora f=11.0592 MHz
void delay (unsigned int k)
{
    unsigned int i,j;
    for ( j = 0; j < k; j++)
      for (i = 0; i <= 84;)  i++;
}

//program główny, znaki o kodach od 0x20 do 0xFF są wysyłane
//kolejno przez UART co około 300 milisekund
void main(void)
{
    char i;
    for (i = 0x20; i <= 0xFF; i++) //pętla wykonywana, gdy i<=255
    {
        putchar(i);                //przesłanie bajtu
        delay (300);               //opóźnienie 0,3 sekundy
    }
}

Podobnie jest z odbiorem. Jednak zanim pokażę przykład odbioru danych, kilka słów wyjaśnienia. Typowo, do odbioru danych ze standardowego urządzenia wejścia – wyjścia (w naszym przypadku jest to UART) służy funkcja getchar(). Jednak tkwi w niej pewna „pułapka”. Zgodnie ze specyfikacją ANSI funkcja ta odsyła odebrany bajt. Można powiedzieć, że występuje efekt echa. I mimo iż jest to zgodne ze specyfikacją standardu, to najczęściej zupełnie niepotrzebne. Są oczywiście sytuacje, w których jest to bardzo wygodna metoda kontroli tego, co zostało wysłane, jednak w mojej praktyce zdarzało się to raczej rzadko. Częściej potrzebowałem po prostu odebrać bajt echo odsyłane do urządzenia nadającego, bardzo w tym przeszkadzało. Z moich doświadczeń wynika, że wśród firm piszących kompilatory przyjęło się pewien standard: obok funkcji getchar() definiowana jest funkcja getkey(), która odbiera znak i nie wysyła echa. Podobnie jest w przypadku kompilatora RC-51. Również i tu wśród funkcji biblioteki stdio.h znajdziemy _getkey(). Uzbrojony w tę wiedzę możesz sam zdecydować, czego chcesz używać. Listing 2 to fragment kodu napisanego dla programatora sterowanego przez port szeregowy.

 

List. 2.Fragment programu do obsługi programatora szeregowego. Wyróżniono funkcje związane z ustawieniem i transmisją UART.

#pragma DEFJ(TIM1_INIT=0xFE) //timer 1 ustala prędkość transmisji
                             //tu 19200 bodów (SMOD będzie równy „1”)
#pragma SMALL                //wybór modelu pamięci programu
#include <stdio.h>           //funkcje wejścia - wyjścia
#include <reg51.h>           //definicje rejestrów

//program główny
void main ()

{
    char temp, temp1, cmd1, cmd2, cmd3;
    set_reset();   //wystawienie sygnału reset dla programowanego uK
                   //faza reset jest zależna od stanu linii resettype
                   //1=AT90, 0=AT89
    PCON |= 0x80;  //ustawienie bitu SMOD na „1”
   
EI |= 0x81;    //włączenie przerwań i zezwolenie na int0
    clr_reset();   //zwolnienie reset z uwagami jak dla set_reset

    while (1)
    {
        while ((temp = _getkey()) == 0x1B);
        switch (temp)
        {
            case 'T':  //'T' typ urządzenia
                device = _getkey();
                put_ret();
                break;
            case 'S': //'S' rodzaj podłączonego programatora
                putchar('A'); //wysłanie napisu „AVR ISP”
                putchar('V');
                putchar('R');
                putchar(' ');
                putchar('I');
                putchar('S');
                putchar('P');

/* lub inaczej – znacznie prościej: printf(“AVR ISP”); instrukcja printf wykorzystuje funkcję putchar(), ale zajmuje to więcej miejsca w pamięci flash. Dodatkową korzyścią jest możliwość wyprowadzania sformatowanych wydruków np. printf(“%#bx”,165); spowoduje wyświetlenie liczby 165 w zapisie szesnastkowym 0xA5 */

                break;
            case 'V':         //'V' wersja programu
                putchar('1'); //wysłanie napisu „10”
                putchar('0');
                break;
            case 'v':         //'v' wersja urządzenia
                putchar('1'); //wysłanie napisu „10”
                putchar('0');
                break;

..............

 

Wyrażenie

while ((temp = _getkey()) == 0x1B)

to pętla, w której wykonywane są dwie operacje. Jedna z nich, to przypisanie zmiennej temp wartości bajtu odebranego przez UART. Druga, to porównanie tego bajtu z kodem ESC (0x1B) i zakończenia działania pętli, jeśli odebrany znak będzie różny od ESC. Zwróćmy uwagę na różnice pomiędzy operacją przypisania (zmienna = wartość) a porównania (zmienna == wartość).
Opisane przykłady transmisji są bardzo proste. Polegają tylko na dołączeniu biblioteki stdio.h, ustawieniu właściwej prędkości transmisji i wywołaniu odpowiedniej dla potrzeb funkcji. Troszkę inaczej (i trudniej) jest w przypadku wykorzystania przerwania. Listing 3 pokazuje przykład programu do obsługi UART wykorzystującego jego przerwanie.

 

List. 3.Przykład obsługi transmisji szeregowej w oparciu o przerwanie generowane przez UART.

/*****************************************
Obsługa transmisji szeregowej przez UART
z wykorzystaniem przerwań.
******************************************/

#include <reg51.h>
#define ROZM_BUFORA_TX 32
#define ROZM_BUFORA_RX 32
#define OSCYLATOR 11059200

unsigned char buf_wysylki[ROZM_BUFORA_TX];
unsigned char buf_odbioru[ROZM_BUFORA_RX];
unsigned char do_wysylki, wyslano;
unsigned char wysylka_wylaczona;
unsigned char do_odbioru, odebrano;

//funkcja obsługująca przerwanie UART; using 2 oznacza, że jest
//używany jest bank rejestrów R0..R7 numer 2
void UART_irq (void) interrupt 4 using 2
{
    if (RI != 0)  //fragment wykonywany, gdy jest znak
    {
        RI = 0;  //zerowanie flagi "do_odbioru"
        if ((do_odbioru+1) != odebrano) buf_odbioru[do_odbioru++] = SBUF;
                 //pobranie znaku do bufora odbioru, gdy jego
       }         //rozmiar jest wystarczający
       if (TI != 0) //fragment wykonywany, gdy znak do wysłania
       {
           TI = 0;  //zerowanie flagi "do wysyłki"
           if (do_wysylki != wyslano)
               SBUF = buf_wysylki[wyslano++];
                   //jeśli indeksy liczby znaków
                   //i liczby znaków do wysłania
           else wysylka_wylaczona = 1; //są różne, pobierz i wyślij
                                       //znak
       }
}


//obliczenie rozmiaru wolnego miejsca w buforze odbioru
unsigned char rozm_bufora_odbioru (void)
{
    return (do_odbioru - odebrano);
}

//obliczenie ilości znaków pozostających do wysyłki
unsigned char rozm_bufora_wysylki (void)
{
    return (do_wysylki - wyslano);
}

//ustawienie prędkości transmisji, inicjacja Timer'a 1
void UART_baudrate (unsigned char baudrate)
{
    EA = 0;  //wyłączenie przerwań
    TI = 0;  //kasowanie flagi przerwania od UART
    do_wysylki = wyslano = 0; //nie wysłano i niw odebrano danych
    wysylka_wylaczona = 1;    //wyłączenie funkcji nadawania
    TR1 = 0;                  //zatrzymanie timer'a 1
    ET1 = 0;                  //wyłączenie przerwań timera 1
    PCON |= 0x80;             //SMOD = 1, mnożnik dla kwarcu x2
    TMOD &= ~0xF0;            //ustawienie trybu pracy timer'a 1
    TMOD |= 0x20;
    //wyliczenie wartości dla TH1
    TH1 = (unsigned char)(256-(OSCYLATOR/(16L*12L*baudrate)));
    TR1 = 1;                  //uruchomienie timer'a 1
    EA = 1;                   //zezwolenie na przerwania
}

//inicjalizacja trybu transmisji szeregowej
void UART_inicjalizacja (void)
{
    UART_baudrate (19200);    //ustawienie prędkości transmisji
    EA = 0;                   //wyłączenie przerwań
    do_wysylki = wyslano = 0; //zerowanie indeksów nadawania i odbioru
    wysylka_wylaczona = 1;
    do_odbioru = odebrano = 0;
    SM0 = 0; SM1 = 1;         //trybu pracy UART = "mode 1"
    SM2 = 0;
    REN = 1;                  //zezwolenie na pracę odbiornika UART
    TI = RI = 0;              //kasowanie flag przerwania UART
    ES = 1;                   //zezwolenie na przerwania od UART
    PS = 0;                   //ustawienie niskiego priorytetu
    EA = 1;                   //załączenie przerwań
}

//przykład własnej implementacji funkcji wysyłającej znak przez UART
signed char _putchar(unsigned char c)
{
    //bufor zbyt mały, błąd
    if ((ROZM_BUFORA_TX-rozm_bufora_wysylki())<=2) return (-1);
    EA = 0;                   //wyłączenie przerwań
    buf_wysylki[do_wysylki++] = c;  //znak do bufora nadawania
    if (wysylka_wylaczona)    //jeśli nadawanie jest wyłączone
    {
        wysylka_wylaczona = 0;
        TI = 1;               //załącz je
    }
    EA = 1;                   //załączenie przerwań
    return (0);               //jeśli operacja poprawna, zwróć 0
}

//przykład wykonania funkcji odbierającej znak z UART
signed int _getchar (void)
{
    unsigned char c;
    if (rozm_bufora_odbioru() == 0)
        return (-1);          //brak odebranych znaków, błąd
    EA = 0;                   //wyłączenie przerwań
    c = buf_odbioru[odebrano++]; //pobranie znaku z bufora
    EA = 1;                   //załączenie przerwań
    return (c);
}

Wróćmy jeszcze na moment do biblioteki stdio.h. Jej opis nie byłby kompletny bez kilku słów na temat funkcji ungetchar() i chyba najważniejszych printf() i scanf().
Jak wspomniałem wcześniej, specyfikacja ANSI dla języka C wymaga, aby funkcja getchar() przesyłała do nadajnika echo odebranego znaku. Nie jest to zbyt użyteczne dla aplikacji związanych z mikrokontrolerami. Ten problem rozwiązuje funkcja _getkey(), która działa identycznie jak getchar() i nie wysyła echa. Do zestawu tych funkcji dołączona jest jeszcze ungetchar(), która umieszcza znak odebrany przez getchar() lub _getkey() z powrotem w buforze odbioru tak, że następne wywołanie getchar() spowoduje odebranie tego samego znaku. Jest ona użyteczna wówczas, gdy kilka różnych funkcji w programie, niezależnie od siebie, korzysta ze znaków odebranych przez UART. Można na przykład wyobrazić sobie sytuację, gdy odebrany znak jest kodem sterującym przeznaczonym dla innej funkcji niż ta, która go odebrała. Odebranie znaku zeruje flagę RI oznaczającą gotowość bajtu do odbioru – ponowne użycie getchar() nie jest możliwe. Wówczas ungetchar() przywraca stan taki, jakby znak był właśnie przed chwilą odebrany. Można wtedy przekazać sterowanie do innego fragmentu programu, który odbierze bajt i właściwie go zinterpretuje.
Funkcja obsługująca standardowe wyjście danych printf() tłumaczy wewnętrzne wartości na znaki. Sposób jej wywołania jest następujący: int printf(char *format, arg1 [, arg2, ...., arg-n] ). Funkcja printf() wykorzystuje putchar() do wysyłania łańcucha znaków powstałego na skutek przekształcenia do wymaganego formatu. Przekształcenie odbywa się zgodnie z wzorcem zawartym w argumencie format. Zawiera on różnego rodzaju obiekty – zwykłe znaki, które są kopiowane wprost do łańcucha wyjściowego oraz specyfikacje różnych przekształceń, z których każda wskazuje na sposób przekształcenia i wysłania kolejnego argumentu printf(). Każdą specyfikację formatu rozpoczyna znak % a kończy znak charakterystyczny dla danego przekształcenia. Pomiędzy znakiem % a znakiem przekształcenia mogą wystąpić dodatkowe symbole sterujące w kolejności takiej, jak poniżej:
· „-” przesuwający przekształcony argument do lewej strony,
· liczba określająca minimalny rozmiar pola,
· „.” oddzielająca rozmiar pola od jego precyzji (części ułamkowej),
· liczba określająca precyzję, to jest maksymalną liczbę znaków dla tekstu, liczbę cyfr po kropce dziesiętnej dla wartości zmiennopozycyjnej lub minimalną liczbę znaków dla wartości stałopozycyjnej,
· „h” lub „l” (litera małe L) jeśli argument całkowity należy wyświetlić odpowiednio – w postaci short lub w postaci long.
Spójrz na tabelę 1. Zebrałem w niej najważniejsze moim zdaniem znaki kontrolujące przekształcenia danych.

 

Tab. 1. Podstawowe przekształcenia funkcji printf()

Znak

Typ argumentu

Przekształcenie do postaci

d, i

int

liczba dziesiętna ze znakiem

o

int

liczba ósemkowa bez znaku i wiodącego zera

x, X

int

liczba szesnastkowa bez znaku i wiodącego 0x

użycie małej litery x w konsekwencji powoduje przy przekształcaniu stosowanie znaków abcdef, natomiast dużego X – ABCDEF

u

int

liczba dziesiętna bez znaku

c

int

pojedynczy znak

s

char*

tekst, wypisywany do momentu napotkania znaku końca tekstu \0 lub osiągnięcia rozmiaru (precyzji) pola

f

double

liczba dziesiętna zmiennopozycyjna, gdzie liczbę cyfr po kropce dziesiętnej określa precyzja

e, E

double

liczba dziesiętna w postaci wykładniczej

p

void*

wskaźnik, reprezentacja zależy od implementacji w konkretnej bibliotece stdio.h

%

 

brak przekształcenia, wypisywany jest znak %

 

Funkcja scanf() jest odpowiednikiem printf() lecz działającym w przeciwną stronę. To znaczy wprowadza ona znaki ze standardowego wejścia, interpretuje je zgodnie z informacjami zawartymi w format oraz zapamiętuje w miejscach określonych przez pozostałe argumenty. Jej wywołanie ma postać: int scanf(char *format, *arg1[, *arg2, ....]). Odczyt danych ze standardowego wejścia zakończy się, gdy scanf() zinterpretuje wszystkie znaki lub dane nie pasują do specyfikacji przekształcenia. Każdy z argumentów funkcji scanf() musi być wskaźnikiem. Do interpretacji wprowadzanego ciągu znaków używane są te same symbole przekształceń, co dla printf().
Funkcjami printf() i scanf () zajmiemy się jeszcze osobno w następnym odcinku kursu a zwłaszcza pierwszą z nich. Jak wcześniej wspomniałem, printf() używa do wysyłania znaków putchar(), toteż zmieniając putchar() w taki sposób aby znaki były kierowane do wyświetlacza LCD zamiast do portu UART, można wykorzystać bogate możliwości formatowania wyświetlanego tekstu. Przyda się to zwłaszcza przy wyświetlaniu liczb zmiennopozycyjnych. Jednak temat związany z przykładami użycia printf(), scanf() i formatowania tekstu jest tak obszerny, że w zupełności wyczerpałby ramy tego odcinka kursu.

SPI, czyli Serial Peripherial Interface a język RC-51

Popularność interfejsu SPI rośnie w bardzo szybkim tempie. Pozwala on na szybką komunikację pomiędzy układami, co jest ogromnie przydatne w nowoczesnych aplikacjach mikrokontrolerów. Kilka słów o zasadzie działania.
Podczas transmisji danych są one wysyłane (szeregowo) przez port MOSI i równocześnie odbierane przez port MISO (również szeregowo). Sygnał SCK synchronizuje przesuwanie, ustawianie stanu wyjścia i próbkowanie stanu wejścia. Sygnał wyboru układu slave SS wybiera indywidualny układ, z którym następuje wymiana danych. Linie MISO i MOSI są jednokierunkowe. Port MISO zawsze pracuje jako wejście, MOSI jako wyjście. Inaczej jest z linią SCK. Jej stan powinien być badany przed wysłaniem danych. W momencie tego „badania” musi ona pracować jako wejściowa. Później jej rola zmienia się na wyjściową, ponieważ układ master wysyła sygnał zegarowy synchronizujący transmisję. Jednak stosując konfigurację z pojedynczym układem master, również i SCK może być zwykłą, jednokierunkową linią portu I/O.
Jako reprezentant układów z rodziny MCS51 z wbudowanym sprzętowym interfejsem SPI posłuży nam AT89S8252. Maksymalna szybkość transmisji jego interfejsu wynosi 1,5Mbit. Istnieje możliwość jej zmiany przez ustawienie odpowiednich bitów rejestrów kontrolnych pełniących rolę nastaw wewnętrznego preskalera. Do ustawiania trybów pracy oraz kontroli statusu interfejsu SPI służą trzy ośmiobitowe rejestry. Ich rola i działanie jest identyczne tak w AT89S8252 jak i w mikrokontrolerach z rodziny AVR. Należą do nich:
· rejestr kontrolny SPCR (umieszczony w obszarze rejestru funkcji specjalnych pod adresem D5H),
· rejestr statusu SPSR (jak wyżej, pod adresem AAH),
· rejestr danych SPDR (jak wyżej, pod adresem 86H).
Po ustawieniu bitu SPE (SPI Enable) znajdującego się w rejestrze SPCR, wyprowadzenia portu P1 mikrokontrolera przejmują rolę linii interfejsu SPI i podłączane są odpowiednio: P1.4 jako SS, P1.5 – MOSI, P1.6 – MISO, P1.7 – SCK.

 

SPCR (Rejestr kontrolny, 96H)

SPIE

SPE

DORD

MSTR

CPOL

CPHA

SPR1

SPR0

 

SPIE                     ustawienie tego bitu oraz bitu ES w rejestrze IE powoduje zezwolenie na generowanie przerwań przez interfejs SPI; generowany jest wektor przerwania numer 5 (23H)

SPE                 ustawienie bitu SPE powoduje załączenie interfejsu SPI oraz połączenie odpowiednich wyprowadzeń portu P1 z liniami interfejsu

DORD             ustawienie bitu DORD powoduje, że jako pierwszy wysłany (przyjęty) zostanie młodszy bit słowa, natomiast wyzerowanie – starszy

MSTR              ustawienie bitu powoduje, że interfejs SPI pracuje w trybie master; wyzerowanie powoduje przejście do trybu slave

CPOL           polaryzacja sygnału zegarowego; stan tego bitu określa, czy pierwsze zbocze sygnału zegarowego SCK jest narastające (zmiana SCK z „0” na „1”, SCK jest „0” gdy SPI nie pracuje), czy też opadające (zmiana SCK z „1” na „0”, SCK jest „1” gdy SPI nie pracuje)
 

Stan bitu CPOL

Zbocze sygnału SCK

uruchamiające transmisję

Zbocze sygnału SCK

kończące transmisję

CPOL = 0

narastające

opadające

CPOL = 1

opadające

narastające

 

CPHA              faza sygnału zegarowego; stan tego bitu określa moment próbkowania i wyprowadzania informacji poprzez interfejs SPI
 

Stan bitu CPHA

Pierwsze zbocze SCK (odpowiednio do CPOL)

Drugie zbocze SCK (odpowiednio do CPOL)

CPHA = 0

próbkowanie MISO

ustawianie MOSI

CPHA = 1

ustawianie MOSI

próbkowanie MISO

 

Stan bitów CPOL i CPHA

Pierwsze zbocze SCK

Drugie zbocze SCK

Tryb pracy SPI

CPOL = 0, CPHA = 0

próbkowanie MISO

ustawianie MOSI

0

CPOL = 0, CPHA = 1

ustawianie MOSI

próbkowanie MISO

1

CPOL = 1, CPHA = 0

próbkowanie MISO

ustawianie MOSI

2

CPOL = 1, CPHA = 1

ustawianie MOSI

ustawianie MOSI

3

Tabela 2. Różne ustawienia bitów CPOL i CPHA powodują różne tryby pracy interfejsu SPI

 

SPR1, SPR0  bity preskalera sygnału zegarowego służące do ustawiania prędkości transmisji interfejsu SPI; nastawy bitów dotyczą tylko pracy interfejsu w trybie master. Częstotliwość sygnału SCK = FOSC / n
 

SPR1

SPR0

Współczynnik podziału n

0

0

4

0

1

16

1

0

64

1

1

128

 

Uwaga: pamiętajmy, że maksymalna częstotliwość sygnału SCK dla AT89S8252 wynosi 1,5 MHz.

 

SPSR (Rejestr statusu, AAH)

SPIF

WCOL

-

-

-

-

-

-

 

SPIF            ustawienie SPIF oznacza skompletowanie słowa odbieranego / przesyłanego poprzez interfejs SPI i wygenerowanie przerwania, jeśli bity SPIE oraz ES są ustawione; bit jest zerowany podczas odczytu rejestru statusu i później dostępu do rejestru danych SPI

WCOL             bit jest ustawiany, jeśli nastąpiła próba zapisu danych podczas trwającej transmisji SPI; bit kasowany w wraz z bitem SPIF

Obsługa sprzętowego interfejsu SPI jest bardzo prosta. Można ją zrobić na co najmniej dwa sposoby: wykorzystując przerwanie generowane przez interfejs SPI lub testując stan bitu SPIF podczas transmisji. Prezentowane poniżej przykłady programów napisanych w języku C wykorzystują mechanizm przerwań. Jest on bardzo wygodny, ponieważ wygenerowanie przerwania upewnia nas, że dane zostały wysłane, czy też odebrane. Nie ma potrzeby wykonywania dodatkowych testów i marnowania cennego czasu mikrokontrolera.

 

Listing 4. Wysłanie bajtu danych, SPI w trybie master. Flaga kolizji nie jest badana.

#include <reg8252.h>
unsigned char spistatus;      //zmienna pomocnicze
sbit   SS = P1^4;             //linia portu sygnału SS
//obsługa przerwania interfejsu SPI (23H)
void SPI_Irq(void) interrupt 5
{
    spistatus = 0;
}

//inicjowanie trybu master interfejsu SPI
void SPI_Master(void)
{
    while (SS != 1);          //oczekiwanie na zwolnienie linii SS
    SPCR = 0xF8;              //ustalenie trybu pracy SPI
}

//wysłanie bajtu przez interfejs SPI
void SPI_Send(unsigned char x)
{
    SPIE = SPE = 0;            //wyłączenie przyjmowania przerwań
    SPI_Master();              //ustalenie trybu master dla SPI
    SS = 0;                    //zerowanie linii wyboru slave SS
    spistatus = 1;             //znak nie został wysłany,spistatus=1
    SPIE = SPE = 1;            //zezwolenie na przyjmowanie przerwań
    SPDR = x;                  //zapis bajtu do rejestru danych SPI
    while (spistatus == 1);    //oczekiwanie na zakończenie wysyłania
                               //bajtu danych
}

 

List. 5. Przykład programu odbierającego znaki w trybie slave.

#include <reg8252.h>
sbit SS = P1^4;                //deklaracja linii SS
unsigned char counter;         //licznik odebranych bajtów
unsigned char buferror;        //błąd przepełnienia bufora SPI
unsigned char idata bufor[20]; //bufor odebranych znaków (20 bajtów)
unsigned char ptr = *bufor;    //wskaźnik do bufora

//procedura obsługi przerwania interfejsu SPI (23H)
//napisana tylko jako przykład programowania!
void SPI_Irq(void) interrupt 5
{
    ptr* = SPDR;               //zapis odebranego znaku do bufora
    ptr++;                     //następna pozycja
                               //błąd, gdy odebrano zbyt dużo znaków
    if (counter++ >20) buferror = 1;
}

//ustawienie trybu slave dla SPI
void SPI_Slave(void);
{
    SPCR = 0xE8;               //bit MSTR = 0
}

//odbiór znaków
void SPI_Receive(void)
{
    SPIE = SPE = 0;            //wyłączenie przyjmowania przerwań
    buferror = counter = 0;    //zerowanie licznika oraz flagi błędu
    ptr = &bufor;              //przypisanie wskazań na początek
                               //bufora odbioru znaków
    SS = 1;                    //zwolnienie SS (na wszelki wypadek)
    SPI_Slave();               //załączenie trybu slave
    SPIE = SPE = 1;            //zezwolenie na przerwania SPI
    . . .

}

 

Przykład programowej obsługi I2C w języku C

Zróbmy króciutką powtórkę wiadomości. Interfejs I2C pozwala na komunikację szeregu różnych układów poprzez prostą, dwuprzewodową magistralę. Dwie dwukierunkowe linie, SDA i SCL, służą do transmisji danych pomiędzy układami. Linia SDA nazywana jest linią danych, natomiast linia SCL linią zegara. Każde urządzenie podłączane jest do nich wprost – nie są wymagane żadne dodatkowe obwody sprzęgające. Ewentualnie wejścia portów mogą być podłączone przez rezystor szeregowy. Układy wyposażone w interfejs I2C mogą pracować w jednym z dwóch możliwych trybów:
- master, to znaczy układ przejmuje kontrolę nad magistralą i przebiegiem transmisji danych, wysyła sekwencję start i stop oraz sygnał zegarowy,
- slave, funkcje układu są kontrolowane przez urządzenie master.
Typowo system zbudowany z wykorzystaniem magistrali I2C posiada jeden mikrokontroler pracujący w trybie master oraz szereg układów slave. Każdy z układów slave musi mieć własny, unikatowy adres. Mikrokontroler odczytując, czy też zapisując dane, inicjuje transmisję posługując się adresem konkretnego układu.
Niektóre z układów podłączonych do magistrali I2C mogą nie tylko odczytywać, lecz również wysyłać dane. Jednym słowem pracują albo jako nadajnik, albo jako odbiornik transmisji. Należą do nich na przykład układy pamięci, czy też przetworników analogowo - cyfrowych. Możliwość wysyłania danych nie determinuje funkcji danego układu jako master. Układ przesyłający nie musi bowiem przejmować kontroli nad ich transmisją – korzysta z sygnałów sterujących obecnych na magistrali I2C. Zgodnie z intencją autorów specyfikacji I2C, układem master jest każdorazowo ten, który inicjuje transmisję wysyłając sygnał „start” przerywa ją wysyłając sygnał „stop” oraz generuje sygnał zegara.
Transmisja odbywa się synchronicznie z sygnałem SCK. Może odbywać się bardzo wolno. Nie są określone żadne wymagania co do minimalnej szybkości. Inaczej jest z jej górną granicą. Ta jest określona jako 100 kbit/s w trybie standard, 400 kbit/s w trybie szybkim oraz 3,4 Mbit/s w trybie „high speed”.
Aczkolwiek typową jest konfiguracja z jednym układem master, możliwe jest również podłączenie wielu układów tego typu do jednej magistrali. Używana jest wówczas procedura tak zwanego arbitrażu w celu rozsądzenia, który z układów master przejmie kontrolę nad magistralą a który będzie czekał. Kilka słów wyjaśnienia. W normalnej sytuacji arbitraż nie jest potrzebny, ponieważ transmisja danych może być zainicjowana tylko wtedy, gdy magistrala jest wolna. Jednak może zdarzyć się tak, że na przykład dwa układy master podłączone do tych samych linii interfejsu nieomal jednocześnie wygenerują sygnał startu. Wówczas arbitraż staje się niezbędny.
Listing 6 pokazuje jak można napisać program do obsługi trybu pojedynczego układu master w języku C. Jest to przykład, który może przydać się wówczas, gdy do magistrali I2C podłączone są układy typu pamięć eeprom, przetwornik analogowo – cyfrowy, układ zegara i pojedynczy mikrokontroler korzystający z tych zasobów sprzętowych. Funkcja Delay() pojawiająca się w różnych miejscach interfejsu, powinna absorbować mikrokontroler na czas około 5..6 µs. W przypadku mikrokontrolera z rodziny 8051 pracującego z zegarem 10..12 MHz, wystarczy 6 instrukcji NOP.

 

List. 6.Przykład realizacji interfejsu I2C dla pojedynczego układu master

/**********************************
pojedynczy układ master I2C
RC-51
**********************************/

#include <reg51.h>
sbit   SDA    P1^0;            //definiowanie połączeń I2C
sbit   SCL    P1^1;

 

//wysłanie sekwencji START, bez badania stanu zajętości I2C

void I2C_Start(void)
{

    SDA = SCL = 1;
    Delay();
    SDA = 0;
    Delay();
    SCL = 0;
}

/* wysłanie sekwencji STOP, funkcja zwraca stan linii SDA
 po zakończeniu; jeśli układ slave wymusza stan niski SDA – błąd transmisji */

bit I2C_Stop(void)
{
    SDA = 0;
    Delay();
    SCL = 1;
    Delay();
    SDA = 1;
    Delay();
    return (~SDA);
}

/* odczyt bajtu z magistrali I2C; jako parametr wywołania podawany jest bit ACK, ponieważ niektóre bajty odczytywane są z NACK */

unsigned char I2C_Read(bit ack)
{
    unsigned char bitCount = 8, temp;
    SDA = 1;
    do
    {
        Delay();
        SCL = 1;
        Delay();
        temp <<= 1;          //wprowadzenie 0
        if (SDA) temp++;     //ustawienie bitu, gdy SDA jest wysoki
        SCL = 0;
    } while(--bitCount);
    SDA = ack;               //wysłanie bitu ACK

    Delay();
    SCL = 1;
    Delay();
    SCL = 0;
    return (temp);
}

/* wysłanie słowa na magistralę I2C, funkcja zwraca stan bitu ACK */
bit I2C_Send(unsigned char byte)
{
    unsigned char bitCount = 9;
    bit temp;
    do
    {
        SCL = 0;
        SDA = byte & 0x80;   // stan SDA jako wynik iloczynu bitowego
        byte = (byte << 1) + 1;
        Delay();
        SCL = 1;

        Delay();
    } while(--bitCount);

    temp = SDA;
    SCL = 0;
    return (temp);           // ACK = 0, NACK = 1
}

Interfejs 1-Wire i język C

Początkowo bardzo egzotyczny interfejs 1-Wire (pamiętam na przykład własną reakcję na pierwsze artykuły w EP na ten temat) zyskuje coraz więcej zwolenników. Transmisja danych z jego użyciem nie jest może oszałamiająco szybka, lecz podłączenie powiedzmy czujników temperatury, czy układów sterowania światłem, przy użyciu tylko jednego przewodu wiodącego sygnał i zasilanie (oczywiście konieczne jest również masa, ale może być poprowadzona na szereg rozmaitych sposobów), to bardzo ciekawa alternatywa dla wielu urządzeń. Jak działa interfejs 1-Wire?
Interfejs 1W oryginalnie był przeznaczony do komunikacji na bardzo małe odległości. Służył do podłączenia na przykład układu zewnętrznej pamięci do mikrokontrolera z użyciem tylko jednego wyprowadzenia. Wkrótce jednak klienci zaczęli się domagać rozwiązań umożliwiających podłączenie układów znajdujących się w znacznie większej odległości. Wówczas to opracowano zupełnie nowe protokoły transmisji danych uwzględniające większe długości połączeń, pracę w sieci i mechanizmy kontroli przesyłanych danych.
Podobnie jak dla większości interfejsów szeregowych, również i dla 1-Wire transmisja przebiega w konfiguracji master – slave. Układ master wyszukuje i adresuje układ slave, steruje przesyłaniem danych. Dane przesyłane są synchronicznie z prędkością do 16,3 kbps (w trybie overdrive do 115 kbps). W dużym uproszczeniu można powiedzieć, że każde opadające zbocze sygnału inicjuje i synchronizuje przesyłany bit. Podobnie jak przy transmisji I2C, nie są określone dolne granice częstotliwości przesyłanych sygnałów. Pozwala to na prostą implementację programów obsługi transmisji w standardzie 1-Wire. Nie jest bowiem wymagany ścisły pomiar czasu impulsów tak, jak przy realizacji programowego interfejsu UART.

 

List. 6. Przykład programowej realizacji interfejsu 1-Wire

/********************************************
  Biblioteka procedur obsługi magistrali 1W
  Raisonance RC-51
********************************************/

#include <reg51.h>

#define MATCH_ROM    0x55    //definicje komend 1W
#define SKIP_ROM     0xCC
#define SEARCH_ROM   0xF0
#define SEARCH_FIRST 0xFF
#define PRESENCE_ERR 0xFF
#define DATA_ERR     0xFF
#define LAST_DEVICE  x00     //0x00 : znaleziono urządzenie
                             //0x01 ... 0x40: kontynuacja poszukiwania

 

#define      XTAL   110592   //definicja rezonatora (8..25MHz!)
#define      nop()  ACC++    //opóźnienie, 1 cykl maszynowy
sbit   one_wire_IO = P1^0;   //definicja podłączenia linii portu 1W

//wysłanie polecenia "1W Reset"
bit one_wire_reset(void)
{
       unsigned char delay;
       bit err;
       //pętla opóźnienia 480 < t < 960 cykli
       delay = (unsigned char)(XTAL/12e6*480/4);
       do //pętle opóźniające służą do wytworzenia
       {  //tzw. time slots, których dokładny opis
           one_wire_IO = 0; //można znaleźć w opisie standardu 1W
           nop();
       } while(--delay);
          //pętla opóźnienia 60 < t < 75 cm.
       delay = (unsigned char)( XTAL / 12e6 * 66 / 2 );
       do
       {
           one_wire_IO = 1;
       }
       while(--delay);
       err = one_wire_IO;   //stan niski oznacza, że urządzenie(a)
                            //1W jest(są) podłączone
                            //opóźnienie 480 < t
       delay = (unsigned char)(XTAL/12e6*(480-66)/4);
       do
       {
           nop();
           nop();
       } while(--delay);
       err = one_wire_IO
       return !err          //poziom niski portu 1W oznacza błąd
}

//przesłanie lub odczyt bitu przez linię 1W
bit one_wire_bit_io(bit b)
{
    unsigned char delay;
    delay = (unsigned char)(XTAL/12e6*15/2-2); //15 > t
    one_wire_IO = 0;       //1
    one_wire_IO = b;       //3
    while(--delay);        //3 + delay * 2
    b = one_wire_IO;
    delay = (unsigned char)(XTAL/12e6*45/2);//60 < t
    while(--delay);
    one_wire_IO = 1;
    return b;
}

//przesłanie bajtu przez linię 1W
unsigned char one_wire_byte_write(unsigned char b)
{
    unsigned char bit_counter = 8;
    do
    {
        b = b >> 1 | (one_wire_bit_io( b & 1
) ? 0x80 : 0);
    } while(--bit_counter);
    return b;
}

//odczyt bajtu z linii 1W
unsigned char one_wire_byte_read( void )
{
    return one_wire_byte_write(0xFF);
}

/* wysłanie komendy (komend) do urządzenia 1W; lista 8 bajtów do wysłania wskazywana przez ptr wypełnienie tablicy wskazywanej przez ptr oznacza,że komendy dotyczą konkretnego urządzenia podłączonego do 1W; wówczas to tablica powinna zawierać jego identyfikator */

void one_wire_send_command(unsigned char command, unsigned char *ptr)
{
    unsigned char byte_count = 8;
    one_wire_reset();
    if(ptr)

    {
       one_wire_byte_write(MATCH_ROM);  //komendy przesyłane do urz.
       do
       {
           one_wire_byte_write(*ptr);
           ptr++;
       } while(--byte_count);
    }
    else

        //ptr = null, komenda dla wszystkich urządzeń
        one_wire_byte_write(SKIP_ROM);
        one_wire_byte_write(command);
}

 

Na listingu 6 spotkać można jedną ciekawą konstrukcję. Wygląda ona tak:

b = b >> 1 | (one_wire_bit_io( b & 1 ) ? 0x80 : 0);

Użyto w niej tak zwanego operatora warunkowego. Działa on w ten sposób, że najpierw wyznaczana jest wartość pierwszego wyrażenia one_wire_bit_io(b & 1). Jeśli wartość tego wyrażenia jest różna od 0, to wynikiem jest wartość wyrażenia drugiego - 0x80, w przeciwnym wypadku trzeciego - 0.

 

Jacek Bogusz
j.bogusz@easy-soft.net.pl

http://www.tomaszbogusz.blox.pl/

Dodaj nowy komentarz

Zawartość pola nie będzie udostępniana publicznie.