PWM Input režim

V úvodním díle tutoriálu o timerech jsem zmiňoval, že jde o jednu z nejpropracovanějších periferií na STM. Tento příklad toho bude důkazem. Mělo by z něj být patrné jak přizpůsobit "hardwarovou" konfiguraci timeru vybranému úkolu. Demonstraci si předvedeme na měření PWM signálu. Přesněji řečeno na měření periody a střídy pulzně šířkové modulace (viz oscilogram níže). Jen pro zajímavost vyjmenuji pár přístupů jak to lze provádět.


Ukázka měřeného signálu

"Arduino" styl

Tento postup má své nepopiratelné výhody. Kód je primitivní a nenáročný k pochopení a poběží na libovolném mikrokontroléru se sebe-tupějším timerem. Tyto výhody jsou ale draze vykoupeny. Jádro nemůže během měření dělat nic jiného a většinu času jen nečině čeká. Reakční doba programu nemusí být nijak závratně vysoká (zvláště při použití některých kvalitně blátivých knihoven), což redukuje přesnost měření a znemožňuje měření kratších period. Jinak řečeno metoda je použitelná pouze pro měření relativně pomalých signálů (desítky až stovky Hz).

Využití Capture události

Využijete Capture události k okamžitému uložení stavu timeru. Tím odpadnou všechny chyby způsobené opožděnou reakcí SW. Jedním kanálem detekujete okamžik příchodu sestupné hrany, druhým kanálem čas příchodu vzestupné hrany. Obě události necháte vyvolat přerušení. Timer necháte běžet neustále na pozadí. V rutinách přerušení pak vyčtete obsah Capture registrů (tedy časové značky příchodu vybraných hran v signálu). Protože ale timer běží stále, musíte s trochou matematiky ošetřit situaci kdy by během měření přetekl. Což samo o sobě není problém. Trochu více péče vyžaduje ošetření situace kdy timer přeteče vícekrát (což ale hrozí jen při měření opravdu divokých signálů s velkým rozsahem hodnot). Shrňme si to. Tato metoda je přesná, relativně dost používaná a má drobná ale řešitelná úskalí. Pořád ale není dokonalá :)

Ať pracuje Hardware !

Teď se teprve dostáváme k samotnému příkladu. V naší ukázce bude timer dělat vše sám a vyvolá přerušení až v okamžiku kdy bude mít změřeny všechny potřebné údaje. Jak takovou konfiguraci na timerech STM32 vytvořit vám napoví následující obrázek.


Konfigurace Timeru pro měření střídy a periody PWM

Vstupem do timeru může být buď CH1 nebo CH2. Protože má Discovery modul na pinu PA0 (TIM2_CH1) tlačítko, využívám CH2 (na PA1) - na obrázku nese název TI2. Z TI2 je možné získat dva signály TI2FP1 a TI2FP2. Každému z nich je možné předřadit digitální filtr a vybrat hranu na kterou mají signály vzniknout. Oba signály (ještě spolu s TRC signálem) jsou vedeny ke Capture registrům 1 a 2. Každému capture registru je možné zdroj signálu volit. Capture registr 2 tak může mimo jiné reagovat i na dění na kanále 1 (TI1) a naopak. Podobná situace je pak mezi kanály 3 a 4. Signály TI1FP1 a TI2FP2 navíc vedou do Trigger Controlleru kde může jejich příchod vyvolat další akce jako například start timeru nebo jeho reset. Z obrázku je patrné že výčet možností není ani zdaleka vyčerpávající, ale nerad bych vás zahltil. Tohle je vše co pro konfiguraci potřebujeme. A teď k tomu jak to celé bude fungovat.

Signál TI2FP2 necháme detekovat vzestupnou hranu. Ta představuje začátek i konec signálu který měříme. TI2FP1 pak nastavíme na detekci sestupné hrany. Ta bude zaznamenávat střídu měřeného signálu (dobu trvání log.1). TI2FP2 navíc zavedeme do trigger controlleru, kterému nastavíme aby timer s příchodem signálu restartoval. Díky tomu začne timer počítat se začátkem periody vždy od nuly. Capture registr 1 necháme odbírat signál TI2FP1. Díky tomu se do něj bude ukládat čas příchodu sestupné hrany (informace o střídě měřeného signálu). Do Capture registru 2 zavedeme signál TI2FP2 (vzestupná hrana) a budeme v něm zaznamenávat periodu. Teď by jste měli zpozornět ! Jeden signál ovládá zároveň capture událost i reset timeru, takže co se stane dřív ? Nejprve proběhne uložení do Capture registru. Od této události si povolíme přerušení. To nás bude informovat o dokončení měření. Raději si to ještě zrekapitulujeme. S příchodem vzestupné hrany se restartuje timer a začíná počítat od nuly. S příchodem sestupné hrany zaznamená svůj čas do Capture registru 1 a s příchodem další vzestupné hrany, zaznamená čas příchodu, restartuje se a vyvolá přerušení. V reakci na přerušení vyzvedneme z obou Capture registrů data a budeme o signálu vědět vše co jsme chtěli.

Digitální filtry ani Prescalery před Capture registry nebudeme potřebovat. Měřit budeme pomocí TIM2, protože je 32bitový. Celé měření poběží na pozadí. Jádro bude obsluhovat pouze jednu rutinu přerušení. Pokud chcete můžete využít služeb DMA a nechat si oba výsledky měření přenášet někam do paměti (Kanál 2 a Kanál 5 DMA). My výsledky čas od času pošleme pomocí USART na terminál v PC. Typicky je ale budete zpracovávat nějak jinak.

Celý zdrojový kód

void init_tim2(void){
LL_TIM_InitTypeDef tim;
LL_TIM_StructInit(&tim);
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
tim.Autoreload = 0xFFFFFFFF; // strop na maximum (nechceme si omezovat rozsah měření)
tim.Prescaler = 0; // prescaler na minimum (nechceme si ničit přesnost měření - zničíme ji až pak výpočtem)
LL_TIM_Init(TIM2,&tim);

// do Capture registru 1 zavedeme signál TI2FP1 (z kanálu TIM2_CH2) s detekcí sestupné hrany, bez filtru, bez předděličky
LL_TIM_IC_Config(TIM2,LL_TIM_CHANNEL_CH1, LL_TIM_ACTIVEINPUT_INDIRECTTI | LL_TIM_ICPSC_DIV1 | LL_TIM_IC_FILTER_FDIV1 | LL_TIM_IC_POLARITY_FALLING);
// do Capture registru 2 zavedeme signál TI2FP2 (z kanálu TIM2_CH2) s detekcí vzestupné hrany, bez filtru, bez předděličky
LL_TIM_IC_Config(TIM2,LL_TIM_CHANNEL_CH2, LL_TIM_ACTIVEINPUT_DIRECTTI | LL_TIM_ICPSC_DIV1 | LL_TIM_IC_FILTER_FDIV1 | LL_TIM_IC_POLARITY_RISING);
// jako trigger slouží signál TI2FP2 (vzestupná hrana na TIM2_CH2 - začátek signálu)
LL_TIM_SetTriggerInput(TIM2,LL_TIM_TS_TI2FP2);
// specifikujeme jak se má s příchozím triggrem naložit - resetovat timer (od vzestupné hrany začínáme měřit čas)
LL_TIM_SetSlaveMode(TIM2,LL_TIM_SLAVEMODE_RESET);

// povolíme si přerušení od kanálu 2 (dokončení měření)
NVIC_SetPriority(TIM2_IRQn,2);
NVIC_EnableIRQ(TIM2_IRQn);
LL_TIM_EnableIT_CC2(TIM2);

// povolíme oba kanály (input capture 1 a 2)
LL_TIM_CC_EnableChannel(TIM2,LL_TIM_CHANNEL_CH1 | LL_TIM_CHANNEL_CH2);
LL_TIM_EnableCounter(TIM2); // spustíme timer
}

// přerušení od timeru 2
void TIM2_IRQHandler(void){
 // nezkoumám zdroj přerušení (vím že to je CC2, jiný jsem nepovolil)
 LL_TIM_ClearFlag_CC2(TIM2); // mažu vlajku přerušení
 t1=LL_TIM_IC_GetCaptureCH1(TIM2); // čas příchodu sestupné hrany (trvání pulzu log.1)
 t2=LL_TIM_IC_GetCaptureCH2(TIM2); // čas příchodu vzestupné hrany (trvání celé periody)
}

Kód si zaslouží další komentář. Strop Timeru volíme maximum (tedy 2^32) abychom si zbytečně neomezovali maximální měřenou délku. Prescaler volím 0, díky tomu je časové rozlišení timeru nejjemnější (1/48 us). Díky 32bit rozsahu je i tak nejdelší měřitelná perioda asi 90s. Konfiguraci vstupních kanálů je možné provádět strukturou a nebo funkcí LL_TIM_IC_Config(). Jejím prvním argumentem je kanál timeru. Druhý argument musí být složen ze čtyř maker.

Dále využíváme funkce LL_TIM_SetTriggerInput() kterou vybíráme zdroj pro TRGI signál (tedy signál do Trigger Controlleru). V našem případě TI2FP2. Poslední funkce která si zaslouží komentář je LL_TIM_SetSlaveMode() která specifikuje jak má Trigger Controller (respektive Timer) s příchozím TRGI signálem naložit. V našem případě má provést reset. Na závěr už jen nesmíme zapomenou oba kanály povolit pomocí funkce LL_TIM_CC_EnableChannel(). Použití této funkce odpadá pokud kanály konfigurujete pomocí struktury. Timer už pak stačí pouze spustit a nechat ho běžet. První výsledek měření ale musíme vyřadit, neboť nemáme kontrolu nad tím v jakém stavu bude měřený signál v okamžiku kdy timer spustíme. Pokud by byl signál opravdu divoký a vyskytovaly se v něm sekvence o vysokých frekvencích (jednotky MHz a vyšší) mohlo by se stát, že v rutině přerušení nestihnete vyčíst data z CCR registrů a dojde k přepsání některého z nich novou hodnotou. Když něco takového hrozí můžete pro jistotu kontrolovat vlajky "Overflow" událostí (pomocí funkcí LL_TIM_IsActiveFlag_CC1OVR() ...). Nastavená vlajka znamená, že došlo k přepisu příslušného CCR registru aniž by byl vyčten.

Několik řádek můžeme věnovat ještě zpracování dat. V rutině přerušení získáme dvě časové informace (periodu t2 a dobu trvání pulzu t1). Informace o střídě (Duty Cycle) zjistíme z poměru t1/t2. Je proto nezbytně nutné abychom se na hodnoty t1 a t2 koukali jako na neoddělitelnou dvojici. Před výpočtem si je tedy zkopírujeme do pomocných proměnných t1_calc a t2_calc. U nich nám nehrozí že je během zpracování v přerušení přepíšeme. Kopii ale musíme provádět s vypnutým přerušením abychom se vyvarovali přepsání jedné z nich během kopírování. Viz následující kus zdrojového kódu.

while (1){
  LL_mDelay(200); // ~5x za sekundu pošli výsledek měření na terminál
  __disable_irq(); // během kopírování t1 a t2 se hodnota žádné z nich nesmí změnit !
  t1_calc=t1; // zkopírujeme si aktuální informaci o střídě a periodě
  t2_calc=t2;
  __enable_irq();
  // trocha celočíselné matematiky (berte hodně s rezervou !)
  // když to má význam bývá lepší posílat surová data než je takhle kazit
  freq=100*(uint64_t)48000000/(uint64_t)t2_calc; // jednotkou frekvence je 0.01Hz
  dcl=(10000*(uint64_t)t1_calc)/(uint64_t)t2_calc; // jednotkou střídy je 0.01%
  // trocha formátování, zjišťování celé části, desetinné části čísla atd.
  snprintf(text,sizeof(text),"%01lu.%02lu Hz, %01u.%02u %%\n\r",freq/100,freq%100,(uint8_t)(dcl/100),(uint8_t)(dcl%100));
  usart1_puts(text); // pošli zprávu do PC
  // celé to trvá cca 2ms
 }

Dovětek bych věnoval rozlišovací schopnosti. Rozlišení měření střídy je tím větší čím vyšší je frekvence timeru. S frekvencí 48MHz je časové rozlišení přibližně 0.02us. Než to obecně popisovat, stačí předvést dva konkrétní příklady ze kterých vám bude vše jasné. Jestliže chci měřit PWM signál jež má frekvenci 500kHz mohu počítat s tím, že rozlišení střídy bude přibližně 1% (500kHz => perioda 2us, 2us/0.02us = 100). Naopak v signálu s frekvencí 50Hz mohu bude rozlišení střídy rovno 20ms/0.02us = 1ppm (tedy jedna miliontina). Celé to úzce souvisí s tím jak pak informaci zobrazovat nebo prezentovat, ale to už je jiná kapitola.


Jen pro zábavu, výpis z terminálu při měření signálu, který jste mohli vidět na oscilogramu

Přímé měření periody

Jednoduchou modifikací výše uvedené ukázky můžeme timer uzpůsobit k měření periody. Stačí vypustit detekci sestupné hrany. Zavedeme tedy signál například do vstupu 1, necháme ho projít digitálním filtrem a detektorem hran a výsledný impulz (TI1FP1) přivedeme jak do Capture registru tak do Trigger controlleru. Stejně jako v předchozím příkladě s příchodem vzestupné hrany dojde k zaznamenání času do capture registru a restartování čítače. Tentokrát ani nemusíme volat přerušení. Timer může měřit periodu neustále na pozadí a kdykoli bude aplikace potřebovat vyčte si aktuální hodnotu z CCR registru. Neuvěřitelně prosté :) Schema konfigurace si můžete prohlédnout níže.


Konfigurace timeru pro měření periody

void init_tim2(void){
LL_TIM_InitTypeDef tim;
LL_TIM_IC_InitTypeDef ic;

// konfigurace PA15 (TIM2_CH1)
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinMode(GPIOA,LL_GPIO_PIN_15,LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetAFPin_8_15(GPIOA,LL_GPIO_PIN_15,LL_GPIO_AF_2);

LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2); // clock pro TIM2
LL_TIM_StructInit(&tim);
tim.Autoreload = 0xFFFFFFFF; // maximum (nebudeme si zbytečně omezovat rozsah)
tim.Prescaler = 0; // 48MHz do timeru pro plné rozlišení
LL_TIM_Init(TIM2,&tim);

ic.ICActiveInput = LL_TIM_ACTIVEINPUT_DIRECTTI; // signál do CC1 je z TIM2_CH1
ic.ICPrescaler = LL_TIM_ICPSC_DIV1; // prescaler před CC1 nepotřebujeme
ic.ICPolarity = LL_TIM_IC_POLARITY_RISING; // detekujeme vzestupnou hranu
ic.ICFilter = LL_TIM_IC_FILTER_FDIV1; // filtr nepoužíváme
LL_TIM_IC_Init(TIM2,LL_TIM_CHANNEL_CH1,&ic); // aplikuj konfiguraci na CH1

LL_TIM_SetTriggerInput(TIM2,LL_TIM_TS_TI1FP1); // Trigger do timeru veden z TI1FP1 (CH1)
LL_TIM_SetSlaveMode(TIM2,LL_TIM_SLAVEMODE_RESET); // Triggerem resetovat timer

LL_TIM_CC_EnableChannel(TIM2,LL_TIM_CHANNEL_CH1); // povolit CH1
LL_TIM_EnableCounter(TIM2); // spustit timer
}

Ti kterým zdrojový kód s konfigurací timeru nestačí si mohou stáhnout celou ukázku zde. V ukázce měřím periodu signálu přiváděného na PA15 a informaci čas od času odesílám usartem (PA9) do terminálu na PC. V ukázce využívám "ořezanou" implementaci funkce printf (ze souboru tiny_printf.c). Odesílací funkce je pak v souboru syscalls.c, proto je celá ukázka v rar archivu.

Doufám že jste z příkladů získali představu jak timer konfigurvat a že se setkáme u dalších dílů tutoriálu.

Home
V1.00 16.8.2017
By Michal Dudka (m.dudka@seznam.cz)