Tento krátký tutoriál je reakcí na pravidelně se vynořující dotaz "Jak regulovat jas mnoha LED?". Já samozřejmě nejsem původním autorem prezentované myšlenky a už ani netuším kde jsem na ni narazil. Ale utkvěla mi v hlavě protože je svým způsobem elegantní. "Hobbysté" (a nejen oni) čas od času řeší různé vizuální aplikace při kterých potřebují řídit jas mnoha LEDkám nebo LEDpáskům a naráží u toho na problém nedostatku časovačů, či obecně "PWM výstupů" mikrokontroléru. Jistě existuje celá řada řešení a jejich volba se odvíjí od různých požadavků počínaje požadovanou obnovovací frekvencí, přes rozlišení ("barevnou hloubku") až po počet kanálů. Prezentované řešení cílí na situace kdy stačí 8-9ti bitové rozlišení jasu s obnovovací frekvencí do stovek Hz a jeho výhodou jsou nulové nároky na hardware (jen s pomocí MCU).
Jistě máte představu jak funguje klasická PWM regulace. Na nějaký zlomek periody aktivujete výstup (rozsvítíte LED) a na zbytek periody ji vypnete. Frekvenci blikání zvolíte tak vysokou aby ji lidské oko nepostřehlo a LED pak působí dojmem trvalého svitu se zvoleným jasem. Dokud se o tuto činnost stará hardware (tedy typicky časovač) je všechno jednoduché a regulace může být velmi jemná (např 16bit). Jakmile vám ale časovače a jejich kanály dojdou a pokusíte se stejný postup vyřešit softwarově vyvstane před vámi nepříjemný problém. Musíte si periodu rozdělit na 256 intervalů (při 8bitovém rozlišení) a na začátku každého z nich si zkontrolovat zda je některou LED potřeba zhasnout (případně rozsvítit). To klade velké nároky na software. Například při frekvenci 150Hz musí mikropočítač 150*256 = 38400 krát za sekundu obsloužit LEDky a vybrané kanály zhasínat/rozsvěcet. Pokud obslužná funkce trvá dejme tomu 4us vytěžuje tato činnost CPU z více jak 15% (1s/(4us*38400)). Se zvětšováním opakovací frekvence nebo rozlišení se situace rapidně zhoršuje (10bit rozlišení vede k 61% vytížení CPU). Tento přístup není optimální.
Elegantnějším řešením je "roztrhnout" pulz na 8,9 nebo 10 menších (podle potřebného rozlišení). Například při 8bitovém rozlišení rozdělíme jednu periodu na osm časových intervalů s šířkami 1/256, 2/256, 4/256, 8/256 až 128/256. Pro každou LED se pak vyberou intervaly tak aby jejich celkový součet dával takovou hodnotu která by odpovídala šířce při klasickém PWM. Názorněji to bude vidět na příkladu:
Pro názornost si dovolím předvést realizaci na oscilogramech. Na prvním oscilogramu jsou žlutou stopou vyznačené začátky intervalů. A modrá stopa znázorňuje průběh odpovídající intenzitě 10/255. Můžete vidět, že následující interval je vždy dvakrát širší než předchozí. A také to, že se sekvence intervalů stále opakuje. Také si můžete všimnou, že intenzita 10/255 se realizuje rozsvícením LED na interval odpovídající 2/255 periody a pak 8/255 periody. Na dalším oscilogramu jsou pak znázorněny postupně všechny intenzity jasu od 1/255 do 10/255.
Obecně asi pro každý způsob realizace PWM pomocí softwaru bude hlavním problémem přesné časování. Každá chyba v časování se může promítnout do jasu. A tím víc čím menší jas zrovna realizujeme. Představme si, že náš (špatně) napsaný program někdy jednu nebo dvě mikrosekundy přidá. Pak se může stát, že při nízkém jasu (kdy LED svítí třeba jen v 32us intervalu) kolísá jas o 2/32, tedy o více jak 6%. To lidské oko jistě postřehne a bude to působit rušivě. Při psaní programu je na to potřeba dbát. Nepříjemná komplikací jsou přerušení. Představme si co se stane když zrovna v okamžiku kdy má náš program zhasnout LED tak přijde přerušení od UARTu a program se bude několik mikrosekund zabývat zpracování dat ... a LED bude svítit i když už má být zhasnutá. Zvláště na starších AVR, kde nelze přerušení prioritizovat, to může být nepříjemný problém.
Jako platformu pro ukázku jsem zvolil MCU Atmega4809. K realizaci časových intervalů jsem využil časovač TCB0. Kdykoli časovač přeteče, vyvolá rutinu přerušení a program v ní jednak přepne stavy LED a taky přepíše strop časovače na novou hodnotu. Díky tomu časovač postupně volá přerušení s časovým odstupem odpovídajícím našim intervalům. Abych se vyvaroval problémů s nepřesným časováním, nastavil jsem přerušení od časovače vyšší prioritu. Takže nedojde k žádnému zdržení i kdyby MCU vykonával jiná přerušení. MCU taktuji maximální frekvencí (20MHz) a intervaly jsem volil od horní části rozsahu časovače a jsou uloženy v poli ccmp_table. Opakovací frekvence tak vyšla na přibližně 153Hz.
Pro názornou demonstraci stačí 8 kanálů (celý PORTD) a případná úprava na více kanálů je triviální. Jak už jsme řekli, přesné časování vyžaduje rychlou odezvu. Aplikace si tedy předem napočítá stavy PORTD ve všech časových intervalech (funkce calculate_mask). Pomocný výstup na PA7 slouží jen jako indikace časových intervalů a taky pomůže udělat si představu o trvání rutiny přerušení. Pro samotnou funkci není potřeba. Vlajka refresh_flag označuje okamžik kdy začalo časování nejdelšího intervalu. To je totiž ten správný okamžik na to přepočítat intenzity na stavy PORTD. Ideálně by tento přepočet měl proběhnout před dokončením intervalu. Pokud se to nestihne tak hrozí, že se stavy PORTD změní během generování a na výstupech budou vznikat glitche (což se projeví jako problikávání a kolísání jasu). V kritickém případě lze tuto funkci "schovat" do rutiny přerušení (na místo kde se nastavuje refresh_flag), ale není to optimální. Rutina trvá relativně dlouho (~200us) během kterých nebude MCU schopné vykonávat ani jiné rutiny přerušení (naše má nejvyšší prioritu) a to může být průser.
// Demo "roztříštěné PWM" s 8bit hloubkou a 8mi kanály (PORTD) #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> // Intenzity (LED) pro jednotlivé kanály (PD0 to PD7) uint8_t intensity_table[8]={ 1, // 0.4% 2, // 0.8% 33, // 12.9% 66, // 25.8% 100, // 39.1% 150, // 58.6% 200, // 78.1% 250, // 97.7% }; volatile uint8_t out_table[8]={0,0,0,0,0,0,0,0}; // stavy výstupů (PORTD) v časových intervalech volatile uint8_t refresh_flag=0; // vlajka značící vhodný okamžik pro změny intenzit a přepočítání stavů výstupů const uint16_t ccmp_table[8]={512,1024,2048,4096,8192,16384,32768,65535}; // časové intervaly freq=20MHz/součet = 153.19Hz void calculate_mask(void); void init_timer(void); int main(void){ _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB,0); // Taktujeme na maximum (20MHz příp. 16MHz) PORTD.DIRSET = 0xFF; // Celý PORTD nastavujeme jako výstup (našich 8 kanálů) PORTA.DIRSET = PIN7_bm; // PA7 je indikační výstup pro osciloskop CPUINT.LVL1VEC = TCB0_INT_vect_num; // Přerušení od Timeru TCB0 má vysokou prioritu sei(); // globální povolení přerušení set_sleep_mode(SLEEP_MODE_IDLE); // volím režim spánku (Idle) init_timer(); // nastavit a spustit timer while (1){ if(refresh_flag){ // generujeme nejdelší interval, je čas přepočítat nové hodnoty intenzity calculate_mask(); // přepočítej stávající intenzity na stavy výstupů v čas.intervalech refresh_flag=0; sleep_enable(); // vlajka ošetřena, můžeme zase uspávat } sleep_cpu(); // uspi CPU (nejde o spotřebuje, ale snižuje jitter) } } // Přerušení od TCB0 - kritické časování ISR(TCB0_INT_vect){ static uint8_t idx=0; // index procházející časové intervaly PORTA.OUTSET = PIN7_bm; // pomocná indikace pro osciloskop PORTD.OUT = out_table[idx]; // zapsat odpovídající stav na výstupy TCB0.CCMP = ccmp_table[idx]; // nastavit nový strop časovači (nový interval) TCB0.INTFLAGS = TCB_CAPT_bm; // vyčistit vlajku idx++; // posunout se na další interval (pro příště) if(idx>=8){ // pokud realizujeme poslední interval idx=0;// začneme příště od začátku refresh_flag=1; // dáme vědět hlavní smyčce že by měla aktualizovat "zobrazovaná" data sleep_disable(); // zakážeme usínání, abychom měli jistotu že program zpracuje vlajku } PORTA.OUTCLR = PIN7_bm; // pomocná indikace pro osciloskop } // inicializace timeru zodpovědného za řízení jasu LED void init_timer(void){ TCB0.CCMP = ccmp_table[0]; // načteme první periodu TCB0.CTRLB = TCB_CNTMODE_INT_gc; // režim timeru přerušení po přetečení TCB0.INTCTRL |= TCB_CAPT_bm; // povolíme přerušení z timeru TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // volím clock F_CPU (20MHz) a spouštím timer } // přepočítá hodnoty jasu na stavy výstupů v jednotlivých časových intervalech void calculate_mask(void){ volatile uint8_t i,j; for(j=0;j<8;j++){ // prochází časové intervaly for(i=0;i<8;i++){ // prochází výstupní kanály if(intensity_table[i] & 1<<j){ // pokud má být i-kanál v j-časovém intervalu zapnut... out_table[j] |= 1<<i; // ...nastavíme mu na výstupní kód 1 (rozsvíceno) }else{ out_table[j] &=~(1<<i); // ...nebo 0 (zhasnuto) } } } }
Když si pozorně prohlédnete oscilogramy, můžete si všimnout, že interval kdy má LED svítit není vždy úplně přesně stejný. Občas je o jeden (ale může to být i víc) cyklů delší nebo kratší. V naší ukázce trvá strojový cyklus 50ns a z oscilogramu níže je patrné, že se šířka náhodně o jeden cyklus mění. To je naštěstí v našem případě zanedbatelně malá nepřesnost (nejkratší interval má u nás 25.6us, a 50ns z něj tvoří jen 0.2%). Chytrá hlava na Avrfreaks.net (sparrow2) poznamenala, že při probouzení ze spánku bude jitter menší neboť se eliminuje situace kdy přerušení přijde uprostřed více-cyklové instrukce (kterou musí CPU nejprve dokončit než přejde do rutiny přerušení). Proto v ukázce čip uspávám. Nejde tedy o redukci spotřeby. V naší ukázce to asi nebude mít praktický vliv. Když ale budete intervaly zkracovat (třeba pro dosažení vyššího rozlišení), vliv tohoto jevu poroste.
Protože timeru přepisujeme strop za chodu. Je potřeba dbát na to abychom přepis provedli rychle. Rychleji než uplyne čas, na který časovač nastavujeme. Jinak se v našem případě (TCB0) vystavujeme riziku vzniku velmi nepříjemného glitche (časovač bude muset přetéct přes své maximum 65536). Případně se můžeme pojistit tím že timer po změně stropu ještě vynulujeme. Kritické je to zejména při nastavování nejkratšího intervalu naší sekvence. Z pohledu na poslední oscilogram se zdá, že by naše aplikace zvládla ještě jeden a možná i dva bity rozlišení navíc nebo dvakrát až čtyřikrát zvednou obnovovací frekvenci.
Home
| V1.00 5.12.2021 /
| By Michal Dudka (m.dudka@seznam.cz) /