Você está na página 1de 33

MICROCONTROLADORES PIC

PROGRAMAÇÃO EM
LINGUAGEM C

HANDS
ON!

O QUE É UM MICROCONTROLADOR?
O microcontrolador PIC é um componente eletrônico
fabricado pela MICROCHIP, capaz de ser programado para
realizar operações lógicas e aritméticas, interagindo com
periféricos (leds, botões, sensores ou até outro PIC). Deste
fato vem o nome PIC (Peripheral Integrated Controller, ou
controlador integrado de periféricos). Um microcontrolador
(uC), diferentemente de um microprocessador, possue
memória volátil (RAM) e não volátil (EEPROM), além de
outros periféricos internos, o que o torna bastante útil. Este
é capaz de operar independente de memórias externas,
além de poder ser programado em linguagem de alto nível,
como a linguagem C.

ESTRUTURA BÁSICA DO PIC


Nesta apostila serão abordados os modelos
PIC16F628A, e PIC16F876A, pois estes estão inclusos em
uma boa gama de possíveis soluções. Apesar da estrutura
aqui mencionada tratar destes específicos modelos, esta
descreve a grande parte dos microcontroladores.
MEMÓRIA FLASH- É a memória em que será guardado
o programa propriamente dito, no formato de “linguagem
de máquina”. Foi desenvolvida na década de 80 pela
Toshiba e é do tipo EEPROM, porém se assemelha com a
RAM, permitindo que múltiplos endereços sejam apagados
ou escritos em uma só operação.
MEMÓRIA SRAM- Static RAM, significa que não precisa
ser periodicamente atualizada como as RAMs comuns, que
sem atualização perdem seus dados. É a memória volátil,
rapidamente acessada, e que é apagada toda vez que a
alimentação se ausenta. Local onde são armazenadas as
variáveis declaradas no programa.
MEMÓRIA EEPROM- É a memória não volátil, que
mantém os dados escritos independente da alimentação do
PIC. É acessada quando se deseja armazenar dados por
períodos indeterminados, como a senha de um usuário.
CPU (Central Processing Unit)- É um conjunto composto
por:
• PC: (Program Counter) Seleciona na FLASH o corrente
comando a ser executado.
• WREGs: (Work Registeres) Registradores especias de
trabalho. São acessados constantemente e servem
para realizar operações de movimentação e tem
acesso direto à ULA.

2
• ULA: Unidade Lógica e Aritmética do uC
(microcontrolador), responsável pelas operações
lógicas e aritiméticas dos WREGs.
FUSÍVEIS: Periféricos especiais responsáveis pelo pleno
funcionamento do sistema. Garantem que o programa não
“trave” e que o uC não opere sob tensões abaixo ou acima
do permitido.
PERIFÉRICOS: Circuitos auxiliares que livram o
programa de realizar trabalhos específios. Funcionam de
forma paralela ao programa do usuário (na FLASH),
ocupando o código apenas para ser configurado ou entregar
o resultado de algum serviço. Os periféricos abordados
nesse documento são:
• GPIO (General Purpose Input/Output): Usados para
entradas digitais, como um botão ou uma chave, e
saídas digitais, como um LED ou um motor DC. O
programa usuário pode setar uma saída, colocando um
pino externo do PIC em nível lógico ‘0’(0V) ou ‘1’(5V).
• ADC (Analog to Digital Converter): Usado para
recolher sinais analógicos externos e transormá-los em
digitais, para então serem processados pelo programa.
• USART (Universal Synchronous and Asynchronous
Receiver-Transmitter): Periférico responsável por
enviar e receber dados através do protocolo RS-232.
• TIMERS: Temporizadores de uso geral. Garantem
precisão de tempo entre eventos.

3
Figura 1 – Estrutura interna básica

HARDWARE
Para o pleno funcionamento do uC é necessário que:
• Ele esteja corretamente alimentado, com seu pino
Vdd ligado em 2,2 à 5,5V e seu pino Vss ligado ao
GND. (Os exemplos deste documento usam a tensão
de alimentação de 5V). É recomendado que um
capacitor de 100nF (de cerâmica com o rótulo 104)
seja posto na alimentação, próximo aos terminais do
uC, pois este funciona como um filtro contra ruídos
gerais.
• O pino MCLR (Masterclear) esteja em 5V. Ao ir para
0V este pino provoca um reset externo do PIC.
• Os pinos OSC1 e OSC2 estejam ligados no oscilador
externo, que nos exemplos será um cristal de
quartzo de 4MHz. Deve-se ressaltar que a
frequência interna (chamada de Fcy) do uC vale ¼
da frequência usada como fonte de relógio (Fosc).
Para uma correta geração de clock externo, é
recomendo o uso de dois capacitores de
desacoplamento nos terminais do cristal, no valor
de 22pF.

4
O uC PIC16F628A tem o benefíco de poder usar um
clock interno de 4MHz, além de não precisar do MCRL.
Isso permite que os três pinos correspondentes sejam
usados como GPIO. Esta será a configuração usada para
este uC nos nossos exemplos.

Figura 2 – Hardware básico: PIC16F628A

Figura 3 – Hardware básico: PIC16F876A


SOFTWARE

5
Como ambiente de desenvolvimento, será usado o
compilador PCWH da CCS. Este é constituído de um IDE
gráfico que pode ser executado em qualquer plataforma
Windows. O compilador, como o próprio nome já diz, tem o
papel de compilar o código em linguagem C para a
linguagem de máquina, a qual está pronta para ser gravada
na memória FLASH do uC. Deve-se ressaltar que este
compilador é case insensitive, ou seja, não diferencia letras
maíusculas de minúsculas. Portanto, se forem declaradas as
variáveis “Temper_Atual” e “temper_atual”, o compilador
acusará erro, pois é visto como ambiguidade.
Os programas embarcados seguem, como os outros
tópicos já vistos, uma estrutura básica. Quando o
dispositivo é ligado o programa inicia sua execução numa
fase chamada de “inicialização”. Nessa fase é feita toda a
configuração do sistema:
• Quais pinos serão entradas ou saídas.
• Quais periféricos serão utilizados e como serão
utilizados.
• Declaração das variáveis usadas pela função main.
• Chamadas de funções (ou comandos) que devem ser
executados apenas no início do processo.
Deve ficar claro que o código pertencente a essa fase é
executado apenas uma vez, sendo executado novamente
apenas se o uC for reiniciado. A partir disto o uC está pronto
para iniciar sua fase de trabalho propriamente dito. Essa
fase recebe o nome de “loop infinito”, pois é onde o
programa deve permanecer enquanto o dispositivo estiver
energizado. Basicamente, as ações sequencias nesse laço
são:
• Leitura das entradas.
• Processamento dos dados.
• Atualização das saídas.

6
Figura 4 – Fluxo básico de um programa embarcado

Pode-se adotar como softwares iniciais:

//--Programa Esqueleto-----// //--Programa Esqueleto-----//

#include <16F628A.h> #include <16F876A.h>


#fuses INTRC_IO, NOMCLR #fuses XT
#use delay(clock=4000000) #use delay(clock=4000000)

#use fast_io(a) #use fast_io(a)


#use fast_io(b) #use fast_io(b)
#use fast_io(c)
void main(void){
set_tris_a(0b11111111); void main(void){
set_tris_b(0xff); set_tris_a(0b11111111);
set_tris_b(0xff);
while(true){ set_tris_c(0xff);

} while(true){
} }
//-------------------------// }
//---------------------//

Explanação:
#include <16FXXXA.h>
Biblioteca que será utilizada para o compilador gerar o
código específico para o dispositivo utilizado.
#fuses INTRC_IO, NOMCLR, XT

7
Fusíveis configurados. INTRC_IO significa que será utilizado
como fonte de relógio o oscilador interno, que é de 4 MHz.
NOMLCR habilita o pino 4 do 16F628 para ser usado como
GPIO. XT indica que a fonte de relógio é um cristal externo
de frequência <= 4MHz. Não serão abordados todos os
fusíveis, porém estes podem ser vistos no CCS em “View”
-> “Valid Fuses”.
#use delay(clock=4000000)
Comando que fornece a base de tempo para as rotinas de
delay usadas no programa, que serão vistas mais adiante.
#use fast_io(a) //(b ou c)
Configura todos os pinos como GPIO (General purpose
In/Out). Cada pino do uC pode ser configurado com
diferentes propósitos, o que pode ser visto nas Figuras 2 e
3. Por omissão, diremos que todos os pinos serão GPIO.
Assim, se quisermos outra função para algum deles,
indicaremos adiante, atualizando o seu propósito. Os canais
GPIO no PIC seguem um padrão: cada um deve estar
associado a um byte e a um bit. Os bytes são nomeados por
vogais e são chamados pelo datasheet de PORT, como
PORTA, PORTB, PORTC, etc. Os bits são mapeados por
números, assim, podemos observar nas Figuras 2 e 3 que o
canal A3 (apresentado como RA3) está no pino 2 do
16F628, e no pino 5 do16F876. Como cada PORT é um byte,
cada um tem 8 canais. Por exemplo, o PORTA vai de A0 até
A7, e este último é identificado pelo programa do usuário
como “pin_a7”. O PIC16F628 contém as portas A e B,
enquanto o PIC16F876 tem as portas A, B e C. Porém, nem
todos os canais endereçáveis em teoria estão disponíveis
nos pinos externos.
void main(void){
Função principal de qualquer programa em C. É onde
realmente começa o programa que será embarcado. Os
comandos mencionados anteriormente são chamados de
“diretivas do compilador”, ou seja, são usados pelo
compilador mas não serão executados pelo PIC. O
compilador utiliza esses comandos para configurar
registradores especiais no momento da gravação. Portanto,
se você escreveu “#fuses XT”, o registrador responsável por
indicar a fonte de relógio será configurado no momento do
download, e não na inicialização.
set_tris_a(0b11111111);

8
Primeiro comando do programa (para este exemplo). Essa
função configura os canais GIPO como entrada ou saída. ‘1’
significa “IN” e ‘0’ “OUT”, o que facilita a memorização pela
semelhança: 1->In e 0->Out. O prefixo “0b” indica que o
número adiante está representado em binário. Para
representar um número em hexadecimal, o prefixo usado é
“0x”, portanto essa função pode ser escrita: “ set_tris_a
(0xff);”. Se um número não tiver nenhum prefixo, este será
interpretado pelo compilador como no formato decimal, que
é nossa forma natural. Podemos dizer então que essa
função também pode ser escrita na forma: “ set_tris_a (255);”,
que surtirá o mesmo efeito. Convém escrever tal função em
binário pela melhor visualização dos canais, que estão
dispostos em ordem decrescente. Portanto, se o primeiro
pino do 16F628 for usado para acender um LED, deve-se
configurar o canal A2 como saída, escrevendo: “set_tris_a
(0b11111011);”. Recomenda-se que todos os canais que não
serão utilizados sejam configurados como entrada, pois
estarão em alta-impedância, admitindo qualquer valor.
Quanto mais pinos são configurados como saída, maior é a
probabilidade do usuário acidentalmente tocá-los ao
manusear o protoboard, e isso pode ocasionar a queima do
dispositivo.
while(true){
Equivale a escrever “while(1==1){” ou “while(1){”. Esse é o
tal loop infinito, onde o programa permanecerá
indefinidamente. É dentro desse while que o programador
deve construir a fase de trabalho do uC.
1º EXEMPLO: Hello World!
//-------- Pisca LED -------- while(true){
output_bit(pin_a0,flag);
#include <16F628A.h> delay_ms(500);
#fuses INTRC_IO, NOMCLR flag=!flag;
#use delay(clock=4000000) }
}
#use fast_io(a)
#use fast_io(b) //-------------------------//

void main(void){ //-------- Pisca LED --------


short flag=0; #include <16F876A.h>
set_tris_a(0b11111110); #fuses XT
set_tris_b(0xff); #use delay(clock=4000000)

9
#use fast_io(a) while(true){
#use fast_io(b) output_high(pin_a0);
#use fast_io(c) delay_ms(500);
output_low(pin_a0);
void main(void){ delay_ms(500);
set_tris_a(0b11111110); }
set_tris_b(0xff); }
set_tris_c(0xff); //-------------------------//

Explanação:
short flag=0;
Declaração e inicialização da variável de 1 bit chamada
“flag”. Também pode ser escrito nas formas: “ boolean
flag=0;” ou “int1 flag=0;”
output_high(pin_a0);
Coloca o pino mencionado em nível lógico 1 (5V).
output_low(pin_a0);
Coloca o pino mencionado em nível lógico 0 (0V).
output_bit(pin_a0, flag);
Coloca o pino mencionado no nível lógico correspondente
ao valor corrente da “flag”.
delay_ms(500);
O programa literalmente pára durante o valor de tempo
informado (em milisegundos). O próprio compilador gera o
código responsável por esta ação, através de contadores,
os quais se baseiam em “#use delay(clock=4000000)”.
flag=!flag;
Realiza o “complemento de um” da variável “flag”. O
símbolo “!” corresponde à operação lógica NOT.
Os pinos configurados como entrada drenam uma
corrente extremamente pequena, na ordem de pico-
ampéres, pois são colocados em alta-impedância. Assim,
podemos considerá-la desprezível.
Os pinos definidos como saída não podem receber
sinal externo (corrente), pois, como já dito anteriormente,
isso certamente causará o falecimento do uC. Estes dois
dispositivos abordados podem fornecer até 25mA por pino
de saída. Portanto, se a carga utilizada drenar mais do que
esse valor, deve ser utilizado um driver, como um
transistor.

10
Figura 5 – Exemplo 1: hardware 16F876

Figura 6 – Exemplo 1: hardware 16F628

Compilação do programa:
Inicialmente abra o compilador PICC (PCWHD), clique
no símbolo de “pastas” (primeiro acima), e em seguida

11
“Close All”. Agora, para criar um novo projeto, clique
novamente no símbolo de “pastas”, e após isso em “New”
-> “Source File”. Escolha o diretório onde ficará salvo este
primeiro programa e o nomeie de “exemplo1”. Digite o
código de exemplo para o uC escolhido e em seguida
compile o programa em “Compile” -> “Compile”, ou através
da tecla de atalho “F9”. Neste processo o compilador gera 5
arquivos de saída, porém será utilizado apenas o
“exemplo1.hex”, que é o código que será transferido para a
memória FLASH do uC.

Gravação do programa:
Coloque o uC escolhido no protoboard e, antes de
colocar os outros componentes (capacitor, etc.), insira
apenas os jumpers relativos ao ICSP (In Circuit Serial
Programming). Plugue agora o cabo do ICSP no protoboard
e no gravador PICKit2, e plugue o cabo USB do gravador no
PC. Feito isso, abra o software PICKit2. Se o procedimento
dito foi feito corretamente, deve aparecer uma mensagem
confirmando a detecção do dispositivo. Se isso não ocorreu,
confira se houve alguma falha no dito procedimento (ordem
dos jumpers, mau contato, etc.), e então clique em “Tools”
-> “Check Communication”. Não se deve continuar a partir
deste ponto até que o uC não tenha sido detectado.
Antes de transferir o código, desabilite a função
“Tools” -> “Fast Programming”, pois esse modo está bem
mais susceptível a falhas de transferência, além de ser
pequena diferença em relação ao modo normal. Para
verificar se o código foi transferido corretamente, habilite a
função “Programmer” -> “Verify on Write”.
Feito tudo, clique em “File” -> “Import Hex”. Escolha o
“exemplo1.hex” que foi gerado na compilação e, depois de
importado o arquivo, clique finalmente em “Write” para
realizar a transferência do código para o uC. Confirmado o
sucesso do download, desconecte o plugue ICSP do
protoboard, insira os componentes necessários e energize o
circuito. Se tudo deu certo, o LED piscará a cada 1 segundo.
Os componentes não precisam ser retirados para
realizar um novo download. Porém, é recomendado que
sempre se inicie a construção de um hardware pelos
jumpers do ICSP, pois na ocorrência de uma falha (muito

12
comum) deve-se reduzir o hardware para encontrar o
defeito.
Outra maneira de realizar o download é pelo botão no
PICKit2 (hardware). Essa função é habilitada em
“Programmer” -> “Write on PICkit Button”. Quando tal
botão é pressionado, o arquivo .hex que está sendo
apontado no campo “Source” do PICKit2 (software) é
transferido para o uC (devidamente conectado). O mesmo
efeito é obtido pelo botão “Write”, eliminando a
necessidade de toda vez importar o arquivo a ser
transferido. Para testar essa funcionalidade, habilite-a
primeiro, depois, se o campo “Source” estiver apontando
para o “exemplo1.hex”, mude o tempo de delay no
programa (compilador) para 100ms e o recompile. Vá
agora para o PICKit2 (software) e, observando a tela,
pressione o botão no PICKit2 (hardware). O programa
percebe que houve uma alteração e realiza um “reloading”.
Os exemplos seguintes abordarão apenas o
PIC16f876A, por questões de total semelhança. Sempre que
for escrever um novo programa que possa se basear no
atual, copie o texto deste, clique em “Close All”, depois em
“New” -> “Source File”. Cole o texto copiado.
2º EXEMPLO: Push-button
/----------------------- push-button -------------------------

#include <16F876A.h> set_tris_b(0xff);


#fuses XT set_tris_c(0xff);
#use delay(clock=4000000)
while(true){
#use fast_io(a) if(input(pin_c4))
#use fast_io(b) output_high(pin_a0);
#use fast_io(c) else
output_low(pin_a0);
void main(void){ }
set_tris_a(0b11111110);

/---------------------------------------------------------------

Explanação:
input(pin_c4);

13
Realiza a leitura externa do nível lógico no pino mencionado
e a retorna para o programa.

Figura 7 – Exemplo 2: hardware 16F876


Em uma entrada utilizada para ler um botão, deve-se
usar um resistor ligado ao terra. Sem o resistor (chamado
de pull-down), se o botão não estiver pressionado, o nivel
lógico na entrada não será 0, pois este estará alta
impedância. Esse estado pode ser denominado floating
(flutuando), onde o sinal não tem um valor fixo, fica
variando aleatoriamente em função de ruídos locais.
Atenção ao ligar um push-button de 4 terminais no
protoboard. Dois de seus terminais estão conectados aos
outros dois. Portanto, se um dos terminais foi ligado à 5V, o
outro terminal conectado à esse também estará em 5V,
inviabilizando o uso do seu ramo no protoboard.

3º EXEMPLO: INTERRUÇÃO EXTERNA


Podemos engatilhar uma rotina específica dentro do nosso
microcontrolador a partir de sinais externos ou mesmo
eventos internos do microcontrolador. Neste exemplo
trabalheremos com interrupções externas, que em resumo
é a execução de determinada rotina quando houver uma
mudança de estado em um pino pré-determinado, sendo
esta uma forma mais eficiente de controlar as atividades do
microcontrolador por eventos externos, já que desta forma

14
não há perda de tempo ao se realizar a leitura do estado do
pino a cada ciclo de trabalho . O hardware que utilizaremos
a seguir é o mesmo do exemplo anterior ( push-button),
exceto pelo fato que o botão deve estar conectado ao pino
responsável por chamar interrupções externas , no caso do
PIC16F876A é o pino rb0.
//--------INTERRUPÇÃO EXTERNA----------------------------------------

#include <16F628A.h> }}
#fuses INTRC_IO, NOMCLR
#use delay(clock=4000000) #INT_RB
#use fast_io(a) Void piscaled_int_ext(void)
#use fast_io(b) {
if(input(pin_b0))
short int cont = 0; {
delay_ms(20);
void main(void){ if(input(pin_b0))
cont=0; {
set_tris_a(0xff); output_bit(pin_a0,cont);
set_tris_b(0b11111011); if(cont==0)
ext_int_edge(L_TO_H); cont=1;
enable_interrupts(GLOBAL); else
enable_interrupts(INT_RB); cont=0;
while(true){ }}}
sleep();

//--------INTERRUPÇÃO EXTERNA----------------------------------------

Explanação:
ext_int_edge(L_TO_H);
Esta função define que o microcontrolador entrará em interrupção
quando o pino de interrupção externa em questão passar do estado 0
para 1( low to high).
enable_interrupts(GLOBAL);
Função responsável por habilitar qualquer tipo de interrupção que
microcontrolador tenha, deve ser usada antes de habilitar qualquer
interrupção especificamente.
enable_interrupts(INT_RB);
Especifica a interrupção que será usada, no caso interrupção externa
por alteração de estado nos pinos RB que são responsáveis pelas
interrupções externas.

4º EXEMPLO: Display LCD 2x16


//----------------------------- LCD -------------------------------------

#include <16F876A.h> #use delay(clock=4000000)


#fuses XT

15
#use fast_io(a)
#use fast_io(b) lig(bl); des(led);
#use fast_io(c) for(i=0;i<6;i++){
seta(led,!input(led)); esp(400);
#include <lcdt.c> }
while(true){
#define lig output_high if(input(bot)){
#define des output_low printf(lcd_putc,"\f Feliz");
#define seta output_bit printf(lcd_putc,"\n :)");
#define esp delay_ms lig(led); lig(bl);
#define led pin_a0 }else{
#define bot pin_c4 printf(lcd_putc,"\f Triste");
#define bl pin_c7 printf(lcd_putc,"\n :(");
void main(void){ des(led); des(bl);
unsigned int i; }
set_tris_a(0b11111110); esp(200);
set_tris_c(0b01111111); }
}
lcd_init();
printf(lcd_putc,"\fCurso de uC PIC ");
printf(lcd_putc,"\nPET-EngElet.Ufes”);
//------------------------------------------------------------//

Explanação:
#include <lcdt.c>
Inclusão da biblioteca do LCD. Esse arquivo deve estar na
mesma pasta onde será salvo o programa que a utiliza.
#define lig output_high
Amarração entre duas entidades. Serve para aumentar a
legibilidade do código. O compilador substitui a parte
esquerda pela direita no momento da compilação do
programa.
unsigned int i;
Declaração da variável de 8 bits e sem sinal (0 – 255)
chamada “i”.
lcd_init();
Inicialização da biblioteca do LCD. Essa função configura os
pinos da porta B (menos o B0, que pode ser configurado na
“main()”), e prepara o LCD para iniciar sua operação.
printf(lcd_putc,"\fCurso de uC PIC ");
Função de impressão no LCD. Existem comandos especiais:
• \f -> Limpa a tela toda e põe o cursor na primeira
posição (1,1).
• \n -> Põe o cursor no início da segunda linha.
• \b -> Retorna o cursor uma posição.

Os caracteres são impressos sempre na posição


corrente do cursor, que pode ser alterada pela função

16
“lcd_gotoxy(x,y);”. A função “lcd_getc(x,y);” retorna o atual
caractere que ocupa a posição informada.
Como os canais B6 e B7 são utilizados para gravação e
comunicação com o LCD, deve-ser usar dois resistores para
aumentar a impedância de entrada dos terminais PGD e
PGC do PICKit2. Sem tais resistores, o LCD e o PIC drenam,
juntos, muita corrente do gravador, resultando numa
distorção do sinal de gravação.
O pino 3 do display é o sinal “V0”, que determina o
contraste entre a parte escrita e o plano de fundo da tela. O
potenciômetro deve ser regulado para obter-se uma boa
visualização.

Figura 8 – Hardware Display LCD

5º EXEMPLO: Conversão A/D


No instante em que o programa usuário requisita ao
módulo um processo de conversão A/D, o corrente valor de
tensão na entrada especificada é convertido em um valor
digital, para então ser manipulado pelo programa. Neste
exemplo o uC será configurado para uma conversão de 8
bits, e o range de tensão a ser convertida será o padrão, ou
seja, a própria alimentação do PIC (0–5V). Portanto, essa
faixa será dividida em 256 partes iguais, e o resultado de
uma conversão será o byte correspondente a parte em que
se encontra a tensão convertida.

17
Figura 9 – Função de transferência da conversão A/D desse exemplo
//------------------------ Voltímetro -----------------------------------

#include <16F876A.h>
#device adc=8 lcd_init();
#fuses XT printf(lcd_putc,"\f Voltimetro
#use delay(clock=4000000) ");
printf(lcd_putc,"\nPET-
#use fast_io(a) EngElet.Ufes");
#use fast_io(b) esp(2000);
#use fast_io(c) output_high(pin_c7);

#include <lcdt.c> while(true){


#define esp delay_ms digital=read_adc();
void main(void){ tensao=
unsigned int digital; 5.0*(float)digital/255.0;
float tensao; printf(lcd_putc,"\fT:
%1.2f",tensao);
set_tris_a(0b11111111); printf(lcd_putc,"\nD:
set_tris_c(0b01111111); %3u",digital);
esp(500);
setup_adc_ports(ALL_ANALOG); }
setup_adc(ADC_CLOCK_INTERN }
AL);
set_adc_channel(0);

//---------------------------------------------------------------------//

Explanação:
O PIC16F628A não tem conversor A/D.
#device adc=8
Configura a resolução da conversão. Pode ser observado na
Figura 9 que a variação mínima que essa conversão pode
reconhecer é de aproximadamente 20mV. Isso significa que

18
um sinal de 10mV e outro de 15mV serão convertidos para
o mesmo valor (0). Isso pode ser ruim para determinadas
aplicações. Por exemplo, o sensor de temperatura LM35
fornece em sua saída 10mV/°C. Uma conversão de 8 bits
não é capaz de perceber a diferença entre 0 e 1°C. Porém,
pode-se melhorar a resolução, utilizando 10 bits. Isso torna
o sistema capaz de perceber uma diferença mínima de 5mV
(0,5°C). Não se deve esquecer de utilizar uma variável do
tipo “unsigned long int” (16 bits), cuja impressão é feita pela
sintaxe “%4lu”. Outra forma de melhorar a resolução é
diminuir o range do valor a ser convertido. Para isso, são
utilizados sinais externos como V REFH e/ou VREFL, os quais
podem ser configurados na próxima função.
setup_adc_ports(ALL_ANALOG);
Configura todas as portas possíveis como analógicas. Neste
caso, nenhuma delas poderá ser utilizada como GPIO. Para
ver as possíveis configurações do periférico ADC, no IDE
CCS, clique com o botão direito em #include <16F876A.h> ->
“Open File at Cursor”, e vá à linha 220. Por omissão
(default), serão adotados como VREFH e VREFL os sinais de
alimentação, VDD e VSS, respectivamente.
setup_adc(ADC_CLOCK_INTERNAL);
Configura a frequência de operação (fonte de relógio) do
hardware.
setup_adc_channel(0);
Este PIC tem vários canais analógicos, porém apenas um módulo
de conversão. Essa função indica o canal que será ligado ao
módulo para o próximo processo. Como este exemplo utiliza
apenas um canal, este foi configurado na “inicialização” e não
sofrerá alteração. Em uma aplicação que utiliza múltiplos canais,
esta função deve estar no “loop infinito”, pois será dinâmica. O
compilador recomenda que entre a configuração de um canal e
a leitura do mesmo deve haver um intervalo de 10μs. Vá no
“Help” do compilador (F1), na aba “Index”, e digite:
“setup_adc_channel”. Veja o tópico “Examples”.
digital=read_adc();
Realiza a conversão A/D do último canal configurado e atribui o
resultado do processo à variável “digital”.
Note que no programa deste exemplo o backlight do
display é ligado após o comando “esp(2000);”. Isso implica que,
em algumas vezes, o backlight estará aceso durante a
apresentação, e nas outras ele estará apagado. Isso se dá ao
fato de que um canal configurado como saída, se não for

19
atualizado, assumirá valores imprevisíveis. Essa é uma comum
fonte de erros, pois saídas podem iniciar-se indevidamente
acionadas e provocar prejuízos. Recomenda-se que, logo após
configurar as entras/saídas, se desative todas as saídas.

Figura 10 – Hardware ADC

6º EXEMPLO: UART
A UART é definida como um periférico de comunicação
utilizado para troca de informações entre dispositivos
digitais. Este módulo se baseia no protocolo RS-232, o mais
popular padrão de comunicação assíncrona, ou seja, entre
dispositivos com fontes de relógio distintas. Por isso, a
grande maioria dos uCs possuem este hardware integrado.
Apesar de seguirem o mesmo protocolo, um uC e uma porta
serial de um PC não podem ser diretamente ligados, pois a
representação elétrica dos símbolos (bits 0 e 1) não é a
mesma. Os uCs utilizam o padrão TTL, ou seja, o bit 0 é
representado como 0V e o bit 1 como 5V (ou a alimentação,
caso seja diferente). Uma porta serial, bem como aparelhos
industriais, reconhecem o sinal lógico 1 um valor entre -25V
e -3V, e o sinal lógico 0 um valor entre 3 e 25V (geralmente
usa-se ±12V). Essa escolha se dá ao fato de aumentar a
relação sinal/ruído, permitindo uma maior taxa de
transmissão, maior distância do cabo que conecta os
dispositivos, além de diminuir a TEB (taxa de erro de bit).

20
Para que um uC e um PC possam trocar informações existe
o MAX232, que é um conversor de padrão elétrico para o
RS-232.
Este módulo também pode ser nomeado como USART,
que significa “Universal Synchronous and Asynchronous
Receiver-Transmitter”. Nessa configuração, além dos sinais
RX e TX, é transmitido também um sinal de relógio,
fornecido por apenas um dos nós envolvidos. Uma
comunicação síncrona permite uma taxa de transmissão
mais elevada, pois o instante da leitura do sinal é bem
definido devido à referência do relógio. O exemplo
demonstrado aqui abordará a comunicação assíncrona.
Em um sistema de comunicação RS-232, todos os
elementos envolvidos devem ser configuradados com os
mesmos parâmentros, pois só assim o sucesso da
transmissão é garantido. Como se os nós da rede “falassem
a mesma língua”. Os parâmetros desse protocolo são:
• Baud rate: Define a taxa de transmissão e
consequentemente o tempo de bit (tbit), que é o
inverso desse número. O valor mais comum é 9600
bps, e outros bastante usados são 2400, 4800, 19200
e 115200 bps (bits por segundo).
• Start bit: É um parâmetro imutável, unitário, tem
valor lógico zero e dura 1 tbit, como todos os outros
bits transmitidos.
• Payload: També chamado de carga útil, é o dado
transmitido. Pode assumir de 5 a 9 bits, e seu valor
padrão é 8 bits (1 byte).
• Parity bit: Bit de paridade. Tem o papel de identificar
erros na transmissão e, se presente, fica entre o
payload e o stop bit. Pode ser configurado de três
formas: paridade par, paridade ímpar ou ausente. Na
paridade par, ele é inserido de forma a tornar o
número de ‘1s’ no “payload+parity bit” um valor par.
Analogamente funciona a paridade ímpar. Dessa
forma, se for enviado o caractere ‘a’ (01100001) e os
dispositivos envolvidos foram configurados com
paridade par, o bit de paridade assumirá valor ‘1’,
totalizando quatro números ‘1s’. Se qualquer um dos
bits for lido erroneamente (devido à ruído), o bit de
paridade (que no caso é ‘1’) acusará o erro na
transmissão, e o dado recebido será descartado.

21
Existe, no entanto, a probabilidade (mesmo que
ínfima) de dois bits serem alterados numa mesma
transmissão, e assim o mecanismo de paridade não
detectará o erro.
• Stop bit: Pode ser unitário ou duplo, e tem valor
lógico um.

No PIC, um canal serial pode ser implementado por


hardware, ou seja, a UART, ou por software. Nessa última, o
programa embarcado é integralmente responsável pela
transmissão/recepção dos dados, tornando a CPU
indisponível para realizar qualquer outra tarefa durante
esse ato. As duas implementações surtem o mesmo efeito
(externo) e o receptor segue o mesmo algorítmo para
capturar o dado:

Figura 11 – Transmissão de um caractere ‘a’ (0x61 ≡ 0b01100001).


Oito bits de payload, sem bit de paridade e um stopbit

• A – O canal é constantemente observado. Espera-se


o startbit.
• B – A transição negativa indica o início do startbit.
Aguarda-se então ½ tbit.
• C – O startbit é verificado, pois a transição pode ter
sido causada por ruído. Se verdadeiro, aguarda-se 1
tbit e o processo continua. Se não, o processo volta
ao estado A.
• D – Inicia-se o processo de aquisição. O bit menos
significativo (LSB) é capturado e aguarda-se 1 tbit
para a aquisição do próximo bit. Todos os outros bits
do payload são capturados desta mesma forma.
• E – É verificado o stopbit, pois existe a chance de
ruídos terem validado as etapas B e C. Se
verdadeiro, o dado recebido é finalmente entregue

22
ao programa usuário. Se falso, o dado é descartado.
Retorna-se ao estado A.

//------------------------------- TX --------------------------------------

#include <16F628A.h> enable_interrupts(GLOBAL);


#fuses INTRC_IO, NOMCLR enable_interrupts(INT_EXT);
#use delay(clock=4000000) while(true){
sleep();
#use fast_io(a) }
#use fast_io(b) }

#use rs232(baud=9600, #INT_EXT


rcv=pin_b1, void trata_int_ext(void){
xmit=pin_b2, parity=N) putc(dado++);
if(dado==‘f’) dado=‘a’;
char dado=‘a’; delay_ms(200);
void main(void){ }
set_tris_a(0xff);
set_tris_b(0b11111011);

ext_int_edge(L_TO_H);
//-----------------------------------------------------------------------//

//------------------------------ RX ---------------------------------------

#include <16f876A.h> dado=getc();


#fuses XT flag=1;
#use delay(clock=4000000) }

#use fast_io(a) void main(void){


#use fast_io(b) set_tris_a(0xff);
#use fast_io(c) set_tris_c(0b10111111);
#include <lcdt.c>
lcd_init();
#use rs232(baud=9600, enable_interrupts(GLOBAL|
rcv=pin_c7, INT_RDA);
xmit=pin_c6,
parity=N) printf(lcd_putc,"\fDado: ");

short flag=1; while(true){


char dado=‘k’; if(flag){
printf(lcd_putc,"\n %c",dado);
#INT_RDA flag=0;
void trata_int_rx(void){ }

23
}
}

//--------------------------------------------------------------------//

Explanação:
#use rs232(baud=9600, rcv=pin_b1, xmit=pin_b2, parity=N)
Definição dos parâmetros do canal serial criado. Se os pinos
mencionados são RX e TX (UART), o canal é implementado
por hardware. Porém, pode-se usar quaisquer outros pinos
de I/O para se criar um canal. Nesse caso, a implementação
é feita por software, e o próprio compilador é o responsável
por esta tarefa.
char dado=‘k’;
Declaração e inicialização da variável global do tipo “char”
(8 bits) chamada “dado”. É dita global porque não foi
declarada dentro de uma função, e, portanto, pode ser
acessada por qualquer uma do programa.
ext_int_edge(L_TO_H);
Configuração da interrupção externa, realizada pelo pino
RB0/INT. O parâmetro “L_TO_H” indica que, se habilitada, a
interrupção ocorrerá na transição low to high, ou seja, na
borda de subida do sinal externo. Portanto, deve-se usar no
pino RB0/INT um push-button com um resistor de pull-
down, assim como descrito no segundo exemplo.
enable_interrupts(GLOBAL);
Para que qualquer interrupção seja acionada, é necessário
habilitar a chave interna chamada GLOBAL. Esse
mecanismo foi criado para que se possa desabilitar todas as
fontes de interrupção (que foram previamente habilitadas)
através de um único comando. Assim, se o programa entra
em uma região crítica, onde não pode ser interrompido, não
se faz necessário desabilitar as interrupções uma a uma.
Funciona como um “disjuntor geral” das interrupções.
enable_interrupts(INT_EXT);
Habilita a interrupção externa.
enable_interrupts(INT_RDA);
Habilita a interrupção por recepção de dado na UART.
sleep();
Coloca o dispositivo em modo sleep. Nesse estado o núcleo
pára de trabalhar e alguns periféricos são desabilitados,
diminuindo drasticamente o consumo de corrente. O
sistema é “despertado” com a ocorrência de uma

24
interrupção, que nesse caso é a externa. Nem todas as
fontes de interrupção podem acordar o processador, como,
por exemplo, o Timer0.
#INT_EXT
Diretiva que indica ao compilador que a próxima função é a
rotina de tratamento para a interrupção externa. No
instante em que a interrupção ocorre, o programa principal
(main()) pára onde estiver e a função “void trata_int_ext(void)”
é executada. Após o seu término, o programa principal volta
a ser executado a partir de onde parou.
#INT_RDA
Indica ao compilador que a próxima função é a rotina de
tratamento para a interrupção gerada pela recepção de um
dado.
putc(dado++);
Comando para enviar um caractere através do canal serial
criado. Se este canal usa o pino TX como saída, a variável
“dado” é simplesmente transferida para o TXREG
(registrador de transmissão), pois a UART se encarregará de
gerar o sinal correspondente. Para o programa, esta tarefa
consome poucos ciclos de relógio, bem como instruções.
Porém, se o canal serial utiliza outro pino como saída, o
programa é o responsável por gerar o sinal adequado.
Neste caso, o compilador insere no código (no momento da
compilação) uma função responsável por realizar tal tarefa.
Deve ficar claro que, durante a execução dessa função, o
processador não pode realizar outra tarefa ou ser
interrompido, pois o sinal de transmissão pode ser
comprometido.
dado=getc();
Transferência do dado recebido por um canal para uma
região na memória RAM (variável “dado”). Esta função deve
estar no início da ISR (Interrupt Service Routine) referente
ao “#INT_RDA”, ou após a validação do teste da flag “kbhit()”,
pois é quando um novo dado estará disponível. Esta flag foi
criada para indicar o instante em que um dado é recebido,
caso não se queira usar a interrupção “ #INT_RDA”, ou caso
tenha-se escolhido um canal por software (o qual não pode
gerar tal interrupção). Se o canal é por hardware (UART),
essa flag é “levantada” no instante em que o stopbit é
confirmado, indicando que o dado está disponível no
RXREG. No entanto, se a implementação é por software,
essa flag é setada assim que se identifica o início do

25
startbit. A partir daí a função “getc()” invoca a sub-rotina
(criada pelo compilador) responsável por receber o frame.
Em ambos os casos, a execução da função “ getc()” faz com
que a flag “kbhit()” seja zerada de forma automática,
preparando o sistema para uma nova recepção.

7º EXEMPLO: Timer 0
Os Timers são periféricos responsáveis pela contagem
de tempo, mas que também podem ser usados para contar
pulsos de um sinal externo. São muito úteis quando se
deseja um intervalo de tempo preciso entre eventos, pois
podem ser configurados pelo programa usuário para contar
uma certa quantidade de tempo (manipulada como número
de clocks). Podem realizar o papel da função “ delay_ms(X);”,
mas o uso de um timer permite que o programa trabalhe
em outras atividades enquanto o tempo está sendo
contado. Esse exemplo aborda apenas o Timer0, que é um
contador crescente e de oito bits (assume valores de 0 a
255). Porém, os outros timers presentes nesses dois PICs
funcionam a partir do mesmo mecanismo, com outras
poucas particularidades. Para utilizar o Timer0 o
programador deve inicialmente configurar a fonte de
contagem, a qual está associada a uma freqüência. Para
preparar o Timer0 para contar uma certa quantidade de
tempo deve-se obter, com uma simples “regra de três”,
quantos pulsos da fonte de contagem corresponde ao
intervalo de tempo desejado. Como a contagem é
crescente, retira-se de 256 o número de pulsos obtido, e o
resultado é carregado para o Timer0. A partir disto, o
programa habilita a contagem e fica livre para realizar
outras atividades, pois o Timer0 estará sendo
incrementado na freqüência da fonte de contagem. Quando
se atinge o numero 255, o próximo incremento faz com que
a contagem assuma o valor zero. Nesse instante o Timer0
acusa o estouro (overflow) ao programa, indicando que se
passou o tempo desejado. O programador pode ainda
habilitar a ocorrência de uma interrupção no momento do
estouro, fazendo com que seja executada uma função
específica nesse instante. Por fim, o programa usuário pode
também ler o valor atual do Timer0 para saber quanto
tempo se passou desde o início da contagem.

26
Figura 12 – Interface entre o programa usuário e Timer0
Por exemplo, se a fonte de contagem tem freqüência
de 1MHz e se deseja “agendar” um estouro para daqui a
150μs, deve-se carregar no Timer0 o número 106 (=256-
150), pois será contado de 106 até 256 (que corresponde
ao zero devido ao overflow). No entanto, se é necessário
que os estouros ocorram periodicamente com intervalos de
150μs, deve-se carregar o Timer0 a cada estouro. Senão,
após o primeiro estouro será contado de 0 a 256,
totalizando 256μs.
Normalmente usa-se a fonte de relógio interna para
contagem, pois não necessita de hardware adicional. E para
que se possa trabalhar com várias faixas de freqüências
existe o mecanismo de “prescale”. O valor atribuído a esse
parâmetro será usado como divisor do clock interno.
Portanto, se é desejado que o Timer0 seja incrementado
com a metade da freqüência interna, o “prescale” deve
configurado com o número 2. Pode-se usar os valores 2, 4,
8, 16, 32, 64, 128 ou 256.
Eis outro exemplo para esclarecer o funcionamento:
deseja-se obter eventos com intervalos de 2048μs. Sabe-se
que nos exemplos abordados neste documento usa-se
freqüência de 4MHz (Fosc), e que a freqüência interna
desses PICs (Fcy) são ¼ da Fosc, ou seja, 1MHz (vide
página 3). Portanto, a partir de Fcy, só é possível contar até
256μs. Para atingir o intervalo desejado pode-se utilizar um
contador para saber quantos estouros se passaram, ou
ajustar o prescale, o que é bem mais simples. Se esse for
configurado com o valor 8, o Timer0 será incrementado a
cada 8μs (Fcy/8), e, portanto, sua contagem pode ser de

27
até 2048μs, que é o intervalo desejado. Para este caso não
é necessário carregar nenhum valor ao Timer0, pois este
deve contar de 0 até 256 (que é o zero da próxima
contagem).

Figura 13 – Comportamento temporal do Timer0 para este sub-


exemplo
O primeiro exemplo deste documento (Pisca LED) pode
ser implementado com o uso do Timer0, sem a
necessidade do programa ter que trabalhar contando o
tempo, e com maior precisão. Isso ocorre no próximo
código, que faz um LED piscar a 0,5Hz.

//-------------------------- Pisca LED ----------------------------------

#include <16F876A.h> cont=0;


#fuses XT led=!led;
#use delay(clock=4000000) output_bit(pin_a0,led);
}
#use fast_io(a) }
#use fast_io(b)
#use fast_io(c) void main(void){
set_tris_a(0b11111110);
short led; set_tris_b(0b11111111);
unsigned int cont; set_tris_c(0b11111111);

#INT_TIMER0 setup_timer_0(RTCC_INTERNAL|
void trata_tmr0(void){ RTCC_DIV_64);
set_timer0(131+get_timer0()); set_timer0(131);
if(++cont==125){

28
enable_interrupts(GLOBAL| }
INT_TIMER0); }

while(true){

//---------------------------------------------------------------------//

Explanação:
Nesse programa o Timer0 é incrementado a cada 64μs e o
período entre estouros corresponde a 125 incrementos, ou
seja, 8ms. Um contador é usado para indicar que se
passaram 125 (coincidência) estouros desde a última
alteração da saída ligada ao LED. Detectada essa
ocorrência, a saída tem seu nível lógico alterado e o
contador é zerado para recomeçar uma nova contagem.
Portanto, o estado do LED muda a cada 64μs x 125 x 125 =
1 segundo.
#INT_TIMER0
Indica ao compilador que a próxima função é a rotina de
tratamento para a interrupção gerada pelo estouro do
Timer0.
set_timer0(131+get_timer0());
A cada estouro o programa carrega o Timer0 com o valor
131. Faz a contagem correspondente ao período de estouro
ser de 131 a 256, ou seja, 125. A função “ get_timer0()” serve
para aumentar a precisão da contagem, pois a função
“set_timer0(131+get_timer0());” não é executada exatamente
no momento do estouro. Existe um pequeno intervalo de
tempo entre o estouro e a execução da rotina de
tratamento, denominado “tempo de latência da
interrupção”. Supõe-se que esse pequeno intervalo
corresponde a 4 incrementos do Timer0. Se não houvesse
a função “get_timer0()”, o Timer0 seria carregado com o
valor 131, porém nesse instante já haveria se passado 4
incrementos desde o último estouro. Isso faria com que o os
estouros ocorressem a cada 129 incrementos, o que destoa
do cálculo realizado. O uso de “get_timer0()” implica que o
valor carregado seja 135 e a contagem até o próximo
estouro seja de 121 incrementos. Esse intervalo mais o
tempo de latência ocasionam 125 incrementos entre
estouros.
if(++cont==125){

29
Teste que indica se passaram 125 incrementos da variável
“conta” desde a última alteração da saída.
setup_timer_0(RTCC_INTERNAL|RTCC_DIV_64);
Configura o clock interno como fonte de contagem e o
prescaler com 64, além de habilitar o funcionamento do
Timer0. Entretanto, a interrupção só ocorrerá se for
habilitada, o que é feito na próxima função.
enable_interrupts(GLOBAL|INT_TIMER0);
Habilita a interrupção referente ao estouro do Timer0.

Figura 14 – Comportamento temporal do Timer0 e cont para esse


exemplo

30
Apêndice:
• Tabela ASCII:
Quando se utiliza a comunicação serial os caracteres
enviados entre dispositivos são codificados de acordo com a
tabela ASCII, enviando-se o valor binário correspondente ao
caractere desejado.

Figura 15 – Tabela de símbolos ASCII e seus respectivos valores em


decimal, octal e hexadecimal
• Pinagem PIC 16f628 e 16f876a
Podemos extrair inúmeras informações do dispositivo olhando
somente para a configuração de pinos.
 VDD/VSS
Pinos utilizados para a alimentação do dispositivo, sendo
VDD a alimentação positiva e VSS a alimentação negativa. Deve
ser verificado sempre a tensão a ser utilizada. Normamlemte
essa pode ser de 2.2 V a 5.5 V, porém alguns modelos não
podem ultrapassar um valor máximo de 3.6 V.
 VPP/MCLR
Pino ativação do chip. Este só estará ligado se a tensão nesse
pino for VDD.

31
 Ry#: Exemplo :
y: Port ao qual o pino está ligado. Para os dispositivos abordados
nesse material vemos os ports A, B e C
#: "Posição" do pino no port. Comumente vemos números de 0
a 7, formando os ports de 8 bits (0 a 7).
Os pinos com essa nomenclatura são utilizados como entradas
e/ou saídas digitais. Deve-se consultar quais destes podem ser
utilizados como entrada ou saída. Como exemplo o pino RA5 do
PIC 16f628 pode ser utilizado somente como entrada.
 RX/TX:
Pinos ligados à comunicação serial (RS232), diretamente ligados
ao periférico UART ou USART, que tira a carga da comunicação
da rotina principal. O pino RX é utilizado para o recebimento de
dados e o pino TX utilizado para enviar dados.
 AN#:
Pinos com essa nomeclatura são utilizados como canais de
conversão A/D, sendo # o número do canal.
 VREF+/VREF-
Estes pinos são utilizados para definir limeites superiores e
inferiores à conversão A/D, respectivamente. Se fizermos V REF+
= 3V e VREF-=1V, somente valores analógicos entre 1 e 3 V
serão considerados, e toda a resolucão será concentrada nesse
intervalo, de modo que para uma conversão de 8 bits, uma
leitura de 0 representará 1V e uma leitura de 255 representará
3V
 T#CKI/ T#OSO/ T#OSI:
Os pinos T#CKI e T#OSI são utilizados como base de clock para
o timer # do microcontrolador, sendo que para um mesmo
timer somente um deve ser utilizado. O T#OS0 é utilizado para
externar a base de clock do timer #.
 OSC1/OSC2
Pinos de entrada para o oscilador quando se utiliza um cristal.
 CLKIN/CLKOUT
Quando se utiliza um oscilador capacitivo ou uma onda
quadrada como base de tempo do PIC, deve-se conectar esse
sinal ao pino CLKIN. Este pode ser externado pelo pino CLKOUT.
 CMP#/CCP#
Pinos diretamente ligados ao periférico
captura/comparação/PWM, sendo sua função configurada por
software para gerar um sinal de frequência fixa e tempo
alto/baixo variáveis(PWM), guardar o valor de contagem do
timer associado com um estímulo externo(captura) e controlar o
pino associado quando o contador chegar ao valor
desejado(comparação).
 SS/SCK/SDI/SDO
Pinos associados à comunicação SPI, sendo SS (slave select) o
pino de seleção do dispositivo a transmitir, SCL o pino de clock

32
compartilhado, SDI e SDO os canais de recebimento e
transimssão, respectivamente.
 SDA/SCL

Pinos associados à comunicação I2C, sendo SCL o pino de clock


compartilhado e SDA o pino de comunicação bidirecional.
 CK/DT
Os pinos CK e DT são utilizados para comunicar-se pela USART
de forma síncrona, sendo CK o clock e DT o canal de dados.
 PGC/PGD/PGM
Pinos associados à gravação do chip. Em PGC deve ser
fornecido o clock, em PGD os dados a serem programados e
PGM é utilizado somente para gravação de baixa tensão.

 INT
Pino utilizado para a interrupção externa. Se houver uma variação
nesse pino a rotina de interrupção é engatilhada.

FIN.

33

Você também pode gostar