W tej części zrealizujemy obsługę wyświetlacza w trochę inny sposób: dodamy komunikację w drugim kierunku i zajmiemy się odczytywaniem flagi zajętości (ang. busy flag), co umożliwi nam zmniejszenie opóźnień występujących podczas komunikacji. W tym celu zmodyfikujemy bibliotekę, która powstała w poprzednim „odcinku”.
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
Połączenia niewiele różnią się od tych z poprzedniego artykułu. Tutaj także korzystamy z trybu 4-bitowego, więc linie danych D0-D3 zostawiamy niepodłączone. Różnicą jest to, że pin RW zamiast do masy podłączamy do dowolnego wolnego GPIO mikrokontrolera.
Sterownik i podświetlenie wyświetlacza, w zależności od modelu, zasilamy napięciem 5v lub 3.3v, ALE w tym wypadku, jeżeli korzystasz z wyświetlacza pracującego w standardzie 5v nie możesz go bezpośrednio połączyć z mikrokontrolerem pracującym w standardzie 3.3v. Tutaj będziemy odczytywać dane ze sterownika LCD i jeżeli będzie on pracował w standardzie 5v to będzie nam na liniach danych wystawiał sygnały na poziomie 5v! Jak łatwo się domyślić, może to doprowadzić do „spalenia pinu”. W np. STMach, co prawda, mamy wejścia 5v tolerant, więc można się pokusić o podłączenie do nich linii danych, ale tego nie zalecam. W takiej sytuacji lepiej kupić wyświetlacz pracujący w standardzie 3.3v, albo dokonać dwukierunkowej konwersji napięć, o czym pisałem w pierwszej części.
In/Out
W kodzie będzie trochę więcej zmian niż w połączeniach, ale nie jest źle 🙂 Po pierwsze należy pamiętać, że kiedy chcemy coś odczytać ze sterownika to piny mikrokontrolera, do których są podłączone linie danych należy ustawić jako wejścia, a kiedy chcemy coś do niego wysłać to piny mikrokontrolera ustawiamy jako wyjścia. Ubierzmy sobie to w funkcje:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void lcd_datapins_out(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void lcd_datapins_in(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; 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); } |
Odczyt danych
W poprzedniej części napisaliśmy funkcję lcd_sendHalf(). Nic w niej nie zmieniamy, pozostaje nieruszona. Musimy tylko napisać analogiczną funkcję do odczytu połowy bajta. Z racji tego, że chcemy korzystać z trybu 4-bitowego to odczyt także musi być przeprowadzony w dwóch etapach. Podczas odczytu, podobnie jak przy zapisie, przesyłane są najpierw 4 najstarsze (najbardziej znaczące), a potem 4 najmłodsze bity.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
uint8_t lcd_readHalf(void) { uint8_t tmp = 0; LCD_E_HIGH; tmp |= (GPIO_ReadInputDataBit(LCD_D4_PORT, LCD_D4_PIN) << 0); tmp |= (GPIO_ReadInputDataBit(LCD_D5_PORT, LCD_D5_PIN) << 1); tmp |= (GPIO_ReadInputDataBit(LCD_D6_PORT, LCD_D6_PIN) << 2); tmp |= (GPIO_ReadInputDataBit(LCD_D7_PORT, LCD_D7_PIN) << 3); LCD_E_LOW; return tmp; } |
Funkcja GPIO_ReadInputDataBit() w zależności od odczytanego stanu na danym pinie zwraca 1 (stan wysoki) lub 0 (stan niski), które następnie przesuwamy o odpowiadającą danej linii danych ilość miejsc.
Gdy mamy funkcję do odczytu półbajta to możemy teraz napisać funkcję do odczytu całości:
1 2 3 4 5 6 7 8 9 10 11 12 |
uint8_t lcd_read_byte(void) { uint8_t result=0; lcd_datapins_in(); LCD_RW_HIGH; result = (lcd_readHalf() << 4); result |= lcd_readHalf(); return result; } |
Jak widać, przed wywołaniem funkcji lcd_readHalf() wywołujemy przedstawioną wcześniej funkcje lcd_datapins_in() oraz ustawiamy na linii sterującej RW stan wysoki, ponieważ chcemy dokonać odczytu danych ze sterownika. Jako że pierwszy odczytywany jest bardziej znaczący półbajt to zapisujemy go w bardziej znaczącym półbajcie zmiennej result – stąd przesunięcie w lewo o 4 miejsca. W kolejnym kroku odczytujemy mniej znaczący półbajt i dopisujemy go do zmiennej result na czterech najmniej znaczących bitach.
Dla jasności dodam, że funkcja lcd_readHalf() zwraca bajt w postaci 0000xxxx, gdzie x to odczytane bity. Przy przypisywaniu drugiego półbajta do zmiennej result mamy sumę logiczną, więc te zera nie wpływają w żaden sposób na zmianę wartości zmiennej.
Jak pisałem w pierwszej części tego cyklu, w zależności od ustawionej wartości na linii RS, odczytywać możemy dwie rzeczy:
- dane z pamięci DDRAM/CGRAM, gdy na linii RS ustawiamy stan wysoki,
- stan wyświetlacza, czyli tzw. busy flag i address counter, gdy na linii RS ustawiamy stan niski.
Busy flag informuje o tym czy sterownik jest zajęty wykonywaniem poprzedniej instrukcji czy może jest już wolny i możemy mu wysłać kolejną komendę.
Dzisiaj interesuje nas ten drugi punkt. Napiszmy w tym celu prostą funkcję:
1 2 3 4 5 |
uint8_t lcd_readFlag(void) { LCD_RS_LOW; return lcd_read_byte(); } |
Wysyłanie danych
No i w końcu możemy przejść do sedna 🙂 Przy komunikacji jednokierunkowej, po wysłaniu każdego bajta w funkcji lcd_writeByte() czekaliśmy zawsze 60µs. Zamiast tego, możemy zmodyfikować tę funkcję i wykorzystać sprawdzanie flagi zajętości, dzięki czemu wprowadzone opóźnienie będzie minimalne. Jeżeli najbardziej znaczący bit w odczytanym bajcie będzie równy zero, tzn. że sterownik zakończył wykonywanie instrukcji i możemy mu wysłać kolejne dane. Funkcja lcd_write_byte() po wprowadzeniu modyfikacji wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 |
void lcd_write_byte(uint8_t data) { lcd_datapins_out(); LCD_RW_LOW; lcd_sendHalf(data >> 4); lcd_sendHalf(data); while( lcd_readFlag() & 0x80 ); } |
Funkcja służy do wysyłania danych, więc nie można zapomnieć o ustawieniu pinów jako wyjścia i stanu niskiego na linii RW.
Inicjalizacja
Procedura inicjalizacji nie ulega żadnej zmianie. Należy jedynie pamiętać o włączeniu pinu RW i ustawieniu go jako wyjście oraz o ustawieniu na nim stanu niskiego przed wysyłaniem 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 27 28 29 |
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_RW_PIN; // <-------------------- GPIO_Init(LCD_RW_PORT, &GPIO_InitStructure); // Inicjalizujemy pin RW GPIO_InitStructure.GPIO_Pin = LCD_E_PIN; GPIO_Init(LCD_E_PORT, &GPIO_InitStructure); } |
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 |
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_RW_LOW; // <--- ustawiamy stan niski 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 ); } |
Pozostałe funkcje nie uległy zmianom. Jak widać, obsługa z wykorzystaniem pinu RW i odczytem flagi nie jest jakoś dużo bardziej skomplikowana, a zyskujemy sporo na szybkości komunikacji. Na tym artykule kończymy serię, ale wpisy dotyczące tych wyświetlaczy jeszcze się pojawią 🙂 Przypominam, że wszystkie kody źródłowe z tego projektu, wraz z ciałami funkcji opóźniająych oraz plikiem main, gdzie wywoływane są funkcje do wyświetlania znaków na LCD, znajdziesz na moim githubie ?
Skoro doszedłeś aż tutaj to przypuszczam, że temat wyświetlaczy Ci się spodobał. Jeśli już przyswoiłeś przekazane w tej serii informacje i swobodnie korzystasz z biblioteki, którą tu przygotowałem lub napisałeś własną, to warto zainteresować się jeszcze tematem buforowania LCD. Nie traktuj jednak tego jako niezbędną podstawę, a raczej ciekawostkę. Nie będę się w tym miejscu więcej rozpisywał na ten temat – chętnych zapraszam do artykułu:
Zostaw komentarz!
Podobała Ci się seria o obsłudze LCD? Masz jakieś pytania lub uwagi? Zostaw mi proszę komentarz, a jeśli uważasz artykuły za wartościowe – podaj je dalej 🙂 Fajnie by było gdyby trafiły do większej ilości osób. Pozdro i do następnego! 😉