Vaja 1: STM32CubeIDE & GPIO
Namen tokratne vaje je, da spoznate delo z okoljem STM32 CubeIDE, ki ga bomo uporabljali celoten semester.
Priprava & uporaba okolja/projekta v STM32Cube IDE
Pred uporabo orodja STM32CubeIDE si morate ustvariti račun na st.com. Aktivni račun potrebujete za uporabo orodja.
- Zaženite STM32CubeIDE in izberite vaš direktorij za delovno okolje (workspace) ter izberite Launch.
- Izberite MyST -> Login ter vpišite e-mail in geslo. V primeru, da ste že vpisani, se vam ni potrebno ponovno prijavljati.
- Ustvarite nov projekt z File -> New -> STM32 Project
- Izberite zavihek MCU/MPU Selector ter v Commercial Part Number vpišite STM32H750XBH6 (glej sliko spodaj). V seznamu desno spodaj izberite iskani MCU ter klinikte Next.
- Vpišite poljubno ime projekta (izogibajte se šumnikom in imenom s presledki) ter kliknite Finish. Ostale nastavitve pustite na prednastavljenih vrednostih.
- Če se pojavi še kakšno pojavno okno, izberite Yes.
Na tokratni vaji nas bo zanimala predvsem datoteka main.c, ki vsebuje tudi vsem dobro znano funkcijo main()
.
Razhroščevanje / debuggiranje
Za začetek bomo vsebino datoteke main.c zamenjali s spodnjimi vrsticami:
#include "main.h"
int a = 1;
float b = 3.14;
int main(void)
{
int i = 0;
++;
i= a + 3;
a = b * 2;
b
while (1) {
--;
i+= 0.01;
b }
}
Projekt bomo naložili na razvojno ploščo, zato jo moramo najprej priklopiti. Če jo obrnete tako, da so vsi USB priklopi na spodnji strani in je zaslon obrnjen proti vam, je pravi priključek drugi iz leve strani.
Projekt prevedemo in spravimo v način za razhroščevanje/debuggiranje z Run -> Debug. Če se pojavi dodatno pojavno okno, izberite OK.
V primeru, da se prikaže pojavno okno ST-LINK firmware verification, izberite Yes. Pojavilo se bo odprlo novo okno, ki je prikazano spodaj. Kliknite Open in update mode ter nato Upgrade. Nato zaprite odprto okno in ponovno kliknite Run -> Debug. Če se pojavi okno, ki vas povpraša če želite preklopiti v izgled za debuggiranje, izberite Switch.
Če ste uspešno sledili korakom, boste opazili, da je prva vrstica znotraj funkcije main()
obarvana. Program se je uspešno prevedel in naložil na ploščo, debugger pa je program ustavil pred prvo vrstico.
V programu imamo nekaj globalnih in lokalnih spremenljivk, katerim bomo sedaj skušali spremljati vrednost. Če ste vajeni debuggiranja z ukazi printf()
, print()
in kar še obstaja podobnih funkcij v drugih jezikih, vas moramo na tej točki malo razočarati. Za nadziranje vrednosti bomo primorani uporabiti bolj resne pristope k debuggiranju. Če ste že kdaj debuggirali v kakšnem orodju za poljuben programski jezik, vam bo nekaj naslednjih stavkov že zelo znanih, sicer pa predlagamo, da jim podrobno sledite.
Na desni strani lahko vidite različne možnosti, ki nam jih ponuja orodje (slika spodaj). Zavihek Variables nam prikazuje vrednosti lokalnih spremenljivk, zaenkrat je tam zapisana spremenljivka i
z vrednostjo 0, saj nismo izvedli še nobene vrstice funkcij main()
. Zavihek Breakpoints nam izpiše mesta na katera smo ustavili t.i. breakpointe, to so točke na katerih želimo da se naš program ustavi. Te dodamo tako, da kliknemo v modro obarvan prostor ob številkah vrstic.
Zavihek Expressions lahko uporabimo za spremljanje globalnih ali lokalnih spremenljivk oz. izrazov, ki uporabljajo spremenljivke. Če izberete Add new expressions
in dodate a
, a + 5
in b
, bi morali videti vrednosti, kot so zapisane na sliki spodaj. Vrednosti spremenljivk v zavihkih Variables in Expressions se osvežijo, ko se program ustavi.
Zavihek Live expressions pa ponuja spremljanje vrednostih globalnih spremenljivk kar med izvajanjem programa. Zavihka Registers in SFRs ponujata spremljanje vrednosti registrov mikrokrmilnika pa tudi spremljanje registrov različnih naprav. Več o tem v naslednjih tednih.
Za izvajanje programa imamo na volje gumbe, ki se nahajajo nad okno z izvorno kodo (glej sliko spodaj).
Prvi gumb iz leve Terminate and Relaunch
bo ustavil debug način in ponovno naložil program. Sledi mu gumb Resume
, ki bo sprožil izvajanje programa. Program se bo ustavil le v primeru, da naleti na breakpoint ali kliknemo gumb Pause
, ki se nahaja desno od gumba Resume
. Naslednja gumba Terminate
in Disconnect
na različna načina ustavita debug način. Preostali gumbi so namenjenu koračnemu pomikanju po programu. Step Into
bo izvedel eno vrstico in se ustavil pred naslednjo. Če je v tej vrstici klic funckije, se bo debuggiranje naprej izvedlo znotraj funkcije. Step Over
bo ravno tako izvedel naslednjo vrstico, le da se debuggiranje nadaljuje v funkciji v kateri se nahajamo trenutno. Morebiten klic funkcije se bo izvedel v celoti. Step Return
, ki je zadnji gumb na voljo v orodni vrstici, pa bo izvedel preostanek funkcije v kateri se nahajamo, ter se ustavil v vrstici za klicem funkcije v kateri smo se pred tem nahajali.
S kliki gumba Step Over
izvedite vrstice 9, 10 in 11. Preglejte zavihka Variables in Expressions, kjer boste sedaj našli nove vrednosti za spremenljivke. V zavihek Live expressions dodajte spremenljivko b
in nato kliknike gumb Resume
. Vrednost spremenljivke b
se bo hitro osveževala, medtem ko vrednosti v ostalih zavihkih med izvajanjem niso dostopne. Čez nekaj časa z gumbom Pause
ustavite program in preglejte še vrednosti v ostalih zavihkih. Če kliknete na katerokoli spremenljivko v oknih za spremljanje njihove vrednosti, boste lahko videli tudi njihov zapis v različnih številskih sistemih in oblikah zapisov.
Namen tokratne vaje je, da spoznate delo z osnovami splošno-namenskega vhoda/izhoda (GPIO) v STM32. Končni cilj je, da prižgemo LED diodo in beremo stanje gumba.
Uvod v GPIO
Delo z naslovi v C-ju in rezervirana beseda volatile
Za delo z GPIO bomo morali znati brati iz specifičnih naslovov ter na njih tudi pisati nove vrednosti. V RISC-V zbirniku bi to naredili z:
, 0x10012008 # naložimo naslov v register
li t0
, 0x01 # naložimo enico v register
li t2, t2, 19 # (1 << 19)
slli t2
, 0(t0) # preberemo trenutno stanje na naslovu
lw t3
, t3, t2 # postavimo 19-ti bit in ohranimo stanje ostalih
or t3, 0(t0) # shranimo rezultat na isti naslov sw t3
Enako bi v C-ju storili z naslednjimi vrsticami:
uint32_t *p = ((uint32_t *)0x10012008);
*p = *p | (1 << 19);
oziroma z uporabo konstant
#define GPIO_OUTPUT_EN ((uint32_t *)0x10012008)
uint32_t *p = GPIO_OUTPUT_EN;
*p = *p | (1 << 19);
ali še krajše
*GPIO_OUTPUT_EN = *GPIO_OUTPUT_EN | (1 << 19);
Rezervirana beseda volatile
Spodaj imamo primer kode, ki v zanki bere stanje bita 7 registra REG_A
, ki je register pomnilniško-preslikane vhodno/izhodne naprave (angl. memory mapped I/O). To pomeni, da njegovo vrednost lahko spreminja tako vhod kot CPE. V primeru, da je vrednost iskanega bita enica, postavimo 19-ti bit v REG_B
, ki je ravno tako register pomnilniško-preslikane V/I naprave:
#define REG_A ((uint32_t *)0x10012000)
#define REG_B ((uint32_t *)0x1001200C)
int main() {
uint32_t *p = (uint32_t *) REG_A;
while(1) {
if (*p & (1 << 7)) {
*REG_B = *REG_B | (1 << 19);
}
}
}
V primeru, da ima C prevajalnik vklopljene optimizacije, se to prevede v:
main:
,REG_A # nalozimo naslov REG_A
li a5,0(a5) # branje vrednosti REG_A
lw a4,0x80 # nalozimo 1 << 7
li a5,0x80 # nalozimo 1 << 19 (lui namesto li)
lui a3and a5,a5,a4 # *p & (1 << 7)
,REG_B # nalozimo naslov REG_B
li a4oznaka_1: bnez a5,oznaka_2 # ce je b7 postavljen preskocimo naslednjo vrstico
loop: j loop # neskoncna zanka
oznaka_2: sw a3,0(a4) # ce b7 ni postavljen, shranimo
j oznaka_1
Če pozorno pregledamo kodo v zbirniku, opazimo, da se vsebina iz naslova REG_A
prebere zgolj enkrat. Prevajalnik namreč nima informacije, da gre za registro pomnilniško-preslikane V/I naprave. Vrednost registra se lahko vsebino na naslovu, na katerega kaže kazalec, spreminja tudi izven normalnega toka programa. Vsebino na naslovu namreč v tem primeru spreminja zunanjost, recimo gumb. Prevajalniku to informacijo damo tako, da ob deklaraciji spremenljivke dodamo rezervirano besedo volatile
:
volatile uint32_t *p = (volatile uint32_t *) REG_A;
Rezervirana beseda volatile se v C-ju uporablja predvsem kadar delamo z:
pomnilniško preslikanimi registri vhodno/izhodnih naprav,
globalnimi spremenljivkami, ki jih spreminjajo prekinitveno-servisne rutine/funkcije - več o tem čez nekaj tednov,
globalnimi spremenljivkami, do katerih dostopa več niti.
GPIO v STM32H7
Na mikrokrmilnikih STM32H750, ki jih uporabljamo, je na voljo 176 splošno-namenskih pinov (angl. general-purpose). Označujemo jih s kratico GPIO. Pini so razdeljeni v skupine po 16. Vsaka skupina šestnajstih pinov pripada ločeni GPIO napravi, ki so označene s črko: GPIOA
, GPIOB
, GPIOC
, GPIOD
, GPIOE
, GPIOF
, GPIOG
, GPIOH
, GPIOI
, GPIOJ
, GPIOK
. Pini znotraj naprave so označeni s številkami od 0 do 15.
Kadar bomo govorili o pinih, bomo uporabljali oznake kot so PA0
, PC13
in PI2
. Vse oznake se začnejo s črko P, ki nam pove, da govorimo o pinu. Temu sledita črkovna oznaka GPIO naprave ter številska oznaka pina znotraj naprave. Oznaka PC13
torej pomeni, da gre za pin 13 na napravi GPIOC
, PA0
pa da gre za pin 0 na napravi GPIOA
.
Gradivo
Povezavi do dokumentacije mikrokrmilnika in razvojne plošče, ki vsebujeta informacije opisane v nadaljevanju:
Upravljanje perifernih ur GPIO naprav
Nastavitve za posamezne pine znotraj GPIO naprave so shranjene v registrih. Kot veste, registri za svoje delovanje potrebujejo uro. Ure GPIO naprav so po resetu izključene, zato bo običajno prva stvar, ki jo bomo naredili, vklop ur naprav, ki jih bomo potrebujemo.
Upravljanje perifernih ur je del ločene naprave Reset and Clock Control oz krajše RCC. Za GPIO naprave skrbi 32-bitni register RCC_AHB4ENR
(RCC AHB4 clock register - register za upravljanje perifernih ur naprav na vodilu AHB4). Spodnjih enajst bitov tega registra je namenjenih enajstim GPIO napravam. Bit 0 skrbi za uro naprave GPIOA
, bit 1 za uro naprave GPIOB
, in tako dalje do bita 10, ki skrbi za uro naprave GPIOK
. Enica na posameznem bitu pomeni, da je periferna ura vklopljena.
Register se nahaja na naslovu 0x580244E0
.
RCC_AHB4ENR
Naslovni prostor GPIO naprav
Vsaka GPIO naprava ima svoj ločen, 1kB velik del naslovnega prostora, v katerem se nahajajo registri za upravljanje njenih pinov. Razdelitev naslovnega prostora je vidna na Slika 2.1. Registri naprave GPIOE
se tako nahajajo med naslovi 0x58021000
in 0x580213FF
, registri naprave GPIOC
pa med naslovom 0x58020BFF
.
V naslovnem prostoru vsake GPIO naprave se nahaja devet 32-bitnih registrov, s pomočjo katerih lahko konfiguriramo vse kar se tiče splošno-namenskih pinov. Danes bomo spoznali tri registre, preostale pa naslednjič.
Odmik | Kratko ime | Polno ime |
---|---|---|
0x00 | MODER |
Mode register |
0x10 | IDR |
Input data register |
0x14 | ODR |
Output data register |
Odmik predstavlja število byteov od začetnega naslova naprave. Register MODER
naprave GPIOE
se torej nahaja na naslovu 0x58021000
, register IDR
na naslovu 0x58021010
, ODR
pa na 0x58021014
. Register MODER
naprave GPIOC
se nahaja na naslovu 0x58020800
, register IDR
na naslovu 0x58020810
, ODR
pa na 0x58020814
.
Inicializacija GPIO naprave - vhod/izhod
Za osnovno inicializacijo GPIO pinov, to je nastavljanje ali bomo pin uporabljali kot vhod ali kot izhod, služi register MODER
. Ostale registre, s katerimi lahko določamo še preostale nastavitve, bomo spoznali naslednjič. Slika 2.2 prikazuje poenostavljeno shemo vhodno/izhodne stopnje, na kateri vidimo vse zgoraj omenjene registre in kako se uporabljajo, da dosežejo njihove funkcije.
MODER - mode register
Register MODER
je namenjen nastavljanju načina delovanja pina - posamezen pin lahko deluje kot vhod, izhod, načinu alternativne funkcije ali analognem načinu. Ker imamo štiri možne nastavitve, za vsak pin potrebujemo dva bita. Kot vidite na Slika 2.3, sta bita 0 in 1 namenjena določanju načina delovanja za pin 0, bita 2 in 3 določanju načina delovanja pina 1, in tako dalje do bitov 30 in 31, ki določata način delovanja pina 15. Vrednost 00
na dveh bitih pomeni, da pin deluje kot vhod, vrednost 01
pa pomeni da pin deluje kot izhod. Vpis vrednosti 10
na dva bita vključi način alternativne funkcije. V tem načinu vrednost na pinu določa ena izmed preostalih naprav v mikrokmilniku (več o tem v naslednjih tednih). Vrednost 11
pin vklopi v analognem načinu. Za začetek nas bosta zanimala predvsem prva dva načina delovanja. Ob resetu je večina pinov, razen redkih izjem, v analognem načinu delovanja.
Uporaba vhoda in izhoda
Inicializaciji naprav sledi uporaba. Če pin uporabljamo kot vhod, nas zanima logična vrednost na vhodu (branje stanje pina). V primeru uporabe kot izhod, pa želimo nastavljati logično vrednost na izhodu.
IDR - Input data register
Register IDR
je sicer 32-biten, vendar uporabljamo zgolj spodnjih 16 bitov (po enega za vsak pin). Kot vidite v Slika 2.4, so biti označeni s črko r
, kar pomeni, da jih lahko samo beremo (read-only). Branje bita n
vrne logično vrednosti na pinu n
.
ODR - Output data register
Tudi pri registru ODR
uporabljamo zgolj spodnjih 16 bitov. Ničla na bitu n
pomeni, da bo na pinu n
GPIO naprave nizka logična vrednost (običajno 0V). Enica na bitu n
pomeni, da bo na pinu n
GPIO naprave visoka logična vrednost (običajno 3.3V ali 5V).
LED in gumb na STM32H750 Discovery razvojni plošči
STM32H750 Discovery plošča nam ponuja nekaj pinov, ki jih lahko uporabimo kot enostaven digitalen vhod ali izhod:
Oznaka pina | Funkcija |
---|---|
PC13 | Modri gumb |
PD3 | Zelena LED - prižgana ob enici na izhodu |
PJ2 | Zelena LED - prižgana ob ničli na izhodu |
PI13 | Rdeča LED - prižgana ob ničli na izhodu |
Primer: inicializacija in uporaba zelene LED (PD3)
Preden lahko naredimo karkoli na napravi GPIO, moramo prižgati uro GPIO naprave. Kot rečeno, se RCC_AHB4ENR
nahaja na naslovu 0x580244E0
. Postaviti moramo pin 3, ker vklapljamo uro za napravo GPIOD
:
#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)
volatile uint32_t *p = RCC_AHB4ENR;
*p = *p | (1 << 3); // bit 3 = GPIODEN
oziroma skrajšano
#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)
*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 3);
Po vklopu registrov naprave GPIOD
nastavimo način delovanja pina 3 na izhod:
#define GPIOD_MODER ((volatile uint32_t *)0x58020C00)
*GPIOD_MODER = *GPIOD_MODER & ~(3 << (2 * 3)); // pobrišemo dva bita na bitih 6 in 7
*GPIOD_MODER = *GPIOD_MODER | (1 << (2 * 3)); // postavimo enico na bitu 6
Zdaj lahko LED končno tudi upravljamo preko registra ODR
:
#define GPIOD_ODR ((volatile uint32_t *)0x58020C14)
// prižgemo LED
*GPIOD_ODR = *GPIOD_ODR | (1 << 3); // postavimo bit 3
// ugasnimo LED
*GPIOD_ODR = *GPIOD_ODR & ~(1 << 3); // pobrišemo bit 3
Še enkrat zapišimo vse skupaj:
#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)
#define GPIOD_MODER ((volatile uint32_t *)0x58020C00)
#define GPIOD_ODR ((volatile uint32_t *)0x58020C14)
// vklopimo uro GPIOD
*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 3); // bit 3 = GPIODEN
// pin PD3 inicializiramo kot izhod
*GPIOD_MODER = *GPIOD_MODER & ~(3 << (2 * 3));
*GPIOD_MODER = *GPIOD_MODER | (1 << (2 * 3));
// prižgemo LED
*GPIOD_ODR = *GPIOD_ODR | (1 << 3);
// ugasnimo LED
*GPIOD_ODR = *GPIOD_ODR & ~(1 << 3);
Primer: inicializacija in uporaba gumba
Zopet začnemo s prižiganjem ure GPIO naprave, tokrat naprave GPIOC
(bit 2).
#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)
*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 2); // bit 2 = GPIOCEN
Nastavimo pin 13, da bo deloval kot vhod.
#define GPIOC_MODER ((volatile uint32_t *)0x58020800)
*GPIOC_MODER = *GPIOC_MODER & ~(3 << (2 * 13)); // pobrišemo dva bita na bitih 26 in 27
nato stanje gumba beremo preprosto z branjem registra IDR:
#define GPIOC_IDR ((volatile uint32_t *)0x58020810)
uint32_t stanje_gumba = *GPIOC_IDR & (1 << 13);
Primer: uporaba gumba in LED
Napišimo sedaj primer, kjer inicializiramo LED in gumb. LED prižgemo kadar je gumb pritisnjen, sicer pa LED ugasnemo. Na vrhu main.c
definiramo vse konstante:
#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)
#define GPIOD_MODER ((volatile uint32_t *)0x58020C00)
#define GPIOD_ODR ((volatile uint32_t *)0x58020C14)
#define GPIOC_MODER ((volatile uint32_t *)0x58020800)
#define GPIOC_IDR ((volatile uint32_t *)0x58020810)
Dodamo še kodo za inicializacijo ter zanko kjer preverjamo stanje guma in prižigamo LED.
// vklopimo uro GPIOD
*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 3) | (1 << 2);
// init LED
*GPIOD_MODER = *GPIOD_MODER & ~(3 << (2 * 3));
*GPIOD_MODER = *GPIOD_MODER | (1 << (2 * 3));
// init gumb
*GPIOC_MODER = *GPIOC_MODER & ~(3 << (2 * 13));
while(1) {
// ce zelite spremljate stanje gumba v "Live Expressions"
// deklarirajte stanje_gumba kot globalno spremenljivko
uint32_t stanje_gumba = *GPIOC_IDR & (1 << 13);
if (stanje_gumba) {
// prižgemo LED
*GPIOD_ODR = *GPIOD_ODR | (1 << 3);
} else {
// ugasnimo LED
*GPIOD_ODR = *GPIOD_ODR & ~(1 << 3);
}
}
Naloga 1
Dodajte inicializacijo preostalih dveh LED diod. Pozorni bodite, da sta LED prižgani, ko je na izhodu logična ničla. Nato napišite program, ki bo po pritisku gumba prižigal in ugašal LED kot kaže spodnji GIF. Za časovne zamike tokrat uporabite zanke z veliko ponovitvami.