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.
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.
HCLK | PCLK | Timer |
---|---|---|
48 MHz | 48 MHz | 48 MHz |
48 MHz | 24 MHz | 48 MHz |
48 MHz | 12 MHz | 24 MHz |
36 MHz | 36 MHz | 36 MHz |
36 MHz | 12 MHz | 24 MHz |
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.
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 }
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.
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í).
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ázkyvoid 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() až 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.
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.
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); }
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 - 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.
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.
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); }
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.
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:
Prescaler | Strop | frekvence | rozlišení v intervalu (1 - 2)ms |
---|---|---|---|
48 | 20000 | 50Hz | 1000 |
24 | 40000 | 50Hz | 2000 | 15 | 64000 | 50Hz | 3200 |
// 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); }
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.
Home
V1.20 3.8.2017
By Michal Dudka (m.dudka@seznam.cz)