Escolar Documentos
Profissional Documentos
Cultura Documentos
Minas Gerais,
Brasil
rodrigomax @ unifei.edu.br
13 de Dezembro de 2013
i
2 Arquitetura de microcontroladores 36
2.1 Acesso à memória . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.2 Clock e tempo de instrução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.3 Esquema elétrico e circuitos importantes . . . . . . . . . . . . . . . . . . . . . . . 41
Multiplexação nos terminais do microcontrolador . . . . . . . . . . . . . . 42
2.4 Registros de conguração do microcontrolador . . . . . . . . . . . . . . . . . . . . 43
5 Anexos 110
5.1 cong.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
5.2 basico.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
5.3 Instalar gravadores/depuradores de PIC em sistemas x64 . . . . . . . . . . . . . . 113
ii
Lista de Figuras
1.1 Camadas de abstração de um sistema operacional . . . . . . . . . . . . . . . . . . 1
1.2 Pesquisa sobre linguagens utilizadas para projetos de software embarcado . . . . 2
1.3 Conguração das ferramentas de compilação . . . . . . . . . . . . . . . . . . . . . 4
1.4 Instalação do ICD2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5 Resumo das congurações do ICD2 no MPLAB . . . . . . . . . . . . . . . . . . . 6
1.6 Pedido de atualização do rmware do ICD2 . . . . . . . . . . . . . . . . . . . . . 6
1.7 Project Explorer do MPLAB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.8 Problema das Referências Circulares . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.9 Solução das referências circulares com #ifndef . . . . . . . . . . . . . . . . . . . . 14
1.10 Loop innito de um device driver gerando erro no sistema . . . . . . . . . . . . . 20
1.11 Exemplo de funcionamento do vetor de interrupção . . . . . . . . . . . . . . . . . 20
iii
3.26 Diagrama de blocos do LM35 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.27 Denição de faixa de valores para AD de 2 bits . . . . . . . . . . . . . . . . . . . 85
3.28 Conversor analógico digital de 2 bits . . . . . . . . . . . . . . . . . . . . . . . . . 85
3.29 Sinais PWM com variação do duty cycle . . . . . . . . . . . . . . . . . . . . . . . 88
iv
Lista de Tabelas
1.1 Softwares utilizados no curso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Ferramentas utilizadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Tipos de dados e faixa de valores . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4 Representação decimal - binária - hexadecimal . . . . . . . . . . . . . . . . . . . . 15
1.5 Alteração de tamanho e sinal dos tipos básicos . . . . . . . . . . . . . . . . . . . 16
1.6 Operação bit set com dene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.7 Operação bit clear com dene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.8 Operação bit ip com dene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.9 Operação bit test com dene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
v
Lista de Programas
1.1 Resumo do disp7seg.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2 Resumo do disp7seg.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3 Estrutura de header . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.4 Operações aritméticas com tipos diferentes . . . . . . . . . . . . . . . . . . . . . . 18
3.1 disp7seg.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.2 disp7seg.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.3 Utilizando a biblioteca disp7seg . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.4 teclado.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.5 teclado.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.6 Exemplo de uso da biblioteca teclado . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.7 lcd.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
3.8 lcd.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
3.9 Exemplo de uso da biblioteca de LCD . . . . . . . . . . . . . . . . . . . . . . . . 71
3.10 Write RTC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.11 Read RTC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.12 Rotinas de escrita e leitura no RTC . . . . . . . . . . . . . . . . . . . . . . . . . . 76
3.13 Rotinas de conversão BCD x inteiro . . . . . . . . . . . . . . . . . . . . . . . . . 77
3.14 serial.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.15 serial.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.16 Exemplo de uso da biblioteca de comunicação serial . . . . . . . . . . . . . . . . . 81
3.17 adc.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.18 adc.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.19 Exemplo de uso da biblioteca de conversores AD . . . . . . . . . . . . . . . . . . 87
3.20 pwm.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
3.21 pwm.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
3.22 Exemplo de uso da biblioteca das saídas PWM . . . . . . . . . . . . . . . . . . . 91
3.23 timer.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.24 timer.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.25 Exemplo de uso da biblioteca de um temporizador . . . . . . . . . . . . . . . . . 93
3.26 Reprodução de sons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
3.27 Fontes de Interrupção . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
3.28 Tratamento das interrupções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.29 Inicialização do sistema com interrupções . . . . . . . . . . . . . . . . . . . . . . 98
3.30 Inicialização do sistema com interrupções . . . . . . . . . . . . . . . . . . . . . . 99
4.1 Exemplo de arquitetura single-loop . . . . . . . . . . . . . . . . . . . . . . . . . . 101
4.2 Problema na sincronia de tempo para o single-loop . . . . . . . . . . . . . . . . . 101
4.3 Exemplo de sistema Interrupt-driven . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.4 Exemplo de sistema Interrupt-driven com base de tempo . . . . . . . . . . . . . . 103
4.5 Exemplo de cooperative multitasking . . . . . . . . . . . . . . . . . . . . . . . . . 105
4.6 Exemplo de cooperative multitasking com uso do top slot . . . . . . . . . . . . . 106
4.7 Exemplo de sistema Cooperative-multitasking com slot temporizado . . . . . . . 107
5.1 cong.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
5.2 basico.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
vi
Capítulo 1
Introdução
The real danger is not that computers will begin to think like men,
but that men will begin to think like computers. - Sydney J. Harris
Programação para sistemas embarcados exige uma série de cuidados especiais, pois estes sistemas
geralmente possuem restrições de memória e processamento. Por se tratar de sistemas com
funções especícas, as rotinas e técnicas de programação diferem daquelas usadas para projetos
de aplicativos para desktops.
Também é necessário conhecer mais a fundo o hardware que será utilizado, pois cada mi-
croprocessador possui uma arquitetura diferente, com quantidade e tipos de instruções diversos.
Programadores voltados para desktops não precisam se ater tanto a estes itens, pois eles pro-
gramam para um sistema operacional, que realiza o papel de tradutor, disponibilizando uma
interface comum, independente do hardware utilizado(Figura 1.1).
Aplicação
Sistema Operacional
Firmware
Hardware
1.1 Linguagem C
Neste curso será utilizada a linguagem C. Esta é uma linguagem com diversas características que
a tornam uma boa escolha para o desenvolvimento de software embarcado. Apesar de ser uma
linguagem de alto nível, permite ao programador um acesso direto aos dispositivos de hardware.
1
2 Introdução
Figura 1.2: Pesquisa sobre linguagens utilizadas para projetos de software embarcado
Fonte: http://www.embedded.com/design/218600142
Como o enfoque deste curso é a programação de sistemas embarcados e não a eletrônica, utili-
zaremos um kit de desenvolvimento pronto, baseado num microcontrolador PIC.
Como periféricos disponíveis temos:
Cada componente terá seu funcionamento básico explicado para permitir o desenvolvimento de
rotinas para estes.
First, solve the problem. Then, write the code. - John Johnson
Instalação
A Tabela 1.1 apresenta os softwares que serão utilizados no curso.
Todos os softwares são gratuitos e estão disponíveis na internet. Para correta instalação
deve-se instalar os softwares segundo a sequência apresentada na Tabela 1.1. Anote o diretório
onde cada software foi instalado.
Após a instalação dos softwares deve-se abrir o arquivo pic16devices.txt (de preferência no
wordpad) que foi instalado no diretório do SDCC dentro da pasta include\pic16 (por padrão
C:\Arquivos de programas\SDCC\include\pic16). No windows vista e windows 7 não é possível
editar arquivos de sistema. Neste caso clique no arquivo com o botão direito > Propriedades >
Segurança > Editar > Usuários e selecionar a opção Controle Total, depois clique em ok. Após
isso será possível editar o arquivo. Procure então a seguintes linhas:
name 18f4550
using 18f2455
Trocar a letra f minúscula da primeira linha, apenas do 18f4550, para um F maiúsculo:
name 18F4550
using 18f2455
Em seguida abra o programa MPLAB e vá ao menu Projects -> Set Language Tool Locati-
ons. Será apresentada uma tela similar a da Figura 1.3.
Selecione a ferramenta Small Device C Compiler for PIC16 (SDCC16). Expanda a opção
Executables. A ferramenta gpasm é obtida no diretório bin dentro de onde foi instalado
o GPUtils, por padrão: C:\Arquivos de programas\gputils\bin. Para as opções sdcc16 e sdcc
link deve-se escolher o arquivo sdcc.exe, que é encontrado no diretório bin dentro do diretório
onde foi instalado o SDCC por padrão: C:\Arquivos de programas\SDCC\bin\. Clicar em
OK. A Tabela 1.2 apresenta um resumo destas opções.
Após estes passos a suíte MPLAB está pronta para trabalhar com o compilador SDCC+GPUtils.
No wizard, escolha a comunicação como USB e depois diga que a placa possui alimentação
independente Target has own power supply. Deixe as outras opções na seleção padrão. Antes
de clicar em concluir verique ao nal se o resumo se parece com o da Figura 1.5.
Na primeira vez que o computador se conectar ao ICD2 é possível que o MPLAB precise
atualizar o rmware do ICD2 conforme o aviso que pode ser visto na Figura 1.6.
Após estes passos o projeto estará criado. Caso a lista de arquivos do projeto não esteja
visível vá ao menu View -> Project.
Para a criação de um novo arquivo vá até o menu File -> New. Neste novo arquivo digite
alguma coisa e salve-o. Caso seja o arquivo que conterá a função principal (main) é costume
salvá-lo com o nome de main.c.
A cada novo arquivo criado é necessário inseri-lo no projeto. Para isso deve-se clicar na pasta
correspondente ao tipo de arquivo que se deseja incluir e em seguida Add Files como pode ser
visualizado na Figura 1.7.
A programação para sistemas embarcados possui diversas características diferentes da progra-
mação voltada para desktop. Do mesmo modo, existem alguns conceitos que geralmente não são
explorados nos cursos de linguagens de programação em C, mas que são essenciais para o bom
desenvolvimento deste curso. Estes conceitos serão explanados neste capítulo.
for
} }
(i = 0; i < 1000; i++) ; for (i = 0; i < 1000; i++) ;
} }
} }
Podemos notar pelo código anterior que aquele que possui indentação facilita na vericação
de quais instruções/rotinas estão subordinadas às demais.
Outra característica de padronização está na criação de nomes de funções e de variáveis. Pela
linguagem C uma função ou variável pode ter qualquer nome desde que: seja iniciada por uma
letra, maiúscula ou minúscula, e os demais caracteres sejam letras, números ou underscore _.
A linguagem C permite também que sejam declaradas duas variáveis com mesmo nome caso
possuam letras diferentes apenas quanto caixa (maiúscula ou minúscula). Por exemplo: var e
vAr são variáveis distintas, o que pode gerar erro no desenvolvimento do programa, causando
dúvidas e erros de digitação.
Por isso convenciona-se que os nomes de variáveis sejam escritos apenas em minúsculas.
Quando o nome é composto, se utiliza uma maiúscula para diferenciá-los como, por exemplo, as
variáveis contPos e contTotal.
Nomes de função serão escritos com a primeira letra maiúscula e no caso de nome composto,
cada inicial será grafada em maiúsculo: InicializaTeclado(), ParaSistema().
Tags de denições (utilizados em conjunto com a diretiva #dene) serão grafados exclusiva-
mente em maiúsculo: NUMERODEVOLTAS, CONSTGRAVITACIONAL.
Cada chave será colocada numa única linha, conforme exemplo anterior, evitando-se constru-
ções do tipo:
Ou
if ( PORTA == 0 x30 ) {
PORTB = 0 x10 ; }
As regras apresentadas visam fornecer uma identidade visual ao código. Tais regras não são
absolutas, servem apenas para o contexto desta apostila. Em geral, cada instituição ou projeto
possui seu próprio conjunto de normas. É importante ter conhecimento deste conjunto e aplicá-lo
em seu código.
O estilo adotado nesta apostila é conhecido também como estilo Allman, bsd (no emacs)
ou ANSI, já que todos os documentos do padrão ANSI C utilizam este estilo. Apesar disto o
padrão ANSI C não especica um estilo para ser usado.
1.5 Comentários
If the code and the comments disagree, then both are probably
wrong. - Norm Schryer
Comentários são textos que introduzimos no meio do programa fonte com a intenção de torná-
lo mais claro. É uma boa prática em programação inserir comentários no meio dos nossos
programas. Pode-se comentar apenas uma linha usando o símbolo // (duas barras). Para
comentar mais de uma linha usa-se o símbolo /* (barra e asterisco) antes do comentário e */
(asterisco e barra) para indicar o nal do comentário.
#include
#define
< s t d i o . h>
1.6 Arquivos .c e .h
Na programação em linguagem C utilizamos dois tipos de arquivos com funções distintas. Toda
implementação de código é feita no arquivo com extensão .c (code ). É nele que criamos as
funções, denimos as variáveis e realizamos a programação do código. Se existem dois arquivos
.c no projeto e queremos que um deles possa usar as funções do outro arquivo, é necessário
realizar um #include.
Os arquivos .h (header ) tem como função ser um espelho dos arquivos .c disponibilizando
as funções de um arquivo .c para serem utilizadas em outros arquivos. Nele colocamos todos
os protótipos das funções que queremos que os outros arquivos usem.
Se quisermos que uma função só possa ser utilizada dentro do próprio arquivo, por motivo
de segurança ou organização, basta declarar seu protótipo APENAS no arquivo .c.
Se for necessário que um arquivo leia e/ou grave numa variável de outro arquivo é recomen-
dado criar funções especícas para tal nalidade.
O programa 1.1 apresenta um exemplo de um arquivo de código .c e o programa 1.2 apre-
senta o respectivo arquivo de header .h.
Podemos notar que no arquivo .h a função AtualizaDisplay() não está presente, deste modo
ela não estará disponível para os outros arquivos. Podemos notar também que para ler ou
gravar a variável digito é necessário utilizar as funções MudaDigito() e LerDigito(). Notar que
não existe acesso direto às variáveis. Este tipo de abordagem insere atrasos no processamento
devido a um efeito conhecido como overhead de funções, podendo inclusive causar travamentos
no sistema caso não exista espaço suciente no stack.
As diretivas de compilação são instruções que são dadas ao compilador. Elas não serão executa-
das. Todas as diretivas de compilação começam com um sinal #, conhecido como jogo da velha
ou hash.
#include
A diretiva de compilação #include é a responsável por permitir que o programador utilize no seu
código funções que foram implementadas em outros arquivos, seja por ele próprio ou por outras
pessoas. Não é necessário possuir o código fonte das funções que se deseja utilizar. É necessário
apenas de um arquivo que indique os protótipos das funções (como elas devem ser chamadas) e
possuir a função disponível em sua forma compilada.
Em geral um arquivo que possui apenas protótipos de funções é denominado de Header e
possui a extensão .h.
#dene
Outra diretiva muito conhecida é a #dene. Geralmente é utilizada para denir uma constante,
mas pode ser utilizada para que o código fonte seja modicado antes de ser compilado.
void MostraSaidaPadrao ( )
{
#include
#define
< s t d i o . h>
#ifdef PADRAO Serial
char * msg = " SERIAL " ;
void main void
PADRAO S e r i a l
#else ( )
SERIAL
char * msg = " LCD " ;
{
MostraSaidaPadrao ( ) ;
#endif
}
printf ( msg ) ;
}
#include
#define
< s t d i o . h>
Pelo código apresentado percebemos que a mesma função MostraSaidaPadrao(), apresenta re-
sultados diferentes dependendo de como foi denida a opção PADRAO.
Os dene's também ajudam a facilitar a localização dos dispositivos e ajustar as congurações
no microcontrolador. Todo periférico possui um ou mais endereços para os quais ele responde.
Estes endereços podem variar inclusive dentro de uma mesma família. Por exemplo: o endereço
da porta D (onde estão ligados os leds) é 0xF83. Para ligar ou desligar um led é preciso alterar
o valor que esta dentro do endereço 0xF83. Para facilitar este procedimento, é denido um
ponteiro para este endereço e rotulado com o nome PORTD. Denir OFF como 0 e ON como 1
facilita a leitura do código.
Toda vez que a função LerTemperatura() for chamada, ela deve fazer um teste e se o valor for
maior que um patamar chamar a função EnviaSerial() com o código 0x30. Para isso o arquivo
temp.h deve incluir o arquivo serial.h.
Toda vez que a função LerSerial() receber um valor, ela deve chamar a função AjustaCalor()
e repassar esse valor. Para isso o arquivo serial.h deve incluir o arquivo temp.h
5 #endif //TAG_CONTROLE
O problema é que deste modo é criada uma referência circular sem m: o compilador lê o
arquivo serial.h e percebe que tem que inserir o arquivo temp.h. Inserindo o arquivo temp.h
percebe que tem que inserir o arquivo serial.h, conforme pode ser visto na Figura 1.8.
temp.h
#include “serial.h”
char LerTemperatura(void);
void AjustaCalor(char val); serial.h
#include “temp.h”
char LerSerial(void);
void EnviaSerial(char val);
temp.h
#include “serial.h”
char LerTemperatura(void);
void AjustaCalor(char val);
A solução é criar um dispositivo que permita que o conteúdo do arquivo seja lido apenas uma
vez. Este dispositivo é implementado através da estrutura apresentada no programa 1.3.
Segundo o código acima, o conteúdo que estiver entre o #ifndef e o #endif, só será mantido
se a tag TAG_CONTROLE NÃO estiver denida. Como isto é verdade durante a primeira
leitura, o pré-compilador lê o arquivo normalmente. Se acontecer uma referência cíclica, na
segunda vez que o arquivo for lido, a tag TAG_CONTROLE já estará denida impedindo
assim que o processo cíclico continue, conforme pode ser visto na Figura 1.9.
Geralmente se utiliza como tag de controle o nome do arquivo. Esta tag deve ser única para
cada arquivo.
19 Jan 2038 at 3:14:07 AM. The end of the world according to Unix
(2
32
seconds after Jan 1st 1970) - Unix date system
O tipo de uma variável informa a quantidade de memória, em bytes, que esta irá ocupar e como
esta deve ser interpretada: com ou sem fração (vírgula). Os tipos básicos de dados na linguagem
temp.h
#ifndef TEMP_H
#define TEMP_H
#include “serial.h”
char LerTemperatura(void);
void AjustaCalor(char val); serial.h
#endif
#ifndef SERIAL_H
#define SERIAL_H
#include “temp.h”
//tag já definida,
//pula o conteúdo
#endif
Podemos notar que as variáveis que possuem maior tamanho podem armazenar valores mai-
ores. Notamos também que apenas os tipos oat e double possuem casas decimais.
5. Apresentar o resultado
0 0000 0 8 1000 8
1 0001 1 9 1001 9
2 0010 2 10 1010 A
3 0011 3 11 1011 B
4 0100 4 12 1100 C
5 0101 5 13 1101 D
6 0110 6 14 1110 E
7 0111 7 15 1111 F
Na linguagem C, por padrão, os tipos são sinalizados, ou seja, possuem parte positiva e
negativa. Por isso é raro encontrar o modicador signed.
Modicadores de acesso
Durante o processo de compilação existe uma etapa de otimização do programa. Durante esta
etapa, o compilador pode retirar partes do código ou desfazer loops com períodos xos. Por
exemplo o código abaixo:
RETURN
Enquanto a variável x for diferente de x o programa não sai do loop. O compilador
entende que esta condição nunca irá acontecer e elimina o loop do código nal. Como é possível
ver no código gerado, a rotina de return está logo após a inicialização do programa _main. Para
variáveis comuns o valor só é alterado em atribuições diretas de valor ou de outras variáveis: (x
= 4;) ou (x = y;).
Entretanto existe uma condição onde a variável x pode alterar seu valor independentemente
do programa. Se esta variável representar um endereço de memória associado a um periférico
físico, seu valor pode mudar independentemente do uxo do programa. Para indicar esta situação
ao programa utilizamos a palavra reservada volatile.
Podemos perceber que, deste modo, o compilador é forçado a ler a variável x duas vezes e realizar
o teste para ver se ela permanece com o mesmo valor.
Em algumas situações é necessário indicar que algumas variáveis não podem receber valores
pelo programa. Para isto utilizamos a palavra reservada const. Utilizamos este modicador
para indicar que a variável representa um local que apenas pode ser lido e não modicado, por
exemplo uma porta para entrada de dados. Nesta situação é comum utilizar as palavras volatile
e const junto.
Modicadores de posicionamento
As variáveis podem ser declaradas utilizando os modicadores near e far. Estes modicadores
indicam ao compilador em qual região de memória devem ser colocadas as variáveis.
A região near geralmente se refere à zero page. É uma região mais fácil de ser acessada. A
região far exige mais tempo para executar a mesma função que a near.
Podemos pensar nestas regiões como a memória RAM e a memória Cache do computador.
A segunda é mais rápida, mas possui um alto custo e por isso geralmente é menor. Em algumas
situações é interessante que algumas variáveis nunca saiam do cache, pois são utilizadas com
grande frequência ou são críticas para o sistema.
Modicador de persistência
Em geral, as variáveis utilizadas dentro das funções perdem seu valor ao término da função. Para
que este valor não se perca podemos utilizar um modicador de persistência: static. Com esse
modicador a variável passa a possuir um endereço xo de memória dado pelo compilador. Além
disso o compilador não reutiliza este endereço em nenhuma outra parte do código, garantindo
que na próxima vez que a função for chamada o valor continue o mesmo.
A soma de dois char, conforme a linha 9, segundo caso pode gerar um problema se ambos
forem muito próximo do valor limite. Por exemplo: 100 + 100 = 200, que não cabe num char,
já que este só permite armazenar valores de -128 à 127.
O terceiro caso (linha 10) está correto, a multiplicação de dois char possui um valor máximo
de 127*127=16.129. O problema é que a multiplicação de dois char gera um outro char, perdendo
informação. É necessário realizar um typecast antes.
O quarto caso (linha 11) pode apresentar um problema de precisão. A divisão de dois inteiros
não armazena parte fracionária. Se isto não for crítico para o sistema está correto. Lembrar que
a divisão de números inteiros é mais rápida que de números fracionários.
O quinto caso (linha 12) pode apresentar um problema de precisão. O resultado da conta de
um número inteiro com um ponto utuante é um ponto utuante. Armazenar esse valor num
outro número inteiro gera perda de informação.
O sexto caso (linha 13) apresenta um problema muito comum. A divisão de dois números
inteiros gera um número inteiro. Não importa se armazenaremos o valor numa variável de ponto
utuante haverá perda de informação pois os operandos são inteiros. Para evitar esse problema
é necessário um typecast.
No sétimo caso (linha 14) pode haver perda de precisão pois o resultado da operação é um
double, e estamos armazenando este valor num oat.
O oitavo caso (linha 15) é similar ao sexto. Estamos realizando uma conta com dois números
inteiros esperando que o resultado seja 0,5. Como os operandos são inteiros a expressão será
avaliada como resultante em Zero. Uma boa prática é sempre usar .0 ou f após o número
para indicar operações com vírgula.
Devemos tomar cuidado também com comparações envolvendo números com ponto utuante.
float x
while
= 0.1;
(x != 1.1) {
printf ( "x = %f\n" , x ) ;
x = x + 0.1;
}
O trecho de código acima apresenta um loop innito. Como existem restrições de precisão nos
números de ponto utuante (oat e double) nem todos os números são representados elmente.
Os erros de arredondamento podem fazer com que a condição (x !=1.1) nunca seja satisfeita.
Sempre que houver a necessidade de comparação com números de ponto utuante utilizar maior,
menor ou variações.
float x
while
= 0.1;
(x < 1.1) {
printf ( "x = %f\n" , x ) ;
x = x + 0.1;
}
Apesar de sutis estes tipos de erro podem causar um mau funcionamento do sistema. Na
Figura 1.10 é apresentado um erro gerado através de um loop innito.
Todo sistema necessita de iniciar em algum lugar. Em geral, os microcontroladores, assim que
ligados, procuram por suas instruções no primeiro ou último endereço de memória, dependendo
da arquitetura utilizada. O espaço de memória disponível neste endereço é geralmente muito
pequeno, apenas o necessário para inserir uma instrução de pulo e o endereço onde está a função
principal. Este espaço é conhecido como posição de reset. Existem ainda outros espaços de
memória similares a este que, geralmente, são alocados próximos. O conjunto destes espaços é
conhecido como vetor de interrupção (Figura 1.11).
Endereço Instrução
0x00 Pulo
0x01 0x8A
0x02 Pulo
0x03 0x55
0x04 ...
0x55 Limpa A
0x56 A recebe
0x57 30
0x58 Testa A
0x59 ...
0x8A A recebe
0x8B 50
0x8C Salva em
0x8D Porta B
0x8E ...
compiladores alocam a função main() em algum lugar da memória onde haja espaço disponível.
Depois disso dispõem de uma instrução de pulo para o primeiro endereço de memória, onde foi
alocada a função main.
Outra coisa interessante é que para sistemas embarcados a função principal não recebe nem
retorna nada. Como ela é a primeira a ser chamada não há como enviar algum valor por parâ-
metro. Ela também não retorna nada pois ao término desta o sistema não está mais operativo.
Em geral sistemas embarcados são projetados para começarem a funcionar assim que ligados e
apenas parar sua tarefa quando desligados. Como todas as funcionalidades são chamadas dentro
da função main()1 espera-se que o programa continue executando as instruções dentro dela até
ser desligado ou receber um comando para desligar. Este comportamento pode ser obtido através
de um loop innito. Abaixo estão as duas alternativas mais utilizadas.
É muito comum necessitar que o microcontrolador que um tempo sem fazer nada. Uma maneira
de atingir esse objetivo é utilizar um laço FOR 2 .
unsigned char i;
for i i ( =0; < 10; i++) ;
Notar que não estamos utilizando os colchetes. Logo após fechar os parênteses já existe um
ponto e vírgula. Para entender como esse procedimento funciona, e estimar o tempo de espera é
preciso entender como o compilador traduz essa função para assembler.
1
Em sistemas mais complexos algumas tarefas são executadas independentemente da função principal, tendo
sua execução controlada através de interrupções.
2
Este método não é aconselhado em sistemas de maior porte.
Percebemos pelo código acima que para realizar um for precisamos de 3 passos de inicialização.
Cada iteração exige 2 passos: uma comparação e um pulo 3 , totalizando 3 ciclos de inicialização
e 3 ciclos de interação.
Se temos um processador trabalhando a 8 MHz, cada instrução é executada em 0.5µs.4 Para
termos um tempo de espera de 0.5s precisamos de 1 milhão de instruções. Se colocarmos loops
encadeados podemos multiplicar a quantidade de instruções que serão executadas. Para obtermos
um valor de 1 milhão de instruções devemos utilizar pelo menos 3 loops encadeados. Os valores
dos loops são obtidos de maneira iterativa.
unsigned char i j, k;
for i i
,
( =0; < 34; i++) // 3 + 34 * (30.003 + 3) = 1.020.207 instruções
{
for j j ( =0; < 100; j++) // 3 + 1 0 0 * (297 + 3) = 30.003 instruções
{
for k k ( =0; < 98; k++) ; // 3 + 98 * (3) = 297 instruções
}
}
O código acima foi projetado para gerar um atraso de tempo de meio segundo. Compilando
e realizando testes práticos podemos conrmar que o tempo real é aproximadamente 0.51 (s).
Esta discrepância acontece porque agora temos 3 loops encadeados e cada qual com sua variável
de controle. Deste modo o compilador precisa salvar e carregar cada variável para realizar a
comparação.
Percebemos assim que para conhecer corretamente o funcionamento do sistema é necessário,
em algumas situações, abrir o código em assembler gerado pelo compilador para entender como
este é executado. Nem sempre o compilador toma as mesmas decisões que nós. Além disso ele
pode gerar otimizações no código. Existem dois tipos de otimização: uma visando diminuir o
tempo de execução do sistema, deixando-o mais rápido e outra que reduz o tamanho do código
nal, poupando espaço na memória.
A seguir apresentamos um exemplo de função que gera delays com tempo parametrizado.
Nos sistemas microcontrolados, existem algumas variáveis onde cada bit tem uma interpretação
3
Este valor só é válido quando estamos trabalhando com variáveis char. Se utilizarmos variáveis int o código
em assembler será diferente e teremos que realizar uma nova análise.
4
Para 8MHz, 1 ciclo = 0.125µs. No PIC, cada instrução precisa de 4 ciclos de clock, portanto 0.5µs.
ou funcionalidade diferente. Por isso é necessário realizar algumas operações que modiquem
apenas os bits desejados, mantendo o restante dos bits da variável inalterados.
As operações da linguagem C que nos permitem trabalhar com as variáveis, levando em conta
os valores individuais de cada bit, são chamadas de bitwise operation.
É importante ressaltar que as operações de bitwise possuem funcionalidade semelhante a suas
respectivas operações lógicas. A diferença é que a lógica opera em cima da variável como um
todo5 enquanto a bitwise opera bit à bit.
NOT
A operação NOT lógica retorna '1' (um) se o valor for '0' (zero) e '0' se o valor for '1'.
A !A
0 1
1 0
A operação bitwise NOT (operador ) executa uma NOT lógica. Isso signica que a operação é
realizada para cada um dos bits da variável, não mais para a variável como um todo. Na tabela
seguinte é apresentada a diferença entre as duas operações.
result = ~A ;
char A = 12; result = ! A; // result = 243
// A = 0 b 0 0 0 0 1 1 0 0 // result = 0 // A = 0 b 0 0 0 0 1 1 0 0
// r = 0 b11110011
AND
A operação AND lógica (operador &&) retorna 0 se algum dos valores for zero, e 1 se os dois
valores forem diferentes de zero.
A B A&&B
0 0 0
0 1 0
1 0 0
1 1 1
A operação bitwise AND (operador &) executa uma AND lógica para cada par de bits e coloca
o resultado na posição correspondente:
char A = 8;
result
//
=
result
A
= 0
& B;
result A B;
char
// A = 0 b 0 0 0 0 1 0 0 0 = &&
// A = 0 b 0 0 0 0 1 0 0 0
B = 5; // result = 1
// B = 0 b 0 0 0 0 0 1 0 1
// B = 0 b 0 0 0 0 0 1 0 1
// r = 0 b00000000
5
Lembrar que para linguagem C uma variável com valor 0 (zero) representa falso, e qualquer outro valor
representa verdadeiro.
OR
A operação OR lógica (operador ||) retorna 1 se algum dos valores for diferente de zero, e 0 se
os dois valores forem zero.
A B A||B
0 0 0
0 1 1
1 0 1
1 1 1
A operação bitwise OR (operador |) executa uma OR lógica para cada par de bits e coloca o
resultado na posição correspondente:
char A = 8;
result
// result
= A
= 13
| B;
result A B;
char
// A = 0 b 0 0 0 0 1 0 0 0 = ||
// A = 0 b 0 0 0 0 1 0 0 0
B = 5; // result = 1
// B = 0 b 0 0 0 0 0 1 0 1
// B = 0 b 0 0 0 0 0 1 0 1
// r = 0 b00001101
XOR
A operação XOR não possui correspondente lógica na linguagem C. Esta operação pode ser
representada como A XOR B = (A && !B)||(!A && B)
A B A⊕B
0 0 0
0 1 1
1 0 1
1 1 0
A operação bitwise XOR (operador ) executa uma XOR lógica para cada par de bits e coloca
o resultado na posição correspondente:
char A = 8;
result
// result
= A
= 13
^ B;
char
// A = 0 b 0 0 0 0 1 0 0 0
// não existe em C // A = 0 b 0 0 0 0 1 0 0 0
B = 5;
// B = 0 b 0 0 0 0 0 1 0 1
// B = 0 b 0 0 0 0 0 1 0 1
// r = 0 b00001101
Shift
A operação shift desloca os bits para a esquerda (operador <<) ou direita (operador >>). É
necessário indicar quantas casas serão deslocadas.
result A result A
char
= << 2; = >> 2;
A = 8; // result = 32 // result = 2
// A = 0 b 0 0 0 0 1 0 0 0 // A = 0 b 0 0 0 0 1 0 0 0 // A = 0 b 0 0 0 0 1 0 0 0
// r = 0 b00100000 // r = 0 b00000010
Para variáveis unsigned e inteiras, esta operação funciona como a multiplicação/divisão por
potência de dois. Cada shift multiplica/divide por 2 o valor. Esta é uma prática muito comum
para evitar a divisão que na maioria dos sistemas embarcados é uma operação cara do ponto de
vista de tempo de processamento.
Não utilizar esta operação com o intuito de multiplicar/dividir variáveis com ponto xo ou
utuante nem variáveis sinalizadas (signed).
Em diversas ocasiões é necessário que trabalhemos com os bits de maneira individual, prin-
cipalmente quando estes bits representam saídas ou entradas digitais, por exemplo chaves ou
leds.
Suponha, por exemplo, que um sistema possua 8 leds ligados ao microcontrolador. Cada led
é representado através de 1 bit de uma variável. Para ligarmos ou desligarmos apenas um led por
vez, não alterando o valor dos demais, devemos nos utilizar de alguns passos de álgebra digital.
Se a operação OR for executada com a máscara criada, o resultado apresentará valor 1 na posição
X e manterá os valores antigos para as demais posições. Exemplo: Ligar apenas o bit 2 da variável
PORTD
for
// mantém o sistema ligado indefinidamente
(;;) ;
}
Se a operação AND for executada com a máscara criada, o resultado apresentará valor 0 na
posição X e manterá os valores antigos para as demais posições. Exemplo: Desligar apenas o bit
2 da variável PORTD.
for
// mantém o sistema ligado indefinidamente
(;;) ;
}
É importante notar que geramos a máscara de maneira idêntica àquela utilizada no caso
anterior, onde todos os valores são zero e apenas o desejado é um. Depois realizamos a inversão
dos valores. Este procedimento é realizado desta maneira porque não sabemos o tamanho da
palavra a ser utilizada no microcontrolador: 8 ou 16 bits. Mesmo assim devemos garantir que
todos os bits obtenham o valor correto, o que é garantido pela operação de negação. A opção de
inicializar a variável com apenas um zero e rotacionar pode não funcionar pois, na maioria dos
sistemas, a função de rotação insere zeros à medida que os bits são deslocados e precisamos que
apenas um valor seja zero.
Se a operação XOR for executada com a máscara criada, o valor na posição X será trocado, de
zero para um ou de um para zero. Exemplo: Trocar o bit 2 e 6 da variável PORTD
for
// mantém o sistema ligado indefinidamente
(;;) ;
}
Realizamos então uma operação AND com a variável. O resultado será zero se o bit X, da
variável original, for zero. Se o bit da variável original for 1 a resposta será diferente de 06 .
Exemplo: Testar o bit 2 da variável PORTD
if
// V e r i f i c a apenas o bit 2
( teste & mascara )
{
PORTD = 0 x00 ; // s e o resultado for verdadeiro liga todos os leds
6
A maioria dos compiladores C adotam uma variável com valor diferente de zero como sendo verdadeiro.
}
else
{
PORTD = 0 xff ; // s e o resultado for falso desliga todos os leds
}
for
// mantém o sistema ligado indefinidamente
(;;) ;
}
char bit = 2 ;
char mascara ;
mascara = 1 << bit ;
arg = arg | mascara ;
Passo a Passo //em 1 linha
arg = arg | (1<< bit ) ;
// ou
arg |= (1<< bit ) ;
// L i g a n d o o bit 2 da porta D
PORTD = PORTD | (1<<2) ;
Exemplo de uso // ou
PORTD |= (1<<2) ;
char bit = 2 ;
char mascara ;
mascara = 1 << bit ;
arg = arg & ~ mascara ;
Passo a Passo //em 1 linha
arg = arg & ~(1<< bit ) ;
// ou
arg &= ~(1<< bit ) ;
// D e s l i g a n d o o bit 2 da porta D
PORTD = PORTD & ~(1<<2) ;
Exemplo de uso // ou
PORTD &= ~(1<<2) ;
char bit = 2 ;
char mascara ;
mascara = 1 << bit ;
arg = arg ^ mascara ;
Passo a Passo //em 1 linha
arg = arg ^ (1<< bit ) ;
// ou
arg ^= (1<< bit ) ;
char bit
char mascara
= 2;
;
mascara bit
if arg mascara
= 1 << ;
Passo a Passo ( & )
if arg
//em 1 linha
( bit& (1<< ))
if
// T e s t a n d o o bit 2 da porta D
( PORTD | (1<<2) )
Exemplo de uso {
// . . .
}
if
// T e s t a n d o o bit 2 da porta D
( BitTst ( PORTD , 2 ) )
Exemplo de uso com de- {
ne // . . .
}
In the beginner's mind there are many possibilities; in the expert's
mind there are few. - Shunryu Suzuki
A vericação de sistemas embarcados apresenta algumas restrições e de modo geral não é possível
inferir sobre a operação do sistema sem paralisá-lo. Como este tipo de sistema possui vários
dispositivos agregados, que funcionam independentemente do processador, é necessário utilizar
abordagens diferentes para realizar o debug.
Devemos lembrar que além do software devemos levar em conta possíveis problemas advindos
do hardware. Debounce, tempo de chaveamento, limite do barramento de comunicação são
exemplos de pontos a serem considerados no momento de depuração.
Externalizar as informações
A primeira necessidade é conhecer o que está acontecendo em teu sistema. Na programação
tradicional para desktop é comum utilizarmos de mensagens no console avisando o estado do
programa.
Devemos ter em mente onde é necessário colocar estes alertas e lembrar de retirá-los do código
nal.
Para a placa em questão utilizaremos o barramento de leds que está ligado à porta D. A ope-
ração deste dispositivo será estudada posteriormente em detalhes. Por enquanto basta sabermos
que cada bit da variável PORTD está ligada a um led diferente. Por causa da construção física
da placa, o led é aceso com valor 0 (zero) e desligado com o valor 1 (um). Além disso temos que
congurar a porta D. Isto é feito iniciando a variável TRISD com o valor 0x008 .
for
// mantém o sistema ligado indefinidamente
(;;) ;
7
Mais informações sobre debug de sistemas embarcados referir ao artigo The ten secrets of embedded debug-
ging de Stan Schneider e Lori Fraleigh
8
As variáveis PORTD e TRISD são denidas como unsigned char e possuem portanto 8 bits.
Devemos utilizar os leds como sinais de aviso para entendermos o funcionamento do programa.
Isto pode ser feito através das seguintes ideias: Se passar desta parte liga o led X, Se entrar
no IF liga o led Y, se não entrar liga o led Z, Assim que sair do loop liga o led W.
Programação incremental
Ao invés de escrever todo o código e tentar compilar, é interessante realizar testes incrementais.
A cada alteração no código realizar um novo teste. Evitar alterar o código em muitos lugares
simultaneamente, no caso de aparecer um erro ca mais difícil saber onde ele está.
Otimização de código
Apenas se preocupe com otimização se estiver tendo problemas com o cumprimento de tarefas.
Mesmo assim considere em migrar para uma plataforma mais poderosa. Sistemas embarcados
preconizam segurança e não velocidade.
Caso seja necessário otimizar o código analise antes o local de realizar a otimização. Não
adianta otimizar uma função grande se ela é chamada apenas uma vez. Utilize-se de ferramentas
do tipo proler sempre que possível. Isto evita a perda de tempo e auxilia o programador a
visualizar a real necessidade de otimização de código.
if
// aqui tem um monte de código . . .
( PORTB >= 5 ) //PORTB não deveria ser um valor maior que 5.
{
BitClr ( PORTD , 3 )
for
; // l i g a o led 3
(;;) ; // t r a v a o programa
}
// aqui continua com um monte de código . . .
Writing in C or C++ is like running a chain saw with all the safety
guards removed. - Bob Gray
Toda variável criada é armazenada em algum lugar da memória. Este lugar é denido de maneira
única através de um endereço.
Para conhecermos o endereço de uma variável podemos utilizar o operador &. Cuidado! Este
operador também é utilizado para realização da operação bitwise AND. Exemplo:
int
// d e c i d i d o pelo compilador
a = 0;
a = a + 1;
printf ( a ); // i m p r i m e o valor 1
printf ( &a ); // i m p r i m e o endereço de a ( por exemplo 157821)
Conhecer o endereço de uma variável é muito útil quando queremos criar um ponteiro para
ela.
Ponteiro é uma variável que, ao invés de armazenar valores, armazena endereços de memória.
Através do ponteiro é possível manipular o que está dentro do lugar apontado por ele.
Para denir um ponteiro também precisamos indicar ao compilador um tipo. A diferença é
que o tipo indica quanto cabe no local apontado pelo ponteiro e não o próprio ponteiro.
Sintaxe:
Exemplo:
int * apint
float * apfloat
;
;
Deve-se tomar cuidado, pois nos exemplos acima, apint e apoat são variáveis que armazenam
endereços de memória e não valores tipo int ou oat. O lugar APONTADO pela variável apint é
que armazena um inteiro, do mesmo modo que o lugar apontado por apoat armazena um valor
fracionário.
Se quisermos manipular o valor do endereço utilizaremos apint e apoat mas se quisermos
manipular o valor que esta dentro deste endereço devemos usar um asterisco antes do nome da
variável. Exemplo:
apfloat = 3 . 2 ;
* apfloat = 3 . 2 ;
A primeira instrução indica ao compilador que queremos que o ponteiro apoat aponte para
o endereço de memória número 3.2, que não existe, gerando um erro. Se quisermos guardar o
valor 3.2 no endereço de memória apontado por apoat devemos utilizar a segunda expressão.
Para trabalhar com ponteiros é preciso muito cuidado. Ao ser denido, um ponteiro tem como
conteúdo não um endereço, mas algo indenido. Se tentarmos usar o ponteiro assim mesmo,
corremos o risco de que o conteúdo do ponteiro seja interpretado como o endereço de algum local
da memória vital para outro programa ou até mesmo para o funcionamento da máquina. Neste
caso podemos provocar danos no programa, nos dados, ou mesmo travar a máquina.
É necessário tomar cuidado ao inicializar os ponteiros. O valor atribuído a eles deve ser
realmente um endereço disponível na memória.
Por exemplo, podemos criar um ponteiro que aponta para o endereço de uma variável já
denida:
// d e f i n i n d o a variável ivar
int ivar ;
int
// d e f i n i n d o o ponteiro iptr
* iptr ;
// o ponteiro iptr recebe o valor do endereço da variável ivar
iptr = &ivar ;
// as próximas linhas são equivalentes
ivar = 4 2 1 ;
* iptr = 4 2 1 ;
Com sistemas embarcados existem alguns endereços de memória que possuem características
especiais. Estes endereços possuem registros de conguração, interfaces com o meio externo e
variáveis importantes para o projetista. É pelo meio da utilização de ponteiros que é possível
acessar tais endereços de maneira simples, através da linguagem C.
Arquitetura de microcontroladores
Any suciently advanced technology is indistinguishable from ma-
gic. - Arthur C. Clarke
36
37 Arquitetura de microcontroladores
tamanho_da_palavra * 2^ tamanho_do_endereco
8 * 2^8 = 2 0 4 8 bytes ou 2 kbytes
O termo possibilidade foi usado pois apesar de poder alcançar toda essa extensão, nem sempre
existe memória física para armazenamento. Podemos imaginar a memória como um armário. Um
armário com 6 suportes pode abrigar até 6 gavetas. Depende do marceneiro fabricar e colocar
as gavetas neste armário. Podemos até indicar a posição onde queremos guardar algum objeto,
mas se a gaveta não foi colocada não é possível armazenar nada (Figura 2.2).
Suporte Existe
número: gaveta?
1 sim
2 sim
3 não
4 não
5 sim
6 não
Suporte Existe
número: gaveta?
1 Vitrine
2 Gaveta
3 Dispenser
4 Não
5 Gaveta
6 Cofre
Stack 1 0x000
GPR1
... 0x0FF
Stack 31 0x100
GPR2
0x1FF
Interrupção
GPR3
Baixa prioridade 0x0008 0x2FF
Alta prioridade 0x0018 0x300
GPR4
0x3FF
0x0028
Memória EEPROM
0x7FFF
Não implementado ...
0X8000
Não implementado 0xF60
0X1FFFFF SFR
0xFFF
O microcontrolador é capaz de realizar apenas uma tarefa por vez. Estas tarefas são executadas
sempre a intervalos regulares denidos pelo clock do sistema. O clock dene então a velocidade
com que o processador trabalha.
Algumas operações são mais demoradas pois são compostas de uma quantidade maior de
tarefas. Por exemplo a soma.
A soma de números inteiros é feita de maneira direta enquanto para somar dois números
fracionários, que estão em notação cientíca1 , é necessário igualar as potências antes de realizar
a soma. Por este motivo a segunda operação é mais demorada que a primeira.
Exemplo:
A = 1.23456 x 10 ^ 5
B = 3.4567 x 10 ^ 4
C = A x B
A = 123456; //C = 4.267503552 x 10 ^9
B = 34567;
C = A x B; // 1 . Converter para o mesmo expoente
//C = 4 2 6 7 5 0 3 5 5 2 // 12.3456 x 10 ^ 4
// 3.4567 x 10 ^ 4
// 1 . Multiplicar os núm er os // 2 . Multiplicar os núm er os
// 123456 // e somar a mantissa
// * 34567 // 12.3456 x 10 ^ 4
// 4267503552 // x 3.4567 x 10 ^ 4
// 42.67503552 x 10 ^ 8
// 3 . Corrigir quantidade de casas dec .
// 4.267503552 x 10 ^ 9
Conhecer quanto tempo o código leva para ser executado permite ao desenvolvedor saber de
maneira determinística qual é a exigência a nível de hardware para o sistema embarcado.
Por exemplo: Um sistema precisa executar 200 operações a cada milésimo de segundo. Cada
operação possui uma quantidade diferente de tarefas conforme podemos ver na Tabela 2.1.
O total de tarefas a serem realizadas é de 341 tarefas por milissegundo. Isso dá uma quanti-
dade de 341 mil tarefas por segundo. Se cada tarefa é realizada em um ciclo de clock, precisamos
de um microcontrolador cujo processador trabalhe no mínimo em 341 kHz.
1
Números fracionários podem ser armazenados de dois modos no ambiente digital. O modo mais comum é o
ponto utuante que se assemelha à notação cientíca.
Para funcionarem, todos os microcontroladores devem ser alimentados com tensão contínua.
O valor varia de modelo para modelo. Alguns podem até mesmo aceitar diversos valores. O PIC
18f4550, por exemplo, pode ser alimentado com qualquer tensão contínua entre 2 e 5,5 volts.
Para gerar o clock necessário, que denirá a velocidade na qual o processador irá trabalhar,
em geral é utilizado um oscilador a cristal, que possui uma ótima precisão.
Alguns microcontroladores podem dispensar o cristal externo optando por utilizar uma malha
RC interna ao chip. Esta alternativa é muito menos precisa e geralmente não permite valores
muito altos de clock. A vantagem é que sistemas que utilizam malha RC interna como osciladores
primários possuem um custo menor que sistemas que dependem de malhas de oscilação externa,
seja ela excitada por outra malha RC ou por um cristal.
Existem alguns circuitos que não são essenciais para o funcionamento do sistema, mas auxi-
liam muito no desenvolvimento. Entre estes tipos de circuito o mais importante é o que permite
a gravação do programa no próprio circuito. Alguns microcontroladores exigem que o chip seja
retirado do circuito e colocado numa placa especial para gravá-lo e somente depois recolocado
na placa para teste. Este é um procedimento muito trabalhoso e, devido ao desgaste mecânico
inerente, reduz a vida útil do chip.
Para evitar estes problemas, os fabricantes desenvolveram estruturas no chip que permitem
que este seja gravado mesmo estando soldado à placa nal. Para isso, basta que o desenvolvedor
disponibilize o contato de alguns pinos com um conector. Este conector será ligado a um gra-
vador que facilitará o trabalho de gravação do programa. Para a família PIC esta tecnologia é
denominada ICSP (in circuit serial programming).
A escolha de qual funcionalidade será utilizada depende do projetista. Em sistemas mais avan-
çados é possível inclusive utilizar mais de uma funcionalidade no mesmo terminal em períodos
alternados, desde que o circuito seja projetado levando esta opção em consideração.
A maioria dos terminais dos microcontroladores podem ser congurados para trabalhar de di-
versas maneiras. Esta conguração é realizada através de registros especiais. Estes registros
são posições de memória pré-denidas pelo fabricante. Para conhecer quais são e o que fazem é
preciso recorrer ao datasheet do componente.
Além dos registros de conguração dos terminais, existem registros que indicam como o mi-
crocontrolador deve operar. O microcontrolador PIC 18f4550 possui dez registros que controlam
seu modo de operação, velocidade, modo de gravação, etc. Estes registros são apresentados na
Figura 2.6.
Dos registros apresentados na Figura 2.6, quatro precisam necessariamente ser congurados
para que o sistema possa funcionar. Dois deles tem relação com a conguração do sistema de
clock: um especica qual é a fonte do sinal de clock, que no caso da placa em questão é um
cristal externo, e o outro indica qual o prescaler a ser usado (PLL).
Além de congurar a frequência básica do clock é necessário desligar o watchdog. Este é
um circuito para aumentar a segurança do sistema embarcado desenvolvido. Para funcionar
corretamente, o programa deve ser preparado para tal nalidade. Ele será explicado em detalhes
na seção 3.13 e por isso será mantido desligado nos próximos exemplos.
A última conguração necessária é desabilitar a programação em baixa tensão. Devido às
ligações feitas na placa, deixar esta opção ligada impede o funcionamento da placa enquanto
estiver ligada ao gravador. Abaixo o trecho de código que realiza estas congurações para o
compilador SDCC.
char
// Pll desligado
code at 0 x300000 CONFIG1L = 0 x01 ;
char
// Oscilador c/ cristal externo HS
code at 0 x300001 CONFIG1H = 0 x0C ;
char
// Watchdog controlado por software
code at 0 x300003 CONFIG2H = 0 x00 ;
char
// Sem programação em baixa tensão
code at 0 x300006 CONFIG4L = 0 x00 ;
#pragma
// Oscilador c/ cristal externo HS
config FOSC = HS
#pragma
// Pll desligado
config CPUDIV = OSC1_PLL2
#pragma
// Watchdog controlado por software
c o n f i g WDT = OFF
#pragma
// Sem programação em baixa tensão
config LVP = OFF
Notar que as diretivas utilizadas são completamente diferentes, mas realizam o mesmo tra-
balho.
Barramento de Led's(3.3)
Display de 7 segmentos(3.4)
Display LCD 2x16(3.6)
Saídas PWM(3.9)
Leitura de teclas(3.5)
Conversor AD(3.8)
1
Periféricos que fornecem informações aos usuários ou enviam comandos da placa eletrônica para o meio externo
2
Periféricos que recebem informações ou comandos do meio externo
45
46 Programação dos Periféricos
O microcontrolador possui portas que permitem o interfaceamento do meio externo para o meio
interno. Algumas portas podem trabalhar como receptoras ou transmissoras de sinais. Algumas
podem operar dos dois modos, sendo necessário congurá-las antes de sua utilização.
O microcontrolador PIC 18f4550 possui 5 portas:
Cada porta está ligada à dois endereços de memória. O primeiro armazena o valor que
queremos ler do meio externo ou escrever para o meio externo dependendo da conguração. O
segundo endereço realiza essa conguração indicando quais bits serão utilizados para entrada e
quais serão utilizados para saída (Tabela 3.1).
void void
// i n í c i o do programa
main ( )
{
// Para que o ponteiro para a porta D e Tris D funcione
// e l e s são definidos como :
// a ) unsigned char : pois os 8 bits representam valores
// b ) volatile : as variáveis podem mudar a qualquer momento
for
// mantém o sistema ligado indefinidamente
(;;) ;
}
Notar que, por serem ponteiros, sempre que precisarmos utilizar o valor de tais variáveis é
necessário que coloquemos o asterisco.
Uma outra maneira de manipular as portas é criar dene's que permitem o uso das portas
como variáveis, sem a necessidade de utilizar ponteiros de modo explícito, nem asteriscos no
código.
for
// mantém o sistema ligado indefinidamente
(;;) ;
}
Como estamos criando um dene, é uma boa prática de programação utilizar apenas letras
maiúsculas para diferenciá-lo de uma variável comum.
Notem que usamos dois asteriscos no dene. É isto que permite que utilizemos o dene como
uma variável qualquer, sem a necessidade de utilizar um asterisco extra em todas as chamadas
da "variável", como no caso dos ponteiros.
A segunda abordagem (com dene) é preferida em relação à primeira pois, dependendo do
compilador, gera códigos mais rápidos além de economizar memória. Além disso, permite que a
denição seja feita apenas uma vez e utilizada em todo o programa.
Para a placa que estamos utilizando, a conguração dos terminais do PIC segue conforme a
Tabela 3.2. Esta conguração reete a opção do autor de acordo com as possibilidades da placa
e também o sistema mínimo para realização de todas as experiências da apostila.
Os terminais não citados na Tabela 3.2 (1, 3, 5, 6, 15, 18, 23 e 24) possuem periféricos
que não serão utilizados neste curso. Os terminais 11 e 31 representam a alimentação positiva.
O comum (terra) está ligado ao 12 e ao 32. O microcontrolador utilizado (18f4550) possui o
encapsulamento DIP. Para outros encapsulamentos favor considerar o datasheet.
Da Tabela 3.2, temos que a porta A possui o primeiro bit como entrada analógica e o terceiro
e sexto como saída digital. Os dois bits digitais servem como controle de ativação do display.
13 OSC1 /CLKI
14 OSC2 /CLKO/RA6
Cristal
16 CCP2
RC1/T1OSI/ Aquecedor
17 CCP1
RC2/ /P1A Ventilador / Buzzer
19 RD0 /SPP0
21 RD2 /SPP2
LCD/7seg/Led
22 RD3 /SPP3
25 TX
RC6/ /CK
26 RX
RC7/ /DT/SDO
RS232
27 RD4 /SPP4
29 RD6 /SPP6/P1C
LCD/7seg/Led
30 RD7 /SPP7/P1D
33 RB0 /AN12/INT0/SDI
34 RB1 /AN10/INT1/SCK
RB2
Saídas para alimentação do teclado
35 /AN8/INT2/VMO
36 RB3 /AN9/CCP2/VPO
37 RB4 /AN11/KBI0/CSSPP
38 RB5 /KBI1/PGM
RB6
Entradas para leitura do teclado
39 /KBI2/PGC
40 RB7 /KBI3/PGD
A porta B possui os 4 primeiros bits como saída e os quatro últimos como entrada. Esta
porta serve para leitura da matriz de chaves. É possível realizar a leitura através de interrupção.
A porta C possui o segundo e terceiro bit como saída PWM e o sétimo e oitavo como
comunicação serial.
A porta D é utilizada como barramento de dados. Os valores escritos nela são transmitidos,
simultaneamente, para os leds, os displays de 7 segmentos e o display de LCD.
A porta E possui apenas os 3 primeiros bits congurados como saídas digitais. São utilizados
para controle de ativação dos displays e também como sinais de controle do LCD.
Existe na placa utilizada um barramento de 8 bits, onde cada linha possui um led associado. Este
barramento está ligado diretamente com a porta D do microcontrolador conforme Figura 3.2.
Podemos notar pela Figura 3.2 que existe um jumper (JP1) que habilita ou não o funcio-
namento destes leds. Além disso percebemos que se o jumper estiver encaixado, os led's estão
permanentemente ligados ao 5 volts. Deste modo, para que o led acenda, é necessário colocar o
valor 0 (zero) no respectivo bit da porta D. Quando um dispositivo é ligado com o valor 0 (zero)
e desligado com o valor 1 (um), dizemos que este dispositivo opera com lógica invertida.
Conforme visto é preciso congurar os pinos da porta D como saída, para isso basta escrever
zero em cada um deles no registro TRISD.
Os displays de 7 segmentos (Figura 3.3) são componentes opto eletrônicos utilizados para apre-
sentar informações para o usuário em formato numérico.
Estes displays foram concebidos com o intuito de gerar os dez algarismos arábicos: 0, 1, 2,
3, 4, 5, 6, 7, 8, 9, sendo que os algarismos 0, 6, 7 e 9 podem ser representados de mais de uma
maneira.
Além dos algarismos é possível representar apenas algumas letras de modo não ambíguo: as
maiúsculas A, C, E, F, H, J, L, P, S, U, Z e as minúsculas: a, b, c, d, h, i, n, o, r, t, u.
Os displays podem ser do tipo cátodo comum ou ânodo comum. Contudo, esta diferença não
será crítica para este estudo. Na Figura 3.4 podemos visualizar o esquema elétrico e a disposição
física de cada led no componente.
Figura 3.4: Diagrama elétrico para display de 7 segmentos com ânodo comum
http://www.hobbyprojects.com/the_diode/seven_segment_display.html
Pela Figura 3.4 podemos notar que para que apareça o número 2 no display é necessário
acender os leds a, b, g, e, d. Se estivermos utilizando um display com cátodo comum, precisamos
colocar um nível alto para ligar o led, ou seja, o led liga com valor 1 (um) e desliga com valor 0
(zero). Isto é também conhecido como lógica positiva. Na Tabela 3.3 são apresentados os valores
em binário e em hexadecimal para cada representação alfanumérica3 . Dentre as letras disponíveis
estão apresentadas apenas os caracteres A, b, C, d, E, F. Estas foram escolhidas por serem as
mais utilizadas para apresentar valores em hexadecimal nos displays. Neste curso utilizaremos a
3
Notar que os valores hexadecimais apresentados servem apenas quando existe uma sequência na ligação entre
a porta do microcontrolador e os pinos do display. Em alguns sistemas, o display pode ser controlado por duas
portas diferentes, ou possuir alguma alteração na sequência de ligação. Para tais casos é necessário remontar a
tabela apresentada.
ordem direta apresentada na Tabela 3.3. A utilização de uma ou outra depende da ligação feita
na placa. A Figura 3.5 apresenta o esquema elétrico disponível.
Para simplicar a utilização deste tipo de display é comum criar uma tabela cujas posições
representam o valor de conversão para o display. Conforme pode ser visto no código a seguir:
for
// a p e n a s para contar tempo
( time = 0 ; time < 65000; time ++) ;
}
}
Multiplexação de displays
Cada display exige 7 ou 8 terminais de controle, caso também seja utilizado o ponto decimal.
Para utilizar 4 displays, por exemplo um relógio com dois dígitos para horas e dois para minutos,
precisaríamos de 32 terminais de saída, o que pode ser um custo4 muito alto para o projeto.
Uma técnica que pode ser utilizada é a multiplexação dos displays. Esta técnica leva em conta
um efeito biológico denominado percepção retiniana. O olho humano é incapaz de perceber
mudanças mais rápidas que 1/30 (s). Outro fator importante é que as imagens mais claras
cam gravadas na retina devido ao tempo que leva para sensibilizar e dessensibilizar as células
(bastonetes).
Deste modo podemos ligar e desligar rapidamente o display que a imagem continuará na
retina. Se ligarmos cada display, um por vez, sequencialmente, de maneira sucientemente
rápida, teremos a impressão que todos estão ligados. A frequência de chaveamento deve ser
mais rápida que 30Hz.
A Figura 3.5 apresenta o circuito com 4 displays multiplexados. Percebemos que os terminais
iguais estão ligados juntos. Percebemos também que os terminais de cátodo comum estão cada
um ligado a uma saída diferente. Com esta arquitetura reduzimos a quantidade de terminais
necessários de 32 para 12, uma economia de 20 terminais.
Mas esta economia tem um custo, o sistema se torna mais complexo pois não podemos ligar
dois displays ao mesmo tempo.
O controle de qual display será ligado é feito através do transistor que permite a ligação do
cátodo comum ao terra, ou o ânodo comum ao VCC (depende do tipo de dispositivo). Para o
correto funcionamento não basta agora acender os leds corretos para formar o número, temos
que seguir um algoritmo mais complexo:
4. desligar o display
6. voltar ao passo 1
4
Microcontroladores com mais terminais possuem um custo superior, mesmo possuindo os mesmos periféricos
internamente.
5
Se a taxa de atualização dos displays for muito baixa, estes vão apresentar uma variação na intensidade, como
se estivessem piscando. Este efeito é chamado de icker.
Criação da biblioteca
O programa 3.1 apresenta um exemplo de código para criar uma biblioteca para os displays de 7
segmentos. O programa 3.2 apresenta o header da biblioteca. Já o programa 3.3 apresenta uma
demonstração de uso da biblioteca.
4 44 PORTE = 0 x00 ;
6 46 PORTD = 0 x00 ;
static char v0 v1 v2 v3
// v a l o r e s dos displays
47
switch
7 // l i g a apenas o display da vez
11 { 51 PORTD = valor [ v0 ] ;
12 v0 = val ; 52 BitSet ( PORTA , 5 ) ;
53 display = 1 ;
13
if
}
( pos == 1 ) 54 break
case
14 ;
15 { 55 1:
20 v2 = val ; 60 2:
21 } 61 PORTD = valor [ v2 ] ;
22 if ( pos == 3 ) 62
63
BitSet ( PORTE , 0 ) ;
display = 3 ;
23 {
v3 val ; 64 break
case
24 = ;
25 } 65 3:
26 } 66 PORTD = valor [ v3 ] ;
67 BitSet ( PORTE , 2 ) ;
28 void InicializaDisplays ( void ) 68
69
display = 0 ;
break
default
29 { ;
32 BitClr ( TRISA , 5 ) ; 72 ;
33 BitClr ( TRISE , 0 ) ; 73 }
34 BitClr ( TRISE , 2 ) ; 74 }
35 // a p e n a s AN0 é analógico
36 ADCON1 = 0 x0E ;
37 // P o r t a de dados
38 TRISD = 0 x00 ;
39 }
5
void main void
// i n í c i o do programa
6 ( )
7 {
8 unsigned int tempo ;
9 InicializaDisplays ( ) ;
10 MudaDigito ( 0 , 0 ) ;
11 MudaDigito ( 1 , 1 ) ;
12 MudaDigito ( 2 , 2 ) ;
13 MudaDigito ( 3 , 3 ) ;
14 for (;;)
15 {
16 AtualizaDisplay ( ) ;
17
for
// g a s t a um t e m p o para evitar o efeito flicker
18 ( tempo =0; tempo < 1 0 0 0 ; tempo ++) ;
19 }
20 }
Para realizar a leitura de uma tecla é necessário criar um circuito que realize a leitura de um
sinal elétrico para o valor zero e outro para o valor um. Os níveis de tensão associados dependem
muito dos circuitos envolvidos. Os níveis mais comuns são os compatíveis com TTL, onde o zero
lógico é representado por 0v (zero volts) e o um lógico é representado por 5v (cinco volts).
Uma maneira de se obter este funcionamento é com o uso de uma chave ligada ao VCC e um
pull-down ou uma chave ligada ao terra (GND) e um pull-up.
Pela Figura 3.6 percebemos que a tensão de saída é igual a VCC quando a chave está desligada.
Não havendo circulação de corrente no circuito a queda de tensão em R1 é zero.
Quando a chave é pressionada uma corrente ui de VCC para o terra passando por R1. Como
não existe nenhuma outra resistência no circuito, toda a tensão ca em cima de R1. Deste modo
a tensão de saída passa a ser zero.
Apesar do funcionamento aparentemente simples, este tipo de circuito apresenta um problema
de oscilação do sinal no momento em que a tecla é pressionada. Esta oscilação é conhecida como
bouncing (Figura 3.7).
Estas oscilações indevidas podem gerar acionamentos acidentais, causando mau funciona-
mento do programa. Para evitar isso podemos utilizar técnicas de debounce, por hardware ou
software.
A opção de debounce por hardware pode ser visualizada na Figura 3.8.
Neste circuito, o capacitor desempenha o papel de amortecedor do sinal. Um circuito com um
resistor e um capacitor possui um tempo de atraso para o sinal. Este é o tempo necessário para
carregar o capacitor. Deste modo as alterações rápidas no sinal, devido à oscilação mecânica da
chave, são ltradas e não ocorre o problema dos chaveamentos indevidos, como pode ser visto
na Figura 3.9. Notar que o nível do sinal ltrado não chega a zero em nenhum momento, devido
à constante de tempo do ltro RC ser maior que o período de debounce.
8MHz) é de 0,56 (µs). Antes de utilizar o valor que estamos lendo na porta em questão devemos
esperar 300 ciclos de clock após alguma mudança para ter certeza que o sinal se estabilizou, ou
seja, a fase de bouncing acabou.
Notar que, no código, o contador é iniciado com o valor 22. Através da análise do assembler
podemos saber que cada ciclo de conferência do sinal possui 14 instruções. Assim é necessário
que o sinal permaneça com o mesmo valor durante 308 ciclos para que a variável valAtual receba
o valor da porta B. Estes valores podem ser determinados empiricamente através de testes com
osciloscópios.
while
// a g u a r d a uma mudança na porta B
( valAtual==PORTB ) ;
// q u a n d o acontecer alguma mudança , conta um t e m p o pra ver se é permanente
valTemp = PORTB ;
tempo = 2 2 ;
while ( tempo > 0 )
{
if ( valTemp == PORTB ) // se não mudar continua a contar
{
tempo −−;
6
Lembrar que cada ciclo do PIC necessita de 4 ciclos de clock externo
}
else
{
valTemp = PORTB ; // se mudar , atualiza o sistema e reinicia o tempo
tempo = 22;
}
}
valAtual = valTemp ; // v a l o r a t u a l i z a d o ;
PORTD = valAtual ; // c o l o c a o v a l o r no b a r r a m e n t o de leds
}
}
for j
// g a s t a tempo para garantir que o pino atingiu o nível alto
( = 0; j < 100; j++) ;
for j
// r e a l i z a o teste para cada bit e atualiza a matriz .
( = 0; j < 4; j++)
{
if (! BitTst ( PORTB , j +4) )
{
chave [ i ] [ j ] = 1 ;
BitSet ( PORTD , j +4* i ) ;
}
else
{
chave [ i ] [ j ] = 0 ;
BitClr ( PORTD , j +4* i ) ;
}
}
for j j
atingiu o nível alto
16 ( j =0; < 1 0 0 ; ++) ;
17
for j
// r e a l i z a o teste para cada bit e atualiza a variável
18 j j
if BitTst PORTB j
( = 0; < 4; ++) {
19 (! ( , +4) ) {
20 BitSet valorNovo i * 4 )+j ) ;
else
( ,(
21 } {
22 BitClr ( valorNovo ,( i * 4 )+j ) ;
23 }
24 }
25 }
26 if valorAntigo
( == valorNovo ) {
27 tempo −−
else
;
28 } {
29 tempo = 1 0 ;
30 valorAntigo = valorNovo ;
31 }
32 if ( tempo == 0 ) {
33 valor = valorAntigo ;
34 }
35 }
}
}
}
É importante notar que o código acima não apresenta debounce em software para as teclas. É
possível realizar um debounce minimizando o gasto com memória e tempo, representando cada
chave como um bit diferente numa variável. Esta será a abordagem utilizada na geração da
biblioteca para o teclado.
Criação da biblioteca
O programa 3.4 apresenta um exemplo de código para criar uma biblioteca para um teclado de
16 teclas com leitura matricial. O header pode ser visto no programa 3.5. Já o programa 3.6
apresenta uma demonstração de uso da biblioteca.
5
void void
// i n í c i o do programa
6 main ( )
7 {
8 InicializaTeclado ( ) ;
9 TRISD = 0 x00 ; // C o n f i g u r a a p o r t a D como saída
10 PORTD = 0 xFF ; // d e s l i g a t o d o s o s l e d s
11 while (1==1)
12 {
13 DebounceTeclas ( ) ;
14 PORTD = LerTeclas ( ) ;
15 }
16 }
O display de LCD utilizado neste curso possui duas linhas por 16 colunas de caracteres, compa-
tível com o HD44780. Na Figura 3.11 é apresentado um modelo genérico deste tipo de display.
A Figura 3.12 apresenta o verso do display com os terminais expostos.
Este mesmo tipo de display pode ser encontrado em diversas versões com tamanhos e cores
diferentes, sendo os mais comuns de 1x8, 2x16 e 4x40. Pode ainda ter 16 ou 14 terminais,
dependendo se existe ou não retro iluminação. Estes terminais são identicados como:
1. Terra 9. Bit 2
7 6 5 4 3 2 1 0
Criação da biblioteca
Para facilitar o controle do display, podemos criar três funções, uma para inicialização, uma para
escrever um caractere e a última para enviar um comando. Estas funções estão apresentadas no
programa 3.8, que constitui um exemplo de biblioteca. Além destas três funções é necessário ter
uma função de delay, que garanta um determinado tempo para que as informações sejam lidas
corretamente pelo LCD.
O header desta biblioteca e um exemplo de como usá-la são apresentados nos programas 3.7
e 3.9, respectivamente.
unsigned char i
( ){
79 BitClr ( PORTE , RS ) ;
37
for i i
;
80 // d e i x a em nível baixo
38 ( =0; i < 25; ++) ;
81 BitClr ( PORTE , RW ) ;
39 }
82 Delay2ms ( ) ;
41 void Delay2ms void 83 }
unsigned char i
( ){
42
for i i
;
43 ( =0; < 200; i++){
44 Delay40us ( ) ;
45 }
46 }
5
void main void
// i n í c i o do programa
6 ( )
7 {
8 unsigned int i j
char msg
, ;
9 " Hello
[] = World !" ;
10 InicializaLCD
for i i i
() ;
11 ( =0; < 1 1 ; ++)
12 {
13 EnviaDados ( msg [ i ] ) ;
14 for ( j = 0 ; j < 6 5 0 0 0 ; j++) ;
15 }
16 for (;;) ;
17 }
Em geral a comunicação entre dois dispositivos eletrônicos é realizada de modo serial, isto é, as
informações são passadas bit à bit do transmissor para o receptor. Este tipo de comunicação
possui algumas vantagens em relação à comunicação paralela, na qual a palavra (byte) é enviada
toda de uma vez.
A primeira é a simplicação do hardware. Como os dados são enviados um a um, a quantidade
de os envolvidos na transmissão é menor.
A segunda é a maior taxa de transmissão, o que a primeira vista pode parecer inconsistente
já que a comunicação paralela envia mais de um bit ao mesmo tempo. Para frequências muito
altas nem sempre o envio das informações são sincronizadas em todos os os. Existe também
o problema do crosstalking, onde o campo magnético gerado por um cabo induz uma tensão
no cabo adjacente, atrapalhando a comunicação. Estes problemas aumentam com a frequência,
limitando assim a máxima transferência possível pelo barramento paralelo. É este o motivo que
levou os projetistas de hardware a desenvolverem o protocolo SATA, em detrimento ao IDE/ATA,
para comunicação entre o HD e a placa mãe conforme pode ser visto na Tabela 3.5.
Para sistemas embarcados em geral não existe grande necessidade de altas taxas de trans-
missão mas de sistemas de baixo custo e conáveis. Existem diversas alternativas entre elas
estudaremos duas das mais comuns: RS232 e I2C.
I2C
O protocolo I2C foi desenvolvido pela Phillips na década de 1980 para permitir que componentes
eletrônicos de uma mesma placa pudessem se comunicar de modo simples e fácil. O protocolo
inicialmente foi desenvolvido para uma comunicação de 100kbps. A versão 4.0 de 2012 7 especica
velocidades de até 5MHz.
Este é um protocolo serial síncrono, ou seja, o clock é enviado junto com o sinal, possibilitando
ao dispositivo receptor saber o momento certo de ler os sinais no barramento. Isto permite o
desenvolvimento de um sistema ou dispositivo mais simples e consequentemente mais barato.
A especicação normatiza um protocolo do tipo mestre/escravo e permite mais de um mestre
ou escravo no barramento. Para isto cada um dos dispositivos possue um registro de 7 ou 10
7
I2C Specication Version 4.0: http://www.nxp.com/documents/user_manual/UM10204.pdf
bits que permite identicá-lo de maneira única. Deste modo é possível construir um sistema
integrado com baixo custo de conexão. A gura 3.16 mostra um exemplo de um barramento com
mais de dois componentes no mesmo barramento.
Isto só é possível por causa da estrura eletrônica escolhida para as conexões: coletor aberto.
Esta estrutura permite que mais de um dispositivo se conecte ao barramento sem causar curtos.
Se um componente está ligado e outro desligado a estrutura evita que aconteça um curto e o
sinal no barramento permanece com nível baixo, gura 3.17.
O envio ou a recepção de dados são sempre iniciados pelo mestre. O primeiro bit informa se
o o mestre deseja ler/escrever um valor do o componente. Os próximos sete bits são o endereço
do dispositivo.
Após o envio do primeiro byte o dispositivo envia um bit indicando que recebeu o comando
corretamente e está pronto para o próximo byte. Se a operação for de escrita, o próximo byte é
enviado pelo mestre, se a operação for de leitura o byte é enviado pelo escravo.
Todas essas operações são sincronizadas pelo clock do mestre, mesmo quando o byte é enviado
pelo escravo. Segundo a norma do protocolo o valor na linha de dados deve ser sempre válido
quando a linha de clock estiver alta.
Deste modo é muito simples, e também bastante comum, implementar o protocolo inteiro em
software.
HT1380
O dispositivo HT1380 é um relógio de tempo real (RTC) com um protocolo proprietário baseado
no I2C. Um RTC é um dispositivo especializado em manter a contagem de tempo para longos
períodos de tempo.
Além dos dois terminais do protocolo (clock e dados) este RTC possui um terminal para ha-
bilitar/desabilitar a comunicação do chip. A comunicação também não exige um bit de resposta
como no I2C. A gura 3.18 apresenta a forma de onda que deve ser gerada para a comunicação
entre os dispositivos.
O byte de comando é composto de 1 bit indicando leitura ou escrita, três bits indicando qual
é o registro de interesse e 1 bit para habilitar/desabilitar o clock interno do chip conforme a
gura 3.19.
O RTC possui 8 registros internos, com endereços de 0b000 à 0b111. Os valores destes
endereços estão codicados em BCD para facilitar a passagem dos valores. Estes registros são
apresentados na gura 3.20.
9 addr <<= 1 ;
10 addr |= 0 x80 ; // w r i t e
11 writeByte ( addr ) ;
12 writeByte ( dados ) ;
14 RTC_RESET_OFF ( ) ;
15 RTC_SCLK_OFF ( ) ;
16 RTC_IO_OFF ( ) ;
17 }
25 RTC_RESET_ON ( ) ;
26 addr <<= 1 ;
27 addr |= 0 x81 ; // r e a d
28 writeByte ( addr ) ;
29 // muda pino para ler sinais
30 RTC_IO_IN ( ) ;
31 DELAY ( ) ; DELAY ( ) ; DELAY ( ) ; DELAY ( ) ;
32 dados = readByte ( ) ;
34 RTC_RESET_OFF ( ) ;
35 RTC_SCLK_OFF ( ) ;
36 RTC_IO_OFF ( ) ;
37 RTC_IO_OUT ( ) ;
38 return dados ;
39 }
para leitura e escrita do RTC e são apresentadas no código 3.12. Note que na escrita é necessário
inverter a direção do terminal de dados, de saída para entrada.
Os registros deste RTC armazenam 8 valores em bcd: segundos, minutos, dias, meses, anos,
dia do mês e um registro de proteção para escrita. BCD é um formato utilizado em sistemas
eletrônicos para armazenar valores decimais em variáveis binárias (bcd = binary codded decimal ).
A conversão de BCD para decimal é simples, basta se utiliza da aritmética binária separando-se
os digitos e multiplicando a dezena por dez antes de somá-la à unidade. Para a conversão de
decimal para BCD é necessário primeiro separar a dezena do valor da unidade de depois colocar
os dois valores defasados de 4 bits numa variável binária. O código 3.13 apresenta duas funções
de conversão e acesso ao registro de segundos.
RS 232
O protocolo de comunicação RS232 (Recommended Standard 232) é muito utilizado para co-
municação entre dispositivos que transmitem ou recebem pouca quantidade de informação. É
um dos protocolos mais antigos ainda em uso, tendo seu primeiro uso em 1962 para máquinas
eletromecânicas de escrever. O padrão RS232 revisão C é datado de 1969. Em 1986 aparece a
revisão D pela EIA (Electronic Industries Alliance). A versão atual do protocolo é datada de
1997 pela TIA (Telecommunications Industry Association) sendo chamada TIA-232-F.
O procedimento de envio de um valor pela serial através do padrão RS232 pode ser visto
como uma operação de bit-shift.
Por exemplo a letra K: em ASCII é codicada como 7610 e em binário como 110100102 . Na
maioria dos dispositivos primeiro se envia o bit menos signicativo. Antes de iniciar a transmissão
dos bits, é enviado um bit de começo, indicando que haverá transmissão a partir daquele instante.
Após isso o bit menos signicativo é enviado para a saída do microcontrolador. Realiza-se então
um shift para direita e o novo bit menos signicativo é reenviado. Esta operação é realizada
oito vezes. Após esse procedimento envia-se um bit de parada, que pode ter a duração de um ou
dois bits.
A Figura 3.21 apresenta o sinal elétrico8 enviado ao longo do tempo para a letra K. Notar
a região em branco, que se estende entre +3 e -3. Ela indica a região de tensão na qual o
sinal não está denido. Caso a tensão lida esteja entre estes limiares, seja devido à ruídos ou
outros problemas, o sistema de recepção não entenderá a mensagem e os dados serão perdidos
ou corrompidos.
Como visto na Tabela 3.6 existem três fórmulas diferentes para calcular a taxa de transmissão.
A melhor maneira de congurar a taxa de transmissão da porta serial é vericar qual dos métodos
gera o menor erro para uma dada taxa de transmissão.
Por exemplo, queremos uma taxa de transmissão de 57,6 kbps. A frequência disponível é um
cristal de 8MHz. Usando as três fórmulas chegamos aos seguintes valores:
A equação que gera o menor erro é a terceira. Como queremos trabalhar com uma comuni-
cação assíncrona, da Tabela 3.6 obtemos que os bits de conguração devem ser: TXSTA(4) = 0,
BAUDCON(3) = 1 e TXSTA(2) = 1. A seguir temos todo o processo de conguração da porta
serial RS232.
O procedimento de serialização dos bits é feito de maneira automática pelo hardware. En-
quanto ele está realizando este processo não devemos mexer no registro que armazena o byte a
ser enviado. Por isso devemos vericar se o registro está disponível. Isto é feito através do bit 4
do registro PIR. Quando este valor estiver em 1 basta escrever o valor que desejamos transmitir
no registro TXREG.
A metodologia apresentada para leitura e escrita de valores é conhecida como pooling. Neste
tipo de abordagem camos parados esperando que o valor esteja disponível para leitura/escrita.
Este é o método mais simples para se controlar qualquer tipo de dispositivo. O problema é que
o processador ca travado em uma tarefa gastando tempo que seria útil para realizar outras
operações. A melhor alternativa para resolver este problema é através de interrupções, que serão
abordadas apenas no tópico 3.12.
Criação da biblioteca
O programa 3.14 apresenta um exemplo de código para criar uma biblioteca para comunicação
serial. O arquivo de header é apresentado no programa 3.15 e o exemplo de uso demonstrado no
programa 3.16.
A seguir o arquivo de header.
5
void main void
// i n í c i o do programa
6 ( )
7 {
8 unsigned int i j
char msg
, ;
9 " Hello World !" ;
unsigned char resp
[] =
10 ;
11 TRISD = 0 x00 ; // a c e s s o aos leds
12 InicializaSerial ( ) ;
13 j =0;
14 for (;;)
15 {
16
for
// d e l a y
17 (i = 0; i < 65000; i++) ;
18 // e n v i a dados
19 EnviaSerial ( msg [ j ] ) ;
20 j ++;
21 if ( j > 11)
22 {
23 j =0;
24 EnviaSerial ( 1 3 ) ;
25 }
26 // r e c e b e dados
27 resp RecebeSerial ( ) ;
if
=
28 ( resp ! = 0 )
29 {
30 PORTD = resp ;
31 }
32 }
33 }
3.8 Conversor AD
Elementos sensores
A conversão AD é muito utilizada para realizarmos a leitura de sensores. Todo sensor é baseado
num transdutor. Um elemento transdutor é aquele que consegue transformar um tipo de grandeza
em outro, por exemplo uma lâmpada incandescente (Figura 3.22).
Podemos utilizar uma lâmpada incandescente como sensor de tensão: pega-se uma lâmpada
de 220V. Liga-se a lâmpada a uma tomada desconhecida. Se o brilho for forte a tomada possui
220V, se o brilho for de baixa intensidade, a tomada possui 127V. Se a lâmpada não ascender
existe algum problema na ação, na tomada ou até mesmo na lâmpada. A lâmpada é um
transdutor de tensão para luminosidade.
Para a eletrônica estamos interessados em transdutores cuja saída seja uma variação de
tensão, corrente ou resistência.
Um sistema muito simples de transdutor de ângulo para resistência é o potenciômetro (Fi-
gura 3.23).
Se o potenciômetro estiver alimentado pelos terminais da extremidade, o terminal central fun-
ciona como um divisor de tensão. O valor de saída é proporcional à posição do cursor. Podemos
aproximar o potenciômetro como duas resistências conforme apresentado na Figura 3.24.
Deste modo a tensão aplicada em RL (supondo que RL é muito maior que R2) é:
VS ∗ R2 R2
VRL = = VS ∗ ( )
R1 + R2 RPot
Se na construção do potenciômetro a variação da resistência ao longo da trilha foi feita de modo
constante, a resistência varia de maneira linear com a posição do cursor. Deste modo podemos
utilizar o potenciômetro como um transdutor de ângulo.
Diversas medidas podem ser realizadas utilizando o conceito de divisor de tensão: luminosi-
dade com LDR's, força com strain-gauges, deslocamento com potenciômetros lineares, etc.
Existem alguns sensores que possuem circuitos de amplicação e condicionamento do sinal
embutidos no mesmo envólucro que o elemento sensor. A estes tipos de sensores damos a deno-
minação de ativos.
9
Com uma precisão de 10 bits conseguimos representar 210 valores diferentes, ou 1024 valores.
Um sensor ativo possui no mínimo 3 terminais: 2 para alimentação e 1 para saída do sinal.
Um exemplo deste tipo de sensor é o LM35 (Figura 3.25) que é utilizado para monitoramento
de temperatura.
Na Figura 3.26 é apresentado o diagrama de blocos do circuito integrado do LM35. O diodo
é utilizado como unidade sensora de temperatura.
Processo de conversão AD
Existem alguns circuitos que realizam a conversão de um sinal analógico advindo de um trans-
dutor para um sinal digital com uma precisão arbitrária.
A abordagem mais simples é a utilização de comparadores. Cada comparador possui um
nível diferente de tensão de referência. Estes níveis são escolhidos de forma que a representação
binária faça sentido.
Exemplo: Conversão de um valor analógico que varia de zero à cinco volts numa palavra
digital de dois bits.
Para N bits temos 2N representações diferentes. É interessante então dividir a amplitude
inicial por 2N divisões iguais. Para N = 2 temos 4 representações de 1.25v cada. É comum
nestes comparadores que a primeira tensão possua um oset.
Representação binária com 2 bits Valor em tensão Valor em Tensão com oset
00 0.000 0.625v
01 1.250 1.875v
10 2.500 3.125v
11 3.750 4.375v
A Figura 3.27 apresenta as faixas de valores e da necessidade de oset para uma faixa mais
representativa dos valores reais.
5
11 4,375
4
3 10 3,125
2 01 1,875
1
00 0,675
Intervalos Limites de
de valor comparação
O circuito eletrônico responsável pelas comparações pode ser visualizado na Figura 3.28.
O circuito da Figura 3.28 é conhecido como conversor analógico digital do tipo ash onde
cada nível de tensão possui seu próprio comparador. Existem outras abordagens que minimizam
o uso de conversores (parte mais cara do circuito) mas inserem atraso no processo de conversão.
O atraso depende do tipo de circuito que é implementado.
Criação da biblioteca
Toda conversão leva um determinado tempo que, conforme citado na seção anterior, depende
da arquitetura que estamos utilizando, da qualidade do conversor e, algumas vezes, do valor de
tensão que queremos converter. Para que o microcontrolador realize corretamente a conversão é
necessário seguir os seguintes passos:
1. Congurar o conversor;
2. Iniciar a conversão;
4. Ler o valor.
As saídas PWM são saídas digitais que possuem um chaveamento acoplado. O sinal muda seu
estado de positivo para zero várias vezes por segundo. A porcentagem do tempo que este sinal
permanece em nível alto dene o ciclo de trabalho, ou duty cycle, da saída. A Figura 3.29
apresenta 3 sinais PWM com a mesma frequência mas com duty cycle diferentes.
Suponha uma saída PWM ligada a um resistor. Quando a saída estiver em nível alto existe
a passagem de corrente elétrica e a resistência aquece. Quando estiver em nível baixo a corrente
para. Como a constante térmica do componente é alta, leva-se alguns segundos para que o
resistor aqueça ou esfrie. Assim é possível ajustar a quantidade de energia média dado uma
frequência sucientemente alta10 de sinal do PWM.
Em outras palavras, se a frequência do PWM for mais alta do que a carga conseguir enxergar,
quando colocarmos o duty cycle em 50%, a carga irá receber 50% da energia total. Se for um
resistor, podemos controlar a temperatura nal deste modo, num motor podemos ajustar a
velocidade de rotação que queremos.
Como citado, a frequência do PWM tem que ser sucientemente alta. Esta frequência de-
pende do circuito implementado no microcontrolador. No caso do PIC 18f4550 é calculada
segundo a fórmula abaixo.
FOSC
Freq.PWM =
[(PR2 ) + 1] ∗ 4 ∗ (TMR2 Prescaler )
Com uma frequência de oscilação de 8MHz (disponível na placa) podemos atingir frequências
que variam de 488Hz até 2MHz.
O problema de trabalhar, no caso do PIC, com frequências muito altas é que perdemos
resolução na denição do duty cycle. Por exemplo, para a frequência de PWM em 2MHz com
um clock de 8MHz temos uma resolução de apenas 2 bits. Ou seja, podemos congurar a saída
para 0%, 25%, 50% ou 75% do valor máximo. A resolução pode ser obtida segundo a fórmula
abaixo:
log( FFPWM
OSC
)
Resolu ção PWM (max ) = bits
log(2)
O PIC 18f4550 permite uma resolução de até 10 bits. Com um oscilador principal de 8 MHz
a frequência máxima do PWM para utilizarmos os 10 bits de resolução é 7812,5 Hz. Para uma
10
Para ser considerada sucientemente alta a frequência do PWM deve possuir um valor mais alto que a maior
constante de tempo do sistema, de modo que este não perceba a oscilação. um fator de 10 vezes em geral é
suciente para que o sistema não sinta esse impacto. No entanto cada caso deve ser analisado em particular
Tabela 3.7: Faixa de frequências máximas e mínimas para cada conguração do prescaler
1 2.000.000 7.812,5
4 500.000 1.953,2
16 125.000 488,3
Criação da biblioteca
Para congurar as saídas PWM devemos especicar a frequência de trabalho através de PR2
e TCON2, além do duty cycle em CCPR1L e CCPR2L. No registro TRISC é congurado o
terminal como uma saída e em CCP1CON e CCP2CON denimos que ele deve trabalhar como
um PWM. O prescaler foi congurado para 16 bits de modo a obter a maior faixa de frequência
audível disponível (Tabela 3.7). Notar que é importante realizar primeiro a multiplicação e
somente depois a divisão, para não haver perda de informação. No programa 3.20 é apresentado
um código exemplo de como criar as rotinas de operação do PWM. O header desta biblioteca
é apresentado no programa 3.21. Por m, o programa 3.22 apresenta um exemplo de utilização
desta biblioteca.
12
void unsigned int
// t e m p o em micro segundos
13 ResetaTimer ( tempo )
14 {
15
unsigned char
// p a r a placa com 8MHz 1 us = 2 ciclos
16 ciclos tempo * 2 ;
=
17 // o v e r f l o w a c o n t e c e com 2^15 − 1 = 6 5 5 3 5 ( max u n s i g n e d i n t )
18 ciclos = 6 5 5 3 5 − ciclos ;
19 ciclos −= 1 4 ; // s u b t r a i t e m p o d e o v e r h e a d ( e x p e r i m e n t a l )
20 TMR0H = ( ciclos >> 8 ) ; // s a l v a a p a r t e a l t a
21 TMR0L = ( ciclos & 0 x00FF ) ; // s a l v a a p a r t e b a i x a
22 BitClr ( INTCON , 2 ) ; // l i m p a a f l a g d e o v e r f l o w
23 }
3.10 Timer
Nos microcontroladores existem estruturas próprias para realizar a contagem de tempo, estas
estruturas são denominadas Timers.
O PIC 18f4550 possui quatro timers. Para utilizarmos a saída PWM temos que congurar o
timer 2, que gera a base de tempo que será comparada com o duty cycle.
Ao invés de contarmos quantas instruções são necessárias para criar um delay de um deter-
minado tempo, podemos utilizar os timers. Escolhemos o valor de tempo que queremos contar,
inicializamos as variáveis e esperamos acontecer um overow 11 na contagem do timer.
Para trabalhar com o timer precisamos basicamente de uma função de inicialização, uma
para resetar o timer e outra para indicar se o tempo congurado anteriormente já passou. Uma
quarta função AguardaTimer(), foi construída para facilitar o desenvolvimento de algumas
rotinas comuns nos programas. Estas rotinas estão implementadas no programa 3.23 cujo header
é apresentado no programa 3.24. O modo de utilizar esta biblioteca é apresentado no programa
3.25.
11
Overow é conhecido como estouro de variável. Toda variável digital possui um valor máximo, por exemplo
255 para uma variável do tipo unsigned char. Se uma variável unsigned char possui o valor 255 e é acrescida de
1, seu valor passa a ser zero e acontece o estouro ou overow.
#define
// n o t a s musicais
8 45 tempo [ ] = {50 , 10 , 50 , 10 , 50 , 10 , ←-
#define
C 523
9 50 , 5, 25 , 5, 50 , 5, 50 , 5, 25 , 5, 50 , 50 , 50 , ←-
#define
CS 5 5 4
10 10 , 50 , 10 , 50 , 10 , 50 , 5, 25 , 5, 50 , 5, 5 0 , ←-
#define
D 587
11 5, 25 , 5, 50 , 50 , 100 , 5, 25 , 5, 25 , 10 , 1 0 0 , ←-
#define
DS 6 2 2
unsigned int
12 5, 50 , 5, 25 , 2, 10 , 2, 10 , 2, 100 , 250};
#define
E 659
13 46 notas [ ] = { G , v , G , v , G , v , E , v , B , ←-
#define
F 698
14 v , G , v , E , v , B , v , G , v , D2S , v , D2S , v , ←-
#define
FS 740
15 D2S , v , E2 , v , B , v , FS , v , E , v , B , v , G , v , ←-
#define
G 784
16 G2S , v , G , v , G , v , G2S , v , G2 , v , F2S , v , F2 , ←-
#define
GS 8 3 0
17 v , E2 , v , F2S , v } ;
#define
A 880
18 47 InicializaPWM ( ) ;
#define
AS 9 3 2
19 B 987 48 InicializaTimer ( ) ;
49 SetaFreqPWM ( notas [ 0 ] ) ;
50 SetaPWM1 ( 5 0 ) ; // g a r a n t e d u t y − c y c l e d e 50%
22 51 for (;;)
#define
// s e g u n d a oitava
23 C* 2 52 {
#define
C2
24 C2S CS * 2 53 AguardaTimer ( ) ;
25 #define D* 2 54 ResetaTimer ( 1 0 0 0 0 ) ;
#define
D2
D2S DS * 2 55 cont ++;
26
27 #define E* 2 56 if ( cont >= tempo [ pos ] )
#define
E2
28 F* 2 57 {
#define
F2
29 F2S FS * 2 58 pos ++;
30 #define G* 2 59 SetaFreqPWM ( notas [ pos ] ) ;
#define
G2
31 G2S GS* 2 60 SetaPWM1 ( 5 0 ) ;
32 #define A* 2 61 cont =0;
#define
A2
33 A2S AS * 2 62 }
34 #define B2 B* 2 63
64 }
}
37
#define
// sem som
38 v 125000
12
Esta é a máxima frequência possível para o PWM operado com prescaler de 16x.
3.12 Interrupção
Até o momento todos os programas que foram desenvolvidos seguiam um uxo sequencial sendo
alterado apenas por chamadas de funções, estruturas de decisão ou loop. Um dos problemas de
se utilizar este tipo de estrutura é que alguns periféricos possuem um tempo muito grande para
realizarem sua função como o conversor AD por exemplo. Nesta situação o que fazemos é iniciar
a conversão e car monitorando uma variável que indicava quando a conversão tinha terminado.
Esta técnica é conhecida como pooling.
O problema de se realizar a leitura de algum periférico por pooling é que o processador perde
tempo realizando operações desnecessárias checando a variável de controle. Uma alternativa é
utilizar um sistema que, quando a operação desejada estivesse nalizada, nos avisasse para que
pudéssemos tomar uma providência. Este procedimento é chamado de interrupção.
Alguns dispositivos possuem a possibilidade de operarem com interrupções. Quando a con-
dição do dispositivo for satisfeita (m da conversão para o AD, chegada de informação na serial,
mudança no valor da variável na porta B) ele gera uma interrupção. A interrupção para o
programa no ponto em que ele estiver, salva todos os dados atuais e vai para uma função pré-
denida. Esta função realiza seu trabalho e assim que terminar volta o programa no mesmo
ponto onde estava antes da interrupção.
Dos dispositivos estudados até agora os que geram interrupção são:
Porta Serial: quando chega alguma informação em RCREG ou quando o buer de trans-
missão TXREG estiver disponível.
Porta B: quando algum dos bits congurados como entrada altera seu valor.
Para gerenciar a interrupção, deve-se criar uma rotina que irá vericar qual foi o hardware
que gerou a interrupção e tomar as providências necessárias. A maneira de declarar que uma
determinada função será a responsável pelo tratamento da interrupção depende do compilador.
Para o compilador SDCC basta que coloquemos a expressão interrupt 1 após o nome da
função.
Para o compilador C18 da Microchip temos que gerar um código em assembler que indicará
qual função será a responsável pela interrupção.
#pragma
// I n d i c a r a posição no vetor de interrupções
#pragma
}
#pragma
code
interrupt NomeDaFuncao
A função que irá tratar da interrupção não retorna nem recebe nenhum valor.
if
paralela
9 ( BitTst ( PIR1 , 4 ) )
if
{ /* c ó d i g o */ } // F l a g de fim de transmissão da Serial
10 ( BitTst ( PIR1 , 5 ) )
if
{ /* c ó d i g o */ } // F l a g de recepção da Serial
11 ( BitTst ( PIR1 , 6 ) )
if
{ /* c ó d i g o */ } // F l a g de fim de conversão do AD
12 ( BitTst ( PIR1 , 7 ) ) { /* c ó d i g o */ } // F l a g de leitura /escrita da porta ←-
if
paralela
13 ( BitTst ( PIR2 , 0 ) )
if
{ /* c ó d i g o */ } // F l a g de comparação do CCP2
14 ( BitTst ( PIR2 , 1 ) )
if
{ /* c ó d i g o */ } // F l a g de overflow do TIMER3
15 ( BitTst ( PIR2 , 2 ) )
if
{ /* c ó d i g o */ } // F l a g de condição de Tensão A l t a / Baixa
16 ( BitTst ( PIR2 , 3 ) ) { /* c ó d i g o */ } // F l a g de detecção de colisão no ←-
if
barramento
17 ( BitTst ( PIR2 , 4 ) )
if
{ /* c ó d i g o */ } // F l a g de fim escrita na memória flash
18 ( BitTst ( PIR2 , 5 ) )
if
{ /* c ó d i g o */ } // F l a g de interrupção da USB
19 ( BitTst ( PIR2 , 6 ) ) { /* c ó d i g o */ } // F l a g de mudança na entrada de ←-
if
comparação
20 ( BitTst ( PIR2 , 7 ) )
if
{ /* c ó d i g o */ } // F l a g de falha no oscilador
21 ( BitTst ( INTCON , 0 ) )
if
{ /* c ó d i g o */ } // F l a g de mudança na PORTA B
22 ( BitTst ( INTCON , 1 ) )
if
{ /* c ó d i g o */ } // F l a g de interrupção externa INT0
23 ( BitTst ( INTCON , 2 ) )
if
{ /* c ó d i g o */ } // F l a g de overflow no TIMER0
24 ( BitTst ( INTCON3 , 0 ) )
if
{ /* c ó d i g o */ } // F l a g de interrupção externa INT1
25 ( BitTst ( INTCON3 , 1 ) ) { /* c ó d i g o */ } // F l a g de interrupção externa INT2
26 }
Existe uma correlação entre o número que vem depois da expressão interrupt para o com-
pilador SDCC e o número ao nal da expressão #pragma code high_vector para o C18. Estes
números representam a posição para a qual o microcontrolador vai quando acontece uma inter-
rupção. Estas posições estão numa área conhecida como vetor de interrupções.
Para o microcontrolador PIC 18f4550 este vetor possui três posições importantes: 0x00(0),
0x08(1) e 0x18(2). O compilador C18 usa a posição física e o SDCC o número entre parênteses.
A posição 0 (0x00) representa o endereço que o microcontrolador busca quando este acaba
de ser ligado. É a posição de reset. Geralmente saímos deste vetor e vamos direto para a função
main().
As posições 1 e 2 (0x08,0x18) são reservadas para as interrupções de alta e baixa prioridade,
respectivamente. É necessário que o programador escolha quais dispositivos são de alta e quais são
de baixa prioridade. Existe ainda um modo de compatibilidade com os microcontroladores mais
antigos no qual todos os periféricos são mapeados na primeira interrupção (0x08). Utilizaremos
este modo por questão de facilidade.
Como todos os periféricos estão mapeados na mesma interrupção, a função deve ser capaz de
diferenciar entre as diversas fontes de requisição. Uma maneira de se realizar esta vericação é
através das ags de controle, ou seja, bits que indicam a situação de cada periférico.
O programa 3.27 apresenta uma função que trata de todas as fontes possíveis de interrupção
para o PIC 18f4550.
Em geral não é necessário tratar todas as interrupções, apenas aquelas que inuenciarão
o sistema. O programa 3.28 apresenta um exemplo de uma função que trata as interrupções
advindas da porta B, do timer 0, da serial e do AD.
Para que a função apresentada no programa 3.28 funcione corretamente devemos inicializar
as interrupções de modo adequado, conforme apresentado no programa 3.29.
3
void main void
// i n í c i o do programa
4 ( )
5 {
6 unsigned int i
unsigned char temp
;
7 ;
8 TRISD =0 x00 ;
9 PORTD =0 x00 ;
10 BitSet ( WDTCON , 0 )
for
; // l i g a o sistema de watchdog
11 (;;)
12 {
13 PORTD ++;
14 for(i = 0; i < 10000; i++)
15 {
16 CLRWTD ( ) ;
17 }
18 }
19 }
3.13 Watchdog
Por algum motivo o software pode travar em algum ponto, seja por um loop innito ou por
esperar a resposta de algum componente através de pooling de uma variável.
A primeira condição pode ser evitada através de um projeto cuidadoso de software aliado a
uma boa validação. Já a segunda exige que os hardwares adjacentes funcionem corretamente.
Se algum hardware apresenta uma falha e não envia a resposta que o microcontrolador está
esperando, este último irá travar. Nestas situações é possível utilizar o watchdog.
O watchdog é um sistema que visa aumentar a segurança do projeto. Ele funciona como
um temporizador que precisa constantemente ser reiniciado. Caso não seja reiniciado no tempo
exigido, o watchdog reinicia o microcontrolador dando a possibilidade de sair de um loop innito
ou de um pooling sem resposta.
Para habilitar o watchdog é necessário alterar os registros de conguração, especicamente
o CONFIG2H (0x300002). Outro método consiste em deixar o watchdog desligado no registro e
ligá-lo através de software, como é apresentado no programa 3.30.
Notar o #dene criado na primeira linha do programa 3.30. A expressão CLRWDT é o
comando em assembler responsável por resetar o watchdog. As diretivas _asm e _endasm
informam ao compilador que os comandos utilizados devem ser transcritos exatamente iguais
para o arquivo assembler a ser gerado.
Se após ligar o watchdog não realizarmos a operação de reset dele, comentando ou excluindo a
função CLRWTD(), o sistema irá travar tão logo o tempo associado ao watchdog tenha expirado
pela primeira vez, reiniciando o sistema. Como apenas reiniciar não soluciona o problema, pois o
programa criado não terá função para reiniciar o watchdog, o sistema continua sendo reiniciado
indenidamente.
Arquitetura de desenvolvimento de
software
Constrained by memory limitations, performance requirements, and
physical and cost considerations, each embedded system design re-
quires a middleware platform tailored precisely to its needs, unused
features occupy precious memory space, while missing capabilities
must be tacked on. - Dr. Richard Soley
100
101 Arquitetura de desenvolvimento de software
Esta é a estratégia utilizada até agora nos exemplos apresentados. Dentro da função principal é
colocado um loop innito. Todas as tarefas são chamadas através de funções.
A vantagem de se utilizar esta abordagem é a facilidade de se iniciar um projeto. Para
sistemas maiores começa a car complicado coordenar as tarefas e garantir a execução num
tempo determinístico. Outro problema é a modicação/ampliação do software. Geralmente a
inserção de uma função no meio do loop pode gerar erros em outras funções devido a restrições
de tempo dos periféricos associados.
No exemplo acima, a inserção da comunicação serial e os cálculos podem atrapalhar a escrita
no display de sete segmentos, gerando icker.
Uma parte dos desenvolvedores de sistemas embarcados, que possuem restrições de tempo de
atendimento mais rigorosos, optam por garantir estas restrições através de interrupções.
Na maioria dos sistemas microcontroladores, as interrupções são atendidas num tempo muito
curto, cerca de alguns ciclos de instrução, o que para a maioria dos sistemas é suciente. Deve-se,
entretanto, tomar cuidado com a quantidade de periféricos que geram interrupções e a prioridade
dada a cada um deles.
Outra abordagem muito utilizada é a geração de uma interrupção com tempo xo, por exem-
plo a cada 5ms.
A grande vantagem da abordagem citada é que a inserção de mais código dentro do loop
principal não atrapalha a velocidade com que o display é atualizado, que está xo em 5(ms).
Inicio
Ler Atualiza
Teclado Display
Atualiza Escreve
Display Serial
Atualiza
Ler Serial Display
Nota-se que após a fase de inicialização o sistema entra num ciclo, como na abordagem one-
single-loop. Outra peculiaridade é que algumas tarefas podem ser executadas mais de uma vez
para garantir as restrições de tempo. No exemplo a tarefa de atualização dos displays é executada
três vezes.
A transposição de uma máquina de estado para o código em C é realizada através de um
switch-case.
É possível retirar todas as atribuições para a variável slot e colocar no slot-bottom a ex-
pressão slot++. A abordagem apresentada foi escolhida por aumentar a robustez do sistema, já
que a variável slot controla todo o uxo do programa.
A inserção de uma nova tarefa é realizada de maneira simples, basta adicionar outro slot, ou
seja, basta inserir um case/break com a tarefa desejada.
Como a máquina está dentro do loop innito, a cada vez que o programa passar pelo case,
ele executará apenas um slot. Esta abordagem gera ainda outro efeito. Como pode ser visto
no código, naturalmente surgem duas regiões: top-slot e bottom-slot. Se algum código for
colocado nesta região ele será executado toda vez, de modo intercalado, entre os slots. Pela
Figura 4.1, percebemos que é exatamente este o comportamento que queremos para a função
AtualizaDisplay(). Deste modo, podemos remodelar o código fazendo esta alteração.
13
switch
// * * * * * * * * * * * início da máquina de estado ************
14 ( slot ) {
15 case 0:
16 ProcessaTeclado ( ) ;
17 slot = 1 ;
18 break
case
;
19 1:
20 AtualizaDisplay ( ) ;
21 slot = 2 ;
22 break
case
;
23 2:
24 RecebeSerial ( ) ;
25 slot = 3 ;
26 break
case
;
27 3:
28 AtualizaDisplay ( ) ;
29 slot = 4 ;
30 break
case
;
31 4:
32 EnviaSerial ( ) ;
33 slot = 5 ;
34 break
case
;
35 5:
36 AtualizaDisplay ( ) ;
37 slot = 0 ;
38 break
default
;
39 :
40 slot
break
= 0;
41 ;
42 }
43 // * * * * * * * * * * * * fim da máquina de estado **************
16
switch
// * * * * * * * * * * * início da máquina de estado ************
17 ( slot )
18 {
19 case 0:
20 ProcessaTeclado ( ) ;
21 slot = 1 ;
22 break
case
;
23 1:
24 RecebeSerial ( ) ;
25 slot = 2 ;
26 break
case
;
27 2:
28 EnviaSerial ( ) ;
29 slot = 0 ;
30 break
default
;
31 :
32 slot
break
= 0;
33 ;
34 }
35 // * * * * * * * * * * * * fim da máquina de estado **************
16
switch
// * * * * * * * * * * * início da máquina de estado ************
17 ( slot )
18 {
19 case 0:
20 ProcessaTeclado ( ) ;
21 slot = 1 ;
22 break
case
;
23 1:
24 RecebeSerial ( ) ;
25 slot = 2 ;
26 break
case
;
27 2:
28 EnviaSerial ( ) ;
29 slot = 0 ;
30 break
default
;
31 :
32 slot
break
= 0;
33 ;
34 }
35 // * * * * * * * * * * * * fim da máquina de estado **************
tempo. Notar que o slot 1 (S.1) gasta um tempo de 2.0(ms), o slot 2 de 3.1 (ms) e o slot 3 apenas
1.2 (ms). Já o top-slot consome 0.5 (ms) e o bottom-slot 0.3 (ms).
Top
S.1
S.2
S.3
Bottom
"vago"
0 5 10 15 20 25 30
Podemos notar que para o ciclo do primeiro slot são gastos 0.5+2.0+0.3 = 2.8(ms). Deste
modo o sistema ca aguardando na função AguardaTimer() durante 2.2 (ms) sem realizar
nenhum processamento útil. Para o segundo slot temos um tempo "livre"de 5-(0.5+3.1+0.3)=1.1
(ms). O terceiro slot é o que menos consome tempo de processamento, possuindo um tempo livre
de 5-(0.5+1.2+0.3)=3.0 (ms).
Top 1 1 1
S.1 3 3 3
Bottom 1 1 1
"vago" 3 3 3
Top 1 1
S.1 1 2 3 3
Bottom 1 1 1
"vago" 2 2 2
Interr. 1 1 1
Cada interrupção gasta um tempo de 1 (ms) conforme pode ser visto na Figura 4.4. Como
temos um tempo vago de 3 (ms) em cada ciclo basta garantir que os eventos que geram a
interrupção não ultrapassem a frequência de 3 eventos a cada 8 (ms).
Anexos
110
111 Anexos
10 // p a r a o compilador C18
11 //#pragma config FOSC = HS // Oscilador c/ cristal externo HS
12 //#pragma config CPUDIV = OSC1_PLL2 // Pll desligado
13 //#pragma c o n f i g WDT = OFF // Watchdog controlado por software
14 //#pragma config LVP = OFF // Sem programação em baixa t e n s ã o \\\ h l i n e
5.1 cong.h
5.2 basico.h
O header basico.h possui o endereço de todos os registros do microcontrolador PIC 18f4550 que
é utilizado nesta apostila. Além disso contém alguns dene's importantes como as funções inline
para limpar a ag de watchdog e para manipulação de bits.
Os passos a seguir devem ser seguidos para instalar os device drivers corretamente em sistemas
operacionais de 64 bits. Atualmente apenas os seguintes aparelhos são suportados:
MPLAB REAL ICE in-circuit emulator
MPLAB ICD 3
Passo 1
Conecte o dispositivo ao PC usando o cabo USB. Para os dispositivos que exigem alimentação
externa, ligue-a. Se estiver usando um hub USB, tenha certeza que este possui energia suciente
para alimentar o dispositivo.
Passo 2
A primeira vez que o dispositivo é conectado aparece uma mensagem indicando que o sistema
encontrou um novo hardware. Quando aparecer uma janela, escolha a opção Localizar e instalar
o driver (recomendado).
Nota: Se aparecer uma mensagem perguntando sobre permissão no Windows 7, clique em sim/-
continuar.
Passo 3
Escolha a opção: Procurar o driver no meu computador (avançado)
Passo 4
Quando aparecer uma janela pedindo para você indicar o caminho, procure em C:\Arquivos
de programa (x86)\Microchip\MPLAB IDE\Drivers64. Clique em continuar
Passo 5
A próxima tela irá perguntar se você quer continuar a instalar o dispositivo. Clique em Instalar
para continuar.
Passo 6
A próxima tela indicará que o software foi instalado corretamente. Clique em fechar para termi-
nar a instalação.
Passo 7
Vericar se o driver está instalado e visível no Gerenciador de dispositivos em Custom USB
Drivers>Microchip Custom USB Driver Abra a janela do gerenciador de dispositivos (Iniciar-
>Painel de controle->Sistema->Gerenciador de dispositivos). Se o driver não fora instalado
corretamente, continue na seção de solução de erros (a seguir)
Solução de erros
Se houve algum problema na instalação do driver siga os passos a seguir.
O Windows tentará instalar o driver mesmo se não encontrar o arquivo correto. No gerenci-
ador de dispositivos dentro da opção Outros dispositivos você deve encontrar um Dispositivo
não conhecido.
Clique com o botão direito no Dispositivo não conhecido e selecione a opção Atualizar o
Driver do menu.
Na primeira tela de diálogo selecione Procurar no meu computador pelos arquivos do driver.
Continuar a partir do passo 4.