V tomto tutoriálu se seznámíme s obrysy timeru TCA a vyzkoušíme tři příklady.
Časovače a čítače (dále timery a countery) jsou na moderních AVR úplně nové. A zdá se, že o poznání dokonalejší než na starých AVR. Jsou rozděleny na typy A,B,D a RTC. Každý má trochu jiné schopnosti, o nichž se postupně dozvíme v dalších dílech. V tomto si vezmeme na mušku Timer Counter A (TCA). Dalo by se říct, že je to takový tradičnější, univerzální, typ timeru/counteru. Je 16bitový, má tři plnohodnotné compare kanály (jistě máte představu co "compare" kanály jsou ze starších AVR) a možnost nastavení "stropu". Jak compare kanály, tak strop mají "double buffer", takže při generování průběhů (typicky PWM) nehrozí žádné glitche (o nichž bude řeč). Krom tří plnohodnotných PWM zvládá generovat i obdélníkové průběhy (tzv. frequency generation. K této schopnosti se vrátíme až se seznámíme s event systémem. Přerušení lze volat od přetečení a všech tří compare událostí. Ve spolupráci s event systémem dokáže čítat signály z libovolného pinu a nejen z něj ale třeba taky z komparátoru nebo jiného timeru/counteru). V případě potřeby ho jde rozdělit na dva 8bitové čítače, obětovat tak rozlišení a zdvojnásobit si množství PWM kanálů na 6. V tomto režimu ale ztrácí spoustu svých dobrých vlastností (o čemž se přesvědčíme ve druhém příkladu). Jeho ovládání je vcelku jednoduché a osobně se domnívám, že jednodušší jak na starých AVR.
V prvním příkladu si předvedeme jak pomocí TCA generovat "PWM" signál pro modelářské servomotorky. "PWM" píšu záměrně v uvozovkách, neboť servo čte šířku pulzu, nikoli střídu (duty cycle). Servu posíláte kladný (5V) pulz s šířkou v rozmezí 1-2ms s opakovací frekvencí mezi 20-200Hz (typicky 50Hz). Pro většinu serv platí, že pulz o šířce 1.5ms je pokyn pro přesun do "středové" polohy, 1ms (resp. 2ms) pulz pak servo interpretuje jako pokyn natočit se do jedné (resp. druhé) krajní polohy. Rozlišení serva většina datasheetů charakterizuje výrazem dead-band a pohybuje se běžně mezi 5-8us. Udává minimální změnu šířky pulzu nutnou k tomu aby se servo pohnulo ze své pozice. My přirozeně chceme abychom mohli měnit šířku s menším nebo alespoň se stejným krokem. S běžnou opakovací frekvenci 50Hz je perioda pulzů 20ms. A jestliže tvoříme PWM s periodou 20ms a s rozlišením 5us, potřebujeme k tomu alespoň 4000 kroků, tedy nejméně 12bit rozlišení. Nějakou daň v podobě rozlišení ale zaplatíme ještě za to abychom periodu timeru nastavili na žádoucích 50Hz, takže 16bit timer je ideální.
Postupně si projdeme ovládací registry a bity s komentářem k čemu slouží. Některé specializované ovládací prvky si dovolím pro srozumitelnost vynechat. Registry mají dvojí funkci, podle toho zda timer používáme jako 16bit, nebo jako 2x8bit. V 16bitovém mód jsou zde:
Na naší tině (a pokud vím na všech ostatních čipech) je k dispozici jen jeden TCA s označením TCA0. Používáme-li TCA v 16bit režimu přistpujeme k jeho registrům a bitům skrze makra s předponou TCA0_SINGLE_ nebo skrze struktury TCA0.SINGLE.. V 8bit režimu naopak využíváme makra TCA0_SPLIT_ nebo struktury TCA0.SPLIT.
Zadání prvního příkladu jsme už probrali, takže se vrhneme na jeho realizaci. Nejprve vyřídíme otázku clocku (se kterým jste se jistě seznámili v předchozím díle). Čip tradičně taktujeme na 20MHz a dáme si záležet na přesné frekvenci. Chybná frekvence bude mít za následek chybné šířky pulzů a tím pádem i chybné polohy serva. Odchylka 1% ve frekvenci timeru se projeví ve stejné míře i v poloze serva a záleží na aplikaci zda lze tuto chybu zanedbat či ne. Chceme-li co nejlepší rozlišení signálu pro servo, snažíme se taktovat timer co nejvyšší frekvencí a volit jeho strop jako co nejvyšší hodnotu. Omezuje nás volba předděličky CLKSEL a potřeba udržovat frekvenci přibližně 50Hz. Pro náš případ vychází jako nejvhodnější dělicí poměr /8. S ní je frekvence timeru 2.5MHz (20MHz/8). Rozlišení je pak 0.4us a maximální perioda, kterou lze ještě vytvořit je 65536*0.4us = 26.2ms (38Hz). Krok 0.4us má dostatečný odstup od "dead-band" běžného serva. Pro 50Hz zvolíme strop timeru na hodnotu 50000 protože 50k*0-4us = 20ms (50Hz). Vychozí šířka pulzu pro serva bude 1.5ms (neutrální poloha) což v našem případě odpovídá hodnotě 1.5ms/0.4us = 3750. Signál pro servo budeme v příkladu řídit funkcí, jejímž argumentem bude hodnota v rozsahu 0 až 1000 (tedy něco jako promile z rozsahu). K demonstraci využijeme všechny tři PWM kanály, aplikace tedy může řidit tři serva. Projděme si tedy zdrojový kód.
/* tutorial TinyAVR 1-Series * timer/Counter A (TCA) - 3x PWM pro servo * Ch.0 na PB0 (WO0) * Ch.1 na PB1 (WO1) * Ch.2 na PB2 (WO2) */ /* Ve fuses zvolen 20MHz oscilátor (lze volit ještě 16MHz) */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #define PERIOD (50000-1) // perioda pro 50Hz (timer clock F_CPU/8=2.5MHz, 2.5MHZ/50000 = 50Hz) #define DEFAULT_PULSE (3750-1) // výchozí šířka pulzu 3750*0.4us = 1.5ms - servo ve středové poloze void clock_20MHz(void); void tca_init(void); void set_servo(uint8_t channel, uint16_t val); // nastavuje PWM na vybraném kanále uint16_t x=0; // na hraní int main(void){ PORTB.DIRSET = PIN0_bm | PIN1_bm | PIN2_bm; // PB0,1,2 výstupy timeru (WO0,1,2) clock_20MHz(); // taktujeme na 20MHz a snažíme se o přesnost tca_init(); // inicializace timeru while (1){ while(x<(1000-10)){ // dokud lze pulz protahovat... x=x+10; // ...protáhneme ho set_servo(0,x); // nastavíme PWM na novou šířku _delay_ms(60); // chvíli počkáme } while(x>10){ // dokud lze pulz zkracovat... x=x-10; // ...zkrátíme ho set_servo(0,x); // nastavíme PWM na novou šířku _delay_ms(60); // chvíli počkáme } } } // poloha serva 0-2 v rozsahu 0 - 1000 void set_servo(uint8_t channel, uint16_t val){ if(val>1000){val=1000;} // ochrana vstupu před nežádoucí hodnotou // trocha matematiky, převod 0 až 1000 na 1 - 2ms timeru (perioda timeru = 0.4us) se správným zaokrouhlováním // vstupem do PWM je hodnota mezi 2500 - 5000 (2500*0.4 = 1000us, 5000*0.4 = 2000us) if(channel==0){TCA0.SINGLE.CMP0BUF = (val*10+2)/4 + 2500 - 1;} if(channel==1){TCA0.SINGLE.CMP1BUF = (val*10+2)/4 + 2500 - 1;} if(channel==2){TCA0.SINGLE.CMP2BUF = (val*10+2)/4 + 2500 - 1;} } void tca_init(void){ TCA0.SINGLE.PER = PERIOD; // perioda timeru (20ms) TCA0.SINGLE.CMP0 = DEFAULT_PULSE; // výchozí šířka PWM TCA0.SINGLE.CMP1 = DEFAULT_PULSE; TCA0.SINGLE.CMP2 = DEFAULT_PULSE; TCA0.SINGLE.CTRLC = 0; // pro jistotu vynulovat výstupy timeru (kdo ví jaká je tam hodnota) // Povolit ovládání výstupu WO0,WO1,WO2, vybíráme režim Single slope PWM TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_CMP1EN_bm | TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // volíme clock pro timer jako F_CPU/8 (2.5MHz) a povolujeme/spouštíme timer TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV8_gc | TCA_SINGLE_ENABLE_bm; } // Nastaví clock 20MHz (z interního 20MHz bez děličky) s využitím kalibrační hodnoty void clock_20MHz(void){ // v případě potřeby zde dočasně vypněte přerušení CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLA = CLKCTRL_CLKSEL_OSC20M_gc; // vybírá 20MHz oscilátor CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLB = 0; // vypne prescaler (děličku) CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.OSC20MCALIBA = 0x99; // zapíše novou hodnotu kalibračního registru (zjištěnou empiricky) }
Nejprve si piny PB0 až PB2 nastavím jako výstupy a taktuji čip na 20MHz. Nastavení timeru provádím ve funkci tca_init(). Po startu čipu je timer vypnutý a já si můžu zápisem do PER nastavit strop timeru. Protože timer počítá od nuly, musím naši spočtenou hodnotu stropu (50000) zmenšit o jedničku. Zapisuji přímo do registru PER a nevyužívám buffer, protože chci strop změnit hned. Podobně zapíšu spočtené výchozí hodnoty (3750) do všech compare registrů (a nezapomenu zmenšit o jedničku). Po startu timeru se tak do serv dostanou signály pro pohyb do neutrální polohy. Zápisem 0 do registru CTRLC ještě pro jistotu vynuluji výstupy timeru. Zde to asi nemá význam, ale pokud by timer před tím běžel a někdo ho zastavil v okamžiku kdy jsou jeho výstupy v neznámém stavu, hrozilo by že se timer rozběhne a během první periody bude na výstupech "binec". Následně timeru povolím ovládat všechny tři své výstupy. Od tohoto okamžiku má nad nimi kontrolu timer a já už je nemůžu ovládat skrze registr PORT. Dokud je ale timer vypnutý, mohu je ovládat skrze registr TCA0.CTRLC. Spolu s povolením výstupů zvolím režim timeru na "Single slope" - tedy běžné PWM, při kterém timer počítá od 0 do stropu. Při rozběhu z nuly (update událost) nastaví svůj výstup do log.1 a při compare události (shoda CMP registru s obsahem čítače) výstup vynuluje. Kdybych to potřeboval, mohl bych směr čítání změnit bitem DIR, ale teď k tomu nemám důvod. Kdo by potřeboval PWM invertovat, vrátí se do kapitoly o GPIO a v příslušném registru PINnCTRL nastaví bit INVEN. Inverze výstupu se tedy neprovádí v timeru, ale v ovládání portu. Nakonec stačí v registru CTRLA nastavit clock a spustit timer bitem ENABLE.
Změnu PWM provádím funkcí set_servo(uint8_t channel, uint16_t val). První argument - channel volí který kanál chceme nastavovat, druhý argument val volí hodnotu kterou danému servu chceme poslat. Funkce na začátku kontroluje zda není zadaná šířka pulzu mimo rozsah. Ten nemusí být striktně limitován na 0-1000. U čínských serv je rozsah pohybu kus od kusu individuální, někdy je to 0.9-1.1ms, jindy nesymetricky 1-1.2ms atd. Některá chytřejší serva zase umožňují rozsah vstupů od 0.75 do 2.25ms a podobně. Pokud je budete chtít použít, musíte si funkci trochu upravit. Po kontrole vstupu přijde trocha matematiky. Hodnotu 0-1000 potřebuji remapovat na rozsah 2500 až 5000. Jedna krajní poloha odpovídá 1ms/0.4us = 2500 a druhá krajní poloha 2ms/0.4us = 5000. Nejprve potřebuji vstupní hodnotu (val) dělit 0.4. S celými čísly to musím provést nepřímo, tedy nejprve násobit 10ti a poté dělit 4mi. Při dělení celého čísla ale dochází k "zaokrouhlování" směrem dolů a to není hezké. Například 7/4 vyjde 1 a já bych byl raději za korektnější 7/4 = 2. Proto musím před dělením přičíst polovinu dělitele. Násobím tedy 10krát, poté přičtu 2 (polovina dělitele) a nakonec dělím 4mi. To by mi mělo zajistit matematicky správné dělení 0.4. Po tom všem musím signál ještě o 1ms (tedy o 2500) posunout. Tím je veškerá matematika hotová. Výsledek nezapisuji přímo do registru CMP ale do "bufferu" CMPnBUF. Timer tedy nejprve dokončí generování aktuální periody ještě se starou hodnotou. Pak přeteče (vznikne update událost) a do CMP registru se zapíše moje nová hodnota z "bufferu" a následující periodu už timer generuje pulz s novou šířkou. Kdybych zapsal přímo do CMP, mohl bych to udělat v nevhodný okamžik a zapříčinit tak vznik glitche. Servo by se pak na okamžik pokusilo rozjet do nové - nesmyslné polohy, což by mohlo mít katastrofální důsledky.
Program nechávám otáčet servem postupně tam a zpátky (jen pro demonstraci). Z následujících oscilogramů se můžeme přesvědčit, že příklad funguje podle očekávání.
Ve druhém příkladu si budeme demonstrovat 8bitový režim TCA a budeme ovládat 6 PWM kanálů. Posloužit mohou třeba k buzení dvou RGB LED, nebo jako improvizované DA převodníky. Určitě ale najdete nespočet dalších aplikací ve kterých shledáte 8bit režim využitelný. TCA se v 8bit režimu rozděluje na "horní" (High) a "dolní" (Low) timer a má dost redukované schopnosti. Neumí pracovat jako counter, pouze jako timer. Nemá "double buffer", takže při změnách frekvence nebo PWM budou na výstupech čas od času nepěkné "glitche" (ukážeme si). Oba timery sdílí jeden clock, ale jinak mohou pracovat nezávisle. Projdeme si opět ovádací registry:
Teď se pojďme podívat na náš příklad. Po programu budeme chtít aby generoval šest 8bitových PWM kanálů. Budeme chtít aby regulace PWM pracovala v plném rozsahu, tedy od 0% do 100%. Samotný PWM režim to neumožňuje, takže si budeme muset pomoct malou fintou. TCA v 8bit režimu vždy čítá směrem dolů, takže PWM průběhy budou zarovnány "doprava" což nás v případě buzení LED nebo RC článků vůbec nezajímá. Program necháme na 5ti kanálech držet pevnou hodnotu PWM a na jednom kanále budeme PWM stále dokola zmenšovat.
Než se do toho pustíme objasníme si jak vzniká "glitch". Náš timer počítá od 255 směrem dolů k 0 a pak začne znovu. Jakmile je hodnota counteru rovna hodnotě komparačního registru, nastane compare událost a výstup timeru se nastavuje do log.1. Jakmile timer dopočítá do 0, jde výstup zpět do log.0. Glith nastane když přepíšeme compare registr na vyšší hodnotu než je aktuální stav čítače dřív než nastane compare událost. Například pokud je aktuální CMP=20, čítač CNT=100 (tedy compare ještě nenastala) a my přepíšeme CMP na 150, tak už v této periodě compare událost nenastane ! Vznikne interval log.0 delší jak jedna perioda - glitch.
Jestliže budete compare registr měnit naprosto náhodně bude pravděpodobnost vzniku glitche tím větší čím je aktuální hodnota compare registru menší a na čím větší hodnotu ho měníme. Tím déle totiž trvá výše zmíněná, nebezpečná, podmínka. Nejhorší je tedy změna z 1 na 255. Chcte-li problému částečně předcházet, tak změnu compare registru provádějte až po compare události. Pokud je clock pro timery dost pomalý a pokud vám nevadí před změnou PWM čekat na vhodnou událost, jde glitchům prakticky zamezit. S vyšší je frekvencí timeru na to budete mít méně času. Neošetřitelná je například změna hodnoty z 1 na 255 s timerem běžícím na maximální frekvenci. Protože bezpečná doba kdy lze compare měnit trvá jen dva tiky timeru (i CPU). Já jsem demonstrační příklad tímto ošetřením nekomplikoval. Pojďme si tedy prohlédnout zdrojový kód.
/* tutorial TinyAVR 1-Series * timer/Counter A (TCA) - 6x 8bit PWM * Ch.0 na PB0 (WO0) * Ch.1 na PB1 (WO1) * Ch.2 na PB2 (WO2) * Ch.3 na PA3 (WO3) * Ch.4 na PA4 (WO4) * Ch.5 na PA5 (WO5) */ /* Ve fuses zvolen 20MHz oscilátor (lze volit ještě 16MHz) */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #define PERIOD 0xFF // strop timeru 255 - plné 8bit rozlišení timeru #define DEFAULT_PULSE 127 // výchozí střída PWM je 50% void clock_20MHz(void); void tca_init(void); void set_pwm(uint8_t channel, uint16_t val); uint16_t x=256; // jen na hraní s hodnotou PWM int main(void){ // konfigurujeme výstupy timeru PORTB.DIRSET = PIN0_bm | PIN1_bm | PIN2_bm; PORTA.DIRSET = PIN3_bm | PIN4_bm | PIN5_bm; clock_20MHz(); // taktujeme na 20MHz tca_init(); // inicializace timeru // prvotní nastavení střídy pro všechny kanály set_pwm(0,32); set_pwm(1,64); set_pwm(2,96); set_pwm(3,128); set_pwm(4,160); set_pwm(5,192); while (1){ // stále dokola snižujeme střídu na kanále 0 if(x>0){x--;}else{x=256;} set_pwm(0,x); _delay_ms(20); } } /* funkce, nastavující střídu PWM signálů * argument channel - vybírá kanál který chceme nastavit ( 0 - 5 ) * argument val vybírá hodnotu PWM ( 0 - 256 ), kde 0 je 0%, 256 je 100% */ void set_pwm(uint8_t channel, uint16_t val){ if(channel==0){ // zjišťujeme který kanál máme měnit if(val>255){ // pokud je hodnota 256 a vyšší, chceme 100% střídu (tedy trvalou log.1) PORTB.PIN0CTRL |= PORT_INVEN_bm; // invertujeme výstup timeru... TCA0.SPLIT.LCMP0 = 0; // ...a nastavíme střídu 0% (výstup tedy bude trvale v log.1) }else{ // jinak chceme na výstupu běžné PWM PORTB.PIN0CTRL &=~PORT_INVEN_bm; // výstup timeru tedy nemá být invertovaný TCA0.SPLIT.LCMP0 = val; // a v compare registru má být zvolená hodnota } } else if(channel==1){ // pro všechny kanály je postup stejný jako pro ch.0 if(val>255){ PORTB.PIN1CTRL |= PORT_INVEN_bm; TCA0.SPLIT.LCMP1 = 0; }else{ PORTB.PIN1CTRL &=~PORT_INVEN_bm; TCA0.SPLIT.LCMP1 = val; } } else if(channel==2){ // pro všechny kanály je postup stejný jako pro ch.0 if(val>255){ PORTB.PIN2CTRL |= PORT_INVEN_bm; TCA0.SPLIT.LCMP2 = 0; }else{ PORTB.PIN2CTRL &=~PORT_INVEN_bm; TCA0.SPLIT.LCMP2 = val; } } else if(channel==3){ // pro všechny kanály je postup stejný jako pro ch.0 if(val>255){ PORTA.PIN3CTRL |= PORT_INVEN_bm; TCA0.SPLIT.HCMP0 = 0; }else{ PORTA.PIN3CTRL &=~PORT_INVEN_bm; TCA0.SPLIT.HCMP0 = val; } } else if(channel==4){ // pro všechny kanály je postup stejný jako pro ch.0 if(val>255){ PORTA.PIN4CTRL |= PORT_INVEN_bm; TCA0.SPLIT.HCMP1 = 0; }else{ PORTA.PIN4CTRL &=~PORT_INVEN_bm; TCA0.SPLIT.HCMP1 = val; } } else if(channel==5){ // pro všechny kanály je postup stejný jako pro ch.0 if(val>255){ PORTA.PIN5CTRL |= PORT_INVEN_bm; TCA0.SPLIT.HCMP2 = 0; }else{ PORTA.PIN5CTRL &=~PORT_INVEN_bm; TCA0.SPLIT.HCMP2 = val; } } } void tca_init(void){ TCA0.SPLIT.CTRLA = 0; // vypneme timer, kdyby běžel nešel by korektně přepnout do 8bit režimu TCA0.SPLIT.CTRLESET = TCA_SPLIT_CMD_RESET_gc; // reset který nastaví registry timeru do výchozí hodnoty TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; // přepneme timer do 8bit režimu TCA0.SPLIT.CTRLC = 0; // vynutíme si nastavení výstupů timeru do log.0 (asi není nutné když jsme timer resetovali) // povolíme všech 6 výstupů timeru (kontrola nad WO0 až WO5) teď patří timeru TCA0.SPLIT.CTRLB = TCA_SPLIT_HCMP0EN_bm | TCA_SPLIT_HCMP1EN_bm | TCA_SPLIT_HCMP2EN_bm | TCA_SPLIT_HCMP0EN_bm | TCA_SPLIT_LCMP0EN_bm | TCA_SPLIT_LCMP1EN_bm | TCA_SPLIT_LCMP2EN_bm; TCA0.SPLIT.HPER = PERIOD; // nastavujeme strop Htimeru TCA0.SPLIT.LPER = PERIOD; // nastavujeme strop Ltimeru TCA0.SPLIT.HCMP0 = DEFAULT_PULSE; // nastavujeme postupně výchozí hodnoty PWM TCA0.SPLIT.HCMP1 = DEFAULT_PULSE; TCA0.SPLIT.HCMP2 = DEFAULT_PULSE; TCA0.SPLIT.LCMP0 = DEFAULT_PULSE; TCA0.SPLIT.LCMP1 = DEFAULT_PULSE; TCA0.SPLIT.LCMP2 = DEFAULT_PULSE; // spustíme oba timery s clockem F_CPU/8 TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV8_gc | TCA_SPLIT_ENABLE_bm; } // Nastaví clock 20MHz (z interního 20MHz bez děličky) void clock_20MHz(void){ // v případě potřeby zde dočasně vypněte přerušení CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLA = CLKCTRL_CLKSEL_OSC20M_gc; // vybírá 20MHz oscilátor CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLB = 0; // vypne prescaler (děličku) }
Ve funkci tca_init() provedeme konfiguraci timeru. Nejprve ho pro jistotu vypneme (CTRLA=0). Pak zadáme příkaz k resetu, čímž nastavíme všechny registry do výchozích hodnot. Teprve potom můžeme timer přepnout do 8bit režimu. Také jsme mohli využít toho že po startu jsou regitry ve výchozích hodnotách a přepnout režim hned. Potom pro jistotu vynulujeme výstupy timeru (i když nad samotnými vývody z čipu nemá timer ještě kontrolu). Tu mu přidělíme hned další instrukcí. Od teď jsou výstupy WO0 až WO5 řízeny timerem (nebo nepřímo programem skrze CTRLC - tedy pokud je timer vypnutý). Pak přijde serie 8mi instrukcí kdy nastavujeme strop na maximum (255) a PWM na výchozí hodnoty (50% střídu). Nakonec timer pustíme bitem ENABLE a zvolíme clock F_CPU/8.
Protože jsem chtěl z PWM vytáhnout maximální rozlišení, nastavil jsem strop timeru na maximální hodnotu, tedy 255. Protože střída PWM nemůže dosáhnout 100%, musíme si pomoct malou oklikou. Pro dosažení 100% střídy ji nastavíme na 0% (výstup timeru trvale v log.0) a příslušný pin invertujeme (tím pádem na něm bude trvale log.1). Invertujeme ho, jak už víme z předchozích vílů, bitem INVEN v příslušném registru PINnCTRL. Tuto proceduru provádíme ve funkci set_pwm(). V hlavní smyčce jen pro zábavu snižujeme každých 20ms střídu PWM. Na oscilogramu níže si můžete všimnou nepěkného glitche při změně ze 100% hodnoty o jednu níže (důsledek toho, že 100% střídu realizujeme pomocí 0%).
Poslední příklad, kde se asi moc nového nedozvíte, je vlastně je jen takový drobeček co mi zbyl z prvních pokusů. Jde v něm o to udělat primitivním způsobem jednoduchou animaci pozvolného rozsvěcení a zhasínání LED. Nabíhání a zhasínání není rovnoměrné ale běží podle tzv error funkce (z hlediska programátora dosti nevhodný název). Průběh jasu je uložen v podobě 36ti hodnot v paměti flash. Stiskem tlačítka (PB4) pozvolna rozsvítíte nebo zhasnete LED na PB5. Tlačítko je řešeno klasicky, tedy pomocí vnitřního pull-up rezistoru. LED na modulku je netradičně připojená proti VCC, takže se rozsvěcí log.0. Abychom si s tím nemuseli lámat hlavu, výstup invertujeme (v PORTB.PORT5CTRL). Z hlediska programu pak LED rozsvěcíme log.1. Na pin s LEDkou si ještě musíme remapovat výstup timeru (WO2). Protože hodnoty jasu (střídy PWM) v tabulce jsou v rozsahu 1 až 255, nastavíme si strop timeru jako 255. Plného svitu (100% střídy) dosáhnme vypnutím timeru a ručním ovládáním pinu. Zhasnutí realizujeme podobně ačkoli by to nebylo potřeba, protože 0% PWM náš timer zvládne. Pin můžete ručně ovládat dvojím způsobem. Buďto timeru odeberete kontrolu nad pinem (vynulováním CMP2EN) a klasicky skrze PORT zapíšete hodnotu jakou chcete. A nebo necháte pin pod kontrolou timeru, zastavíte ho a skrze registr TCA0.CTRLC manipulujete s výstupem. Protože po dokončení "animace" timer vypínám, mohu ke kontrole pinu použít druhou možnost. Pozvolné rozsvěcení začnu spuštěním timeru s 0% střídou a pak ji postupně navyšuji (v takovém počtu kroků jako má naše pole s jasem - tedy 36). Interval mezi změnou střídy je v makru ANIMACE (v ms) a můžete jej změnit. Po skončení animace timer vypnu a výstup timeru nastavím do log.1 (nebo log.0 podle toho zda rozsvěcím nebo zhasínám).
/* tutorial TinyAVR 1-Series * timer/Counter A (TCA) - animace rozsvěcení a zhasínání LEDky (jen pro legraci) * Ch.2 na PB5 (WO2) - LED proti VCC, na kitu * Tlačítko na PB4 (proti GND), taky na kitu */ /* Ve fuses zvolen 20MHz oscilátor (lze volit ještě 16MHz) */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #define ANIMACE 30 // zpomalení animace (trvání = ANIMACE*sizeof(jas) [ms], konkrétně 30*36 ~ 1s) // tabulka přechodu jasu const uint8_t jas[]={ 1,2,3,4,6,8,11,15,20,26,33,41,51,61,73,86,99,114,128,142,157,170,183,195,205,215,223,230,236,241,245,248,250,252,253,254,255 }; void clock_20MHz(void); void tca_init(void); void animace_rozsveceni(void); void animace_zhasinani(void); int main(void){ PORTB.DIRCLR = PIN4_bm; // tlačítko (PB4) je vstup PORTB.PIN4CTRL |= PORT_PULLUPEN_bm; // aktivujeme na tlačítko vnitřní pullup PORTB.DIRSET = PIN5_bm; // výstup pro LEDku (PB5) PORTB.PIN5CTRL = PORT_INVEN_bm; // LEDka je proti VCC, invertujeme výstup - log.1 rozsvěcí LED PORTMUX.CTRLC = PORTMUX_TCA02_ALTERNATE_gc; // Remaptujeme výstup timeru (WO2) na PB5 (LEDku) clock_20MHz(); // taktujeme na 20MHz tca_init(); // inicializace timeru while (1){ while(PORTB.IN & PIN4_bm){} // čekej na stisk animace_rozsveceni(); // animace rozsvěcování while(PORTB.IN & PIN4_bm){} // čekej na stisk animace_zhasinani(); // animace zhasínání } } void animace_rozsveceni(void){ uint8_t i=0; // k procházení polem jasů TCA0.SINGLE.CNT = 0; // vynulovat čítač před rozběhem TCA0.SINGLE.CMP2 = 0; // první perioda je 0 PWM (zhasnuto) TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm; // spustíme časovač // procházíme všechny jasy od nejmenšího po největší (odpředu) for(i=0;i<sizeof(jas);i++){ TCA0.SINGLE.CMP2BUF = jas[i]; // nastavíme nový jas _delay_ms(ANIMACE); // necháme LED nějaký čas svítit vybraným jasem } TCA0.SINGLE.CTRLA &=~TCA_SINGLE_ENABLE_bm; // vypneme timer TCA0.SINGLE.CTRLC |= TCA_SINGLE_CMP2OV_bm; // nastavíme výstup do log.1 (rozsvítí trvale LED) } void animace_zhasinani(void){ uint8_t i; // k procházení polem jasů TCA0.SINGLE.CNT = 0; // vynulovat čítač před rozběhem TCA0.SINGLE.CTRLA |= TCA_SINGLE_ENABLE_bm; // spustíme timer // procházíme všechny jasy od největšího po nejmenší (odzadu) for(i=sizeof(jas);i>0;i--){ TCA0.SINGLE.CMP2BUF = jas[i-1]; // nastavíme nový jas _delay_ms(ANIMACE); // necháme LED nějaký čas svítit vybraným jasem } TCA0.SINGLE.CTRLA &=~TCA_SINGLE_ENABLE_bm; // vypneme časovač TCA0.SINGLE.CTRLC &=~TCA_SINGLE_CMP2OV_bm; // vynulujeme výstup timeru (zhasne trvale LED) } void tca_init(void){ // Nastavíme timeru prescaler 256 => clock do timeru je 20M/256 = 78kHz TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV256_gc; // Nastavíme strop 255, PWM frekvence je ~304Hz TCA0.SINGLE.PER = 255; // vynulujeme výstup timeru (zhasne trvale LED) TCA0.SINGLE.CTRLC &=~TCA_SINGLE_CMP2OV_bm; // přidělíme timeru výstup a zapneme režim Single slope PWM TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // povolíme timeru běžet i během debugu abychom viděli během ladění aktuální jas TCA0.SINGLE.DBGCTRL = TCA_SINGLE_DBGRUN_bm; } // Nastaví clock 20MHz (z interního 20MHz bez děličky) void clock_20MHz(void){ // v případě potřeby zde dočasně vypněte přerušení CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLA = CLKCTRL_CLKSEL_OSC20M_gc; // vybírá 20MHz oscilátor CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLB = 0; // vypne prescaler (děličku) }
Musím poznamenat že 8bitový režim mě svou neschopností realizovat PWM v rozsahu 0-100% trochu zklamal. Nenapadl mě žádný jednoduchý způsob jak toto omezení obejít bez glitchů, takže pokud na nějaký narazíte nebo už nějaký znáte, dejte vědět a doplnil bych ho. I tak na mě ale TCA udělal vcelku příjemný dojem a něco mi dává tušit, že se s ním bude dobře pracovat. Doufám že jste si z tohoto dílu něco odnesli a budu se těšit u dalších tutoriálů ... třeba i video :)
Home
| V2.03 12.1.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /