Programování mikrokontrolérů architektury AVR (7)

Zdroj: SOOM.cz [ISSN 1804-7270]
Autor: Prog0el
Datum: 20.5.2016
Hodnocení/Hlasovalo: 1.63/8

Ve čtvrtém díle našeho seriálu jsme si popsali řízení sedmisegmentového displaye. Tento display je ovšem nevhodný k zobrazování textu. Řízení zobrazovače, který si popíšeme, již bude probíhat na vyšší úrovni a nebudeme muset řešit multiplexování, čímž dojde k výrazné úspoře času procesoru.

Znaková sada

Použitý zobrazovač je alfanumerického typu, který je schopen zobrazovat mimo číslice i písmena a speciální znaky. Jedním ze základních parametrů, dle kterého tyto displaye vybíráme, je počet znaků a řádků. Jedním z nejčastějších typů je display "16×2", což je alfanumerický zobrazovač složený ze dvou řádků a na každém z těchto řádků se nachází 16 znaků. Každý znak je tvořen maticí o šířce 5px a výšce 8px. V praxi je spodní řada pixelů využívána pouze pro kurzor, tj. reálně má každý znak 5px na šířku a 7px na výšku.
Příklad provedení displaye 20×2 znaků:

Display zepředu
Display zezadu

Využít můžeme buďto předdefinovaných znaků v rámci znakové sady displaye umístěné v jeho paměti či můžeme znaky nadefinovat vlastní prostým popisem, které pixely mají být zobrazeny při zavolání příslušného znaku. Uživatelských znaků je možno dodefinovat 8.

Pro popis schopností displaye zobrazit příslušné znaky se používá tabulka znaků. V tabulce znaků se nachází znaky organizovány tak, že jeho poloha na ose x a y vytváří číselný kód, pod kterým je možno znak volat a tím i zobrazit. Na ose y se nachází nejnižší nibble osmibitového slova a na ose x nibble vyšší. Mnoho z vás se již setkalo s tabulkou ASCII, se kterou je znaková sada zobrazovače z velké části kompatibilní.

Vypisovaný obsah LCD se zapisuje do DDRAM paměti. Znaky, které uživatel nadefinoval, jsou uloženy v CGRAM pouze do odpojení napájení.

Komunikace probíhá postupem nastavení pozice kurzoru a zasíláním kódu znaků, které mají být zobrazeny, což je osmibitové číslo znaku. Mnoho lidí by jistě v tabulce znaků marně hledalo českou diakritiku, i když údajně existují výjimky displayů s českou znakovou sadou, tak si definici znaku demonstrujeme právě na definici znaku české znakové sady.

Znaková sada použitého LCD:

Tabulka znaků

Pinout zobrazovače

1: GND - nulový potenciál displaye 2: VCC- napájení +5V 3: CONTRAST- nastavení kontrastu pomocí odporového děliče mezi zemí a napájením. 4: RS- Přepínání „data/ instrukce“ 5: R/W- Volba módu zápisu či čtení 6: E- synchronizace 7÷14: 8 bit sběrnice (7. bit je LSB) 15: Anoda podsvětlovací LED. Je třeba mít na mysli, že většina modulů nemá integrován rezistor sloužící k omezení proudu LED diodou a je jej nutno vypočítat dle upraveného ohmova zákona, jak je uvedeno v prvním díle našeho seriálu. 16: Katoda podsvětlovací LED, která je většinou spojena s GND již přímo na modulu

Číslování pinů zobrazovače je znázorněno na jeho desce plošného spoje:

Vývody LCD

Podsvícení není výbavou všech zobrazovačů. Zobrazovače bez podsvícení mají piny 15 a 16 většinou nevyužity. Před použitím konkrétního displaye se vždy podívejte do katalogového listu výrobku, zda odpovídá pinout. V mém případě bylo podsvícení připojeno k napájecím pinům včetně integrovaného rezistoru.

Komunikace

Komunikace se zobrazovačem začíná inicializací, při které nastavujeme parametry zobrazovače a typ datové sběrnice, jelikož se zařízením je možno komunikovat jak po celé osmibitové sběrnici, tak po polovině sběrnice ve dvou cyklech. Toto řešení je častější a nese s sebou úsporu počtu vodičů a pinů mikrokontroléru. Nevýhodou je poloviční rychlost přenosu, která v praxi nepřináší žádné obtíže, a popisem osmibitového přenosu se z těchto důvodů nebudu zaobírat. Při použití čtyřbitového přenosu dat použijeme pouze 4 nejvyšší datové piny displaye a zbylé čtyři nejnižší datové piny uzemníme. Následně v prvním kroku zašleme nejvyšší nibble dat a hned po něm, v dalším cyklu nibble nižší.

Jak bylo popsáno v předchozí kapitole, pin „RS“ slouží k volbě, zda jsou příchozí informace data (1) či instrukce (0). Instrukce jsou schopny řídit funkce displaye, které jsou popsané v tabulce příkazů níže. V případě, že potřebujeme provést zaslání dat, kterými může být příkaz k vypsání konkrétního znaku, pak pomocí pinu „RS“ provedeme přepnutí do módu „data“.

Pin „RW“ přepíná, zda z displaye čteme (1) či do něj zapisujeme (0). Jelikož číst z LCD v našem příkladě nebudeme potřebovat, tak připojíme pin „RW“ k zemi, čímž nastavíme display do módu zápisu.

Vývod „E“ je obdoba signálu „!strobe“ využívaného v řadiči HD44780 podobném paralelním portu, jenž je známý též jako „LPT“ port. Účelem tohoto pinu je označit situaci na datové sběrnici za platnou. Pouze se vzestupnou hranou signálu na pinu „E“ jsou data na sběrnici brána na zřetel a zároveň by v této době nemělo docházet ke změnám na sběrnici, jelikož by pak data mohla být poškozena. Existence tohoto pinu nám dokazuje, že se jedná o synchronní přenos. Uspořádání sběrnice zase potvrzuje fakt, že se jedná o paralelní přenos; jen tak na okraj.

Tabulka příkazů řadiči HD44780

instrukceRSRWD7D6D5D4D3D2D1D0
vymazat LCD0000000001
nastavit DDRAM adresu na 00h000000001X
A-posun kurzoru L: 0/R: 1; B- posun textu ano: 1/ne: 000000001AB
A-zapnout LCD: 1; B-zapnout kurzor: 1; C-zapnout blikání kurzoru: 10000001ABC
A- text: 1/ kurzor: 0; posunout o jeden znak vlevo- B:0 nebo vpravo- B:1000001ABxx
A-mód- 4-bit: 0/ 8bit: 1; B: jednořádkový LCD: 0/ víceřádkové: 1; Volba rozlišení znaku- 5×8: 0/ 5×10: 1;00001ABCxx
Nastavení adresy v CGRAM a přepnutí na zápis do ní0001ADRESA
Nastavení adresy v DDRAM a přepnutí na zápis do ní001ADRESA
Zapsat data do zvolené paměti10DATA

Inicializace

Myslím, že již dále nemá cenu vysvětlovat řízení LCD pouze teoreticky, a proto si uvedeme příklad, jak probíhá inicializace LCD po zapnutí. Pro demonstraci připojíme LCD k mikrokontroléru AT mega 8 takto:
RS: PC0
E: PC1
D4÷D7: PC2÷PC5

://Zapiš byte do LCD (, RS)
void lcd_write(unsigned char data, unsigned char rs){
unsigned char msb_lsb_switch=0; 	//Přepínání mezi zápisem MSB-LSB a konec
unsigned char data_msb;		 //Proměnná pro uložení vyššího nibble dat
//Rozdělení byte na vyšší a nižší nibble:
data_msb = data & 0b11110000; 	//Vyšší nibble
data_msb = data_msb >> 2;
data &= 0b00001111;			//Nižší nibble
data = data < 2;
if (rs==1){				//Nastavení signálu RS dle argumentu funkce
data++;
data_msb++;
}
while (msb_lsb_switch!=2){		//Dokud není konec přenosu 
switch(msb_lsb_switch){	//Přenes nejvyšší, či nejnižší nibble dat
case 0:
 PORTC=data_msb;
break;
case 1:
PORTC=data;
break;
	}
msb_lsb_switch++; 		//Přejít k dalšímu kroku
_delay_us(10);		// Čekání na ustálení úrovně na pinech
//Vygenerování impulzu na E dle předchozího popisu:
PORTC|=0b00000010;
_delay_us(10);
PORTC&=0b11111101;
_delay_us(45);			//Čekání na provedení instrukce
	}
	_delay_ms(2); //Čekání na provedení instrukce po jejím ukončení
} //Konec deklarace funkce
DDRC=0b00111111; 	//Nastavení části pinů portu C jako výstup
_delay_us(100); 	// Čekání na ustálení úrovně na pinech
PORTC=0b00001000;	//Nastavení čtyřbitového módu (LCD je stále v osmibitovém módu) 
_delay_us(10);		// Čekání na ustálení úrovně na pinech
PORTC|=0b00000010;	//Náběžná hrana synchronizačního pulzu na E
_delay_us(10);		//Pin E je držen v logické 1
PORTC&=0b11111101; 	//Sestupná hrana na pinu E
_delay_us(40);		//Čekání na provedení instrukce
//Nyní již komunikujeme ve čtyřbitovém módu s využitím námi nadefinované funkce
lcd_write(0b00101000,0); //Inicializace 4-bit módu ještě jednou
lcd_write(0b00000110,0); //Posun kurzoru a textu směrem vpravo
lcd_write(0b00001100,0); //LCD zapnut, kurzor a jeho blikání vypnuto
lcd_write(0b00000001,0); //Vymazání DDRAM a návrat na první znak (adresa 00h)

Sluší se ještě dodat, že pokud nečteme tzv. „busy flag“, tedy informaci z LCD o tom, zda je zobrazovač připraven přijímat data, pak je nutno zajistit, že inicializace LCD proběhne až asi 2 sekundy po jeho zapnutí; toho lze v praxi docílit použitím zpožďovací smyčky po zapnutí mikrokontroléru, jenž LCD řídí. Bylo by škoda tímto časem plýtvat, což lze částečně napravit provedením inicializace LCD až jako poslední komponent našeho zařízení.

Nyní je LCD připraveno k použití a můžeme se konečně pustit do výpisu textu. Jako první krok je nutno nastavit pozici kurzoru. Pozice kurzoru se nastavuje adresou v paměti DDRAM, kde prvnímu znaku prvního řádku náleží adresa 00h a první znak druhého řádku se nachází na adrese 40h. Každý další znak na řádku má adresu o jednu vyšší, než znak předchozí.

Pokud jednou nastavíme adresu, můžeme již pouze vypisovat jednotlivé znaky, jelikož posun kurzoru o daný směr již byl nastaven v inicializaci displaye.

lcd_write(0b10000001,0); //Nastavení adresy v DDRAM na 01h (první řádek, druhý znak)
lcd_write(‘a‘,1); //Výpis znaku „a“ na nastavenou pozici

Po inicializaci je již práce s displayem vcelku snadná, že? Jak bychom ale řešili výpis celých řetězců? Pro tento účel si napíšeme jednoduchou funkci, která bude volat námi napsanou funkci lcd_write():

void lcd_puts(char input[]){
	int i=0; //Iterátor sloužící k indexování pole input[] datového typu char
	while(input[i] != '\n'){ //Dokud není detekován konec řádku...
		lcd_write(input[i], 1); //...je vypisován znak po znaku z řetězce v arg.
		i++; //Inkrementace iterátoru
	}
}

Popsaná funkce využívá faktu, že řetězec znaků (string) je pole znaků o datovém typu char, zakončený znakem identifikující konec řádku (‚\n‘). Použití této funkce si ukážeme na příkladu, který vypíše text „Ahoj“ na šestý znak druhého řádku LCD:

lcd_write(0b10000001,0); //Nastavení adresy v DDRAM na 45h (druhý řádek, šestý znak)
lcd_puts(„Ahoj\n“);	     // Nezapomínejte používat znak \n. Jinak dojde k chybě.

V praxi nám jistě přijde vhod převod čísla nějakého datového typu na konkrétní číslo. Pokud se podíváte na tabulku znaků, která je mimo jiné v tomto případě kompatibilní s ASCII tabulkou, pak přijdete na to, že znak nuly má adresu 0x30. Další číslice až po znak „9“ se nachází v řadě za zmíněnou adresou. Na základě tohoto poznatku můžeme říct, že převod celého čísla v intervalu <0;9> můžeme realizovat přičtením čísla 0x30 k číslu převáděnému. U větších čísel je ovšem nutno počítat s více ciframi, což si vysvětlíme na příkladě. Mějme číslo 126, které chceme pomocí ASCII kódu zobrazit na LCD. Pokud číslo vydělíme stem, pak dostaneme stovkovou cifru. Desítkovou cifru získáme zbytkem po celočíselném dělení (modulo) stem děleného deseti. Proč deseti? Protože zbytek po celočíselném dělení trojciferného čísla stem je dvouciferný, de facto obsahující jednotkovou a desítkovou cifru, což pro nás není akceptovatelný formát. Jednotky jsou zbytkem po celočíselném dělení převáděného čísla. Příklad realizace v programovacím jazyce:

char number=176; //Naše ukázková proměnná typu char s číslem 176d
lcd_write(0b10000000,0); //Nastavení adresy v DDRAM na 00h (první řádek, první znak)
lcd_write(((number / 100) + 0x30),1); 	//Výpočet stovek
lcd_write((((number % 100)/10) + 0x30),1);	//Výpočet desítek
lcd_write(((number % 10) ) + 0x30),1);	//Výpočet jednotek

Při návrhu algoritmů tohoto typu se snažte ošetřovat vstup tak, aby nevznikaly vyšší adresy znaků, než 39h, který odpovídá znaku „9“. Posléze by se totiž začaly zobrazovat znaky číslice ani zdaleka nepřipomínající.

Dalším poznatkem, na který bych vás rád upozornil je fakt, že by mělo být nějakým způsobem zajištěno, aby v průběhu komunikace s LCD nedocházelo k přerušení, které by mohlo přenos přerušit v tu nejnevhodnější chvíli a zapříčinit vypisování nesmyslných údajů, což se mi také stalo kdysi při mém prvním seznamování s tímto typem externí periferie mikrokontroléru.

Definice uživatelských znaků

Jak jsem již poznamenal, je možno nadefinovat celkem osm uživatelských znaků. Tyto znaky jsou dostupné na adresách 0÷15. Interval znaků na adresách 0÷7 odpovídá znakům 8÷15. Jednotlivé znaky se zapisují do paměti CGRAM, která je, jak již bylo zmíněno, volatelní (energeticky závislá), z čehož vyplývá, že definici uživatelských znaků musíme provádět po každém zapnutí zařízení, což ovšem v praxi žádné komplikace nezpůsobuje.

Práce s pamětí CGRAM je obdobná, jako práce s DDRAM, pouze s tím rozdílem, že adresa CGRAM není sedmibitová, jako v případě DDRAM, ale šestibitová. Vrchní tři bity slouží k identifikaci znaku pomocí čísla 0÷7/ 8÷15 a spodní tři bity k označení řádku pixelů na zobrazovači. Vrchní řádek se nachází na adrese 000b. Jako poslední řádek se používá řádek sedmý na adrese 110b, pod kterým se nachází řádek 111b využívaný kurzorem. Po volbě čísla znaku a řádku, který chceme modifikovat potřebujeme přenést data k zápisu. to provedeme nám známým příkazem pro zápis dat, v našem případě s využitím naší funkce lcd_write(). Na vrchních třech bitech nezáleží, jelikož máme pouze pět pixelů na řádek. Bit číslo čtyři odpovídá prvnímu pixelu konkrétního znaku. Bity, které budou mít logickou hodnotu „1“ způsobí zobrazení pixelu, který ovládají. Logická nula označuje, logicky, skrytí daného pixelu. Záměrně se vyhýbám pojmu „svícení pixelů“, jelikož se nejedná o technologii OLED. Jak je tomu již zvykem, i tuto, dosud poměrně abstraktní skutečnost si vysvětlíme na následujícím příkladě definice znaku „ř“ na adresu 5h v CGRAM:

Práce s pamětí CGRAM je obdobná, jako práce s DDRAM, pouze s tím rozdílem, že adresa CGRAM není sedmibitová, jako v případě DDRAM, ale šestibitová. Vrchní tři bity slouží k identifikaci znaku pomocí čísla 0÷7/ 8÷15 a spodní tři bity k označení řádku pixelů na zobrazovači. Vrchní řádek se nachází na adrese 000b. Jako poslední řádek se používá řádek sedmý na adrese 110b, pod kterým se nachází řádek 111b využívaný kurzorem. Po volbě čísla znaku a řádku, který chceme modifikovat potřebujeme přenést data k zápisu. to provedeme nám známým příkazem pro zápis dat, v našem případě s využitím naší funkce lcd_write(). Na vrchních třech bitech nezáleží, jelikož máme pouze pět pixelů na řádek. Bit číslo čtyři odpovídá prvnímu pixelu konkrétního znaku. Bity, které budou mít logickou hodnotu „1“ způsobí zobrazení pixelu, který ovládají. Logická nula označuje, logicky, skrytí daného pixelu. Záměrně se vyhýbám pojmu „svícení pixelů“, jelikož se nejedná o technologii OLED. Jak je tomu již zvykem, i tuto, dosud poměrně abstraktní skutečnost si vysvětlíme na následujícím příkladě definice znaku „ř“ na adresu 5h v CGRAM:

//definice znaku 5h; 'ř'
lcd_write(0b01101000,0); 	//Řádek 0 
lcd_write(0b00001010,1); 	//Data: _#_#_
lcd_write(0b01101001,0); 	//Řádek 1
lcd_write(0b00000100,1); 	//Data: __#_
lcd_write(0b01101010,0); 	//Řádek 2
lcd_write(0b00010110,1); 	//Data: #_##_
lcd_write(0b01101011,0); 	//Řádek 3
lcd_write(0b00011001,1); 	//Data: ##__#
lcd_write(0b01101100,0); 	//Řádek 4
lcd_write(0b00010000,1); 	//Data: #____
lcd_write(0b01101101,0); 	//Řádek 5
lcd_write(0b00010000,1); 	//Data: #____
lcd_write(0b01101110,0); 	//Řádek 6
lcd_write(0b00010000,1); 	//Data: #____
lcd_write(0b01101111,0); 	//Řádek 7
lcd_write(0b00000000,1); 	//Data: _____
//Vypsání znaku provedeme následovně:
lcd_write(0b10000000,0); //Nastavení adresy v DDRAM na 00h (první řádek, první znak)
lcd_write(5,1);	     //Výpis uživatelsky definovaného znaku umístěného na ADDR. 5  	

Praktický příklad

Praktický příklad bude relativně jednoduchý a efektní. Níže uvedený program bude vypisovat různé texty na display a měl by být schopen vás dovést k plnému porozumění významu funkcí. Může dát jisté úsilí vyvinout funkci, která s LCD bude komunikovat, avšak obsluha takovéto funkce již bude výrazně jednodušší a na vyšší úrovni. S ohledem na programovou paměť je neustálé volání funkcí hospodárnější, avšak tato úspora je vykoupena vyšším časem zpracování výsledného kódu. V praxi se většinou jedná o zanedbatelnou dobu. Na internetu také existuje celá řada knihoven pro LCD, nejen s tímto populárním řadičem LCD, ale i rozličné grafické zobrazovače. Plně rozumím použití těchto knihoven, které vám výrazně zrychlí proces vývoje, avšak musím jim vytknout fakt, že nemáte šanci porozumět každému detailu, který se v daný okamžik odehrává. Dejte si také pozor na komerční využití vašich zařízení, jelikož mnoho knihoven limituje jejich oblast použití pouze na účely nekomerční.

Nejdříve si uvedeme schéma zapojení:

Schéma zapojení

Schéma zapojení není nijak záludné až na rezistory R1 a R2. Zmíněné rezistory nastavují kontrast zobrazovače a více, než jejich hodnota je důležitý jejich poměr. Čím vyšší bude poměr odporu rezistoru R1 ku R2 vyšší, tím bude vyšší i kontrast. Použil jsem zobrazovač 20×2 znaků, avšak myslel jsem na ty z vás, kteří si zakoupí zobrazovač 16×2 znaků a nepřekračoval jsem v ukázkovém programu 16 znaků na řádek. Rovněž jsem přišel na to, že můj zobrazovač nevyžaduje použití externích rezistorů k nastavení kontrastu a pin „CONTRAST“ stačilo připojit k zemi, což nemusí být pro všechny zobrazovače volbou vedoucí k čitelnému zobrazení; u většiny LCD je nutno provést nastavení rezistory. Pokud jste kontrast nastavili příliš malý, pak jsou zobrazované znaky příliš slabě viditelné či zcela skryté. Pokud byste nastavili kontrast do takové míry, že vám budou zobrazované pixely splývat s pixely vypnutými, pak je nutno kontrast naopak snížit. Jestliže LCD zapnete a neinicializujete, pak se rozsvítí pouze vrchní řádek LCD, což značí chybu inicializace. Pokud tento problém nastane, zkontrolujte stav připojení LCD k mikropočítači a pokud máte tu možnost, zkuste, zda stejná chyba nastává i s jinými zobrazovači, Pokud ne, pak je display vadný. Osobně jsem se příliš nesetkal s nějakou značnou kazivostí těchto komponent, avšak jednou se mi podařilo LCD zničit přepólováním napájecího zdroje, což několik zobrazovacích jednotek, které mi prošly rukama, ač nedobrovolně, přežilo.

Konstrukci jsem zapojil v nepájivém kontaktním poli následovně:

Realizace na nepájivém kontaktním poli

Zdrojový kód aplikace pro náš mikrokontrolér je z mého pohledu dostatečně okomentovaný a pro jeho pochopení vás odkážu přímo k jeho stažení. V kódu se nevyskytují žádné části, které bychom si předtím nevysvětlili. Vysvětlením tohoto typu zobrazovače jsme si rozšířili možnosti datového výstupu pro uživatele o mocný nástroj umožňující zobrazení relativně velkého množství dat. V příštím díle si ukážeme jednu z možností, jak provést jednoduché měření s výstupem na tento zobrazovač a postavíme si jednoduchý měřící přístroj.

Reference

https://www.sparkfun.com/datasheets/LCD/HD44780.pdf