V tomto tutoriálu se budu věnovat dvěma funkcím, které se nevešly do předchozích dílů - proto ten název "střípky". V předchozích dílech jsem se vyhnul velmi zajímavým Phase Correct PWM režimům. Tuto chybu bych rád napravil, neboť jsou pro většinu aplikací vhodnější než Fast PWM, které jsem používal k většině demonstrací. Phase Correct režimy umí regulovat PWM od 0% do 100% bez potřeby tyto stavy ošetřovat "ručně". Další funkce, která je v datasheetu trochu skrytá, je přerušení od ICR v některých CTC režimech. Vymyslel jsem jednoduchou modelovou situaci kde může mít opodstatnění, ve které navíc narazíme na jeden relativně zákeřný obecnější problém. Všechny příklady jsou psané pro čp Atmega644, ale kód by měl být platný i pro jiné čipy jako třeba Atmega328 pouze s úpravou výběru vývodů. Vrhněme se tedy na první téma.
O činnosti těchto módů už byla řeč ve třetím dílu tutoriálu o timerech, takže jen stručná rekapitulace. Bavíme se o módech Phase and Frequency Correct PWM a Phase Correct PWM, které pro stručnost budeme nazývat "correct PWM". Oba módy se liší jen v tom kdy dochází k přepisu stropu a OCR registrů, s klidnou hlavou je tedy můžete považovat za stejné. V těchto módech timer čítá střídavě nahoru a dolů ("dual-slope operation"). Compare událost nastává cestou nahoru i cestou dolů a průběhy na obou PWM kanálech jsou tak "zarovnané na střed". Kvůli Dual-slope je frekvence poloviční jak u Fast PWM režimů a spočítá se ze vztahu:
F_PMW = F_TIM / (2*N*TOP)
kde N je předdělička timeru (1,8,64,256 nebo 1024), TOP je strop časovače (tedy buď 255,511,1023 nebo volitelný pomocí OCR1A či ICR1) a F_TIM je clock timeru - tedy typicky clock čipu (F_CPU). Například v módu s fixním 10bit rozlišením, tedy se stropem 0x03FF (1023) a clockem 8MHz je perioda rovna 3.91kHz. Hodnotu PWM zapisujete jako vždy do registrů OCR1A nebo OCR1B a smí nabývat od 0 do TOP včetně. Tedy v našem modelovém příkladu 0 až 1023 (včetně). Zápisem 0 dojde k tomu, že se na příslušném kanálu nastaví PWM 0% (tedy kanál zůstane stále vynulovaný), zápisem 1023 (100% PWM) se kanál nastaví trvale do log.1 (pro invertované výstupy to bude opačně). Dejte ale pozor, a nezapisujte do OCR1x větší hodnotu než strop, jinak zůstane kanál ve stavu jako by jste zapsali 0 (nedojde totiž k žádné compare události). Než se vrhneme na triviální příklad, udělejme malou matematickou vsuvku. Na rozdíl od Fast PWM lze hodnotu PWM počítat snadno jako pouhý podíl mezi stropem a hodntou OCR registru. Kvůli tomu nelze s lichým stropem generovat střídu přesně 50%. Pokud vaše aplikace vyžaduje z nějakého důvodu střídu přesně 50%, vyhněte se fixním režimům (8bit, 9bit a 10bit), protože ty mají lichý strop. Tím je asi vše podstatné řečeno a pojďme se podívat na velice jednoduchý příklad. Necháme Atmegu644 generovat pozitivní PWM na OC1A (PD5) a invertovaný (negativní) PWM na OC1B (PD4). Program bude postupně procházet tabulku 16 různých hodnot PWM, která bude obsahovat i 0% a 100% hodnotu abychom si ověřili, že tyto hodnoty pracují správně. Využijeme fixní 10bitový režim a necháme ho pracovat na nejvyšší možné frekvenci (s čipem taktovaným na 8MHz). Perioda a frekvence by tedy měly odpovídat modelové situaci z výpočtu výše. Další komentář si příklad nezaslouží, následuje zdrojový kód a ilustrační oscilogram.
// A) Demonstrace Phase correct PWM mode Timeru 1 - schopnost regulovat PWM od 0% do 100% (Atmega644PA) #define F_CPU 8000000 #include <avr/io.h> #include <util/delay.h> // tabulka PWM hodnot const uint16_t table[]={0,127,255,383,511,639,767,895,1023,895,767,639,511,383,255,127}; uint8_t i; // index procházející tabulku int main(void){ DDRD |= (1<<DDD4) | (1<<DDD5); // PD4 (OC1B), PD5 (OC1A) výstupy // Mód 3 - (PWM, Phase Correct, 10-bit), OC1A pozitivní PWM, OC1B invertovaný PWM TCCR1A = (1<<COM1A1) | (1<<COM1B1) | (1<<COM1B0) | (1<<WGM11) | (1<<WGM10); OCR1A = 0; // začínáme s 0% OCR1B = 0; // začínáme se 100% (invertovaný kanál) TCCR1B = (1<<CS10); // spouštíme timer s 8MHz clockem while (1){ // každou vteřinu nastavíme PWM na jednu hodnotu z tabulky for(i=0;i<16;i++){ OCR1A = table[i]; OCR1B = table[i]; _delay_ms(1000); } } }
Je celkem dobře známé, že CTC módy se stropem v OCRA mohou po jeho dosažení vyvolat OCA přerušení. Mohou tedy sloužit ke generování pravidelných přerušení s nastavitelnou frekvencí. Jak je to ale s CTC módem se stropem ICR1 ? ICR1 totiž jinak slouží k záznamu obsahu čítače s příchodem vnější události (více o něm ve čtvrtém dílu). Může timer v tomto módu také generovat přerušení od "stropu" ? Ano může. Jakmile čítač napočítá do stropu, nastaví vlajku ICF1 a pokud je to povoleno, vyvolá přerušení TIMER1_CAPT_vect. Logicky, je-li ICR použit jako strop, nemůže zastávat svou původní roli a přicházíme kompletně o "input capture" funkci. Jako proti váhu si ale uvolníme OCR1A a můžeme tedy timerem realizovat celkem tři časy. Protože příkladů na různé časování jsme už viděli dost, budeme přerušení od ICR demonstrovat v trochu odlišné situaci.
Představme si aplikaci, která počítá vnější impulzy. Mohou to být kusy součástek na výrobní lince, nebo impulzy z průtokoměru a případně jiných čidel s "impulzním" výstupem. A dejme tomu, že potřebujeme počítat sady. Tedy skupiny například 20ti impulzů. Po sesbírání každé sady chceme vyvolat přerušení. Dále chceme vyvolat přerušení v půlce a ve tří čtvrtinách sady (například abychom připravili HW na ukončení sady) a jako bonus si ještě udržovat informaci o celkovém počtu kusů/impulzů od vynulování. Takovou aplikaci by nebyl problém napsat "pollingem", nebo na bázi externích přerušení. Ale zvládne ji i Timer/Counter. Jako clock counteru vybereme vnější signál (vzestupnou hranu). Zvolíme mód CTC se stropem ICR1. Do ICR1 zapíšeme kolik impulzů má sada, do registrů OCR1A a OCR1B pak "značky" (půl sady a tři-čtvrtě sady). Abychom viděli, že náš SW funguje správně, necháme ho s každým přerušením vygenerovat krátký impulz na pinech PA0 až PA2. Tím přeneseme celou úlohu počítání na Timer a jádro se pak může věnovat jiným úkolům, nebo šetřit energii v Idle spánku. Nyní mrkněte na zdrojový kód a demonstrační oscilogram.
// C) Využití přerušení od ICR v CTC režimu // Timer 1 čítá vnější události (vzestupné hrany), každých "n" událostí (makro SADA) vyvolá přerušení // Dále vyvolá přerušení jakmile počet vnějších impulzů překročí další dvě volitelné hodnoty (ZNACKA0 a ZNACKA1) // Přerušení si na osciloskopu zobrazujeme pomocí impulzů na pinech PA0,PA1,PA2 #define F_CPU 8000000 #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h> // makra pro ovládání signalizačních výstupů #define PA0_L PORTA &=~(1<<PORTA0) #define PA1_L PORTA &=~(1<<PORTA1) #define PA2_L PORTA &=~(1<<PORTA2) #define PA0_H PORTA |= (1<<PORTA0) #define PA1_H PORTA |= (1<<PORTA1) #define PA2_H PORTA |= (1<<PORTA2) #define ZNACKA0 10 // první značka např. v 50% sady #define ZNACKA1 15 // druhá značka např. v 75% sady #define SADA 20 // sada vnějších impulzů (např 20 kusů/jednotek/impulzů) uint8_t celkovy_pocet(uint32_t* pocet); // funkce zjistí aktuální počet všech impulzů od vynulování volatile uint32_t pocet_sad=0, pocet_kusu=0; // proměnné udržující info o počtu sad/impulzů uint32_t tmp; // pomocná proměnná int main(void){ DDRA |= (1<<DDA0) | (1<<DDA1) | (1<<DDA2); // signalizační výstupy DDRB &=~(1<<DDB1); // PB1 (T1) input - sem přivádíme impulzy ICR1 = SADA-1; // počet impulzů na sadu (počítáno od nuly, proto -1) OCR1A = ZNACKA0-1; // první značka OCR1B = ZNACKA1-1; // druhá značka // volíme režim CTC se stropem ICR, clock externí signál - vzestupná hrana TCCR1A = 0; TCCR1B = (1<<WGM13) | (1<<WGM12) | (1<<CS12) | (1<<CS11) | (1<<CS10); // vymažeme vlajky všech použitých přerušení TIFR1 = (1<<OCF1A) | (1<<ICF1) |(1<<OCF1B); // spovolíme všechna relevantní přerušení TIMSK1 |= (1<<OCIE1A) |(1<<OCIE1B) | (1<<ICIE1); sei(); // globálně povolíme přerušení while (1){ // není co dělat, i když to nikde nevypisujeme, čteme pravidelně celkový počet impulzů _delay_ms(10); while(celkovy_pocet(&tmp)){} // opakovaně čteme data tak dlouho dokud neproběhne čtení korektně pocet_kusu = tmp; // korektní výsledek můžeme můžeme ho přijmout } } // funkce vrací 0 pokud úspěšně přečetla data a uložila je na zvolenou adresu // funkce vrací 1 pokud je podezření, že bylo čtení narušeno přetečením čítače uint8_t celkovy_pocet(uint32_t* pocet){ uint32_t celkem; //uint16_t tmp1; cli(); // vypneme na chvíli přerušení protože čteme 16bit registr celkem = SADA*pocet_sad + TCNT1; // sečteme aktuální hodnotu v sadě s celkovým počtem před tím // zkontrolujeme zda nehrozí chybné čtení dat if(TIFR1 & (1<<ICF1)){ // důvodné podezření, že timer přetekl během vyčítání ! sei(); // zapneme zpátky přerušení return 1; // vracíme error } *pocet = celkem; // vrátíme na vybranou adresu počet impulzů sei(); // zapneme zpátky přerušení return 0; // a dáme vědět že vše proběhlo v pořádku } // přerušení po dosažení první značky (10 impulzů) ISR(TIMER1_COMPA_vect){ PA0_H; // generujeme signalizační impulz _delay_us(200); PA0_L; } // přerušení po dosažení druhé značky (15 impulzů) ISR(TIMER1_COMPB_vect){ PA1_H; // generujeme signalizační impulz _delay_us(200); PA1_L; } // přerušení po dosažení celé sady (20 impulzů) ISR(TIMER1_CAPT_vect){ pocet_sad++; // inkrementujeme počítadlo sad PA2_H; // generujeme signalizační impulz _delay_us(200); PA2_L; }
Všimněte si, že ve funkci celkovy_pocet() vypínáme přerušení. Čteme v ní obsah counteru (TCNT1) a to je 16bit registr. Jeho čtení tedy probíhá v rámci dvou instrukcí. Nejprve se přečte dolních 8bitů a poté horních 8bitů. Pokud by mezi těmito dvěma instrukcemi přišlo přerušení a v něm bychom přistupovali k libovolnému 16bit registru našeho timeru, mohli bychom přečíst nebo zapsat chybná data. Můžete správně namítnout, že to v našem programu neděláme a takže tento problém nehrozí. Stačí, ale ukázku upravit a na problém zapomenout (a to se stane velice snadno) a máte zaděláno na jednu z nejzákeřnějších chyb. Přichází totiž náhodně, velmi velmi zřídka a velice těžko se chytá. Je tedy vhodné předcházet jí programově vždy, ať už hrozí nebo ne. V naší funkci ale existuje ještě jeden problém ! Představte si, že čítač má napočítáno do 19, tedy zbývá mu jediný impulz k dokončení sady a volání přerušení. V proměnné pocet_sad je například 1 (máme tedy celkem 20+19 = 39 impulzů). V této situaci zavoláme naši funkci, vypneme přerušení a než stihneme přečíst obsah TCNT1 přijde 20tý impulz a čítač přeteče (a vynuluje se). Z TCNT1 pak vyčteme nulu, ale v pocet_sad máme stále 1, dojdeme tedy k závěru, že celkový počet je 20 impulzů (na rozdíl od reality kdy jich je 40) ! Co tedy s tím ? Můžeme si po výpočtu zkontrolovat zda vlajka ICF1 nenaznačuje, že čítač přetekl. Pokud ne, máme jistotu, že jsou data korektní. Pokud přetekl je tu důvodné podezření, že jsou data chybná. Došlo-li k přetečení až po čtení hodnoty TCNT1 jsou data v pořádku, došlo-li k tomu před tím, máme chybný počet. Protože nemáme způsob jak rozhodnou, která z těchto situací nastala je vhodnější data zahodit a pokusit se o čtení znovu. Toto opakování zajišťuje v hlavní smyčce cyklus while. Opakuje čtení tak dlouho dokud se nepovede úspěšně. To zní dost škaredě. V praxi je "defektních" čtení naprosté minimum a jen velmi zřídka se musí čtení opakovat a v podstatě nikdy vícekrát jak jednou. Pravděpodobnost chybného čtení roste s frekvencí vnějších impulzů a se zmenšováním stropu timeru (častěji přetéká). Kdo potřebuje může si celé ošetření zabudovat přímo do naší funkce. Předesílám, že jsem si funkci jen v rychlosti otestoval na simulátoru, takže se nemůžu 100% zaručit, že bude pracovat bez chybně. Zmíněný problém na vás bude čekat v kterékoli aplikaci kde se pokusíte čítač vnějšího signálu "rozšířit" počítáním kolikrát přetekl.
V závěru si dovolím malou jedovatou poznámku na adresu Microchipu. Po velkém "přeorávání" datasheetů (jak jinak nazvat proces po kterém vznikly chyby na místech, která byla před tím správně), se mimo jiné změnilo značení bitů ve všech registrech TIMSKx a TIFRx. Z původního označení s pořadovým číslem timeru (např. OCIE0B) se stalo značení bez pořadového čísla (napří. OCIEB). Tím přestal aktuální datasheet (10/2016) korespondovat s hlavičkovými soubory v Atmel Studiu 7 (aktualizovanými ke dnešnímu dni - 26.8.2018). Takže vy kdo píšete dle datasheetu, připravte se, že vás kompilátor občas překvapí neexistencí některých názvů. Pozitivní novinka (alespoň pro mě) jsou makra na konci hlavičkového souboru která mapují funkce všech pinů (u Megy644). Nastavujete-li tedy například OC1B jako výstup můžete využít maker OC1B_DDR a OC1B_BIT a podobně. Kód se díky tomu stane čitelnější, protože nemusíte do komentářů psát poznámku o tom že PD4 se kterým právě manipulujete je OC1B. Doufám že se vám tutoriál trochu rozšířil obzory a budu se těšit u dalších dílů.
Home
V1.00 26.8.2018
By Michal Dudka (m.dudka@seznam.cz)