STM32 Timery

Timery na STM32 jsem si velice oblíbil. Ať si vymyslíte sebe šílenější úlohu skoro vždycky je dokážete nakonfigurovat tak aby ji zvládali autonomně a s hardwarovou přesností. Ruku v ruce se širokými schopnostmi jde i jistá složitost. Naštěstí jsou timery rozděleny do funkčních celků a je jen na vás kterým z nich porozumíte a jaké možnosti se tak před vámi otevřou. Designéři STM s nimi nijak nešetřili a tak se jich na některých čipech sejde klidně 16. Než si začneme vyprávět o tom jak fungují, měli bychom si v nich udělat trochu pořádek. Timery nesou jména TIM1,TIM2,TIM3 atd. a jsou až na výjimky mezi různými řadami čipů shodné. Jinak řečeno jestliže máte na STM32F051 timer TIM3 s nějakými schopnostmi, tak na libovolném jiném čipu, který má také TIM3 budou jeho schopnosti stejné. Což má mimo jiné tu výhodu, že tahle sada tutoriálu bude uplatnitelná na libovolné čipy. Pokud si chcete udělat přehled o kategoriích timerů, podívejte se do appnote AN4776 General-purpose timer cookbook. Timery se od sebe liší v podstatě jen ve výbavě.

Jádrem každého timeru je prescaler, counter a auto-reload registr (strop timeru). Prescaler slouží jako dělička vstupního signálu (ať už se bere odkudkoli). Z něj jde signál do counteru (čítače), který se s každým "tiknutím" inkrementuje. Jakmile dopočítá do svého stropu (Auto-Reload register), přeteče do nuly a nastaví UPDATE vlajku. Vznikne tzv UPDATE událost a při ní se může odehrát spousta zajímavých věcí jako například odeslání pulzu k jiné periferii nebo třeba k zavolání přerušení. K tomuto jádru se pak dolepují různé další moduly. Abychom si ale neukousli zbytečně velké sousto, zůstaneme zatím jen u toho jádra a podíváme se na Basic timer (TIM6). Ten totiž nic jiného než toto holé jádro neobsahuje.


Schema TIM6 z datasheetu

TIM6 má jen jeden zdroj signálu a tím je clock ze sběrnice APB (na níž je timer připojen). Pokud nepoužíváte APB prescaler (více v kapitole o clocku), je frekvence jdoucí do timeru rovna frekvenci na APB sběrnici. Pokud ale prescaler používáte, jde do timeru vždy dvojnásobek frekvence než je na ABP. Smysl tohoto opatření je v tom aby mohly timery běžet s nejvyšší frekvencí i na rychlejších čipech (všechny krom F0 a L0), kde je frekvence APB omezená. Raději do tabulky shrnu několik různých konfigurací HCLK,PCLK a frekvence timeru.

HCLKPCLKTimer
48 MHz48 MHz48 MHz
48 MHz24 MHz48 MHz
48 MHz12 MHz24 MHz
36 MHz36 MHz36 MHz
36 MHz12 MHz24 MHz

Jak jsme si už řekli, clock prochází prescalerem. Ten jej může dělit libovolným 16bit číslem a snížit tak frekvenci přicházejícího signálu. Máme-li například clock 48MHz a chceme aby do counteru přicházela frekvence 1MHz (tedy aby inkrementoval každou mikrosekundu), stačí prescaler nastavit na dělení 48. Protože ale prescaler počítá od nuly je do něj v takovém případě nutné zapsat hodnotu 47. Dalším klíčovým prvkem je Auto-Reload registr, tedy strop counteru. Stejně jako u prescaleru je do něj nutné zapisovat číslo počítané "od nuly". Chceme-li aby přetekl po započtení 1000 impulzů, je nutné strop nastavit na 999. Pozor si dejte při změně prescaleru, ta neprobíhá okamžitě, ale až s přetečením counteru. To je ale možné vyvolat uměle funkcí LL_TIM_GenerateEvent_UPDATE(). A když jsme ty funkce nakousli, pojďme se podívat na sezam těch základních.

Pokud jste pozorně četli, všimli jste si, že jsem do seznamu zařadil i funkci nastavující One-Pulse režim aniž bych vám jej objasnil. Hned to napravím. One-Pulse mód slouží k tomu aby celý cyklus timeru proběhl jen jednou. Je-li nastaven, timer se po UPDATE události (tedy typicky po přetečení) vypne. Teď vás asi nenapadá žádné rozumné využití, ale v kombinaci s jinými prvky pokročilejších timerů se jich najde celá řada. Několik dalších funkcí, které momentálně nemají žádný pořádný význam jsem vám zatím zamlčel. Bude o nich řeč později, teď je čas na první ukázku.

6A - Basic Timer periodické přerušení

Basic Timer může vykonávat různé podřadné úkoly. Za všechny jmenuji například orientační měření času založené na ručním spouštění a zastavování timeru (tak měří čas uživatelé Arduina). Jenže takovou hrubou a nepřesnou práci nemá smysl demonstrovat v příkladu. Ukážeme si proto sice suchý ale o poznání použitelnější příklad - Periodické přerušení. Náš čip (STM32F051) obsahuje jen jeden Basic Timer a to TIM6. Čip budeme taktovat na plných 48MHz. SYSCLK,HCLK i PCLK poběží také na 48MHz. Tím je jasné jaká frekvence půjde do TIM6. Budeme realizovat přerušení každých 500ms a v rutině přerušení budeme blikat s LEDkou. Abychom mohli realizovat čas 500ms, musíme správně nakonfigurovat prescaler a strop timeru. Existuje více správných konfigurací. Můžeme dát například prescaler na hodnotu 48000 a do timeru tedy pustit frekvenci 1kHz a strop nastavit na 500. Nebo můžeme prescaler nastavit na 2400 a strop na 10000. Kombinací je mnoho. Pokud by byla požadovaná frekvence nějaká atypická (například 3.867 Hz), může být volba prescaleru a stropu složitý úkol (a typicky nepovede k přesnému řešení). O tom jak se povoluje přerušení jste se už mohli dočíst v tutoriálu o EXTI. Takže jen ve zkratce. Nejprve je nutné nakonfigurovat NVIC. V našem případě povolíme přerušení s názvem TIM6_DAC_IRQn. Jde o sdílené s přerušení od TIM6 a od DA převodníku. Pak musíme nakonfigurovat zdroje přerušení v timeru. U Basic timeru máme na výběr jen jeden a to je přerušení od přetečení (UPDATE události). Před tím než ho povolíte je dobré smazat UPDATE vlajku. Protože pokud by z jakéhokoli důvodu zůstala nastavená, přerušení by se zavolalo ihned po jeho povolení. V rutině přerušení pak nesmíme zapomenout UPDATE vlajku mazat. Pokud bychom využívali přerušení i od DA převodníku, museli bychom navíc rozpoznat kdo přerušení vyvolal. Víc komentáře si příklad nezaslouží. Důležité části zdrojového kódu si můžete prohlédnout dole (celý kód lze stáhnout pomocí odkazu)

Celý zdrojový kód ukázky

// konfigurace a spuštění TIM6
void init_tim6(void){
LL_TIM_InitTypeDef tim;  // deklarace konfigurační struktury ...
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM6); // spustíme clock do TIM6
LL_TIM_StructInit(&tim); // předplníme strukturu výchozími hodnotami
// a nastavíme jen položky které nás zajímají
tim.Prescaler = 47999; // clock timeru = 48MHz/48000 = 1kHz
tim.Autoreload = 499; // strop časovače je 500 => perioda 0.5s
LL_TIM_Init(TIM6,&tim); // aplikujeme nastavení na TIM6

// nastavíme prioritu a povolíme přerušení v NVIC
NVIC_SetPriority(TIM6_DAC_IRQn,2);
NVIC_EnableIRQ(TIM6_DAC_IRQn);

// povolíme přerušení od přetečení v TIM6
LL_TIM_ClearFlag_UPDATE(TIM6); // nejprve pro jistotu vymažeme vlajku
LL_TIM_EnableIT_UPDATE(TIM6); // a přerušení povolíme...

// nakonec čítač spustíme
LL_TIM_EnableCounter(TIM6);
}


// rutina přerušení od TIM6
void TIM6_DAC_IRQHandler(void){
// vyčistíme vlajku (likvidujeme zdroj přerušení)
LL_TIM_ClearFlag_UPDATE(TIM6);
LL_GPIO_TogglePin(GPIOC,LL_GPIO_PIN_8); // přepínáme LED
}

General Purpose Timer

Když už chápeme jádro timeru, je na čase udělat si prohlídku po jeho dalších funkcích. Provedeme ji nejprve stručně aby vám v detailech neunikly spojitosti. Až později se budeme věnovat jednotlivým blokům detailněji. Roztočte mozkové závity na plné obrátky, protože další obrázek to bude potřebovat.


Blokové schema General purpose timeru - velmi užitečný pomocník při jeho konfiguraci

Nejprve se projdeme po obvodu schematu. Všimněte si čtyř kanálů TIMx_CH1 až TIMx_CH4. Ty mohou sloužit jako vstupy i jako výstupy (záleží na konfiguraci). Vlevo jsou znázorněny v roli vstupů, vpravo pak v roli výstupů. TIMx_ETR slouží výhradně jako vstup. Nejčastěji funguje jako zdroj vnějšího clocku nebo jako trigger (ale umí i další věci). Dále se zaměříme na zdroje vnitřního signálu. Prvním z nich je CK_INT, s tím jsme už měli tu čest, je to clock z APB sběrnice. A pak je tu čtveřice signálu ITR0 až ITR3, těmi lze přivádět signál od ostatních timerů. Stejně tak je přítomná TRGO linka, která naopak vede signál do ostatních timerů (a nejen do nich). Vstup ETR je vybaven bloky k předzpracování přicházejícího signálu. Lze pomocí nich obracet polaritu, detekovat hrany, dělit a filtrovat. Pak je tu ustření blok nazvaný Trigger Controller. Ten se stará o manipulaci se samotným counterem. Může mu volit zdroj signálu, může ho mazat, zapínat a vypínat a může mu volit směr čítání a to vše v reakci na některý ze svých vstupů. Bohatou výbavu má také každý z Compare/Capture kanálů čítače (dále jim budu říkat jen "kanál"). V režimu vstupu můžete vidět vstupní filtr, detektor hran i prescaler. K tomu všemu je na vstupu sada multiplexerů umožňující signály různě přesměrovávat. Většina těchto vstupů pak vede do Capture / Compare registru. To je klíčový prvek, který má dvě role. V roli Capture se v reakci na vnější podnět uloží do registru sav counteru. Záznam proběhne okamžitě, což má přirozeně využití při měření časové informace. V druhé roli, tzv. Compare, se neustále porovnává hodnota Counteru a Compare/Capture registru. V okamžiku kdy jsou si oba rovny nastane tzv Compare událost, která může vyvolat nějakou akci (přerušení, DMA request a další). Krom toho výsledek porovnání může ovlivňovat stav výstupu timeru a generovat tak autonomně různé průběhy. V rámci celého schematu jsou signály vedeny mezi různými bloky a znázorňují jaké bloky a signály se mohou vzájemně ovlivňovat. Za povšimnutí stojí malé ikonky "eventů" a "přerušení / DMA requestů", které naznačují, že daný blok může tuto akci iniciovat. U vstupů kanálu CH1 až CH3 se krčí malý XOR blok, který umožňuje vytvořit interface pro signál z Hallových senzorů snímajících otáčky motoru. Potěší také Encoder Interface umožňující připojit na první dva kanály inkrementální enkodér (příjemný prvek uživatelského ovládání).

6B - Přerušení od compare událostí

V této ukázce si předvedeme jednoduché využití Compare události. Prescalerem zredukujeme vstupní clock do timeru na 1kHz. Periodu timeru nastavíme na 1000ms (tedy jednu sekundu). Compare registry kanálu CH1 až CH3 si naplníme hodnotami 800,820 a 980. Tím zařídíme že ke Compare událostem na jednotlivých kanálech dojde 800,820 respektive 980ms po startu timeru. U všech tří compare událostí a u přetečení timeru si povolíme přerušení. Každou sekundu tedy dojde k volání rutiny přerušení čtyřikrát. Při prvním volání, v čase 800ms rozsvítíme LED, o 20ms později, kdy dojde k vyvolání přerušení od druhého kanálu ji zhasneme. Celou akci pak zopakujeme o dalších 180ms později. V přerušení od Compare události kanálu CH3 opět rozvítíme LED a s přerušením od přetečení timeru ji zhasneme. Vznikne tak krátký dvojblik, jehož celé časování se odehrává na pozadí. V praxi se najde docela dost úloh na "časování", které bude možné takovým způsobem řešit. Naposledy mi metoda posloužila při otevírání závěrek v experimentu. Těm totiž otevření nějakou dobu trvá a je potřeba do ní pustit signál s předstihem. Přirozeně není problém nastavit timer tak aby neběžel periodicky, ale aby se rozběhl až na nějaký vnější nebo vnitřní podnět (ale o tom až později). Všimněte si, že TIM3 má jen jednu rutinu přerušení, za to zdrojů které mohou přerušení vyvolat je více. Proto je nutné si v rutině přerušení zjistit který zdroj jej vyvolal a nezapomenout pak smazat jeho vlajku (podle níž jsme zdroj identifikovali).

Celý zdrojový kód ukázky

void TIM3_IRQHandler(void){
// teď musíme rozpoznat která ze čtyř událostí vyvolala rutinu přerušení
if(LL_TIM_IsActiveFlag_CC1(TIM3)){ // je to compare událost prvního kanálu ?
 LL_TIM_ClearFlag_CC1(TIM3); // událost jsme obsloužili, mažeme její vlajku
 LL_GPIO_SetOutputPin(GPIOC,LL_GPIO_PIN_9); // rozsviť LED
 }
if(LL_TIM_IsActiveFlag_CC2(TIM3)){ // je to compare událost druhého kanálu ?
 LL_TIM_ClearFlag_CC2(TIM3); // událost jsme obsloužili, mažeme její vlajku
 LL_GPIO_ResetOutputPin(GPIOC,LL_GPIO_PIN_9); // zhasni LED
 }
if(LL_TIM_IsActiveFlag_CC3(TIM3)){ // je to compare událost třetího kanálu ?
 LL_TIM_ClearFlag_CC3(TIM3); // událost jsme obsloužili, mažeme její vlajku
 LL_GPIO_SetOutputPin(GPIOC,LL_GPIO_PIN_9); // zase rozsviť LED
 }
if(LL_TIM_IsActiveFlag_UPDATE(TIM3)){ // je to přetečení timeru ?
 LL_TIM_ClearFlag_UPDATE(TIM3); // událost jsme obsloužili, mažeme její vlajku
 LL_GPIO_ResetOutputPin(GPIOC,LL_GPIO_PIN_9); // a zase zhasni LED
 }
}

void init_tim3(void){
LL_TIM_InitTypeDef tim;
LL_TIM_OC_InitTypeDef oc;
// inicializujeme struktury (abychom je nemuseli vyplňovat celé)
LL_TIM_StructInit(&tim);
LL_TIM_OC_StructInit(&oc);
// povolíme clock pro TIM3
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3);
tim.Prescaler = 47999; // 1kHz do timeru
tim.Autoreload = 999; // přetečení každou sekundu
LL_TIM_Init(TIM3,&tim);

// nastavíme komparační hladiny kanálů CH1 až CH3
oc.CompareValue = 800; // 0.8s - rozsvítit
oc.OCMode = LL_TIM_OCMODE_FROZEN; // žádná akce s výstupy
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH1,&oc);
oc.CompareValue = 820; // 0.82s - zhasnout
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH2,&oc);
oc.CompareValue = 980; // 0.98s - rozsvítit (a s updatem zhasnout)
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH3,&oc);

// povolíme přerušení od přetečení + od všech tří compare kanálů
LL_TIM_EnableIT_UPDATE(TIM3);
LL_TIM_EnableIT_CC1(TIM3);
LL_TIM_EnableIT_CC2(TIM3);
LL_TIM_EnableIT_CC3(TIM3);
// což mimochodem šlo klidně zapsat jedním úspornějším příkazem v CMSIS
// TIM3->DIER |= TIM_DIER_UIE | TIM_DIER_CC1IE | TIM_DIER_CC2IE | TIM_DIER_CC3IE;

// v NVIC povolíme přerušení od TIM3
NVIC_SetPriority(TIM3_IRQn,3); // nízká priorita
NVIC_EnableIRQ(TIM3_IRQn);

// a čítač spustíme
LL_TIM_EnableCounter(TIM3);
}

Pár poznámek ke konfiguraci Capture/Compare (CC) kanálů. Existuje relativně přehledná varianta jejich konfigurace pomocí struktury. Pro konfiguraci v roli Compare slouží strukura LL_TIM_OC_InitTypeDef. Konfigurace se do Timeru nahraje voláním funkce LL_TIM_OC_Init(). Opět je možné strukturu předplnit výchozími hodnotami skrze funkci LL_TIM_OC_StructInit(). Struktura se přirozeně hodí hlavně k inicializaci nebo ke kompletní rekonfiguraci kanálu. Pokud aplikace potřebuje měnit jen jednotlivé parametry kanálu, může k nim přistupovat skrze sadu funkcí. Momentálně vás však bude zajímat pouze skupina funkcí LL_TIM_OC_SetCompareCH1()LL_TIM_OC_SetCompareCH4(). Pomocí kterých můžete obsah Compare registru měnit. Všimněte si předpony LL_TIM_OC_, kterou jsou pojmenovány funkce ovládající kanál v roli Compare.

6C - Generování frekvencí

Název této ukázky je trochu nejednoznačný, takže ho upřesním. V následujících několika ukázkách si předvedeme jak pomocí Compare události ovládat výstupy timeru. Režimů je hodně, takže si vybereme jen ty na první pohled nejzajímavější. Ukázku provedeme opět na timeru TIM3. Ten může ovládat piny PC6,PC7,PC8,PC9,PA6,PA7,PB0,PB1,PB4 a PB5, což poznáte podle označení TIM3_CH1 až TIM3_CH4. My použijeme čtveřici PC6 až PC9. Vybrané piny přirozeně nakonfigurujeme jako "alternate function". K nastavení kanálu do režimu Compare můžeme použít buď jednotlivé funkce a nebo jej konfigurovat strukturou. Konfigurace stukturou je jistější, protože se s voláním funkce LL_TIM_OC_Init() vždy nastaví všechny parametry kanálu. Takže nezáleží na tom jak byl nastaven před tím. Struktura má čtyři prvky.

S poslední položkou seznamu se vypořádáme jako první. Pracovních režimů specifikujících jak může timer zacházet s výstupním pinem je přibližně 8. První a nejjednodušší co může být je režim "Frozen", v něm timer výstup nijak nemění (což ale neznamená že ho neovládá). Pak jsou režimy "Active" a "Inactive". Ty fungují jednorázově, tedy jakmile nastane compare událost, timer nastaví výstup do log.1 (režim "active") nebo do log.0 (režim "Inactive"). A pokud s ním nic dalšího sami neuděláte tak si svůj stav ponechá. Dalším režimem je "Toggle", v něm timer při compare události přepne výstupní hodnotu (tento režim budeme v ukázce používat). Pak je tu dvojice režimů "Forced Active" a "Forced Inactive". Ty slouží k tomu aby jste mohli manuálně ovládat výstupy i když je má pod kontrolou timer. Poslední dvojice režimů je "PWM1" a "PWM2" jejichž smysl je asi jasný - slouží ke generování pulzně šířkové modulace (PWM) a povíme si o nich více později. Aby měl timer nad výstupem kontrolu, musí být parametr OCState povolen (Enable). Všechny tyto a ještě mhohé další parametry můžete ovládat i jednotlivě pomocí vybraných funkcí.

Naše aplikace využije režimu "Toggle" k tomu aby generovala fázově posunuté signály. Frekvenci timeru nastavíme prescalerem na 1MHz a periodu na 1ms. Generovaný průběh v režimu toggle má vždy střídu 50% (log.1 trvá stejně dlouho jako log.0) - pokud tedy zrovna neměníte strop časovače. Využití najde všude tam kde potřebujete generovat signály s nastavitelnou frekvencí. Protože k přepnutí výstupu dochází v okamžiku compare události, máme dobrou kontrolu nad tím v jakých časových rozestupech k tomu na jednotlivých kanálech dojde. Kanál 2 nechám přepínat o 100us později než kanál 1. Kanál 3 pak o 200us později než kanál 1. Kanál 4 si nechám na hraní a budu jeho fázový posun (časový odstup od přepnutí na kanálu 1) postupně měnit (viz animace níže). Abych jeho změnu nějak načasoval, využiji přerušení od přetečení (Update), které nastává každou milisekundu. Budu v něm odpočítávat čas a každých 100ms změním hodnotu Compare registru na kanálu 4 a tím i jeho posun vůči kanálu 1. A to je vše. Nehledejte v tom žádný smysl, je to jen takové hraní aby jste se seznámili s možnostmi timeru. Výsledek pokusu si můžete prohlédnout na gif-animaci pod zdrojovým kódem.

Celý zdrojový kód ukázky

// rutina přerušení timeru
void TIM3_IRQHandler(void){
static uint16_t counter;
static uint16_t phase=0;
LL_TIM_ClearFlag_UPDATE(TIM3); // nezapomeneme smazat vlajku
counter++; // odpočítáváme 100ms
if(counter>=100){ // každých 100ms změníme fázi na kanálu 4
 counter=0; // počítáme znovu od začátku
 phase=phase+10; // zpozdi kanál 4 o dalších 10us
 if(phase>999){phase=0;} // víc jak 1000 nemá smysl (strop časovače)
 LL_TIM_OC_SetCompareCH4(TIM3,phase); // nastav fázi na CH4
 }
}

// konfigurace timeru
void init_tim3(void){
LL_TIM_InitTypeDef tim;
LL_TIM_OC_InitTypeDef oc;
// inicializujeme struktury (abychom je nemuseli vyplňovat celé)
LL_TIM_StructInit(&tim);
LL_TIM_OC_StructInit(&oc);
// povolíme clock pro TIM3
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3);
tim.Prescaler = 47; // 1MHz do timeru (clock čipu 48MHz)
tim.Autoreload = 999; // přetečení každou ms (1kHz)
LL_TIM_Init(TIM3,&tim);
// část nastavení shodná pro všechny kanály
oc.OCPolarity = LL_TIM_OCPOLARITY_HIGH; // tímto lze kanál invertovat
oc.OCState = LL_TIM_OCSTATE_ENABLE; // přidělujeme timeru ovládání výstupu
oc.OCMode = LL_TIM_OCMODE_TOGGLE; // přepnout výstup při Compare události

oc.CompareValue = 99; // 100us po přetečení timeru
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH1,&oc);

oc.CompareValue = 199; // 200us po přetečení timeru
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH2,&oc);

oc.CompareValue = 299; // 300us po přetečení timeru
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH3,&oc);

oc.CompareValue = 399; // 400us po přetečení timeru
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH4,&oc);

LL_TIM_EnableIT_UPDATE(TIM3); // povolím přerušení od přetečení
NVIC_SetPriority(TIM3_IRQn,3); // nízká priorita
NVIC_EnableIRQ(TIM3_IRQn);
// a čítač spustíme
LL_TIM_EnableCounter(TIM3);
}


Gif Animace průběhu na výstupech timeru (patrná perioda 2ms, rozestup hran signálů po 100us)

Ukázka si zaslouží malý dovětek, který můžete klidně přeskočit. Náš program mění hodnotu Compare registru aby změnil zpoždění nebo předstih průběhu na kanálu 4. Mění ji ale jen o malý kousek (10us), takže si okem nemusíte všimnout následků jaké může mít takto akce na tvar průběhu. Abychom se v tom lépe orientovali, dívejme se na signál (na kanálu 4) jako na kladné pulzy o šířce 1ms oddělené od sebe 1ms mezerou. Aby jste zvětšili zpoždění kanálu 4, musíte ho v konečném důsledku trošku zbrzdit. Nevyhnutelně musíte buď prodloužit některou mezeru mezi pulzy a nebo některý z pulzů (a nebo obě). O tom jak ke změně dojde rozhoduje okamžik kdy Compare registr přepíšete. Takže pokud by vám záleželo na tom aby pulz zůstal pořád 1ms dlouhý a všechny změny fáze by jste chtěli provádět výhradně protažením nebo zkrácením mezery mezi pulzy, musíte zajistit, že ke změně Compare registru dojde jen v době kdy je na výstupu mezera. To je v případě relativně dlouhých mezer jednoduché, ale u kratších pulzů může řešení narazit. Naštěstí máme k takovému účelu hardwarové prostředky - funkci Preload. O ní se více dozvíte v tutoriálu o PWM, kde bude nepostradatelná.

PWM

PWM - Pulzně šířková modulace je velmi šťavnaté sousto. Nejspíš sami víte že výčet možných použití je skoro nekonečný. Můžeme začít jasem LED ať už v domácnosti nebo podsvětlením displeje. Pokračovat můžeme výkonovou regulací topných těles a otáček stejnosměrných motorů. Pomocí PWM můžeme regulovat proud, ovládat DC-DC měniče, generovat audiosignál nebo jej využívat jako nízkofrekvenční DA převodník. Proto se některé jeho možnosti pokusím demonstrovat alespoň na dvou příkladech.

Přehled PWM režimů

Díky tomu, že timer umí čítat nahoru i dolů, máme k dispozici dva PWM režimy. První z nich se nazývá "Edge aligned" a výstup má jednu logickou hodnotu dokud je v counteru menší hodnota než v compare registru a opačnou hodnotu jindy. Zda to bude log.1 nebo log.0 záleží na volbě režimu PWM1 nebo PWM2, případně na tom zda je výstup příslušného kanálu invertovaný. Analogicky funguje PWM režim i když timer čítá směrem dolů. Nechtěl bych ale zabíhat příliš do detailů, protože bychom mohli v záplavě kombinací zabloudit. Pro detaily vás tedy odkážu do datasheetu. Tento režim v kombinaci s One-Pulse módem (o němž už byla řeč) tvoří ideální nástroj na generování pulzů a budeme se mu věnovat později.

Druhý PWM režim se nazývá "Center-aligned" a jak už název napovídá jsou při něm výstupní průběhy na všech kanálech zarovnané "na střed". V této chvíli ještě netuším zda se pro něj v tutoriálu najde místo, takže se nechte překvapit.

6E - PWM poprvé (Regulace jasu LED)

Stručný přehled PWM režimů máme za sebou a můžeme se vrhnout na ukázky. V první z nich budeme pomocí PWM regulovat jas LED. Můžete jej použít k nastavení barvy RGB LED, k regulaci jasu LED osvětlení nebo podsvícení displejů. A aby to nebyla taková nuda budeme jas dynamicky měnit. Využijeme obě LED na Discovery kitu (na PC8 a PC9) a budeme jejich jas měnit se "sinusovým" průběhem aby rozsvěcení a zhasínání LED působilo plynulým dojmem.

Frekvence PWM není u takové aplikace příliš důležitá. Stačí aby blikání nebylo patrné lidským okem. V ukázce budeme využívat PWM s 8bitovou hloubkou, protože pro plynulé stmívání malé LEDky je 256 úrovní jasu víc než dost. Stačilo by i méně - odhadem tak 64 úrovní. Tabulka udávající průběh jasu v čase bude mít také 256 prvků (opět by k tomuto účelu asi stačilo méně). Celá tabulka nám tak zabere 256B paměti a protože je konstantní, bude uložena ve Flash paměti nikoli v RAM. Díky tomu, že jde o sinusový průběh, stačilo by teoreticky skladovat v paměti jen čtvrtinu tabulky a zbylé hodnoty by bylo možné snadno dopočítávat. Podobných metod jak ušetřit paměť je hodně. Ale náš kit má 64kB Flash paměti, takže si nebudeme dělat zbytečné násilí a zůstaneme u 8bit hloubky PWM a tabulce o 256 krocích. Naopak určitě můžete najít aplikace, kde budete vyžadovat jemnější krok změny jasu a využijete klidně plné 16bit rozlišení timeru.

Prescaler timeru jsem volil 256 a strop také 256 což vede k frekvenci PWM 732.4Hz (48MHz/256/256) a periodě 1,37ms. Nikdo vám nebrání prescaler zvýšit a zredukovat si frekvenci až někde ke 120Hz (nižší frekvenci už by někdo mohl vnímat jak blikání). Poslední otázku kterou musíme vyřešit je frekvence s jakou budeme měnit jas LED a tedy dobu trvání postupného ztmavení a zesvětlení LED (prostě celého cyklu). Protože na to nejsou kladeny žádné konkrétní nároky, zvolil jsem že ke změně jasu dojde každé 4 periody timeru. V rutině přerušení kde dochází ke změně jasu je proměnná counter která slouží k identifikaci každého čtvrtého přetečení. Celý cyklus tedy trvá 1.37ms*256*4 = 1.4s. To je doba za kterou by se měla LED plynule rozsvítit a zhasnout. Pokud by tato doba byla daná nějakými vnějšími okolnostmi, museli bychom využít jiný timer aby realizoval periodické přerušení a během něj měnit jas. Pak bychom měli kompletní kontrolu nad dobou jednoho cyklu. S využitím DMA bychom mohli celé blikání realizovat s pomocí dvou timerů s plně laditelnou frekvencí a zcela bez intervence jádra. Tedy vše to co teď vidíte by mohlo probíhat na pozadí. Ale o tom až v dalším dílu (zde). Teď se pojďte podívat na zdrojový kód a průběh z osciloskopu (gif-animace).

Celý zdrojový kód

// rutina přerušení timeru (Přetečení)
void TIM3_IRQHandler(void){
static uint16_t counter=0;
LL_TIM_ClearFlag_UPDATE(TIM3); // nezapomeneme smazat vlajku
counter++; // počítadlo period
if(counter>=COUNTS){ // každou n-tou periodu nastav nový jas LED
 counter = 0;
 if(i<255){i++;}else{i=0;} // index dalšího vzroku z tabulky
 if(j<255){j++;}else{j=0;} // index dalšího vzroku z tabulky
 LL_TIM_OC_SetCompareCH3(TIM3,pwm_table[i]); // nastav PWM na CH3
 LL_TIM_OC_SetCompareCH4(TIM3,pwm_table[j]); // nastav PWM na CH4
 }
}

// konfigurace timeru
void init_tim3(void){
LL_TIM_InitTypeDef tim;
LL_TIM_OC_InitTypeDef oc;
// inicializujeme struktury (abychom je nemuseli vyplňovat celé)
LL_TIM_StructInit(&tim);
LL_TIM_OC_StructInit(&oc);
// povolíme clock pro TIM3
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3);
tim.Prescaler = 256; // 48M/256 = 187.5kHz do timeru
tim.Autoreload = 0xFF; // strop timeru 255, f=~732Hz (T=~1.37ms)
LL_TIM_Init(TIM3,&tim);

// použijeme kanály CH3 a CH4
oc.OCPolarity = LL_TIM_OCPOLARITY_HIGH; // polarita běžná
oc.OCState = LL_TIM_OCSTATE_ENABLE; // přidělujeme timeru ovládání výstupu
oc.OCMode = LL_TIM_OCMODE_PWM1; // režim PWM1
oc.CompareValue = pwm_table[i]; // startovací hodnota PWM pro CH3
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH3,&oc); // aplikujeme nastavení na CH3 (PC8)
oc.CompareValue = pwm_table[j]; // startovací hodnota PWM pro CH4
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH4,&oc); // aplikujeme nastavení na CH4 (PC9)
// nutné zapnout Preload - jinak hrozí vznik glitchů při změně PWM
LL_TIM_OC_EnablePreload(TIM3,LL_TIM_CHANNEL_CH3);
LL_TIM_OC_EnablePreload(TIM3,LL_TIM_CHANNEL_CH4);

LL_TIM_EnableIT_UPDATE(TIM3); // povolím přerušení od přetečení
NVIC_SetPriority(TIM3_IRQn,3); // nízká priorita
NVIC_EnableIRQ(TIM3_IRQn);
// a čítač spustíme
LL_TIM_EnableCounter(TIM3);
}


Postupně se měnící hodnota PWM. Kdo má připojené LED vidí krásné kolísání jasu

Funkce Preload

Jistě jste si ve zdrojovém kódu všimli funkce LL_TIM_OC_EnablePreload(). Ta zapíná "Preload" funkci pro příslušný kanál. Když ji zapnete není možné přímo měnit Compare registr. Když se do něj pokusíte zapsat, zapíšete ve skutečnosti hodnotu do "shadow" registru. Tam hodnota setrvá až do okamžiku přetečení timeru (Update události), teprve tehdy se vaše hodnota dostane do Compare registru. Je to důležitá funkce zabraňující vzniku "glitchů". Pokud totiž máte přímou kontrolu nad Compare registrem, můžete do něj zapsat i ve velmi nevhodný okamžik. Co se může stát ilustruji na obrázku dole. Na žlutém průběhu vidíte PWM u které dochází ke změně hodnoty střídy. Okamžik kdy dochází k zápisu do Compare registru je vyznačen sestupnou hranou modrého průběhu. Střídu měním z menší hodnoty na větší ve velmi nevhodný okamžik. Hodnota counteru (dejme tomu 100) je už vyšší jak hodnota Compare registru (dejme tomu 50) a na výstupu je log.0. V tomto okamžiku přepíšu obsah compare registru na hodnotu vyšší než je counter, dejme tomu na hodnotu 200. Od tohoto okamžiku je v Counteru méně než v Compare registru a na výstupu je log.1 až do okamžiku kdy Counter překročí Compare registr. Takto vznikne na výstupu nepěkný průběh - glitch. A není to jediná situace kdy může "glitch" vzniknout. Jediný bezpečný okamžik kdy měnit obsah Compare registru je právě s přetečením timeru. A na to je tu funkce Preload. Úplně stejný účel má i funkce LL_TIM_EnableARRPreload() ("Auto-reload preload"), která obdobným způsobem ošetřuje změnu stropu timeru.


Glitch - neošetřený zápis do Compare registru může vyústit v tento nepěkný průběh (žlutá)

6D - PWM podruhé (ovládání serva)

Další aplikací kde PWM využijete bude řízení servomotorku. Ten vyžaduje signál o frekvenci 50Hz s délkou pulzu od 1ms do 2ms. Úhel do kterého se má servo natočit je zakódována právě v délce pulzu (reskeptive v hodnotě střídy PWM). Pulz o délce 1ms servu říká že se má natočit do jedné krajní polohy (řekněmě 0°) a pulz o délce 2ms mu dává pokyn otočit se do úhlu 180°. Méně kvalitní serva mohou mít rozsah trochu menší. Po předchozí ukázce by pro vás řízení serva mělo být hračka. Pojďme si tedy jen ve stručnosti provést malý rozbor volby parametrů. Jak už bylo řečeno frekvence signálu má být 50Hz, jak tedy zvolit strop timeru a prescaler ? Do tabulky si nejprve shrneme několik variant:

PrescalerStropfrekvencerozlišení v intervalu (1 - 2)ms
482000050Hz1000
244000050Hz2000
156400050Hz3200

Z tabulky je patrné, že čím je Prescaler menší a strop timeru větší tím větší je rozlišení PWM. Serva mají ale omezenou přesnost i počet kroků na otáčku, takže pro ně bude bohatě stačit první varianta s prescalerem 48. I tak máme možnost ladit šířku pulzu po jedné us. Servo si připojíme na CH1 TIM3 (konkrétně na PC6). Aby naše aplikace nebyla úplně statická, necháme servo hýbat postupně tam a zpět. Protože postupná změna polohy vyžaduje nějaké časování, využiji zde opět rutiny přerušení abych šířku pulzu měnil. V reálné aplikaci bude ke změně docházet typicky na nějaký vnější podnět (uživatelský vstup nebo signál ze senzoru). Vy už se můžete podívat na zdrojový kód, pár ukázek toho jak signál vypadá a fotografii testovací sestavy (o níž bude ještě řeč).

Celý zdrojový kód

// rutina přerušení timeru
void TIM3_IRQHandler(void){
static uint16_t pwm=1500, direction=0;
LL_TIM_ClearFlag_UPDATE(TIM3); // nezapomeneme smazat vlajku
if(direction){  // pokud krokujeme nahoru
 if(pwm<1990){pwm=pwm+10;} // pokud je ještě kam krokovat zvětši PWM
 else{direction=0;} // jinak přehoď směr krokování
 }
else{
 if(pwm>1010){pwm=pwm-10;}  // zmenšuj PWM pokud je ještě kam
 else{direction=1;}  // jinak změň směr krokování
 }
LL_TIM_OC_SetCompareCH1(TIM3,pwm); // nastav nové PWM
}

// konfigurace timeru
void init_tim3(void){
LL_TIM_InitTypeDef tim;
LL_TIM_OC_InitTypeDef oc;
// inicializujeme struktury (abychom je nemuseli vyplňovat celé)
LL_TIM_StructInit(&tim);
LL_TIM_OC_StructInit(&oc);
// povolíme clock pro TIM3
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3);
tim.Prescaler = 47; // 1MHz do timeru (clock čipu 48MHz)
tim.Autoreload = 19999; // f=50Hz (T=20ms)
LL_TIM_Init(TIM3,&tim);

// použijeme jen kanál CH1
oc.OCPolarity = LL_TIM_OCPOLARITY_HIGH; // běžná polarita
oc.OCState = LL_TIM_OCSTATE_ENABLE; // přidělujeme timeru ovládání výstupu
oc.OCMode = LL_TIM_OCMODE_PWM1; // režim PWM1
oc.CompareValue = 1499; // počáteční šířka pulzu 1.5ms
LL_TIM_OC_Init(TIM3,LL_TIM_CHANNEL_CH1,&oc); // aplikujeme nastavení na CH1
// nutné zapnout Preload - jinak hrozí vznik glitchů při změně PWM
LL_TIM_OC_EnablePreload(TIM3,LL_TIM_CHANNEL_CH1); // !

LL_TIM_EnableIT_UPDATE(TIM3); // povolím přerušení od přetečení
NVIC_SetPriority(TIM3_IRQn,3); // nízká priorita
NVIC_EnableIRQ(TIM3_IRQn);
// a čítač spustíme
LL_TIM_EnableCounter(TIM3);
}


Gif-animace signálu pro servo


Statický snímek různých signálů pro servo


Fotografie testovací sestavy

Závěrečnou poznámku věnujeme problematice různých napětí serva a STM32. Typicky je servo napájeno 5V. I v předchozí ukázce jsem servo napájel z 5V z Discovery kitu (tedy z USB). Protože se u mnoha čínských serv nedohledáte jaké je minimální napětí na vstupu, které servo spolehlivě akceptuje jako log.1, nemáte jistotu že 3.3V z STMka bude stačit. Nejjednodušší je to prostě vyzkoušet. A ideálně to zkoušet nejen pro 3.3V ale i pro nějaká nižší napětí, třeba pro 3.1V aby jste měli jistou rezervu. Jenže co když servo 3.3V signál nespolkne spolehlivě. V takovém případě ho budete muset upravit na 5V. Než ho ale začnete uparvovat s pomocí tranzistorů nebo integrovaných obvodů, případně modulků z číny, vzpomeňte si na kapitolu o GPIO. Některé piny (jako námi použitý pin PC6) tolerují 5V napájení. Všechny piny pak umí v roli výstupu pracovat s "otevřeným kolektorem" (Open-Drain). Z toho plyne, že stačí mezi výstup STMka a 5V napájení připojit pullup rezistor (např 4k7) a nakonfigurovat výstup jako "Open-Drain" a vše je vyřešeno.

Odkazy

Home
V1.20 3.8.2017
By Michal Dudka (m.dudka@seznam.cz)