Kurs STM32 F1 HAL – #9 – SPI w praktyce, ekspander IO

Kurs STM32 F1 HAL – #9 – SPI w praktyce, ekspander IO

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.

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.

Podłączenie SPI

Schemat podstawowego podłączenia jest następujący:

381px-SPI_single_slave.svg

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

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

363px-SPI_three_slaves.svg

Ź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!

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.

Zestaw elementów do kursu

Gwarancja pomocy na forum Błyskawiczna wysyłka

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!

Kup w Botland.com.pl

Wstępne ustawienia SPI

Chcąc oprogramować interfejs SPI, zaczynamy jak zawsze - od podłączenia zegarów:

Następnie konfigurujemy piny interfejsu. Zaczniemy od linii wyjściowych, czyli SCK oraz MOSI:

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

Kolejny krok to konfiguracja linii wejściowej MISO. Tutaj też zobaczymy podobieństwo do RX z UART:

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:

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:

Aby skorzystać z interfejsu, musimy jeszcze wysterować odpowiednio linię CS. Przykładowo wysłanie jednego bajtu wygląda więc następująco:

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.

Pełny kod przykładu:

Więcej parametrów

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.

Opis wyprowadzeń układu MCP23S08.

Opis wyprowadzeń układu MCP23S08.

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.

Sterowanie wyjściem ekspandera przez SPI.

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:

MCP23S08_reg

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:

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:

  1. wysyłamy identyfikator urządzenia 0x40,
  2. następnie podajemy adres rejestru, przykładowo MCP_OLAT,
  3. 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):

Włączenie i wyłączenie diody odbywa się poprzez zapis do rejestru OLAT:

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.

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:

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:

  1. wysyłamy identyfikator urządzenia 0x41,
  2. następnie adres rejestru np. MCP_GPIO,
  3. odbieramy zawartość rejestru.

Funkcja odczytująca rejestr ma więc postać:

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.

Teraz możemy napisać program, który będzie włączał diodę po naciśnięciu przycisku:

Zadanie domowe 9.3

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.

Nawigacja kursu

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

A w następnym odcinku...

Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk
Redakcja: Damian Szymański

ekspander, kursSTM32F1HAL, SPI, stm32

Komentarze

Trwa przerwa techniczna - komentarze do tego wpisu są dostępne na forum: