AVR-GCC. Παίζοντας με bits. Aka 101 Programing.

Όταν εργαζόμαστε με προγραμματισμό μικροελεγκτών η δυνατότητα επεξεργασίας μεμονωμένων bit είναι πολύ χρήσιμη ή ακόμη απαραίτητη. Μερικές περιπτώσεις που χρειάζεται ο χειρισμός μεμονωμένων bit είναι:

  • Η ενεργοποίηση η απενεργοποίηση bit σε διάφορους καταχωρητές.
  • Το “πακετάρισμα” bit σε byte για την εγγραφή τους στην μνήμη Flash.
  • Εκτέλεση πράξεων με δυνάμεις το 2.

Λογική πράξη AND.
Το σύμβολο της πράξης AND είναι το &. Εάν ένα από τα δύο bit στην πράξη AND είναι μηδέν (0) τότε το αποτέλεσμα θα είναι μηδέν (0).

uint8_t a = 55;     //Δυαδικό 00110111
uint8_t b = 101;    //Δυαδικό 01100101
uint8_t c = a&b;    //Δυαδικό 00100101 ή 37 δεκαδικό

Σε κάθε ένα από τα 8 bit των μεταβλητών a και b εκτελείτε η λογική πράξη AND και το αποτέλεσμα (37 στο δεκαδικό) μεταφέρεται στην μεταβλητή c.

Μία από τις ποιο συχνές χρήσεις του τελεστή AND είναι για την επιλογή bits σε μία ακέραια τιμή. Αυτό αποκαλείτε masking. Αν για παράδειγμα θέλουμε να έχουμε πρόσβαση στο λιγότερο σημαντικό ψηφίο (LSB) μίας μεταβλητής x και να αποθηκεύσουμε το bit αυτό σε μία άλλη μεταβλητή y, μπορούμε να κάνουμε το παρακάτω.

int x = 5;       // Δυαδικό: 101
int y = x & 1;   // Τώρα το y = 1
x = 4;           // Δυαδικό: 100
y = x & 1;       // Τώρα το y = 0

Λογική πράξη OR.
Το σύμβολο της πράξης OR είναι το |. Εάν ένα από τα δύο bit στην πράξη OR είναι ένα (1) τότε το αποτέλεσμα θα είναι ένα (1).

uint8_t a = 55;     //Δυαδικό 00110111
uint8_t b = 101;    //Δυαδικό 01100101
uint8_t c = a|b;    //Δυαδικό 01110111 ή 119 δεκαδικό

Σε κάθε ένα από τα 8 bit των μεταβλητών a και b εκτελείτε η λογική πράξη OR και το αποτέλεσμα (119 στο δεκαδικό) μεταφέρεται στην μεταβλητή c.

Μία από τις ποιο συχνές χρήσεις του τελεστή OR είναι για να ενεργοποιήσουμε (λογικό 1) κάποιο bit σε μια μεταβλητή. Αν για παράδειγμα θέλουμε να αντιγράψουμε την μεταβλητή x στην μεταβλητή y κάνοντας το LSB ένα (1), μπορούμε να κάνουμε το παρακάτω.

y = x | 1;

Λογική πράξη XOR.
Το σύμβολο της πράξης XOR (eXclusive OR) είναι το ^. Εάν τα δύο bit στην πράξη XOR είναι διαφορετικά τότε το αποτέλεσμα θα είναι ένα (1). Εάν είναι ίδια τότε το αποτέλεσμα θα είναι μηδέν (0).

uint8_t a = 55;     //Δυαδικό 00110111
uint8_t b = 101;    //Δυαδικό 01100101
uint8_t c = a^b;    //Δυαδικό 01010010 ή 82 δεκαδικό

Σε κάθε ένα από τα 8 bit των μεταβλητών a και b εκτελείτε η λογική πράξη ΧOR και το αποτέλεσμα(82 στο δεκαδικό) μεταφέρεται στην μεταβλητή c.

Μία από τις ποιο συχνές χρήσεις του τελεστή ΧOR είναι για την εναλλαγή κάποιου bit σε μια μεταβλητή. Αν για παράδειγμα θέλουμε να αντιγράψουμε την μεταβλητή x στην μεταβλητή y εναλλάσσοντας το LSB, μπορούμε να κάνουμε το παρακάτω.

y = x ^ 1;

Λογική πράξη ΝΟΤ.
Το σύμβολο της πράξης ΝΟΤ είναι το ~. Η πράξη NOT δεν χρειάζεται δύο bits. Εφαρμόζεται σε ένα μόνο bit. Αν το bit είναι ένα (1) γίνεται μηδέ (0), αν είναι μηδέν (0) γίνεται ένα (1).

uint8_t a = 55;     //Δυαδικό 00110111
uint8_t b = ~a;     //Δυαδικό 11001000 ή 200 δεκαδικό

Σε κάθε ένα από τα 8 bit της μεταβλητής a εκτελείτε η λογική πράξη NOT και το αποτέλεσμα (200 στο δεκαδικό) μεταφέρεται στην μεταβλητή b. Ας δούμε και το παρακάτω παράδειγμα.

int8_t a = 55;     //Δυαδικό 00110111
int8_t b = ~a;     //Δυαδικό 11001000 ή -56 δεκαδικό

Το μείον προκύπτει από το ποιο σημαντικό bit (MSB) που σε έναν ακέραιο int8_t (-128..127) είναι το πρόσημο του αριθμού. Η κωδικοποίηση αυτή των αρνητικών και θετικών αριθμών ονομάζεται συμπλήρωμα ως προς δύο (2)*. Δείτε τον παρακάτω πίνακα.

* Two’s complement

Τελεστές Ολίσθησης (Shift).
Υπάρχουν δύο τελεστές ολίσθησης. Ο τελεστής αριστερής ολίσθησης (left shift) με σύμβολο << και ο τελεστής δεξιάς ολίσθησης (right shift) με σύμβολο >>. Αυτοί οι τελεστές μετακινούν τα bit αριστερά ή δεξιά τόσες θέσεις όσες ορίζει το δεξί μέρος του τελεστή.

uint8_t a = 55;       //Δυαδικό 00110111
uint8_t b = a << 2;   //Δυαδικό 11011100 ή 220 δεκαδικό  
uint8_t c = b >> 2;   //Δυαδικό 01010010 ή 55 δεκαδικό

Τα bit που μετακινούνται δεξιά ή αριστερά χάνονται, δεν επανέρχονται!

uint8_t a = 131;      //Δυαδικό 10000011
uint8_t b = a << 2;   //Δυαδικό 00001100 ή 12 δεκαδικό  
uint8_t c = b >> 2;   //Δυαδικό 00000011 ή 3 δεκαδικό

uint8_t a = 131;      //Δυαδικό 10000011
uint8_t b = a >> 2;   //Δυαδικό 00100000 ή 32 δεκαδικό
uint8_t c = b << 2;   //Δυαδικό 10000000 ή 128 δεκαδικό

Τελεστές ανάθεσης.
Πολλές φορές θέλουμε να εκτελέσουμε μία πράξη σε μία μεταβλητή και να αποθηκεύσουμε το αποτέλεσμα πίσω σε αυτή την μεταβλητή.

x = x+5; // Αύξηση του x κατά 5

Η παραπάνω παράσταση μπορεί να γραφεί ως εξής

x += 5;  // Αύξηση του x κατά 5

Κατά επέκταση έχουμε

uint8_t x = 1; // Δυαδικό: 00000001
x &= 1;        // Δυαδικό: 00000001
x ^= 4;        // Δυαδικό: 00000101 – Εναλλαγή με μάσκα 00000100
x ^= 4;        // Δυαδικό: 00000001 – Εναλλαγή με μάσκα 00000100

Χρήση των παραπάνω στον κώδικα μας.

Θέλουμε το pin PB1 της PORTB να λειτουργεί σαν έξοδος. Αυτό που πρέπει να κάνουμε είναι να γράψουμε το αντίστοιχο bit στον καταχωρητή DDRB σε λογικό ένα (1).

Μπορούμε να κάνουμε το εξής:

DDRB = 0b00000010; // Δυαδικό σύστημα

ή

DDRB =  2; // Δεκαδικό σύστημα

ή

DDRB = 0x02; // Δεκαεξαδικό σύστημα

Στον παραπάνω τρόπο πρέπει να προσέχουμε να μην αλλάξουμε την κατάσταση των άλλων bit. Έστω για παράδειγμα πως σε μια προηγούμενη παράσταση είχαμε δηλώσει και κάποια άλλα pin σαν εξόδους. Έστω τα PB4, PB5. Τότε θα έπρεπε να γράψουμε:

DDRB = 0b00110010; // Δυαδικό σύστημα

ή

DDRB =  50; // Δεκαδικό σύστημα

ή

DDRB = 0x32; // Δεκαεξαδικό σύστημα

Ένας άλλο τρόπος θα ήταν ο παρακάτω.

BBRD |= (1<<PB3);

Από το αρχείο iom8.h που αφορά τον ATmega8 βλέπουμε πως

/* PORTB */
#define PB7     7
#define PB6     6
#define PB5     5
#define PB4     4
#define PB3     3
#define PB2     2
#define PB1     1
#define PB0     0

Δηλαδή όπου για παράδειγμα PB1 είναι σαν να έχουμε 1. Ο καταχωρητής μας DDRB έχει την τιμή 0b00110000 και θέλουμε να ενεργοποιήσουμε και το PB1. Τι κάνει λοιπόν η παρακάτω δήλωση;

DDRB |= (1<<PB1);

ή

DDRB = DDRB | (1 << PB1);

ή

DDRB = DDRB | (0b00000001 << 1);

Το (0b00000001 << 1) γίνεται 0b00000010. Οπότε έχουμε:

DDRB = DDRB |  0b00000010;

Η τιμή του DDRB είναι 0b00110000 όπως είπαμε. Οπότε έχουμε:

DDRB =  0b00110000 |  0b00000010;
DDRB =  0b00110010;

Όπως είδαμε καταφέραμε να ενεργοποιήσουμε το bit που θέλαμε χωρίς να ανησυχούμε για τα άλλα.

Έστω πως τώρα θέλουμε να καθαρίσουμε (λογικό 0) το PB5. Θα γράψουμε το εξής:

DDRB &= ~(1 << PB5);

ή

DDRB = DDRB & ~(1 << 5);

ή

DDRB = DDRB & ~ (0b00000001 << 5);

Το (0b00000001 << 1) γίνεται 0b00100000. Οπότε έχουμε:

DDRB = DDRB & ~ (0b00100000);

Το ~ (0b00100000) γίνεται 0b11011111. Οπότε έχουμε:

DDRB = DDRB & 0b11011111;

Η τιμή του DDRB είναι 0b00110010 όπως είπαμε. Οπότε έχουμε:

DDRB =  0b00110010 & 0b11011111;

 

DDRB = 0b00010010;

Όπως είδαμε καταφέραμε να καθαρίσουμε το bit που θέλαμε χωρίς να ανησυχούμε για τα άλλα.

Έστω πως τώρα πως θέλουμε να ενεργοποιήσουμε δύο bits τα PB1, PB2. Θα γράψουμε το εξής:

DDRB |= (1<<PB1)|(1<<PB2);

ή

DDRB = DDRB | (1 << PB1)|(1 << PB2);

ή

DDRB = DDRB | (0b00000001 << 1) | (0b00000001 << 2);

Το (0b00000001 << 1) γίνεται 0b00000010, το (0b00000001 << 2) γίνεται 0b00000100,. Οπότε έχουμε:

DDRB = DDRB |  0b00000010 | 0b00000100;

Το 0b00000010 | 0b00000100 γίνεται 0b00000110. Οπότε έχουμε:

DDRB = DDRB |  0b00000110;

Η τιμή του DDRB είναι 0b00110000 όπως είπαμε. Οπότε έχουμε:

DDRB =  0b00110000 |  0b00000110;
DDRB =  0b00110110;

Έστω πως τώρα πως θέλουμε να καθαρίσουμε δύο bits τα PB4, PB5. Θα γράψουμε το εξής:

DDRB &= ~((1 << PB5)|(1 << PB4));

ή

DDRB = DDRB & ~((1 << 5)|(1 << 4));

ή

DDRB = DDRB & ~((0b00000001 << 5)|(0b00000001 << 4));
DDRB = DDRB & ~(0b00100000 | 0b00010000);
DDRB = DDRB & ~(0b00110000);
DDRB = DDRB & 0b11001111;

Η αρχική τιμή του DDRB είναι 0b00110110. Οπότε έχουμε:

DDRB = 0b00110110 & 0b11001111;
DDRB = 0b00000110;

Έστω τώρα πως θέλουμε να εναλλάξουμε (αν είναι 0 να γίνει 1, αν είναι ένα να γίνει 0) την κατάσταση ενός bit. Έστω του PB1. Θα γράψουμε:

DDRB ^= (1 << PB1);

 

DDRB = DDRB ^ (1 << PB1);

 

DDRB = DDRB ^ (0b00000001 <<  1);
DDRB = DDRB ^ 0b00000010;

Η αρχική τιμή του DDRB είναι 0b00110110. Οπότε έχουμε:

DDRB = 0b00110110 ^ 0b00000010;
DDRB = 0b00110100;

Αν η αρχική τιμή του DDRB ήταν 0b00110100 θα είχαμε:

DDRB = 0b00110100 ^ 0b00000010;
DDRB = 0b00110110;

Συνοπτικά έχουμε:

  y = (x >> n) & 1;  // n=0..7. Αποθηκεύει το n-στο bit της x στην y. Η y
                   // γίνεται 0 ή 1.
x &= ~(1 << n);    // Αναγκάζει το n-στο bit της x να γίνει 0. Τα άλλα bit δεν
                   // επηρεάζονται.
x &= (1<<(n+1))-1; // Δεν επηρεάζει τα n χαμηλότερα bit της x και όλα τα άλλα 
                   // γίνονται 0.
x |= (1 << n);     // Αναγκάζει το n-στο bit της x να γίνει 1. Τα άλλα bit δεν
                   // επηρεάζονται.
x ^= (1 << n);     // Εναλλάσσει το n-στο bit της x. Τα άλλα bit δεν
                   // επηρεάζονται.

x = ~x;            // Εναλλάσσει όλα τα bit της x.

Παρακάτω είναι μία συνάρτηση που εκμεταλλεύεται τα παραπάνω για να μας επιστρέφει τον αριθμό των bit που είναι σε λογικό 1 σε μία μεταβλητή των 8 bit.

uint8_t CountSetBits (uint8_t x){
    uint8_t count = 0;
    uint8_t n = 0;

    for (n=0; n<8; n++){        
            if (x & (1<<n)){   
            count++;           // Αύξηση του μετρητή αν είναι 1.
        };
    };
    return count;             // Η συνάρτηση επιστρέφει τον αριθμό των
                              // bits που είναι 1.
}


BitFields

Όταν προγραμματίζουμε μικροελεγκτές μπορεί να χρειαστεί να εξετάζουμε κάθε byte ή ακόμη και κάθε bit του byte ξεχωριστά. Πολύ συχνά μπορεί να χρειαστεί να αποθηκεύσουμε μία μεταβλητή που είναι ή 0 ή 1. Αν την αποθηκεύσουμε στον μικρότερο τύπο δεδομένων που είναι το unsigned char που έχει μήκος 8 bit τότε είναι φανερό πως σπαταλάμε 7 bit. Η C έχει ένα δυνατό εργαλείο που μας επιτρέπει συνδυάσουμε τα 8 bit μίας μεταβλητής ενός byte και να τα αντιμετωπίσουμε το κάθε ένα (σχεδόν) σαν ξεχωριστή μεταβλητή. Μιλάμε για τα Bit Fields (πεδία bit). Αυτά Δηλώνονται σαν μία δομή. Ας δούμε ένα παράδειγμα.

struct bitfields {
   unsigned bStatus_1:1; // 1 Bit για την bStatus_1.
   unsigned bStatus_2:1; // 1 Bit για την bStatus_2.
   unsigned bNochNBit:1; // Ακόμη ένα Bit.
   unsigned b2Bits:2;    // Αυτό το πεδίο έχει μήκος 2 Bit.
   // Όλα αυτά καταλαμβάνουν το μέγεθος μίας μεταβλητής των 8 Bit.
   // Τα τρία εναπομείναντα Bit δεν χρησιμοποιούνται.
} bitflags;

Η πρόσβαση σε ένα ένα τέτοιο πεδίο γίνεται με πρόσβαση στην δομή με την χρήση του τελεστή της τελείας (.).

bitflags.bStatus_1 = 1;
bitflags.bStatus_2 = 0;
bitflags.b2Bits = 3 ;

Με την χρήση BitFields κερδίζουμε χώρο στην RAM του μικροελεγκτή αλλά ο κώδικας μας είναι ποιο δύσκολα συντηρίσιμος και χάνει την αξιοπιστία του (τα BitFields εξαρτώνται από την κάθε αρχιτεκτονική ενός μικροεπεξεργαστή). Οι νέοι στον χώρο είναι καλύτερα να χρησιμοποιούν και τα 8 bit (π.χ. uint8_t) και ας αποθηκεύουν μεταβλητές μόνο ενός bit. Περισσότερες πληροφορίες στην παρακάτω σελίδα.

Bit Field

Advertisements
This entry was posted in AVR, Electronics and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s