W tej części zajmiemy się tworzeniem mini biblioteki do obsługi wyświetlacza o wielkości 2×16 pracującego w trybie 4 bitowym, bez odczytu flagi zajętości. Jeśli nie czytałeś poprzedniego artykułu to zdecydowanie warto zacząć od niego, ponieważ zawarłem tam wiele istotnych informacji, do których będę się tutaj odnosił.
Kod z tego przykładu powinieneś bez większego problemu przenieść na dowolny mikrokontroler, jakiego używasz. Ja wykorzystam tutaj biblioteki SPL dla mikrokontrolerów STM32. Całkowite kody źródłowe dla mikrokontrolera STM32F103 znajdziesz na moim githubie.
Połączenia
Sterownik i podświetlenie wyświetlacza, w zależności od modelu, zasilamy napięciem 5v lub 3.3v. Dokładniej opisałem to w punkcie 5v vs 3.3v. Przy transmisji jednokierunkowej będziemy wyłącznie zapisywać dane do sterownika, więc pin RW należy na stałe podłączyć do masy.
Wyświetlacz może pracować w dwóch trybach: 8 lub 4-bitowym. Zawsze korzystam z trybu 4-bitowego i ten sposób opiszę. Implementacja dla trybu 8-bitowego jest prostsza, więc w razie potrzeby, wszelkie modyfikacje nie powinny stanowić dużego problemu. W trybie 4-bitowym wykorzystuje się tylko 4 linie danych (D4-D7) zamiast 8, co jest jego ogromną zaletą. Możemy je podłączyć do dowolnych niezajętych GPIO mikrokontrolera, a pozostałe (D0-D3) pozostają niepodłączone. W takim trybie najpierw wysyłamy starszą (bardziej znaczącą), a potem młodszą połowę bajta. Zakończenie transmisji zachodzi dopiero po wysłaniu obu części. Wynikające z tego spowolnienie jest mało znaczące. Zostały jeszcze piny E i RS, które podobnie jak D4-D7 podłączamy do dowolnych niezajętych GPIO.
Makra do wyboru pinów i portów
Aby ułatwić sobie konfigurację wyświetlacza w różnych projektach, warto na początku utworzyć makra dla pinów i portów mikrokontrolera, do których połączone będą poszczególne piny wyświetlacza. Dzięki temu, jeśli będziemy chcieli zmienić któreś połączenie wystarczy, że wprowadzimy zmianę tylko w tym jednym miejscu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#define LCD_E_PIN GPIO_Pin_7 #define LCD_E_PORT GPIOC #define LCD_RS_PIN GPIO_Pin_9 #define LCD_RS_PORT GPIOA #define LCD_D4_PIN GPIO_Pin_13 #define LCD_D4_PORT GPIOB #define LCD_D5_PIN GPIO_Pin_2 #define LCD_D5_PORT GPIOD #define LCD_D6_PIN GPIO_Pin_2 #define LCD_D6_PORT GPIOC #define LCD_D7_PIN GPIO_Pin_3 #define LCD_D7_PORT GPIOC |
Aby późniejszy kod był bardziej przejrzysty można też pokusić się o stworzenie makr dla funkcji ustawiających stan niski lub wysoki na pinach E i RS.
1 2 3 4 |
#define LCD_E_HIGH GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_SET) #define LCD_E_LOW GPIO_WriteBit(LCD_E_PORT, LCD_E_PIN, Bit_RESET) #define LCD_RS_LOW GPIO_WriteBit(LCD_RS_PORT, LCD_RS_PIN, Bit_RESET) #define LCD_RS_HIGH GPIO_WriteBit(LCD_RS_PORT, LCD_RS_PIN, Bit_SET) |
Makra komend
Poniżej przedstawiam tabelę komend prosto z dokumentacji sterownika HD44780:
Będziemy z nich korzystali, więc aby nie musieć więcej razy zaglądać do tej tabeli także warto stworzyć makra dla poszczególnych komend:
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 |
#define LCD_CLEAR 0x01 #define LCD_HOME 0x02 #define LCDC_ENTRY_MODE 0x04 #define LCD_EM_SHIFT_CURSOR 0x00 #define LCD_EM_SHIFT_DISPLAY 0x01 #define LCD_EM_LEFT 0x00 #define LCD_EM_RIGHT 0x02 #define LCD_ONOFF 0x08 #define LCD_DISP_ON 0x04 #define LCD_CURSOR_ON 0x02 #define LCDC_CURSOR_OFF 0x00 #define LCDC_BLINK_ON 0x01 #define LCDC_BLINK_OFF 0x00 #define LCD_SHIFT 0x10 #define LCDC_SHIFT_DISP 0x08 #define LCDC_SHIFT_CURSOR 0x00 #define LCDC_SHIFT_RIGHT 0x04 #define LCDC_SHIFT_LEFT 0x00 #define LCD_FUNC 0x20 #define LCD_8_BIT 0x10 #define LCD_4_BIT 0x00 #define LCDC_TWO_LINE 0x08 #define LCDC_FONT_5x10 0x04 #define LCDC_FONT_5x7 0x00 #define LCDC_SET_CGRAM 0x40 #define LCDC_SET_DDRAM 0x80 |
No i dodajmy jeszcze makra dla adresów pamięci DDRAM odpowiadające pierwszym polom wyświetlacza z każdego wiersza. Adresy te były przedstawione w pierwszym artykule i dla wyświetlacza 2×16 miały następujące wartości:
1 2 |
#define LCD_LINE1 0x00 #define LCD_LINE2 0x40 |
W zasadzie to najgorsze za nami. Pozostały do napisania funkcje potrzebne do obsługi wyświetlacza, ale zapewniam, że są one banalnie proste.
Wysyłanie danych
Jak już wspominałem wykorzystamy tryb 4-bitowy i wysyłanie bajta danych będzie zachodziło w dwóch turach po 4 bity. Wysyłanie sprowadza się do ustawienia odpowiednich stanów na poszczególnych liniach danych DB4-DB7. Stany na tych liniach oczywiście odpowiadają wartościom bitów połowy przesyłanego bajta. Informację o tym, że już ustawiliśmy stany na liniach danych przekazujemy dla kontrolera LCD poprzez zbocze opadające na pinie E. Dlatego na początku funkcji ustawimy stan wysoki na tej linii, a na końcu zmienimy na niski:
1 2 3 4 5 6 7 8 9 |
void lcd_sendHalf(uint8_t data) { LCD_E_HIGH; GPIO_WriteBit(LCD_D4_PORT, LCD_D4_PIN, (data & 0x01)); GPIO_WriteBit(LCD_D5_PORT, LCD_D5_PIN, (data & 0x02)); GPIO_WriteBit(LCD_D6_PORT, LCD_D6_PIN, (data & 0x04)); GPIO_WriteBit(LCD_D7_PORT, LCD_D7_PIN, (data & 0x08)); LCD_E_LOW; } |
Funkcja GPIO_WriteBit() jest funkcją z biblioteki SPL i służy do ustawiania stanów logicznych na określonych pinach. Jako argument przyjmuje numer portu i pinu oraz wartość, jaka ma zostać przypisana: 1 odpowiada stanowi wysokiemu, 0 niskiemu. Stosując kolejno maski 0x01, 0x02, 0x04, 0x08 uzyskujemy odpowiedni stan na odpowiednich liniach danych, odpowiadający bitom przesyłanej danej.
Gdy mamy już funkcję do przesłania połowy bajta, możemy teraz napisać taką, która umożliwi nam przesłanie całości:
1 2 3 4 5 6 7 |
void lcd_write_byte(uint8_t data) { lcd_sendHalf(data >> 4); lcd_sendHalf(data); delay_us(60); } |
Jak już wspominałem, na początku przesyłamy starszą, a potem młodszą połowę. Funkcja lcd_sendHalf() została napisana tak, że przesyła młodszą połowę bajta, który jej przekazujemy jako argument, dlatego przy pierwszym wywołaniu funkcji lcd_write_byte() dokonujemy przesunięcia bitowego w prawo o 4 miejsca, aby na pozycjach młodszego półbajtu znajdowały się wartości odpowiadające starszemu półbajtowi przesyłanej wartości. Jeśli nie do końca to widzisz, to „wywołaj” sobie na kartce funkcję lcd_write_byte() z jakimś argumentem i przeanalizuj wszystko krok po kroku. Na końcu, ze względu na to, że nie możemy odczytać stanu Busy Flag, bo korzystamy z transmisji jednokierunkowej musimy odczekać chwilę, w czasie, której sterownik „przetrawi” poprzednią porcję informacji. Ja ustawiłem 60µs, ale gdyby pojawiały się jakieś problemy to warto tę wartość zwiększyć.
Jak wiesz, przesłany bajt danych może być interpretowany przez sterownik albo jako komenda, albo znak do wyświetlenia. Wszystko zależy od stanu na linii RS. Napiszmy więc dwie funkcję, które nam to umożliwią:
1 2 3 4 5 |
void lcd_write_cmd(uint8_t cmd) { LCD_RS_LOW; lcd_write_byte(cmd); } |
1 2 3 4 5 |
void lcd_char(char data) { LCD_RS_HIGH; lcd_write_byte(data); } |
Nie ma w nich nic co wymagałoby dodatkowego opisu. Dodam tylko, że na wykonanie poszczególnych komend sterownik potrzebuje różną ilość czasu. Poszczególne czasy znajdziesz w tabeli komend z pierwszego artykułu.
Inicjalizacja
Mając te funkcje możemy już zrobić wszystko. Przejdźmy więc do inicjalizacji LCD. Cały proces jest przedstawiony w nocie katalogowej i dla trybu 4-bitowego wygląda następująco:
Wystarczy, że wszystko wykonamy zgodnie z powyższą instrukcją. No to do dzieła 🙂
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 |
void LCD_Init(void) { // Oczywiście na początku musimy zainicjowac piny mikrokontrolera. // Stworzyłem w tym celu oddzielną funkcję lcd_gpio_init(). // Jej ciało przedstawię w kolejnym punkcie. lcd_gpio_init(); delay_ms(15); LCD_E_LOW; LCD_RS_LOW; lcd_sendHalf(0x03); delay_ms(4); delay_us(100); lcd_sendHalf(0x03); delay_us(100); lcd_sendHalf(0x03); delay_us(100); lcd_sendHalf(0x02); delay_us(100); // Już jesteśmy w trybie 4-bitowym. Tutaj dokonujemy ustawień wyświetlacza: lcd_write_cmd( LCD_FUNC | LCD_4_BIT | LCDC_TWO_LINE | LCDC_FONT_5x7); lcd_write_cmd( LCD_ONOFF | LCD_DISP_ON ); lcd_write_cmd( LCD_CLEAR ); delay_ms(5); lcd_write_cmd( LCDC_ENTRY_MODE | LCD_EM_SHIFT_CURSOR | LCD_EM_RIGHT ); } |
Jak już wiesz z komentarza w kodzie, musimy jeszcze napisać funkcję, która będzie odpowiadała za inicjalizacje GPIO mikrokontrolera – musimy ustawić je jako wyjścia, a w przypadku STM32 włączyć jeszcze taktowanie portów.
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 |
void lcd_gpio_init( void) { // Włączamy taktowanie portów: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); // Ustawiamy piny jako wyjścia GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = LCD_D4_PIN; GPIO_Init(LCD_D4_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = LCD_D5_PIN; GPIO_Init(LCD_D5_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = LCD_D6_PIN; GPIO_Init(LCD_D6_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = LCD_D7_PIN; GPIO_Init(LCD_D7_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = LCD_RS_PIN; GPIO_Init(LCD_RS_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = LCD_E_PIN; GPIO_Init(LCD_E_PORT, &GPIO_InitStructure); } |
Ustawianie kursora
Napiszmy jeszcze funkcję, która umożliwi nam ustawienie kursora w dowolnej pozycji na wyświetlaczu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void lcd_locate(uint8_t x, uint8_t y) { switch(y) { case 0: lcd_write_cmd( LCDC_SET_DDRAM | (LCD_LINE1 + x) ); break; case 1: lcd_write_cmd( LCDC_SET_DDRAM | (LCD_LINE2 + x) ); break; } } |
Jak widać funkcja przyjmuje dwa argumenty: x i y. Są to odpowiednio numery kolumny i wiersza wyświetlacza indeksowane od zera, w których chcemy ustawić kursor i zaczynać wpisywać znaki. Jak już wiesz, poszczególnym polom wyświetlacza odpowiadają określone adresy pamięci DDRAM i to właśnie je będziemy ustawiać. Przypomnę, że dla wyświetlacza 2×16 są one przyporządkowane w następujący sposób:
Komenda LCDC_SET_DDRAM służy do ustawiania adresu pamięci DDRAM. W instrukcji switch, w zależności od wybranego wiersza, ustawiamy odpowiedni adres. W przypadku wyświetlacza np. 4 wierszowego, należy dopisać jeszcze case 2 i case 3 oraz stworzyć makra LCD_LINE3 i LCD_LINE4.
Podsumowanie
No i dochodzimy do końca tej części. Mam nadzieję, że po przeczytaniu wszystko stało się w miarę jasne 🙂 Mamy podstawowe funkcje, na podstawie których możemy już bez problemu tworzyć kolejne: np. funkcja do wyświetlania stringów:
1 2 3 4 5 |
void lcd_str(char *text) { while(*text) lcd_char(*text++); } |
Poniżej przedstawiam jeszcze główną funkcję programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include "stm32f10x.h" #include "../LCD_HD44780/lcd.h" #include "common.h" #include "config.h" int main(void) { TIMERS_Config(); LCD_Init(); lcd_str_XY(1,0,"EmbeddedDev.pl"); lcd_locate(7,1); lcd_char(':'); lcd_char(')'); while(1) { } } |
Jak widać, na początku musimy pamiętać jedynie o inicjalizacji LCD, a potem już dowolnie korzystamy z opisywanych tu funkcji ? Ja inicjalizuję tu także timery na potrzeby funkcji delay_ms() i delay_us(). Opóźnienia można zrealizować na wiele sposobów, ale gdybyś miał z tym problem to możesz skorzystać z mojego kodu – całkowite kody źródłowe z tego projektu wraz z ciałami funkcji opóźniających znajdziesz na moim githubie
Jak już wspominałem, ten sposób jest trochę prostszy, ale niestety spowalnia komunikację, więc jeśli zależy Ci na szybkości to warto korzystać z pinu RW i odczytu busy flag, co opisuję w kolejnym artykule: