Čítač / časovač 1 nejen na Attiny

Abstrakt

V tomto volném pokračování seriálu se podíváme na zoubek 16bitovému čítači/časovači 1 a předvedeme si využití funkcí, které na 8bitovém čítači nejsou.

Stručný přehled

Protože 16bitový Čítač/Časovač 1 je v jádru podobný jako 8bitový Čítač/Časovač 0, nebudu se zde zabývat do hloubky principem jeho činnosti a zaměřím se jen na funkce které jsou odlišné. Těm, kteří se setkávají s čítačem poprvé, doporučuji přečíst si nejprve předchozí díly seriálu (zde). Díky tomu, že drtivá většina členů rodiny Atmega i Attiny obsahuje alespoň jeden 16bitový čítač/časovač (vyšší řady jako Atmega64A/128A obsahují dva a nejvyšší řady jako Atmega640/1280/2560 až čtyři), bude tento návod s drobnými odlišnostmi platit nejen pro čipy řady Attiny. Čítač/časovač 1 díky své 16bitové architektuře umožňuje elegantnější řešení úkolů než jeho 8bitový bráška (čítač/časovač 0). Stejně jako čítač 0 ho lze taktovat interním i externím signálem (pin T1). Má dvě compare jednotky, které mohou ovládat piny OC1A a OC1B. Je schopen generovat obdélníkový průběh o volitelné frekvenci a širokou paletu PWM signálů (s mnohem lepším rozlišením než 8bit časovač). Navíc ale obsahuje capture jednotku, která je schopna zaznamenávat čas nezávisle na programu. Ta najde uplatnění zvláště při měření časů, periody, frekvence nebo PWM.

Stručně o ovládání

Ve stručnosti shrnu základní ovládací prvky Čítače 1. Vzhledem k tomu, že většina ovládacích prvků je podobná jako u čítače 0 (s nímž jste již seznámeni) mělo by to pro vás být vlastně opakování. Stejně jako čítač 0 je na čítači 1 možnost čítat externí signál a to ze vstupu T1 (na Attiny2313 pin PD5). Úplně stejný je i způsob volby zdroje signálu pro čítač. Vybíráte ho pomocí bitů CS10,CS11 a CS12 v registru TCCR1B. Opět lze čítač vypnout, pustit do něj přímo clock čipu nebo výstup z předděličky s dělícími poměry 8,64,256 a 1024. Externí signál lze čítat na vzestupnou nebo na sestupnou hranu. Čítání probíhá v registru TCNT1. K dispozici máme dvě compare jednotky s registry OCR1A a OCR1B. Od obou compare jednotek je možné volat přerušení (nastavením bitů OCIE1A a OCIE1 v registru TIMSK) a jejich stav je indikován vlajkami OCF1A a OCF1B v registru TIFR. Stejně tak je možné volat přerušení od přetečení (nastavením bitu TOIE1 v registru TIMSK). Vlajka od přetečení TOV1 se chová také stejně a najdete ji zase v regisru TIFR. Čítači mohou být přiděleny piny OC1A a OC1B (u Attiny2313 na pinech PB4 a PB3). Přidělení se provádí pomocí bitů COM1A1, COM1A0,COM1B1 a COM1B0 v registru TCCR1A a jejich chování je opět shodné jako u čítače 0. Jediný rozdíl je v tom, že PWM módů je více.

Co umí navíc ?

Odlišnostem budeme věnovat trochu více prostoru. První odlišnost je v 16bitové architektuře. Registry TCNT1, OCR1A, OCR1B a ICR1 jsou 16bitové a rozděleny na horní a dolní část. TCNT1 je rozdělen na TCNT1L a TCNT1H a analogicky jsou rozděleny i tři další registry. Přístup k nim nevyžaduje žádné složité zacházení, protože překladač dělá práci za vás. Vy k nim můžete formálně přistupovat jako k jednomu 16bit registru. Krom 16bitového designu přibyla input capture jednotka. Té patří pin ICP1 (na PD6 u Tiny2313). Umožňuje zaznamenat stav čítače v okamžiku kdy přijde na pin vzestupná nebo sestupná hrana a můžete u ní zapnout i digitální filtr. Krom toho může capture událost vyvolat i analogový komparátor. Ovládání capture jednotky je velice jednoduché. Bitem ICNC1 zapínáte digitální filtr a bitem ICES1 volíte hranu, která má capture událost spouštět (oba bity se nachází v registru TCCR1B). Jakmile vámi vybraná hrana dorazí na vstup ICPI, překopíruje se stav čítače (registru TCNT1) do registru ICR1 a tam počká než jej zpracujete nebo než přijde nová capture událost. O čemž vás informuje vlajka ICF1 v registru TIFR. Tu jako obvykle můžete smazat zápisem log.1, případně se maže sama při vyvolání rutiny přerušení. Přerušení od capture události můžete povolit nastavením bitu ICIE1 v registru TIMSK. Dále se oproti časovači 0 výrazně rozšířily možnosti PWM signálů. Přibyly 8-bitové, 9-bitové a 10-bitové varianty. Je v nich možné dosáhnout regulace od 0 do 100%. V CTC módu už není potřeba obětovat registr OCR1A jako strop čítače a místo něj může posloužit registr ICR1 (pokud nevyužíváte capture jednotku), takže zůstanou k dispozici ve všech režimech dva PWM kanály. Seznam všech módů můžete vidět v tabulce 1.

Tabulka 1 - Módy čítače / časovače 1
MódWGM13WGM12WGM11WGM10Režim činnostiStrop
00000Normal0xFFFF
10001Phase Correct PWM, 8-bit0x00FF
20010Phase Correct PWM, 9-bit0x01FF
30011PhaseCorrect PWM, 10-bit0x03FF
40100CTCOCR1A
50101Fast PWM, 8-bit0x00FF
60110Fast PWM, 9-bit0x01FF
70111Fast PWM, 10-bit0x03FF
81000Phase and Frequency correct PWMICR1
91001Phase and Frequency correct PWMCR1A
101010Phase correct PWMICR1A
111011Phase correct PWMOCR1A
121100CTCICR1
131101--
141110Fast PWMICR1
151111Fast PWMOCR1A

Příklady

A) Generování přesných frekvencí

Značnou nevýhodou pro generování frekvencí byl u čítače 0 jeho 8bitový design. V CTC módu jste měli k dispozici jen 256 hodnot stropu čítače. S volbou předděličky (4 možnosti) to dělá celkem 4*256 = +-1024 možných frekvencí při stálém clocku čipu (který jde programově také dělit, ale to typicky nebudete chtít). To je nic moc, jestliže máte pokrýt pásmo dejme tomu od 10Hz do 50kHz. Zkuste si nastavit 8bitový čítač 0 aby realizoval čas (přetékal každých) 625us. Musíte použít předděličku 8 a naplnit čítač hodnotou 78.125 :D což přirozeně nejde. S 16bitovým čítačem, máte i s předděličkou k dispozici okolo čtvrt milionu kombinací a zrealizovat 625us, 624us nebo 623us je snadné. I když jsme již stejný úkol prováděli s čítačem 0, předvedeme si příklad jak generovat 1s (1Hz). Nebudeme k tomu potřebovat žádné přerušení a signál bude generován naprosto autonomně. Abychom v ukázce viděli něco z "novinek" na čítači 1 použijeme CTC mód se stropem ICR1 (mód 12) - nastavíme bity WGM13 a WGM12. Jestliže máme generovat 1Hz (perioda 1s) potřebujeme aby se během této doby změnil stav OC1A dvakrát, časovač tedy necháme přetékat každých 500ms. Strop časovače můžeme volit v rozsahu 0-65535. S předděličkou 8 potřebujeme strop časovače 500000us/8us = 62500. Protože časovač počítá od nuly, nahrajeme do ICR1 (stropu časovače) hodnotu 62499. Nastavením bitu COM1A0 přidělíme časovači výstup OC1A. Časovač pustíme nastavením bitu CS11 (clock/8). A to je vše :) Kdo by chtěl, může si nastavením bitu ICIE1 v registru TIMSK povolit přerušení a provádět každou půl vteřinu nějakou akci. Výsledek je patrný na obrázku a1.


Obrázek a1 - 1Hz pomocí čítače/ časovače 1 v CTC režimu se stropem ICR1

// A) - generování 1Hz pomocí 16bit čítače časovače 1
#include <avr/io.h>

int main(void){
DDRB = (1<<DDB3); // OC1A výstup
TCCR1A = (1<<COM1A0);  // přidělujeme čítači výstup OC1A
ICR1 = 62499;	// perioda čítače
TCCR1B = (1<<CS11) | (1<<WGM13) | (1<<WGM12);   // předdělička 8, mód CTC se stropem ICR1
while(1){
 asm("nop");
 }
}

C) Input Capture k měření PWM

V příkladu s komparátorem (zde) jste mohli vidět využití input capture k měření periody. V následujícím příkladu necháme čítač 1 měřit střídu a periodu signálu. Protože výpočet není úplně triviální a provádíme ho v rutině přerušení je vhodnější taktovat Atmel na vyšší frekvenci. Doba výpočtu bude limitovat minimální měřitelnou střídu a maximální měřitelnou frekvenci. Na PB3 si budeme indikovat jak dlouho tráví program v rutině přerušení abychom měli představu jaké nejkratší signály je schopen spolehlivě zpracovat. Já se rozhodl čip taktovat na 8MHz. Když k čítači pustím clock s předděličkou 8, bude čítač pracovat na 1MHz, takže jeho časová jednotka bude pěkně čitelná 1us. Není ale nutné použít tuto konfiguraci, s 16MHz by čítač s předděličkou 8 měl základní jednotku 0.5us. A bez předděličky by krok čítače byl 1/16us tedy 62.5ns. Vyšší takt zvětšuje časové rozlišení ale zkracuje nejdelší měřitelný úsek (při 1MHz 65.535ms, při 16MHz 4.095ms). Náš program pracuje následovně. Nejprve nastavíme capture jednotku čítače na detekci nástupné hrany a povolíme od capture události přerušení. Jakmile je hrana detekována, v rutině přerušení si uložíme čas jejího příchodu do proměnné nastupna a přepneme čítač na detekci sestupné hrany. Po příchodu sestupné hrany je opět volána rutina přerušení, kde si do proměnné sestupna uložíme čas příchodu sestupné hrany. Z časů sestupné a vzestupné hrany si spočítáme t_low což je doba po kterou byl signál v log.0 a t_high, dobu po kterou byl signál v log.1. Pak dáme pomocí proměnné posli pokyn "while" cyklu aby provedl odeslání dat. K tomu využijeme jednoduchou funkci která ve odešle data protokolem SPI do světa. V mém případě je detekuji osciloskopem a zobrazím. Odeslání dat přirozeně může proběhnout jinou cestou, UARTem, nebo mohou být zobrazena na displeji, případně jinak zpracována. Protože ale návod vznikal ještě před návodem k jednotce USART, nevyužívá odesílání dat do PC. Střídu signálu už pak můžete snadno dopočítat jako DCL = t_high/(t_high+t_low). Protože je to ale desetinné číslo vyvstává tu jistá libovůle v reprezentaci (budeme chtít DCL v procentech ? v promile ? v jakých jednotkách ?). Proto program odesílá pouze t_low a t_high jakožto klíčové parametry vstupního signálu.

Obrázek C1 - měření periody a střídy pomocí input capture
světle modrá - měřený signál
červená - odezva rutiny přerušení
žlutá a tmavě modrá - datový výstup
Obrázek C2 - měření periody a střídy pomocí input capture
světle modrá - měřený signál
červená - odezva rutiny přerušení
žlutá a tmavě modrá - datový výstup

Z obrázku C1 je patrné, že vstupní signál (světle modrý) má délku kladného pulzu 42us. Délka záporné části signálu je 386us. Na obrázku C2 vidíte, že rutina přerušení trvá okolo 5us (červený průběh). Je tedy vhodné aby byla šířka vstupního pulzu o něco delší, pak je zaručeno, že program stihne vše zpracovat. Z výstupních dat je patrné že program naměřil délku kladného pulzu 43us a délku záporného pulzu 385us. Vzhledem k tomu, že rozlišovací schopnost měření čítačem je 1us, jsou odchylky +-1us v toleranci. Pro přesnější měření takového signálu by bylo vhodné vypnout čítači předděličku a taktovat ho 8MHz.

// C) - input capture - měření periody a střídy
#include <avr/io.h>
#define F_CPU 8000000
#include <avr/interrupt.h>

#define CLK_H PORTB |=(1<<PORTB1)
#define CLK_L PORTB &=~(1<<PORTB1)
#define CLK_TGL PINB = (1<<PINB1) // tohle je finta (!), sekce 10.1.2 v datasheetu :)
#define DATA_H PORTB |=(1<<PORTB0)
#define DATA_L PORTB &=~(1<<PORTB0)

volatile unsigned int t_high=0,t_low=0; // parametry měřeného signálu
volatile char posli=0;

void posli_zpravu(unsigned int data);

ISR(TIMER1_CAPT_vect){
static unsigned int sestupna=0, nastupna=0;
static char faze=0;	// rozhoduje o tom kterou hranu detekujeme
PORTB |= (1<<PORTB3); // indikuje začátek rutiny přerušení
// pokud detekujeme nastupnou hranu
if(faze==0){
	nastupna=ICR1;	// ulož si zachycený čas vzestupné hrany	
	TCCR1B &=~ (1<<ICES1); // detekuj příští sestupnou hranu
	if(sestupna<nastupna){t_low=nastupna-sestupna;}
	else{t_low=((0xffff-sestupna)+nastupna);}
	}
// pokud detekujeme sestupnou hranu
if(faze==1){
	sestupna=ICR1;// ulož si zachycený čas sestupné hrany
	TCCR1B |= (1<<ICES1); // detekuj příští vzestupnou hranu
	if(nastupna<sestupna){t_high=sestupna-nastupna;} // ošetřujeme přetečení čítače
	else{t_high=((0xffff-nastupna)+sestupna);}
	posli=1;
	}
faze++;	// příště měříme jinou hranu
if(faze>1){faze=0;}
PORTB &=~(1<<PORTB3); // indikuje konec rutiny přerušení
}

int main(void){
DDRB = (1<<DDB3) | (1<<DDB1) | (1<<DDB0); // signalizační výstupy
TIMSK = (1<<ICIE1); // povolujeme přerušení od input capture
TIFR = (1<<ICF1); // pro jistotu mažeme vlajku
// pouštíme časovač 1 s předděličkou 8, s detekcí vzestupné hrany, bez filtru
TCCR1B = (1<<CS11) | (1<<ICES1);
sei(); // povolujeme globálně přerušení

while(1){
 if(posli){
	posli_zpravu(t_high); // pokud máme k dispozici nová data tak je odesíláme
	posli_zpravu(t_low);
	posli=0;
	}
 }
}

void posli_zpravu(unsigned int data){
unsigned int j=(1<<15);	// bude sloužit jako maska
char i;	// do forcyklu
CLK_L;	// připrav linky do neutrálního stavu před vysíláním
DATA_L;
for(i=0;i<16;i++){ // odešli 16 bitů
	if(data & j){DATA_H;}else{DATA_L;} // podívej se jestli je i-tý bit dat v log.1 nebo log.0
	j=j>>1;	// nastav novou masku
	CLK_TGL;	// udělej tick clockem
	asm("nop");	// trochu clock natáhni aby se mi dobře detekoval
	asm("nop");
	CLK_TGL;
	}
DATA_L;	// ukliď komunikační linky do neutrálního stavu
CLK_L;
}

Měřicí sestava nebyla nijak náročná, na vstup ICPI jsem připojil pull-down rezistor 10k, aby měl signál definovanou hodnotu i když je odpojený. Čip jsem taktoval 8MHz z externího oscilátoru (viz foto).

B) Input Capture s digitálním filtrem

Jak jsme řekli v úvodu, input capture modul je schopen zaznamenat stav čítače v reakci na nájákou událost. K dispozici jsou dva signály, které mohou input capture událost spustit. Je to signál na pinu ICPI nebo výstup analogového komparátoru. Příklad využití s komparátorem najdete v seriálu o komparátoru (odkaz). Zde se mu tedy věnovat nebudu. Po výběru zdroje signálu si můžeme ještě zvolit zda má capture modul reagovat na vzestupnou nebo na sestupnou hranu. Výběr hrany provádíme bitem ICES1 v registru TCCR1B, zápisem log.1 detekujeme vzestupnou hranu, log.0 pak sestupnou hranu. Bit ICNC1 v tomtéž registru slouží k zapnutí digitálního filtru na input capture modulu. Filtr funguje tak, že s clockem procesoru vzorkuje vstupní signál, pokud jsou čtyři po sobě jdoucí vzorky shodné, bude na výstupu filtru jejich hodnota. Pokud má alespoň jeden z posledních čtyř vzorků jinou hodnotu než ostatní, výstup filtru se nemění a zůstává na něm předchozí hodnota. Šum v podobě impulzů kratších jak 3 cykly procesoru tedy filtrem neprojde a nevyvolá capture událost. Jakmile ke capture události dojde, je nastavena vlajka ICF1 v registru TIFR a pokud je bitem ICIE1 (v registru TIMSK) povoleno přerušení, je zavoláno. Nejprve si budeme demonstrovat funkci digitálního filtru. V následujícím zdrojovém kódu jsou připraveny obě varianty s filtrem i bez filtru. Řádek s vypnutým filtrem je zakomentovaný, stačí jej odkomentovat a zakomentovat řádek se zapnutým filtrem.

// B) - přerušení input capture - demonstrace digitálního filtru
#include <avr/io.h>
#include <avr/interrupt.h>

ISR(TIMER1_CAPT_vect){
PORTB |= (1<<PORTB3); // děláme pulz na výstupu
PORTB &=~(1<<PORTB3); // signalizujeme tím že input capture zachytil signál
}

int main(void){
DDRB = (1<<DDB3); // signalizační výstup
TIMSK = (1<<ICIE1); // povolujeme přerušení od input capture
TIFR = (1<<ICF1); // pro jistotu mažeme vlajku
// pouštíme časovač 1 bez předděličky s detekcí vzestupné hrany
//TCCR1B = (1<<CS10) | (1<<ICES1); // bez digitálního filtru
TCCR1B = (1<<CS10) | (1<<ICES1) | (1<<ICNC1); // s digitálním filtrem
sei(); // povolujeme globálně přerušení

while(1){
 asm("nop");
 }
}

Na obrázcích B1,B2 a B3 vidíte výsledky pokusů. Na obrázku B1 jsme spustili input capture bez filtru. Je patrné, že i velmi krátký pulz (menší jak 800ns) spouští capture událost (která nám v přerušení vytvoří pulz na PB3). Modrý průběh je vstupní signál do čítače, červený je pak jeho odezva. Po zapnutí filtru je výsledek na obrázku B2, čítač žádný záchyt neregistruje - pulzy jsou příliš úzké. Jejich šířku musíme rozšířit málo přes 3us, teprve pak začne čítač opět zachytávat (viz obrázek 3). Ale jen sporadicky. Jen zřídka se podaří trefit se pulzem tak aby 4 vzorky vzdálené od sebe 1us byly uvnitř. Pulzy nad 4us už zachytí spolehlivě. Z toho plyne, že filtr spolehlivě odstraní všechny pulzy kratší než 3us (s taktem procesoru 1MHz), což odpovídá teorii :) Je potřeba si ale uvědomit, že zapnutím filtru zpozdíme capture událost o 4 cykly procesoru a v případě potřeby s tím počítat.

Obrázek B1 - input capture bez filtruObrázek B2 - input capture s filtrem - nezachytává krátké pulzyObrázek B3 - input capture s filtrem, nejkratší pulzy které projdou (>3us)

D) Attiny2313 - 4 kanálové PWM (synchronizace časovačů)

V tomto příkladě si předvedeme jak je možné s Attiny2313 generovat synchronizované 4 kanálové PWM. Příklad nebude přenositelný na řady Atmega. Ty totiž podobnou funkci provádí jinak. Na Attiny je předdělička, která dělí clock čipu 8,64,256 a 1024. A každému časovači lze jako zdroj taktovacího signálu zvolit buď přímo clock čipu nebo jeden z výstupů předěličky (a přirozeně i externí signál). Díky tomu, že je předdělička pro oba časovače shodná, je zaručeno, že dva časovače "tiknou" (tedy inkrementují svůj TCNTx registr) vždy ve stejném okamžiku. Pokud jsou tedy připojeny na signál z předděličky. A to bez ohledu na to kdy je program spustí. Tento fakt sám o sobě je dosti nepříjemný, protože z něj plyne, že nelze deterministicky určit, kdy dojde k první inkrementaci časovače. Využíváte-li předděličku dejme tomu 64. A spustíte časovač, může k jeho inkrementaci dojít třeba hned v dalším strojovém cyklu (v případě že v předděličce se naakumulovala hodnota 63) a nebo také až po 63 strojových cyklech. Za jak dlouho to bude záleží na stavu předděličky a tu nelze číst. Aby jste ale měli přece jen nějakou šanci se s problémem vypořádat, máte možnost předděličku restartovat nastavením bitu PSR10 v registru GTCCR. Restartováním předděličky máte jistotu, že začíná počítat od nuly.

Synchronizace časovačů pak může proběhnout následovně. Nejprve oba časovače spustíte. To bohužel nemůžete provést jedinou instrukcí. Spustíte tedy napřed první časovač a potom druhý. Protože ale nemůžete zjistit stav předděličky, nemáte záruku, že mezi spuštěním první a druhého časovače nedošlo k inkrementaci v prvním časovači. Pak restartujete předděličku. Od toho okamžiku vám tiká 8,64,256 nebo 1024 strojových cyklů než předdělička přeteče a inkrementuje čítače. Během této doby musíte stihnout hodnoty v obou čítačích sjednotit. Například zapsáním 0 do TCNTx registrů. Od toho okamžiku máte zaručeno že v obou čítačích je stejná hodnota a z vlastností předděličky je zaručeno že oba inkrementují ve stejný okamžik. To se dá nazvat synchronní činností. Protože však jde o časově náročnou akci, je dobré si před spuštěním vypnout přerušení. Tak jak je postup sepsán by měl fungovat vždy, protože zápis hodnoty 0 do TCNT0 (8bit časovač), trvá 1 strojový cyklus a zápis do TCNT1 (16bit) trvá 2 strojové cykly. Rychlost rutiny závisí na hodnotách které do časovačů ukládáte a na míře optimalizace programu kompilátorem. Bývá proto vhodnější si pohledem na assemblerovský kód zkontrolovat zda se akce stihne. Zvlášť při použití předděličky 8, protože na celou akci je jen 8 strojových cyklů. U jakékoli další předděličky (64,256,1024) už není tato zvýšená opatrnost nutná (na synchronizaci je času dost). V takovém případě (když je času dost) je možné postupovat i tak, že nejprve dojde k restartování předděličky a teprve pak ke kompletnímu nastavení a spuštění čítačů.

V rámci příkladu předvádím tu těžší variantu, tedy synchronizaci s předděličkou 8. Další komentář není potřeba, protože vše ostatní už bylo napsáno v předchozích příkladech. Výsledek pokusu vidíte na obrázku d1. Je patrné že PWM je na všech kanálech synchronní.

// D) - Attiny2313 4 kanálové PWM (synchronizace časovačů)
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void){
DDRB = (1<<DDB2) | (1<<DDB3) |(1<<DDB4); // OC0A, OC1A, OC1B výstup
DDRD = (1<<DDD5); // OC0B výstup

TCCR0A = (1<<COM0A1) | (1<<COM0B1) | (1<<WGM01) | (1<<WGM00); // fast PWM + oba kanály
TCCR1A = (1<<COM1A1) | (1<<COM1B1) | (1<<WGM10);
TCCR1B = (1<<WGM12); // fast PWM (top 0x00FF) + oba kanály
OCR0A = 200; // nastavení hodnoty PWM
OCR0B = 200;
OCR1A = 200;
OCR1B = 200;

TCCR0B |= (1<<CS00) | (1<<CS01); // spouštíme čítač 1
TCCR1B |= (1<<CS10) | (1<<CS11); // spouštíme čítač 0
cli(); // zakázat všechna přerušení
GTCCR = (1<<PSR10); // vynulovat předděličku, od teď máme 8 strojových cyklů
TCNT0=0; // rychle nastavit čítače
TCNT1=0; // na stejnou hodnotu	
sei(); // povolit zpět přerušení

while(1){
 asm("nop"); // nic nedělej
 }
}


Obrázek d1 - synchronizace Časovače 0 a Časovače 1 na Attiny2313

Odkazy