KursyPoradnikiInspirujące DIYForum

Kurs STM32 – #10 – SPI w praktyce, wyświetlacz graficzny

Kurs STM32 – #10 – SPI w praktyce, wyświetlacz graficzny

W poprzedniej części kursu poznaliśmy działanie interfejsu SPI. W praktyce wykorzystaliśmy go do komunikacji z ekspanderem portów MCP23S08.

Teraz zajmiemy się ciekawszym tematem, czyli wyświetlaczem graficznym z telefonu Nokia 5110. Jest to tani i popularny moduł, który może ułatwić interakcję z budowanym urządzeniem.

Wyświetlacz LCD - Nokia 5110

Zanim zaczniemy podłączać przewody i programować, powinniśmy poświęcić chwilę na zapoznanie się z naszym modułem. Ten typ wyświetlacza był kiedyś wykorzystywany w telefonach Nokia 5110, stąd nazwa modułu. Ma on rozdzielczość 84 na 48 pikseli i monochromatyczną matrycę LCD. Wchodzi on w skład rozszerzonych zestawów dedykowanych do tego kursu.

04614_5

W załączniku do tego artykułu można znaleźć dokumentację sterownika PCD8544, na którym bazuje wyświetlacz. Jak widać na zdjęciu (oraz w dokumentacji), jest on sterowany przez interfejs SPI oraz kilka dodatkowych linii.

  • RST - linia resetująca rejestry wyświetlacza. Przed rozpoczęciem pracy z wyświetlaczem, należy wygenerować na niej stan zero przez co najmniej 100ns. Podczas normalnej pracy wyświetlacza, na tej linii powinien być ciągle stan wysoki.
  • CE - jest to linia CS interfejsu SPI, nazwa pochodzi od Chip Enable
  • DC - linia ustalająca, czy przesyłamy dane (stan wysoki), czy komendy dla wyświetlacza (stan niski)
  • Din - linia danych, czyli MOSI interfejsu SPI
  • CLK - linia zegarowa SPI, odpowiada SCLK
  • VCC - napięcie zasilające moduł (3.3V)
  • BL - podświetlanie wyświetlacza
  • GND - masa

Jak widzimy znajdziemy tutaj jednokierunkowy interfejs SPI (nie możemy odczytywać danych z wyświetlacza). Producent zastosował inne oznaczenia linii (CE zamiast CS, Din zamiast MOSI i CLK zamiast SCLK), jednak sam interfejs działa bez zmian.

Dodatkowe linie sterujące to: RST, która resetuje zawartość pamięci i rejestrów sterownika wyświetlacza oraz DC, która pozwala na wybór typu przesyłanych danych. Można przesyłać dane przeznaczone do wyświetlania lub komendy nim sterujące. Poza tym mamy trzy linie zasilające: masę (GND), zasilanie 3.3V (VCC) oraz zasilanie dla podświetlania (BL).

Czas na praktykę!

Czas na praktykę!

Podłączenie wyświetlacza z płytką Nucleo

Gdy już poznaliśmy nasz moduł, czas połączyć go z płytką Nucleo-STM32. Wykorzystamy ten sam interfejs SPI, co w poprzedniej części kursu. Nowe linie sterujące (DC, CE, RST) podłączymy do portu C. Zostawimy pin PC0 wolny - dzięki temu, będziemy mogli w przyszłości podłączyć jednocześnie MCP23S08 oraz wyświetlacz.

Wykorzystamy więc kolejne piny PC1, PC2 i PC3. Pin sterujący podświetlaniem możemy podłączyć do PC4, ale jeśli chcemy regulować jasność świecenia, wykorzystajmy do tego wyjście PWM - omawialiśmy je w 7 części kursu. Podłączymy więc linię BL do pinu PB6.

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!

Zamów w Botland.com.pl »

Kompletny schemat montażowy wygląda następująco:

spi-lcd_bb

Podłączenie wyświetlacza graficznego do STM32.

Komunikacja z wyświetlaczem

Ponieważ nowych linii sterujących jest nieco więcej, zdefiniujemy stałe, które będą określały, do których pinów podłączyliśmy wyświetlacz (wszystkie dotyczą portu C):

Konfiguracja SPI została omówiona w poprzedniej części, teraz kod wygląda dokładnie tak samo. Dodajemy tylko konfigurację nowych linii:

Linia CE oraz RST jest aktywowana stanem niskim, więc wystawiamy na nich logiczną jedynkę. Pierwszym krokiem podczas komunikacji z wyświetlaczem jest zresetowanie jego kontrolera. W tym celu, na wyjściu RST musimy wygenerować stan 0 przez co najmniej 100 ns.

Napiszmy więc prostą procedurę:

Opóźnienie nie było konieczne, ale w razie problemów zawsze można dodać małe opóźnienie do procedury resetowania.

Następny etap to wysyłanie komend sterujących pracą wyświetlacza. Do rozróżnienia między danymi, a komendami służy linia DC, stan niski informuje o wysyłaniu komendy sterującej. Teraz możemy napisać prostą procedurę, która wyśle komendę do wyświetlacza:

Ostatni etap, to wysłanie danych do wyświetlania. Przygotujemy procedurę, która za jednym wywołaniem prześle cały bufor z pamięci RAM lub Flash do wyświetlacza. Jako parametry będziemy podawali adres bufora oraz liczbę bajtów do przesłania:

Skąd weźmiemy dane do wyświetlania? Możemy je wygenerować programowo. Inna możliwość to wykorzystanie konwertera bitmap do przetłumaczenia obrazka na tablicę wartości. Wykorzystamy drugą opcję i wyświetlimy teraz logo Forbota.

Konwerter, po wczytaniu mapy bitowej z grafiką i ustawieniem odpowiedniej rozdzielczości ekranu, przygotuje dla nas tablicę, taką jak poniższa. Obsługa programu jest bardzo intuicyjna. Gdyby ktoś miał z nią jednak problemy, to proszę pytać w komentarzach.

Fragment tablicy, w której zapisany został kawałek loga Forbota:

Mamy już wszystkie dane potrzebne do obsługi wyświetlacza. Nie będziemy w tej chwili wnikać w wartości rejestrów sterownika ekranu, napiszemy więc kod z magicznymi liczbami. Jednak ciekawskich odsyłam do wspomnianej wcześniej dokumentacji.

Tylko tyle kodu jest niezbędne, aby zobaczyć logo na wyświetlaczu. Oczywiście musieliśmy wcześniej przygotować sporo funkcji pomocniczych, ale teraz obsługa LCD jest już łatwa.

Pełny kod programu:

Efekt działania programu w praktyce wygląda następująco:

STM32 i wyświetlacz graficzny.

STM32 i wyświetlacz graficzny.

Zadanie domowe 10.1

Przygotuj program, który będzie na ekranie wyświetlał inną bitmapę (np.: Twój avatar). Zdjęciem działającego układu pochwal się w komentarzu!

Reorganizacja kodu

Dotychczas wszystkie przykłady były pisane w jednym pliku. Taki styl programowania jest odpowiedni do prostych i krótkich przykładów, ale w miarę jak programy stają się bardziej rozbudowane jest bardzo niewygodny i nieczytelny.

Znacznie lepszym rozwiązaniem jest podzielenie dużego programu na mniejsze moduły. Takie moduły są czytelniejsze, a co więcej możemy je wykorzystywać w kolejnych programach.

Spróbujemy podzielić program sterujący wyświetlaczem na mniejsze moduły, a jednocześnie dodać więcej funkcji do samego sterowania wyświetlaczem.

W pierwszym przykładzie po prostu skopiowaliśmy gotowy bufor danych z pamięci Flash do wyświetlacza. Teraz będziemy postępować dwustopniowo - w pamięci RAM mikrokontrolera zadeklarujemy tablicę z buforem danych obrazu. Będziemy w niej przygotowywać dane do wyświetlenia, a następnie w jednym kroku kopiować całą tablicę do LCD.

Takie działanie ma kilka zalet. Po pierwsze dostęp do pamięci RAM jest o wiele szybszy niż do danych wyświetlacza. Po drugie nie możemy odczytywać danych z wyświetlacza, więc manipulacja zawartością ekranu byłaby dużo trudniejsza.

Najpierw podzielmy nasz program na mniejsze moduły. W poprzednich częściach kursu, często wykorzystywaliśmy procedurę delay_ms(). Jest ona bardzo wygodna, ale za każdym razem musieliśmy ją wklejać do kodu. Zamiast tego przygotujemy dla niej moduł, który w przyszłości może jeszcze nam się przydać.

W języku C moduł składa się z dwóch części: pliku nagłówkowego z rozszerzeniem .h oraz kodu źródłowego, z rozszerzeniem .c. Plik nagłówkowy powinien zawierać tylko deklaracje zmiennych i funkcji, natomiast cały kod powinien zostać wstawiony do pliku .c.

Zobaczmy to na przykładzie. Najpierw przygotowujemy plik delay.h. W celu dodania nowego pliku najlepiej w widoku projektu, kliknąć na katalog src i wybrać opcję New, a następnie Header File lub Source File (zależnie, czy dodajemy plik .h, czy .c):

Tworzenie plików nagłówkowych.

Tworzenie plików nagłówkowych.

Następnie treść wypełniamy w poniższy sposób:

Dyrektywy #ifndef służą unikaniu wielokrotnego włączania tego samego pliku nagłówkowego. Natomiast, to co ważne, to deklaracja procedury delay_ms(). Jest opatrzona słówkiem kluczowym extern, żeby zasygnalizować iż jest to tylko deklaracja - kod procedury znajdzie się w innym pliku.

Słówko extern jest niezbędne przy deklarowaniu zmienych. W przypadku procedur, szczególnie osoby programujące w C++ mogą je omijać. Ale ponieważ programujemy w C, zostawimy je dla zwiększenia czytelności kodu.

Kod procedury oraz wszystko co jest jej potrzebne do działania (zmienną licznikową, procedurę obsługi przerwania) przenosimy do pliku delay.c, który tworzymy analogicznie do wcześniejszego, musimy tylko wybrać odpowiedni typ, czyli Source File:

Jaka jest zaleta takiego postępowania? Najważniejsza to podział większego problemu na mniejsze. Kiedy używamy modułu delay, interesuje nas tylko plik nagłówkowy delay.h - implementacja jest dla nas nieistotna. Zakładamy, że to, co zadeklarowane zostało przetestowane i działa. Jak zobaczymy później pliki z kodem mogą być o wiele dłuższe niż pliki nagłówkowe. Dużo łatwiej jest wiec pamiętać tylko o zawartości pliku delay.h niż zastanawiać się która funkcja do czego służy.

W poprzednim programie, bardzo dużo miejsca zajęła deklaracja danych z logo Forbot-a. Czytanie takiego kodu nie jest najprzyjemniejsze. Gdybyśmy dodali kolejne obrazki, plik main.c byłby prawie nieczytelny. Wydzielmy więc dane bitmap do oddzielnego modułu. Nazwijmy go bitmap.h:

Jak widać jest to właściwie jedna linijka - łatwo do przeczytania i zrozumienia. Natomiast implementacja wygląda następująco (bitmap.c):

Ponieważ rysowanie gotowych bitmap może nam się wydać niewystarczające, dodamy procedurę wyświetlającą napisy. Niestety w tym celu będziemy potrzebowali fontów. Wykorzystamy gotowy font o wymiarach 5x8 pikseli.

Pamiętajcie proszę o licencji - Beerware, którą znajdziecie w pliku z kodem!

Plik font.h:

Implementacja w pliku font.c:

Mając już wszystkie moduły pomocnicze możemy napisać naszą bibliotekę graficzną. Plik nagłówkowy lcd.h wygląda następująco:

Jak widzimy, przygotowujemy 5 procedur:

  • lcd_setup - konfiguruje wyświetlacz, powinna być wywołana na początku programu
  • lcd_clear - czyści bufor ekranu
  • lcd_draw_bitmap - rysuje bitmapę na całym ekranie
  • lcd_draw_text - wypisuje tekst na ekranie
  • lcd_copy - przesyła zawartość bufora do wyświetlacza

Jest to prosta biblioteka, ale jej kod jest już dość długi. Tutaj pojawia się jednak zaleta modułowości - nie musimy go czytać. A nawet jeśli przeczytamy (do czego zachęcam), nie będziemy musieli pamiętać wszystkich jego detali. Do używania wystarczy znajomość pliku nagłówkowego.

Kod lcd.c:

Program główny

Mamy już przygotowaną bibliotekę. Napiszmy teraz prosty programik, który będzie jej używał. Najpierw wyświetlimy logo Forbot-a. Poczekamy 1 s, po czym wypiszemy w kolejnych wierszach napis "Forbot!". Kod jest bardzo prosty:

Pełny kod programu (main.c):

Efekt działania powyższego programu widoczny jest na poniższym zdjęciu:

Wyświetlanie napisów na wyświetlaczu przez SPI na STM32.

Wyświetlanie napisów na wyświetlaczu przez SPI na STM32.

Zadanie domowe 10.2

Napisz program, który na ekranie wyświetlacza umieści komunikat przesłany do mikrokontrolera przez UART. Zadbaj o odpowiednie łamanie wierszy!

Rozbudowa biblioteki graficznej

Poprzednia wersja biblioteki jest bardzo uboga. Dodajmy więc jeszcze dwie funkcje:

  • lcd_draw_pixel - do zapalania wybranego punktu na ekranie
  • lcd_draw_line - rysującą linię między wybranymi punktami

Dzięki temu będziemy mogli rozszerzyć możliwości naszego wyświetlacza. Do pliku nagłówkowego warto również dodać stałe opisujące wymiary ekranu - będzie się z nich łatwiej korzystać. Nowy nagłówek biblioteki wygląda następująco (lcd.h):

Do rysowania linii wykorzystywany jest algorytm Brensenhama. Kod lcd.c:

Aby zaprezentować nowe możliwości, napiszemy program, który narysuje ramkę dookoła ekranu, po czym będzie wyświetlał obracającą się linię:

Działanie programu widoczne jest na poniższej animacji (słabo jakość wynika z kompresji obrazu):

Prosta animacja na wyświetlaczu.

Prosta animacja na wyświetlaczu.

Zadanie domowe 10.3

Napisz program, który zapełni wszystkie punkty na ekranie (piksel po pikselu).

Podświetlanie ekranu

Dotychczas nie wykorzystywaliśmy podświetlania ekranu. Niewątpliwą zaletą technologii LCD w porównaniu z TFT jest możliwość pracy wyświetlacza nawet bez podświetlania (czyli wykorzystując światło z otoczenia).

Możemy jednak dodać odpowiedni kod i sprawdzić jak łatwo jest sterować jasnością podświetlania. Sterowanie PWM omawialiśmy już w kursie, więc po prostu wykorzystamy omówiony tam kod:

W praktyce program działał następująco:

Zadanie domowe 10.4

Napisz program, który dostosuje jasność podświetlenia do informacji odczytanej z podłączonego fotorezystora. Im otoczenie jest ciemniejsze, tym jasność podświetlenia powinna być większa.

Zadanie domowe 10.5

Rozbuduj program z ćwiczenia 10.4 tak, aby ilustrował na ekranie aktualny poziom światła. Np.: w formie paska postępu lub skali.

Podsumowanie

W tej części napisaliśmy własną bibliotekę obsługi wyświetlacza graficznego. Poznaliśmy w sumie dwa układy peryferyjne podłączane za pomocą interfejsu SPI: I/O expander oraz wyświetlacz LCD. Podłączanie innych układów przebiega podobnie - samo SPI działa właściwie zawsze tak samo. Natomiast jakie są protokoły komunikacyjne trzeba doczytać w dokumentacji układów.

W kolejnym odcinku kursu omówimy kolejny interfejs - I2C. Dzięki jego pomocy będzie mogli użyć zewnętrznej pamięci EEPROM oraz w wersji bardziej zaawansowanej będziemy odczytywać dane z akcelerometru oraz magnetometru!

Nawigacja kursu

Autor kursu: Piotr (Elvis) Bugalski
Korekta językowa: Paweł Szwagierek

Redakcja: Damian (Treker) Szymański

Załączniki

LCD-Nokia5110_datasheet (pdf, 155 KB)

kurs, kursSTM32, lcd, nokia, SPI, stm32, wyświetlacz

Trwa ładowanie komentarzy...