C1 - Pisca LED

MicrocontroladorPICPIC16F887MPLABX-XC8SimulIDE

Pisca LED

Uma prática costumeira em programação, sempre que se testa a compilação em um projeto novo, é exibir na tela a mensagem "Hello world" ou em português "Ola mundo". Tal prática ocorre pelo fato de que, exibir uma mensagem na saída padrão, que geralmente é o monitor, é o programa mínimo funcional que se pode executar em um computador.

Em um sistema microcontrolado, o programa equivalente ao "Ola mundo" é o acionamento intermitente de uma dispositivo de sinalização em uma saída digital, ou seja, fazer um LED conectado a uma pino piscar: Pisca LED.

Assim, o Pisca LED ou Blink LED é o programa mínimo e ponto de partida no estudo dos sistemas microcontrolados, e de extrema importância por testar a compilação do programa (firmware), sua gravação no dispositivo alvo e ao menos uma parte do circuito em sua execução, validando o processo de compilação, gravação e execução.

Objetivo

Acionar um LED de forma que ‘pisque’ com frequência de 1Hz.

Conteúdo

  • Estrutura básica de um programa;
  • Comentários;
  • Diretivas de configuração;
  • Inclusão de biblioteca padrão;
  • Programa principal;
  • Configuração e acionamento de pino como saída.

Circuito (Hardware)

O dispositivo responsável por acionar o LED é o microcontrolador(uC), através de um pino configurado como saída. Este pino pode estar fornecendo corrente como fonte (source) ao ramo ou drenando (sink) corrente dele.

As configurações como fonte e dreno são mostradas na Figura 1 respectivamente em (a) e (b).

Figura 1(a): Ligação de pino como fonte (source) Figura 1(b): Ligação de pino como dreno (sink)
circuito circuito

O pino do uC possui limitação de corrente, que pode variar a depender do modelo ou fabricante.

O uC aqui utilizado, PIC16F887, possui corrente máxima de 25mA por pino, tanto como fonte quanto como dreno, conforme indica o seu datasheet (seção 17.0 Electrical especifications, página 241).

Note que apesar de cada pino possuir uma corrente máxima, outra limitação é que a somatória dessas corrente não pode ultrapassar 200mA para o conjunto dos pinos do PORTA, PORTB e PORTE. Bem como para o PORTC e PORTD também limitado a 200mA.

Como em ambas as ocasiões a corrente máxima é a mesma, não existe uma predileção por uma configuração de montagem ou outra. Tal predileção ocorre quando a capacidade combinada de uma configuração é maior do que a outra, normalmente drenar corrente é a condição mais comum.

Para o uC aqui abordado, ambas as formas de ligação possuem as mesmas condições, não havendo diferença em capacidade de condução para o pino como fonte ou dreno.

A vantagem de utilizar o pino como fonte é trabalhar com uma lógica direta, em que o estado lógico 1(verdadeiro, +5V) produz o acionamento do LED, enquanto que na configuração do pino como dreno a lógica de acionamento é invertida, pois o pino em estado lógico 0(falso, 0V) produz o acionamento do LED.

Programa (Firmware)

O programa em microcontrolador é denominado firmware, assim como para computador é denominado software.

Um único arquivo de código fonte (Source Files) é criado nesse projeto, como pode-se ver na Figura 2, denominado main.c.

Figura 2: Árvore de diretório do projeto
circuito

É uma boa prática nomear o arquivo fonte com o nome main.c quando ele possuir a função principal do programa, que possui o mesmo nome. Os demais arquivos fonte devem possuir nomes relacionados às funções que eles contém.

Segue o código fonte para o projeto Pisca LED.

/*
 * File:   main.c
 * Author: josewrpereira
 *
 * Created on 12 April 2021, 19:39
 * 
 * IDE:         MPLAB X IDE v5.45
 * Compiler:    XC8 v2.31
 * Operating System: Debian GNU/Linux bullseye/sid
 * Kernel: Linux 5.10.0-5-amd64
 * Architecture: x86-64
 * 
 * Objetivo: 
 *      Piscar o LED com intervalo de 500 ms
 * 
 * Pinos    |nº     |Conexão
 *  VDD     |11,32  | Alimentação (Vcc/+5V)
 *  VSS     |12,31  | Alimentação (GND/0V)
 *  RD0     |19     | LED (source/sink)
 */

                                // Inclui diretivas de configuração do uC
// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO 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 and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = OFF      // RE3/MCLR pin function select bit (RE3/MCLR pin function is digital input, MCLR internally tied to VDD)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = OFF      // Brown Out Reset Selection bits (BOR disabled)
#pragma config IESO = OFF       // Internal External Switchover bit (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is disabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

#include <xc.h>                 // Inclui biblioteca padrão do compilador XC8 
                                // para microcontroladores Microchip.

#define _XTAL_FREQ 4000000      // Define frequência do oscilador em uso.

void main(void)                 // Função principal = main.
{                               // Início do escopo da função main.
    PORTDbits.RD0 = 0;          // Inicia RD0 com o valor 0.
    TRISDbits.TRISD0 = 0;       // Configura RD0 como Saída.
    
    while( 1 )                  // Laço de repetição infinito.
    {                           // Inicio do escopo do laço de repetição.
        PORTDbits.RD0 = 1;      // Liga RD0.
        __delay_ms(500);        // Espera 500 ms.
        PORTDbits.RD0 = 0;      // Desliga RD0.
        __delay_ms(500);        // Espera 500 ms.
    }                           // Fim do escopo laço de repetição.
    return;                     // Caracteriza main como uma função sem retorno.
}                               // Fim do escopo da função main.

Estrutura do programa

O programa presente no arquivo fonte main.c possui o seguinte arranjo:

  • Comentários;
  • Diretivas de configuração;
  • Inclusão de bibliotecas;
  • Diretivas de compilação;
  • Definições úteis ao programa;
  • Programa principal.

Comentários

Os comentários são trechos do código que não são compilados e servem para auxiliar na compreesão do código.

Os comentários são importantes para evidenciar ou explicar trechos de código que não são explícitos, de forma a garantir uma boa compreensão para quem venha a estudar o código, ou ainda evidenciar a identificação do projeto, do autor, informações de versão de interfaces e compiladores utilizados para criar o projeto, seu objetivo e um mapa simplificado de alocação de pinos, como usado no código aqui apresentado.

Em linguagem C os comentátios podem ser feitos em bloco ou em linha:

  • Bloco: tem como delimitador inicial /* e o final */. Todo o conteúdo entre os delimitadores não são considerados pelo compilador, sendo úteis apenas para os programadores.
  • Linha: a marcação // indica que todo o conteúdo à direita até o fim da linha é comentário, e da mesma forma não é considerado pelo compilador.

Uma das informações possíveis para inserir nos comentários e que considero ser útil é mostrar quais pinos são usados e em qual aplicação, como um mapa de alocação de pinos, conforme segue:

 /*
 * Pinos    |nº     |Conexão
 *  VDD     |11,32  | Alimentação (Vcc/+5V)
 *  VSS     |12,31  | Alimentação (GND/0V)
 *  RD0     |19     | LED (source/sink)
*/

Diretivas de configuração

Algumas diretivas como #pragma possibilitam um método de disponibilizar informaçẽos direto ao compilador, como por exemplo o acesso a registradores de configuração do microcontrolador, que ocorrem de forma exclusiva durante a gravação do código.

Existem diversas configurações possíveis, inicialmente usaremos as seguintes configurações:

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO 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 and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = OFF      // RE3/MCLR pin function select bit (RE3/MCLR pin function is digital input, MCLR internally tied to VDD)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = OFF      // Brown Out Reset Selection bits (BOR disabled)
#pragma config IESO = OFF       // Internal External Switchover bit (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is disabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

Um destaque para a primeira linha em que o registrador FOSC é configurado para o oscilador interno. Por uma questão de simplificação de montagem inicial do circuito, optou-se por tal configuração por não necessitar de um cristal oscilador e seus capacitores conectados externamente junto aos pinos 13 e 14. A limitação dessa escolha é que a frequência do oscilador interno, que por padrão é de 4MHz, atinge no máximo 8MHz, enquanto que ao utilizar um cristal externo pode-se trabalhar com até 20MHz, e a tolerância de calibração do oscilador interno pode variar em função da temperatura, o que não afeta nossas aplicações didáticas.

Inclusão de bibliotecas

O código #include <xc.h> inclui a biblioteca padrão do compilador XC8 para microcontroladores Microchip, de modo a oferecer funções básicas e as definições ou nomenclaturas dos registradores.

Todas as definições como PORTDbits.RD0, TRISDbits.TRISD0, FOSC, WDTE, __delay_ms() estão declaradas ou linkadas em xc.h, assim sendo uma biblioteca obrigatória para nosso uso.

Definições úteis ao programa

A diretiva #define, como o próprio nome indica, define um “apelido” para um valor. Esse apelido é substituido pelo real valor antes da compilação do código pelo pré-processador, de modo que apenas faz sentido para o programador.

A grande vantagem é que ao utilizar um valor muitas vezes durante o código pode-se alterá-lo falcilmente se ele possuir um #define associado. Suponha um código que utiliza um valor constante em seus cálculos, e é definido como #define PI 3.14. Dorante os cáculos usa-se o “apelido” PI (… PI * raio … ), e ele aparece imuneras vezes no programa. Daí você programador percebe que precisa aumentar a precisão dos cálculos, e esta aproximação com duas casas decimais não é mais suficiente, agora são necessárias cinco casas decimais. Seria penoso demais procurar por todas as inserções do PI no código para mudá-los um a um, além do fato de ser muito provavel a inserção de erros. Com o #define basta mudar na sua declaração para #define PI 3.14159, que o pré-processador substitui em todos os lugares que foi utilizado o seu “apelido”.

A definição _XTAL_FREQ deve assumir o valor do oscilador que estiver sendo utilizado na configuração, neste caso, o oscilador interno com o valor padrão é 4MHz.

Essa definição é importante sempre que for utilizada alguma função que envolva contagem de tempo, como a função __delay_ms().

Programa principal

Todo programa em linguagem C deve possuir uma função principal, cujo nome é obrigatório main. As demais funções devem possuir nomes de acordo com a tarefa que executam, mas são de total responsabilidade do programador.

Como no microcontrolador não há um sistema operacional para invocar a execução do programa, a função principal não necessita de um retorno, bem como não possui argumentos de entrada. Para indicar essa ausência de parâmetros usa-se a palavra reservada void que significa vazio.

void main(void)
{                               // Início do escopo da função main.

    return;                     // Caracteriza main como uma função sem retorno.
}                               // Fim do bloco da função main.

Parametrizar, habilitar e inicializar

A estrutura básica do programa para o microcontrolador(firmware) consiste em dois blocos de código, sendo o primeiro para a configuração e o segundo para executar as funções do programa.

O bloco de configuração consiste em parametrizar, habilitar e/ou inicializar os periféricos e dados utilizados no programa.

O bloco para as funções do programa consiste no código que normalmente é executado de forma repetida enquanto o sistema estiver em execução, ou seja, enquanto o microcontrolador estiver ligado.

O programa aqui apresentado utiliza o pino D0 do PORTD do microcontrolador por ser o PORT mais fácil de trabalhar.

Registradores

Todos os pinos estão conectados ao menos em um circuito específico.

Cada circuito específico realiza funções como, entrada digital, saída digital, contadores, comunicação, leitura de sinal analógico, etc, e são chamados de periféricos.

Todos os periféricos, são configurados por sinais elétricos em portas específicas, e tais sinais podem ser acessados através do mapeamento deles em um conjunto de registradores denominados Registradores de Funções Especiais (SFR - Special Function Register).

A Figura 3 mostra o mapa de registradores do PIC16F887 e pode ser acessada na página 25 do seu datasheet.

Figura 3: Mapa dos Registradores de Funções Especiais (SFR)
SFR
Fonte: Datasheet do PIC16F887

Em destaque estão os registradores PORTD e TRISD, que são os responsáveis pela configuração e manipulação dos pinos conectados ao PORTD, entre eles o pino D0 em que está conectado o ramo do circuito com o LED.

A Figura 4 mostra a representação do registrador PORTD, utilizado para acessar (ler ou escrever) os pinos nele associado, como o pino 19 ou RD0 do microcontrolador.

Figura 4: Registrador PORTD
PORTDreg
Fonte: Datasheet do PIC16F887

O PORTD possui 8 bits acessíveis individualmente e independentes, como indicado pelo R/W acima do nome de cada bit, respectivamente cada bit pode ser lido ou escrito.

O -x significa que o valor de reset é desconhecido, podendo ser 0 ou 1, mas não é possível afirmar qual valor que estará cada bit ao ligar ou religar o microcontrolador.

Ao setar qualquer um dos bits do registrador com a instrução PORTDbits.RDn = 1; sendo n o número do bit que se está manipilando, o respectivo pino assume a tensão de alimentação do chip, +5V.

Ao resetar qualquer um dos bits do registrador com a instrução PORTDbits.RDn = 0; sendo n o número do bit que se está manipilando, o respectivo pino assume o valor de 0V.

Para realizar a leitura do pino, pode-se utilizar o seguinte comando: var = PORTDbits.RDn;, sendo n o número do bit acessado e var a variável que recebe o estado do pino acessado.

Os dois estados lógicos são representados com os valores booleanos 0 e 1 e tais estados são assumidos quando a tensão no pino está dentro de um determinado intervalo de tensão:

  • 2,0V < Nível Lógico Alto < 5.0V
  • 0,0V < Nível Lógico Baixo < 0,8V

Note que para o intervalo entre 0,8 e 2,0, não há um estado bem definido, assim não é possível garantir para qual estado lógico será interpretada a leitura do pino.

As ações de setar e resetar somente são válidas se o pino estiver configurado como saída de dados. A ação de ler o estado do pino pode ser feita independente de configuração, seja entrada ou saída.

Para a configuração da direção de dados em um pino do PORTD, utiliza-se o registrador TRISD, conforme mostrado na Figura 5.

Figura 5: Registrador TRISD
TRISDreg
Fonte: Datasheet do PIC16F887 pág. 57

O TRISD possui 8 bits acessíveis para leitura ou escrita individualmente e independentes, como indicado pelo R/W acima do nome de cada bit. O -1 significa que o valor de reset é 1, ou seja, todos os pinos são inicializados como entrada ao ligar o uC.

Um pino do PORTD pode ser configurado como entrada de dados com a seguinte instrução: TRISDbits.TRISDn = 1;, onde n é o número do bit que está se configurando.

Para a configuração do pino como saída pode-se utilizar o seguinte comando: TRISDbits.TRISDn = 0;.

Tais registradores são um mapeamento do circuito da Figura 6, conforme segue.

Figura 6: Diagrama dos pinos RD<4:0>
DiagramaRD0-4
Fonte: Datasheet do PIC16F887 pág. 58

Note que as ações de escrita e leitura de determinados registradores produzem sinais de controle (borda de descida no biestável) no circuito do respectivo pino.

  • O terminal WR PORTD é acionado quando a ação de escrita no PORTD ocorre.
  • O mesmo ocorre para WR TRISD.
  • Para a ação de leitura do registrador, é gerado um sinal de acionamento em RD TRISD ou RD PORTD, respectivamente.

Os comandos de escrita acionam biestáveis (flip-flops) para armazenar a configuração ou o último dado escrito no pino.

Para a leitura há apenas buffers de acesso ao dado do pino mediante os sinais de controle.

As instruções de inicialização e configuração são apresentados abaixo e cada linha está ilustrada nas Figuras 7 e 8 em seguida:

    PORTDbits.RD0 = 0;          // Inicia RD0 com o valor 0.
    TRISDbits.TRISD0 = 0;       // Configura RD0 como Saída.
Figura 7: Inicialização do pino RD0-4 Figura 8: Configuração do RD0-4 como saída
PORTD0-0 TRISD0-0
Fonte: Datasheet do PIC16F887 pág. 58 Fonte: Datasheet do PIC16F887 pág. 58

Ao infinito e além

A condição mais comum é que o programa fique sendo executado de forma repetida durante todo o período em que o microcontrolador estiver ligado, assim utiliza-se um laço de repetição com uma condição que é verdadeira sempre.

    while( 1 )                  // Laço de repetição infinito.
    {                           // Inicio do laço de repetição.
      /*Aqui vai o seu programa*/
    }                           // Fim do laço de repetição.

Note que em linguagem C, para uma variável numérica, todo valor diferente de zero possui o estado lógico 1 (verdadeiro), enquanto que apenas o zero possui o estado lógico 0 (falso).

Após a configuração do pino e sua inicialização, dentro do loop infinito é inserida a sequência de passos que atende ao objetivo proposto.

  1. Acionar RD0, ligando o LED;
  2. Produzir um atraso de 500ms;
  3. Desacionar RD0, desligando o LED;
  4. Produzir um atraso de 500ms.

Codificando esses passos temos:

        PORTDbits.RD0 = 1;      // Liga RD0.
        __delay_ms(500);        // Espera 500 ms.
        PORTDbits.RD0 = 0;      // Desliga RD0.
        __delay_ms(500);        // Espera 500 ms.

Os quatro passos completam o ciclo de acionamento do LED, e então este ciclo se repete em função do laço a qual ele está inserido, garantindo assim a correta execução e atendendo ao objetivo inicial de acionar o LED de forma intermitente com frequẽncia de 1Hz, ou seja, 500ms ligado e o mesmo tempo desligado.

Ligação de pino como fonte (source) Ligação de pino como dreno (sink)
circuito circuito

Agora é a sua vez!

Crie o seu projeto, copie o código, execute-o, procure os erros, arrume-os, seja resiliente, leia novamente a explicação, busque outras fontes, pergunte, responda, faça alterações conscientes no código, explore, divirta-se.

Ficou alguma dúvida, entre em contato.

Bom trabalho!


« Arquivo de Configuração (config.h) « C1 - Pisca LED » C1 - Botão pulsador e LED »


Voltar