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
...
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
...
...
10000 16
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
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.