Porty se u STM32 nazývají GPIO (General Purpose Input Output), což je v podstatě daleko popisnější název než port a proto se ho budu držet. Vývody STM32 jsou organizovány do skupin po 16ti a tyto skupiny se označují GPIOx (např GPIOC, GPIOF atd.). Není však nutné aby byla celá 16tice fyzicky přítomna. Podíváte-li se například na rozložení pinů STM32F051 v pouzdře LQFP64 můžete si všimnout že GPIOA, GPIOB i GPIOC jsou kompletní 16tice. z GPIOD je na čipu pouze PD2 a z GPIOF jen PF0 a PF1.
Několik dalších pinů si zaslouží komentář:
Každý vývod může sloužit jako GPIO nebo mu může být přidělena tzv. alternativní funkce, kdy ovládání pinu podléhá nějaké periferii. Každý pin může sloužit více periferiím. Díky tomu máte při návrhu hardwaru volné ruce při výběru na které piny si vyvedete například USART nebo výstup časovače. Přehlednou tabulku všech alternativních funkcí hledejte v datasheetu pod názvem "Alternate functions". Každý sloupec tabulky je označen názvem AF0,AF1,AF2... a slouží jako kód k volbě alternativní funkce. Pokud se vám nechce listovat tabulkou, můžete využít nástroj CubeMX od ST, kde lze vybírat funkce jednotlivým pinům v příjemném grafickém prostředí.
K rozblikání LED si přirozeně musíte nejprve ujasnit jak je k STM připojena. Na STM32F0 Discovery je zelená LED připojena mezi PC9 a GND a modrá led je stejným způsobem připojena k PC8. Než začnete s GPIOC jakkoli pracovat musíte mu nejprve spustit clock. Po startu je totiž vypnutý. Poté musíte inicializovat piny. Což je možné hned několika způsoby, buď použijete funkce LL_GPIO_SetPinMode() (uvidíte v dalším příkladě) nebo strukturu LL_GPIO_InitTypeDef a fci LL_GPIO_Init(). Já v příkladu zvolil druhou variantu, protože umožňuje nastavovat více pinů zároveň. Ovládání pinů pak probíhá pomocí funkcí LL_GPIO_SetOutputPin() a LL_GPIO_ResetOutputPin(). Ty vužívají přístupu k registru BSRR a nastavení nebo vynulování pinu probíhá jako atomická operace. Vstupem obou funkcí může být libovolná kombinace pinů, je tedy možné nulovat nebo nastavovat více pinů zároveň. Pokud potřebujete přepnout hodnotu pinu můžete použít funkci LL_GPIO_TogglePin(), dávejte ale pozor že neprobíhá atomicky. Jinak řečeno pokud během provádění funkce přijde přerušení a změní hodnotu na GPIOC, můžete mít zaděláno na nepěkný problém. Pokud potřebujete na celé GPIO zapsat obsah nějaké proměnné (tedy předem nevíte které piny budete nastavovat a které nulovat) můžete na to použít funkci LL_GPIO_WriteOutputPort(). Čekací rutinu delay() zde realizuji tupým algoritmem. V praxi bývá užitečnější využít Systick nebo některý z časovačů. Vzor takového řešené můžete najít v některém z dalších dílů tohoto tutoriálu. |
Zdrojový kód
// 1A) blikání LED // Deska STM32F051 Discovery, LED na pinech PC8 a PC9 #include "stm32f0xx.h" #include "stm32f0xx_ll_bus.h" // kvůli fcím pro povolování clocku periferiím #include "stm32f0xx_ll_gpio.h" // kvůli fcím pro práci s GPIO LL_GPIO_InitTypeDef gp; // struktura s nastavením pinů void delay(uint32_t del); // hloupý delay #define DELAY_VAL 1000000 int main(void){ LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC); // spustit clock pro GPIOC LL_GPIO_StructInit(&gp); // předplnit gp výchozími hodnotami gp.Pin = LL_GPIO_PIN_8 | LL_GPIO_PIN_9; // piny 8 a 9 gp.Mode = LL_GPIO_MODE_OUTPUT; // budou výstupy LL_GPIO_Init(GPIOC,&gp); // aplikovat nastavení na GPIOC while (1){ LL_GPIO_ResetOutputPin(GPIOC,LL_GPIO_PIN_8 | LL_GPIO_PIN_9); // zhasni obě LED delay(DELAY_VAL); // chvíli počkej LL_GPIO_SetOutputPin(GPIOC,LL_GPIO_PIN_8); // Rozsviť LED na PC8 delay(DELAY_VAL); // chvíli počkej LL_GPIO_SetOutputPin(GPIOC,LL_GPIO_PIN_9); // Rozsviť LED na PC9 delay(DELAY_VAL); // chvíli počkej } } // hloupý delay void delay(uint32_t del){ volatile uint32_t i; for(i=0;i<del;i++){}; } |
Zjistit spolehlivě stisk tlačítka není úplně triviální úloha, protože je skoro vždy nutné nějak ošetřit zákmity. Při letmém pohledu na schema zapojení tlačítka na Discovery boardu by se nám mohlo zdát že to bude díky kondenzátoru C22 hračka. Ale pozorný čtenář si všimne poznámky "Not Fitted", tedy "Neosazen". Ze schematu si tedy zapamatujeme že je tlačítko připojeno mezi PA0 a VDD spolu s externím pull-down rezistorem 220k (R29). Dokud je uvolněno bude na vstupu PA0 log.0. Stiskem tlačítka přivedeme na vstup log.1. Každá změna logické hodnoty (tedy stisk i uvolnění tlačítka) bude provázena serií logických impulzů (důsledek zákmitů). Jeden z nejjednodušších způsobů jak se se zákmity vypořádat je skenovat stav talčítka jen velmi zřídka. Dejme tomu tak 20-50x za sekundu. Tím zajistíme, že mezi dvěma okamžiky kdy software zkoumá stav talčítka uplyne doba 20-50ms. Ta je výrazně delší než doba trvání zákmitů (doba přechodu tlačítka z jednoho do druhého stavu). Jedinou drobnou nevýhodou této metody je fakt že nedokáže zachytit impulzy kratší jak oněch 20-50ms (což je ale přesně to co chceme !). Mírně se také prodlužuje reakční čas softwaru, ale stěží si lze myslet, že obsluha bude schopná spoždění 20ms rozeznat. Ve zdrojovém kódu si všimněte toho že funkci LL_AHB1_GRP1_EnableClock() lze předat skupinu argumentů, a lze tak zároveň zapínat clock více periferiím. Klíčová pro práci je funkce LL_GPIO_IsInputPinSet(), která bude mít typicky jako vstupní argument pouze jeden konkrétní pin (jako v našem případě PA0). Dokud vrací 0 víme, že je tlačítko uvolněno, jakmile vrátí 1 je tlačítko stisknuto. Nás ale ani tak nezajímá jestli je nebo není stisknuto. Zajímá nás pouze okamžik kdy bylo stisknuto, ne to zda stisk trvá. Musíme si proto pomoc proměnnou button_status. Funkce delay() je pro účely příkladu realizována velmi hloupě. Typicky pro skenování tlačítek využijete nějaké periodické přerušení (od Systick nebo od časovače). |
Zdrojový kód
// 1B) detekce stisku tlačítka // Deska STM32F051 Discovery // LED na pinu PC8 // Tlačítko na PA0 s externím pull-down rezistorem a stiskem proti VDD #include "stm32f0xx.h" #include "stm32f0xx_ll_bus.h" // kvůli fcím pro povolování clocku periferiím #include "stm32f0xx_ll_gpio.h" // kvůli fcím pro práci s GPIO void delay(uint32_t del); // hloupý delay uint8_t button_status=1; int main(void){ // spustit clock pro GPIOC (LED) a GPIOA (Tlačítko) LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC | LL_AHB1_GRP1_PERIPH_GPIOA); // PC8 jako výstup LL_GPIO_SetPinMode(GPIOC,LL_GPIO_PIN_8,LL_GPIO_MODE_OUTPUT); // PA0 jako vstup LL_GPIO_SetPinMode(GPIOA,LL_GPIO_PIN_0,LL_GPIO_MODE_INPUT); while (1){ delay(20000); // kontrolu tlačítka provádět přibližně 50x za vteřinu if(!LL_GPIO_IsInputPinSet(GPIOA,LL_GPIO_PIN_0) && button_status==0){ button_status=1; // tlačítko stisknuto LL_GPIO_TogglePin(GPIOC,LL_GPIO_PIN_8); // přepni stav LED } if(LL_GPIO_IsInputPinSet(GPIOA,LL_GPIO_PIN_0)){ button_status=0; // tlačítko uvolněno } } } // hloupý delay void delay(uint32_t del){ volatile uint32_t i; for(i=0;i<del;i++){}; } |
Tento příklad demonstruje jak se pinům přiřazují alternativní funkce. Opět nebudeme dělat nic složitého, pouze blikat LEDkami. LEDky přidělíme časovači a ten už se o blikání postará. Nicméně zpět k jádru věci. Tedy k alternativním funkcím. Podívejte se na tabulky 14 a 15 v datasheetu, najdete-li si řádek například s pinem PA8, vidíte že může zastávat funkci MCO, USART1_CK (clock) nebo třeba TIM1_CH1 (1.kanál časovače 1). V prvním řádku taublky si pak můžete pro každou funkci přečíst kód (AF0,AF1 atd). My máme LED na pinech PC8 a PC9 a podle Tabulky 13 vidíme, že mohou sloužit jako kanál 3 a 4 časovače 3. Bohužel tabulku s kódy alternativních funkcí pro GPIOC i GPIOD by jste heldali marně. Osobně to považuji za botu v datasheetu. Selskou úvahou jsem usoudil, že alternativní funkce pro GPIOC i GPIOD budou mít vždy kód AF0, protože pro jejich piny je k dispozici pouze jedna funkce. Výsledný kód funguje a můžu tedy s potěšením konstatovat, že jsem odhadoval správně. A nakonec je vlastně dobře že tuto komplikaci zmiňuji zde, protože je to relativně neobvyklá situace. Většina jiných čipů má tabulky Alernativních funkcí v datasheetech kompletní. A teď k samotnému zdrojovému kódu. Klíčové je nastavit položku Mode na hodnotu LL_GPIO_MODE_ALTERNATE, tím pinu přidělujete Alternativní funkci. V položce Alternate pak volíte která z alternativních funkcí má být nastavena. V našem případě volíme AF0. Voláním funkce LL_GPIO_Init() se zvolené nastavení aplikuje a od toho okamžiku má nad PC8 a PC9 kontrolu časovač. Který ale není touto dobou ještě inicializován. Do té doby než to program provede je na pinech nějaká neznámá hodnota (což nezamená že by se nedalo zjistit co tam bude). Inicializace časovače je v této chvíli asi spíš kouzlo, ale přece jen ve stručnosti povím co se děje. Nejprve spustím časovači clock (tak jako každé používané periferii) a poté mu povolím ovládání kanálu 3 a 4. Dále pak zvolím jakým způsobem má tyto kanály ovládat (zda jsou to vstupy, výstupy, jestli bude generovat PWM nebo jako v našem případě pouze přepínat výstupní úroveň kanálu).Aby LED neblikaly zároveň, tak výstup kanálu 3 invertuji. Následně musím nastavit parametry samotného čítače. Čip mi běží z interního 8MHz oscilátoru, signál do čítače podělím 8000 a pustím do něj tedy pouze 1kHz. Strop čítače nastavím na 500, takže přeteče dvakrát za vteřinu. S každým přetečením se pak výstupní hodnota na obou kanálech přepne. Dvě přepnutí utvoří jeden cyklus bliknutí LEDkou, perioda blikání by tedy měla odpovídat 1s. Ale o časovačích si budeme hodně povídat v jiné části tutoriálu. |
Zdrojový kód
// 1C) Demonstrace alternativních funkcí // časovač TIM3 ovládá PC8 a PC9 a bliká LED // Deska STM32F051 Discovery, LED na pinech PC8 a PC9 #include "stm32f0xx.h" #include "stm32f0xx_ll_bus.h" // kvůli fcím pro povolování clocku periferiím #include "stm32f0xx_ll_gpio.h" // kvůli fcím pro práci s GPIO #include "stm32f0xx_ll_tim.h" // kvůli konfiguraci časovače LL_GPIO_InitTypeDef gp; void init_tim3(void); int main(void){ // spustit clock pro GPIOC (LEDky) LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC); // vyplnění struktury gp zvoleným nastavením pinů gp.Pin = LL_GPIO_PIN_8 | LL_GPIO_PIN_9; // Pin 8 a Pin 9 gp.Mode = LL_GPIO_MODE_ALTERNATE; // Alternativní funkce gp.OutputType = LL_GPIO_OUTPUT_PUSHPULL; // výstup typu push-pull gp.Pull = LL_GPIO_PULL_NO; // žádný pullup ani pulldown rezistor gp.Speed = LL_GPIO_SPEED_HIGH; // plná rychlost gp.Alternate = LL_GPIO_AF_0; // alternativní funkce 0 LL_GPIO_Init(GPIOC,&gp); // aplikujeme nastavení ze struktury "gp" GPIOC init_tim3(); // spustit timer 3 (zatím neřešte jak na to...) while (1){ // STMko nemá co dělat... } } void init_tim3(void){ // spustit clock pro časovač LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3); // povolit kanál 3 LL_TIM_CC_EnableChannel(TIM3,LL_TIM_CHANNEL_CH3); // nasatvit režim Toggle (přepínání) pro kanál 3 LL_TIM_OC_SetMode(TIM3,LL_TIM_CHANNEL_CH3,LL_TIM_OCMODE_TOGGLE); // povolit kanál 4 LL_TIM_CC_EnableChannel(TIM3,LL_TIM_CHANNEL_CH4); // nasatvit režim Toggle (přepínání) pro kanál 4 LL_TIM_OC_SetMode(TIM3,LL_TIM_CHANNEL_CH4,LL_TIM_OCMODE_TOGGLE); // otočit polaritu výstupu na kanále 3 LL_TIM_OC_ConfigOutput(TIM3,LL_TIM_CHANNEL_CH3,LL_TIM_OCPOLARITY_LOW); // předdělička pro časovač 8MHz/8000 = 1kHz (7999 protože počítáme od nuly) LL_TIM_SetPrescaler(TIM3,7999); // perioda pro časovač 1kHz/500 = 2Hz (499 protože počítáme od nuly) LL_TIM_SetAutoReload(TIM3,499); // spustit časovač 3 LL_TIM_EnableCounter(TIM3); } |
Když se řekne výstup, většina z vás si asi představí že je vývod mikrokontroléru nakonfigurován tak aby pevně držel logickou úroveň ať už je to log.0 nebo log.1. Po takovém výstupu se vyžaduje aby byl schopen do vývodu dodávat proud a nebo z něj proud odebírat. Ve vnitřní struktuře mikrokontroléru se takový typ výstupu realizuje pomocí dvojice tranzistorů. Takové uspořádání má vysokou rychlost přeběhu, protože dokáže rychle vybít nebo nabít parazitní kapacity vodičů. Naproti tomu uspořádání Open-Drain, nebo Open-Collector a nebo česky "otevřený kolektor", má ve své vnitřní struktuře pouze jeden tranzistor schopný vývod "uzemnit". Z principu je tedy výstup schopen proud pouze odebírat. Aby mohl fungovat je nutné připojit k němu externí rezistor, který na vývod přivádí log.1. Toto uspořádání je pomalejší, neboť se parazitní kapacity musí nabíjet skrze externí rezistor (který nelze volit libovolně malý). Takže otázka zní k čemu nám Open-Drain výstupy jsou ? Mají hned dvě zajímavá použití. Externí rezistor je možné připojit k jinému napětí než je napětí čipu. Díky tomu je možné komunikovat s logickými obvody napájenými jiným napětím. Při komunikaci s 1.8V logikou stačí pull-up rezistor (R1) připojit na 1.8V a výstupní napětí budou buď 0V nebo 1.8V. V případě STM32 se nabízí ještě možnost připojit pull-up rezistor na vyšší napětí, například 5V. Vybrané vývody STM32 jsou 5V tolerantní a je na nich možné realizovat komunikaci s 5V systémem. Další výhoda Open-Drain konfigurace je možnost spojovat výstupy aniž by hrozilo nebezpečí zkratu. Vícero zařízení může být spojeno jednou linkou a výsledná logická úroveň je logický součin výstupů všech zařízení. Jinak řečeno linka je v log.1 pouze pokud všechna zařízení mají na svých výstupech log.1 (nemají otevřený tranzistor). Stačí aby jediné zařízení nastavilo na svém výstupu log.0 (otevřelo tranzistor) a stáhne sběrnici do log.0. Toto uspořádání využívá například I2C. Obecně se často využívá jako "sdružené" externí přerušení. Více zařízení sdílí jednu linku nakonfigurovanou v čipu jako externí přerušení. Jakmile dojde v některém ze zařízení k nějaké zajímavé události, přivede na sběrnici log.0, tím probudí mikrokontrolér a ten pak zjistí kdo a proč ho volá.
Home
V1.22 3.12.2017
By Michal Dudka (m.dudka@seznam.cz)