Boa noite a todos, hoje vamos falar de algo um pouco diferente do normal até aqui, o tema hoje abordado serão os operadores lógicos binários e de salientar que são binários(em inglês bitwise, pois eles operam bit a bit e não num numero como um todo, mas em cada bit que cria o numero), não são apenas úteis para o nosso atmega/arduino, mas para qualquer micro-controlador e até programas feitos para correr no nosso pc, pois saber dominar os operados lógicos pode-nos poupar trabalho a fazer variadas operações, sendo um dos exemplos as divisões e as multiplicações.
Para começar vamos introduzir os operadores e os respectivos símbolos/comandos que os invocam:
AND &
OR |
NOT ~
XOR ^
Shift left <<
Shift right >>
Para ajudar á sua compreensão vou descrever as tabelas da verdade para cada um dos operadores, as tabelas da verdade servem para mostrar qual é o resultado de um dado operador conforme as entradas variam, vamos definir também que a partir daqui, o valor lógico HIGH será representado por '1' e o valor lógico LOW será representado por '0'.
Tabela da verdade do AND:
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1
Tabela da verdade do OR:
0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1
Tabela da verdade do XOR:
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0
Tabela da verdade do NOT:
NOT 0 = 1
NOT 1 = 0
Tabela da verdade para o Shift left aplicado a uma variável de 8 bits:
0x01<<0 = 0b00000001
0x01<<1 = 0b00000010
0x01<<2 = 0b00000100
0x01<<3 = 0b00001000
0x01<<4 = 0b00010000
0x01<<5 = 0b00100000
0x01<<6 = 0b01000000
0x01<<7 = 0b10000000
Tabela da verdade para o Shift right aplicado a uma variável de 8 bits:
0x80>>0 = 0b10000000
0x80>>1 = 0b01000000
0x80>>2 = 0b00100000
0x80>>3 = 0b00010000
0x80>>4 = 0b00001000
0x80>>5 = 0b00000100
0x80>>6 = 0b00000010
0x80>>7 = 0b00000001
Tabela para converter de binário para hexadecimal:
Binário Hexadecimal Decimal
0000 = 0 = 0
0001 = 1 = 1
0010 = 2 = 2
0011 = 3 = 3
0100 = 4 = 4
0101 = 5 = 5
0110 = 6 = 6
0111 = 7 = 7
1000 = 8 = 8
1001 = 9 = 9
1010 = A = 10
1011 = B = 11
1100 = C = 12
1101 = D = 13
1110 = E = 14
1111 = F = 15
Numeração dos bits numa variável de 8bits:
0b00000000
||||||||
|||||||bit0
||||||bit1
|||||bit2
||||bit3
|||bit4
||bit5
|bit6
bit7
Depois, para montarmos um numero de 8 bits em binário para decimal é simples, fazemos assim:
0b10101001 dividimos este numero em duas partes e removemos o 0b que só está lá para dizer ao nosso compilador que é um numero binário, e vamos ficar com 1010 e 1001, depois é só olhar para a tabela de cima e escrever 0x que diz ao compilador que é um numero em hexadecimal e juntamos a tradução dos 4 bits no numero ou letra que lhe corresponde e no caso do exemplo que estou a dar fica 0xA9.
De notar que nas tabelas de verdade para o shift right e o shift left apenas usei um valor em cada uma, mas posso colocar lá outro valor(no caso do shift left não tem de ser sempre 0x01 e no caso do shift right não tem de ser sempre 0x80, usei esses números pois são o que me dão um bit sozinho á direita ou á esquerda respectivamente).
Então vamos aprofundar os nosso conhecimentos sobre os nossos operadores lógicos, se repararem e por vezes para simplificar o nosso raciocinio e até foi assim que eu memorizei os operadores, é que podemos olhar para o AND como uma multiplicação entre os dois valores, mas atenção que não podemos usar o AND como alternativa ao * porque como referi em cima, este operadores operam individualmente sobre cada bit, o OR pode ser encarado como uma adição, o XOR como um detector de diferenças, pois se reparar-mos na tabela de verdade do XOR vê-mos que quando os bits são diferentes ele tem 1 como resultado e quando são iguais o seu resultado é 0, os shifts esses sim podem ser usados para multiplicar, usando um Shift Left, e para dividir usando um Shift Right, mas temos de ter em atenção que eles só dividem e multiplicam em potencias de dois, no caso de uma variável de 8 bits só podemos usa-los para multiplicar ou dividir por 2,4,8,16,32,64,128 e 256, e no caso da divisão não ficamos com números fraccionários nem com resto nem nada.
As aplicações dos operadores lógicos binários são mais que muitas, e vou agora listar algumas das mais úteis para os nosso micro-controladores.
unsigned char variavel = 0; //Aqui está a nossa variável inicializada a 0
Para colocar o bit0 a 1 usamos o OR do seguinte modo:
variavel = variavel | 0x01;
O 0x01 é a chamada de Máscara(em inglês MASK ou BITMASK ) e como podemos constatar pela tabela da verdade do OR o valor que irá ser guardado na nossa variável será 0x01, e agora podemos tirar partido dos operados compostos(na verdade não sei bem o nome disto em português, em ingles chamam-se compound assignments) e podemos escrever algo mais compacto e rápido de se escrever e que tem precisamente o mesmo efeito:
variavel |= 0x01;
Isto não tem qualquer diferença a nível de código gerado, é apenas mais rápido de escrever-mos, podem usar o modo que quiserem.
Agora, se quiser-mos voltar a colocar o bit0 a 0 não usamos o OR mas sim o AND:
variavel = variavel & ~0x01;
Como podem ver agora estamos a usar não apenas o AND mas também o NOT,mas não era suposto termos de usar só o AND?
Sim, era e é, mas nós gostamos de olhar para o nosso código e perceber em que bit vamos mexer e assim é facil de reconhecer que vamos mexer no bit0, mas como esta máscara só serve para colocar bits a 1, temos de fazer o NOT da mesma e depois fazer o AND do resultado com a nossa variável.
Vou mostrar-vos como é que isto funciona:
variavel tem o valor 0x01 = 0b00000001
a mascara é 0b00000001
~0b00000001 = 0b11111110 -> a mascara fica assim após o NOT
variavel AND mascara =
0b00000001
&0b11111110
------------------
0b00000000 -> e colocamos o bit0 a 0
É claro que também podíamos fazer:
variavel = variavel & 0xFE;
Mas não seria de todo tão perceptível á primeira vista reparar que íamos colocar o bit0 a 0, tal como o OR podemos usar o operador composto e o código fica assim:
variavel &= ~0x01;
Também podemos usar o AND para saber se um bit está a 0 ou a 1 por exemplo para um if(), e de que modo?
if(variavel & 0x04){
// Se o bit 2 estiver a 1 o if é executado
}
else{
//Se o bit 2 estiver a 0 o else é executado
}
Atenção numa coisa, se o bit2 for 0, a condição do if é zero, mas se o bit2 for 1 o valor é DIFERENTE de 0, mas não é necessariamente 1, vai ser um valor igual ou superior a 1, se fizerem:
if((variavel & 0x04) == 1)
Não se admirem que o vosso código não funciona como querem, porque ele vai funcionar tal e qual lhe estão a pedir, têm de ter atenção nestes pormenores, o primeiro exemplo funciona perfeitamente, não precisam de colocar nenhum ==.
Agora o operador XOR, para que será que o queremos usar?
Bem, é útil para fazer um bit mudar de 0 para 1 e de 1 para 0, quando nós não sabemos e/ou não queremos sequer saber de qual o valor do bit que queremos fazer mudar de estado, por exemplo para fazer o led do pino 13 do arduino piscar, basta usar o XOR, e não uma cadeia de if/else's ou coisas ainda mais estranhas.
Vamos supor que temos um ciclo infinito(loop) em que queremos mudar o estado do bit0 a cada iteração que fazemos nesse loop, basta-nos usar:
variavel = variavel ^0x01;
Ou mais uma vez a versão reduzida:
variavel ^= 0x01
;
Ao longo de um programa, podemos acabar por ter mascaras de bits algo complexas que não queremos estar a re-escrever sempre que as queremos usar, por isso, podemos usar um #define, e em vez de usar-mos 0x01 sempre que queremos usar a nossa máscara escrevemos MASK, exemplo:
#define MASK 0x01
//O nosso main começa aqui, temos umas quantas linhas de código até que precisamos da nossa máscara
variavel |= MAKS
E agora os Shifts, que podem ser usados para várias coisas, por exemplo queremos colocar o bit5 a 1, em vez de vermos como se escreve isso em binário, usamos um shift left para criar a nossa máscara, deste modo:
variavel |= (1<<5);
Para o AND, o XOR e o NOT o funcionamento é precisamente o mesmo, usamos (1<<x), em que x varia de 0 até 7 consoante o bit que queremos alterar ou comparar usando um if, pode-se usar o Shift left á vontade que tudo continua a funcionar certinho, e se quiser-mos fazer por exemplo uma multiplicação por dois, podemos usar o shift left para a fazer, eis um exemplo:
2<<1 = 4 mas como?
2 em binário é 0b00000010
0b00000010 << 2 = 0b00000100 que é 4
Não esquecer que estamos em base 2, logo fazemos um shift left para multiplicar por 2, dois shifts para multiplicar por 4, ou seja estamos a multiplicar por 2^(numero de shifts left), neste caso o ^ não quer dizer XOR mas sim elevado a. Atenção que qualquer valor que passe para a "frente" do bit7 é perdido, e a isso chama-se overflow, podemos por exemplo prevenir isto usando um int que tem 16 bits em vez de um char/unsigned char/uint8_t que tem apenas 8.
A divisão é igual, apenas podemos dividir por potencias de dois e se o resultado devia dar algum numero com virgula o mesmo não é criado, ficamos apenas com o resultado inteiro e não o fraccionário, por exemplo 7/2:
7 em binário é 0b00000111
0b00000111>>1 = 0b00000011
0b00000011 em decimal é 3
Mas nós sabemos que 7/2 = 3.5 mas como já referi anteriormente, qualquer numero que passe para "trás" do bit 0 é perdido e a isto chama-se underflow e não é possível recuperar o bit perdido, podemos encarar os Shift Rights como divisões por 2^(numero de shifts right).
Por agora é tudo, e espero que tenham ficado a compreender um pouco melhor toda esta confusão de operadores bitwise e de todo o seu poder apesar da sua aparente simplicidade podem tornar o vosso código mais rápido e eficiente e depois de estudarem isto um bocadinho e fazer as contas no papel recorrendo ás tabelas de verdade quando têm duvidas ajuda muito.
Bom estudo pessoal
Tiago Ângelo
15/10/2010