Operazioni bit per bit.

Questo argomento è dedicato alle operazioni sui bit (operazioni con i bit, matematica dei bit). Da esso imparerai come operare con i bit, le celle elementari della memoria del microcontrollore.
Questo argomento è uno dei più difficili da comprendere, quindi scopriamo perché è necessario essere in grado di lavorare con i bit: 
  • Lavoro flessibile e veloce direttamente con i registri del microcontrollore.
  • Lavorare direttamente con microcircuiti esterni (sensori, ecc.), il cui controllo consiste nella scrittura e nella lettura di registri, i cui dati possono essere impacchettati in byte nel modo più bizzarro.
  • Archiviazione dei dati più efficiente: comprimere più valori in una variabile e decomprimere.
  • Creazione di simboli e altre informazioni per display a matrice.
  • I calcoli più veloci.
  • Analisi del codice di qualcun altro.
Questo argomento è basato sul tutorial originale sulle operazioni sui bit di Arduino, puoi leggerlo qui - tutto è descritto lì in modo un po' più dettagliato.
Sistema binario.
Nel mondo digitale, che comprende anche il microcontrollore, le informazioni vengono memorizzate, convertite e trasmesse digitalmente, cioè sotto forma di zeri e uno. Di conseguenza, la cella di memoria elementare che può memorizzare 0 o 1 è chiamata bit.
La cella di memoria minima che possiamo modificare è 1 bit, e la cella di memoria che ha un indirizzo in memoria e possiamo accedervi è un byte, che consiste di 8 bit, ognuno prende il suo posto (nota: in altre architetture in un byte può avere più o meno bit, in questo argomento si parla di AVR e di un byte a 8 bit).

Richiama il sistema numerico binario dal corso di informatica della scuola:

Binario        Decimale
0000               0
0001               1
0010               2
0011               3
0100               4
0101               5
0110               6
0111               7
1000               8
1001               9
... 
...
10000            16
Notare la sequenza?
Qui devi anche vedere l'importanza del potere di due: assolutamente tutto è legato ad esso nelle operazioni di bit. Diamo un'occhiata alle prime 8 potenze di due in diversi sistemi numerici:
2 alla potenza     DEC     BIN
0                               1            0b00000001
1                               2            0b00000010
2                               4            0b00000100
3                               8            0b00001000
4                              16           0b00010000
5                              32           0b00100000
6                              64           0b01000000
7                              128         0b10000000
Quindi, una potenza di due "punta" espliciti al numero di bit in un byte, contando da destra a sinistra (nota: questo potrebbe essere diverso in altre architetture). Lascia che ti ricordi che non importa in quale sistema di calcolo lavori: al microcontrollore non importa e vede uno e zero in ogni cosa. Se "attiviamo" tutti i bit in un byte, otteniamo il numero 0b11111111 in binario o 255 in decimale.
Se "aggiungi" il byte intero nella rappresentazione decimale di ciascun bit: 128+64+32+16+8+4+2+1, ottieni 255. È facile intuire che il numero 0b11000000 è 128+64, cioè 192. In questo modo si ottiene l'intero intervallo da 0 a 255, che si inserisce in un byte. Se prendi due byte, tutto sarà uguale, ci saranno 16 celle, lo stesso per 4 byte - 32 celle con uno e zero, ognuna ha il proprio numero in base alla potenza di due.

Altri sistemi numerici.
I dati nella memoria del microcontrollore sono archiviati in rappresentazione binaria, ma oltre ad essa esistono altri sistemi numerici in cui possiamo lavorare. Non è necessario tradurre i numeri da un sistema numerico all'altro: al programma non importa in quale formato si alimenta il valore della variabile, verranno automaticamente interpretati in forma binaria. Vengono introdotti diversi sistemi numerici principalmente per comodità del programmatore.
Ora in sostanza: Arduino supporta quattro sistemi numerici classici: binario, ottale, decimale ed esadecimale.
Base                        prefisso      esempio        caratteristiche
2 (binario)                 B o 0b           0b1101001     cifre 0 e 1
8 (ottale)                   0                     0175                cifre da 0 a 7
10 (decimale)           no                  100500            cifre 0 - 9
16 (esadecimale)     0x                   0xFF21A         cifre 0-9, lettere A-F
La caratteristica principale del sistema esadecimale è che consente di scrivere numeri decimali lunghi più brevi, ad esempio, un byte (255) verrà scritto come 0xFF, due byte (65535) come 0xFFFF e tre terribili tre byte (16777215) come 0xFFFFFF.
Il sistema binario viene solitamente utilizzato per la rappresentazione visiva dei dati e le configurazioni di basso livello di vari hardware. Ad esempio, una configurazione è codificata in un byte, ogni bit al suo interno è responsabile di un'impostazione separata (on/off) e passando un byte del modulo 0b10110100, puoi configurare immediatamente un sacco di cose. La documentazione su questo è scritta nello stile di "il primo bit è responsabile di questo, il secondo di quello" e così via. Passiamo alla modifica degli stati dei bit.

Macro per la manipolazione dei bit.
La libreria Arduino.h ha alcune utili macro che ti consentono di attivare e disattivare i bit in un byte:
Macro                                            Azione
bitRead(value, bit)                         Legge il bit numerato in value
bitSet(value, bit)                          Attiva (imposta a 1) il bit numerato in value
bitClear(valore, bit)                     Disattiva (imposta a 0) il bit numerato in valore
bitWrite(value, bit, bit value)      Imposta il bit numero bit su bit value (0 o 1) in value.
bit(bit)                                           Restituisce 2 alla potenza di bit
Altre macro integrate
_BV(bit)                                              Restituisce 2 alla potenza di bit
bit_is_set(value, bit)                       Verifica dell'inclusione (1) del bit nel value
bit_is_clear(value, bit)                   Verifica dell'inclusione (0) del bit nel valore

Esempio semplice:
// qui myByte == 0 
byte myByte = 0; 
// qui myByte diventa 128 o 0b10000000
bitSet(myByte, 7); 
// qui myByte diventa 192 o 0b11000000 
bitWrite(myByte, 6, 1); 

Questo è già sufficiente per un lavoro a tutti gli effetti con i registri. Poiché si tratta di macro, funzionano il più rapidamente possibile e non sono peggiori delle operazioni di bit elementari scritte a mano. Di seguito analizzeremo il contenuto di queste macro e vedremo come funzionano, ma per ora facciamo conoscenza con le operazioni logiche elementari.

Operazioni sui bit.

Operazione a bit AND
Noto anche come "moltiplicazione logica", viene eseguito dall'operatore & o and e restituisce quanto segue:
0 & 0 == 0
0 & 1 == 0
1 & 0 == 0
1 & 1 == 1
L'applicazione principale dell'operazione AND è una maschera di bit. Consente di "prendere" solo i bit specificati da un byte: 
myByte = 0b11001100;
myBits = myByte & 0b10000111;
/*
0b11001100
        &
0b10000111
         =
0b10000100
*/
// myBits ora è 0b10000100
Puoi anche usare l'operatore composto &=
myByte = 0b11001100;
myByte &= 0b10000000; // prendiamo il bit più a sinistra
/*
0b11001100
        &=
0b10000000
          =
0b10000000
*/
// myByte ora è 0b10000000

Operazione a bit OR.
Noto anche come "addizione logica", viene eseguito dall'operatore | o or e restituisce quanto segue:
0 | 0 == 0
0 | 1 == 1
1 | 0 == 1
1 | 1 == 1
L'uso principale dell'operazione OR è di impostare un bit in un byte:
myByte = 0b11001100;
myBits = myByte | 0b00000001; // impostare il bit №0
/*
0b11001100
         |
0b00000001
          =
0b11001101
*/   
// myBits ora è 0b11001101
myBits = myBits | bit(1); // impostare il bit №1
/*
0b11001101
         |
0b00000010
          =
0b11001111
*/   
// myBits ora è 0b11001111
 Puoi anche usare l'operatore composto |=
myByte = 0b11001100;
myByte |= 16; // 16 - quarto bit, attiva il bit #4
/*
0b11001100
          |=
0b00010000
          =
0b11011100
*/   
// myByte ora è 0b11011100 

Hai già capito che puoi puntare ai bit desiderati in qualsiasi modo conveniente: in forma binaria (0b00000001 - bit zero), in forma decimale (16 - quarto bit) o ​​usando le macro bit() o _BV() (bit(7) dà 128 o 0b10000000, _BV(7) fa lo stesso)

Operazione a bit NOT.
Viene eseguito con l'operatore ~ e inverte semplicemente il bit:
~0 == 1
~1 == 0
 Può anche invertire un byte:
myByte = 0b11001100;
myByte = ~myByte; // invertire
/*
0b11001100
          ~
0b00110011
*/
// myByte теперь 0b00110011

Operazione a bit XOR.
Eseguito con l'operatore ^ o xor ed esegue le seguenti operazioni:
0 ^ 0 == 0
0 ^ 1 == 1
1 ^ 0 == 1
1 ^ 1 == 0
Questa operazione viene solitamente utilizzata per invertire lo stato di un singolo bit:
myByte = 0b11001100;
myByte ^= 0b10000000; // invertire il settimo bit
/*
0b11001100
          ^  
0b10000000
          =
0b01001100
*/  
// myByte ora è 0b01001100

Spostamento di bit.
Il spostamento di bit è un operatore molto potente, permette di "spostare" letteralmente i bit di un byte a destra e a sinistra usando gli operatori >> e <<, e di conseguenza il composto >>= e <<=. Se i bit superano i limiti del blocco (8 bit, 16 bit o 32 bit) vengono persi.
myByte = 0b00011100;
myByte = myByte << 3; // sposta 3 a sinistra
/*
0b00011100
       << 3
0b11100000
*/ 
// myByte ora è 0b11100000
myByte >>= 5;
/*
0b11100000
      >>= 5
0b00000111
*/ 
// myByte ora è 0b00000111
myByte >>= 2;
/*
0b00000111 
      >>= 2
0b00000001
*/ 
// myByte ora è 0b00000001 // il resto dei bit vengono persi.

Un spostamento di bit non fa altro che moltiplicare o dividere un byte per 2 alla potenza. Sì, questa è un'operazione di divisione, che viene eseguita in un ciclo di clock del processore! Torneremo su questo di seguito. Osserva il lavoro dell'operatore di turno e confrontalo con le macro bit() e _BV():
1 << 0 == 1
1 << 1 == 2
1 << 2 == 4
1 << 3 == 8
...
1 << 8 == 256
1 << 9 == 512
1 << 10 == 1024
Alzare un due a una potenza! Un punto importante: quando si sposta oltre 15, è necessario convertire il tipo di dati, ad esempio, in unsigned long: 1 << 17 darà come risultato 0, perché lo spostamento viene eseguito in una cella int. Ma se scriviamo 1UL << 17, il risultato sarà corretto.

Abilita/disabilita bit.
Ricordiamo l'esempio del paragrafo sull'operazione a bit OR , sull'impostazione del bit desiderato. Queste opzioni di codice fanno la stessa cosa:
myByte = 0b11000011; 
// abilita il bit numero 3 in modi diversi 
myByte |= (1 << 3); 
myByte |= bit(3); 
myByte |= _BV(3); 
bitSet(myByte, 3); 
// mioByte è uguale a 0b11001011

Abilita più bit contemporaneamente.
myByte = 0b11000011;
// abilita i bit 3 e 4 in modi diversi
myByte |= (1 << 3) | (1 << 4);
myByte |= bit(3) | bit(4);
myByte |= _BV(3) | _BV(4);
// myByte è uguale a 0b11011011

Disabilitare i bit? È un po' diverso qui, usando & = e ~.
myByte = 0b11000011;
// disabilita il bit numero 1 in modi diversi
myByte &= ~(1 << 1);
myByte &= ~_BV(1);
bitClear(myByte, 1);
// myByte è uguale a 0b11000001

Disabilita più bit contemporaneamente.
myByte = 0b11000011; 
// disabilita i bit numero 0 e 1 in modi diversi
myByte &= ~( (1 << 0) | (1 << 1) ); 
myByte &= ~( _BV(0) | _BV(1) ); 
// myByte è uguale a 0b11000000

Sono queste costruzioni che si trovano nel codice e nelle librerie di alto livello, ecco come viene svolto il lavoro con i registri del microcontrollore. 

Torniamo alle macro di Arduino:
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))
#define bit(b) (1UL << (b))
le macro sono costituite dalle stesse operazioni e spostamenti di bit elementari.

Calcoli veloci.
Come ho detto, le operazioni bit per bit sono le più veloci. Se è richiesta la massima velocità di calcolo, possono essere ottimizzati e adattati a "potenze di due", ma a volte il compilatore lo fa da solo. Considera le operazioni di base:
  • Divisione per 2^n - spostamento a destra per n. Ad esempio, val / 8 può essere scritto come   val >> 3. Il compilatore non ottimizza da solo la divisione, che può velocizzare questa operazione di circa 15 volte con l'ottimizzazione manuale.
  • Moltiplica per 2^n - sposta a sinistra per n. Ad esempio, val * 8 può essere scritto come val << 3. Il compilatore ottimizza la moltiplicazione stessa, quindi non ha senso nell'ottimizzazione manuale. Ma si può trovare in altre fonti.
  • Il resto della divisione per 2^n è una maschera di bit degli n bit meno significativi. Ad esempio, val % 8 può essere scritto come val & 0b111. Il compilatore ottimizza da solo tali operazioni, quindi non ha senso l'ottimizzazione manuale. Ma si può trovare in altre fonti.
Nota: le operazioni precedenti funzionano solo con tipi di dati interi!

Risparmia memoria.
Usando le operazioni a bit, puoi risparmiare un po' di memoria comprimendo i dati in blocchi. Ad esempio, una variabile booleana occupa 8 bit di memoria, sebbene accetti solo 0 e 1. Puoi comprimere 8 variabili booleane in un byte, in questo modo:

Comprimere i bit in byte, macro.
//memorizza i flag come 1 bit
// macro
#define B_TRUE(bp,bb) (bp) |= (bb)
#define B_FALSE(bp,bb) (bp) &= ~(bb)
#define B_READ(bp,bb) bool((bp) & (bb))
// è così che memorizza i flag , i valori devono essere come una potenza di due.
#define B_FLAG_1 1
#define B_FLAG_2 2
#define B_LED_STATE 4
#define B_BUTTON_STATE 8
#define B_BUTTON_FLAG 16
#define B_SUCCESS 32
#define B_END_FLAG 64
#define B_START_FLAG 128
 // questo byte memorizzerà 8 bit
byte boolPack1 = 0;
void setup() 
     // l'essenza è questa: funzioni macro che impostiamo/leggiamo un bit in un byte boolPack1 
     // scrivi true nel flag B_BUTTON_STATE  
     B_TRUE(boolPack1, B_BUTTON_STATE); 
     // scrivi false nel flag  B_FLAG_1  
     B_FALSE(boolPack1, B_FLAG_1); 
     // leggi il flag B_SUCCESS (ad esempio, leggi in una variabile booleana) 
     boolean successFlag = B_READ(boolPack1, B_SUCCESS); 
     // o utilizzare nella condizione
     if (B_READ(boolPack1, B_SUCCESS)) 
          { 
               // eseguire quando la condizione è soddisfatta
          }
}
void loop() 
}

Versione con funzioni Arduino.
// esempio di compressione di flag di bit in byte 
// usando le funzioni di Arduino 
byte myFlags = 0; // tutte flag su false 
// i nomi possono essere definiti 
// numeri nell'ordine 0-7 
#define FLAG1 0 
#define FLAG2 1 
#define FLAG3 2 
#define FLAG4 3 
#define FLAG5 4 
#define FLAG6 5 
#define FLAG7 6 
#define FLAG8 7 
void setup() 
     // imposta FLAG5 su true 
     bitSet(myFlags, FLAG5); 
     // imposta FLAG1 su true 
     bitSet(myFlags, FLAG1); 
     // imposta FLAG1 su false 
     bitClear(myFlags, FLAG1); 
     // legere FLAG5 
     bitRead(myFlags, FLAG5); 
     // condizione con flag 7 
     if (bitRead(myFlags, FLAG7)) 
          { 
               // se FLAG7 == true 
          } 
void loop() 
{
}

Molto comodo per comprimere un intero pacchetto di flag.
// opzione per comprimere i flag in un array.
#define NUM_FLAGS 30 // numero di flag
byte flag[NUM_FLAGS / 8 + 1]; // array di flag compressi
//Macro per lavorare con un blocco di flag.
// alza flag (blocco, numero)
#define setFlag(flag, num) bitSet(flag[(num) >> 3], (num) & 0b111)
// abbassare flag (blocco, numero)
#define clearFlag(flag, num) bitClear(flag[(num) >> 3], (num) & 0b111)
// scrivi flag (blocco, numero, valore)
#define writeFlag(flag, num, state) ((state) ? setFlag(flag, num) : clearFlag(flag, num))
// leggi flag (blocco, numero )
#define readFlag(flag, num) bitRead(flag[(num) >> 3], (num) & 0b111)
// abbassa tutti flag
#define clearAllFlags(flag) memset(flag, 0, sizeof(flag))
// alza tutti flag
#define setAllFlags(flag) memset(flag, 255, sizeof(flag))
void setup() 
     Serial.begin(9600); 
     clearAllFlags(flag); 
     writeFlag(flag, 0, 1); 
     writeFlag(flag, 10, 1); 
     writeFlag(flag, 12, 1); 
     writeFlag(flag, 15, 1); 
     writeFlag(flag, 15, 0); 
     writeFlag(flag, 29, 1); 
     // visualizzerà tutto 
     for (byte i = 0; i < NUM_FLAGS; i++) 
          Serial.print(readFlag(flag, i));
}
void loop() 
{
}

Esempio di compressione.
Facciamo un altro esempio: è necessario memorizzare diversi numeri nell'intervallo da 0 a 3 nel modo più compatto possibile, ovvero in rappresentazione binaria, questi sono 0b00, 0b01, 0b10 e 0b11. Vediamo che 4 di questi numeri possono essere inseriti in un byte (il massimo richiede due bit):
// numeri per esempio
byte val_0 = 2; // 0b10
byte val_1 = 0; // 0b00
byte val_2 = 1; // 0b01
byte val_3 = 3; // 0b11
byte val_pack = ((val_0 & 0b11) << 6) | ((val_1 & 0b11) << 4) | ((val_2 & 0b11) << 2) | (val_3 & 0b11);
// ottenuto 0b10000111
Abbiamo semplicemente preso i bit desiderati (in questo caso, i due bassi, 0b11) e li abbiamo spostati alla distanza desiderata. Per decomprimere, fallo in ordine inverso:
byte unpack_1 = (val_pack & 0b11000000) >> 6;
byte unpack_2 = (val_pack & 0b00110000) >> 4;
byte unpack_3 = (val_pack & 0b00001100) >> 2;
byte unpack_4 = (val_pack & 0b00000011) >> 0; 
E recuperiamo i nostri byte. Inoltre, la maschera può essere sostituita con una voce più comoda per il lavoro facendo scorrere 0b11 alla distanza desiderata:
byte unpack_1 = (val_pack & 0b11 << 6) >> 6;
byte unpack_2 = (val_pack & 0b11 << 4) >> 4;
byte unpack_3 = (val_pack & 0b11 << 2) >> 2;
byte unpack_4 = (val_pack & 0b11 << 0) >> 0; 
Ora, seguendo lo schema, puoi creare tu stesso una funzione o una macro per leggere il blocco:
#define UNPACK(x, y) ( ((x) & 0b11 << ((y) * 2)) >> ((y) * 2) )
Dove x è il pacchetto e y è il numero di sequenza del valore compresso. Diamo un'occhiata: 
Serial.println(UNPACK(val_pack, 3));
Serial.println(UNPACK(val_pack, 2));
Serial.println(UNPACK(val_pack, 1));
Serial.println(UNPACK(val_pack, 0)); 

"Trucchi" con i bit.
Puoi fare molte cose interessanti con operazioni bit, funzionerà molto rapidamente e occuperà poco spazio. Un enorme elenco di trucchi e hack di bit può essere trovato in questo articolo, ce ne sono molti e tutti con esempi. C'è un'altra piccola raccolta degli hack più semplici e utili qui.
Crea il tuo sito web gratis! Questo sito è stato creato con Webnode. Crea il tuo sito gratuito oggi stesso! Inizia