Nesta aula, aprenderemos a controlar o periférico GPIO para acionar e ler os pinos digitais.

Na aula anterior, aprendemos a mexer nos bits de configuração dos microcontroladores PIC e AVR.

É importante dizer também que já vimos como gravar os microcontroladores na aula 5.

Informações básicas

O que é o GPIO

O GPIO é um periférico responsável por controlar os pinos de entrada e saída digital do microcontrolador. Isto significa que ele é capaz de:

  • Definir se um pino é saída ou entrada.
  • Se o pino for saída, então ele pode controlar o estado do pino (0 ou 1).
  • Se o pino for entrada, ele pode ler o estado do pino (0 ou 1).
  • Entre outros detalhes.

Com estas configurações, é possível fazer o microcontrolador ler e controlar ‘elementos’ que estão externos ao seu circuito. Por exemplo, o microcontrolador pode acionar LEDs, controlar motores (com a ajuda de um circuito de potência), ler botões ou ler determinados sensores.

Portanto, utilizando apenas o GPIO, é possível fazer uma gama de projetos com os microcontroladores.

Como controlar o periférico

Um detalhe que talvez tenha se perdido nas aulas anteriores é a forma de controlar os periféricos. Bem, normalmente, os periféricos possuem uma série de registradores que são por onde as configurações são feitas.

No caso, estes registradores ficam localizados na memória de dados do microcontrolador. Ou seja, na memória que não retém os dados caso o chip não seja mais alimentado. Relembre o exemplo do AVR visto na aula 3:

Mapa da memória de dados do AVR
Fonte: Datasheet do ATmega328p [pag 18]

Os registradores dos periféricos (chamados de ‘registradores I/O’) vão do endereço 0x20 ao 0xFF, sendo que uma parcela destes são ‘registradores estendidos’. 

Os ‘registradores estendidos’ vão acabar sendo iguais aos registradores I/O ‘normais’. Existirá diferença se você estiver programando em Assemby, pois aí será necessário utilizar certas instruções para acessar estes registradores (pág 17 do datasheet).

E não é necessário saber o endereço dos registradores, pois as IDEs fornecem arquivos de cabeçalho com os registradores já mapeados. Portanto, basta chamar o nome do registrador no programa, por exemplo:

PORTB = 1; // Coloca ‘1’ no registrador PORTB

Nomes dos registradores

Normalmente, os registradores vão ter um nome ‘padronizado’ para o GPIO (AVR e PIC):

  • PORT – Utilizado para controlar um pino de saída.
    • No caso do PIC, ele é usado para ler o pino também. E, no PIC, pode aparecer um registrador chamado LAT que é semelhante ao PORT.
  • PIN – Utilizado para ler um pino de entrada.
    • Válido para o AVR.
  • TRIS – Utilizado para definir um pino como entrada ou saída.
    • Válido para o PIC.
  • DDR – Utilizado para definir um pino como entrada ou saída.
    • Válido para o AVR.

Estes registradores vão receber uma letra na frente do nome para identificar o grupo de pinos ao qual ele pertence. E podem existir os grupos: A, B, C… e para cada um terá um registrador associado: PORTA, PORTB, PORTC… (o mesmo vale pros outros registradores).

Dentro de cada registrador (normalmente 8 bits), vai ter 1 bit responsável por controlar cada pino. E esta lógica segue a numeração dos pinos. Isto é, o 1º bit menos significativo controla o pino 0 do grupo especificado, o 2º bit controla o pino 1 e assim em diante. Ex: 1º bit do PORTB controla o pino PB0 (ou RB0 no caso do PIC).

Linguagem C e operações bit a bit

Uma etapa importante da programação de microcontroladores que este curso não apresentará é a introdução à linguagem C. Decidi não colocar esta parte, pois já existem inúmeros cursos muito bem feitos sobre o assunto.

Então, se você não tem conhecimento sobre programação ou sobre a linguagem C, recomendo que faça um curso ou leia um livro sobre o assunto, pois é fundamental entender a linguagem para saber programar bem e para entender alguns conceitos que abordarei.

De toda forma, utilizarei, de agora em diante, algumas macros que acessam bits individuais dos registradores e que podem ser muito úteis. Não pretendo explicar seu funcionamento pelo motivo anterior. Mas são macros para: setar um bit específico; limpar um bit específico; ler um bit específico; comutar um bit específico (0 para 1 ou 1 para 0).

1
2
3
4
#define SET_BIT(REG, PIN) (REG |= 1 << PIN)
#define CLR_BIT(REG, PIN) (REG &= ~(1 << PIN))
#define READ_BIT(REG, PIN) (REG & (1 << PIN))
#define TOGGLE_BIT(REG, PIN) (REG ^= 1 << PIN)

AVR

Circuito de GPIO

O circuito de um único pino de GPIO do AVR pode ser um pouco chatinho de entender. Então, não pretendo mostrar e explicar o circuito aqui. Porém, você pode ver o circuito de GPIO do ATmega328p na página 85 do datasheet.

De toda forma, acho interessante mostrar o circuito equivalente de um pino para evidenciar algumas características que o ATmega328p tem. Veja abaixo o circuito equivalente do pino Pxn (x é o grupo e n é o número do pino, exemplo: PB2).

Circuito equivalente de um pino
Fonte: Datasheet do ATmega328p

Com base na imagem acima, temos:

  • Diodo ligado entre o pino e o GND:
    • Garante que a tensão do pino não seja negativa. Isto, porque, se a tensão no pino for menor que GND, este diodo passa a conduzir.
    • Se não houver um limitador de corrente (resistor), o pino pode ser danificado em caso de tensão negativa.
  • Diodo ligado entre o pino e o Vcc:
    • Garante que a tensão do pino não fique acima de Vcc. Isto, porque, se a tensão no pino for maior que Vcc, este diodo passa a conduzir.
    • Se não houver um limitador de corrente (resistor), o pino pode ser danificado em caso de tensão acima de Vcc.
  • Chave ligada a um resistor de pull-up:
    • Por meio da chave, é possível habilitar ou desabilitar a funcionalidade do resistor de pull-up no pino.

Registradores de GPIO

Por padrão, os registradores são inicializados com 0.

É importante lembrar que cada bit dos registradores está associado a um pino específico. Vejamos:

  • DDR:
    • Define se o pino é saída (1) ou entrada (0).
    • Exemplo definindo pino PB2 como saída e demais pinos como entrada:
      • DDRB = 0b100.
  • PORT:
    • Se o pino for configurado como saída, este registrador controla seu estado: 1 -> Vcc na saída; 0 -> GND na saída.
    • Exemplo definindo pino PD5 como nível alto e demais pinos em nível baixo:
      • PORTD = 0b10000.
  • PIN:
    • Se o pino for configurado como entrada, este registrador lê seu estado.
    • Exemplo lendo os pinos do grupo C:
      • variavel = PINC;

Além destes, existe um outro registrador, chamado de MCUCR, que é responsável, também, por habilitar/desabilitar os resistores de pull-up. Na realidade, apenas o bit 4 deste registrador que faz isto, e o bit 4 recebe o nome de PUD. Veja a tabela abaixo para entender como todos estes registradores controlam o pino digital.

Registradores de GPIO
Fonte: Datasheet do ATmega328p

Se o bit PUD estiver em 1, ele desabilita todos os resistores de pull-up. E quando ele está em 0, é o registrador PORT que habilita/desabilita o pull-up em cada pino individualmente (isto se o pino for uma entrada).

Rotina de delay

É importante comentar sobre o comando de delay, pois ele pode ser útil em diversos programas. No caso, vão existir os seguintes comandos:

  • _delay_ms(tempo);
    • Cria um delay em milissegundos.
  • _delay_us(tempo);
    • Cria um delay em microssegundos.

Para utilizar os delays corretamente, é preciso fazer duas coisas: 

  1. Incluir a biblioteca associada:
    • #include <util/delay.h>
  2. Definir a frequência do clock por meio do seguinte define:
    • #define F_CPU 16000000UL
    • O exemplo acima é para um clock de 16MHz.
    • A configuração errada deste parâmetro pode fazer seu delay ficar mais lento ou mais rápido do que deveria.

Exemplo de escrita e leitura do GPIO

Para ilustrar como utilizar os registradores, vou criar um exemplo onde um LED é acionado caso um botão seja pressionado.

Circuito

O circuito abaixo não possui as ligações de alimentação, clock e RESET. Estes detalhes foram vistos nas aulas anteriores. O circuito serve apenas para dar uma ideia de como seria a ligação com o LED e o botão. 

Circuito com botão e LED ligados no ATmega328p

O fio preto na ligação acima é o GND (ligado no pino 8 do microcontrolador).

O LED e seu resistor estão ligados no pino PD2. E o botão está ligado no pino PD3 de um lado e no GND do outro. Ou seja, quando pressionado, o botão manda nível baixo no pino. Como não existe resistor de pull-up no circuito acima, teremos que ativá-lo na programação.

Código

Para utilizar os registradores do GPIO no código, é preciso incluir a seguinte biblioteca:

#include <avr/io.h>

Com isto, é só programar os registradores de acordo com o que foi mostrado anteriormente.

As demais informações importantes estão comentadas no código. Veja abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Estou utilizando o Arduino para testar, o qual usa um cristal externo de 16MHz
#define F_CPU   16000000UL

// Inclui a biblioteca com as definições dos registradores de GPIO
// e a biblioteca do delay
#include <avr/io.h>
#include <util/delay.h>

// Macros uteis para fazer operações bit a bit
#define SET_BIT(REG, PIN) (REG |= 1 << PIN)
#define CLR_BIT(REG, PIN) (REG &= ~(1 << PIN))
#define READ_BIT(REG, PIN) (REG & (1 << PIN))
#define TOGGLE_BIT(REG, PIN) (REG ^= 1 << PIN)

// Pinos utilizados
#define LED     PD2
#define BOTAO   PD3


int main(void)
{
    // Define o LED como saída (1) e o BOTAO como entrada (0)
    DDRD = 1 << LED;
   
    // Outra forma de fazer isto:
    // SET_BIT(DDRD, LED);
    // CLR_BIT(DDRD, BOTAO); // Redundante, mas é bom pra quem for ler o código entender o propósito dele
   
    // Habilita o pull-up no pino do botão
    // O PUD inicializa em 0, então é preciso apenas colocar 1 no bit PORTD
    SET_BIT(PORTD, BOTAO);
   
    // Posso armazenar a leitura do botão em uma variável
    uint8_t leitura = READ_BIT(PIND, BOTAO);
   
    while (1)
    {
        // Como o botão é ativado em 0, então eu testo se a leitura é igual a 0 (exclamação para barrar)
        if(!READ_BIT(PIND, BOTAO))
        {
            // Liga o LED
            SET_BIT(PORTD, LED);
           
            // Delay para minimizar o efeito de 'bounce' do botão
            _delay_ms(100);
        }
        // Se não foi pressionado
        else
        {
            // Desliga o LED
            CLR_BIT(PORTD, LED);
        }
       
        // Assim como abordado na aula 5, é possível colocar um delay aqui para 'economizar' energia
        // O delay só não pode ser grande a ponto de atrapalhar a leitura do botão
        //_delay_ms(300);
    }
}

Observação

Uma coisa interessante de ser falada é o tamanho ocupado pelo código. Após compilá-lo, obtive como resultado:

Uso da memória de programa: 170 bytes (0,5% cheia)

Uso da memória de dados: 0 bytes (0,0% cheia)

Isso faz sentido, já que nosso código é bem pequeno e simples. Por outro lado, compilando um código no Arduino com a mesma lógica, eu obtive:

Uso da memória de programa: 1044 bytes (3% cheia)

Uso da memória de dados: 9 bytes (0% cheia)

O Arduino gastou 874 bytes a mais para o “mesmo código”. Na realidade, o código base do Arduino pro ATmega328p (e demais microcontroladores) possui algumas definições e configurações adicionais que acabam gastando mais memória mesmo.

Mas, como pôde ser visto, a diferença para este caso foi relativamente pequena. Em outros casos, a diferença de performance e ou espaço ocupado é bem significativa.

PIC

Circuito de GPIO

O circuito de um único pino de GPIO do PIC pode ser um pouco chatinho de entender. Então, não pretendo mostrar e explicar o circuito aqui. Porém, você pode conferir os circuitos dos pinos do PIC16f628A nas páginas 33 a 44 do datasheet.

De toda forma, acho interessante mostrar um trecho do circuito do pino RB0, onde é possível ver algumas características interessantes dele. Veja abaixo:

Parte do circuito de GPIO do PIC
Fonte: Datasheet do PIC16f628A

Com base na imagem acima, temos:

  • Diodo ligado entre o pino e o GND (Vss):
    • Garante que a tensão do pino não seja negativa. Isto, porque, se a tensão no pino for menor que GND, este diodo passa a conduzir.
    • Se não houver um limitador de corrente (resistor), o pino pode ser danificado em caso de tensão negativa.
  • Diodo ligado entre o pino e o Vdd:
    • Garante que a tensão do pino não fique acima de Vdd. Isto, porque, se a tensão no pino for maior que Vdd, este diodo passa a conduzir.
    • Se não houver um limitador de corrente (resistor), o pino pode ser danificado em caso de tensão acima de Vdd.
  • Chave de pull-up:
    • Por meio desta chave, é possível garantir um pull-up no pino. E ela pode ser habilita/desabilitada.

Registradores de GPIO

É importante lembrar que cada bit dos registradores está associado a um pino específico. Vejamos:

  • TRIS:
    • Define se o pino é saída (0) ou entrada (1).
    • Exemplo definindo pino PA2 como saída e demais pinos como entrada:
      • TRISA = 0b11111011;
    • Ou então, você pode acessar o pino diretamente (mais prático):
      • TRISAbits.TRISA2 = 0;
    • Em alguns PICs, os pinos podem ser configurados como analógicos ou digitais. Nesse caso, para fazer as configurações acima, é preciso garantir que o pino seja digital. E isto é feito por meio de um registrador chamado ANSEL.
  • PORT:
    • Se o pino for configurado como saída, este registrador controla seu estado: 1 -> Vdd na saída; 0 -> GND na saída.
    • Exemplo definindo pino PB1 como nível alto e demais pinos em nível baixo:
      • PORTB = 0b10;
    • Ou então:
      • PORTBbits.RB1 = 1;
    • Se o pino for configurado como entrada, este registrador lê seu estado.
    • Exemplo lendo os pinos do grupo A:
      • variavel = PORTA;
    • Ou então, lendo um bit específico:
      • variavel = PORTBbits.RB0;

Específico para o PIC16f628a:

Além destes, existe um outro registrador, chamado de OPTION_REG, que é responsável por habilitar/desabilitar os resistores de pull-up. Na realidade, apenas o bit 7 deste registrador que faz isto, e o bit 7 recebe o nome de RBPU.

Se o bit RBPU estiver em 1, ele desabilita todos os pull-ups do PORTB e habilita se estiver em 0. No PIC16f628a, só existem pull-ups no grupo de pinos B.

Rotina de delay

É importante comentar sobre o comando de delay, pois ele pode ser útil em diversos programas. No caso, vão existir os seguintes comandos:

  • __delay_ms(tempo);
    • Cria um delay em milissegundos.
  • __delay_us(tempo);
    • Cria um delay em microssegundos.

Para utilizar os delays corretamente, é preciso definir a frequência do clock por meio do seguinte define:

    • #define _XTAL_FREQ 4000000
    • O exemplo acima é para um clock de 4MHz.
    • A configuração errada deste parâmetro pode fazer seu delay ficar mais lento ou mais rápido do que deveria.

Exemplo de escrita e leitura do GPIO

Para ilustrar como utilizar os registradores, vou criar um exemplo onde um LED é acionado caso um botão seja pressionado.

Circuito

O circuito abaixo não possui as ligações de alimentação, clock e RESET. Estes detalhes foram vistos nas aulas anteriores. O circuito serve apenas para dar uma ideia de como seria a ligação com o LED e o botão. 

O fio preto na ligação acima é o GND (ligado no pino 5 do microcontrolador).

O LED e seu resistor estão ligados no pino RA3. E o botão está ligado no pino RB0 de um lado e no GND do outro. Ou seja, quando pressionado, o botão manda nível baixo no pino. Como não existe resistor de pull-up no circuito acima, teremos que ativá-lo na programação.

Código

Obs: Não estamos utilizando aquelas macros que comentei no começo do post, pois o PIC permite o acesso individual dos bits de forma bem prática.

Para utilizar os registradores do GPIO no código, é preciso incluir a biblioteca abaixo. Ela inclui várias outras importantes para a programação do PIC que você estiver utilizando:

#include <xc.h>

Com isto, é só programar os registradores de acordo com o que foi mostrado anteriormente.

As demais informações importantes estão comentadas no código. Veja abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Bits de configuração
// A aula 6 ensina a configurá-los
#pragma config FOSC = INTOSCIO  // Oscillator Selection bits (INTOSC oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = ON       // Power-up Timer Enable bit (PWRT enabled)
#pragma config MCLRE = ON       // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is MCLR)
#pragma config BOREN = ON       // Brown-out Detect Enable bit (BOD enabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable bit (RB4/PGM pin has digital I/O function, HV on MCLR must be used for programming)
#pragma config CPD = OFF        // Data EE Memory Code Protection bit (Data memory code protection off)
#pragma config CP = OFF         // Flash Program Memory Code Protection bit (Code protection off)

// Inclui a biblioteca com as definições dos registradores de GPIO
#include <xc.h>

// Estou utilizando o oscilador interno de 4MHz
#define _XTAL_FREQ  4000000

#define BOTAO       PORTBbits.RB0
#define BOTAO_TRIS  TRISBbits.TRISB0
#define LED         PORTAbits.RA3
#define LED_TRIS    TRISAbits.TRISA3

void main(void)
{
    // Define LED como saída (0) e BOTAO como entrada (1)
    LED_TRIS = 0;
    BOTAO_TRIS = 1;
   
    // Habilita o pull-up no pino do botão
    OPTION_REGbits.nRBPU = 0;
   
    // Posso armazenar a leitura do botão em uma variável
    unsigned char leitura = BOTAO;
   
    for(;;)
    {
        // Como o botão é ativado em 0, então eu testo se a leitura é igual a 0 (exclamação para barrar)
        if(!BOTAO)
        {
            // Liga o LED
            LED = 1;
           
            // Delay para minimizar o efeito de 'bounce' do botão
            __delay_ms(100);
        }
        else
        {
            // Desliga o LED
            LED = 0;
        }
       
        // Assim como abordado na aula 5, é possível colocar um delay aqui para 'economizar' energia
        // O delay só não pode ser grande a ponto de atrapalhar a leitura do botão
        //__delay_ms(300);
    }
   
    return;
}

Observação

Uma comparação interessante de ser feita é no espaço ocupado pelo programa feito no PIC em relação ao programa feito no AVR. No caso, após compilar o código, obtive o seguinte resultado:

Uso da memória de programa: 35 bytes (1,7% cheia)

Uso da memória de dados: 5 bytes (2,2% cheia)

Embora o PIC normalmente apresente uma memória menor que o AVR (de forma geral), é perceptível que o “mesmo” programa feito no PIC ocupa menos espaço. Ou seja, uma característica meio que se equilibra com a outra.

Um dos motivos para isto é que a instrução do processador no PIC16f628A ocupa 14 bits, enquanto no AVR ela ocupa 16 bits.

Observações finais

Finalmente programamos o AVR e o PIC para acionar LEDs e ler botões. Agora você já pode fazer diversos projetinhos interessantes, por exemplo um cubo de LED.

Antes de avançar nosso estudo para os periféricos “mais complexos”, estudaremos as interrupções na próxima aula.