Programando Raspberry Pi em baixo nível em C - GPIO

Quando comprei meu primeiro Raspberry Pi e fiz alguns testes com a GPIO minha ideia era acessar a GPIO em baixo nível. Como em microcontroladores, no Raspberry Pi a programação em baixo nível é feita por acesso a registradores, que é o acesso a memoria. Porem em microcontroladores é possível ter acesso a recursos de CPU, no Raspberry o acesso fica restrito a periféricos, incluindo a GPIO.

Mas qual é a diferença em programar em baixo nível ou através de bibliotecas? No Raspberry Pi as bibliotecas mais famosas são em Python, a gpiozero e a RPi.GPIO.

A velocidade de acesso é o maior diferencial. Em alguns testes com o osciloscópio e rodando programas com um loop, onde dentro do loop a porta é habilitada e desabilitada, é possível perceber uma diferença absurda de velocidade conforme as fotos abaixo:


Baixo nível
(meu osciloscópio mede apenas até 50 Mhz)


RPi.GPIO


gpiozero

Mas o meu principal proposito foi apenas o aprendizado. Em buscas na internet pelo assunto achei esse ótimo artigo, que possui uma biblioteca bem simples. Encontrei também este excelente livro que foca na biblioteca em C bcm2835, onde por sinal o autor usou como nome o modelo do processador. Primeiro vou mostrar o que acontece por trás dessas bibliotecas, da forma mais simplista possível, depois comento sobre elas.

A documentação do processador não é excelente, mas ajuda bem e pode ser baixada nos links:

Links Espelhos: BCM2835 e BCM2837

Na pagina 6 do manual em 1.2.3 diz que a memoria física para periféricos inicia em 0x3F00 0000 (0x2000 0000 para RPi1), termina em 0x3FFF FFFF (0x20FF FFFF para RPi1) e a memoria virtual (bus address) inicia em 0x7E00 0000. Na pagina 90 temos os endereços de registradores da GPIO, o primeiro registrador (GPFSEL0) inicia em 0x7E20 0000.

A diferença de 0x7E20 0000 para 0x7E00 000 é 0x20 0000. Nosso endereço base de periféricos na memoria física é 0x3F00 0000 (0x2000 0000 para RPi1), então nosso endereço base da GPIO na memoria física será:
RPi2/3: 0x3F00 0000 + 0x20 0000 = 0x3F20 0000
RPi 1  : 0x2000 0000 + 0x20 0000 = 0x2020 0000.

Daqui para frente vou basear na posição apenas do RPi2/3 0x3Fxx xxxx.

O primeiro passo para acessar os registradores é fazer um mapeamento de memoria, não vou entrar em detalhes aqui por isso ser uma função do Linux, para quem quiser aprofundar recomendo o man da mmap() e este livro no cap. 49.

O mapeamento é feito com este código:


Onde é aberto um arquivo em memfd, que é usado em um dos parâmetros para o mapeamento. Os outros parâmetros que interessam são o tamanho (size_t length) e offset (off_t offset) que é nossa posição inicial 0x3F20 0000 da memoria física. A função mmap() faz o mapeamento apontando para o ponteiro map que é o que realmente precisamos.

O registrador FSEL define a função do pino que pode ser entrada, saída e ter funções alternativas (ALT0, ALT1...). Na pagina 102 é possível ver todas essas funções. Cada FSEL controla 10 pinos e cada pino é controlado por 3 bits. Por exemplo, se quiser setar o pino 4 para saída é necessário setar o bit 12 como 1:


Se for o pino 18 como ALT5 terá que ser setado o bit 25 como 1:

É possível ver que por padrão os pinos ficam como entrada (000).

Observe que todos os registradores trabalham com 32 bits (4 bytes), então as posições de memoria dos registradores avançam a cada 4 bytes. Desconsidere a primeira linha repetida com a segunda porque é um erro do manual.


Cada pino usa 3 bits, com um total de 10 pinos são 30 bits, os dois que sobram são reservados (30 e 31). Portanto é preciso observar em qual registrador está qual pino. Verificando na tabela acima e na pagina 92 do manual, para setar o pino 4 é preciso trabalhar como base a posição de memoria 0x7E20 0000 (GPFSEL0), já para o pino 18 na posição 0x7E20 0004 (GPFSEL1). Como estes valores são de posições virtuais, na física será 0x3F20 0000 e 0x3F20 0004.

Como já temos a memoria base da GPIO mapeada no ponteiro map, basta escrever no bit correto. Para setar pino 4 como saída, podemos criar um novo ponteiro, apontar para o map e escrever o bit 12, como abaixo:

volatile uint32_t* pfsel4 = map;
*pfsel4 = 0x1000; //onde 2^12 DEC = 1000000000000 BIN = 0x1000

Use um conversor DEC/HEX/BIN para entender melhor.

Sim, para quem trabalha programando registradores sabe que está não é a melhor forma e o ideal é sempre usar bitshift com bitwise. Mas o proposito aqui é mostrar de forma bem simples como eles trabalham.

Para setar o pino 18 como ALT5 então seria:

volatile uint32_t* pfsel18 = map + 0x1;
*pfsel18 = 0x2000000; // onde 2^25

O "+ 0x1" é porque precisamos ir para a posição de memoria 0x7E20 0004 (GPFSEL1). Em ponteiros quando você soma um, você avança uma posição de memoria e não adiciona um valor. Uma boa dica aqui para não se perder é pegar o valor final da posição de memoria do manual, dividir por 4 e somar com o ponteiro principal, tipo:

map + 0x04/4 // soma  1 DEC - GPFSEL0
map + 0x1C/4 // soma  7 DEC - GPSET0
map + 0x28/4 // soma 10 DEC - GPCLR0

O próximo passo é escrever nos bits que setam o pino para nível alto ou baixo. O registrador de nível alto é o GPSET e o de nível baixo o GPCLR. Aqui o entendimento é um pouco mais fácil já que é preciso apenas um bit por pino, então para o pino 4 é preciso setar o bit 4 e para o pino 18 o bit 18.

Vamos criar dois ponteiros, um para SET e outro para CLR, apontar para o map somando as posições de memorias desejadas:

volatile uint32_t* pset = map + 0x1C/4; // GPSET0
volatile uint32_t* pclr = map + 0x28/4; // GPCLR0

Agora basta escrever nos bits correspondentes, neste exemplo colocando tanto o pino 4 quanto o 18 em nível alto e baixo:

// Para pin 4 seta o bit 4 (2^4)
*pset=0x10; // Coloca em nível alto
*pclr=0x10; // Coloca em nível baixo

// Para pin 18 seta o bit 18 (2^18)
*pset=0x40000;
*pclr=0x40000;

Abaixo segue o código completo para piscar um led no GPIO4 ou GPIO18:

Para o pino 4:
Para o pino 18:

Aqui um outro exemplo usando o GPIO3 como entrada (registrador GPLEV0) e o GPIO18 como saída. Colocando o GPIO3 em nível baixo o led acende. No GPLEV (ponteiro plev, linha 27) é feito um and com o bit 3 para "pegar" o bit em 1.



Para a execução use o sudo antes já que o usuário comum não tem acesso ao /dev/mem

Na próxima postagem irei falar sobre as duas bibliotecas que comentei.

Referencias:
https://www.muddyengineer.com/2016/11/c-programming-raspberry-pi-2-gpio-driver/
https://www.iot-programmer.com/index.php/books/17-raspberry-pi-and-the-iot-in-c
https://www.amazon.com.br/Linux-Programming-Interface-System-Handbook/dp/1593272200

Comentários