1-AIN-140: Princípy počítačov - hardvér - 1. cvičenie

Hlavným cieľom týchto cvičení je zoznámiť sa s programovacím jazykom C a dozvedieť sa čo-to o nízkoúrovňovom spracovaní signálov a programovaní jednočipových mikropočítačov (po anglicky microcontroller). Na cvičeniach budeme preto programovať mobilné roboty Tatrabot (sú riadené 32-bitovými jednočipákmi STM32F103) v jazyku C. To by ale na zvládnutie základov programovania v C-čku bolo málo a preto budeme doma riešiť domáce úlohy - na to môžete použiť ľubovoľné C-čko, napr. Microsoft Visual Studio 2015, ktoré je aj nainštalované vo výpočtových halách H6. Na svoje počítače si tento softvér môžete legálne nainštalovať na základe dohody so spoločnosťou Microsoft. Ak treba, preštudujte si krátky návod ako začať.

Počítač

Počítač je zariadenie, ktoré dokáže spracovávať informácie. Základnou jednotkou informácie je jeden bit - jedna hodnota, ktorá môže nadobudnúť jeden z dvoch stavov: pravda-nepravda, resp. on-off, alebo 0-1. Bity sa po 8 zoraďujú do bajtov. Počítač podľa určeného postupu (algoritmus) zapísaného vo forme programu vykonáva nejaký užitočný výpočet. Samotný výpočet prebieha v súčiastke, ktorej hovoríme procesor (CPU=central processing unit) typicky pozostávajúcej z viacerých častí: riadiaca jednotka (control unit), aritmeticko-logická jednotka (ALU), registre - rýchla pamäť vnútri CPU na ukladanie medzivýsledkov výpočtu a údajov s ktorými procesor pracuje. Procesor pracuje v krokoch, pričom v každom kroku vykoná jednu inštrukciu, ktorú si prečíta z programovej pamäte počítača. Inštrukcie sú v pamäti zoradené jedna za druhou a takáto postupnosť inštrukcií tvorí vykonávaný program. Jeden krok (jedna inštrukcia) v skutočnosti môže trvať rôzne dlho - niekoľko "taktov" procesora - každý procesor pracuje na nejakej "taktovacej frekvencii", ktorá udáva počet taktov za sekundu, napr. procesory STM32F103, s ktorými budeme pracovať pracujú na frekvencii 72 000 000 taktov za sekundu, čiže 72MHz. Každé miesto v pamäti (programovej aj dátovej) má svoju adresu - čo je len poradové číslo bajtu v pamäti na ktorom sa inštrukcia alebo dátový údaj nachádza. Pamäte dokážu pomerne rýchlo vyvolať údaj uložený na akejkoľvek adrese, alebo tam novú hodnotu zapísať (RAM = random access memory = pamäť s náhodným prístupom - t.j. rovnako rýchlo dokáže informáciu uložiť na hociktorú adresu, prípadne hodnotu odtiaľ prečítať (opakom je pamäť SAM (sequential access memory), kde je možné údaje čítať/zapisovať iba postupne rad za radom tak ako nasledujú a skoky na iné adresy sú typicky veľmi pomalé, ak vôbec sú možné (predstavte si starý páskový walkman).

základná architektúra počítača

Ani pamäť RAM však napriek veľkej snahe inžinierov väčšinou nie je taká rýchla ako procesor a preto býva procesor často vybavený vyrovnávacou pamäťou (cache), do ktorej sa dočasne ukladajú hodnoty z hlavnej pamäte, s ktorými procesor pracuje často a s hlavnou pamäťou sa zosynchronizujú, až keď stihnú, hoci procesor môže medzitým pokračovať vo svojom výpočte. Niektoré inštrukcie pracujú len s vnútornými registrami procesora, iné vyžadujú načítanie alebo zapísanie údajov z/do dátovej pamäte počítača, prípadne prečítanie hodnoty z vstupného zariadenia, alebo poslanie hodnoty na výstupné zariadenie. Vďaka vstupným a výstupným zariadeniam môže počítač komunikovať so svojím okolím - inak by jeho výpočet nemal žiaden zmysel. Komunikácia medzi procesorom, pamäťou a vstupnými/výstupnými zariadeniami prebieha po dátovej zbernici, po ktorej sa v jednom okamihu naraz prenáša celý bajt = 8-bitová architektúra, dva bajty = 16-bitová architektúra, alebo 4, či až 8 bajtov (32-bitová, či 64-bitová architektúra). Veľkosť vnútorných registrov procesora preto typicky zodpovedá šírke (počtu bitov) dátovej zbernice. Dátová a programová pamäť je niekedy oddelená (tzv. Harvardská architektúra) a niekedy zmiešaná (von Neumannova architektúra). Základný program, ktorý sa štartuje po zapnutí počítača je často nahraný v neprepisovateľnej pamäti (ROM = read-only memory), ktorej obsah sa nestráca ani po odpojení zdroja napájania, čo pre väčšinu RAM - najmä tých rýchlejších - neplatí. Jednočipové počítače majú typicky viacero rôznych pamätí "typu RAM" - a to SRAM, flash a eeprom. SRAM (static RAM) potrebuje na uchovanie údajov zdroj napätia, po jeho odpojení všetko zabudne, zatiaľ čo pamäte flash a eeprom si údaje uchovajú - typicky do pamäte flash nahrávame program (chceme, aby zariadenie s jednočipákom - napr. chladnička) fungovalo podľa zabudovaného programu aj po vypnutí a zapnutí - čiže flash funguje väčšinou ako programová pamäť (hoci procesor z nej vie čítať a niekedy do nej aj zapisovať aj údaje) a do pamäte eeprom sa typicky ukladajú konfiguračné údaje (napr. hodnoty pre kalibráciu kompasového senzora, konfigurácia sieťovej karty a podobne).

Zariadenia s ktorými budeme pracovať obsahujú jednočipový mikropočítač STM32F103 - čo je síce maličký, ale inak plnohodnotný počítač, v ktorom je CPU rady ARM, pamäť, zbernica aj niekoľko vstupných a výstupných zariadení.

obrázok zariadenia s STM32F103

Dnes veľmi rozšírené procesory rady ARM majú tzv. RISC (reduced instruction set computer) - čiže na rozdiel od veľkých procesorov rady x86 (napr. Pentium) sú optimalizované pre vyšší výkon a nižšiu spotrebu vďaka vhodnému výberu základnej sady inštrukcií, ktoré procesor dokáže v jednom kroku vykonať.

architektúra stm32f103

K týmto zariadeniam však zatiaľ nemáme pripojený žiadny displej ani klávesnicu, takže programovať ich budeme na bežnom stolnom počítači (PC) v operačnom systéme Windows pomocou vývojového prostredia Eclipse upraveného pre potreby programovania STM32F103 v jazyku C - čiže pomocou prostredia ChibiStudio. ChibiStudio obsahuje sadu knižníc pre obsluhu nízkoúrovňových zariadení počítača - skoro maličký operačný systém ChibiOS.

Prvé, čo musíme urobiť, je spojazdniť toto prostredie a zistiť ako program napísaný v prostredí ChibiStudio dokážeme preniesť do ARM, použijeme tento návod. Zariadenie pripojíme pomocou prevodníka USB-serial, ktorý po zapojení do USB portu na PC vytvorí tzv. virtuálny sériový port. Sériový port je zariadenie počítača, cez ktorý je možné prenášať údaje na/z iné zariadenie - napr. tiež počítač. K starším modelom počítačov sa napr. počítačová myš pripájala cez sériový port.

obrázok prevodníka USB - seriový port

Na rozdiel od paralelného portu (ktorý umožňuje v jednom časovom okamihu preniesť viac bitov súčasne - typicky celý jeden bajt) sa údaje na sériovom porte prenášajú po bitoch za sebou (sériovo = v rade, čiže za sebou, séria => angl. series => postupnosť). Obe zariadenia treba vopred nastaviť na dohodnutú prenosovú rýchlosť (počet bitov za sekundu - BPS, niekedy nazývané aj baud). Skutočná prenosová rýchlosť bude o niečo nižšia, pretože každý bajt predchádza riadiaci bit tzv. "start bit", ktorý signalizuje začiatok prenosu a nakoniec je nasledovaný tzv. "stop bitom", signalizujúcim koniec prenosu každého bajtu. V prípade menej spoľahlivej linky sa pridáva aj paritný bit (parita počtu prenesených jednotiek), pomocou ktorého vieme odhaliť chybu prenosu, ak nastala iba v jednom bite.

architektúra stm32f103

Sériové porty typicky dokážu prenášať údaje v oboch smeroch súčasne (tzv. full duplex), aj keď niektoré zariadenia to neumožňujú a v jednom časovom okamihu sa vždy prenáša len jedným smerom, hoci oba smery sú prípustné (tzv. half duplex), fungujú tak napr. niektoré moduly BlueTooth. Informácie zo zariadenia odchádzajú cez vývod označený Tx (transmit) a vchádzajú do neho cez vývod označený Rx (receive). Pri prepojení zariadení teda musíme Tx jedného zariadenia prepojiť na Rx druhého zariadenia a naopak. Prenos informácií sa deje pomocou elektrického napätia, ktoré je vyjadrené voči zemi (GND = ground, 0V) a preto pri prepojení viacerých zariadení nesmieme nikdy zabudnúť prepojiť ich vývody GND (musia mať spoločnú zem). Typicky sa v zariadeniach tohto typu kóduje logická 1 ako napätie 5V a logická 0 ako napätie 0V (tzv. norma TTL), aj keď náš procesor funguje na 3.3V logike. Iné procesory používajú ešte nižšie napätia (napr. 1.6V alebo 1.8V), čo im umožňuje znížiť spotrebu, i stratový výkon vyžiarený ako teplo do okolia.

Náš počítač obsahuje až 3 sériové porty označené USART1, USART2, USART3 (universal serial asynchroneous receiver and transmiter) a v jeho datasheete na strane 31 v zozname všetkých vývodov procesora zistíme, že Tx je na pine (vývode) A9 a Rx je na pine A10. Preto pin A9 prepojíme s Rx na USB prevodníku a pin A10 prepojíme s pinom Tx na USB prevodníku. Riadiace signály CTS, RTS sa môžu použiť v prípade, že si zariadenia chcú signalizovať pripravenosť prijať, resp. vyslať nasledujúci bajt, vďaka čomu potom nehrozí, že by sa niektorý odvysielaný bajt stratil len preto, že program na jednej strane nestihne informáciu spracovať. Náš prevodník tieto riadiace signály vyvedené nemá, takže ich nepoužijeme a ani to v našom prípade nie je potrebné, keďže prenos cez sériovú linku (použijeme 38400 BPS) je vzhľadom na taktovaciu frekvenciu oboch počítačov dostatočne pomalý. Nezabudneme prepojiť zem (GND).

V návode sme sa dočítali ako vytvoriť program v jazyku C, skompilovať ho a preniesť cez prevodník do počítača, teraz sa zamerajme na samotný programovací jazyk C.

Každý program vytvorený v jazyku C sa skladá z jednej, alebo viacero funkcií, ktoré sú rozmiestnené do jedného alebo viacerých zdrojových súborov (.c). Každý takýto súbor (modul) kompilátor preloží do strojového kódu, čiže jazyka inštrukcií, ktoré sa môžu uložiť do programovej pamäte procesora a ten ich môže priamo začať vykonávať - a vzniknú súbory s príponou .o. Naše prostredie vtedy volá kompilátor nasledujúcim spôsobom:
gcc -c zdrojak.c
Jednotlivé moduly sa potom pospájajú (zlinkujú, link) do výsledného programu (v našom prípade uloženého v spustiteľnom formáte .elf, v prípade OS Windows je to formát .exe). Prostredie vtedy volá kompilátor napr. príkazom:
gcc -o vyslednyprogram *.o
V prípade, že funkcia z jedného modulu chce volať funkciu z iného modulu, je to možné, ale musí obsahovať tzv. prototyp funkcie, ktorú z druhého modulu volá. Príklad:

Majme modul program.c, ktorý obsahuje funkciu int main() a tá chce volať funkciu int scitaj(int a, int b) z modulu operacie.c. Potom by to malo vyzerať nejak takto:

---------------------
// program.c

int scitaj(int a, int b);  // prototyp

int main()
{
  // telo funkcie main
  scitaj(1, 2);
}
---------------------
// operacie.c

int scitaj(int a, int b)
{
   // telo funkcie scitaj
}
---------------------
Prvú vec, ktorú si všimneme, je že čokoľvek napísané za znakmi "//" je poznámka, ktorú si prekladač (kompilátor) nevšíma. Viacriadkové poznámky môžeme zapísať medzi /* a */, napr.

---------------------

/* prva funkcia, ktora sa po starte programu
   spusti je funkcia main */
int main()
{
}
---------------------
Prototypy funkcií sa spolu s inými užitočnými definíciami umiestňujú do hlavičkových súborov (header file, prípona .h) - ktoré obsahujú iba hlavičky = prototypy funkcií. Robí sa to pre prehľadnosť a zamedzenie duplicity, čo je jeden zo základných informatických princípov, pretože každá duplicita vedie k potenciálnym chybám, keď pri zmene zabudneme opraviť všetky výskyty. Prototyp funkcie - ako každý iný príkaz v jazyku C je ukončený bodkočiarkou - výnimkou je zložený príkaz: { príkaz1; príkaz2; ... }, za ktorý sa bodkočiarka nedáva.

Všetko, čo sa nachádza medzi zloženými zátvorkami za hlavičkou funkcie je telo funkcie, čiže postupnosť príkazov, ktoré sa rad za radom vykonajú po zavolaní (=spustení) príslušnej funkcie.

Ak si v programe potrebujeme nejaké údaje uložiť do pamäte, používame premenné, čo sú pomenované miesta v pamäti. Jazyk C vyžaduje, aby sme každej premennej stanovili jej typ, ktorý sa počas behu programu už nemení. Základné typy sú: Ak teda chceme zadefinovať celočíselnú premennú, ktorú budeme potrebovať vo funkcií fn(), môžeme napísať:

---------------------
void fn()
{
  int a;
  // telo funkcie fn
}
---------------------
ak by sme potrebovali viac premenných, môžeme ich napísať do toho istého riadku:

---------------------
void fn()
{
  int a, b;
  double c;
  // telo funkcie fn
}
---------------------
Premenná ktorá je definovaná vnútri funkcie (lokálna premenná), nie je mimo nej viditeľná - a hlavne neexistuje inokedy, ako v čase, keď sa daná funkcia vykonáva. Podľa novšieho štandardu (C99), ktorý používame aj my, nie je potrebné definovať lokálne premenné hneď na začiatku funkcie, ale kdekoľvek uprostred funkcie, kde sa to programátorovi hodí. Premenná definovaná mimo funkcie (globálna premenná) je potenciálne viditeľná vo všetkých funkciách v danom module - a hlavne - existuje po celú dobu behu programu. Ak chceme sprístupniť premennú definovanú mimo funkcií v inom module, treba použiť kľúčové slovo extern:

---------------------
// program.c

int a;
// ...
---------------------

---------------------
// inyprogram.c

extern int a;

void fn()
{
  a = 4;
  // ...
}
---------------------

Zapísanie hodnoty do pamäte, ktorú označuje premenná x, urobíme príkazom priradenia, napr. x = 42;. Jazyk C pozná základné aritmetické, logické, relačné (=porovnávacie) a bitové operátory:

Aritmetické
operátorčo robípríklad
+sčítaniec = a + b;
-odčítanied = c - 3;
*násobeniew = x * y;
/delenie - v prípade celých vstupných hodnôt je celočíselnéz = zz / 2;
%zvyšok po delenízv = zz % 2;
++zvýšenie operandu o 1x++;
--zníženie operandu o 1y--;

Relačné
operátorčo robípríklad
==porovnanie na rovnosť - sú rovnéif (a == b) { // urob volaco }
!=porovnanie na rovnosť - líšia sawhile (ch != 'c') { // spracuj znak ch a nacitaj dalsi }
<menšíreturn a < b;
>väčšíif (a > b) return;
>=väčší alebo rovnýint c = (a >= b);
<=menší alebo rovnýfor (int i = 1; i <= 10; i++) { // vypíš i }

Logické
operátorčo robípríklad
&&a zároveň - ANDif ((a == b) && (a > 10)) { // a aj b su vacsie ako 10 a su rovnake }
||alebo - ORa_alebo_b = a || b;
!negácia - NOTif (!game_over) { play_next_move(); }

Bitové
operátorčo robípríklad
&AND po bitochc = a & 1;// vytiahne najnizsi bit z a
|OR po bitochc = c | 3; // nastavi posledne dva bity v c na 1
^XOR po bitochx = x ^ 0x40; // flipne bit 2^6 v premennej x
~negácia po bitoch - výsledok je číslo s opačnými hodnotami všetkých bitovx = ~y;
>>bitový posun vpravoa = b >> 2; // vydeli b styrmi
<<bitový posun vľavoa = b << 4; // vynasobi b sestnastimi


Operátor priradenia môžeme kombinovať s inými operátormi, aby sme získali stručnejší zápis, napr. namiesto x = x + 3; môžeme zapísať x += 3;.

Typický program nerobí vždy presne to isté, ale v závislosti od vstupných hodnôt vykoná rozličnú postupnosť príkazov - v takom prípade sa program vetví, alebo cyklí. Jazyk C pozná nasledujúce riadiace príkazy:

Podmienka
if (podmienka) prikaz;
alebo
if (podmienka) prikaz1; else prikaz2;
pričom prikaz1 alebo prikaz2 môže byť aj zložený, napr.

if (podmienka)
{
  prikaz1_1;
  ...
}
else prikaz2;

Cyklus while
while (podmienka_opakovania) prikaz;
alebo

while (podmienka_opakovania)
{
  prikaz;
  ...
}

Cyklus do-while

do 
{
  prikaz;
  ...
} while (podmienka_opakovania);

Cyklus for

for (inicializacny_prikaz; podmienka_opakovania; reinicializacny_prikaz)
  prikaz;
alebo

for (inicializacny_prikaz; podmienka_opakovania; reinicializacny_prikaz)
{
  prikaz;
  ...
} 

V prípade cyklu for sa pri prvom priebehu vždy vykoná inicializačný príkaz, potom sa kontroluje platnosť podmienky. Ak je splnená, telo cyklu prebehne nasledované reincializačným príkazom. Ďalej sa to celé opakuje od kontroly platnosti podmienky až dovtedy, keď raz podmienka nebude splnená a môže sa pokračovať ďalším príkazom.

Aby sme mohli začať písať jednoduché programčeky, hodí sa nám ešte príkaz na vypísanie znakového reťazca na výstup. V ChibiOS na to použijeme funkciu:

chprintf(PC, "text, ktory je ukonceny znakmi navrat vozika (CR) a posun na novy riadok (LF)\r\n");
a v prostredí Microsoft Visual Studio - jazyk C++, ktoré budeme používať na riešenie domácich úloh, napíšeme:

printf("text, ktory je ukonceny prechodom na novy riadok\n");
Aby sme túto funkciu mohli použiť, treba na začiatku modulu uviesť príkaz:

#include <stdio.h>
ktorý označuje, že na danom mieste sa pri kompilácií má vsunúť celý obsah hlavičkového súboru stdio.h, ktorý obsahuje prototypy funkcií na prácu so štandardným vstupom a výstupom. A teraz sa už môžeme pustiť do napísania niekoľkých jednoduchých programov.

---------------------
// vypise cisla 1-10, kazde na samostatny riadok, %d oznacuje, ze za retazcom 
// nasleduje dalsi argument - cele cislo, ktore sa ma vypisat na danom mieste
#include <stdio.h>

int main()
{
  for (int i = 1; i <= 10; i++)
    printf("%d\n", i);
  return 0;
}
---------------------

---------------------
// vytlaci stvorec 10x10 hviezdiciek 

#include <stdio.h>

int main()
{
  for (int i = 0; i < 10; i++)
  {
    for (int j = 0; j < 10; j++)
      printf("*");
    printf("\n");
  }
  return 0;
}
---------------------

---------------------
//  funkcia sum(n) pocita sucet cisel 1-n
// vedeli by ste ju napisat efektivnejsie?
#include <stdio.h>

int sum(int n)
{
  int s = 0;
  for (int i = 1; i <= n; i++)
    s += i;
  return s;  // vrati tuto hodnotu z funkcie sum()
}

int main()
{
  printf("sucet cisel 1-100 je: %d\n", sum(100));
  return 0;
}
---------------------
pokračovanie...