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.

  1. Zaženite STM32CubeIDE in izberite vaš direktorij za delovno okolje (workspace) ter izberite Launch.
  2. Izberite MyST -> Login ter vpišite e-mail in geslo. V primeru, da ste že vpisani, se vam ni potrebno ponovno prijavljati.
  3. Ustvarite nov projekt z File -> New -> STM32 Project
  4. 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.

  1. Vpišite poljubno ime projekta (izogibajte se šumnikom in imenom s presledki) ter kliknite Finish. Ostale nastavitve pustite na prednastavljenih vrednostih.
  2. Č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 = a + 3;
    b = b * 2;

    while (1) {
        i--;
        b += 0.01;
    }
}

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). Debug gumbi

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:

li t0, 0x10012008   # naložimo naslov v register

li t2, 0x01         # naložimo enico v register
slli t2, t2, 19     # (1 << 19)

lw t3, 0(t0)        # preberemo trenutno stanje na naslovu

or t3, t3, t2       # postavimo 19-ti bit in ohranimo stanje ostalih
sw t3, 0(t0)        # shranimo rezultat na isti naslov

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:
            li     a5,REG_A      # nalozimo naslov REG_A
            lw     a4,0(a5)      # branje vrednosti REG_A
            li     a5,0x80       # nalozimo 1 << 7
            lui     a3,0x80      # nalozimo 1 << 19 (lui namesto li)
            and     a5,a5,a4     # *p & (1 << 7)
            li     a4,REG_B      # nalozimo naslov REG_B
oznaka_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 AHB4 clock register - 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.

Slika 2.1: Razdelitev naslovnega prostora za GPIO naprave

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

Registri GPIO naprav
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.

Slika 2.2: Poenostavljena shema vhodno/izhodne stopnje z registri

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.

Slika 2.3: MODER - Mode register

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.

Slika 2.4: IDR - Input data register

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

Slika 2.5: ODR - Output data register

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.