Klasyczne wyświetlacze alfanumeryczne, LCD są nadal jednym z najczęściej używanych peryferiów przez hobbystów zajmujących się mikrokontrolerami. Są świetne do wyświetlania prostych danych, debugowania, a ich uruchomienie jest bardzo proste. Zwłaszcza, że sterownik ma już zdefiniowane w pamięci znaki. Do tego wyglądają bardzo urokliwie, tak „old school’owo”.
Wyświetlacze możemy znaleźć w różnych wersjach kolorystycznych i wymiarowych, ja posłużę się tutaj klasycznym 2×16 (2 wiersze, 16 kolumn). Typ wyświetlacza nie ma większego znaczenia jeśli posiada on sterownik HD44780 (lub jest z nim kompatybilny).
Obsługiwać taki wyświetlacz możemy przy użyciu ośmiu, lub czterech linii danych. Istnieje też możliwość sterowania wyświetlaczem poprzez magistralę I2C z pomocą expander’a opartego na module PCF8574. Ja będę pisał kod obsługi dla transmisji 4-bitowej, oraz I2C.
LCD HD44780
Powyżej znajduje się opis wyprowadzeń klasycznego wyświetlacza LCD 2×16. Pierwsze dwa to zasilanie całego modułu, trzeci odpowiada za dostosowanie kontrastu, następne trzy piny służą do obsługi transmisji. Kolejne osiem wyprowadzeń służy do komunikacji -My, w trybie 4-bitowej komunikacji skorzystamy tylko z czterech ostatnich (4-7). A ostatnie dwa wyprowadzenia to anoda i katoda podświetlenia LED.
Do wyświetlacza możemy przesyłać komendy, oraz dane definiujące jaki znak chcemy wyświetlić. Do wyboru tego, co chcemy przesłać służy pin RS (Register Select), który zależnie czy jest w stanie niskim lub wysokim, ustawia odpowiednio tryb przesyłania komend/danych.
Pin RW (Read/Write) jak sama nazwa wskazuje, służy do wybrania trybu pracy modułu – odczyt/zapis. Wyprowadzenie te, można zewrzeć do GND jeżeli nie chcemy korzystać z trybu odczytu. Tryb odczytu rzadko kiedy się stosuje, w zasadzie to jest on tylko użyteczny do odczytywania flagi zajętości modułu (Busy Flag) by trochę przyspieszyć transmisję. Weduług mnie, mało ma to sensu zważając na to jak mało danych wysyłamy do tego modułu. Dlatego, raczej nie będę pokazywał obsługi trybu odczytu.
Pin Enable (EN), używamy do „zatrzaskiwania” wysyłanych danych.
Tyle teorii powinno starczyć. Obsługa tych wyświetlaczy jest na tyle prosta, że raczej nie będzie problemu ze zrozumieniem samego programu.
LCD AVR – biblioteka
Kod dostępny w serwisie github, dla:
Transmisji 4-bitowej: https://github.com/mcinm/AVR/blob/main/LCD_4bit
Transmisji I2C: https://github.com/mcinm/AVR/tree/main/LCD_i2c
Plik nagłówkowy
W pliku nagłówkowym trzeba zdefiniować następujące zmienne..
#define LCD_DATA_PORT PORTD #define LCD_DATA_DDR DDRD #define LCD_D7 6 #define LCD_D6 3 #define LCD_D5 4 #define LCD_D4 5 #define LCD_RSPORT PORTB #define LCD_RSDDR DDRB #define LCD_RS 4 #define LCD_RWPORT PORTB #define LCD_RWDDR DDRB #define LCD_RW 1 #define LCD_ENPORT PORTB #define LCD_ENDDR DDRB #define LCD_EN 3 // RS PIN #define RS_HIGH LCD_RSPORT |= (1<<LCD_RS) #define RS_LOW LCD_RSPORT &= ~(1<<LCD_RS) // EN PIN #define EN_HIGH LCD_ENPORT |= (1<<LCD_EN) #define EN_LOW LCD_ENPORT &= ~(1<<LCD_EN)
A także komendy, których pozwoliłem sobie tutaj nie przekopiowywać. Dostępne są one w kodzie umieszczonym na githubie. Powyżej definiujemy sobie jakie piny odpowiadają za transmisję. Ja nieco to uprościłem i z góry zakładam, że wszystkie piny danych (D) będę podpinał do tego samego portu mikrokontrolera. To nie jest jednak konieczność.
Na samym końcu mamy makra zmieniające stany na pinach sterujących. Pinu RW nie obsługuję, ponieważ podpiąłem go do GND.
Wysyłanie komend i danych
static inline void lcd_send_4bits(unsigned char data) { if (data&(1<<0)) LCD_data_PORT |= (1<<LCD_D4); else LCD_DATA_PORT &= ~(1<<LCD_D4); if (data&(1<<1)) LCD_DATA_PORT |= (1<<LCD_D5); else LCD_DATA_PORT &= ~(1<<LCD_D5); if (data&(1<<2)) LCD_DATA_PORT |= (1<<LCD_D6); else LCD_DATA_PORT &= ~(1<<LCD_D6); if (data&(1<<3)) LCD_DATA_PORT |= (1<<LCD_D7); else LCD_DATA_PORT &= ~(1<<LCD_D7); } // Send byte - just send 4 bytes, 2 times void lcd_byte(unsigned char data) { EN_HIGH; lcd_send_4bits(data>>4); EN_LOW; EN_HIGH; lcd_send_4bits(data); EN_LOW; _delay_us(120); } // Send command void lcd_cmd(unsigned char cmd) { RS_LOW; lcd_byte(cmd); } // Send data void lcd_data(unsigned char data) { RS_HIGH; lcd_byte(data); }
Powyżej pokazałem jak wyglądają cztery podstawowe funkcje; wysyłanie połowy bajtu (korzystamy z transmisji 4-bitowej, więc bajt musimy przesłać jako dwie „połówki”), wysyłanie całego bajtu, i na końcu z użyciem funkcji wysyłającej bajt – dwie funkcje do wysyłania komend, oraz danych.
Tak jak wspominałem wcześniej, w przypadku wysyłania danych i komend jedyne co się zmienia to stan linii RS (Register Select).
Dzięki tym funkcjom możemy już w prosty sposób rozmawiać z naszym wyświetlaczem.
Inicjalizacja wyświetlacza
Na samym początku wyświetlacz trzeba jeszcze zainicjalizować. Sekwencje inicjalizacyjną można znaleźć w notach katalogowych wyświetlaczy/sterownika. Ja napisałem to w sposób następujący:
static inline void lcd_init_pins(void) { LCD_DATA_DDR |= (1<<LCD_D7) | (1<<LCD_D6) | (1<<LCD_D5) | (1<<LCD_D4); LCD_RSDDR |= (1<<LCD_RS); LCD_ENDDR |= (1<<LCD_EN); } void lcd_clear(void) { lcd_cmd(0x01); _delay_ms(2); // Jeżeli występują problemy, zwiększ wartość o 1-2 ms } // Init dispay void lcd_init(void) { lcd_init_pins(); RS_LOW; EN_LOW; _delay_ms(15); EN_HIGH; lcd_send_4bits(0x03); EN_LOW; _delay_ms(4.1); EN_HIGH; lcd_send_4bits(0x03) EN_LOW; _delay_us(100); EN_HIGH; lcd_send_4bits(0x03); EN_LOW; _delay_us(100); EN_HIGH; lcd_send_4bits(0x02); EN_LOW; _delay_us(100); lcd_cmd( LCDC_FUNC|LCDC_FUNC4B|LCDC_FUNC2L|LCDC_FUNC5x7 ); lcd_cmd( LCDC_ONOFF|LCDC_CURSOROFF ); lcd_cmd( LCDC_ONOFF|LCDC_DISPLAYON ); lcd_cmd( LCDC_ENTRY|LCDC_ENTRYR ); lcd_clear(); }
Na samym początku napisałem jeszcze funkcję do inicjalizacji pinów odpowiedzialnych za transmisję, by nie musieć wpisywać tego ręcznie. Funkcja ta ma dopisek „static inline” co powinno sprawić, że kompilator w miejscu jej wywołania po prostu umieści kod w niej zawarty, zamiast jej faktycznie wywoływać.
Kolejno funkcja czyszcząca wyświetlacz, która zawiera tylko wykonanie komendy. Utworzyłem ją tak wcześnie, gdyż wywołuję ją podczas inicjalizacji, bo po jej wykonaniu może coś pozostać na wyświetlaczu.
I na sam koniec nasza inicjalizacja. Nie będę jej dokładnie omawiać, można samemu ją prześledzić z otwartą dokumentacją techniczną układu HD44780. W jej trakcie, do wysyłania, niektórych komend użyłem funkcji wysyłającej 4 bity. To z tego powodu, że te wartości spokojnie zmieszczą się w tych czterech bitach i w ten sposób nieco przyspieszymy wykonanie całej funkcji.
Istotne mogą być cztery ostatnie operacje wysłania komendy, w nich definiujemy podstawową pracę wyświetlacza. Możemy zdefiniować ile linii posiada wyświetlacz, czy kursor ma być włączony itp. Te makrodefinicję znajdują się oczywiście w pliku nagłówkowym.
Wysyłanie znaków
Skoro do czynienia mamy z wyświetlaczem alfanumerycznym, to istotne dla nas najbardziej będzie wyświetlanie na nim znaków. To jest bardzo proste, ponieważ sterownik ma już w pamięci, wgrane podstawowe znaki.
W powyższej grafice możesz zobaczyć znaki jakie są zdefiniowane w sterowniku wyświetlacza. Spójrzmy na przykład na literę 'A’. Jej wartość binarna wyczytana z tabelki to 01000001, co daje 65 w systemie dziesiętnym, a więc taką samą jaką litera 'A’ ma w systemie ASCII.
Jeżeli więc, będziemy chcieli wyświetlić literę 'A’ na wyświetlaczu musimy do niego wysłać jej reprezentację w formie numery ASCII. Wyglądać będzie to następująco:
lcd_data('A');
I to wszystko, ponieważ funkcja „lcd_data” literę 'A’ wyślę jako unsigned char, czyli właśnie 65.
Poniżej zapisałem funkcje do lokalizowania kursora, wysyłania ciągu znakowego, oraz liczby.
// Send string void lcd_str(char * str) { while (*str) lcd_data(*str++); } // Send digit/number void lcd_int(signed short num) { char buff[4]; itoa(num, buff, 10); lcd_str(buff); } // Set cursor void lcd_cursor(unsigned char y, unsigned char x) { unsigned char xy = x+y * 0x40; lcd_cmd((xy | 0x80)); }
Funkcja „lcd_int” działa w taki sposób, że przekazujemy jej liczbę i z pomocą funkcji „itoa” zamienia ją w ciąg znakowy. Jeżeli będziesz potrzebował wyświetlać liczby składające się z większej ilości liczb, zamień wartość bufora przeznaczonego na przechowywanie ciągu.
Poniższy kod wyświetla teskt „MCINM 54”:
lcd_cursor(0,0); lcd_str("MCINM 54");
Oczywiście cyfry, można umieścić (tak jak ja to zrobiłem), po prostu w stringu. Funkcja „lcd_int” jest przydatna jeżeli chcemy wyświetlić wartość liczbową, którą przechowujemy w jakiejś zmiennej.
Definiowanie własnych znaków
Zdefiniowane, z góry znaki mogą nas nieco ograniczać, szczególnie jeśli chcemy, przykładowo używać typowo polskich znaków jak 'ą’ itp.. Możemy sobie z tym poradzić samodzielnie definiując w pamięci sterownika 8 własnych znaków. Osiem, ponieważ tyle wolnego miejsca jest pozostawione w sterowniku dla programisty. Poniżej kod…
// Define own characters void lcd_define_character(unsigned char addr, const unsigned char *tab_pointer) { lcd_cmd(LCDC_SET_CGRAM | ((addr & 0x7) << 3)); for (unsigned char i=0; i<8; i++) lcd_data(&tab_pointer[i]); // Load from RAM //for (unsigned char i=0; i<8; i++) lcd_data(pgm_read_byte(&tab_pointer[i])); // Load from flash lcd_cmd(LCDC_SET_DDRAM); }
Funkcja ta, może wyglądać dosyć skomplikowanie jednak jest bardzo prosta. Pierwsze co tutaj robimy, to wysłanie komendy, której wartością jest suma makrodefinicji „LCD_SET_CGRAM” (to właśnie w pamięci CGRAM zapisywane są znaki), oraz tego drugiego wyrażenia. Wyrażenie te, wyznacza adres pierwszego bajtu dla podanego przez nas numeru „tablicy”.
Do dyspozycji mamy 8 tablic przechowujących 8 bajtów, a więc ich adresy to 0x00-0x07, 0x08-0xF itd… I wyrażenie ((addr & 0x7) << 3), wyznacza na podstawie podanego przez nas numeru tablicy, adres jej pierwszej komórki.
Równie dobrze moglibyśmy zapisać to w taki sposób:
lcd_cmd(LCDC_SET_CGRAM); lcd_cmd(((addr & 0x7) << 3));
Kolejna linia przedstawia pętlę, która wypełnia wybraną przez nas przestrzeń w pamięci.
Jedno pole znaku to matryca 5×8 pikseli, wystarczy więc po kolei wysłać 8 liczb. Poniższa grafika powinna dobrze zilustrować definicję takiego znaku.
I na samym końcu, przełączamy wyświetlacz w tryb obsługi pamięci DDRAM. W tej funkcji napisałem, jednak dwie instrukcje for, z czego jedną zakomentowałem. Możesz dzięki temu sobie zmienić, czy chcesz definiować znaki zdefiniowane w pamięci RAM, czy FLASH. Domyślnie w kodzie jest ustawiona definicja z RAM.
Tak wygląda przykład użycia funkcji. Efektem będzie zdefiniowanie znaku 'ą’ i wyświetlenie tekstu „ąąą”.
unsigned char char_tab[8] = {0,0,14,1,15,17,15,2}; lcd_define_character(0x00, char_tab); lcd_cursor(0,0); lcd_data(0x00); lcd_data(0x00); lcd_data(0x00);
PCF8574 i I2C
Na rynku można znaleźć ciekawe moduły do tych wyświetlaczy, oparte o układ PCF8574. Jest to expander pinów GPIO sterowany po przez magistralę I2C. Bardzo przyjemne, szczególnie do prototypowania/debugowania naszych urządzeń, bo wystarczy podłączyć wyświetlacz czterema przewodami.
Jeżeli nie jesteś zaznajomiony z magistralą I2C możesz sprawdzić wpis o implementacji programowego I2C. Zawiera on trochę wiedzy teoretycznej.
https://mcinm.pl/programowa-implementacja-i2c-w-mikrokontrolerach-avr/
PCF8574 obsługa
Obsługa tego układu jest bardzo prosta. Posiada on 8 wyprowadzeń, służących jako GPIO więc jak się można domyśleć, do przesłania konkretnego ustawienia ich stanów wystarczy zaledwie 1 bajt.
LCD pcf8574 i2c biblioteka
Właściwie jedyne co się zmienia, to zawartość podstawowych funkcji do realizacji transmisji. Dodatkowo, na samym początku pliku C umieściłem zmienną…
unsigned char conf=0;
Będzie ona naszym rejestrem, który będziemy modyfikować i przesyłać do układu pcf. W nim będzie zawarta aktualna konfiguracja pinów. Poniżej przykład:
void EN_LOW(void) { conf &= ~(LCD_EN); pcf_send(conf); } void EN_HIGH(void) { conf |= LCD_EN; pcf_send(conf); }
Te dwie funkcje służą do zmiany stanu pinu EN. Wartość makrodefinicji „LCD_EN” jest oczywiście zdefiniowana w pliku nagłówkowym.
To raczej powinno wystarczyć. Na githubie znajdziesz dokładny kod obsługi, nie ma sensu go tutaj jednak przywoływać, gdyż jest to praktycznie to samo co wyżej tylko z lekkimi modyfikacjami. Znajdziesz tam także prostą bibliotekę do obsługi I2C, wartość rejestru TWBR w niej jest równa 32. Taka wartość daje prędkość 100 KHz, dla atmegi8 8MHz. 100 KHz jest prędkością zalecaną dla tego modułu. Poniżej zamieszczam jeszcze schemat w jaki sposób pcf jest połączony z wyświetlaczem.
Dodaj komentarz