Kursy • Poradniki • Inspirujące DIY • Forum
Interfejs SPI
SPI (ang. Serial Peripheral Interface), to szeregowy, synchroniczny interfejs komunikacyjny służący do transmisji danych pomiędzy układami scalonymi. SPI w odróżnieniu od interfejsu I2C do przesyłania danych wykorzystuje 3 linie transmisyjne, co pozwala na komunikację full-duplex.
Zastosowanie poszczególnych linii danych:
- MISO (Master Input Slave Output) - transmisja do urządzenia master.
- MOSI (Master Output Single Output) - transmisja do urządzeń slave.
- SCLK (Serial Clock) - sygnał zegarowy generowany przez urządzenie master.
Dodatkowo do każdego urządzenia typu slave musi zostać podłączona linia SS (Slave Select).
Pozwala ona na wybranie urządzenia, które ma odbierać
aktualnie transmitowane dane.
Jest to przydatne, gdy do magistrali podłączonych jest wiele urządzeń, z którymi komunikuje się urządzenie master. Można dzięki temu uprościć ramki danych o adresy układów, do których wysyłane są informacje.
Przy większej ilości urządzeń staje się to jednak problematyczne, ponieważ trzeba prowadzić osobną linię do każdego układu, z którym chcemy się komunikować.
Transmisja danych przez SPI
Urządzenie master odpowiada za generowanie sygnału zegarowego na linii SCLK. Dane transmitowane są do podłączonego urządzenia za pomocą linii MOSI. Urządzenie może odpowiadać na linii MISO. Mamy tu więc do czynienia z komunikacją full-duplex.
Za pomocą interfejsu SPI jesteśmy w stanie osiągnąć wielokrotnie
wyższe prędkości transmisji niż w przypadku I2C.
W przeciwieństwie do I2C, nie jest tu nam potrzebna wiedza na temat ramki danych. Musimy znać jednak kilka szczegółów, którymi cechuje się transmisja SPI.
Polaryzacja sygnału zegarowego
Polaryzację sygnału zegarowego (parametr transmisji CPOL) można skonfigurować na 2 sposoby:
- CPOL = 0 - oznacza to, że linia zegarowa w stanie bezczynności (Idle) przyjmuje stan niski (zazwyczaj 0V). W stanie aktywnym przyjmuje stan wysoki, odpowiadający logicznej "jedynce" w systemie (w naszym przypadku - 3V).
- CPOL = 1 - odwrotność przypadku poprzedniego.
Przesunięcie w fazie odbierania/wysyłania danych
Drugim z parametrów transmisji, z którym spotkamy się w SPI jest określenie, na którym zboczu zegara następuje odbieranie danych, a na którym transmisja. Odpowiada za to parametr CPHA.
- CPHA = 1 - dane są transmitowane na zboczu active⇒idle (przejście ze stanu aktywnego do stanu jałowego). Odbiór danych następuje na zboczu przeciwnym, a więc idle⇒active. Dla parametru CPOL = 0, transmisja danych odbywać się będzie na zboczu opadającym (3V ⇒ 0V), a odbiór danych na zboczu narastającym (0V⇒3V).
- CPHA = 0 - odwrotnie do sytuacji poprzedniej.
W skrócie, CPHA=0 powoduje transmisję danych na pierwszym zboczu sygnału zegarowego, a CPHA=1 na drugim zboczu sygnału zegarowego.
Informacje te przydadzą nam się w praktyce, ponieważ musimy być w stanie rozróżnić w jakim trybie pracuje urządzenie, z którym zamierzamy się komunikować.
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zestaw elementów do przeprowadzenia wszystkich ćwiczeń z kursu STM32 F4 można nabyć u naszego dystrybutora! Zestaw zawiera m.in. płytkę Discovery, wyświetlacz OLED, joystick oraz enkoder.
Zamów w Botland.com.pl »Wyświetlacz OLED
Aby wykorzystać interfejs SPI w praktyce, użyjemy graficznego wyświetlacza OLED. Można go znaleźć w zestawie elementów stworzonym specjalnie na potrzeby tego kursu. Sprzęt z którym będziemy pracować, to kolorowy wyświetlacz o przekątnej ekranu 0,95" wyprodukowany w technologii OLED. Rozdzielczość ekranu, to 96x64 piksele.
Jako sterownik wyświetlacza wykorzystano układ SSD1331. Przed przystąpieniem do konfiguracji warto przyjrzeć się krótkiej instrukcji użytkownika omawianego wyświetlacza. Na ostatniej stronie znajdziemy rysunek przedstawiający schemat transferu danych
Z powyższego rysunku możemy wyciągnąć dwa istotne wnioski. Po pierwsze, dane transmitowane są od bitu najstarszego, a więc od MSB (Most Significant Bit). Po drugie, próbkowanie odbywa się na zboczu narastającym. Oznacza to, że transmisja danych z mikrokontrolera musi się odbywać również na zboczu narastającym!
Wyprowadzenia wyświetlacza
Z boku wyświetlacza znajdują się piny, dzięki którym można się z nim komunikować. Przyjrzyjmy się wszystkim po kolei:
- VCC - zasilanie układu. Wyświetlacz można zasilać z 3 lub 5V.
- GND - masa układu.
- NC (Not Connected) - pin niepodłączony.
- DIN (Data In/Device In) - inna nazwa na MOSI (Master Output Slave Input). Wejście danych.
- CLK (Clock) - linia zegarowa.
- CS (Chip Select) - inna nazwa linii SS (Slave Select). Stan niski aktywuje nasłuchiwanie w urządzeniu, do którego mają być przesyłane dane.
- D/C (Data/Command) - stan niski na tej linii powoduje, że odbierane dane odczytywane są jako komendy do sterownika. W przeciwnym wypadku, jako zwykłe dane.
- RES (Reset) - stan niski na tej linii powoduje zresetowanie wyświetlacza.
Warto zauważyć, że żadna z linii nie odpowiada za transmisję danych z wyświetlacza do urządzenia master - komunikacja z tym układem jest jednostronna.
Konfiguracja mikrokontrolera
Spróbujemy teraz skonfigurować SPI tak, aby można było przesyłać dane do wyświetlacza.
Krok 1. Tworzymy nowy projekt i konfigurujemy zegary.
Krok 2. Uruchamiamy SPI1 na panelu z lewej strony. Normalnie skonfigurowalibyśmy go w trybie Full-Duplex, jednak jak już wcześniej zauważyliśmy, komunikacja z wyświetlaczem jest tylko jednostronna. Będziemy korzystać z jednej linii transmisyjnej, a konkretnie MOSI. Wystarczy w takim razie, jeśli skonfigurujemy SPI w trybie "Transmit Only Master".
Krok 3. Konfigurujemy 3 dodatkowe wyprowadzenia na linie CS, DC oraz RESET w trybie GPIO_Output. Aby ułatwić sobie później podłączanie wyświetlacza, warto również dodać etykiety do linii zegara oraz danych, tak jak na poniższym obrazku.
Krok 4. Przechodzimy do panelu konfiguracyjnego SPI.
- Frame Format - typ ramki danych. Praktycznie zawsze jest to format Motoroli.
- Data Size - szerokość ramki danych. SPI pozwala na przesyłanie ramek o szerokości 8 lub 16 bitów. Pozostawiamy domyślną wartość 8 Bits.
- First Bit - stanowi o stronie, od której zaczyna się transmisja bajtu. MSB (Most Significant Bit) - od najważniejszego bitu, a więc od bitu [7]. LSB (Least Significant Bit), od bitu [0]. Jak wiemy z dokumentacji, do sterownika wyświetlacza dane należy wysyłać w trybie MSB.
- Prescaler - dzielnik zegara taktującego SPI.
- Baud Rate - prędkość transmisji w bitach na sekundę.
- Clock Polarity (CPOL) - opisane na początku artykułu.
- Clock Phase (CPHA) - opisane na początku artykułu.
Pozostałe parametry w tej chwili nas nie interesują.
Krok 5. Generujemy projekt pod nazwą 07_SPI_OLED.
Uruchomienie wyświetlacza OLED
Teraz musimy skomunikować się z wyświetlaczem, aby wyświetlić pożądane grafiki.
Napisanie kodu realizującego te funkcje od zera wymaga dużej ilości pracy,
dlatego zazwyczaj wykorzystuje się do tego gotowe biblioteki.
Bibliotekę do obsługi wyświetlacza można znaleźć na stronie producenta. Nie jest ona jednak napisana pod biblioteki HAL i przystosowanie jej do naszych zastosowań wymaga wyodrębnienia kluczowych plików oraz dokonania drobnych zmian w kodzie.
Zmodyfikowaną przeze mnie, gotową do użycia bibliotekę
znajdziecie w paczce OLED.zip dołączonej do artykułu.
Dołączenie biblioteki
W bibliotece znajdują się 2 foldery - inc oraz src. Należy przekopiować ich zawartości do odpowiadających katalogów w głównym folderze projektu. Zawartość inc przekopiujemy więc do folderu .../07_SPI_OLED/Inc, a zawartość src do folderu .../07_SPI_OLED/Src. Następnie musimy dołączyć pliki nagłówkowe do projektu. Można to zrobić na dwa sposoby.
Dodawanie biblioteki - sposób 1
W drzewku projektu klikamy prawym przyciskiem myszy na folder User i wybieramy Import.
Następnie zaznaczamy General > File System i klikamy Next.
Naszym oczom ukaże się panel importowania plików. W celu sprecyzowania położenia plików, które mają być dodane, wybieramy Browse i odszukujemy położenie folderu .../07_SPI_OLED/Src. W moim przypadku jest to:
D:BibliotekiDocumentsStudiaForbotkurs-stm32f4Programy 7_SPI_OLEDSrc
Po sprecyzowaniu odpowiedniej ścieżki do plików wybieramy te, które mają być dodane (Fonts.c i SSD1331.c) i klikamy Finish.
Jeżeli wszystko wykonaliśmy poprawnie, w drzewku projektu powinny się pokazać dwa dodane przed chwilą pliki:
Dodawanie biblioteki - sposób 2
W eksploratorze plików należy zaznaczyć pliki do zaimportowania, a następnie przeciągnąć je w odpowiednie miejsce w drzewku projektu.
Używanie biblioteki do wyświetlacza OLED
Biblioteka jest już w pełni gotowa do użycia. Aby z niej skorzystać, należy dołączyć główny plik nagłówkowy, a więc SSD1331.h do pliku main.c
/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx_hal.h"
/* USER CODE BEGIN Includes */
#include "SSD1331.h"
/* USER CODE END Includes */
Otwierając ten plik możemy podejrzeć jakie funkcje udostępnia nam biblioteka.
Rozmiary czcionek
Pierwszą istotną rzeczą jaką tam znajdziemy są rozmiary czcionek zdefiniowane dla tej biblioteki. Rozmiary podane są w pikselach.
#define FONT_1206 12
#define FONT_1608 16
Kolory
Kolejną istotną rzeczą są wstępnie zdefiniowane kolory. Na listingu poniżej widzimy sposób definiowania każdego z nich. Na tej podstawie jesteśmy w stanie wygenerować nowe - wystarczy tylko znać odpowiedni "przepis" na dany kolor, będący mieszanką trzech składowoych RGB.
#define RGB(R,G,B) (((R >> 3) << 11) | ((G >> 2) << 5) | (B >> 3))
enum Color {
BLACK = RGB(0, 0, 0), // black
GREY = RGB(192, 192, 192), // grey
WHITE = RGB(255, 255, 255), // white
RED = RGB(255, 0, 0), // red
PINK = RGB(255, 192, 203), // pink
YELLOW = RGB(255, 255, 0), // yellow
GOLDEN = RGB(255, 215, 0), // golden
BROWN = RGB(128, 42, 42), // brown
BLUE = RGB(0, 0, 255), // blue
CYAN = RGB(0, 255, 255), // cyan
GREEN = RGB(0, 255, 0), // green
PURPLE = RGB(160, 32, 240), // purple
};
Funkcje rysujące
Ostatnią sekcją w omawianym pliku są prototypy funkcji, z których będziemy korzystać w celu obsługi wyświetlacza. Chcąc narysować prostokąt, linię czy okrąg, wystarczy udać się w to miejsce i zbadać składnię wywołania danej funkcji.
extern void ssd1331_init(void);
extern void ssd1331_draw_point(uint8_t chXpos, uint8_t chYpos, uint16_t hwColor);
extern void ssd1331_draw_line(uint8_t chXpos0, uint8_t chYpos0, uint8_t chXpos1, uint8_t chYpos1, uint16_t hwColor);
extern void ssd1331_draw_v_line(uint8_t chXpos, uint8_t chYpos, uint8_t chHeight, uint16_t hwColor);
extern void ssd1331_draw_h_line(uint8_t chXpos, uint8_t chYpos, uint8_t chWidth, uint16_t hwColor);
extern void ssd1331_draw_rect(uint8_t chXpos, uint8_t chYpos, uint8_t chWidth, uint8_t chHeight, uint16_t hwColor);
extern void ssd1331_fill_rect(uint8_t chXpos, uint8_t chYpos, uint8_t chWidth, uint8_t chHeight, uint16_t hwColor);
extern void ssd1331_draw_circle(uint8_t chXpos, uint8_t chYpos, uint8_t chRadius, uint16_t hwColor);
extern void ssd1331_display_char(uint8_t chXpos, uint8_t chYpos, uint8_t chChr, uint8_t chSize, uint16_t hwColor);
extern void ssd1331_display_num(uint8_t chXpos, uint8_t chYpos, uint32_t chNum, uint8_t chLen, uint8_t chSize, uint16_t hwColor);
extern void ssd1331_display_string(uint8_t chXpos, uint8_t chYpos, const uint8_t *pchString, uint8_t chSize, uint16_t hwColor);
extern void ssd1331_draw_1616char(uint8_t chXpos, uint8_t chYpos, uint8_t chChar, uint16_t hwColor);
extern void ssd1331_draw_3216char(uint8_t chXpos, uint8_t chYpos, uint8_t chChar, uint16_t hwColor);
extern void ssd1331_draw_bitmap(uint8_t chXpos, uint8_t chYpos, const uint8_t *pchBmp, uint8_t chWidth, uint8_t chHeight, uint16_t hwColor);
extern void ssd1331_clear_screen(uint16_t hwColor);
Podłączenie wyświetlacza
Ostatnią rzeczą, którą musimy zrobić przed rozpoczęciem pisania kodu jest fizyczne podłączenie wyświetlacza do płytki Discovery. Na początku artykułu przygotowaliśmy sobie ładne oznaczenia w STMStudio, więc nie powinno być problemu z odnalezieniem gdzie co należy podłączyć:
- DiscoveryF4 - Wyświetlacz OLED
- 3V lub 5V - VCC
- GNG - GND
- PA5 - CLK
- PA7 - DIN
- PC5 - CS
- PB1 - DC
- PE7 - RES
Na płytce wyświetlacza znajduje się jeszcze jedno wyprowadzenie - NC. Jest to skrót od Not Connected. Takie piny po prostu pozostawiamy niepodłączone.
Pierwszy program - Hello World
Czas napisać nasz pierwszy program wykorzystujący wyświetlacz OLED!
Krok 1. Prawie każde urządzenie przed rozpoczęciem pracy należy zainicjalizować. Odpowiednią funkcję znajdziemy w pliku SSD1331.h
ssd1331_init();
Krok 2. Następnie warto wyczyścić cały ekran, żeby mieć puste pole do rysowania.
ssd1331_clear_screen(BLACK);
Warto zauważyć, że funkcja clear_screen przyjmuje jako parametr kolor,
jakim ma wypełnić cały ekran.
Krok 3. Kiedy wyświetlacz jest już przygotowany, możemy spróbować wyświetlić dane:
ssd1331_display_string(0, 0, "Hello World!", FONT_1608, GREEN);
Funkcja display_string w pierwszych dwóch argumentach przyjmuje pozycję piksela (x,y), od którego ma zacząć rysować. Kolejnym parametrem wywołania jest napis do wyświetlenia - w naszym wypadku "Hello World". Ostatnie dwa parametry, to rozmiar oraz kolor napisu.
Krok 4. Możemy zbudować i wgrać program na mikrokontroler. Finalną wersję podstawowego Hello World przedstawia poniższy listing.
/* USER CODE BEGIN 2 */
ssd1331_init();
ssd1331_clear_screen(BLACK);
ssd1331_display_string(0, 0, "Hello World!", FONT_1608, GREEN);
/* USER CODE END 2 */
Jeżeli poprawnie skonfigurowaliśmy SPI, zaimportowaliśmy bibliotekę, dobrze podłączyliśmy kabelki i napisaliśmy kod bez błędów, po wgraniu i uruchomieniu programu na ekranie powinniśmy zobaczyć nasze pierwsze na tym wyświetlaczu Hello World.
Gdzie tu jest SPI?
Jak na razie w kodzie zajmowaliśmy się głównie wyświetlaczem, którego podobno obsługujemy przez SPI. Nie wywołaliśmy żadnej funkcji odpowiadającej za komunikację poprzez SPI, a jednak wyświetlacz działa. To dlatego, że w dołączonej bibliotece dodane są już odpowiednie wywołania. Można je znaleźć w pliku LIB_Config.h.
extern SPI_HandleTypeDef hspi1;
#define __SSD1331_RES_SET() HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_SET);
#define __SSD1331_RES_CLR() HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_RESET);
#define __SSD1331_DC_SET() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET);
#define __SSD1331_DC_CLR() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET);
#define __SSD1331_CS_SET() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
#define __SSD1331_CS_CLR() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
// Transmisja danych przez SPI w trybie blokujacym
#define __SSD1331_WRITE_BYTE(__DATA) HAL_SPI_Transmit(&hspi1, &__DATA, 1, 1000)
// Transmisja danych przez SPI z wykorzystaniem przerwan
//#define __SSD1331_WRITE_BYTE(__DATA) HAL_SPI_Transmit_IT(&hspi1, &__DATA, 1)
// Transmisja danych przez SPI z wykorzystaniem DMA
//#define __SSD1331_WRITE_BYTE(__DATA) HAL_SPI_Transmit_DMA(&hspi1, &__DATA, 1)
Ich analizę pozostawiam dla chętnych. Na tym etapie programowania nie trzeba zagłębiać się dalej. To, co najważniejsze, czyli wykorzystanie wyświetlacza LCD w praktyce można otrzymać bazując na opisanych do tej pory operacjach,
Podsumowanie
W tym odcinku nauczyliśmy się obsługiwać wyświetlacz OLED. Poznaliśmy również podstawy komunikacji za pomocą interfejsu SPI.
W następnym artykule zajmiemy się kolejnym sposobem przesyłania danych. Tym razem będzie to USB, czyli jeden z najczęściej używanych standardów komunikacji. To tyle na dziś. Jeżeli macie problemy lub uwagi, to piszcie w komentarzach!
Nawigacja kursu
Autor kursu: Bartek (Popeye) Kurosz
Redakcja: Damian (Treker) Szymański
Załączniki
Powiązane wpisy
F4, kurs, kursSTM32F4, OLED, programowanie, SPI, stm32, wyświetlacz
Trwa ładowanie komentarzy...