Alfanumeryczny wyświetlacz LCD

Ponieważ nadal jestem początkującym programistą mikrokontrolerów, a także odświeżam swoje znajomości języka C z czasów studenckich, to wybaczcie mi moje potknięcia i niedociągnięcia, a także to że w swoich perypetiach podpierać się będę doświadczeniami nabytymi z niektórych książek (np. AVR i ARM7. Programowanie mikrokontrolerów dla każdego).

Po kliku programach z podwójnym siedmiosegmentowym wyświetlaczem LED przyszedł czas na bardziej złożony układ, a mianowicie dwuliniowy alfanumeryczny wyświetlacz LCD zgodny ze standardem HD44780. Nie będę się tu rozpisywał na temat samego wyświetlacza, jak on działa i jak się z nim komunikować, gdyż wszystko co Wam potrzeba znajdziecie w tych artykułach cz. 1, cz. 2, cz. 3 i cz. 4 – bardzo dobre, mi wyjaśniły wszystko. Przejdę natomiast do samego schematu podłączenia.

Schemat podłączenia

LCD_ATmega8.png

A tak to wygląda w realu. Niestety, jak na nowicjusza przystało, popełniłem mały błąd. W wyświetlaczu LCD należało przylutować goldpiny, aby podłączyć go do płytki stykowej, ponieważ nie posiadałem prostych goldpinów tylko takie zagięte pod kątem prostym, to przylutowałem właśnie takie. Wyświetlacz działa, ale jak się okazało jest on przymocowany do góry nogami (drobna niewygoda, która nie przeszkadza w samym programowaniu).

 LCD_ATmega8_podlaczenie.jpg

Dwie rzeczy wymagają dodatkowego wyjaśnienia. Pierwsza to taka, że nie każdy wyświetlacz LCD musi być wyposażony w dwa dodatkowe piny (15 i 16), które służą do dodatkowego podświetlenia wyświetlacza. Mój posiada, zatem pin 15 (anodę) podłączyłem do zasilania, a pin 16 (katodę) podłączyłem do GND, dzięki temu wyświetlacz pięknie świeci, wszystko jest wyraźne i nie wymaga dodatkowej zabawy z kontrastem. Druga to taka, ze obecne zadanie jakie wykonujemy nie wymaga odczytywania danych z wyświetlacza, zatem linię R/W na stałe podpiąłem do GND, co oznacza transmisję danych tylko w jednym kierunku (do wyświetlacza).

Dobrze wróćmy zatem do naszego zadania. Można tu podejść na dwa sposoby (jak podpowiada książka): skorzystać z gotowej biblioteki do obsługi modułów HD44780 lub napisać sobie samemu kilka prostych funkcji. Przyznam się od razu, że nie udało mi się skorzystać z gotowej biblioteki (pewnie brak doświadczenia), niemniej wariant z napisaniem własnych funkcji udał się w 100%. Było to dłuższe rozwiązanie, ale pozwoliło mi także zrozumieć sposób działania samego wyświetlacza i właśnie tymi zdobytymi umiejętnościami chciałem się tu z Wami podzielić.

Pisanie programu czas zacząć

Zatem zaczynamy. Tworzymy szablon naszego programu dołączając odpowiednie biblioteki.

#include <avr/io.h>
#include <string.h>
#include <util/delay.h>

int main(void){
while(1){
}
return 0;

 Teraz przyszedł czas na skonfigurowanie kilku niezbędnych stałych zgodnie z zaprezentowanym powyżej schematem podłączenia.

#define LCD_DDR DDRB
#define LCD_PORT PORTB
#define LCD_RS 2
#define LCD_EN 3
#define LCD_DB4 4
#define LCD_DB5 5
#define LCD_DB6 6
#define LCD_DB7 7

Wyświetlenie znaku

Mając tak zdefiniowane stałe napiszemy pierwszą funkcję, która będzie wysyłała do naszego wyświetlacza pojedynczy znak.

void LCD_SendChar(int8_t bajt)
{
	// wlaczenie linii ENABLE
	LCD_PORT |= _BV(LCD_EN);

	//wysłanie 4 starszych bitów
	LCD_PORT = (bajt & 0xF0)|(LCD_PORT & 0x0F);

	// potwierdzenie wysłana danych poprzez opadnięcie ENABLE
	LCD_PORT &= ~(_BV(LCD_EN));

	// odczekanie jednego cyklu
	asm volatile("nop");

	LCD_PORT |= _BV(LCD_EN);
	//wysłanie 4 młodszych bitów
	LCD_PORT = ((bajt & 0x0F)<<4)|(LCD_PORT & 0x0F);
	LCD_PORT &= ~(_BV(LCD_EN));

	// odczekanie niezbędnej długości czasu na potwierdzenie wprowadzenia danych
	_delay_us(40);
}

Nasza funkcja będzie wysyłała znak lub wartość numeryczną (wartość ASCII znaku) do naszego wyświetlacza dwoma etapami po 4 bity. Najpierw cztery starsze bity, a następnie cztery młodsze bity. Pamiętajmy, że przesyłając dane musimy je potwierdzić opadającą linia EN. W tym celu ustawiamy w pierwszej kolejności stan linii EN na 1(dla wyjaśnienia dodam, że polecenie _BV(LCD_EN) jest równoważne przesunięciu bitowemu (1<<LCD_EN). Następnie ustawiamy cztery starsze bity naszego PORTB nie zmieniając czterech młodszych wartości. Całość operacji potwierdzamy zmianą stanu linii EN na wartość 0. W analogiczny sposób przesyłamy  cztery młodsze bity zmiennej bajt, przesuwając je o cztery pozycje w lewo. Ważne jest aby między obydwoma wysłaniami odczekać jeden cykl, a po wykonaniu całości operacji odczekać minimalną ilość czasu (w przypadku wysłania dany jest to 40 mikrosekund). 

Wyczyść ekran 

Kolejną funkcją, która będzie wykorzystywała już napisaną przez nas funckję LCD_SendChar, jest LCD_Clear. Jej zadaniem jest wyczyszczenie ekranu naszego wyświetlcza z wszystkich aktualnie wyświetlonych informacji, a także tych pozostających w buforze. Warto tutaj nadmienić, iż większość wyświetlaczy mimo, iż ma tylko 16 znaków widocznych w każdej linii, to jest w stanie przechować napis o długości 40 znaków w każdej linii. 

W odróżnieniu od funkcji LCD_SendChar, funkcja LCD_Clear bedzie zamiast danych wysyłać do naszego wyswietlacza rozkaz wyczyszczenia. Jak już pewnie wiecie z powyższych artykułów nasz wyswietlacz potrafi rozróżnić informację czy przychodzi rozkaz, czy zestaw danych do wyswietlenia. Wszystko odbywa się za sprawą linii RS, która jeśli ma stan niski to informuje układ o tym, że otrzymał rozkaz, a jeśli ma stan wysoki to o tym, że otrzymał zestaw danych do wyświetlenia.

Zgodnie z tą zasadą zaczynamy od przestawienia linii RS na stan niski, wysyłamy rozkaz o wartości 1 (bitowy odpowiednik 0b00000001), który odpowiada rozkazowi wyczyść ekran. Po tej komendzie przestawiamy stan linii RS na wysoki, tak by można było ponownie wysyłać dane. Całość operacji, zgodnie z dokumentacją, może chwilę trwać, więc musimy zapewnić niezbędny minimalny czas odczekania, który w przypadku czyszczenia ekranu wynosi 1,64 milisekundy.

Kod naszej funkcji prezentuje się nastepująco.

void LCD_Clear()
{
	// przestawia na linii RS wartość na 0 po to by wysłać komendę a nie dane
	LCD_PORT &= ~(_BV(LCD_RS));
	// wysyłamy polecenie wyczyszczenia LCD
	LCD_SendChar(1);

	// przestawia linię RS na wartość 1 odpowiadającą wprowadzaniu danych
	LCD_PORT |= _BV(LCD_RS);

	// maksymalny czas oczekiwania na wyczyszczenie ekranu LCD
	_delay_ms(1.64);
}

Włączenie i konfiguracja wyświetlacza

Kolejnym etapem naszej pracy będzie odpowiednie włączenie i skonfigurowanie wyświetlacza. 

Zacznijmy zatem od ustawienia kierunku wyjściowego w naszym schemacie dla wszystkich istotnych linii (DDR4-DDR7 i LCD_RS i LCD_EN).

LCD_DDR = (0xF0)|(_BV(LCD_RS))|(_BV(LCD_EN));

Ustawiamy, także stan niski na wszystkich naszy liniach PORTD.

LCD_PORT = 0;
_delay_ms(45);

Dodatkowo odczekujemy niezbędny czas, aby stan pierwotny zdążył się ustawić.

Tak jak wspomniałem wcześniej, transfer danych do wyswietlacza, będzie odbywał się czterema liniami w dwóch etapach. Tą informację należy skonfigurować na samym początku. Dodatkowo w tym samym rozkazie ustawiamy obsługę dwóch wierszy oraz wymiar naszego znaku (5×8).

// rozpoczecie wysyłania komendy
LCD_PORT &= ~(_BV(LCD_RS));
// ustawienie parametrow wyswietlacza 
// BIT 4: 1 - 8 linii, 0 - 4 linie,
// BIT 3: 1 - 2 wiersze, 0 - 1 wiersz
// BIT 2: 0 - wymiar znaku 5x8; 1 - wymiar 5x10 LCD_SendChar(0b00101000); LCD_PORT |= _BV(LCD_RS);

Kolejnym etapem konfiguracji jest ustawienie informacji o tym jak ma się zmieniać adres po zapisie danych i czy ma się po zapisie przesuwać okno czy kursor. W naszym przypadku kod ten będzie wyglądał następująco. 

LCD_PORT &= ~(_BV(LCD_RS));
// BIT 2 - tryb pracy wyświetlacza (inkrementowanie zapisu danych)
// BIT1: 1 - przesunięcie okna, 0 - przesunięcie kursora LCD_SendChar(0b00000110); LCD_PORT |= _BV(LCD_RS);

Ostatnim etapem konfiguracji jest ustalenie jak ma wyglądać kursor i jak ma się zachowywać

LCD_PORT &= ~(_BV(LCD_RS));
// BIT2: 1 - wyświetlacz włączony, 0 - wyłączony // BIT1: 1 - włączenie wyświetlania kursora, 0 - kursor niewidoczny
// BIT0: 1 - kursor miga, 0 - kursor nie miga LCD_SendChar(0b00001100); LCD_PORT |= _BV(LCD_RS);

Na koniec pozostało już tylko wyczyszczenie naszego LCD tak by po włączeniu nie było na nim żadnych znaków. Cały ten kod zamykamy w funkcję LCD_SwitchOn, która będzie miała następującą postać

void LCD_SwitchOn()
{
	// ustawienie kierunku wyjściowego dla wszystkich linii
	LCD_DDR = (0xF0)|(_BV(LCD_RS))|(_BV(LCD_EN));

	// ustawienie początkowego stanu niskiego na wszystkich liniach
	LCD_PORT = 0;


	// rozpoczęcie wysyłania komendy 
	LCD_PORT &= ~(_BV(LCD_RS));
	// ustawienie parametrów wyświetlacza
	// BIT 4: 1 - 8 linii, 0 - 4 linie,
	// BIT 3: 1 - 2 wiersze, 0 - 1 wiersz
	// BIT 2: 0 - wymiar znaku 5x8; 1 - wymiar 5x10
	LCD_SendChar(0b00101000);
	LCD_PORT |= _BV(LCD_RS);

	LCD_PORT &= ~(_BV(LCD_RS));
	// BIT 2 - tryb pracy wyświetlacza (inkrementowanie zapisu danych)
	// BIT1: 1 - przesunięcie okna, 0 - przesunięcie kursora
	LCD_SendChar(0b00000110);
	LCD_PORT |= _BV(LCD_RS);


	LCD_PORT &= ~(_BV(LCD_RS));
	// BIT2: 1 - wyświetlacz włączony, 0 - wyłączony
	// BIT1: 1 - włączenie wyświetlania kursora, 0 - kursor niewidoczny
	// BIT0: 1 - kursor miga, 0 - kursor nie miga
	LCD_SendChar(0b00001100);
	LCD_PORT |= _BV(LCD_RS);

	LCD_Clear();
}

 

Wypisanie pierwszego znaku

Dzięki posiadaniu funkcji włączającej i wpisywania znaku możemy już napisać nasz Pierszyc tekst na naszym wyświetlaczu.

#include <avr/io.h>
#include <string.h>
#include <util/delay.h>

int main(void){ LCD_SwitchOn(); LCD_SendChar('H'); LCD_SendChar('e'); LCD_SendChar('l'); LCD_SendChar('l'); LCD_SendChar('o'); LCD_SendChar(' '); LCD_SendChar('W'); LCD_SendChar('o'); LCD_SendChar('r'); LCD_SendChar('l'); LCD_SendChar('d'); LCD_SendChar('!'); LCD_SendChar('!'); LCD_SendChar('!');

while(1){
}
return 0;

Jak widać po załadowaniu programu do naszego mikrokontrolera na naszym wyświetlaczu pojawił się upragniony tekst Hello World!!!. Można by powiedzieć, że zadanie zostało wykonane, jednak nie wyobrażam sobie pisania jakiegokolwiek tekstu w przyszłości po jednej literze. Stąd proponuję stworzyć dodatkową funkcję, która przyjmie dowolny ciąg znaków i wyświetli go na naszym wyświetlaczu.

Wypisanie ciągu znaków

Nasza funkcja LCD_SendText przyjmuje dwa argumenty: text do wyświetlenia i długość tekstu do wyświetlania.

void LCD_SendText(char *text, int8_t textLength)
{
	int8_t k=0;
	while(k < textLength)
	{
		LCD_SendChar(text[k]);
		k++;
	}
}

W rezultacie nasza główna funkcja może wyglądać następująco:

#include <avr/io.h>
#include <string.h>
#include <util/delay.h>

int main(void){
char napis[14] = "Hello World!!!"; LCD_SwitchOn(); LCD_SendText(napis, 14);

while(1){
}
return 0;

A oto efekt końcowy

hello_world.jpg

Co dalej …

Dalej to już można wszystko, możliwości są przeogromne. Ja postanowiłem sobie wyizolować wszystkie funkcje dotyczące LCD jak i stałe z tym związane, tak aby móc łatwiej używać ich w innych projektach. Wzbogaciłem także bibliotekę o możliwość przesuwania kursora w lewo i w prawo o ustaloną ilość pozycji, jak i możliwość przesuwania ekranu w lewo i prawo. Kolejnymi etapami może być także stworzenie funkcji zapisującej znak do pamięci LCD, tak by można było z niego skorzystać przy wyświetlaniu tekstu, ale to może w następnym artykule.

 

Alfanumeryczny wyświetlacz LCD
Przewiń do góry