Vaja 0: STM32CubeIDE & C

Namen tokratne vaje je, da spoznate delo z okoljem STM32 CubeIDE, ki ga bomo uporabljali celoten semester. Hkrati bomo še osvežili znanje programskega jezika C s poudarkom na nekaterih sklopih, ki bodo za nas pomembnejši.

Priprava okolja in 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.

Vprašanje za ponovitev 1. letnika

Zakaj pri b vrednost ni točno 3.14?

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.

Vprašanje za ponovitev 1. letnika

Pod Default je videna dejanska vrednost števila s plavajočo vejico. Zakaj je vrednost pod Decimal tako drugačna? Kako je to program “izračunal” oz kaj predstavlja?

Preostanek gradiva je namenjen ponovitvi programske jezika C.

Podatkovni tipi definirani v stdint.h

Pri tej vaji in v nadaljevanju semestra bomo veliko uporabljali t.i standardne celoštevilske podatkovne tipe (angl. Standard Integer Types), ki so definirani po standardu C99. Njihove definicije se nahajajo v zaglavni datoteki (angl. header file) <stdint.h>. Tipi imajo obliko intN_t (za predznačena cela števila) in uintN_t (za nepredznačena/unsigned cela števila), kjer je N lahko 8, 16, 32 in ponekod tudi 64.

Na voljo imamo torej tipe uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t in na določenih sistemih tudi uint64_t, int64_t.

Uporabljali jih bomo vedno, kadar bo pomembno koliko bitov ima spremenljivka s katero operiramo. Na primer pri delu z registri naprav. Za osnovne podatkovne tipe, kot je int, namreč nimamo zagotovila o številu bitov, ki jih za njih uporabimo na ciljnem sistemu. Večinoma sicer spremenljivke tipa int zasedejo 32 bitov, vendar ne nujno vedno. Zaglavna datoteka pa bo poskrbela, da imajo zgoraj našteti tipi vedno toliko bitov, kot je zapisano v imenu tipa.

V funkcijo main() dodajte spodnje vrstice in preglejte kakšne so njihove vrednosti. Ne pozabite dodatki #include <stdint.h>. Če vas je kakšna vrednost presenetila, si skušajte razjasniti zakaj je drugačna od pričakovanj.

int8_t v = 50;
int8_t w = 129;
uint8_t p = 1;
int16_t t = -1;
int32_t k = -1;
int16_t r = 0b1001;
uint32_t x = 0x12345678;

Kazalci

Kazalci v jeziku C so pogosto trn v peti vsakega študenta. Pri predmetu Organizacija računalniških sistemov s kazalci ne bomo ustvarjali kompleksnih podatkovnih struktur, ampak bomo uporabljali njihov primarni namen – kazanje na specifični naslov. Kazalec je namreč zgolj spremenljivka, katere vrednost ni neka poljubna številka ampak predstavlja naslov. Tip kazalca nam pove kakšnega tipa je spremenljivka, ki se nahaja na naslovu, ki je “shranjen” v kazalcu.

Kazalec definiramo tako, da poleg tipa dodamo zvezdico. int* predstavlja kazalec na spremenljivko tipa int. Naslov torej kaže na 4 bajte, katerih vrednost predstavlja predznačeno celo število. Če imamo spremenljivko a definirano kot int a = 4;, kazalec na spremenljivko dobimo z ukazom int *p = &a;. Znak & pred imenom spremenljivke namreč vrača njen naslov. Spodaj je podana koda običajnih ukazov, ki jih izvajamo s kazalci. Razlaga je podana v komentarjih:

// definiramo kazalec
int *p;
// definiramo spremenljivko 
int a = 26;

p = &a; // kazalec kaze na a, recimo da se nahaja na naslovu 0x200
*p = 12; // vrednost na naslovu 0x200 nastavimo na 12, kar pomeni da je vrednost a 12

*(p+1) = 7; // vrednost na naslovu 0x204 nastavi na 7   
// v zgornjem primeru s povecavo stevca za 1 pridemo do naslova 0x204 (0x200 + 0x4), do stirice pridemo, ker je na naslovu 0x200 spremenljivka tipa int, ki zaseda 4 bajte

Kazalce bomo pri ORS uporabljali predvsem takrat, ko bomo želeli pisati na točno določene naslove. Ali pa, ko bomo želeli da funkcija spreminja vrednost podanega argumenta. Primer takšne uporabe je prikazan spodaj. Funkciji fun_value podamo vrednost spremenljivke x, ki jo potem v funkciji spremenimo. Ker je bila spremenljivka podana kot vrednost, se ta sprememba ne pozna v funkciji, ki kliče fun_value. V primeru, da si želimo takšnega obnašanja, moramo argument x podati kot kazalec, v funkciji pa spreminjati vrednost na naslovu na katerega kaže ta kazalec. Tak primer je prikazan v funkciji fun_reference.

void fun_value(int a) {
    // ...
    a = 4;
    // ...
}

void fun_reference(int* a) {
    // ...
    *a = 7;
    // ...
}

int main() {
    int x = 1;
    fun_value(x);
    // x je tu se vedno 1
    fun_reference(&x);
    // x je sedaj 7 tudi v main funkciji
}

Strukture

Struktura v jeziku C je zbirka spremenljivk različnih osnovnih tipov.

struct circle {
    int x;
    int y;
    uint32_t z;
    float r;
    uint16_t a;
    uint16_t b;
    double c;
};

struct circle a;
a.x = 4;
a.y = 6;

struct circle *b;
b->x = 3;
b->y = 7;

Posebnost struktur, ki jo bomo s pridom izkoriščali tekom semestra, je ta, da se elementi strukture v pomnilniku vedno nahajajo zaporedno. Če se zgornja struktura začne na naslov 0x200, potem vemo, da se element x nahaja na naslovu 0x200, element y na naslovu 0x204, element z na naslovu 0x208, element r na naslovu 0x20C, element a na naslovu 0x210, element b na naslovu 0x212, ter element c na naslovu 0x214.

Druga posebnost struktur je njihova uporaba v kombinaciji s kazalci. Kazalec na strukturo definiramo enako kot vsak drug kazalec, kar lahko vidite v zgornjem primeru. Naivno bi tako lahko pričakovali, da bomo do posameznih elementov dostopali z *b.x = 3;. Težava nastopi, ker ima pika najvišjo prioriteto med operatorji zato bi to morali zapisati kot (*b).x, kar pa je precej neberljivo in zoprno za pisati. Namesto tega uporabimo bolj pregledno obliko s puščico: b->x.

Bitne operacije

C omogoča sledeče operacije nad biti: - bitni in: x & y - bitni ali: x | y - bitni xor: x ^ y - bitna negacija: ~x - pomik bitov v desno x >> 2 - pomik bitov v levo x << 3

Pri pomiku bitov v levo, se izpraznjeni bit vedno postavi na vrednost 0. Pri pomiku v desno se na izpraznjeno mesto pri nepredznačenih številih vpiše ničla, pri predznačenih pa se na izpraznjeno mesto vpiše vrednost zadnjega bita pred pomikom (predznaka). Bitne operacije so pri delu z mikrokrmilniki nepogrešsljive. Z njimi bomo izvajali predvsem sledeče operacije:

Postavi bit (Set Bits)

Pogosto bomo želeli bit b_i v n-bitni spremenljivki spremeniti v enico, pri tem pa ostale bite ohraniti na istem stanju. Na ta način bomo na primer prižgali LED diodo, zagnali motorček, itd. To najlažje dosežemo z operacijo ali a = a | x;, kjer je x konstanta v kateri je zgolj bit b_i nastavljen na 1. Takšno število najlažje ustvarimo tako, da enico pomaknemo za i mest v levo. Ukaz, ki v spremenljivki a nastavi bit b_i je tako:

a = a | (1 << i);

Pobriši bit (Reset Bits)

Seveda bomo želeli na enak način bite tudi pobrisati oz. ponastaviti, torej vrednost bita b_i v n-bitnem številu spremeniti v ničlo, ostale bite pa ohraniti v istem stanju. Najenostavneje to dosežemo z operacijo in - a = a & x;, kjer je x konstanta v kateri je zgolj bit b_i nastavljen na 0, ostali biti pa na 1. Takšno konstanto najlažje ustvarimo tako, da enico pomaknemo za i mest, kot v prejšnjem primeru, nato pa dobljeno konstanto negiramo. Ukaz, ki spremenljivki a pobriše bit b_i je tako:

a = a & ~(1 << i);

Negiraj bit (Toggle Bits)

Z operacijo negacije obrnemo vrednost posameznega bita pri čemer stanje ostalih bitov ohranimo. To dosežemo na enak način kot pri postavljanju bita, le da namesto operacije ali uporabimo operacijo ekskluzivni ali xor:

a = a ^ (1 << i);

Testiraj bit (Test Bits)

Velikokrat nas bo zanimala vrednost bita b_i v n-bitni spremenljivki. Na ta način bomo na primer ugotavljali ali je gumb pritisnjen ali spuščen. Najenostavnejši postopek za testiranje vrednosti bita b_i je:

// testiraj bit i
a & (1 << i);

// primer testiranja bita j
if (a & (1 << j)) {
    // ce je j-ti bit enica
} else {
    // ce je j-ti bit nicla
}

Naloga 0

Implementirajte naslednje funkcije:

  • reset_bit(uint32_t x, uint8_t p) – resetiraj p-ti bit spremenljivke x.
  • reset_two_bits(uint32_t x, uint8_t p) – resetiraj bita p in p+1 spremenljivke x.
  • set_bit(uint32_t x, uint8_t p) – postavi p-ti bit spremenljivke x
  • set_two_bits_to(uint32_t x, uint8_t p, uint8_t n) – bita p in p+1 spremenljivke x nastavi na vrednost n - možne vrednosti so 0 do 3
  • set_vector_length(struct vector a*), ki prejme kazalec na strukturo struct vector s tremi elementi: x, y, length. Prva dva elementa sta predznačeni celi števili, length pa število v plavajoči vejici. Funkcija vector_length na podlagi vrednosti x in y izračuna dolžino vektorja length = sqrt(x^2+y^2) in jo zapiše v length. Funkcija nič ne vrača.
uint32_t a;
uint32_t b;

a = 0xf;
b = reset_bit(a, 2); // 0xB
a = 0xA;
b = reset_bit(a, 0); // 0xA

a = 0xFF;
b = reset_two_bits(a, 3); // 0xE7
a = 0xB7;
b = reset_two_bits(a, 3); // 0xA7

a = 0xB;
b = set_bit(a, 0); // 0xB
a = 0xE;
b = set_bit(a, 2); // 0xE

a = 0xEF;
b = set_two_bits_to(a, 3, 1); // 0xEF
a = 0xB7;
b = set_two_bits_to(a, 3, 2); // 0xB7

struct vector c;
c.x = 4;
c.y = -2;
set_vector_length(&c);
// c.length = 4.47