Kurs STM32L4 – #10 – ekspander portów (SPI), quiz

Kurs STM32L4 – #10 – ekspander portów (SPI), quiz

SPI to szeregowy interfejs komunikacyjny, dzięki któremu do mikrokontrolerów podłącza się wiele przydatnych modułów (np. wyświetlacze TFT).

W tej części kursu STM32L4 omówimy podstawy komunikacji przez SPI na przykładzie ekspandera portów. Stworzymy też swoją pierwszą bibliotekę.

Czego nauczysz się z tej części kursu STM32L4?

Podczas wykonywania ćwiczeń z tej części kursu poznasz podstawowe informacje o SPI. Zaczniemy od komunikacji z popularnym ekspanderem portów, czyli układem, dzięki któremu możemy zyskać osiem dodatkowych GPIO. Przy okazji zmienimy też sposób generowania projektów przez CubeMX, aby możliwe było łatwe tworzenie własnych bibliotek. Wiedzę z tej części kursu wykorzystamy później do obsługi wyświetlacza graficznego, z którym również będziemy komunikować się za pomocą SPI.

Szeregowe interfejsy komunikacyjne

W trakcie programowania mikrokontrolerów mamy do wyboru wiele różnych interfejsów, dzięki którym nasze układy mogą komunikować się z innymi podzespołami na tej samej płytce PCB, z osobnymi modułami lub nawet z innymi urządzeniami i komputerami. Interfejsy te mają swoje wady i zalety, warto więc znać różne możliwości, aby dobierać najlepsze rozwiązania do konkretnego projektu.

MCP23S08 to ekspander GPIO, z którym można komunikować się przez SPI

MCP23S08 to ekspander GPIO, z którym można komunikować się przez SPI

Poznaliśmy już jeden interfejs szeregowy, który był asynchroniczny – oczywiście chodzi o UART. Teraz dla odmiany pora na SPI (ang. serial peripheral interface), czyli popularny interfejs synchroniczny. Jest to jeden z częściej używanych standardów, przydający się np. podczas podłączania ekspanderów IO, wyświetlaczy lub innych modułów, które wymagają przesyłania stosunkowo dużej ilości danych.

SPI vs. UART – najważniejsze różnice

Interfejs UART wykorzystywaliśmy do komunikacji z PC. Jego podstawową cechą jest asynchroniczność, czyli brak konieczności przesyłania sygnału zegarowego. Interfejs ten wykorzystuje raptem dwie linie (Rx i Tx), a oba urządzenia biorące udział w komunikacji są równorzędne względem siebie. Rozwiązanie to ma dwie główne „wady”. Po pierwsze, urządzenia, które chcą przesyłać między sobą informacje przez UART, muszą posiadać precyzyjne źródło taktowania. Po drugie, interfejs ten pozwala na przesyłanie danych ze stosunkowo niewielkimi prędkościami.

Przykład komunikacji między dwoma mikrokontrolerami przez UART

Przykład komunikacji między dwoma mikrokontrolerami przez UART

Z kolei SPI to interfejs synchroniczny, więc sygnał zegarowy jest przesyłany między komunikującymi się układami. Nawet jeśli sygnał ten nie będzie idealny, to i tak nic wielkiego się nie stanie, bo „zawahania” będą identyczne dla wszystkich układów. Po drugie, interfejs ten umożliwia szybką komunikację między wieloma różnymi układami. Jednak niestety w tym przypadku pojawia się drobna komplikacja – układy połączone po SPI nie są równorzędne. Jeden z nich jest zawsze układem nadrzędnym (ang. master), a pozostali uczestnicy komunikacji to układy podrzędne (ang. slave).

Najczęściej układem nadzorującym komunikację przez SPI jest mikrokontroler, na który sami piszemy program – takim przypadkiem będziemy zajmować się w kursie. Można oczywiście wykorzystać STM32 w roli układu podrzędnego, ale wbrew pozorom jest to trudniejsza wersja od tzw. mastera.

Przykład komunikacji między dwoma układami scalonymi przez SPI

Przykład komunikacji między dwoma układami scalonymi przez SPI

Interfejs SPI do działania potrzebuje minimum czterech linii sygnałowych:

  • MOSI (ang. master output slave input) – dane wysyłane z mastera do slave’a,
  • MISO (ang. master input slave output) – dane wysyłane ze slave’a do mastera,
  • SCLK (ang. serial clock) – linia zegara,
  • SS (ang. slave select) lub CS (ang. chip select) – zanegowana linia aktywująca układ podrzędny.

Ostatnia linia nazywana jest zamiennie SS lub CS (w kursie będziemy korzystać z tej drugiej nazwy). Niezależnie od nazwy linia ta służy do aktywacji układu, który w danej chwili będzie komunikował się z układem nadrzędnym.

Każdy układ podrzędny posiada dedykowaną linię CS. Podłączając więcej niż jeden układ peryferyjny do interfejsu SPI, będziemy potrzebowali kolejnych linii CS – po jednej na każdy układ podrzędny. Pozostałe linie, na których przebiega właściwa komunikacja, mogą być podłączone do wielu układów (poziom wysoki na CS sprawia, że układ podrzędny ignoruje to, co dzieje się na liniach sygnałowych).

Przykład komunikacji między układem nadrzędnym i trzema układami podrzędnymi

Przykład komunikacji między układem nadrzędnym i trzema układami podrzędnymi

Wybór aktywnego układu odbywa się przez wystawienie stanu niskiego na odpowiedniej linii CS. Tylko jeden układ powinien być aktywny w danej chwili, więc na pozostałych liniach CS powinien być wtedy ustawiony stan wysoki. Jest to modelowe wykorzystanie SPI, ale oczywiście można trochę „naginać” te zasady – np. można aktywować jednocześnie kilka układów podrzędnych, aby odebrały te same dane, które wysyła jednorazowo układ nadrzędny.

Wszystkie te rozwiązania prowadzą do tego, że interfejs SPI pozwala na uzyskanie znacznie większych prędkości transmisji danych. Przykładowo nasz mikrokontroler, czyli STM32L476RG, posiada aż trzy sprzętowe interfejsy SPI, a każdy z nich może komunikować się z prędkością do 40 Mbit/s.

Gotowe zestawy do kursów Forbota

 Komplet elementów  Gwarancja pomocy  Wysyłka w 24h

Zamów zestaw elementów i wykonaj ćwiczenia z tego kursu! W komplecie płytka NUCLEO-L476RG oraz m.in. wyświetlacz graficzny, joystick, enkoder, czujniki (światła, temperatury, wysokości, odległości), pilot IR i wiele innych.

Zamów w Botland.com.pl »

Komunikacja przez SPI

Po wstępie teoretycznym przechodzimy do praktyki. W tej części kursu zajmiemy się wykorzystaniem SPI do komunikacji z ekspanderem GPIO, a konkretnie z MCP23S08. Układy tego typu przydają się, gdy zaczyna nam brakować wolnych pinów w mikrokontrolerze, ponieważ posiadają one np. osiem pinów, które możemy w swoim programie wykorzystywać tak jak inne wejścia/wyjścia. Główna różnica jest taka, że pracą tych pinów sterujemy poprzez wysyłanie odpowiednich komend przez SPI.

Konfiguracja SPI na STM32L4

Zaczynamy od nowego projektu: STM32L476RG, który pracuje z maksymalną częstotliwością 80 MHz, do tego uruchamiamy debugger. Podczas ćwiczeń będziemy korzystać z modułu SPI2. Przechodzimy więc w CubeMX do Connectivity > SPI2 i ustawiamy tryb interfejsu (ang. mode) na Full-Duplex Master. Konfigurator automatycznie oznaczy wtedy trzy piny, które będą używane przez SPI, czyli MOSI na PC3, MISO na PC2 oraz SCK na PB10.

Co z linią CS? Tutaj mamy dwie opcje – możemy skorzystać również ze sprzętowej obsługi tej linii, ale wówczas bylibyśmy ograniczeni do używania tylko jednego modułu podrzędnego. W tym przykładzie byłoby to wystarczające, ale pokażemy, jak samodzielnie sterować linią CS, bo takie rozwiązanie jest bardziej uniwersalne, jeśli mamy w planach rozbudowywanie naszego projektu o nowe podzespoły.

Od razu przechodzimy też do dolnego okna z parametrami SPI2 i ustawiamy tam następujące wartości:

  • Frame Format: Motorola
  • Data Size: 8 Bits
  • Prescaler (for Baud Rate): 8
  • Clock Polarity (CPOL): Low
  • Clock Phase (CPHA): 1 Edge
Konfiguracja pierwszego projektu, który wykorzystuje SPI

Konfiguracja pierwszego projektu, który wykorzystuje SPI

Podczas konfiguracji takich interfejsów należy zawsze brać pod uwagę możliwości układu nadrzędnego oraz podrzędnego. Nasz mikrokontroler umożliwia transmisję przez SPI z prędkością do 40 MBit/s, ale ekspander, czyli MCP23S08, obsługuje maksymalnie 10 MBit/s. Dlatego musieliśmy ustawić preskaler na 8 (bo 80 MHz przez 8 daje 10 MHz).

Na koniec jeszcze krótka informacja o parametrach CPOL i CPHA. Każdy z nich ma dwie dopuszczalne wartości, co daje razem łącznie cztery kombinacje. Więc dla uproszczenia przyjmuje się, że SPI może działać w jednym z czterech trybów, nazywanych po prostu od SPI mode-0 do SPI mode-3. Dla nas liczy się to, aby układ nadrzędny używał tego samego trybu co układ podrzędny.

Ustawienie parametrów w poszczególnych trybach

Ustawienie parametrów w poszczególnych trybach

MCP23S08 obsługuje tryby numer 0 i 3, więc ustawiliśmy tutaj parametry, które odpowiadają trybowi numer 0. Więcej informacji na temat trybów pracy SPI można znaleźć np. na Wikipedii.

Programowe sterowanie linią CS

Nie będziemy korzystać ze sprzętowej obsługi linii CS. Musimy zatem skonfigurować jeszcze jeden pin, który wykorzystamy do tego zadania. Wystarczy nam właściwie dowolne wyprowadzenie, które będzie skonfigurowane jako GPIO_Output – na potrzeby tego przykładu wybieramy pin PC0 i przypisujemy mu np. etykietę IOEXP_CS.

Konfiguracja dodatkowej linii CS

Konfiguracja dodatkowej linii CS

Skorzystamy jednak z jeszcze jednej opcji. Na linii CS domyślnie powinniśmy mieć logiczną jedynkę, bo stan niski na wejściu CS jest dla układów podrzędnych znakiem rozpoczęcia transmisji. Zmieniamy więc parametr GPIO output level na High. Dzięki temu kod wygenerowany przez CubeMX będzie od razu domyślnie po starcie układu ustawiał na tej linii stan wysoki.

Podłączenie układu

Teraz, gdy znamy już wszystkie piny, możemy przejść do podłączenia układu. Całość łączymy na płytce stykowej zgodnie z poniższym schematem.

Schemat ideowy i montażowy układu z ekspanderem

Schemat ideowy i montażowy układu z ekspanderem

Tym razem najwygodniej będzie podłączyć się do następujących wyprowadzeń na Nucleo:

Użyte wyprowadzenia na płytce Nucleo

Użyte wyprowadzenia na płytce Nucleo

Podczas łączenia tego typu układów warto posiłkować się pinoutem danego układu. Tutaj napotkamy jednak pewne rozbieżności w nazewnictwie. Producent w dokumentacji posługuje się nazwami SCK, SI, SO oraz CS – są to odpowiednio SCK, MOSI, MISO oraz CS. 

Opis wyprowadzeń ekspandera (fragment dokumentacji)

Opis wyprowadzeń ekspandera (fragment dokumentacji)

Układ ten jest wyposażony w zanegowane wejście RESET. Jeśli pojawi się na nim logiczne zero, to ekspander przestanie działać, a jego wewnętrzne ustawienia wrócą do wartości domyślnych. W naszym przypadku najlepiej będzie wymusić na tym wejściu na stałe logiczną jedynkę.

Na koniec to, co najważniejsze, czyli linie GP0-GP7, a zatem nasze dodatkowe GPIO, które zyskujemy, gdy decydujemy się na użycie ekspandera. Zaczynamy skromnie od jednej diody święcącej, dzięki której uda nam się sprawdzić, czy komunikacja z ekspanderem działa poprawnie.

Wysyłanie i odbieranie danych przez SPI

Biblioteka HAL zapewnia zestaw wygodnych funkcji do komunikacji przez SPI. Właściwie większość operacji uda nam się wykonać za pomocą gotowych funkcji: HAL_SPI_Transmit, HAL_SPI_Receive oraz HAL_SPI_TransmitReceive.

Funkcja HAL_SPI_Transmit wysyła bufor danych przez interfejs SPI. Jej parametry to kolejno: wskaźnik do struktury opisującej interfejs (czyli zmiennej hspi2 utworzonej przez CubeMX), wskaźnik na dane do wysłania, liczba bajtów i czas, jaki maksymalnie możemy czekać na ich wysyłanie (tzw. timeout). Druga funkcja, czyli HAL_SPI_Receive, ma bardzo podobne parametry, ale tym razem dane są odbierane i wstawiane do bufora przekazanego jako parametr pData.

Jak już wiemy, SPI działa w pełni dwukierunkowo, więc jednocześnie dane są wysyłane i odbierane. Za pomocą funkcji HAL_SPI_TransmitReceive mamy dostęp właśnie do takiej możliwości. W tej sytuacji musimy przekazać jako parametry dwa bufory – jeden z danymi do wysłania, a drugi na odebrane dane. Oba bufory muszą mieć ten sam rozmiar, więc funkcja przyjmuje tylko jeden parametr, który określa długość danych (odbieranych i wysyłanych).

Omówienie rejestrów MCP23S08

Wiemy już, jak można komunikować się przez SPI. Nie wiemy jednak, co należy wysyłać do ekspandera, aby układ działał zgodnie z naszymi oczekiwaniami. Tutaj konieczne jest przestudiowanie dokumentacji konkretnego ekspandera. Zanim przejdziemy do pisania programu, musimy więc poznać MCP23S08.

Oczywiście najlepiej byłoby przeczytać całą notę katalogową, ale na ten moment możemy ograniczyć się do omówienia najważniejszych informacji z dokumentacji. Idźmy więc od początku: ekspander jest wyposażony tylko w 11 rejestrów. Nas głównie interesują cztery z nich: IODIR, OLAT, GPIO oraz GPPU. Każdy z nich to po prostu miejsce na 8-bitową liczbę, do którego zapisujemy jakąś wartość (aby układ zadziałał zgodnie z naszymi oczekiwaniami) lub z którego ją odczytujemy (aby się dowiedzieć, co dzieje się w środku tego układu).

Rejestry układu MCP23S08

Rejestry układu MCP23S08

IODIR to rejestr, który pozwala na ustawianie kierunku działania pinów GP0-GP7. Jeśli chcemy, aby dany pin pracował jako wejście, należy w dane miejsce wpisać 1; jeśli wpiszemy tam 0, to pin zostanie skonfigurowany jako wyjście. Z noty wyczytamy jeszcze, że możemy zapisywać dane do tego rejestru (aby skonfigurować układ), ale możemy też odczytać jego wartość, aby sprawdzić, jaka funkcja jest obecnie przypisana do danego pinu. Domyślnie wszystkie piny są wejściami.

Rejestr IODIR – konfiguracja trybu pracy GPIO

Rejestr IODIR – konfiguracja trybu pracy GPIO

Czyli przykładowo:

  • IODIR = 00000000 – wszystkie piny są wyjściami,
  • IODIR = 11111111 – wszystkie piny są wejściami,
  • IODIR = 00000001 – pin GP0 jako wejście, reszta jako wyjścia.

Powyższe liczby zapisane zostały w systemie binarnym. Jeśli chcemy, aby wszystkie piny były wejściami, to do rejestru wpisujemy dziesiętnie 255 lub heksadecymalnie 0xFF. Możliwy jest również zapis binarny jako 0b11111111, ale taki zapis nie jest jeszcze rozpoznawany poprawnie przez wszystkie kompilatory (stąd najczęściej w programach korzysta z zapisu heksadecymalnego).

    OLAT to rejestr, który wykorzystamy do tego, aby zmienić stan pinów, które pracują w roli wyjść. Jeśli konkretny bit rejestru ustawimy na 1, to na danym wyjściu będzie logiczny stan wysoki. Analogicznie ustawienie bitu na 0 sprawi, że na wyjściu będzie niski stan logiczny.

    Rejestr OLAT – sterowanie wyjściami

    Rejestr OLAT – sterowanie wyjściami

    GPIO to rejestr, z którego odczytamy aktualny stan na pinach skonfigurowanych wcześniej jako wejścia. Sytuacja jest analogiczna do rejestru OLAT – odczytanie 1 będzie oznaczało, że na danym wejściu jest wysoki stan logiczny.

    Rejestr GPIO – odczytywanie stanu wejść

    Rejestr GPIO – odczytywanie stanu wejść

    Ostatni rejestr, który nas teraz interesuje, to GPPU. Jest to rejestr, który w przypadku pinów działających jako wejścia pozwala na aktywację rezystorów podciągających dla danego pinu. Oczywiście wpisanie wartości 1 aktywuje pull-upa, a 0 wyłącza go (i takie jest domyślne ustawienie).

    Rejestr GPPU – aktywacja rezystorów podciągających na wejściach

    Rejestr GPPU – aktywacja rezystorów podciągających na wejściach

    Komunikacja z MCP23S08

    Komunikacja z MCP23S08 jest stosunkowo prosta – na początku zawsze wysyłamy do niego przez SPI dwa bajty: adres układu wraz z informacją o tym, czy będziemy dane zapisywać, czy odczytywać, a także numer rejestru, który nas interesuje.

    Wartości A1 i A0 są zawsze zerami, bo odpowiednie piny połączyliśmy z masą, ale i tak ich funkcja jest domyślnie wyłączona. Czyli – mówiąc najprościej – w naszym przypadku pierwszy bajt będzie miał wartość 0x40, gdy będziemy zapisywać dane, natomiast 0x41  – gdy będziemy chcieli coś odczytać.

    Nawiązanie komunikacji z ekspanderem przez SPI

    Nawiązanie komunikacji z ekspanderem przez SPI

    Po wysłaniu tej kombinacji (adres układu i numer rejestru) wartość danego rejestru może zostać przesłana (albo my wysyłamy nową wartość, która ma zostać zapisana, albo układ odsyła obecną wartość tego rejestru). Wszystko będzie nieco jaśniejsze, gdy przejdziemy do pisania programu. Czas więc wrócić do STM32CubeIDE i napisać nasz pierwszy program.

    Sterowanie diodą za pomocą ekspandera

    Zaczynamy od definicji stałych, które będą odpowiadały rejestrom adresów MCP23S08. Używanie tzw. magicznych liczb w kodzie programu jest raczej mało czytelne, bo kto będzie pamiętał, że np. 0x0A to rejestr OLAT? Dla własnej wygody definiujemy więc stałe z adresami wszystkich rejestrów układu.

    Teraz możemy przeprowadzić próbę komunikacji z układem. Docelowo będziemy sterować diodą, która jest podłączona do ekspandera. Chcemy najpierw ustawić pin PG0 w trybie wyjścia. Musimy więc najpierw wysłać adres układu (0x40), później adres rejestru IODIR (0x00), a dopiero później prześlemy informację o kierunku działania poszczególnych pinów. Chcemy, aby GP0 było wyjściem, ale inne piny mają być wejściami, czyli zapiszemy do tego rejestru 11111110.

    Dane do ekspandera wyślemy za pomocą funkcji HAL_SPI_Transmit. Moglibyśmy wywołać ją trzy razy (adres układu, rejestr, wartość dla rejestru), ale znacznie lepszym wyjściem będzie skorzystanie z tego, że funkcja ta może przyjąć jako argument tablicę bajtów, które mają zostać wysłane. Dzięki temu cały kod, który ustawi odpowiedni pin jako wyjście, może wyglądać następująco (wystarczy wywołać go raz):

    Brakuje nam jeszcze sterowania linią CS. Przed rozpoczęciem komunikacji powinniśmy ustawić na niej logiczne 0, a po zakończeniu 1. Dodajemy zatem zwyczajne wywołania HAL_GPIO_WritePin:

    Możemy już wykorzystać te informacje do napisania pierwszego programu. Przed pętlą nieskończoną wysyłamy informacje na temat konfiguracji GPIO, a następnie w pętli sterujemy naszym nowym GPIO. Kod migający diodą podłączoną do ekspandera będzie praktycznie identyczny jak powyższy, zmianie ulegnie jedynie wybrany przez nas rejestr – tym razem będziemy zapisywać do rejestru OLAT.

    Nasz pierwszy program wygląda więc następująco:

    Po uruchomieniu tego programu powinna zacząć migać dioda, którą podłączyliśmy do ekspandera.

    Efekt działania programu testującego ekspander

    Efekt działania programu testującego ekspander

    Program działa, ale jest nieelegancki. Użyliśmy metody kopiuj-wklej i aż trzy razy powtórzyliśmy ten sam kod. Znacznie lepiej będzie wydzielić ten fragment do oddzielnej funkcji. Kolejne wywołania różnią się tylko numerem rejestru i zapisywaną wartością. Możemy więc przekazywać je jako parametry do naszej funkcji, która będzie pośredniczyła w wysyłaniu danych:

    Dzięki użyciu takiej funkcji nasz program będzie wyglądał znacznie lepiej:

    Obsługa przycisku za pomocą ekspandera

    Wiemy już, jak sterować wyjściami ekspandera, teraz pora na test wejść. Między masę a wyprowadzenie GP1 podłączamy przełącznik – specjalnie bez zewnętrznego rezystora podciągającego.

    Schemat ideowy i montażowy dla przykładu testującego wejścia ekspandera

    Schemat ideowy i montażowy dla przykładu testującego wejścia ekspandera

    Wyprowadzenie GP1 jest już skonfigurowane jako wejście (tak działa nasz poprzedni program). Jednak w związku z tym, że nie podłączyliśmy na płytce zewnętrznego rezystora podciągającego, będziemy musieli włączyć odpowiedni pull-up wewnątrz MCP23S08. Tutaj przyda się rejestr GPPU:

    Stan przycisku odczytamy z rejestru GPIO. Musimy tylko przygotować odpowiednią procedurę, która ułatwi nam odczytywanie rejestrów. Opis protokołu znajdziemy w dokumentacji MCP23S08 – odczyt z rejestru przebiega następująco:

    1. wysyłamy adres urządzenia 0x41 (bo 0x40 było dla zapisu),
    2. następnie wysyłamy adres rejestru, który nas interesuje,
    3. odbieramy zawartość danego rejestru.

    Funkcja odczytująca dowolny rejestr może więc wyglądać następująco:

    Taka wersja zadziała, jednak wywoływanie zarówno HAL_SPI_Transmit, jak i HAL_SPI_Receive zajmuje stosunkowo dużo czasu. Lepiej będzie, jeśli uprościmy program, używając HAL_SPI_TransmitReceive. W rzeczywistości komunikacja po SPI jest zawsze dwukierunkowa, więc poprzednio (podczas wysyłania danych) również otrzymywaliśmy informacje zwrotne, ale je ignorowaliśmy.

    Podobnie jest podczas odbierania danych – i tak musimy najpierw coś wysłać. Wygodnie będzie więc, jeśli będziemy mieć dwie tablice – jedną na dane do wysłania, a drugą na to, co odbierzemy. Wówczas nasza funkcja przyjmie poniższą postać:

    Gdybyśmy chcieli jeszcze zmniejszyć zużycie pamięci, moglibyśmy użyć tego samego bufora zarówno do wysyłania, jak i odbierania danych. Ostateczna wersja funkcji odczytującej dane z ekspandera może zatem wyglądać następująco:

    Teraz możemy tę funkcję wykorzystać w naszym programie głównym. W wyniku wywołania tej funkcji zwrócona zostanie zawartość całego rejestru GPIO, a my chcemy sprawdzić tylko zawartość jednego bitu, który powie nam, czy przycisk jest wciśnięty. Tutaj przyda się operator koniunkcji bitowej, który w języku C przyjmuje formę pojedynczego znaku: &.

    Pin, do którego podłączyliśmy przyciski, to GP1, czyli informacja o tym, czy przycisk jest wciśnięty, będzie zapisana na tym bicie rejestru XXXXXXXX. Możemy więc „wydobyć” informację na temat tego bitu, jeśli dokonamy następującej koniunkcji bitowej: XXXXXXXX & 00000010 – wynikiem takiej operacji będzie 0, jeśli X jest równe 0, lub 1, gdy X jest równe 1. W programie posługujemy się liczbami zapisanymi w systemie szesnastkowym, więc do koniunkcji używamy zapisu 0x02.

    Implementacja tego rozwiązania może wyglądać następująco:

    W pętli odczytujemy zawartość rejestru GPIO i za pomocą koniunkcji bitowej sprawdzamy, jaka jest wartość bitu, który odpowiada GP1. Jeśli wartość ta jest równa 0, to znaczy, że przycisk został wciśnięty (przycisk zwiera wejście do masy), więc włączamy diodę, a w przeciwnym wypadku wyłączamy ją.

    Efekt działania programu testującego wejścia ekspandera

    Efekt działania programu testującego wejścia ekspandera

    Zmiana struktury projektu CubeMX

    Napisaliśmy dwie przydatne funkcje do obsługi ekspandera MCP23S08. Mogą one przydać się również podczas innych naszych projektów. Warto byłoby zatem stworzyć z nich np. własną bibliotekę. Niestety, umieszczanie programu w jednym pliku nie ułatwia ponownego wykorzystania kodu. Przenieśmy więc funkcje obsługujące MCP23S08 do nowego modułu, aby były one łatwo dostępne w przyszłości.

    Przed utworzeniem własnej biblioteki musimy jednak zmienić ustawienia projektu. Wracamy więc do perspektywy CubeMX i wybieramy zakładkę Project Manager (na prawo od Clock Configuration). Po lewej stronie okna zostaną wyświetlone pionowo trzy zakładki – wybieramy tam Code Generator i zaznaczamy opcję Generate pheripheral initialization as a pair of *.c/*.h files per peripheral.

    Zmiana ustawień projektu w CubeMX

    Zmiana ustawień projektu w CubeMX

    Po zapisaniu zmian w projekcie CubeMX wygeneruje nowy szablon. Do tej pory cały kod generowany przez to narzędzie był umieszczony w pliku main. Po zmianie ustawień każdy moduł peryferyjny, czyli np. GPIO, UART, czy SPI, będzie posiadał własny plik z kodem programu oraz plik nagłówkowy. Warto teraz podejrzeć, jak zmieniła się struktura projektu.

    Podgląd nowego pliku spi.h

    Podgląd nowego pliku spi.h

    Dzięki podzieleniu pliku na kilka mniejszych kod jest nieco czytelniejszy, ale najważniejsze jest dla nas to, że w pliku spi.h pojawiła się poniższa linijka z modyfikatorem extern. Dzięki niej będziemy mieć dostęp do zmiennej hspi2 również w innych plikach z tego projektu (także z naszej małej biblioteki).

    Wydzielanie kodu do własnej biblioteki

    Pora, aby stworzyć własną bibliotekę, czyli wydzielić kod sterujący ekspanderem do osobnych plików, dzięki czemu będziemy mogli zaimportować te funkcje do innego projektu. Zaczynamy od stworzenia nowego pliku z kodem źródłowym. Przechodzimy do eksploratora projektu i wybieramy miejsce, w którym chcemy utworzyć dany plik. Przechodzimy więc do Core\Src i klikamy tam prawym przyciskiem myszy, a następnie wybieramy opcję New > Source File.

    Opcja pozwalająca na dodanie nowego pliku z kodem źródłowym

    Opcja pozwalająca na dodanie nowego pliku z kodem źródłowym

    Po wybraniu tej opcji pojawi się nowe okno, w którym wpisujemy nazwę pliku, np. mcp23s08.c, i klikamy przycisk Finish. Zostaniemy przeniesieni wtedy do nowego pliku, który będzie właściwie pusty – będzie tam tylko informacja o dacie jego utworzenia oraz o autorze.

    Potrzebujemy jeszcze pliku nagłówkowego. Tworzymy go w analogiczny sposób, z tą różnicą, że tym razem ma się on znaleźć w katalogu Core\Inc. Klikamy więc prawym klawiszem myszy w tę lokalizację, a dalej wybieramy New > Header File. Wpisujemy nazwę mcp23s08.h i kończymy, klikając Finish.

    Nowy plik nagłówkowy oprócz komentarza na temat daty i autora będzie jeszcze zawierał taki kod:

    Celem tych instrukcji jest unikanie wielokrotnego włączania tych samych plików nagłówkowych. Część między #ifndef a #endif to miejsce, gdzie będziemy wstawiać nasze deklaracje funkcji, zmiennych itd. Dzięki użyciu tych nieco dziwnych poleceń, nawet jeśli inne moduły włączą nasz plik nagłówkowy więcej niż raz, to kompilator tylko za pierwszym razem będzie przetwarzał ten kod.

    Teraz możemy przenieść do nowego pliku nagłówkowego definicje rejestrów MCP23S08. Należy tam od razu przenieść deklaracje funkcji mcp_reg_read oraz mcp_reg_write. W związku z tym, że używamy typów uint8_t, to powinniśmy jeszcze dodać tutaj nagłówek stdint.h. Ostatecznie zawartość tego pliku może więc wyglądać następująco:

    Przy tej okazji warto wspomnieć o komentarzach. Wiele poradników zachęca do pisania jak największej liczby komentarzy, ale nie jest to takie jednoznaczne, bo ich nadmiar bywa gorszy niż ich brak. Jednak akurat pliki nagłówkowe powinno się dobrze opisać w komentarzach. Zakładając, że stworzymy przydatny kawałek programu, to pewnie będziemy chcieli się nim podzielić z innymi programistami.

    Komentarze w plikach nagłówkowych powinny zawierać informacje, które są potrzebne do tego, aby wygodnie korzystać z utworzonych przez nas funkcji. Dostępne są nawet programy tworzące gotową dokumentację na podstawie odpowiednio przygotowanych komentarzy (np. Doxygen) – to bardzo ważna opcja w przypadku nieco większych projektów.

    Pozostało nam jeszcze skopiować treść naszych funkcji do pliku mcp23s08.c. Jego zawartość powinna ostatecznie wyglądać następująco:

    Nasza „biblioteka” korzysta z dwóch plików nagłówkowych. Pierwszy to mcp23s08.h, czyli plik, który sami przed chwilą utworzyliśmy, drugi to plik spi.h, który został utworzony przez CubeMX. Teraz możemy wrócić do pliku main.c i usunąć definicje rejestrów oraz deklaracje funkcji mcp_reg_read i mcp_read_write. W zamian wystarczy, że dodamy do projektu plik nagłówkowy naszej biblioteki:

    Pora skompilować program. Powinien działać jak poprzednio, ale cały kod związany z ekspanderem MCP23S08 jest od teraz w oddzielnym module i jeśli w przyszłości będziemy chcieli skorzystać z tego kodu, to wystarczy skopiować i zaimportować pliki mcp23s08.c i mcp23s08.h do projektu (można je np. przeciągnąć do odpowiednich folderów w eksploratorze projektu).

    Quiz – sprawdź, ile już wiesz!

    Przygotowaliśmy aż cztery quizy, dzięki którym sprawdzisz, jak dużo zapamiętałeś z tego kursu. Masz za sobą już dziesięć części kursu, więc możesz zabrać się za kolejny quiz – składa się on z 15 pytań testowych (jednokrotnego wyboru), a limit czasu to 15 min. W rankingu liczy się pierwszy wynik, ale w quizie będziesz mógł później wziąć udział wielokrotnie (w ramach treningu).

    Przejdź do quizu nr 2 z 4 »

    Bez stresu! Postaraj się odpowiedzieć na pytania zgodnie z tym, co wiesz, a w przypadku ewentualnych problemów skorzystaj ze swoich notatek. To nie są wyścigi – ten quiz ma pomóc w utrwaleniu zdobytej już wiedzy i wyłapaniu tematów, które warto jeszcze powtórzyć. Powodzenia!

    Quiz - najnowsze wyniki

    Oto wyniki 10 osób, które niedawno wzięły udział w quizie. Teraz pora na Ciebie! Uwaga: wpisy w tej tabeli mogą pojawiać się z opóźnieniem, pełne wyniki są dostępne „na żywo” na stronie tego quizu.

    # Użytkownik Data Wynik
    1Emtorek19.08.2021, 22:40100%, w 158 sek.
    2michaltronik27.08.2021, 19:2193%, w 232 sek.
    3Andrzej822701.08.2021, 18:4586%, w 143 sek.
    4Frantick08.07.2021, 19:1086%, w 177 sek.
    5Leroy13.09.2021, 00:2880%, w 207 sek.
    6olol89801.09.2021, 14:5573%, w 216 sek.
    7oxfrd09.07.2021, 11:4960%, w 206 sek.
    8szymon81210.08.2021, 20:5053%, w 215 sek.
    9tylkoTechno16.09.2021, 13:4340%, w 303 sek.
    10marville30.07.2021, 00:2733%, w 94 sek.

    Zadanie domowe

    1. Rozwiąż powyższy quiz.
    2. Rozbuduj naszą bibliotekę o funkcję MCP_GPIO_WritePin, dzięki której będzie można sterować pracą konkretnego wyjścia ekspandera, podając jako argument numer wyjścia (od 0 do 7) i stan, który ma zostać ustawiony (0 lub 1).
    3. Dodaj do biblioteki również funkcję MCP_GPIO_ReadPin, dzięki której możliwe będzie sprawdzanie stanu wyjścia. Funkcja powinna jako argument przyjmować numer pinu do sprawdzenia, a wynik powinien być zwracany jako 0 lub 1.
    4. Rozbuduj funkcję MCP_GPIO_ReadPin, tak aby sprawdzała, czy wybrany przez nas pin został wcześniej ustawiony faktycznie jako wyjście. Jeśli nie, to funkcja powinna zwrócić kod błędu −1.

    Podsumowanie – co powinieneś zapamiętać?

    Za nami omówienie podstaw komunikacji przez SPI. Najważniejsze, abyś po wykonaniu ćwiczeń z tej części kursu wiedział, jak wysyłać i odbierać dowolne bajty przez SPI i jak wydzielać fragmenty kodu do osobnych plików. Umiejętności te będą niezbędne podczas następnych ćwiczeń!

    Czy wpis był pomocny? Oceń go:

    Średnia ocena 4.7 / 5. Głosów łącznie: 12

    Nikt jeszcze nie głosował, bądź pierwszy!

    Artykuł nie był pomocny? Jak możemy go poprawić? Wpisz swoje sugestie poniżej. Jeśli masz pytanie to zadaj je w komentarzu - ten formularz jest anonimowy, nie będziemy mogli Ci odpowiedzieć!

    W kolejnej części kursu będziemy kontynuować temat komunikacji przez SPI. Zajmiemy się jednak znacznie bardziej rozbudowanym i trudniejszym układem, czyli wyświetlaczem graficznym. Taki moduł będzie wymagał od nas znacznie więcej pracy – zarówno w kontekście samego SPI, jak i reszty kodu.

    Nawigacja kursu

    Główny autor kursu: Piotr Bugalski
    Współautor: Damian Szymański, ilustracje: Piotr Adamczyk
    Oficjalnym partnerem tego kursu jest firma STMicroelectronics
    Zakaz kopiowania treści kursów oraz grafik bez zgody FORBOT.pl

    ekspander, kurs, kursSTM32L4, SPI

    Trwa ładowanie komentarzy...