STM32 GPIO I

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ář:

Ačkoli je provozní napětí STM32 typicky mezi 2-3.6V toleruje většina pinů napětí 5V. V tabulce "Pin definitions" v datasheetu je můžete najít podle zkartky FT (Five Volt Tolerant). Až na vyjímky to bývají všechny piny, které neslouží krystalovým oscilátorům nebo AD převodníku. Tato schopnost vám umožňuje bez následků přijímat signál z 5V zařízení. Výstupní charakteristiky umožňují z výstupu napájet přímo LED nebo jiné zátěže až do proudu +-20mA. Na každém pinu lze softwarově zapnout pull-up nebo pull-down resistor o odporu přibližně 40kOhm. Stejně tak je u každého pinu možné řidít rychlost přeběhu, ta ovlivňuje jednak rušení a jednak spotřebu, ale nikdy jsem v praxi nenarazil na situaci kdy bych ji potřeboval řídit. Jen pro zajímavost si můžete prohlédnout jak se rychlosti přeběhu liší pro všechny tři režimy (platné pouze pro STM32F051, čipy s vyšším taktem mohou mít jiné strmosti).


Tři rychlosti přeběhu u STM32F051 (berte s rezervou, měřeno na 70MHz osciloskopu)

Alternativní funkce

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í.

Seznam příkladů

1A) Blikání LED

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++){};
}

1B) Detekce stisku tlačítka

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++){};
}

1C) Přidělení alternativní funkce

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);
}

Push-Pull vs Open Drain

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á.


rozdíl mezi Push-Pull a Open-Drain konfigurací


Ukázka zapojení více Open-Drain výstupů na jednu linku

Odkazy

Home
V1.22 3.12.2017
By Michal Dudka (m.dudka@seznam.cz)