Kurs STM32 LL cz. 22. Komunikacja SPI w trybie polling

Poznaliśmy już podstawowe informacje o komunikacji SPI. Możemy przejść do implementacji podstawowych funkcji, czyli inicjalizacji, wysyłania i odbierania danych. Wykonamy także projekt z obsługą wyświetlacza e-Paper komunikujący się w trybie Transmit-Only.

Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.

[PROGRAM] Komunikacja w trybie Master (polling)

Inicjalizacja

Aby poprawnie używać SPI, najpierw musimy skonfigurować piny. Wybieramy tryb AF0, brak podciągnięcia, oraz niską częstotliwość pracy GPIO. Taką samą konfigurację wybieramy dla pinów MOSI, MISO oraz SCK.

LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);

LL_GPIO_SetPinOutputType(SPI_MOSI_GPIO_Port, SPI_MOSI_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(SPI_MOSI_GPIO_Port, SPI_MOSI_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(SPI_MOSI_GPIO_Port, SPI_MOSI_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetAFPin_0_7(SPI_MOSI_GPIO_Port, SPI_MOSI_Pin, LL_GPIO_AF_0);
LL_GPIO_SetPinMode(SPI_MOSI_GPIO_Port, SPI_MOSI_Pin, LL_GPIO_MODE_ALTERNATE);

LL_GPIO_SetPinOutputType(SPI_MISO_GPIO_Port, SPI_MISO_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(SPI_MISO_GPIO_Port, SPI_MISO_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(SPI_MISO_GPIO_Port, SPI_MISO_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetAFPin_0_7(SPI_MISO_GPIO_Port, SPI_MISO_Pin, LL_GPIO_AF_0);
LL_GPIO_SetPinMode(SPI_MISO_GPIO_Port, SPI_MISO_Pin, LL_GPIO_MODE_ALTERNATE);

LL_GPIO_SetPinOutputType(SPI_SCK_GPIO_Port, SPI_SCK_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(SPI_SCK_GPIO_Port, SPI_SCK_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(SPI_SCK_GPIO_Port, SPI_SCK_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetAFPin_0_7(SPI_SCK_GPIO_Port, SPI_SCK_Pin, LL_GPIO_AF_0);
LL_GPIO_SetPinMode(SPI_SCK_GPIO_Port, SPI_SCK_Pin, LL_GPIO_MODE_ALTERNATE);

Pinem CS będziemy sterowali programowo. Wykorzystamy do tego pin PA4 w trybie wyjścia Push-Pull.

LL_GPIO_SetPinOutputType(SPI_CS_GPIO_Port, SPI_CS_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(SPI_CS_GPIO_Port, SPI_CS_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(SPI_CS_GPIO_Port, SPI_CS_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(SPI_CS_GPIO_Port, SPI_CS_Pin, LL_GPIO_MODE_OUTPUT);

Teraz przejdziemy do konfiguracji SPI. Najpierw włączamy taktowanie.

LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SPI1);

Konfiguracja powinna odbywać się przy wyłączonym SPI.

LL_SPI_Disable(spi);

Teraz ustawiamy SPI w trybie Master.

LL_SPI_SetMode(spi, LL_SPI_MODE_MASTER);

Następnie wybieramy komunikację Full-duplex.

LL_SPI_SetTransferDirection(spi, LL_SPI_FULL_DUPLEX);

Konfigurujemy polaryzację jako niską oraz odczytanie danych na pierwsze zbocze.

LL_SPI_SetClockPolarity(spi, LL_SPI_POLARITY_LOW);
LL_SPI_SetClockPhase(spi, LL_SPI_PHASE_1EDGE);

Pinem CS będziemy sterowali programowo.

LL_SPI_SetNSSMode(spi, LL_SPI_NSS_SOFT);

Wykorzystamy komunikację z prędkością 8 MBits/s. Ponieważ magistrala SPI jest taktowana sygnałem PCLK 64 MHz, wybieramy perskaler 8.

LL_SPI_SetBaudRatePrescaler(spi, LL_SPI_BAUDRATEPRESCALER_DIV8);

Format danych wymaga kolejności od najstarszego bitu.

LL_SPI_SetTransferBitOrder(spi, LL_SPI_MSB_FIRST);

Dane przesyłamy po 8 bitów.

LL_SPI_SetDataWidth(spi, LL_SPI_DATAWIDTH_8BIT);

Wybieramy format Motorola.

LL_SPI_SetStandard(spi, LL_SPI_PROTOCOL_MOTOROLA);

Na koniec wybieramy jeszcze, aby zdarzenie RXNE występowało przy 8 bitach w kolejce RX FIFO.

LL_SPI_SetRxFIFOThreshold(spi, LL_SPI_RX_FIFO_TH_QUARTER);

Wysyłanie danych

W pierwszej kolejności pokażę, jak wysyłać dane. SPI jest wyłączony, dlatego przed transmisją go włączamy.

LL_SPI_Enable(spi);

Następnie przed wysłanie każdego bajtu czekamy, aż flaga TXE będzie ustawiona. Potem wysyłamy bajt.

while(count < size)
{
	while (!LL_SPI_IsActiveFlag_TXE(spi))
		;
	LL_SPI_TransmitData8(spi, *(data+count));
	count++;
}

Po wysłaniu wszystkich bajtów chcemy wyłączyć SPI. Musimy wykonać to zgodnie z procedurą opisaną w dokumentacji. Czekamy aż kolejka TX FIFO będzie pusta, czyli wyjdą z niej wszystkie dane.

while (LL_SPI_GetTxFIFOLevel(spi) != LL_SPI_TX_FIFO_EMPTY)
	;

Teraz czekamy, aż cała transmisja się zakończy. Wskazuje na to flaga Busy.

while (LL_SPI_IsActiveFlag_BSY(spi) != 0)
	;

Wyłączamy SPI.

LL_SPI_Disable(spi);

Musimy odczytać jeszcze wszystkie dane z kolejki RX FIFO, aby po ponownym uruchomieniu SPI nie powodowały problemów.

while (LL_SPI_GetRxFIFOLevel(spi) != LL_SPI_RX_FIFO_EMPTY)
{
	LL_SPI_ReceiveData8(spi);
}

Na koniec jeszcze czyścimy flagę Overrun na wypadek, gdyby wystąpiło przepełnienie.

LL_SPI_ClearFlag_OVR(spi);

Odbieranie danych

W podobny sposób realizujemy odbiór danych. Włączamy zatem SPI.

LL_SPI_Enable(spi);

Jak już opisywałem wcześniej, magistrala SPI działa w ten sposób, że każde odebranie danych wymaga wygenerowania sygnału zegarowego, czyli wysłania danych. Przy odbiorze mogą to być dowolne dane – ważne, żeby pojawiła się na magistrali transmisja. Potem czekamy, aż dana pojawi się w rejestrze i odczytujemy dane.

while(count < size)
{
	while (!LL_SPI_IsActiveFlag_TXE(spi))
		;
	LL_SPI_TransmitData8(spi, DUMMY_BYTE);
	while (!LL_SPI_IsActiveFlag_RXNE(spi))
		;
	*(data+count) = LL_SPI_ReceiveData8(spi);
	count++;
}

Teraz wykonujemy procedurę wyłączenia. Czekamy aż kolejka TX FIFO będzie pusta, czyli wyjdą z niej wszystkie dane.

while (LL_SPI_GetTxFIFOLevel(spi) != LL_SPI_TX_FIFO_EMPTY)
	;

Czekamy, aż cała transmisja się zakończy. Wskazuje na to flaga Busy.

while (LL_SPI_IsActiveFlag_BSY(spi) != 0)
        ;

Wyłączamy SPI.

LL_SPI_Disable(spi);

Musimy odczytać jeszcze wszystkie dane z kolejki RX FIFO, aby po ponownym uruchomieniu SPI nie powodowały problemów.

while (LL_SPI_GetRxFIFOLevel(spi) != LL_SPI_RX_FIFO_EMPTY)
{
	LL_SPI_ReceiveData8(spi);
}

Na koniec jeszcze czyścimy flagę Overrun.

LL_SPI_ClearFlag_OVR(spi);

Wyświetlacz e-Paper

Omówiliśmy konfigurację i sposób działania magistrali SPI. Czas najwyższy, aby przygotować praktyczny przykład. W tym celu wykorzystamy wyświetlacz e-Paper. Ja do ćwiczenia użyję modułu 1.54inch e-Paper Module (B) od Waveshare.

1.54inch e-Paper Module (B) to moduł z wyświetlaczem e-Paper o przekątnej 1,54”. Ma rozdzielczość 200×200 pikseli oraz wbudowany sterownik, który komunikuje się przez interfejs SPI. Moduł z dopiskiem (B) pozwala na wyświetlanie treści w trzech kolorach: czarnym, białym lub czerwonym.

Wyświetlacze e-Paper są bardzo ciekawym rozwiązaniem, które zyskało na popularności dzięki temu, że pobiera bardzo mało prądu. Wyświetlacze te nie potrzebują podświetlenia, a ostatnio wyświetlana treść pozostaje widoczna nawet po wyłączeniu zasilania. Dzięki temu wyświetlacz pobiera prąd tylko w momencie odświeżania (zmiany treści). Ale jak to w życiu, jeśli są zalety, to są również wady. Wyświetlacze e-Paper potrzebują dość długich czasów na pojawienie się nowej treści. Np. w przypadku mojego modułu z trójkolorowym wyświetlaczem, do pełnego odświeżenia potrzeba aż 14 s. Dlatego tego typu wyświetlacze stosowane są tam, gdzie częste odświeżanie treści nie jest potrzebne. Są powszechnie stosowane m.in. w czytnikach ebooków lub jako cyfrowe etykiety w sklepach.

Do ćwiczenia możesz wykorzystać wyświetlacz z inną przekątną czy liczbą kolorów. Nie będziemy zagłębiali się w sposób komunikacji z wyświetlaczem i wykorzystamy biblioteki udostępnione przez producenta – mając inny moduł po prostu skopiujesz inny plik biblioteczny.

Zanim przejdziemy do kodu obsługi wyświetlacza, musimy go podłączyć do Nucleo. Moduł e-Paper wykorzystuje 8 pinów:

e-PaperNucleo-G071RB
VCC3.3V
GNDGND
DINPA7
CLKPA1
CSPA4
DCPC1
RSTPC0
BUSYPC2

Piny przedstawione w tabeli znajdziemy na płytce Nucleo w miejscach oznaczonych na grafice.

Piny VCC i GND odpowiadają za zasilanie. DIN i CLK to piny interfejsu SPI – odpowiednio MOSI i SCLK. Będziemy tylko wysyłać dane, dlatego linia MISO nie jest nam potrzebna. CS odpowiada za wybór urządzenia. 

Dodatkowo wyświetlacz wymaga trzech pinów sterujących pracujących jako GPIO. DC (Data/Command) to wybór typu przesyłanych informacji. RST odpowiada za reset modułu, a BUSY informuje o tym, że wyświetlacz przetwarza informacje i należy poczekać z wysyłką kolejnych danych.

[PROGRAM] Obsługa wyświetlacza e-Paper w trybie master transmit-only (polling)

Jak wspomniałem, do obsługi wyświetlacza wykorzystamy bibliotekę producenta. Można ją pobrać ze strony waveshare.com.

Przechodzimy do folderu STM32->STM32-F103ZET6 i kopiujemy do naszego projektu folder User. Znajdziemy w nim foldery:

  • Config – konfiguracja hardware
  • e-Paper – biblioteki do obsługi wszystkich modułów e-Paper od Waveshare
  • Examples – przykładowe użycie bibliteki
  • Fonts – biblioteka do obsługi czcionek
  • GUI – biblioteka do łatwego dodawania tekstów i grafik

W pierwszej kolejności dodajemy nagłówki używanych bibliotek.

#include "DEV_Config.h"
#include "EPD_1in54b.h"
#include "GUI_Paint.h"

Zadeklarujemy również dwie tablice, które będą przechowywały obraz. Ze względu na to, że korzystam z trzykolorowego wyświetlacza, potrzebuję dwóch tablic. Tło będzie białe, natomiast w jednej tablicy umieszczę treść w kolorze czarnym, zaś w drugiej w kolorze czerwonym. Robimy to w ten sposób, ponieważ tak wyświetlacz przyjmuje dane. Wysyłamy pełne dwie tablice, a sterownik sam odpowiednio nakłada je na siebie.

#define IMAGE_SIZE_EPD_200_200_PX 5000
uint8_t black_image[IMAGE_SIZE_EPD_200_200_PX];
uint8_t red_image[IMAGE_SIZE_EPD_200_200_PX];

Dlaczego mają rozmiar 5000? 200 pikseli razy 200 pikseli daje 40 000. Ale w jednym elemencie tablicy uint8_t zmieścimy 8 pikseli, dlatego 40 000 dzielimy przez 8. Daje nam to 5000.

Teraz przejdźmy do inicjalizacji układów peryferyjnych. Konfigurujemy piny RST i DC jako wyjścia oraz BUSY jako wejście cyfrowe.

LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOC);

LL_GPIO_SetPinOutputType(RST_GPIO_Port, RST_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(RST_GPIO_Port, RST_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(RST_GPIO_Port, RST_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(RST_GPIO_Port, RST_Pin, LL_GPIO_MODE_OUTPUT);

LL_GPIO_SetPinOutputType(DC_GPIO_Port, DC_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(DC_GPIO_Port, DC_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(DC_GPIO_Port, DC_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(DC_GPIO_Port, DC_Pin, LL_GPIO_MODE_OUTPUT);

LL_GPIO_SetPinPull(BUSY_GPIO_Port, BUSY_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(BUSY_GPIO_Port, BUSY_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(BUSY_GPIO_Port, BUSY_Pin, LL_GPIO_MODE_INPUT);

Następnie inicjalizujemy interfejs SPI zgodnie z algorytmem opisanym wcześniej.

spi_init();

Teraz wywołamy funkcję inicjalizujacą modułu. Ustawia ona w stan początkowy piny DC, BUSY i RST.

DEV_Module_Init();

Tutaj musimy się chwilę zatrzymać. Obsługa modułu wymaga konfiguracji pliku DEV_Config.h. Domyślnie biblioteka korzysta z funkcji HAL-a. Dlatego musimy ją zmodyfikować. Definicje pinów będą wyglądały podobnie (albo nawet tak samo).

#define EPD_RST_PIN     RST_GPIO_Port, RST_Pin
#define EPD_DC_PIN      DC_GPIO_Port, DC_Pin
#define EPD_CS_PIN      SPI_CS_GPIO_Port, SPI_CS_Pin
#define EPD_BUSY_PIN    BUSY_GPIO_Port, BUSY_Pin

Makra funkcji sterujących pinami GPIO zmodyfikujemy do postaci wykorzystujących nazwy z biblioteki Low Layer.

#define DEV_Digital_Write(_pin, _value) (_value == 0 ? LL_GPIO_ResetOutputPin(_pin):LL_GPIO_SetOutputPin(_pin))
#define DEV_Digital_Read(_pin) LL_GPIO_IsInputPinSet(_pin)

Modyfikujemy też makro do użycia opóźnienia.

#define DEV_Delay_ms(__xms) LL_mDelay(__xms);

Pozostaje jeszcze zmodyfikowanie funkcji void DEV_SPI_WriteByte(), która odpowiada za wysłanie jednego bajtu danych po SPI. Zmieniamy funkcję z biblioteki HAL-a na napisaną przez nas.

void DEV_SPI_WriteByte(UBYTE value)
{
	spi_write_data(&value, 1);
}

Teraz możemy wrócić do obsługi wyświetlacza w funkcji main. Inicjalizujemy wyświetlacz i go czyścimy.

EPD_1IN54B_Init();
EPD_1IN54B_Clear();

Dodajemy dwa wskaźniki na tablice treści w kolorze czarnym i czerwonym. Na wskaźnikach będzie nam łatwiej operować.

uint8_t *BlackImage, *RedImage;
BlackImage = black_image;
RedImage = red_image;

Teraz wykorzystamy funkcje z biblioteki GUI. Wskazujemy bibliotece, gdzie przechowujemy dane o wyświetlanej treści. Do funkcji jako przedostatni argument przekazujemy orientację treści. Orientacja ta dotyczy jednak tylko treści grafik lub napisów tworzonych przy pomocy biblioteki. Wrzucając cały obraz, orientacja ta nie będzie zachowana. Podejrzewam, że wymagałoby to zbyt dużej ilości obliczeń i taka funkcjonalność nie została zaimplementowana.

Paint_NewImage(BlackImage, EPD_1IN54B_WIDTH, EPD_1IN54B_HEIGHT, ROTATE_270, WHITE);
Paint_NewImage(RedImage, EPD_1IN54B_WIDTH, EPD_1IN54B_HEIGHT, ROTATE_270, WHITE);

Teraz wybieramy do modyfikacji treść w kolorze czarnym. Czyścimy tło kolorem białym i dodajemy obraz. Ja przygotowałem bitmapę z logiem kursu. Jak wspomniałem, musi być ona od razu przygotowane w odpowiedniej orientacji.

Paint_SelectImage(BlackImage);
Paint_Clear(WHITE);
Paint_DrawBitMap(logo);

Teraz wybieramy kolor czerwony. Na tej warstwie dodamy napis “stm32wrobotyce.pl” umieszczony na dole wyświetlacza.

Paint_SelectImage(RedImage);
Paint_Clear(WHITE);
Paint_DrawString_EN(5, 175, "stm32wrobotyce.pl", &Font16, WHITE, BLACK);

Teraz pozostaje wyświetlić przygotowane treści.

EPD_1IN54B_Display(BlackImage, RedImage);

Możemy uruchomić przykład. Pojawienie się grafiki i tekstu chwilę zajmie. Czas może różnić się w zależności od użytego wyświetlacza. Jest zawsze podany w opisie modułu jako “Refresh time”. Efekt wywołane przygotowanego projektu jest widoczny na zdjęciu poniżej. Warto sprawdzić, czy po odłączeniu zasilania od zestawu Nucleo obraz nadal będzie widoczny. Jest to niewątpliwie największa zaleta wyświetlaczy e-Paper.

[Dodatek] Przygotowanie tablicy z grafiką pod wyświetlacz e-Paper

Jeżeli chciałbyś wyświetlić własną grafikę, trzeba odpowiednio przygotować tablicę z danymi. W dużym skrócie pokażę Ci jak to zrobić w szybki i łatwy sposób.

Wykorzystamy do tego gotowy program Image2Lcd. Wypakowujemy folder i uruchamiamy aplikację Img2Lcd.exe.

Otwieramy plik z grafiką (Open. Jak wspomniałem, w zależności w jakiej orientacji chcesz wyświetlać obraz, musisz go odpowiednio obróć wcześniej np. za pomocą Painta. Zrób kilka prób, a na pewno zorientujesz się w czym rzecz 🙂

Wybieramy parametry po lewej stronie okna. Chcemy otrzymać tablicę C (C array), tryb skanowania poziomy (Horizontal Scan), obraz monochromatyczny oraz rozdzielczość (w moim przypadku) 200 x 200 pikseli i klikamy strzałkę obok, aby zatwierdzić zmiany. Odznaczamy “Include head data”, ponieważ nie potrzebujemy, aby w tablicy na początku były umieszczone informacje o rozmiarze obrazu. Dodatkowo zaznaczamy opcję “Reverse color”. W przypadku wyświetlaczy e-Paper kolor biały jest oznaczany wartościami 0xFF, a czarny 0x00. Dlatego aby poprawnie wyświetlić grafikę, ta opcja jest niezbędna.

Teraz możemy zapisać plik klikając “Save”. Dodajemy plik do projektu. Dodatkowo aby był on widoczny w pliku main, przed funkcją main() musimy dodać zmienną globalną z atrybutem extern.

extern const unsigned char logo[];

Oczywiście nazwa powinna być taka, jaką zapisaliśmy w pliku z obrazem. Teraz grafika jest gotowa do użytku.

Chciałbyś otrzymywać na bieżąco informacje o nowych artykułach z kursu? Zapisz się do newslettera!

TO NIE TYLKO MAIL Z INFORMACJĄ O NOWEJ LEKCJI, ALE TAKŻE DODATKOWE MATERIAŁY. NIE PRZEGAP NOWEJ TREŚCI I DODATKOWYCH BONUSÓW. PRZEJDŹ DO STRONY KURSU I PODAJ SWÓJ ADRES E-MAIL. NIE ZAPOMNIJ POTWIERDZIĆ CHĘCI DOŁĄCZENIA W PIERWSZEJ WIADOMOŚCI!
Repozytorium GitHub

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *