Operácie

Typy premenných v avr-gcc

Zo stránky SensorWiki

Verzia z 21:25, 17. máj 2021, ktorú vytvoril Balogh (diskusia | príspevky) (→‎Ale aj tak ich proste chcem!)
(rozdiel) ← Staršia verzia | Aktuálna úprava (rozdiel) | Novšia verzia → (rozdiel)


Základné[1] typy premenných v avr-gcc

K dispozícii máme všetky bežné typy, ktoré sa používajú v jazyku C.

                 Veľkosť             Rozsah
                  
  (signed) char  1 Byte (8 bitov)    <−127, +127>
  unsigned char  1 Byte (8 bitov)    <0, 255>  
  
    signed int   2 Byty (16 bitov)   <−32,767, +32,767>
  unsigned int                       <0, 65,535>
   
   signed long   4 Byty (32 bitov)   <−2,147,483,647, +2,147,483,647>
 unsigned long                       <0, 4,294,967,295>
  
         float   4 Byty (32 bitov)    IEEE-754  https://en.wikipedia.org/wiki/Single-precision_floating-point_format    
   
        double    =float[2]

Okrem toho má programátor k dispozíci aj nové typy podľa štandardu C99, ktoré majú pevne definovanú veľkosť a tým pádom je zdrojový kód prenositeľnejší. Tieto typy nájdete v knižnici <inttypes.h> a sú dostupné aj cez hlavičkový súbor (header) <stdint.h>.

        int8_t  1 Byte (8 bitov)      je to alias pre signed char
       uint8_t                        8-bit unsigned type
     
       int16_t  2 Byty (16 bitov)     = signed int /  16-bit signed type
      uint16_t                        16-bit unsigned type. 
      
       int32_t  4 Byty (32 bitov)     32-bit signed type
      uint32_t                        32-bit unsigned type
     
       int64_t  8 Bytov (64 bitov)    64-bit signed type[3]
      uint64_t                        64-bit unsigned type


Okrem vyššieuvedených pridáva C99 aj typ bool definovaný v <stdbool.h> headri. Okrem toho sú tam aj makrá pre hodnoty true and false.

Má zmysel používať tento nový typ, keď aj tak zeberie 1 bajt podobne ako unsigned char? Rozdiel je vidieť v nasledovnom príklade. Používame v ňom aj deklaráciu volatile, aby nám pri pokusoch kompilátor nevyhodnotil tento kód ako nevyužitý a nevyhodil ho celkom z výsledného programu.

 #include <stdbool.h>

 volatile bool  log_x; 
 volatile char char_x;
 
 main(void)
 {
    log_x = 255;            // obsahuje true
   char_x = 255;	    // obsahuje 255, cize true
 
    log_x =  log_x + 1;     // stale obsahuje true, pretoze 1+1 = 2 a to je nenulova hodnota, cize true
   char_x = char_x + 1;     // tu bude 0, cize false, pretoze 255+1=256, hodnota pretecie a dostaneme nulu, cize false

 }


Poznámky:

  1. Zatiaľ vynechávame polia, smerníky a typy Enum, Union a pod.
  2. Anywhere the compiler sees "double", it says to itself, "Aha! The puny human said double. But I know better, so I'll replace it with float. Muwahahaha!"
  3. Note: This types are not available when the compiler option -mint8 is in effect.


Aritmetické operácie so základnými typmi

Pozrime sa podrobnejšie, aká je výpočtová náročnosť pre jednoduché aritmetické operácie. Ak by ste si to chceli vyskúšať, je potrebné deklarovať premenné ako volatile, inak kompilátor (vcelku rozumne) usúdi, že premenné, s ktorými nič nerobíme nie sú potrebné a z kódu ich vyhodí. V komentároch na každom riadku uvádzame okrem veľkosti premennej v pamäti aj na koľko inštrukcií sa preloží daný riadok a koľko bude trvať jeho vykonávanie. Nepočítali sme presne počet strojových cyklov, uspokojili sme sa s odčítaním času v simulátore. Hodnoty platia pre kryštál 16 MHz. Veľkosť kódu je len orientačná, tam kde sa volali špecifické podprogramy uvádzame CALL.

                            //      Premenna:                Kod:                                 Poznamka:
                            //   Miesto v pamati    Počet inštrukcií   Veľkosť kódu   Rýchlosť 
   log_y =   !log_x;        //       1 Byte          5 instrukcii       14 Bytov       0,44 us  
   int_y =    int_x + 3;    //       2 Byte          5 instrukcii       18 Byte        0,63 us    - 2 Byte vyber z pamati, scitaj (s vyhodou ADIW - Add immediate to word) a vrat do pamati
  char_y =   char_x + 3;    //       1 Byte          3 instrukcie       10 Byte        0,31 us
  long_y =   long_x + 3;    //       4 Byte         11 instrukcii       38 Byte        1,25 us    - lebo tu uz tahame a ukladame 4 bajty z pamati a do pamate
 float_y =  float_x + 3;    //       4 Byte         17 instrukcii       + CALL         8,06 us    - volame podprogram pre floaty
double_y = double_x + 3;    //       4 Byte         17 instrukcii       + CALL         8,06 us    - toto je presne to iste ako float<ref>A</ref>

No a takto to vyzerá zasa s tými zložitejšími operáciami[1]

                            //      Premenna:                Kod:                                 Poznamka:
                            //   Miesto v pamati    Počet inštrukcií   Veľkosť kódu   Rýchlosť 
   int_y =    int_x / 3;    //       2 Byte           6 instrukcii       + CALL        15,19 us   - lebo na / uz nie je instrukcia
   int_y =    int_x / 2;    //       2 Byte           9	instrukcii       26 Byte        0,81 us   - lebo :2 sa da nahradit posunom vpravo >> 
  char_y =   char_x / 3;    //       1 Byte           4 instrukcie       + CALL	        5,38 us 
  long_y =   long_x / 3;    //       4 Byte          11 instrukcii       + CALL	/      39,37 us   - lebo uz tahame a ukladame 4 bajty z pamati a do pamate
 float_y =    int_x / 3;    //       = ZLE =         13 instrukcii       + 2xCALL!     16,88 us   - tu je vysledok 0, lebo celociselne delenie len ulozime do premennej float
 float_y =  float_x / 3.0;  //       4 Byte          17 instrukcii       + CALL /      30,81 us   - volame podprogram pre floaty
double_y = double_x / 3.0;  //       4 Byte          17 instrukcii       + CALL /      30,93 us   - to iste co float


Vidíme, že operácia delenia má už za následok veľký nárast záťaže procesora. Je to tak preto, lebo na delenie neexistuje v procesoroch AVR strojová inštrukcia a tak sa musí operácia delenia nahradiť algoritmom. Pre iné procesory, napr. s jadrom ARM to nemusí byť pravda. Na druhej strane vidno, že delenie dvoma dokáže kompilátor rozpoznať a nahradiť veľmi rýchlou a účinnou inštrukciou posunu vpravo.

Upozorňujeme zvlášť na piaty riadok, kedy môže byť programátor zaskočený, že výsledkom delenia je nula, hoci použil premennú typu float. Je to však už príliš neskoro. Kompilátor najprv vypočíta pravú stranu, na ktorej vidí len celočíselné delenie a výsledok potom už len prekonvertuje do formátu IEEE a uloží do float premennej. Správny postup je o riadok nižšie, kde explicitne uvedieme, že delíme reálnym čislom.



Potrebujem vo svojom programe pracovať s reálnymi číslami!

Nie, nepotrebujete. Posúďte sami.

Na obrázku nižšie vidno signál z osciloskopu, na ktorom šírka impulzu znázorňuje dobu trvania dvoch operácií. Raz v celočíselnej aritmetike a druhý v plávajúcej rádovej čiarke. Rozdiel v rýchlosti je dvojnásobný a objem kódu je neporovnateľný. K programu, v ktorom používame plávajúucu čiarku, treba pribaliť celú knižnicu, ktorá zaberá cca 0,5-2kB (v závislosti od počtu použitých operácií).



Prvý impulz predstavuje dĺžku trvania celočíselného delenia a druhý to isté v plávajúcej aritmetike.

Aj tak potrebujem reálne čísla!

Potrebujem, lebo máme na úlohu vypočítať plnenie, ktoré sa udáva ako číslo z intervalu <0;1>. Hoci do vzorca D = Ton/(Ton+Toff) vstupujú len celé čísla, musíme vypočítať podiel, ktorý nemôžeme previesť na celé číslo. Napísal som si program, ale nefunguje mi:

 int T_on = 720;
 int T_off = 344;
	 
 float Duty = T_on/(T_on+T_off);
	 
 printf("T_on: %d T_off: %d Duty: %d\n",T_on,T_off,Duty);

Čo znamená, že nefunguje? Vypíše totiž toto:

T_on: 720 T_off: 344 Duty: 0

Hm. To znamená, že to funguje presne tak, ako je napísané v programe. Najprv sa vypočíta podiel 720/(720+344), čo je podľa nás síce 0,67 ale podľa počítača, ktorý to počíta len v celočíselnej aritmetike je to 0. No a priradenie celého čísla na pravej strane výrazu do premennej typu float vlastne na výsledku nič nezmení.

OK, tak to teda zmením nasledovne:

 int T_on = 720;
 int T_off = 344;
	 
 float Duty = (float)T_on/(T_on+T_off);
	 
 printf("T_on: %d T_off: %d Duty: %f\n",T_on,T_off,Duty);

Tentoraz je výstup programu nasledovný:

T_on: 720 T_off: 344 Duty: ?

Otáznik je tam preto, lebo tentoraz už je výsledkom operácie naozaj číslo v plávajúcej desatinnej čiarke (a prísušne nabobtnaný program o knižnicu math.h), ale funkcia printf sa štandartne kompiluje v zjednodušenej verzii, ktorá nevie zobrazovať float premenné. Interne, v programe však už premenná Duty obsahuje číslo 0,676...

Môžeme sa o tom presvedčiť napríklad takto:


 int T_on = 720;
 int T_off = 344;
	 
 float Duty = (float)T_on/(T_on+T_off);
 int iDuty = (int)(Duty*1000);
	 
 printf("T_on: %d T_off: %d Duty: %d\n",T_on,T_off,iDuty);

s nasledovným výsledkom

T_on: 720 T_off: 344 Duty: 676

z ktorého vieme usúdiť, že skutočná hodnota premennej je 0,676


A je to zároveň návod, ako vypočítať plnenie bez toho, aby sme zapojili do výpočtu celý moloch knižnice pre float čísla spolu s ďalším pre ich výpis.

 unsigned long int iDuty;
	  
	 unsigned int T_on = 720;
	 unsigned int T_off = 344;
	 
	 iDuty = T_on*1000UL;
	 iDuty = iDuty/(T_on+T_off);
	 
	 unsigned int celaCast = iDuty/1000;  // Operacia / je celociselne delenie
	 unsigned int  desCast = iDuty%1000;  // Operacia % je zvysok po deleni
	 
         printf("T_on: %d T_off: %d Plnenie: %d,%d\n",T_on,T_off,celaCast,desCast);

V ukážke sme číslo vynásobili 1000 (pozor na nevyhnutný prídavok UL aby bolo kompilátoru jasné, že pracujeme s unsigned long, inak nastane podobná situácia ako pri float - na pravej strane je všetko int, takže aj výsledok bude int a len sa priradí do long premennej). Násobenie 1000 posunie výsledky o tri desatinné miesta, preto aj výsledok dostaneme s presnosťou na tri desatinné miesta.

Takýto vynásobený výsledok potom rozdelíme na celú časť a zvyšok (pomocou operátora %) a vo výpise ich zobrazíme tesne za sebou oddelené desatiinou čiarkou.

T_on: 720 T_off: 344 Plnenie: 0,676

Samozrejme, že ak nám stačí daná veličina s presnosťou len na 2 desatinné miesta, násobíme a delíme len 100.

Vidíte, že sme dokázali počítať aj s desatinnými číslami a pritom sme sa zaobišli aj bez typu float. Úspora miesta a rýchlosti je vo vnorených systémoch veľmi podstatná.


Ale aj tak ich proste chcem!

Nevidím dôvod, prečo by tento program nemal fungovať. Ja tam tie floaty proste chem a basta!

 int T_on = 720;
 int T_off = 344;
	 
 float Duty = (float)T_on/(T_on+T_off);
	 
 printf("T_on: %d T_off: %d Duty: %f\n",T_on,T_off,Duty);


OK, tak teda dobre.


Nastavenie spravnych moznosti pre linker.

V AVR Studiu z menu vyberieme Project -> Properties (Alt+F7) a na zalozke Toolchain vyberieme zo sekcie AVR/GNU C Linker polozku Miscelanous, kde do prazdneho riadku vlozime nasledovne parametre pre linker:

-Wl,-u,vfprintf  -lprintf_flt

kde

-Wl znamena, ze nasleduju ciarkami oddelene options pre linker
-u znamena, ze vyzadujeme prilinkovat kniznicu ktora nasleduje za ciarkou
vfprintf je kniznica, ktoru potrebujeme pre spravnu funkciu printf(), sprintf(), atd.
-lprintf_flt znamena, ze pozadujeme UPLNU implementaciu ktora umozni aj pouzivanie float


No a je to.

T_on: 720 T_off: 344 Duty: 0.676692


Program, v ktorom sme používali len celé čísla narastol z 3338 Bytov (10% kapacity) na 5222 Bytov (čo predstavuje 15,9% kapacity pamäte), teda o 50%, čo je výranzý prírastok. Ak niekedy bude čas, tak odmeriame aj ako sa to prejavilo na dobe vykonávania jedného cyklu.

Literatúra

  1. BB