LCD na AVR

Úvod

V "arduino" komunitě se název "LCD" vžil pro alfanumerické LCD s driverem, kterým se v tomto článku zabývat nebudeme. Bavit se budeme o LCD bez driveru. Nejprve vás stručně seznámím s obsahem tutoriálu, ať se můžete rozhodnout zda chcete jeho čtení věnovat váš vzácný čas. Napřed si řekneme o LCD něco obecně, pak si uděláme exkurzi do vnitřností LCD driveru na Atmega169. Ukážeme si jak vypadají průběhy, kterými se LCD budí. Postavíme si jednoduchý přípravek a zprovozníme "průměrný" displej. Nakonec uděláme "micro-power" pokus a ukážeme si jak rozjet Atmel i s displejem s minimální spotřebou. Upozorňuji vás, že kvůli velkému objemu informací nemusí být tento tutoriál úplně jednoduché sousto.

Motivace

Displeje z tekutých krystalů (LCD), které znáte třeba z multimetrů, mají v aplikacích svou nezastupitelnou roli díky svým dvěma klíčovým přednostem. Jednou z nich je kontrast. Zatímco klasické LED nebo TFT displeje ztrácí kontrast když na ně posvítíte, u LCD naopak kontrast roste. Hodí se tedy například pro aplikace kde bude displej na přímém slunci. Druhou předností je spotřeba, která se odvíjí od plochy a pohybuje se běžně v řádu jednotek uA (respektive uW). LCD může ale zaujmout z ryze designových důvodů, cifry bývají typicky velké a k dostání jsou běžně "šestnácti" a "čtvrnácti" segmentové varianty schopné zobrazovat celou abecedu. Mnoho z nich má také různé speciální vzory jako třeba indikaci stavu baterie.

Displej DE 161

Daní za zmíněné přednosti je jistá náročnost jejich řízení. Displeje je potřeba budit střídavým napětím bez stejnosměrné složky, která by způsobovala elektrolýzu a degradaci. Displeje, které mají pro každý segment samostatný vývod můžete řídit přímo jakýmkoli jednočipem a nebo pro úsporu vývodů třeba přes kaskádu posuvných registrů (i když tím asi přijdete o možnost nízké spotřeby). Tomu způsobu řízení (buzení) se říká "static drive" a setkáte se s ním u displejů s menším počtem segmentů (dejme tomu do 32 - tedy 4 cifry). Budete asi souhlasit že připojovat 32 a více vývodů je značně nepraktické, proto se LCD vyrábí i ve variantách s vícero společnými elektrodami. Jejich buzení už ale není tak triviální a vyžaduje schopnost driveru vytvářet více než jen dvě hodnoty napětí (např 3.3V a 0V). Já se vyhnu výkladu teorie o řízení LCD, neboť to pro praktickou aplikaci není až tak klíčové a odkážu zájemce na seznam literatury na konci článku. My využijeme čip Atmega169A vybavený LCD driverem.

Atmega169

Atmega169 je asi nejznámější zástupcem několika málo "Atmelů" s LCD driverem. Dá se sehnat z číny od 25kč, u nás se jeho cena pohybuje mezi 80-120kč a kvůli počtu vývodů je k dostání pouze v SMD provedení ( 64pinů s "větší roztečí"). Alternativou s větším počtem vývodů je Atmega3290 (100pinů s "menší roztečí"). Mimo běžné periferie, které znáte z jiných "Atmelů" obsahuje LCD driver schopný řídit displeje v konfiguraci až 4x25 segmentů (mega3290 pak až 4x40 segmentů). Převedeme-li tyto počty na "cifry", bavíme se o 12-ti až 14-ti cifrách v případě Megy169 a 20-ti až 22-ti cifrách v případě Megy3290. Bez problémů tedy lze odřídit například dva klasické čtyřmístné displeje. LCD driver na čipu je navržen tak aby byl schopen pracovat i v "low power" režimech, což si v tutoriálu vyzkoušíme. Narazit můžete na čipy Atmega169A, Atmega169PA, případně Atmega169P. Písmeno "P" značí "pico-power" a tyto čipy mají těsnější limity odběru. Zatímco si "A" verze při 2V a 1MHz vezme až 1mA, u "P" verze máte záruku, že to bude pod 0.44mA. Typická hodnota je ale u obou 0.35mA. Význam tyto parametry dostanou až když budete čip trápit například vysokou teplotou.

Ovládání LCD driveru

Než se začneme zabývat hardwarem a připojením displeje k jednočipu, projdeme si trochu teorie abychom měli přehled o funkcích LCD driveru. Driver má dva druhy vývodů. COM0,1,2,3 slouží k buzení společných elektrod a SEG0 až SEG24 k buzení segmentů. Jejich roli si objasníme postupně. Driver pracuje autonomně a o celé zobrazování se stará sám. Krom inicializace interaguje software s driverem jen pokud je potřeba měnit obraz. Jako u všech periferií se driver ovládá skrze skupinu registrů. Projdeme jednotlivé funkce a nakonec si ve stručném přehledu popíšeme ve kterých registrech je lze ovládat. LCD driver lze taktovat buď clockem jádra (takže např 1MHz, 8MHz a pod.) nebo asynchronním clockem z Timeru 2 (typicky 32.768kHz), který znáte z předchozích dílů. Frekvence má vliv na spotřebu a na "kvalitu obrazu". Platí, že čím je nižší tím menší je spotřeba. Proti tomu jde ale požadavek držet obnovovací frekvenci (frame-rate) nad cca 30Hz, jinak displej bliká. Typicky tedy budete hledat nějaký kompromis. K nastavování frekvence vám slouží předdělička (prescaler) a dělička (divider). Frame-rate spočítáte pomocí vztahu
frame_rate = f_clk/(K*N*D)
kde:
N je hodnota předděličky (16,64,128,256,512,1024,2048,4096)
D je hodnota děličky (1,2,3,4,5,6,7,8)
K je 8 pro duty 1/4 a 6 pro duty 1/3,1/2 a u Static drive (o tom více za chvíli)

Pinout Atmega169. PA0 až PA3 jsou společné elektrody COM, Segmenty jsou označeny SEG. (z datasheetu)

Další co potřebujete řídit je kontrast displeje. To lze několika mechanismy. První zmíníme dobu buzení, kterou lze nastavit jako fixní s časem 70 až 1150us, případně jako polovinu taktu nebo celý takt. Čím delší je doba buzení, tím větší je kontrast. Bez buzení se displej během periody postupně vybíjí a klesá kontrast. Druhou možností jak lze ovlivnit kontrast je amplitudou budicí waveformy. Její maximum je rovno napětí VLCD, které lze generovat buď vnitřním generátorem nebo ho do čipu přivést z vnějšku (na pin LCDCAP). Vnitřní generátor vám umožňuje nastavit napětí v rozsahu 2.6V až 3.35V bez ohledu na napájecí napětí čipu. Za tento komfort ale platíte zvýšenou spotřebou, zvláště pokud je rozdíl mezi VLCD a napájecím napětím velký. Jinak řečeno pokud například z 1.8V chcete generovat 3.3V. Na několika oscilogramech se pokusím znázornit tvary budicích průběhů pro displej s duty 1/3 a bias 1/3. Bias 1/3 znamená že driver k buzení využívá 4 napěťové úrovně (VLCD, 2/3 VLCD, 1/3 VLCD a GND). To je patrné na žlutém a modrém průběhu. Modrý průběh je napětí na společné elektrodě (COM), žlutý na vybraném segmentu (SEG). To co rozhoduje o kontrastu vybraného segmentu je rozdíl napětí mezi COM a SEG, který je na červeném průběhu (ten nejdůležitější). VLCD je v tomto případě přibližně 2.9V, napěťové úrovně by tedy měly být přibližně 2.9V, 1.9V, 1V a 0V. Všimněte si, že všechny červené průběhy mají nulovou stejnosměrnou složku (což je hlavní důvod proč musí driver vytvářet tak šílené průběhy a nevystačí si se stejnosměrným napětím). Na posledních oscilogramech se můžete podívat jaký vliv má zkracování doby buzení. Komentáře pod obrázky dovysvětlí o co jde.

Červeně dva různé průběhy napětí na neaktivním segmentu. Rozkmit je přibližně 2V, při tak malém napětí není segment aktivovaný a je tedy průhledný.
Dva různé průběhy napětí na aktivním segmentu. Rozkmit je vysoký (přibližně 3V) což aktivuje segment (je neprůhledný - "černý").
Levý oscilogram představuje dobu buzení 50% na aktivním segmentu. Polovinu času drží driver pevné napětí, druhou polovinu času je odpojený a dochází k vybíjení displeje.
Pravý oscilogram znázorňuje buzení zkrácené na méně jak polovinu taktu na aktivním segmentu. Driver drží pevné napětí jen zlomek taktu, což má za následek další snížení kontrastu i spotřeby.

Vraťme se k ovládání driveru. Podle počtu společných elektrod vašeho displeje musíte rozhodnout o "Duty". K dispozici máte čtyři možnosti:

Výběrem Duty jednoznačně rozhodnete které z pinů PA0 až PA3 budou mít roli COM a které zůstanou k obecnému použití. Do jisté míry si můžete vybírat kolik SEG pinů bude přiděleno LCD driveru a kolik jich zůstane volných k jiným účelům. Minimální konfigurace alokuje pro driver piny SEG0 až SEG12, přidávat pak můžete zhruba po dvou pinech až po SEG0 až SEG24. Víc jich totiž na Atmega169 nemáte. Přesný výběr naleznete v tabulce 24-3 v datasheetu. Bias - tedy počet napěťových úrovní budícího průběhu - lze vybrat 1/2 nebo 1/3 (nepočítám li "Static Drive", který by se dal nazvat jako Bias 1/1). K dalším funkcím patří například "Blanking", která uzemní LCD a zhasne ho. Například při vypínání displeje můžete zamezit nevzhlednému postupnému "pohasínání". Zmíním se ještě o dvou funkcích, sloužících ke snížení odběru. První z nich nese název "Low Power Waveform" a vybírá jiný průběh buzení. Detailnější informace datasheet neobsahuje, ale zdá se že snižuje počet změn napětí na LCD a tedy i proudové nároky na nabíjení a vybíjení kapacit displeje. Druhou funkcí sloužící k redukci spotřeby je možnost vypnout výstupní buffery (které udržují napěťové úrovně dané výběrem bias). Ty jsou v čipu vytvořeny pomocí odporových děličů a po vypnutí bufferů jsou děliče připojeny přímo na displej. Nabíjení jejich kapacity pak probíhá déle, což může redukovat kontrast. Tím jsme asi všechny relevantní funkce prošli, zbývá už jen přerušení a jeho vlajka, které může přicházet se začátkem "frame". Pojďme si tedy všechny funkce shrnout spolu s obsahem řídicích registrů

LCDCRA

LCDCRB

LCDFRR

LCDCCR

Do teď jsme ještě nezmínili nic o tom jak driver pozná které segmenty má udržovat aktivované a které ne. Tuto informaci mu předáváte do "LCD Memory", tedy do 20ti registrů LCDDR0LCDDR19. Můžete do nich zapisovat libovolně, driver si kopíruje jejich hodnotu do stínových registrů vždy se začátkem nového "frame". Chcete-li se pojistit proti situaci kdy část displeje zobrazuje "stará" data a část "nová", protože jste zapisovali do "LCD memory" zrovna v okamžiku příchodu nového "frame", můžete si počkat na LCDIF nebo využít přerušení. Jakmile se nastaví víte, že přišel nový frame a máte spoustu času provést zápis. Nejspíš můžete ale celý problém hodit za hlavu, protože je-li frame-rate řádově desítky Hz, nemá lidské oko příliš šance nějaké chybné překreslení postřehnout. Význam jednotlivých bitů v "LCD memory" je přímočarý a logický. Registry LCDDR0LCDDR4 postupně tvoří "pole" 40ti bitů (0 až 39) náležející společné elektrodě COM0. Chcete-li tedy rozsvítit segmenty SEG7 a SEG8 připojené ke COM0, zapíšete log.1 na nejvyšší bit LCDDR0 a nejnižší bit LCDDR1. To je vše. Stav segmentů vázaných na COM1 pak analogicky nastavujete v registrech LCDDR5LCDDR9 a tak dále. Na celé konfiguraci LCD driveru je právě mapování znakové sady na konkrétní segmenty ten největší opruz. Jak na to si ale ukážeme až v příkladu.

Organizace LCD memory (z datasheetu)

Hardware

Než se pustíme do praktických testů podíváme se na hardware. Protože Atmega169A je k dostání jen v SMD provedení, potřebujete na testy nějakou redukci.Můžete ji koupit na Ebay za necelý dolar (hledejte "TQFP adapter") a doosazovat na ni filtrační kondenzátory. Nebo si můžete vlastní improvizovanou desku vyrobit. Já zvolil druhou variantu. Na desce jsem uspořádal všechny vývody vedle sebe. Díky tomu se dá zapojit do kontaktního pole a všechny vývody se dají přehledně popsat. Výrobní podklady ve formátu .sch a .brd (Eagle) lze stáhnout v archivu se zdrojovými kódy.

Testboard pro Atmega169A (s minimální úpravou použitelný i pro jiné Megy v pouzdře TQFP64)

K ukázce využiju displej DE 161. Je k dostání v TME za cca 100kč. Je konstruovaný pro 3V budicí napětí s parametry 1/3 duty a 1/3 bias. Duty 1/3 nám napovídá, že má tři společné elektordy (COM). Skládá se ze 31 segmentů (čtyři 7-segmentové cifry a tři desetinné tečky). Datasheet čísluje COM od jedničky, což se nám nehodí, neboť Atmel má číslování od nuly. Dovolím si tedy ve zdrojovém kódu používat přečíslování a číslovat i COM na displeji od nuly. Geometricky jsou segmenty přiřazeny COM elektrodám podle následujícího obrázku (což vás vlastně nemusí zajímat). Tato konfigurace nám bude nepěkně komplikovat práci při vytváření znakové sady.

Horní segmenty se ovládají přes COM1, střední segmenty přes COM2 a dolní segmenty včetně desetinných teček pomocí COM3 (Číslování COM dle datasheetu displeje).

Displej připojíme k testovací desce podle prvních dvou sloupců následující tabulky. Tabulku pro přehlednost najedete i v souboru lcd.h v archivu s projektem. Cifry displeje čísluji od nuly zprava. Zkratka DP znamená desetinnou tečku. DP1 odpovídá tečce za cifrou č.1 atd. Označování segmentů je obvyklé, a,b,c,d,e,f,g ve směru hodinových ručiček (a také ho najdete v datashetu).

Pin Displeje Pin Atmelu funkce pinu segment displeje
COM0 (Atmel)
segment displeje
COM1 (Atmel)
segment displeje
COM2 (Atmel)
1
2
3
4 PA4 SEG0 3B 3C 3DP
5 PA5 SEG1 2B 2C 2DP
6 PA6 SEG2 1B 1C 1DP
7 PA7 SEG3 0B 0C
8 PA2 COM2
9
10
11 PA0 COM0
12 PG2 SEG4 0A 0G 0D
13 PC7 SEG5 0F 0E
14 PC6 SEG6 1A 1G 1D
15 PC5 SEG7 1F 1E
16 PC4 SEG8 2A 2G 2D
17 PC3 SEG9 2F 2E
18 PC2 SEG10 3A 3G 3D
19 PC1 SEG11 3F 3E
20 PA1 COM1

Příklad 1

První příklad jako klasicky věnujeme základům. Zkusíme rozběhnout displej a zobrazit na něm "běžící čas". Nebudeme brát ohled na spotřebu a nic podobného.
Inicializace driveru - lcd_init() - je jednoduchá a přímočará. Nejprve vymažeme LCD memory, abychom měli jistotu, že se nám při startu nezobrazí nějaký nesmysl. V dalším kroku nastavíme kontrast. Protože zatím neřešíme spotřebu, využijeme vnitřní generátor VLCD a nastavíme ho na 3.2V, dobu buzení nastavíme na maximum. Tím si zajistíme dobrý kontrast za všech podmínek. Obnovovací frekvenci (frame-rate) budeme odvozovat od taktu jádra protože obecně nemusíme mít k dispozici jiné zdroje clocku. Jádro běží na 1MHz a pro kvalitní zobrazení naladíme frekvenci do pásma 40-50Hz. Konkrétně 1MHz/512/7/6 = 46.5Hz. Použitý displej vyžaduje konfiguraci 1/3 duty (3 společné elektrody) a 1/3 Bias. Displej je malý a zabírá pouze 12 vývodů SEG, driveru tedy přidělíme pouze SEG0 až SEG12. Poslední SEG12, bude nevyužitý, ale menší konfiguraci Atmel neumožňuje. Po spuštění začne driver generovat budící průběhy. Na displeji se ale ještě nic zobrazovat nebude. Všechny segmenty jsou v LCD memory deaktivované. Pro lepší čitelnost programu jsem v souboru lcd.h připravil sadu maker, sloužících k ovládání displeje. Vyhneme se tak nutnosti listovat datasheetem a hledat jestli kombinace 0b0101 je napětí 3.2V nebo 3.05V ...

// Základní ovládání LCD - "běžící čas"
#define F_CPU 1000000UL

#include <avr/io.h>
#include <util/delay.h>
#include "lcd.h" // makra zjednodušující práci s LCD

void lcd_init(void);
void disp(uint16_t val);
void lcd_write(uint8_t *characters, uint8_t dots);
uint8_t x[4]={0,0,0,0}; // pole znaků zobrazených na LCD
volatile uint16_t tmp=0; // pomocná proměnná

int main(void){
 lcd_init();
 
 while (1){
  tmp++; // tupé počítadlo, abychom viděli že displej pracuje
  if(tmp>9999){tmp=0;}
  disp(tmp); // zobraz číselnou hodnotu 
  _delay_ms(100);
 }
}

void lcd_init(void){
 // vymažu paměť používaným segmentům
 LCDDR0 = 0;
 LCDDR1 = 0;
 LCDDR5 = 0;
 LCDDR6 = 0;
 LCDDR10 = 0;
 LCDDR11 = 0;
 // doba buzení naplno, napětí VLCD 3.2V - chceme vysoký kontrast
 LCDCCR = LCD_TIME_FULL | LCD_DRIVE_3V20;
 // obnovovací frekvence 1MHz/(512*7*6) = 46.5Hz
 LCDFRR = LCD_PRESC_512 | LCD_DIVIDE_7;
 // Duty 1/3, využity segmenty SEG0 až SEG12
 LCDCRB = LCD_DUTY_3 | LCD_PORTMASK_12;
 // spustit LCD driver (s interním generátorem VLCD)
 LCDCRA = (1<<LCDEN);
}

Obraz na displeji řídí funkce lcd_write(). Jejím argumentem je pole čtyř znaků a úkolem funkce je podle těchto znaků rozhodnout které segmenty aktivovat. Realizuje tedy znakovou sadu. Obecně je tuto znakovou sadu potřeba napsat pro každou cifru zvlášť. Jistý prostor pro nějaké drobné zobecnění tu je, ale najít a napsat ho by bylo asi pracnější než vytvořit znakové sady pro každou cifru samostatně. Jádro funkce je tedy nutné psát podle tabulky zapojení, kterou jste si prohlédli o pár odstavců výše.

Demonstrujeme si tvorbu znakové sady na příkladu jedné cifry. Dejme tomu, že chceme zobrazit znak "3" na nulté pozici. K tomu musíte aktivovat segmenty 0A,0B,0C,0D a 0G.

tímto postupem zpracovává naše funkce všechny cifry (každou testuje na jeden z 10ti znaků). Kdo chce bohatší znakovou sadu například o písmena, bude si muset dát tu práci a doplnit ji. Jakýkoli jiný znak než 0-9 naše funkce pochopí jako prázdný znak. Než abych ve funkci zapisoval přímo do jednotlivých LCDDR, zapíšu vše do dočasných proměnných mem a přepis do LCD memory provedu až na konci funkce, když už mám vše připraveno. Krátký komentář si zaslouží i funkce disp(). Jejím úkolem je převést číselnou hodnotu uint16_t na řetězec čtyř cifer, které poté zobrazujeme.

// převede číselnou hodnotu na dekadické vyjádření (zjistí cifry)
void disp(uint16_t val){
 if (val>9999){val=9999;}
 x[3] = val/1000;   // tisíce
 val = val%1000;
 x[2] = val/100;    // stovky
 val = val%100;
 x[1] = val/10;    // desítky
 x[0] = val%10;    // jednotky
 lcd_write(x,0b001); // zapiš cifry do LCD ram podle znakové sady, desetinnou tečku za 1 cifru
}

// škaredá a důležitá funkce realizující znakovou sadu
// argumentem je pole čtyř bytů se znaky (0 až 9) a byte kódující pozici zobrazovaný desetinných teček
// znak mimo rozsah 0 až 9 vede ke zhasnutí všech segmentů dané cifry
void lcd_write(uint8_t *characters, uint8_t dots){
 uint8_t mem0=0,mem1=0,mem5=0,mem6=0,mem10=0,mem11=0;;

 if(dots & (0b1<<0)){mem10 |= 0b100;}
 if(dots & (0b1<<1)){mem10 |= 0b010;}
 if(dots & (0b1<<2)){mem10 |= 0b001;}
 
 // digit 0
 switch (characters[0]){
  case 0:
  mem0 |= 0b111000;
  mem5 |= 0b101000;
  mem10 |= 0b10000;
  break;
  
  case 1:
  mem0 |= 0b001000;
  mem5 |= 0b001000;
  break;

  case 2:
  mem0 |= 0b011000;
  mem5 |= 0b110000;
  mem10 |= 0b10000;
  break;
  
  case 3:
  mem0 |= 0b011000;
  mem5 |= 0b011000;
  mem10 |= 0b10000;
  break;

  case 4:
  mem0 |= 0b101000;
  mem5 |= 0b011000;

  break;
  
  case 5:
  mem0 |= 0b110000;
  mem5 |= 0b011000;
  mem10 |= 0b10000;
  break;

  case 6:
  mem0 |= 0b110000;
  mem5 |= 0b111000;
  mem10 |= 0b10000;
  break;
  
  case 7:
  mem0 |= 0b011000;
  mem5 |= 0b001000;
  break;
  
  case 8:
  mem0 |= 0b111000;
  mem5 |= 0b111000;
  mem10 |= 0b10000;
  break;
  
  case 9:
  mem0 |= 0b111000;
  mem5 |= 0b011000;
  mem10 |= 0b10000;
  break;
  
 }
  // digit1 ...
  // .... část funkce vynechána ....
 
// nakopíruj konfiguraci do LCD memory 
 LCDDR0 = mem0;
 LCDDR1 = mem1;
 LCDDR5 = mem5;
 LCDDR6 = mem6;
 LCDDR10 = mem10;
 LCDDR11 = mem11;
}

Program sice nic smysluplného nedělá, ale můžete si na něm ověřit zda máte vše správně zapojeno. Navíc si můžete vyzkoušet kontrast při různém napětí VLCD, případně dělat testy s ostatními parametry driveru. Otázka kontrastu je zajímavá, takže jsem pro vás připravil obrázkovou závislost kontrastu na budicím napětí.

Obrázková závislost kontrastu na napětí budícícm napětí (VLCD).

Příklad 2 (Low-power)

V tutoriálech o low-power technikách jsme si zavedli modelový příklad, ve kterém má "Atmel" za úkol hlídat napětí lithiového článku, kontrolovat jeho nabíjení a reagovat na vybití pod kritickou mez. Budeme se této šablony držet i při pokusech s LCD. Naše aplikace bude mít stejný úkol, navíc ale bude zobrazovat napětí akumulátoru na displej. Hodnotu bude obnovovat jedenkrát za sekundu. Zbylý čas bude spát, o probouzení se stará Timer 2 s "hodinovým" krystalem. Díky tomu že provozní napětí lithiového článku leží v pásmu 3-4.2V, můžeme vypnout vnitřní generátor VLCD a použít přímo napětí z baterie. Vypnutím generátoru uspoříme energii, ale ztratíme možnost ovládat kontrast displeje napětím. To ale u většiny displejů nevadí, protože při 3V má většina z nich stále ještě slušný kontrast. Při vyšších napětích (dejme tomu od 3.2V) si dokonce můžeme dovolit snižovat kontrast zkracováním doby buzení a spořit tak další energii. Dále díky tomu že nepoužíváme interní generátor VLCD, smíme čip provozovat na minimálním napětí 1.8V, čímž docílíme dalšího snížení odběru (za předpokladu že máte napěťový stabilizátor s malým provozním proudem). My použijeme stabilizátor MCP1700-1.8V s typickým klidovým proudem 1.6uA. Napájecí napětí budeme měřit na ADC0 přes dělič s odporem 3M (viz schema). Výstup stabilizátoru má slušnou stabilitu, takže poslouží také jako reference pro AD převodník.Ze spánku bude náš čip probouzet Timer 2 s hodinovým krystalem, který použijeme také jako clock pro LCD driver. Displej necháme zapojený stejně jako v předchozí ukázce.

Schema zapojení druhého příkladu. LCD přivádíme přímo ze vstupního napětí (z baterie), čip běží na napětí 1.8V.

Napětí stabilizátoru jsem změřil přesně voltmetrem a vložil do programu jako konstantu VREF. Stejně tak jsem přesně stanovil poměr děliče (makro DELIC). Tím jsem si připravil vše potřebné pro výpočet napájecího napětí. Při pokojové teplotě se zobrazovaná hodnota držela s chybou do +-10mV (vůči multimetru) v rozsahu vstupních napětí 2.6 až 4.3V. Podívejme se ale na funkce, které proti prvnímu příkladu přibyly. Funkce nastav_kontrast() řidí dobu buzení podle napájecího napětí. Skupina maker K0K5 rozděluje napětí baterie do sedmi intervalů, makra Kx_KONTRAST pak specifikují pro každý interval dobu buzení. Má-li baterie vyšší napětí než 3.6V, volím nejkratší dobu buzení, protože má minimální spotřebu a kontrast je i tak slušný. Jak napětí klesá, funkce zjišťuje do kterého ze zvolených pásem napětí spadá a postupně zvyšuje dobu buzení. Dorovnává tak pokles kontrastu s cílem udržet displej čitelný (za cenu drobného zvyšování spotřeby). Jak napětí klesá pod 2.9V, klesá kontrast i při plné době buzení. To je daň za to, že nepoužíváme interní regulátor. Napětí lithiového článku by se ale do těchto hodnot nemělo dostat.

Funkce operation() je jen drobně upravená z tutoriálu low-power II. Takže ji popíšu jen ve stručnosti. Nastartuje ADC, zahájí AD převod s využitím režimu spánku. Po čtyřech převodech, AD převodník vypne, zprůměruje výsledky a spočítá napájecí napětí. Poté zkontroluje stav nabití a indikuje ho na pinech PE0 a PE1. Nakonec zavolá funkci na korekci kontrastu. Po jejím dokončení přechází čip opět do hlubokého spánku. Funkce lcd_init() konfiguruje driver. Vybere clock z hodinového krystalu (asynchronní Timer 2 už běží). Nastaví vnější signál jako zdroj pro VLCD, Frame rate konfigurujeme jako 32768/(16*8*6) = 42.7Hz. Ostatní konfigurace je stejná jako v předchozím příkladu. Ze zdrojového kódu jsem si dovolil vypustit lcd_write(), protože se od předchozího příkladu nemění. Archiv s celým projektem si můžete stáhnout.

// B) Napájení čipu přes 1.8V stabilizátor, napětí baterie měřeno přes dělič 1/3 (3Mohm + 100nF), reference pro ADC z VCC (1.8V)
// VLCD připojeno přímo na zdroj energie (lithiový akumulátor) - nevyužívá vnitřní generátor VLCD (úspora energie - kolísá kontrast, částečně jej aplikace kompenzuje)
#define F_CPU 1000000UL // frekvence v aktivním režimu

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <avr/power.h> // k zapínání/vypínání periferií
#include <avr/sleep.h> // funkce režimu spánku
#include "lcd.h" // makra pro snadnější ovládání LCD + mapa zapojení LCD
#define BLANK 0xff; // prázdný znak na LCD

#define VREF 1802 // [mV] napětí stabilizátoru (reference pro ADC) 
#define DELIC 2.9889 // dělicí poměr 3M děliče z napětí akumulátoru
#define VCC_K (uint16_t)(round(VREF*DELIC)) // konstanta pro výpočet napětí akumulátoru
#define BEZNY_KONTRAST LCDCCR = LCD_TIME_70 // běžný kontrast 
#define K0_KONTRAST LCDCCR = LCD_TIME_150 // kontrast v intervalu (K1,K0)
#define K1_KONTRAST LCDCCR = LCD_TIME_450 // kontrast v intervalu (K2,K1)
#define K2_KONTRAST LCDCCR = LCD_TIME_575 // kontrast v intervalu (K3,K2)
#define K3_KONTRAST LCDCCR = LCD_TIME_850 // kontrast v intervalu (K4,K3)
#define K4_KONTRAST LCDCCR = LCD_TIME_HALF // kontrast v intervalu (K5,K4)
#define K5_KONTRAST LCDCCR = LCD_TIME_FULL // maximální kontrast (pro napětí pod K5)
// Makra určující napěťová pásma pro zvolené kontrasty
#define K0 3600
#define K1 3400 
#define K2 3300 
#define K3 3200 
#define K4 3100 
#define K5 2900 // napětí pro zvýšení kontrastu na maximum
#define PREPETI 4200 // napětí nabitého akumulátoru
#define PODPETI 3000 // napětí vybitého akumulátoru
#define NABITO_ON PORTE |= (1<<PE0) // indikuje plně nabitý akumulátor
#define NABITO_OFF PORTE &=~(1<<PE0)
#define VYBITO_ON PORTE |= (1<<PE1) // indikuje vybitý akumulátor
#define VYBITO_OFF PORTE &=~(1<<PE1)

void lcd_init(void);
void tim2_init(void);
void init_adc(void);
void disp_volt(uint16_t val);
void operation(void);
void nastav_kontrast(uint16_t vcc);
void lcd_write(uint8_t *characters, uint8_t dots);
uint8_t x[4]={0,0,0,0}; // obsah LCD displeje
uint16_t volt; // hodnota napájecího napětí, globální, je k dispozici dalším funkcím

int main(void){
 ACSR |= (1 << ACD); // vypneme analogový komparátor aby nežral
 power_all_disable(); // vypneme všechny periferie aby nežraly
 ADMUX = (1<<REFS0); // připojíme k ADC referenci AVCC (aby se měl čas kondenzátor na AREF nabít) 
 tim2_init(); // spustíme 32.768kHz asynchronní oscilátor a zdroj periodického probouzení
 power_lcd_enable(); // pustíme šťávu do LCD driveru
 lcd_init(); // konfigurujeme a spouštíme LCD driver
 DDRE |= (1<<DDE0) | (1<<DDE1); // nastavujeme indikační výstupy
 DDRF &=~(1<<DDF0); // PF0 (ADC0) jako vstup 
 DIDR0 = ADC0D;  // vypneme na ADC0 vstupní buffer
 sei(); // povolíme přerušení
 
 while (1){
  set_sleep_mode(SLEEP_MODE_PWR_SAVE); // chceme spát hluboce
  sleep_mode(); // dobrou noc ...
  operation(); // probouzíme se a provádíme "smysluplnou" činnost
 }
}

// nastavuje "kontrast" LCD podle napájecího napětí a volených konstant
void nastav_kontrast(uint16_t vcc){
 if(vcc>K0){BEZNY_KONTRAST;}
 else if(vcc>K1){K0_KONTRAST;}
 else if(vcc>K2){K1_KONTRAST;}
 else if(vcc>K3){K2_KONTRAST;}
 else if(vcc>K4){K3_KONTRAST;}
 else if(vcc>K5){K4_KONTRAST;}
 else{K5_KONTRAST;}    
}

// hlavní činnost naší aplikace
void operation(void){
 uint16_t tmp=0; // tady budeme ukládat výsledky ADC převodu
 uint8_t i; // pomocná proměnná
 
 power_adc_enable(); // zapnout ADC
 ADMUX = (1<<REFS0) | 0; // reference AVCC (1.8V), vstup ADC0
 // Prescaler /4 (1MHz/4 = 250kHz pro ADC), povolit přerušení, vyčisti vlajku, spustit ADC
 ADCSRA = (1<<ADPS1) | (1<<ADIE) | (1<<ADIF) | (1<<ADEN);
 set_sleep_mode(SLEEP_MODE_ADC); // připravíme se na "ADC noise reduction" spánek
 // průměrujeme výsledek ze 4 měření
 for(i=0;i<4;i++){
 sleep_mode(); // uspáním se automaticky spustí AD převod
 tmp = tmp+ADC; //.. jakmile se probudíme akumulujeme výsledek převodu
 } 
 ADCSRA = 0; // vypneme ADC
 power_adc_disable(); // odpojíme ADC od šťávy
 
 tmp=tmp>>2; // dokončíme průměrování (dělení 4mi)
 volt=((uint32_t)tmp*VCC_K)/1024; // spočítáme napětí akumulátoru (v mV)
 disp_volt(volt/10); // zobrazíme napětí na LCD (v desítkách mV)
 
 // zkontrolujeme stav akumulátoru a signalizujeme ho 
 if(volt>PREPETI){NABITO_ON;}else{NABITO_OFF;} 
 if(volt<PODPETI){VYBITO_ON;}else{VYBITO_OFF;}
 nastav_kontrast(volt); // nastavíme kontrast podle napětí
}

// zobrazí napětí na LCD, napětí v jednotkách 10mV
void disp_volt(uint16_t val){
 if (val>9999){val=9999;} // nesmyslně vysoká čísla saturujeme
 x[3] = val/1000; // řád tisíců
 if(x[3]==0){x[3]=BLANK;} // nulu zobrazovat nechceme
 val = val%1000;
 x[2] = val/100; // řád stovek
 val = val%100;
 x[1] = val/10; // řád desítek
 x[0] = val%10; // řád jednotek
 lcd_write(x,0b010); // zapíšeme zjištěné cifry do paměti LCD driveru
}

// přerušení od Asynchronního timeru 2 (1Hz) 
ISR(TIMER2_COMP_vect){
 asm("nop"); // budíme se z hlubokého spánku, je čas změřit napájecí napětí
}

// přerušení od dokončeného AD převodu
ISR(ADC_vect){
 asm("nop"); // jen se probudíme - převod dokončen
}

// konfigurace asynchronního timeru
void tim2_init(void){
 ASSR = (1<<AS2); // přepínám časovač 2 do asynchronního režimu
 OCR2A = 31; // budeme generovat 1Hz (f_timeru/1Hz - 1 = 32/4 - 1 = 31)
 TCCR2A = (1<<WGM21) | (1<<CS22) | (1<<CS21) | (1<<CS20); // spustit timer s clockem 32.768kHz/1024 = 32Hz
 // počkat až se konfigurace zapíše do registrů timeru (!!!)
 while((ASSR & (1<<OCR2UB)) || (ASSR & (1<<TCR2UB))){};
 TIFR2 = (1<<OCF2A); // vyčistit vlajku
 TIMSK2 |= (1<<OCIE2A); // povolit přerušení od compare události (stropu) 
}

void lcd_init(void){
 // vymažu paměť používaným segmentům
 LCDDR0 = 0;
 LCDDR1 = 0;
 LCDDR5 = 0;
 LCDDR6 = 0;
 LCDDR10 = 0;
 LCDDR11 = 0;
 // nastavím kontrast, napětí vnitřního regulátoru nenastavuji 
 LCDCCR = BEZNY_KONTRAST;
 // clock pro driver 32768Hz/16/8 = 256Hz (frame = 256/6 = 43Hz)
 LCDFRR = LCD_PRESC_16 | LCD_DIVIDE_8; 
 // Bias 1/3, duty 1/3, využity segmenty 0-12, Asynchronní clock (z TIM2)
 LCDCRB = LCD_DUTY_3 | LCD_PORTMASK_12 | (1<<LCDCS);
 // spouštím LCD driver s externím zdrojem VLCD 
 LCDCRA = (1<<LCDEN) | (1<<LCDCCD); 
}

Výsledky

Spotřebu odhaduji stejnou metodikou jako v tutoriálu Low-power II, tedy z doby kdy se 3.3mF kondenzátor vybije ze 4.2V na 3V (o čemž nás informuje testovaný čip na pinech PE0 a PE1). V tomto případě aplikace vydržela 349s, což odpovídá průměrné spotřebě 11.3uA. Zahrneme-li 20% toleranci kapacity kondenzátoru můžeme říct že by měla ležet v pásmu 9.1-13.6uA. Pro srovnání výdrž takové aplikace z klasického 2Ah lithiového článku (16850) je okolo 20ti let, tedy odběr je nejspíš nižší jak samovybíjení článku. Přirozeně se nabízí několik způsobů jak dále snížit odběr. Lze vyzkoušet různé "low-power" módy driveru. Dále se dá zapracovat na děliči napětí (systematický odběr přes 1uA), lze ho například připojovat tranzistorem až před měřením. V takovém případě se nabízí využít výstup OC2 ovládaný timerem 2. Ten může dělič aktivovat ještě před probuzením čipu a dát mu čas k nabití filtračního kondenzátoru C8. Další prostor se najde v optimalizaci programu.

Změřená výdrž naší aplikace. Sestupná hrana PE0 značí dosažení 4.2V, vzestupná hrana PE1 pak dosažení kritických 3V.
Fotografie sestavené ukázky. Šedý drát vedoucí z rohu čipu, je signál VLCD přiváděný z "vnějšku" (z kondenzátoru).

Závěrem

Obávám se, že se mi nepodařilo všechny informace seřadit v tom nejlepším pořadí, i tak ale doufám že máte představu o možnostech řízení LCD přímo Atmelem. Řekl bych, že potenciál pro využití v praxi je veliký a doufám, že se někdo z vás odhodlá toto řešení nasadit. Těším se u dalších dílů (uf asi si dám pauzu).

Odkazy a zdroje

Home
V1.01 6.9.2018
By Michal Dudka (m.dudka@seznam.cz)