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.
Uwaga! Ten kurs został zarchiwizowany. Sprawdź najnowszy kurs STM32 »
Wyświetlacz graficzny
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. Wyświetlacz ten wchodzi w skład rozszerzonych zestawów dedykowanych do tego kursu.
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 100 ns. 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 (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, czyli 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.
Maksymalna częstotliwość pracy tego interfejsu SPI, to 4 MHz.
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ę!
Podłączenie wyświetlacza do 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!
Masz już zestaw? Zarejestruj go wykorzystując dołączony do niego kod. Szczegóły »
Kompletny schemat montażowy wygląda następująco:
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):
C
1
2
3
#define LCD_DC GPIO_PIN_1
#define LCD_CE GPIO_PIN_2
#define LCD_RST GPIO_PIN_3
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ę:
C
1
2
3
4
5
voidlcd_reset()
{
HAL_GPIO_WritePin(GPIOC,LCD_RST,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC,LCD_RST,GPIO_PIN_SET);
}
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.
C
1
2
3
4
5
6
7
8
9
lcd_reset();
lcd_cmd(0x21);
lcd_cmd(0x14);
lcd_cmd(0x80|0x2f);//Ustawienie kontrastu
lcd_cmd(0x20);
lcd_cmd(0x0c);
lcd_data(logo_mini_mono,sizeof(logo_mini_mono));
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.
Efekt działania programu w praktyce wygląda następująco:
STM32 i wyświetlacz graficzny.
W razie problemów z kontrastem (cały jasny lub ciemny ekran) należy eksperymentować z ustawieniami kontrastu, za który odpowiada pogrubiony fragment poniższej linijki:
lcd_cmd(0x80 | 0x2f); //Ustawienie kontrastu
W celu ustawienia kontrastu nie należy edytować wartości 0x80!
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.
Tworzą więc pewnego rodzaju bibliotekę potrzebnych nam funkcji.
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 bitmap.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):
Następnie treść wypełniamy w poniższy sposób:
C
1
2
3
4
5
6
7
8
#ifndef BITMAP_H_
#define BITMAP_H_
#include <stdint.h>
externconstuint8_t forbot_logo[];
#endif /* BITMAP_H_ */
Dyrektywy #ifndef służą unikaniu wielokrotnego włączania tego samego pliku nagłówkowego. Natomiast, to co ważne, to deklaracja tablicy forbot_logo(). Jest opatrzona słówkiem kluczowym extern, żeby zasygnalizować iż jest to tylko deklaracja - sama tablica zdefiniowana jest w innym pliku.
Słówko extern jest niezbędne przy deklarowaniu zmiennych. W przypadku procedur, szczególnie osoby programujące w C++ mogą je omijać.
Definicję tablicy z logo forbota umieszczamy w pliku bitmap.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 bitmap, interesuje nas tylko plik nagłówkowy bitmap.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 bitmap.h niż zastanawiać się która funkcja do czego służy.
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 o licencji - Beerware, którą znajdziecie w pliku z kodem ;)
Plik font.h:
C
1
2
3
4
5
6
7
8
#ifndef FONT_H_
#define FONT_H_
#include <stdint.h>
externconstuint8_t font_ASCII[][5];
#endif /* FONT_H_ */
Implementacja w pliku font.c:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/*
7-17-2011
Spark Fun Electronics 2011
Nathan Seidle
This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license).
This code writes a series of images and text to the Nokia 5110 84x48 graphic LCD:
http://www.sparkfun.com/products/10168
Do not drive the backlight with 5V. It will smoke. However, the backlight on the LCD seems to be
happy with direct drive from the 3.3V regulator.
Although the PCD8544 controller datasheet recommends 3.3V, the graphic Nokia 5110 LCD can run at 3.3V or 5V.
No resistors needed on the signal lines.
You will need 5 signal lines to connect to the LCD, 3.3 or 5V for power, 3.3V for LED backlight, and 1 for ground.
*/
#include "font.h"
//This table contains the hex values that represent pixels
//for a font that is 5 pixels wide and 8 pixels high
constuint8_t font_ASCII[][5]={
{0x00,0x00,0x00,0x00,0x00}// 20
,{0x00,0x00,0x5f,0x00,0x00}// 21 !
,{0x00,0x07,0x00,0x07,0x00}// 22 "
,{0x14,0x7f,0x14,0x7f,0x14}// 23 #
,{0x24,0x2a,0x7f,0x2a,0x12}// 24 $
,{0x23,0x13,0x08,0x64,0x62}// 25 %
,{0x36,0x49,0x55,0x22,0x50}// 26 &
,{0x00,0x05,0x03,0x00,0x00}// 27 '
,{0x00,0x1c,0x22,0x41,0x00}// 28 (
,{0x00,0x41,0x22,0x1c,0x00}// 29 )
,{0x14,0x08,0x3e,0x08,0x14}// 2a *
,{0x08,0x08,0x3e,0x08,0x08}// 2b +
,{0x00,0x50,0x30,0x00,0x00}// 2c ,
,{0x08,0x08,0x08,0x08,0x08}// 2d -
,{0x00,0x60,0x60,0x00,0x00}// 2e .
,{0x20,0x10,0x08,0x04,0x02}// 2f /
,{0x3e,0x51,0x49,0x45,0x3e}// 30 0
,{0x00,0x42,0x7f,0x40,0x00}// 31 1
,{0x42,0x61,0x51,0x49,0x46}// 32 2
,{0x21,0x41,0x45,0x4b,0x31}// 33 3
,{0x18,0x14,0x12,0x7f,0x10}// 34 4
,{0x27,0x45,0x45,0x45,0x39}// 35 5
,{0x3c,0x4a,0x49,0x49,0x30}// 36 6
,{0x01,0x71,0x09,0x05,0x03}// 37 7
,{0x36,0x49,0x49,0x49,0x36}// 38 8
,{0x06,0x49,0x49,0x29,0x1e}// 39 9
,{0x00,0x36,0x36,0x00,0x00}// 3a :
,{0x00,0x56,0x36,0x00,0x00}// 3b ;
,{0x08,0x14,0x22,0x41,0x00}// 3c <
,{0x14,0x14,0x14,0x14,0x14}// 3d =
,{0x00,0x41,0x22,0x14,0x08}// 3e >
,{0x02,0x01,0x51,0x09,0x06}// 3f ?
,{0x32,0x49,0x79,0x41,0x3e}// 40 @
,{0x7e,0x11,0x11,0x11,0x7e}// 41 A
,{0x7f,0x49,0x49,0x49,0x36}// 42 B
,{0x3e,0x41,0x41,0x41,0x22}// 43 C
,{0x7f,0x41,0x41,0x22,0x1c}// 44 D
,{0x7f,0x49,0x49,0x49,0x41}// 45 E
,{0x7f,0x09,0x09,0x09,0x01}// 46 F
,{0x3e,0x41,0x49,0x49,0x7a}// 47 G
,{0x7f,0x08,0x08,0x08,0x7f}// 48 H
,{0x00,0x41,0x7f,0x41,0x00}// 49 I
,{0x20,0x40,0x41,0x3f,0x01}// 4a J
,{0x7f,0x08,0x14,0x22,0x41}// 4b K
,{0x7f,0x40,0x40,0x40,0x40}// 4c L
,{0x7f,0x02,0x0c,0x02,0x7f}// 4d M
,{0x7f,0x04,0x08,0x10,0x7f}// 4e N
,{0x3e,0x41,0x41,0x41,0x3e}// 4f O
,{0x7f,0x09,0x09,0x09,0x06}// 50 P
,{0x3e,0x41,0x51,0x21,0x5e}// 51 Q
,{0x7f,0x09,0x19,0x29,0x46}// 52 R
,{0x46,0x49,0x49,0x49,0x31}// 53 S
,{0x01,0x01,0x7f,0x01,0x01}// 54 T
,{0x3f,0x40,0x40,0x40,0x3f}// 55 U
,{0x1f,0x20,0x40,0x20,0x1f}// 56 V
,{0x3f,0x40,0x38,0x40,0x3f}// 57 W
,{0x63,0x14,0x08,0x14,0x63}// 58 X
,{0x07,0x08,0x70,0x08,0x07}// 59 Y
,{0x61,0x51,0x49,0x45,0x43}// 5a Z
,{0x00,0x7f,0x41,0x41,0x00}// 5b [
,{0x02,0x04,0x08,0x10,0x20}// 5c
,{0x00,0x41,0x41,0x7f,0x00}// 5d ]
,{0x04,0x02,0x01,0x02,0x04}// 5e ^
,{0x40,0x40,0x40,0x40,0x40}// 5f _
,{0x00,0x01,0x02,0x04,0x00}// 60 `
,{0x20,0x54,0x54,0x54,0x78}// 61 a
,{0x7f,0x48,0x44,0x44,0x38}// 62 b
,{0x38,0x44,0x44,0x44,0x20}// 63 c
,{0x38,0x44,0x44,0x48,0x7f}// 64 d
,{0x38,0x54,0x54,0x54,0x18}// 65 e
,{0x08,0x7e,0x09,0x01,0x02}// 66 f
,{0x0c,0x52,0x52,0x52,0x3e}// 67 g
,{0x7f,0x08,0x04,0x04,0x78}// 68 h
,{0x00,0x44,0x7d,0x40,0x00}// 69 i
,{0x20,0x40,0x44,0x3d,0x00}// 6a j
,{0x7f,0x10,0x28,0x44,0x00}// 6b k
,{0x00,0x41,0x7f,0x40,0x00}// 6c l
,{0x7c,0x04,0x18,0x04,0x78}// 6d m
,{0x7c,0x08,0x04,0x04,0x78}// 6e n
,{0x38,0x44,0x44,0x44,0x38}// 6f o
,{0x7c,0x14,0x14,0x14,0x08}// 70 p
,{0x08,0x14,0x14,0x18,0x7c}// 71 q
,{0x7c,0x08,0x04,0x04,0x08}// 72 r
,{0x48,0x54,0x54,0x54,0x20}// 73 s
,{0x04,0x3f,0x44,0x40,0x20}// 74 t
,{0x3c,0x40,0x40,0x20,0x7c}// 75 u
,{0x1c,0x20,0x40,0x20,0x1c}// 76 v
,{0x3c,0x40,0x30,0x40,0x3c}// 77 w
,{0x44,0x28,0x10,0x28,0x44}// 78 x
,{0x0c,0x50,0x50,0x50,0x3c}// 79 y
,{0x44,0x64,0x54,0x4c,0x44}// 7a z
,{0x00,0x08,0x36,0x41,0x00}// 7b {
,{0x00,0x00,0x7f,0x00,0x00}// 7c |
,{0x00,0x41,0x36,0x08,0x00}// 7d }
,{0x10,0x08,0x08,0x10,0x08}// 7e ~
,{0x78,0x46,0x41,0x46,0x78}// 7f DEL
};
Mając już wszystkie moduły pomocnicze możemy napisać naszą bibliotekę graficzną. Plik nagłówkowy lcd.h wygląda następująco:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef __LCD__
#define __LCD__
#include <stdint.h>
#include "stm32f1xx.h"
#define LCD_DC GPIO_PIN_1
#define LCD_CE GPIO_PIN_2
#define LCD_RST GPIO_PIN_3
externSPI_HandleTypeDef spi;
voidlcd_setup(void);
voidlcd_clear(void);
voidlcd_draw_bitmap(constuint8_t*data);
voidlcd_draw_text(introw,intcol,constchar*text);
voidlcd_copy(void);
#endif // __LCD__
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.
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:
Efekt działania powyższego programu widoczny jest na poniższym zdjęciu:
Wyświetlanie napisów na wyświetlaczu przez SPI na STM32.
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):
Działanie programu widoczne jest na poniższej animacji (słabo jakość wynika z kompresji obrazu):
Prosta animacja na wyświetlaczu.
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 tej części napisaliśmy własną bibliotekę obsługi wyświetlacza graficznego. Łącznie poznaliśmy dwa układy peryferyjne podłączane za pomocą interfejsu SPI: I/O ekspander oraz wyświetlacz LCD. Podłączanie innych układów przebiega podobnie - samo SPI działa właściwie zawsze tak samo.
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!
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY na bazie Arduino i Raspberry Pi.
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY z Arduino i RPi.
Trwa ładowanie komentarzy...