7 de dezembro de 2021

Do bit Ao Byte

Embarcados, Linux e programação

Aumentar GPIO na Raspberry Pi Pico

GPIO na Raspberry

Tem diversas opções para expandir os GPIO na Raspberry Pi Pico ou em qualquer outra MCU; ou outras Raspberry. Para quem acompanha o blog, sabe que escrevo com frequência sobre o meu expansor de IO preferido: PCF8574, do qual também tenho meu artigo preferido: “Dominando PCF8574 com Arduino“. Mas aqui vamos usar a RPi Pico com o firmware MicroPython, então esse artigo será o meu preferido sobre a RPi Pico, pois além de utilizarmos a PCF8574 para expandir o GPIO na Raspberry Pi Pico, ainda vamos usar o I2C!

Configurar I2C na Raspberry Pi Pico

Para expandirmos o GPIO na Raspberry Pi Pico com PCF8574 precisaremos configurar o barramento I2C. É tão simples quanto no Arduino, mas o código final é muito menor. Confira.

Usar Python já é por si só muito mais fácil do que programar em C/C++ para MCUs, mas usar MicroPython chega a ser covardia de tão fácil que fica. Primeiro, vamos dar uma conferida no pinout da placa em relação ao barramento I2C.

Nessa seção do artigo vamos ver o procedimento padrão para configurar qualquer dispositivo I2C na Raspberry Pi Pico e identificar seu endereço. Use como referência para qualquer outro dispositivo I2C que precisar configurar.

Repare que temos o I2C quase na placa inteira, mas a placa tem apenas duas controladoras I2C. Isso significa que podemos usar 2 I2C nos pinos desejados, mas apenas 1 barramento pode ser habilitado em cada canal.

I2C ControllerGPIO Pins
I2C0 – SDAGP0/GP4/GP8/GP12/GP16/GP20
I2C0 – SCLGP1/GP5/GP9/GP13/GP17/GP21
I2C1 – SDAGP2/GP6/GP10/GP14/GP18/GP26
I2C1 – SCLGP3/GP7/GP11/GP15/GP19/GP27

Em nosso exemplo vamos configurar o GP0 e GP1, que são os pinos 1 e 2 da placa. No barramento I2C podemos ter até 127 dispositivos conectados à MCU, desde que cada um esteja em um endereço exclusivo. Se por alguma razão for necessário configurar um segundo barramento, veja na tabela acima quais os pinos disponíveis para o segundo canal I2C (que são as últimas duas linhas).

Se precisasse de mais um barramento ainda, dá pra fazer por software. Enfim, não faltam recursos.

Experimentar antes de escrever o programa

Essa é mais uma vantagem em utilizar MicroPython. Não precisamos criar o programa de expansão do GPIO na Raspberry e então experimentar posteriormente. Já podemos testar “inflow”. Usando a Thonny IDE podemos digitar código Python no prompt, da mesma forma que o fluxo de nosso programa seguiria. Ou não, podemos apenas escrever um código de teste, como faremos aqui para pegar o endereço do dispositivo. O procedimento é simples:

  • Definir os pinos SDA e SCL
  • Configurar o barramento I2C
  • Varrer pelo dispositivo conectado

E a execução é tão simples quanto:

from machine import Pin
from machine import I2C

sda_pin = Pin(0)
scl_pin = Pin(1)

i2c = I2C(0,sda=sda_pin,scl=scl_pin,freq=400000)
print(i2c.scan())

A velocidade padrão do barramento é 400000, já temos isso de cabeça. Na linha que definimos a configuração, apontamos para o canal 0, passamos os pinos e a velocidade do barramento. Só isso. Em seguida fazemos um scan. Não tem como ser mais fácil!

Agora que já temos o dispositivo configurado e o endereço do dispositivo identificado, passemos ao objetivo do artigo.

Aumentar GPIO na Raspberry Pi Pico com PCF8574

Agora vamos começar a brincadeira. Primeiro, para saber quais os métodos disponíveis na classe I2C, chamamos o help (o qual tenho enfatizado fortemente nos vídeos relacionados, não perca essa playlist).

Bem, tem alguma limitação. Agora não dá pra chamar o help no método, então não dá pra saber quais os parâmetros necessários. Mas sem problema, a documentação é sua amiga.

Para escrever para um dispositivo, usamos os parâmetros ADDR, VALUE, sendo endereço e valor. Para ler, podemos usar a função readfrom passando o endereço e o número de bytes desejados.

O PCF8574 é um expansor de IO de 8 bits, ou seja, 1 Byte. Assim, podemos escrever valores entre 0 e 255 e ler 1 byte.

Leitura de bytes

Qual o valor inicial dos pinos do PCF8574? Vimos que o endereço é o 34. Vamos ler 1 byte:

Isso pode ser uma pegadinha pra quem não está habituado. Pra dizer a verdade, nem sei se tem outra forma de fazer a conversão porque sempre usei assim, mas de qualquer modo, eis a forma de manipular a leitura:

Lembre-se: A leitura do PCF8574 retornou um byte; em byte, portanto devemos ler o índice 0 para converter o valor.

Escrita de bytes

Já sabemos ler, agora vamos escrever e confirmar a mudança. Temos que escrever em bytes, o formato é bytes tanto para leitura quanto para escrita. Mas não dá pra ficar manipulando bits assim, precisamos fazer a conversão, certo? Mas o primeiro passo é entender como escrever um valor inteiro para o dispositivo. A ordem é essa:

  • Converta o valor decimal para byte.
  • Escreva o valor para o endereço I2C. O retorno será 1 quando estiver ok.
  • Leia o valor para confirmar que o PCF8574 armazenou o valor.

Na primeira conversão dizemos que queremos 1 byte, na primeira ordem. Meio bagunçado escrever assim, particularmente não gosto, mas a outra forma não é tão melhor:

Ao menos parece mais intuitivo, não?

Agora vamos aos conceitos do PCF8574 para entender como manipular cada bit.

Dominando PCF8574 com MicroPython

Mesmo que já conheça a base binária, preciso referenciar os conceitos.

Os bits são lidos da direita para a esquerda. Temos 8 pinos de IO para usar, portanto, 1 byte. Assim, os valores para cada um dos pinos é:

128 64 32 16 8 4 2 1.

Suponhamos que todos os pinos estejam em LOW: 0x00. Se quisermos levantar o primeiro e o último bit, devemos escrever o valor 129 para o PCF8574, É fácil entender; onde deve ficar em HIGH, basta colocar o valor 1. Se passar o valor em decimal, basta somar o valor que está na posição dos bits. Se quiser escrever diretamente em binário, também pode. Basta escrever em binário: 0b10000001. A leitura e escrita ficaria assim:

Fácil ou não? Mas…

…como manter os estados?

Suponhamos que você já tenha colocado esses 2 bits em HIGH; o primeiro e o oitavo bit, portanto, o valor atual no PCF8574 é 129. Se quisermos levantar o bit no quarto pino, devemos escrever o valor 8. Só que já existem 2 bits em HIGH, se escrevermos 8 diretamente, os demais pinos irão para o estado 0. Como mantê-los em HIGH? Dá pra fazer de dois modos, sendo que o segundo modo que exemplificarei é o que chamo de “limpo”. O primeiro é mais fácil de entender, então vamos lá (não esperava que aumentar os GPIO na Raspberry fosse transparente, certo?).

Memória de bits

Essa forma, como citei, não é a melhor. Mas vai funcionar.

Pense que o valor de cada bit, do bit 7 ao bit 0 é 128, 64, 32, 16, 8, 4, 2, 1. Se desejássemos levantar os 3 primeiros bits precisaríamos somar 4+2+1:

bits_value = 4+2+1
i2c.writeto(34,bytes([bits_value])

Se quisermos então adicionar o sétimo bit, somamos mais 128. Se quisermos baixar o primeiro bit, subtraímos 1. Pra não precisar ficar fazendo esse tipo de contas, podemos criar um array correspondente à sequência de bits, mas pra ficar fácil de interpretar, montamos o array invertido:

pins = [1,2,4,8,16,32,64,128]
bits_value = pins[0] + pins[7] #1 + 128
i2c.writeto(34,bytes([bits_value])

E para baixar o pino, subtrair. Mas para subtrair será necessário validar que já não estava em LOW. Por exemplo, suponhamos que o bit 7 esteja em LOW e subtraímos 128, será o caos. Por isso a segunda solução é ideal, utiliza menos código (não vai precisar de condicionais pra avaliar os estados dos pinos) e é o que se utiliza normalmente:

Bitwise com MicroPython

Usando bitwise, manipulamos os bits sem preocupação com o estado atual. Podemos inverter um estado, levantar se já estiver em HIGH, baixar mesmo que já esteja em LOW e preservar os estados dos demais pinos usando “máscara”.

Vamos ver cada uma das respectivas iterações e então voltamos ao exemplo anterior.

Bitwise: AND

Com AND podemos apenas testar o valor do bit na posição desejada. Vamos iniciar com os 8 bits em LOW, e testar o sétimo bit:

valor_lido&(1<<7)

Isso não muda o valor, apenas o testa. Se o oitavo pino estiver em LOW, retornará 0, senão retorna o valor posicional binário.

Bitwise: OR

Usando OR, levantamos o bit na posição indicada, não importa o valor anterior do pino. Para preservar o valor anterior, usamos a máscara:

valor_lido|(1<<0)

Com isso, teremos o valor anterior e adicionalmente o bit 0, que guarda o valor 1. Considerando o oitavo bit que já havíamos colocado em HIGH, significa que agora devemos ter um valor de 128+1:

Bitwise: AND NOT

Do mesmo modo, podemos fazer o contrário; baixar um pino independente do estado anterior.

valor&~(1<<7)

Considerando que o bit 7 e o bit 0 estejam levantados, com essa última instrução teremos apenas o bit 0 levantado, ou seja, o valor será 1.

Repare que o valor era 128, que é o valor do oitavo bit. No bit 0 o valor é 1. O que fizemos aqui foi levantar o bit, não escrever o valor. Quando levantamos o bit 0, temos o valor do bit 7 + o valor do bit 0, que dá 129. Para guardar o valor, reatribuímos o resultado à variável valor. Por fim, baixamos o bit com AND NOT. Com o bit 7 em LOW resta apenas o bit 0 em HIGH, que em decimal é 1.

Bitwise: XOR

Com XOR podemos inverter o valor do bit na posição desejada. Já fiz isso em algum artigo, exemplificando com o blink. A única coisa que muda é o símbolo, que passa a ser o circunflexo:

valor^(1<<7)

Veja o exemplo:

Tabela de referência

Para recapitulação, eis o que usamos:

ANDvalor&(1<<7)128Testa o valor do bit na posição 7.
ORvalor = valor|(1<<0)128+1Levanta o bit na posição 0.
AND NOTvalor = valor&~(1<<7)129-128Baixa o bit na posição 0.
XORvalor = valor^(1<<0)0 Inverte o valor do bit na posição 0.

Dá pra fazer outros tipos de manipulação, deslocar com estouro de base etc. Tem muita aplicação, como por exemplo, em esteganografia.

Código de exemplo

Para não precisar montar um circuito, vou exemplificar usando o LED builtin da Raspberry Pi Pico, mas essa é a prova do bitwise, não da expansão de GPIO na Raspberry:

from machine import Pin
from time import sleep_ms as delay

builtin = Pin(25,Pin.OUT)
    while True:
        builtin.value(builtin.value()^1)
        delay(500)         

Isso faz um blink de 500ms de duração. Fácil ou não? Não sei se dará tempo de editar o vídeo a tempo dessa publicação, mas acompanhe a playlist de MicroPython em nosso canal no Youtube. Aproveite para se inscrever!