Displeje z tekutých krystalů (LCD) stojí ve stínu klasických LED zobrazovačů (lidově "sedmisegmentovek"). Je to překvapivé hlavně proto že LCD mají své nesporné výhody. A není to jen nízká spotřeba. LCD displeje excelují v prostředí s přímým slunečním svitem protože na rozdíl od LED neztrácí kontrast. Pominout nejde ani jejich designová funkce, mnohdy prostě vypadají lépe. Mám za to že jejich řidší použití pramení hlavně z "komplikovanějšího" řízení. No a proto si uděláme malou exkurzi k STM32L1 abychom se přesvědčili, že je to hračka.
Než se do toho pustíme, raději vám ve stručnosti představím na co se podíváme. Nebudeme se do detailu zabývat teorií řízení LCD, uděláme si jen zběžný exkurz. Předvedeme si jak konkrétní displej připojit k STM32L, nakonfigurujeme si driver a zmíníme se o jeho funkcích. Mrkneme se jak vypadají napěťové průběhy které displej řídí. Ukážeme si jak z hlediska programátora displej ovládat a celé to složíme do jednoduchého programu s rozumně malým odběrem. Takže pokud máte čtvrthodinku času a zajímá vás to, čtěte !
Jádro problému s buzením je v tom že na LCD smíte přivádět pouze střídavý signál s nulovou stejnosměrnou složkou. Stejnosměrný proud by totiž prováděl nevratný elektrolytický rozklad displeje. Pro displeje s jednou společnou elektrodou to není v podstatě žádná komplikace. Na řídicí elektrodu se přivádí obdélníkový průběh se střídou 50%, na vývody připojené k jednotlivým segmentům se pak přivádí buď stejný signál (napětí proti společné elektrodě je nulové a segment je "zhasnutý") a nebo invertovaný průběh (napětí proti společné elektrodě je střídavě kladné a záporné a segment je "aktivní"). Tento styl buzení se nazývá "static drive". Problém přichází až v situaci kdy má displej, kvůli úspoře vývodů, více společných elektrod. Pak už si nevystačíte s buzením pomocí dvou potenciálů (tedy například 5V a 0V). Podle počtu společných elektrod pak potřebujete typicky 3 nebo 4 různá napětí. K tomu využíváte dalších vlastností LCD. Kontrast segmentu závisí nelineárně na efektivní hodnotě střídavého napětí, který mezi něj a jeho společnou elektrodu přivádíte. Do určité hodnoty můžete střídavé napětí zvyšovat aniž by to vedlo k "aktivaci" segmentu. Klíčem tedy je udržet efektivní hodnotu napětí u "zhasnutého" segmentu pod určitou hodnotou a u "aktivovaného" segmentu nad jinou hodnotou. Samotný průběh napětí už může být libovolně divoký (a uvidíte že bude). Tento způsob buzení se nazývá "dynamic drive".
Existují postupy jak zvládnout dynamické řízení jen s pomocí GPIO a externích rezistorů (odkazy jsou ke konci článku), ale proč se tak trápit když jsou k sehnání mikrokontroléry s driverem. Což přirozeně není výsada jen STM (namátkou třeba Atmega169). LCD drivery najdete na čipech STM32L100/152/162, dále na čipech L053/073/083 a L496/476/433 (více o tom zde, zde a zde). My se podíváme na čip STM32L100RC, protože je k dostání na nejlevnějším Discovery kitu.
Krátce o schopnostech driveru na STM32L100. Maximální počet segmentů může být až 4x32 (tedy 4 společné elektrody, každá se 32 segmenty) nebo 8x28 (8 společných elektrod, každá s 28 segmenty). V dynamickém režimu umí budit displeje vyžadující 1/2,1/3,1/4 bias (umí tedy generovat až 5 úrovní napětí). Přirozeně zvládá i práci ve statickém režimu. Driver obsahuje vlastní step-up měnič, takže může pracovat nezávisle i z nízkého napájecího napětí. Výstup měniče lze nastavovat v osmi krocích v rozsahu 2.6V až 3.6V a lze tedy řídit i kontrast. Zajímavou funkcí je i blikání vybraných segmentů (případně celého displeje). Vše může běžet "na pozadí" a v režimech spánku.
Pojďme ke konkrétní aplikaci. Předvedeme si jak připojit a rozběhnout takový "obyčejný" displej. Přibližně za dolar je v číně k dostání 6ti místný segmentový displej GDC0570 s výškou znaku cca 8mm. Díky tomu že obsahuje symbol dvojtečky můžete ho použít i ke zobrazování času. Symbol desetinné tečky zase umožňuje zobrazovat různé veličiny, napětím počínaje a teplotou konče. Užitečně se jeví i malý symbol stavu baterie. V dolní části výkresu je rozpis vývodů. Displej má 4 společné elektrody (COM1 až COM4) a 13 vývodů pro řízení celkem 48 segmentů. Prodejce uvádí, že je 3V a má být řízen dynamicky s 1/3 Bias (tedy pomocí 4 úrovní napětí).
V datasheetu nalistujeme tabulku "Alternate function" a ve sloupečku AFIO11 máme informaci o LCD funkcích každého pinu. Piny PA8,PA9,PA10 a PB9 mají funkce COM0 až COM3. K nim je potřeba připojit COM vývody displeje. Piny s označením SEGx se pak připojují k vývodům jednotlivých segmentů a můžete je připojit libovolně. Nezáleží tedy příliš na tom zda využijete SEG23 nebo SEG11 atd. Piny PC10,11,12 a PD2 mají trojí funkci. Mohou pracovat jako společné elektrody COM4 až COM7 pro buzení dispejů s 8mi COM vývody. Nebo mohou mít funkci segmentů SEG28 až SEG31. Smysl třetí funkce (SEG40 až SEG43) mi zůstává utajen, protože ať děláte co děláte nikdy víc jak 4x32 segmentů neodřídíte. Asi je to relikt z čipů řady 152, kde lze díky většímu počtu vývodu řídit displeje do rozměru až 4x44. Nám bude tato funkce jen zavazet. Důležitou funkci má pin VLCD. K němu je přiveden výstup step-up měniče a je potřeba ho filtrovat kondenzátorem 1uF (na Discovery kitu je vše zařízeno). Komu to situace umožní, může na tento pin přivést vnější napětí a měnič nepoužít. Připojení displeje je tedy velice jednoduché, spojíte COM elektrody a segmenty připojíte kam vám to zrovna vyjde. Já se snažil připojit displej k "pravé" straně kitu. Záměrně jsem ale jeden segment (č.5 na displeji) přivedl na pin PA1 (SEG0). Zařídil jsem si tak možnost využít "blikací" funkci na dvojtečce. Funkce blikání totiž může zjednodušeně řečeno blikat buď na SEG0 nebo s celým displejem.
Driver sdílí clok s RTC modulem a může využívat buď:
void lcd_init(void){ LCD_InitTypeDef lcd; // struktura s konfigurací LCD RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // clock pro PWR blok PWR_RTCAccessCmd(ENABLE); // povolit přístup do RTC domény (potřebujeme LSI) RCC_LSICmd(ENABLE); // spustit LSI (26-56Khz ~37kHz, naše situace cca 42kHz) while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET){} // počkat na rozběh LSI RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); // zvolit clock pro RTC a LCD z LSI RCC_APB1PeriphClockCmd(RCC_APB1Periph_LCD,ENABLE); // spustit clock do LCD lcd.LCD_Bias = LCD_Bias_1_3; // "3" hodnoty napětí (Vcc, 2/3 Vcc, 1/3 Vcc, GND) lcd.LCD_Duty = LCD_Duty_1_4; // 4 COM elektrody lcd.LCD_Prescaler = LCD_Prescaler_16; // clock pro LCD = ~37kHz / 16 = ~2.3kHz lcd.LCD_Divider = LCD_Divider_18; // takt buzení segmentů ~128Hz (měřeno. ~145Hz) lcd.LCD_VoltageSource = LCD_VoltageSource_Internal; // napětí z interního step-up měniče LCD_Init(&lcd); // aplikovat konfiguraci LCD_ContrastConfig(LCD_Contrast_Level_3); // výstupní napětí step-up nastavit na 3V LCD_PulseOnDurationConfig(LCD_PulseOnDuration_1); // budit displej ze začátku periody větším proudem LCD_MuxSegmentCmd(ENABLE); // Remap funkcí SEG40-SEG43 na SEG28-SEG31 LCD_WaitForSynchro(); // počkat až se zápis do registrů LCD provede LCD_Cmd(ENABLE); // spustit LCD driver while(LCD_GetFlagStatus(LCD_FLAG_ENS) == RESET){} // počkat na start LCD while(LCD_GetFlagStatus(LCD_FLAG_RDY) == RESET){} // počkat na rozběh Step-Up měniče }
Nejspíš jste se pozastavili nad funkcí LCD_PulseOnDurationConfig(). Driver má pro generování budicích napětí integrované děliče. Každý dělič (pro každý bias režim) je v čipu dvakrát. Jednou sestaven z rezistorů s vysokým odporem (cca 7MOhm) a jednou s nižším odporem (cca 240kOhm). Pro nízký odběr je vhodnější napětí generovat děličem s vyšším odporem. Některé displeje ale mají větší kapacity a ty by se skrze tento dělič nemusely stihnout nabít a napětí na segmentech displeje by pak nemělo potřebný průběh (a klesal by kontrast). V takovém případě je možné na volitelný zlomek doby připojovat "tvrdší" dělič z menších odporů. Dobu připojení "tvrdého" děliče lze volit jako počet period signálu za prescalerem. Náš displej má relativně malé kapacity a tak mu stačí nejkratší možný interval.
Další komentář si zaslouží funkce LCD_MuxSegmentCmd(). Ta remapuje segmenty 40-43 na 28-31. Nejde vlastně o nic jiného než o remap v paměti, který nám drobně usnadní následné řízení displeje. Funkce LCD_WaitForSynchro() čeká až dojde k zápisu do LCD. LCD totiž běží na jiném clocku než jádro. Samozřejmě kromě konfigurace LCD je potřeba provést konfiguraci GPIO. S tím předpokládám nebudete mít problém a tak si ji dovolím přeskočit (všechny využité piny jsou Alternate Function s AF11).
Než přejdeme k dalšímu programovému ovládání LCD, podíváme se jak vypadají napětí budicí náš displej. Nejprve si prohlédneme průběh na COM elektrodách. Každý "frame" trvá čtyři takty a střídá se sudý a lichý. Přísně vzato je perioda průběhu na každém COM rovna 8 taktům. Jak vzápětí uvidíte, napětí na segmentech se bude opakovat každé 4 takty. Nepěkné "spiky" vznikají při nabíjení kapacit displeje z "napěťového děliče" driveru. Napětí Vcc a GND tím netrpí, neboť jsou dodávána přímo z napájení, tedy z velmi tvrdého zdroje. Vidět můžete všechny čtyři úrovně napětí Vcc, 2/3Vcc, 1/3Vcc a GND.
Teď si prohlédneme napěťový průběh pasivního ("zhasnutého") segmentu. Modrý průběh odpovídá napětí na COM elektrodě, Červený průběh je napětí na segmentu displeje (SEGx). Rozdíl těchto dvou napětí tvoří budicí napětí pro příslušný segment a můžete si jej prohlédnout na spodním trávově zeleném průběhu (COM - SEGx). Všimněte si že má signál střídu 50% (nutnost) a že jeho amplituda nepřekračuje 2.2Vpp. Při takto nízké hodnotě je segment deaktivován a je tedy "zhasnutý".
Na řadě je aktivní segment. Modrý průběh je opět napětí na COM a zelený průběh je napětí na aktivním segmentu (SEGy). Poslední, trávově zelený, graf je rozdíl napětí mezi COM a SEGy (tedy napětí které "cítí" vybraný segment). A můžete si všimnou, že amplituda je přibližně 5.9Vpp. Efektivní hodnota napětí je pak nadlimitní a segment je aktivován ("rozsvícený").
Pominu-li funkci blikání tak se zobrazování řídí pouze obsahem LCD_RAM. Každý COM vývod driveru má přidělených 44bitů paměti (ve dvou 32bitových blocích). Protože čip může využívat až 8 COM vývodů, máme tedy 16x32bit paměťových buněk. Zápisem log.1 říkáte driveru, že daný segment na daném COM vývodu má být aktivní, log.0 pak, že má být "zhasnutý". Organizaci paměti (pro první 4 COM) si můžete prohlédnout na obrázku dole. Segmenty SEG32 až SEG39 nejsou na našem čipu vůbec přítomny, SEG39 až SEG43 máme remapovány na SEG28 až SEG31. Díky tomu pro nás horní polovina paměťového bloku každého segmentu ztrácí smysl. Zápis do RAM se provádí skrze buffer. Jakmile zapíšete vše potřebné, zavoláte funkci LCD_UpdateDisplayRequest(). Pak se spustí proces přepisu o jehož ukončení se můžete dozvědět vlajkou UDD (Update Display Done), která může i vyvolat přerušení. Během tohoto procesu by nemělo být možné do RAM zapisovat.
V knihovně jsou paměťové buňky označeny LCD_RAMRegister_0 až LCD_RAMRegister_15 podle jejich pořadí v paměti. Komplikace nastanou v okamžiku kdy se rozhodnete vytvořit "znakovou sadu". My používáme 4 COM segmenty (COM0 až COM3). Díky tomu že jsme "remapovali" segmenty 40-43 na 28-31, stačí nám na každý COM paměť o velikosti 32bit. Našich celkově 48 segmentů na displeji je různě rozesetých do 4*32 = 128 bitů. Pojďme si tedy nejprve prohlédnou co je v našem případě kam připojené.
/* Displej GDC0507 připojení ke kitu * * Common map: * disp(pin) stm32(pin) * COM1(17) COM3 (PB9) * COM2(16) COM2 (PA10) * COM3(15) COM1 (PA9) * COM4(14) COM0 (PA8) * * segment map (vztahujeme k STM32): * PIN(stm): PB3 PB4 PB5 PB13 PB14 PB15 PB8 PC6 PA1 PC10 PC11 PC12 PD2 * AF: SEG7 SEG8 SEG9 SEG13 SEG14 SEG15 SEG16 SEG24 SEG0 SEG28 SEG29 SEG30 SEG31 * PIN(disp): 10 11 12 1 2 3 13 4 5 6 7 8 9 * COM0: 5D -- 6D T4 1D -- -- 2D COL1 3D -- 4D DOT1 * COM1: 5E 5C 6E T3 1E 1C 6C 2E 2C 3E 3C 4E 4C * COM2: 5G 5B 6G T2 1G 1B 6B 2G 2B 3G 3B 4G 4B * COM3: 5F 5A 6F T1 1F 1A 6A 2F 2A 3F 3A 4F 4A * * SUMMARY: * digit 1: SEG14 + SEG15 * digit 2: SEG24 + SEG0 * digit 3: SEG28 + SEG29 * digit 4: SEG30 + SEG31 * digit 5: SEG7 + SEG8 * sigit 6: SEG9 + SEG16 * col1: SEG25 * dot1: SEG31 * battery: SEG13 */
V horní části tabulky je rozpis jak jsou spojeny COM displeje s COM driveru. Prostřední (největší tabulka) mapuje spojení segmentů displeje s SEG vývody driveru. Pojmenování jednotlivých segmentů displeje si můžete prohlédnout v "datasheetu" k displeji (na začátku článku). Chceme-li tedy aktivovat dejme tomu segmenty 6A, 6B a 3G musíme zapsat do paměti pro COM3 a COM2. V COM2 (tedy v LCD_RAMRegister_4) musíme aktivovat bity SEG16 a SEG28. V COM3 (LCD_RAMRegister_6) aktivujeme bit SEG16. Tento postup musíte nějak algoritmizovat a to je asi to nekomplikovanější co vás při použití LCD čeká. Nemám odvahu pokoušet se popsat svůj postup. Snažil jsem se u něj zachovat si "znakovou sadu" v podobě kterou lze snadno měnit.
// pozice COM pro segmenty v pořadí ABCDEFGH (viz "segment map") const uint8_t com_key[8]={3,2,1,0,1,3,2,0}; // pozice cifer(digitů) v LCD RAM (viz "segment map") const uint32_t digit_segment[6][2]={ {1<<14,1<<15}, {1<<24,1<<0}, {1<<28,1<<29}, {1<<30,1<<31}, {1<<7, 1<<8 }, {1<<9, 1<<16} }; // znaková sada const uint8_t charset[12]={ // segmenty: HGFEDCBA 0b00111111, // 0 0b00000110, // 1 0b01011011, // 2 0b01001111, // 3 0b01100110, // 4 0b01101101, // 5 0b01111101, // 6 0b00000111, // 7 0b01111111, // 8 0b01101111, // 9 0b00000000, // blank 0b01000000 // minus sign }; void display(uint8_t symbol, uint8_t* char_array){ uint32_t com[4]={0,0,0,0}; // budoucí obsah RAM LCD driveru uint8_t charmask,i=0,j; // tento blok se prostě nedá okomentovat :D jde o to přepsat "segment map" do RAM for(j=0;j<6;j++){ // pro každou číslici charmask=charset[char_array[j]]; for(i=0;i<3;i++){ if(charmask & 1<<i){ com[com_key[i]] |= digit_segment[j][1]; } } for(i=3;i<8;i++){ if(charmask & 1<<i){ com[com_key[i]] |= digit_segment[j][0]; } } } if(symbol & BAT_0){com[3] |= 1<<13;} // symbol "obalu baterie" T1 (na COM3 SEG13) if(symbol & BAT_1){com[0] |= 1<<13;} // "jedna čárka" T4 if(symbol & BAT_2){com[1] |= 1<<13;} // "druhá čárka" T3 if(symbol & BAT_3){com[2] |= 1<<13;} // "třetí čárka" T2 if(symbol & DOT){com[0] |= 1<<31;} // desetinná tečka if(symbol & COLON){com[0] |= 1<<0;} // dvojtečka // jestli se ještě nestihl dokončit poslední zápis do LCD RAM tak počkej while(LCD_GetFlagStatus(LCD_FLAG_UDR) != RESET){} // zápis do RAM LCD_Write(LCD_RAMRegister_0,com[0]); LCD_Write(LCD_RAMRegister_2,com[1]); LCD_Write(LCD_RAMRegister_4,com[2]); LCD_Write(LCD_RAMRegister_6,com[3]); // vyřídit blikání segmentu if(symbol & BLINK){LCD_BlinkConfig(LCD_BlinkMode_SEG0_COM0,LCD_BlinkFrequency_Div128);} else{LCD_BlinkConfig(LCD_BlinkMode_Off,LCD_BlinkFrequency_Div128);} LCD_UpdateDisplayRequest(); // pokyn k přepisu bufferu v LCD }
Funkci display() v prvním argumentu specifikujeme vzhled "speciálních" symbolů jako je dvojtečka, desetinná tečka nebo symbol baterie. Makra s argumenty, které je možné kombinovat, najdete na začátku zdrojového kódu. Druhým argumentem je pole 6ti znaků/cifer, které má funkce zobrazit. Znaková sada obsahuje krom cifer i prázdný znak (10) a znaménko minus (11). Zápis do LCD RAM provádí funkce LCD_Write(). Funkci blikání lze aktivovat na SEG0 na COM0 (jeden blikající segment), dále na SEG0 na všech COM (bliká až 8 segmentů podle počtu použitých COM) a nebo pro celý displej. Frekvenci blikání lze volit a odvíjí se od "frame frequency". Přirozeně ve své aplikaci si napíšete zobrazovací funkci podle vlastních představ (a počítám že elegantněji).
Práci s displejem máme za sebou, teď si poskládáme ukázkový program. V první řadě musíme nějak nastavit clock čipu. Protože naše ukázka nemá nic zásadního na starosti a jen tupě počítá, můžu si dovolit čip podtaktovat. Nemám v plánu zabývat se do hloubky low-power technikami (protože jim sám pořádně nerozumím), takže jen stručně. STM32L100 má MSI (Multispeed internal RC oscillator), jehož frekvenci lze snadno přepínat v rozsahu 65kHz - 4MHz. Naše aplikace bude využívat nejnižšího kmitočtu 65.536kHz a ještě zvolí prescaler na SYSCLK /2, takže jádro poběží na frekvenci 32.768 KHz. Díky tomu že nevyužíváme vysokých frekvencí, můžeme snížit napětí pro jádro a konfigurujeme vnitřní LDO na 1.2V. Tím se sice připravíme o možnosti využívat ADC a DAC, ale to nám teď nevadí a získáme další úsporu energie. Protože nepoužíváme mnoho periferií a máme malý odběr, můžeme napěťový regulátor přepnout do "Low Power Run" módu a zase něco uspořit. To všechno provedeme ve funkci pwr_init()
void pwr_init(void){ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // clock pro PWR funkce RCC_HCLKConfig(RCC_SYSCLK_Div2); // cock z MSI / 2 = 32.768kHz RCC_PCLK2Config(RCC_HCLK_Div1); // obě APB sběrnice s frekvencí SYSCLK RCC_PCLK1Config(RCC_HCLK_Div1); RCC_MSIRangeConfig(RCC_MSIRange_0); // 32.768 KHz PWR_VoltageScalingConfig(PWR_VoltageScaling_Range3); // jádro na 1.2V (přicházíme o ADC a DAC) while(PWR_GetFlagStatus(PWR_FLAG_VOS) != RESET){} // počkat na změnu napětí // s malým odběrem si můžeme dovolit pustit regulátor v "LowPower" režimu a snížit tak jeho odběr PWR_EnterLowPowerRunMode(ENABLE); }
Abychom uspořili další energii přepneme všechny nevyužité piny do režimu "Analog input" a vypneme jim clock. Tuto operaci provádí funkce lowpower_gpio() a všimněte si že než abych přemýšlel nad tím které piny nepoužívám, tak je "vypnu" všechny a později během inicializace LCD si zapnu ty které potřebuji. Tímto krokem přijdete i o piny přidružené k SWD rozhraní, tedy jinak řečeno přijdete o "debug". A vzhledem k tomu, že SWD má spotřebu značně převyšující spotřebu naší aplikace, tak je to vcelku logický krok. Po konfiguraci necháme aplikaci běžet a provádět vcelku hloupý úkon. Pomocí TIM7 časujeme přibližně 1s interval (čas se odvíjí od nepříliš přesného MSI) a s každým přetečením timeru inkrementujeme číslo na displeji.
Odběr celé aplikace v chodu je přibližně 32uA a nepochybuji o tom že ho půjde ještě skoro o řád, ale já netuším jak :) Důkazem nechť je fotografie údaje z ampérmetru (1 dílek, 10uA).
Nekladl jsem si za cíl cokoli z problematiky LCD probrat vyčerpávajícím způsobem. Přirozeně mi šlo jen o to si to všechno vyzkoušet a celý tento návod je jen takový (časově náročný) boční produkt. I přes to doufám že vás to do jisté míry osvěžilo a že snad někoho inspiruji vybavit svou aplikaci "LCDčkem". Celý zdrojový kód ukázky si můžete stáhnout zde. Těším se s Vámi na shledanou u dalších návodů.
Home
V1.01 8.4.2018
By Michal Dudka (m.dudka@seznam.cz)