Poznaliśmy już jeden interfejs szeregowy, który był asynchroniczny. Oczywiście chodzi o UART. Teraz dla odmiany pora na bardzo popularny, interfejs synchroniczny, którym jest SPI.
W tej części kursu STM32 wykorzystamy go do podłączenia ekspandera portów.
Uwaga! Ten kurs został zarchiwizowany. Sprawdź najnowszy kurs STM32 »
SPI vs. UART - najważniejsze różnice
Interfejs UART wykorzystywaliśmy do komunikacji z komputerem PC. Jego podstawową cechą jest asynchroniczność - czyli brak konieczności przesyłania sygnału zegarowego. Jak pamiętamy interfejs do komunikacji wykorzystuje raptem dwie linie RX oraz TX. Wadą takiego rozwiązania jest niewielka prędkość transmisji. Więcej na ten temat znaleźć można w 5 części kursu STM32.
Teraz poznamy nowy interfejs - SPI. Jest to interfejs synchroniczny, więc sygnał zegarowy będzie przesyłany między komunikującymi się układami. W odróżnieniu od UART, w przypadku SPI, komunikujące się układy nie są równorzędne. Jeden jest układem nadzorującym (ang. Master), a drugi podwładnym (ang. Slave). Komunikacja, podobnie jak poprzednio, odbywa się jednocześnie w obu kierunkach.
Interfejs pozwala również na podłączenie więcej niż jednego slave-a do magistrali.
Podłączenie SPI
Schemat podstawowego podłączenia jest następujący:
Źródło: Wikipedia
Jak widzimy do podłączenia potrzebujemy 4 linii. Najczęściej master-em jest mikrokontroler i takim przypadkiem będziemy się dalej zajmować. Można również wykorzystywać STM32 jako slave, ale wbrew pozorom jest to trudniejszy przypadek, niż wersja master.
Pierwsza linia jest oznaczana SS (Slave Select), która służy do wyboru urządzenia – pozostałe linie mogą być podpięte do wielu układów, ale tylko jeden może być w danej chwili aktywny. Linia ta często jest nazywana CS, czyli Chip Select - i tej nazwy będziemy używać podczas kursu.
Pozostałe linie to:
MOSI (Master Output Slave Input) – dane wysyłane z mastera do slave
MISO (Master Input Slave Output) – dane wysyłane z slave do mastera
SCLK (Serial Clock) – linia zegara
Dzięki temu, że sygnał zegarowy jest przesyłany na liniach interfejsu, komunikacja jest znacznie łatwiejsza. Możliwe jest również uzyskanie znaczenie większych prędkości transmisji niż w przypadku interfejsu UART.
Podłączając więcej niż jeden układ peryferyjny do interfejsu SPI, będziemy potrzebowali kolejnych linii CS - po jednej na każdy podłączony układ slave.
Wybór aktywnego układu dokonywany jest przez wystawienie stanu niskiego na odpowiedniej linii CS. Tylko jeden układ może być aktywny w danej chwili (pozostałych liniach CS stan wysoki).
Źródło: Wikipedia
Teraz pora na przejście do praktyki. Na początku zajmiemy się ekspanderem portów, a w kolejnej części wyświetlaczem graficznym!
Czas przejść do praktyki!
Komunikacja przez SPI
Mikrokontroler STM32F103RB posiada dwa interfejsy SPI, każdy o prędkości transmisji do 18 Mbit/s. Do przykładów wykorzystamy pierwszy interfejs, czyli SPI1. Na początek musimy odszukać w dokumentacji, na których pinach dostępne są wyprowadzenia interfejsu:
SPI1_SCK - PA5
SPI1_MISO - PA6
SPI1_MOSI - PA7
Moglibyśmy wykorzystać sprzętowe sterowanie linią CS (dostępna na linii PA4), jednak w przypadku master-a sterowanie jest bardzo proste - wystarczy wystawić logiczne 0 na początek transmisji oraz 1, gdy komunikacja zostanie zakończona.
Wykorzystamy więc zwykłą linię GPIO. Dzięki temu będziemy mogli podłączyć więcej niż jeden układ slave (sprzętowo możemy obsłużyć tylko jedną linię CS, więc tylko jeden układ slave). Sprzętowe sterowanie linią CS jest natomiast bardzo wygodnie, gdybyśmy implementowali układ typu slave.
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!
Masz już zestaw? Zarejestruj go wykorzystując dołączony do niego kod. Szczegóły »
Wstępne ustawienia SPI
Chcąc oprogramować interfejs SPI, zaczynamy jak zawsze - od podłączenia zegarów:
C
1
2
3
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_SPI1_CLK_ENABLE();
Następnie konfigurujemy piny interfejsu. Zaczniemy od linii wyjściowych, czyli SCK oraz MOSI:
C
1
2
3
4
5
6
GPIO_InitTypeDef gpio;
gpio.Mode=GPIO_MODE_AF_PP;
gpio.Pin=GPIO_PIN_5|GPIO_PIN_7;// SCK, MOSI
gpio.Pull=GPIO_NOPULL;
gpio.Speed=GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio);
Konfiguracja przypomina stosowaną dla linii TX interfejsu UART. Jest to wyjście sterowane sprzętowo, w trybie push-pull. Zmieniamy prędkość linii na wysoką - domyślnie konfiguracja używa linii o maksymalnej prędkości 2 MHz, więc zbyt wolnych dla interfejsu, który może osiągać prędkość nawet 18 Mbit/s (czyli 18 MHz).
Interfejs SPI w który wyposażony jest STM32F103RB
może pracować z prędkością do 18 Mbit/s.
Kolejny krok to konfiguracja linii wejściowej MISO. Tutaj też zobaczymy podobieństwo do RX z UART:
C
1
2
3
gpio.Mode=GPIO_MODE_AF_INPUT;
gpio.Pin=GPIO_PIN_6;// MISO
HAL_GPIO_Init(GPIOA,&gpio);
Ostatni krok to linia CS. Stosujemy sterowanie programowe, możemy więc wybrać dowolny, dostępny pin. Wykorzystamy PC0 ponieważ mamy już opanowane sterowanie portem C:
C
1
2
3
4
gpio.Mode=GPIO_MODE_OUTPUT_PP;
gpio.Pin=GPIO_PIN_0;// CS
HAL_GPIO_Init(GPIOC,&gpio);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_0,GPIO_PIN_SET);
Zaraz po inicjalizacji linii wystawiamy na niej logiczną 1. Linia CS jest aktywowana stanem niskim, a nie chcemy, żeby układ podłączony do tej linii CS działał w tej chwili - wystawiając 1, wyłączamy komunikację z podłączonym układem slave.
Konfiguracja interfejsu SPI
Mając skonfigurowane piny, możemy uruchomić sam interfejs SPI. Postępowanie przebiega jak zwykle: deklarujemy zmienną konfiguracyjną typu SPI_HandleTypeDef, ustawiamy interesującą nas konfigurację, po czym wywołujemy HAL_SPI_Init.
Na początek użyjemy trochę uproszczonej konfiguracji:
Ustawiamy tryb pracy jako SPI_MODE_MASTER. Ponieważ będziemy sami sterować linią CS (NSS to inna nazwa CS) wybieramy tryb programowy SPI_NSS_SOFT. Zegar systemowy, jak pamiętamy ma częstotliwość 8 MHz. Stosujemy dzielnik 8, aby otrzymać częstotliwość równą 1 MHz (czyli 1 Mbit/s). Po inicjalizacji SPI, uruchamiamy interfejs wywołując funkcję __HAL_SPI_ENABLE.
Procedura komunikacji
Na początek użyjemy metody podobnej jak w poprzedniej części kursy - nie jest to optymalne rozwiązanie dla biblioteki HAL, ale pozwoli nam poznać działanie funkcji HAL_SPI_TransmitReceive.
Gdy mamy skonfigurowane SPI, możemy napisać procedurę komunikującą się poprzez ten interfejs. Dane są jednocześnie wysyłane (przez linię MOSI) oraz odbierane (linia MISO), więc zamiast typowych dwóch procedur - do wysyłania i odbierania mamy jedną, wspomnianą wcześniej HAL_SPI_TransmitReceive. Przyjmuje ona aż 5 parametrów, pierwszym jest wskaźnik do struktury opisującej nasz interfejs SPI, następnie bufor nadawczy, później odbiorczy, wielkość bufora (oba muszą mieć identyczną wielkość) oraz maksymalny czas komunikacji.
W poprzedniej edycji kursu napisaliśmy prostą funkcję spi_sendrecv, spróbujemy teraz zrobić to samo:
W pierwszej linii zerujemy stan CS, czyli aktywujemy układ slave. Następnie wysyłamy bajt o wartości 0x40 (przykładowa wartość). Możemy wysłać na raz więcej danych i najczęściej właśnie tak się postępuje. Gdy skończymy komunikować się z układem, zwalniamy interfejs wystawiając logiczne 1 na linii CS.
Przy pierwszym podejściu trochę uprościliśmy problem konfiguracji interfejsu SPI. W przypadku biblioteki StdPeriph dostarczane były specjalne funkcje ustawiające domyślną konfigurację. Twórcy biblioteki HAL poszli nieco inną drogą i wolą, żebyśmy ustawili wszystko, czy nas to interesuje czy niekoniecznie.
Pierwszy przykład wykorzystał pewną sztuczkę. Zmienna spi była zadeklarowana jako globalna, więc wszystkie jej pola były domyślnie zerowane. Tak się składa, że wartość zero była też tym co oczekujemy. Oczywiście nie powinno się tak programować, więc poniżej zamieszam pełny kod wypełniający pola zmiennej spi:
Od tego momentu będziemy używali właśnie takiej wersji - ale już szczegółów wszystkich ustawień niestety nie opiszemy. Zachęcamy natomiast do doczytania w dokumentacji biblioteki oraz samego mikrokontrolera. Więcej informacji o samym SPI można znaleźć też np. na Wikipedii.
Ekspander portów MCP23S08
Umiemy już skonfigurować SPI, czas wykorzystać naszą wiedzę w praktyce. Jako pierwszy przykład wykorzystamy układ MCP23S08, czyli ekspander wyprowadzeń I/O. Układ ten pozwala na podłączenie dodatkowych 8 linii wejścia/wyjścia do naszego mikrokontrolera.
Zaczniemy od powtórzenia przykładu z migającą diodą, który wykorzystywaliśmy w części 4 kursu, omawiając GPIO. Tym razem diodę podłączymy nie bezpośrednio do mikrokontrolera, ale do I/O ekspandera. Schemat podłączenia przedstawia rysunek:
Sterowanie wyjściem ekspandera przez SPI.
Trzeba pamiętać o podłączeniu podciągnięcia linii RESET układu MCP23S08. Linia nie posiada wbudowanego rezystora pull-up. Układ nie będzie działał, jeśli reset zostawimy niepodłączony.
Połączenie między mikrokontrolerem, a MCP23S08 jest zgodne z tym, co mówiliśmy o SPI. Jako linię CS wykorzystujemy PC0, poza tym podłączyliśmy MISO, MOSI oraz SCLK. MCP23S08 posiada 8 linii wejścia/wyjścia. Są one sterowane za pomocą rejestrów, podobnie jak np.: układy AVR. W dokumentacji znajdziemy pełny opis dostępnych rejestrów:
Rejestry dostępne w MCP23S08.
Nas będą interesowały rejestry:
IODIR - ustawia kierunek działania linii, podobnie jak DDRA w przypadku AVR
GPIO - pozwala na odczyt stanów linii, odpowiada PINA dla AVR
OLAT - zapis do tego rejestru ustawia stan linii wyjściowych - odpowiada PORTA
GPPU - pozwala na podłączenie rezystorów podciągających
Nie jest wygodne pamiętanie adresu każdego rejestru - zdefiniujemy więc odpowiednie stałe:
C
1
2
3
4
5
6
7
8
9
10
11
#define MCP_IODIR 0x00
#define MCP_IPOL 0x01
#define MCP_GPINTEN 0x02
#define MCP_DEFVAL 0x03
#define MCP_INTCON 0x04
#define MCP_IOCON 0x05
#define MCP_GPPU 0x06
#define MCP_INTF 0x07
#define MCP_INTCAP 0x08
#define MCP_GPIO 0x09
#define MCP_OLAT 0x0a
Komunikacja z ekspanderem
Kolejny krok, to odszukanie w dokumentacji, jak wygląda zapis i odczyt z odpowiednich rejestrów. Zapis wygląda następująco:
wysyłamy identyfikator urządzenia 0x40,
następnie podajemy adres rejestru, przykładowo MCP_OLAT,
oraz wartość do zapisania.
W poprzedniej edycji wykorzystaliśmy do komunikacji naszą funkcję spi_sendrecv i za jej pomocą zdefiniowaliśmy następującą procedurę:
W tej postaci kod zadziała, ale będzie mało efektywny. Wywołujemy w nim trzy razy spi_sendrecv, która za każdym razem przesyła jeden bajt używając HAL_SPI_TransmitReceive. Znacznie efektywniej będzie tylko razy wywołać HAL_SPI_TransmitReceive i przesłać wszystkie dane jednocześnie.
Pewną niedogodnością funkcji HAL_SPI_TransmitReceive jest wymaganie podania zarówno bufora nadawczego, jak i odbiorczego. Jest to optymalne wykorzystanie sprzętu, bo dane są przesyłane jednocześnie w obu kierunkach. Jednak jeśli zapisujemy wartość do rejestru, nie interesują nas odbierane informacje. Na szczęście autorzy biblioteki HAL udostępnili jeszcze dwie funkcje: HAL_SPI_Transmit oraz HAL_SPI_Receive. Wymagają one tylko jednego bufora, więc idealnie pasują do naszych potrzeb:
Teraz możemy napisać właściwy program. Po skonfigurowaniu SPI trzeba ustawić odpowiednie rejestry układu MCP23S08. Chcemy, aby pin zerowy był wyjściem. W tym celu musimy wyzerować pierwszy bit rejestru IODIR (1 - oznacza wejście, 0 - wyjście):
C
1
mcp_write_reg(MCP_IODIR,~0x01);
Włączenie i wyłączenie diody odbywa się poprzez zapis do rejestru OLAT:
C
1
2
3
4
//Wlacz diode
mcp_write_reg(MCP_OLAT,0x01);
//Wylacz diode
mcp_write_reg(MCP_OLAT,0x00);
Kompletny program migający diodą wygląda więc następująco:
Działanie programu w praktyce widoczne jest na poniższym filmie:
Zadanie domowe 9.1
Podłącz do ekspandera kilka diod świecących np.: 4 i napisz program, który pozwala na włączanie i wyłączanie diod poprzez wysyłanie odpowiednich komend sterujących przez UART (np. 1on, 1off).
Zadanie domowe 9.2
Wykorzystaj podłączenie z zadanie 9.1 i stwórz licznik Johnsona. Do układu podłącz dodatkowo potencjometr, którym będzie można regulować prędkość pracy licznika (w tym celu użyj ADC).
Obsługa przycisku
Sprawdziliśmy jak sterować wyjściem ekspandera. Teraz przetestujemy linię wejściową. W tym celu możemy dodać na płytce jakiś przełącznik lub najprościej wykorzystać przewód, którym będziemy zwierać odpowiednie sygnały. Na poniższym schemacie montażowym role przełącznika odgrywa pomarańczowy przewód, który wyróżniony został strzałką.
Odczytywanie wejść ekspandera przez SPI.
Ponieważ nie podłączyliśmy zewnętrznego rezystora podciągającego, będziemy musieli włączyć odpowiedni rezystor wbudowany w MCP23S08. W tym celu wystarczy ustawić odpowiedni bit rejestru GPPU:
C
1
mcp_write_reg(MCP_GPPU,0x02);
Stan przycisku odczytamy z rejestru GPIO. Musimy więc przygotować procedurę odczytywania rejestrów (zapisywać już potrafimy). Opis protokołu znajdziemy w dokumentacji MCP23S08 - odczyt z rejestru przebiega następująco:
Jak pamiętamy komunikacja przez SPI za każdym razem odbywa się w obu kierunkach. Więc kiedy wysyłamy np. identyfikator 0x41, jednocześnie odbieramy wartość od układu peryferyjnego. Jednak ta wartość jest nieistotna, więc nie zapamiętujemy jej. Podobnie dzieje się podczas wysyłania adresu rejestru.
Odczyt natomiast polega na wysłaniu czegokolwiek (czyli wysyłamy przykładowo wartość 0xff) oraz zapamiętaniu odebranej wartości.
Teraz możemy napisać program, który będzie włączał diodę po naciśnięciu przycisku:
Skonfiguruj jeden pin ekspandera jako wejście, a pozostałe jako wyjścia, do których podłączone będą diody świecące. Stan diod powinien wizualizować wyjście licznika binarnego, którego kierunek pracy określany ja za pomocą stanu panującego na jedynym wejściu ekspandera.
Podsumowanie
W tej części poznaliśmy interfejs SPI, za pomocą którego możemy rozszerzać możliwości mikrokontrolera poprzez podłączanie układów peryferyjnych. Jako przykład posłużył nam moduł dodatkowych linii wejścia-wyjścia MCP23S08. Układy tego typu okazują się niezastąpione, gdy zaczyna brakować wolnych portów I/O mikrokontrolerze.
Uwaga! Ten kurs został zarchiwizowany. Sprawdź najnowszy kurs STM32 »
W kolejnej części kursu zobaczymy jak za pomocą SPI sterować wyświetlaczem graficznym! Od tego momentu wszystkie projekty będą mogły komunikować się z użytkownikiem w ciekawszy sposób.
A w następnym odcinku...
Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk Redakcja: Damian Szymański
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY na bazie Arduino i Raspberry Pi.
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY z Arduino i RPi.
Trwa ładowanie komentarzy...