C kalba AVR mikrokontrolerių programavime

Neretai internete ar kitoj literatūroj pasitaiko neblogų užuominų, kaip efektyviau parašyti algoritmus, kad kodas būtų vykdomas greičiau bei sukompiliuotos programos dydis būtų kuo mažesnis. Norint parašyti gerą ir efektyvų algoritmą bet kuria aukštesnio lygio kalba, reikia išmanyti pačio kompiliatoriaus veikimą. Na kompiliatorių neimsim nagrinėti, ir nėra reikalo, tačiau pažvelgsim į keletą gudrybių ar taisyklių, kurių laikantis galima savo sukompiliuotą kodą padaryti daug efektyvesniu.

Pradžia

Taigi, ka daryti, kai programa viršija programų atminties limitą, arba kai kuriose vietose neužtenka greičio. Aišku visada galima viską rašyti asmebleriu, bet argi tai išeitis??? O gal visgi įmanoma pasiekti panašių rezultatų ir su C?

Ne paslaptis, jog tą patį rezultatą galima pasiekti įvairiais būdais, pvz., vieni linkę kurti masyvus, o kiti dinaminius kintamuosius, vieni mėgsta case kiti if. Taigi, vienokių ar kitokių metodų pasirinkimą lemia Programavimo Stilius, kuris pas kiekviena praktiškais yra skirtingas.
Pavyzdžiuose bus naudojamas AVR-GCC kompiliatorius su optimizavimo raktais „-O0″(be optimizacijos), „-O3″(maksimaliai optimizuojamas greitis), „-Os“ (optimizacija pagal dydį) -rezultatai panašūs į „-O3“.

Ko nesugeba C

C yra aukšto lygio kalba, o tai reiškia, jog kalba nėra pririšta prie konkrečios aparatūros, todėl, programuotojas neturi priėjimo prie procesoriaus resursų, pavyzdžiui prie steko ar vėliavėlių registro. Tia ir yra didžiausias aukšto lygio kalbų apribojimas. Pavyzdžiui su gryna C:

  • Neįmanoma patikrinti ar buvo perpildymas atliekant aritmetinę operaciją (tam juk reikia nuskaityti perpildymo vėliavėlę);

  • Negalima organizuoti daugiagijų operacijų, nes čia būtina išsaugoti registrų būseną.

Būtent tokiom užduotims atlikti yra naudojamos bibliotekos, pvz., IO.H.

Harvardo Architektūra

Paprastai įprasta programuoti Fon-Njumeno architektūrai (programų ir duomenų atmintis yra ta pati). Tačiau kalbant apie Harvardo tipo architektūrą, egzistuoja daugelis atminties sričių: Flash, RAM, EEPROM…Kiekviena jų labai skiriasi.

Tradiciniame C nėra numatytos adresų sritys. Juk būtų patogu aprašyti taip:

ram char buffer[10]; // Masyvas RAM atmintyje
disk char file[10]; // Masyvas Diskefor (i=0;i<10;i++) // Rašom į masyvą 10 simbolių ‘0’
{
file[i]=’0′; //Į diską
}
strncpy(file,buffer,10); // Rašom iš disko į buferį

Kadangi to nėra numatyta, reikalingos specialios funkcijos darbui su skirtingais atminčių tipais.

Struktūrų masyvas ar masyvų „struktūra“

Struktūra – viena iš patogių C kalbos konstrukcijų. Struktūrų naudojimas daro aiškesnį, skaitamesnį kodą, kita vertus, tai vienintelis iš paprastesnių būdų fiziškai tvarkingai surašyti duomenis į atmintį. Tačiau pažvelkime ar struktūrų naudojimas visada yra pateisinamas.

Pavyzdžiui aprašome 10 sensorių:

struct SENSOR
{
unsigned char state;
unsigned char value;
unsigned char count;
}

struct SENSOR Sensors[10];

Kaip manote ka kompiliatorius darys norėdamas paimti i-tojo sensoriaus reikšmę. Ogi i padaugins iš 3 ir prides 1. Taigi reikia daugybos operacijos, tam, kad išrinkti vieną baitą – labai neefektyvu

Gal geriau tada:

unsigned char Sensor_states[10];
unsigned char Sensor_values[10];
unsigned char Sensor_counts[10];

Mažiau akivaizdu, bet užtat greičiau vykdoma – nebereikia dauginimo operacijos nuskaitymui.

Bet kita vertus struktūros turi pranašumą atliekant operacijas su pačiomis struktūromis, pvz., kopijuojant.

Verta paminėti, jog kompiliatorius daugybos operaciją pakeitė į poslinkį ir sumą, tačiau kitaip bus jeigu struktūroje bus koks 13 elementų.

Taigi rezultatai:

==============================================================

                                     -O0             -O3
                                 žodžių  taktų    žodžių taktų
==============================================================
Kreipimasis į struktūros el       16      19      12       13
Kreipimasis į masyvą               9      12       6        7
--------------------------------------------------------------
Išlošiame (kartais)               1.8     1.6     2.0      1.9 
==============================================================
Struktūros kopijavimas            41      81      26       42
Masyvo elementų kopijavimas       44      55      43       49
--------------------------------------------------------------
Išlošiame (kartais)               0.9     1.5     0.6      0.9 
==============================================================

Listingus galima pažiūrėti čia.

Atšakojimas „Switch“

C kalboje sąlyginius sakinius patogu realizuoti su switch(). Konstrukcija gerai pasiduoda optimizavimui. Tačiau yra tam tikrų niuansų, pvz., switch operatorius argumentą perkeičia į int tipo, net jeigu argumentas yra char tipo. Taigi pavyzdys:

char a;
char With_switch()
{
        switch(a)
        {
        case '0': return 0;
        case '1': return 1;
        case 'A': return 2;
        case 'B': return 3;
        default:  return 255;
        }
}
char With_if()
{
             if(a=='0') return 0;
        else if(a=='1') return 1;
        else if(a=='A') return 2;
        else if(a=='B') return 3;
        else return 255;
}

Tipų perkeitimas prideda nemažai papildomo kodo. Listingas yra čia.

Gauti rezultatai:

====================================
                       -O0     -O3
                       žodžiai žodžiai
====================================
With_Switch             57      33      
With_If                 40      25       
------------------------------------
Išlošiama (kartai)      1.4     1.3     
====================================

Baitai su ženklu

Dar vienas pavyzdukas. Nuo AVR-GCC ver3.2 nebeturi galimybės perduoti „char“ tipo argumentų arba gražinti funkcijos rezultatų. „char“ yra išplečiamas iki „int“.

char b;
unsigned char get_b_unsigned()          
{
        return b;
}
signed char get_b_signed()
{
        return b;
}

Sukompiliavus su „-O3“ raktu gaunami rezultatai, kurie nepriklauso nuo „b“ kintamojo. listingo vaizdas:

get_b_unsigned:
        lds r24,b     ; LSB kopijuojasi
        clr r25       ; MSB lygus nuliui.
        ret
get_b_signed:
        lds r24,b     ; LSB kopijuojasi
        clr r25          ;MSB lygus nuliui,...
        sbrc r24,7    ; praleisti jeigu LSB teigiamas
        com r25       ; kitaip 0xFF 
        ret

Nepaisant to, jog gražinamas rezultatas yra vienas baitas, GCC visada skaičiuoja ir antrą. Taigi, jeigu skaičius be ženklo, tai MSB visada lygus nuliui, o jeigu su ženklu, tai procesoriui reikia atlikti dvi papildomas komandas.

Aišku daugeliu atveju tai mažai įtakoja į programos greitaeigiškumą, bet jeigu žinai, kad rezultato su ženklu nebus, tai verčiau imti „unsigned“ tipą. Ypač tai aktualu tiems kas mėgsta naudoti daug mažų funkcijų, nors šiuo atveju greitaeigiškumas nukenčia kita prasme.

Paprastumo dėlei galima nustatyti „-funsigned-char“ makefile, kuri priverčia kompiliatorių traktuoti visus „char“ kaip „unsigned“, nes kitu atveju kompiliatorius galvoja priešingai…

Pagalba kompiliatoriui

Būna situacijų, kai dideliuose programos šakojimuose yra vienodų dalių, t.y. šakos baigiasi vienodai. Pvz., išvalyti buferį, inkrementuoti skaitliuką, nustatyti vėliavėlę ir t.t. Ne visada patogu tokias operacijas sudėti į vieną funkciją arba makrokomandą. Pasirodo, kompiliatorius sugeba ir pats tai padaryti – jam tik reikia truputį padėti.

Panagrinėkime pavyzdį. Funkcija atlieka kažką, priklausomai nuo argumento, o paskui atlieka vienodas operacijas: inkrementuoja skaitliuką, apnulina būseną bei prie indekso prideda ilgį (nereikia gilintis čia tik pavyzdys). surašome switch() tipo sakinį:

void long_branch(unsigned char c)               
{
        switch(c)
        {
        case 'a':
                UDR = 'A';
                count++;
                index+=length;
                state=0;
                break;
        case 'b':
                UDR = 'B';
                state=0;
                count++;
                index+=length;
                break;
        case 'c':
                UDR = 'C';
                index+=length;
                state=0;
                count++;
                break;
        defualt:
                error=1;
                state=0;
                break;
        }
}

Sukompiliuojam su „-o3“ raktu. Rezultatas – 66 žodžiai. Ka gi perrašome ši sakinį šiek tiek kitaip:

void long_branch_opt(unsigned char c)   
{
        switch(c)
        {
        case 'a':
                UDR = 'A';
                count++;
                index+=length;
                state=0;
                break;
        case 'b':
                UDR = 'B';
                count++;
                index+=length;
                state=0;
                break;
        case 'c':
                UDR = 'C';
                count++;
                index+=length;
                state=0;
                break;
        defualt:
                error=1;
                state=0;
                break;
        }
}

Sukompiliavus gaunasi – 36 žodžiai.

Kas pasikeitė? Ogi nieko, tik sutvarkius kiekvienos šakos pabaigas kad visose būtų vienodai. Kompiliatorius atpažino vienodas vietas, sukompiliavo vieną kartą ir į jų vietas įrašė JMP. Svarbu tik tiek, kad pasikartojančios vietos būtų pabaigoje, kitaip neveiks.

Realiose programose ne visada gali pavykti to pasiekti, tačiau:

  • Galima kartais dirbtinai išskirti vienodas sritis pabaigoje bloko (arba net dirbtinai sukurti);

  • Ne būtinai reikia stengtis kad visos atšakos baigtųsi vienodai – galima jas išskirti i grupes.

Galima kodo apimtį sumažinti gana žymiai vien sukeitus komandas vietomis.

Kam reikia dinaminių sąrašų

Programavime madinga naudoti dinaminę atmintį. Tokiems tikslams naudojama speciali struktūra heaparba dinamis sąrašas. Kompiuteriuose šių struktūrų valdymu užsiima operacinė atmintis, bet mikrokontroleriuose kur nėra operacinės sistemos, kompiliatorius sukuria specialų segmentą. Be to standartinėje bibliotekoje yra numatytos funkcijos malloc ir free atminties išskyrimui ir atlaisvinimui.

Dinaminę atmintį naudoti kai kada yra paranku, bet kaip sakoma už įvairius patogumus reikia mokėti. O kai resursai yra riboti – kaina gali būti lemiama.

Panagrinėkim kas nutinka kai naudojama heap (dinaminių kintamųjų rinkinys ar masyvas – nežinau kaip tiksliai ir išversti). Parašome paprastą programėlę kur nėra naudojama dinaminė atmintis:

char a[100];
void main(void)                         
{
        a[30]=77;
}

kaip ir reikėjo tikėtis, sukompiliuotas kodas gavosi kompaktiškas. įrašymas į masyvą atliekamas dviem taktais (adresas kiekvieno elemento yra žinomas iš anksto).

Programos dydis gavosi 50 žodžių (kartu su „stratup“ kodu bei pertraukimo vektoriais.) Duomenų atmintis – 100 baitų (be masyvo ten daugiau nieko nėra). main() funkcija atliekama per 9 taktus kartu su steko inicializavimu.

Dabar padarykime tą patį su heap:

char * a;
void main(void)                         
{
        a=malloc(100);
        a[30]=77;
        free(a);
}

Rezultate gaunasi 325 programos žodžių, duomenų atmintis 114baitų. įrašas į masyvą atliekamas 5 komandomis per šešis taktus. main() funkcija atliekama per 147 taktus (kartu su steko inicijavimu).

Programa išaugo 275-iais žodžiais, iš kurių „malloc“ užima 157 žodžius bei funkcija“free“ – 104. Likusieji 14 žodžių užima iškvietimai į funkcijas „malloc“ ir „free“. Gaunasi daug sudėtingesnis kreipimasis į masyvą bei iniciavimas įrašant nulius.

14 atminties baitų užima: heap atminties organizavimo kintamieji (10 baitų), 2 baitus pati masyvo rodyklė bei 2 baitai yra yra išskiriami prieš atminties bloką tam, tam kad ten saugoti bloko dydį, kuris reikalingas teisingai įvykdyti „free“ operaciją.

Taigi dinaminės atminties naudojimas, kai mikrokontrolerio resursai yra riboti – nepateisinamas.

Tipinės klaidos

Apžvelgsime kai kurias tipines klaidas kurios gali padėti išvengti nepatogumų.

Eilutės skaitymas iš flash atminties

#include <avr/io.h>
#include <avr/pgmspace.h>
prog_char hello_str[]="Hello AVR!";
void puts(char * str)
{
        while(*str!=0)
        {
                PORTB=*str++;
        }
}
void main(void)
{
        puts(Hello_str);
}

AVR –GCC nesupranta kur rodyklė turi rodyti – į programų ar duomenų atmintį. Pagal nutylėjimą dažniausiai rodyklė rodo į RAM. Kad nuskaityti iš eilutę iš Flash atminties, reikia naudotis makrokomanda, kuri yra “pgmspace.h” bibliotekoje:

#include <avr/io.h>
#include <avr/pgmspace.h>
prog_char hello_str[]="Hello AVR!";
void puts(char * str)
{
        while(PRG_RDB(str) != 0)
        {
                PORTB=PRG_RDB(str++);
        }
}
void main(void)
{
        puts(Hello_str);
}

Bito skaitymas iš porto

void Wait_for_bit()
{
        while( PINB & 0x01 );                   
}

Kai yra įjungta optimizacija, kompiliatorius pirmiausia apskaičiuoja (PINB & 0x01) ir tada įrašo reikšmę į registrą ir tik po to tikrina. Kompiliatoriui nėra aišku, kad PINB gali pasikeisti bet kuriuo momentu nepriklausomai nuo programos vykdymo. Kad išvengti šito, reikia naudoti makrokomanda iš failo “sfr_defs.h”(įtraukta į “io.h” biblioteką). Pavyzdžiui:

void Wait_for_bit()
{
        while ( bit_is_set(PINB,0) );
}

Pertraukimo vėliavėlės laukimas

Ši funkcija turi laukti, kol įvyks pertraukimas:

unsigned char flag;
void Wait_for_interrupt()
{
        while(flag==0);                   
        flag=0;
}
SIGNAL(SIG_OVERFLOW0)
{
        flag=1;                   
}
Problema ta pati. Kompiliatoriui nėra aišku kada pertraukimų vėliavėlė gali pasikeisti.

Sprendimas: paskelbti kintamąjį su modifikatoriumi “volatile”:

volatile unsigned char flag;
void Wait_for_interrupt()
{
        while(flag==0);                   
        flag=0;
}
SIGNAL(SIG_OVERFLOW0)
{
        flag=1;                   
}

Vėlinimas

Ši funkcija turi užvėlinti laiką. Pvz.:

void Big_Delay()
{
long i;
        for(i=0;i<1000000;i++);                   
}

Problema slypi AVR-GCC kompiliatoriaus optimizavime. Kompiliatoriui aišku, kad funkcija nieko nedaro- nieko negražina ir nekeičia globalinių nei lokalių kintamųjų. Tokią funkciją galima suoptimizuoti iki nulio. Kompiliatorius šiuo atveju palieka keletą ciklo iteracijų.

Sprendimas: reikia naudotis makrokomandomis iš “delay.h” arba įrašyti į ciklą asemblerio komandą, kad kompiliatorius sukompiliuotų pilną ciklą:

#define nop() {asm("nop");}
void Big_Delay()
{
long i;
        for(i=0;i<1000000;i++) nop();                   
}

Šaltinis:

Skelbta Elektronika Pažymėti: ,

Parašykite komentarą