Академический Документы
Профессиональный Документы
Культура Документы
AUTOR
ii Sumário
Parte I
Programação Embarcada
Capítulo 1
3
Introdução
1.1 Hardware utilizado 4
1.2 Ambiente de programação 4
Capítulo 2
5
Linguagem C
2.1 Indentação e padrão de escrita 6
2.2 Comentários 8
2.3 Arquivos .c e .h 8
2.4 Diretivas de compilação 9
2.5 Tipos de dados em C 13
2.6 Operações aritméticas 16
2.7 Função main() 18
2.8 Ponteiros e endereços de memória 21
Capítulo 3
23
Operações com bits
3.1 NOT 23
3.2 AND 24
3.3 OR 25
3.4 XOR 25
3.5 Shift 26
3.6 Ligar um bit (bit set) 27
3.7 Desligar um bit (bit clear) 27
3.8 Trocar o valor de um bit (bit flip) 28
3.9 Verificar o estado de um bit (bit test) 29
3.10 Criando funções através de define’s 30
Capítulo 4
33
Debug de sistemas embarcados
4.1 Externalizar as informações 33
4.2 Programação incremental 34
4.3 Checar possíveis pontos de memory-leak 34
4.4 Cuidado com a fragmentação da memória 34
4.5 Otimização de código 35
4.6 Reproduzir e isolar o erro 35
ii
Parte II
Arquitetura de microcontroladores
Capítulo 5
37
Microcontrolador
5.1 Acesso à memória 39
5.2 Clock e tempo de instrução 40
5.3 Registros de configuração do microcontrolador 41
Capítulo 6
43
Esquema elétrico e circuitos importantes
6.1 Multiplexação nos terminais do microcontrolador 44
Parte III
Programação dos Periféricos
Capítulo 7
46
Portas de E/S
7.1 Acesso às portas do microcontrolador 47
7.2 Configuração dos periféricos 48
Capítulo 8
51
Barramento de Leds
Capítulo 9
53
Display de 7 segmentos
9.1 Multiplexação de displays 55
9.2 Criação da biblioteca 56
Capítulo 10
58
Leitura de teclas
10.1 Debounce por software 60
10.2 Arranjo de leitura por matriz 61
10.3 Usando tristate ou terminais como entrada 62
10.4 Criação da biblioteca 63
Capítulo 11
65
Display LCD 2x16
11.1 Criação da biblioteca 70
Capítulo 12
72
Comunicação serial
12.1 I2C 73
12.2 RS 232 77
12.3 Criação da biblioteca 80
Capítulo 13
82
Conversor AD
13.1 Elementos sensores 82
13.2 Processo de conversão AD 85
13.3 Criação da biblioteca 86
Capítulo 14
88
Saídas PWM
14.1 Criação da biblioteca 89
Capítulo 15
92
Timer
15.1 Reprodução de Sons 94
Capítulo 16
96
Interrupção
Capítulo 17
100
Watchdog
Parte IV
Arquitetura de desenvolvimento de software
Capítulo 18
103
One single loop
Capítulo 19
105
Interrupt control system
Capítulo 20
107
Cooperative multitasking
20.1 Fixação de tempo para execução dos slots 111
20.2 Utilização do tempo livre para interrupções 113
Capítulo 21
114
Anexos
Programação Embarcada
Parte I
1 * Introdução, 3
2 * Linguagem C, 5
2
C APÍTULO
1
Introdução
1.1 Hardware utilizado 4
1.2 Ambiente de programação 4
Instalação 4
Programação para sistemas embarcados exige uma série de cuidados especiais, pois estes sis-
temas geralmente possuem restrições de memória e processamento. Por se tratar de sistemas com
funções específicas, 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 micropro-
cessador possui uma arquitetura diferente, com quantidade e tipos de instruções diversos. Progra-
madores voltados para desktops não precisam se ater tanto a estes itens, pois eles programam 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
3
Uma opção para se obter “artificialmente” esta camada de abstração que era gerada pelo sistema
operacional é a utilização de dois itens: um compilador próprio para o componente em questão
e uma biblioteca de funções. O compilador será o responsável por traduzir a linguagem de alto
nível em uma linguagem que o microcontrolador consegue entender. A biblioteca de funções, ou
framework, em geral, é disponibilizada pelos fabricantes do microcontrolador.
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:
• 1 display LCD 2 linhas por 16 caracteres (compatível com HD77480)
• 4 displays de 7 segmentos com barramento de dados compartilhados
• 8 leds ligados ao mesmo barramento dos displays
• 16 mini switches organizadas em formato matricial 4x4
• 1 sensor de temperatura LM35C
• 1 resistência de aquecimento ligada a uma saída PWM
• 1 motor DC tipo ventilador ligado a uma saída PWM
• 1 buzzer ligado a uma saída PWM
• 1 canal de comunicação serial padrão RS-232
Cada componente terá seu funcionamento básico explicado para permitir o desenvolvimento de
rotinas para estes.
Instalação
2
Linguagem C
2.1 Indentação e padrão de escrita 6
2.2 Comentários 8
2.3 Arquivos .c e .h 8
2.4 Diretivas de compilação 9
#include 9
#define 9
#ifdef, #ifndef, #else e #endif 10
2.5 Tipos de dados em C 13
Representação binária e hexadecimal 13
Modificadores de tamanho e sinal 14
Modificadores de acesso 14
Modificadores de posicionamento 16
Modificador de persistência 16
2.6 Operações aritméticas 16
2.7 Função main() 18
Rotinas de tempo 19
2.8 Ponteiros e endereços de memória 21
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.
Em termos de utilização geral, segundo o índice TIOBE, é a segunda linguagem mais utilizada
atualmente, conforme Figura 2.1.
5
Também é a escolha da maioria dos programadores e gerentes de projetos no que concerne ao
desenvolvimento de sistemas embarcados como pode ser visto na Figura 2.2.
Figura 2.2: Pesquisa sobre linguagens utilizadas para projetos de software embarcado
Fonte: http://www.embedded.com/design/218600142
Podemos notar pelo código anterior que aquele que possui indentação facilita na verificaçã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: “kpInit()”, “ParaSistema()”.
Tags de definições (utilizados em conjunto com a diretiva #define) serão grafados exclusivamente
em maiúsculo: “NUMERODEVOLTAS”, “CONSTGRAVITACIONAL”.
Cada chave será colocada numa única linha, conforme exemplo anterior, evitando-se construções
do tipo:
Ou
1 if (PORTA == 0x30){
2 PORTB = 0x10;}
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 especifica um estilo para ser usado.
Comentários 2.2
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 final do comentário.
1 #include <stdio.h>
2 #define DIST 260 // distancia entre SP e Ita
3 int main(int argc, char* argv[]){
4 /∗ esse programa serve para
5 mostrar como se insere comentários ∗/
6 printf ("São Paulo está a %d Km de Itajubá", DIST);
7 return 0;
8 }
Arquivos .c e .h 2.3
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,
definimos 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 é recomendado
criar funções específicas para tal finalidade.
O programa 2.1 apresenta um exemplo de um arquivo de código “.c” e o programa 2.2 apresenta
o respectivo arquivo de header “.h”.
Podemos notar que no arquivo “.h” a função ssdUpdate() 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 suficiente no stack.
Código 2.1: Resumo do ssd.c
1 //variável usada apenas dentro deste arquivo
2 static char temp;
3 //variável que será usada também fora do arquivo
4 static char valor;
5 //funções usadas dentro e fora do arquivo
6 void MudaDigito(char val){
7 valor = val;
8 }
9 char LerDigito(void){
10 return valor;
11 }
12 void ssdInit(void){
13 //código da função
14 }
15 //função usada apenas dentro deste arquivo
16 void ssdUpdate(void){
17 //código da função
18 }
As diretivas de compilação são instruções que são dadas ao compilador. Elas não serão execu-
tadas. 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 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”.
#define
Outra diretiva muito conhecida é a #define. Geralmente é utilizada para definir uma constante,
mas pode ser utilizada para que o código fonte seja modificado antes de ser compilado.
Original Compilado Resultado na Tela
1 #define CONST 15
1 void main(void){
2 void main(void){
2 printf("%d", 15 * 3); 1 45
3 printf("%d", CONST * 3);
3 }
4 }
1 void MostraSaidaPadrao(){
2 #ifdef PADRAO Serial
1 #include <stdio.h>
3 char * msg = "SERIAL";
2 #define PADRAO Serial
4 #else
3 void main(void){ 1 SERIAL
5 char * msg = "LCD";
4 MostraSaidaPadrao();
6 #endif
5 }
7 printf(msg);
8 }
1 #include <stdio.h>
2 #define PADRAO LCD
3 void main(void){ 1 LCD
4 MostraSaidaPadrao();
5 }
Pelo código apresentado percebemos que a mesma função MostraSaidaPadrao(), apresenta resulta-
dos diferentes dependendo de como foi definida a opção PADRAO.
Os define’s também ajudam a facilitar a localização dos dispositivos e ajustar as configuraçõ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 está dentro do endereço 0xF83. Para facilitar este procedimento, é definido um ponteiro para
este endereço e rotulado com o nome PORTD. Definir OFF como 0 e ON como 1 facilita a leitura
do código.
As diretivas #ifdef, #ifndef, #else e #endif são muito utilizadas quando queremos gerar dois
programas que diferem apenas num pequeno pedaço de código. Por exemplo dois sistemas de
controle de temperatura. O primeiro possui um display de LCD, capaz de mostrar a temperatura
textualmente. O segundo sistema executa a mesma função que o primeiro, mas é um dispositivo
mais barato, portanto possui apenas um led indicativo de sobretemperatura. O código pode ser
escrito da seguinte maneira:
6 led = 1;
7 }else{
8 led = 0;
9 }
10 #endif //LCD
11 }
No momento da compilação o pré-compilador irá verificar se a “tag” LCD foi definida em algum
lugar. Em caso positivo o pré-compilador irá deixar tudo que estiver entre o #ifdef e o #else e retirará
tudo que está entre o #else e o #endif.
Outra função muito utilizada destas diretivas é para evitar a referência circular. Supondo dois
arquivos, um responsável pela comunicação serial (serial.h) e o segundo responsável pelo controle
de temperatura (temp.h). O projeto exige que a temperatura possa ser controlada pela porta serial
e toda vez que a temperatura passar de um determinado patamar deve ser enviado um alerta pela
porta serial. O arquivo da porta serial (serial.h) tem as seguintes funções, apresentadas a seguir.
1 char LerSerial(void);
2 void serialSend(char val);
1 char LerTemperatura(void);
2 void AjustaCalor(char val);
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 serialSend() com o código 0x30. Para isso o arquivo temp.h
deve incluir o arquivo serial.h.
1 #include "serial.h"
2 char LerTemperatura(void);
3 void AjustaCalor(char val);
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
1 #include "temp.h"
2 char LerSerial(void);
3 void serialSend(char val);
O problema é que deste modo é criada uma referência circular sem fim: 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 2.3.
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 2.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 definida. 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
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);
o arquivo for lido, a tag “TAG_CONTROLE” já estará definida impedindo assim que o processo
cíclico continue, conforme pode ser visto na Figura 2.4.
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
Geralmente se utiliza como tag de controle o nome do arquivo. Esta tag deve ser única para
cada arquivo.
Tipos de dados em C 2.5
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 C
são apresentados na Tabela 2.1.
Tabela 2.1: Tipos de dados e faixa de valores
Tipo Bits Bytes Faixa de valores
char 8 1 -128 à 127
int 16 2 -32.768 à 32.767
float 32 4 3,4 x 10-38 à 3,4 x 1038
double 64 8 3,4 x 10-308 à 3,4 x 10308
Podemos notar que as variáveis que possuem maior tamanho podem armazenar valores maiores.
Notamos também que apenas os tipos float e double possuem casas decimais.
A grande maioria dos processadores trabalha com dados binários, ou seja, aqueles que apenas
assumem valores 0 ou 1. Por isso os tipos apresentados anteriormente podem ser representados
utilizando a base 2. Um valor do tipo char que possui 8 bits será representado por um número de 8
algarismos, todos 0 (zeros) ou 1 (uns). Para realizarmos a conversão de um número na base decimal
para a base 2 podemos seguir o seguinte algoritmo:
1. Dividir o número por 2
2. Anotar o valor do resto (0 ou 1)
3. Se o valor é maior que 0 voltar ao número 1
4. Escrever os valores obtidos através do passo 2 de trás para frente.
5. Apresentar o resultado
Por exemplo o número 18.
18/2 = 9, resto 0
9/2 = 4, resto 1
4/2 = 2, resto 0
2/2 = 1, resto 0
1/2 = 0, resto 1
Lendo do último resultado para o primeiro temos que
1810 = 100102
Devido à grande utilização de números binários na programação de baixo nível é muito comum
escrevermos estes números na base 16 ou hexadecimal. A vantagem de escrever o número nesta
base é que existe uma conversão simples de binário para hexadecimal e o número resultante ocupa
bem menos espaço na tela.
A base hexadecimal possui 16 "unidades"diferentes. Como existem apenas 10 algarismos no
sistema de numeração arábico (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) utilizamos 6 letras para complementá-los
(A, B, C, D, E, F). A conversão, entre valores binários, decimais e hexadecimais, é apresentada na
Tabela 2.2.
Para converter de binário para hexadecimal basta dividir o número em grupos de 4 em 4, da
direita para a esquerda, e utilizar a tabela acima.
Por exemplo o número 18. Sabemos que este número em binário é representado por 100102 .
Separando o número de 4 em 4 algarismos temos:
1-0010
Pela tabela:
Tabela 2.2: Representação decimal – binária - hexadecimal
Decimal Binário Hexadecimal Decimal Binário Hexadecimal
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
12 = 116
00102 = 216 .
Logo:
100102 . = 1216 .
Um modificador de tipo altera o significado dos tipos base e produz um novo tipo. Existem
quatro tipos de modificadores, dois para o tamanho (long e short) e dois para sinal (unsigned e
signed). Um tipo declarado com o modificador long pode ter tamanho MAIOR ou IGUAL ao tipo
original. Um tipo declarado como short deve ter tamanho MENOR ou IGUAL ao tipo original. A
decisão cabe ao compilador utilizado.
Os tipos declarados como signed possuem um bit reservado para o sinal. Deste modo o valor
máximo que podem atingir é menor. Os tipos declarados como unsigned não podem assumir valo-
res negativos, em compensação podem atingir o dobro do valor de um tipo signed. Na Tabela 2.3
são apresentadas algumas variações possíveis.
Tabela 2.3: Alteração de tamanho e sinal dos tipos básicos
Tipo Bytes Excursão máxima
unsigned char 1 0 à 255
signed char 1 -128 à 127
unsigned int 2 0 à 65.535
signed int 2 -32.768 à 32.767
long int 4 -2.147.483.648 à 2.147.483.647
unsigned long int 4 0 à 4.294.967.295
short int 2 -32.768 à 32.767
Na linguagem C, por padrão, os tipos são sinalizados, ou seja, possuem parte positiva e negativa.
Por isso é raro encontrar o modificador signed.
Modificadores 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 fixos. Por
exemplo o código abaixo:
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 final. 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 fluxo 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 modificador para
indicar que a variável representa um local que apenas pode ser lido e não modificado, por exemplo
uma porta para entrada de dados. Nesta situação é comum utilizar as palavras volatile e const
juntas.
Modificadores de posicionamento
As variáveis podem ser declaradas utilizando os modificadores near e far. Estes modificadores
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.
Modificador 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 modificador de persistência: static. Com esse
modificador a variável passa a possuir um endereço fixo 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 flutuante é um ponto flutuante. 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
flutuante 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 float.
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 flutuante.
1 float x = 0.1;
2 while (x != 1.1) {
3 printf("x = %f\n", x);
4 x = x + 0.1;
5 }
O trecho de código acima apresenta um loop infinito. Como existem restrições de precisão nos
números de ponto flutuante (float e double) nem todos os números são representados fielmente. 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 flutuante utilizar maior, menor
ou variações.
1 float x = 0.1;
2 while (x < 1.1) {
3 printf("x = %f\n", x);
4 x = x + 0.1;
5 }
Apesar de sutis estes tipos de erro podem causar um mau funcionamento do sistema. Na
Figura 2.5 é apresentado um erro gerado através de um loop infinito.
Todo sistema precisa ser iniciado 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 2.6).
A maneira de indicar o ponto de início de um programa depende do compilador. Em geral os
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.
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 ...
Figura 2.6: Exemplo de funcionamento do vetor de interrupção
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 infinito. Abaixo estão as duas alternativas mais utilizadas.
Rotinas de tempo
É muito comum necessitar que o microcontrolador fique um tempo sem fazer nada. Uma ma-
neira de atingir esse objetivo é utilizar um laço FOR2 .
1 Em sistemas mais complexos algumas tarefas são executadas independentemente da função principal, tendo sua exe-
Notar que não estamos utilizando as chaves ao final do loop for. 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.
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.
1 unsigned char i, j, k;
2 for(i=0; i < 34; i++){ //3 + 34 ∗ (30.003 + 3) = 1.020.207 instruções
3 for(j=0; j < 100; j++){ //3 + 100 ∗ (297 + 3) = 30.003 instruções
4 for(k=0; k < 98; k++); // 3 + 98 ∗ (3) = 297 instruções
5 }
6 }
O código acima foi projetado para gerar um atraso de tempo de meio segundo. Compilando e
realizando testes práticos podemos confirmar 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 con-
trole. 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 final,
poupando espaço na memória.
A seguir apresentamos um exemplo de função que gera delays com tempo parametrizado.
1
2 void delay(unsigned int DL){
3 unsigned char i, j, k;
4 while(DL--){ //executa DL vezes.
5 for(i=0; i < 34; i++){ //3 + 34 ∗ (30.003 + 3) = 1.020.207 instruções
6 for(j=0; j < 100; j++){ //3 + 100 ∗ (297 + 3) = 30.003 instruções
7 for(k=0; k < 98; k++); // 3 + 98 ∗ (3) = 297 instruções
8 }
9 }
3 Este valor só é válido quando estamos trabalhando com variáveis char. Se utilizarmos variáveis int o código em
Toda variável criada é armazenada em algum lugar da memória. Este lugar é definido 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:
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 definir 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:
1 int *apint;
2 float *apfloat;
Deve-se tomar cuidado, pois nos exemplos acima, apint e apfloat são variáveis que armazenam
endereços de memória e não valores tipo int ou float. O lugar APONTADO pela variável apint é
que armazena um inteiro, do mesmo modo que o lugar apontado por apfloat armazena um valor
fracionário.
Se quisermos manipular o valor do endereço utilizaremos apint e apfloat mas se quisermos
manipular o valor que esta dentro deste endereço devemos usar um asterisco antes do nome da
variável. Exemplo:
1 apfloat = 3.2;
2 *apfloat = 3.2;
A primeira instrução indica ao compilador que queremos que o ponteiro apfloat 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 apfloat devemos utilizar a segunda expressão.
Para trabalhar com ponteiros é preciso muito cuidado. Ao ser definido, um ponteiro tem como
conteúdo não um endereço, mas algo indefinido. 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 real-
mente 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á defi-
nida:
Com sistemas embarcados existem alguns endereços de memória que possuem características
especiais. Estes endereços possuem registros de configuraçã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.
C APÍTULO
3
Operações com bits
3.1 NOT 23
3.2 AND 24
3.3 OR 25
3.4 XOR 25
3.5 Shift 26
3.6 Ligar um bit (bit set) 27
3.7 Desligar um bit (bit clear) 27
3.8 Trocar o valor de um bit (bit flip) 28
3.9 Verificar o estado de um bit (bit test) 29
3.10 Criando funções através de define’s 30
Nos sistemas microcontrolados, existem algumas variáveis onde cada bit tem uma interpretação
ou funcionalidade diferente. Por isso é necessário realizar algumas operações que modifiquem
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 todo
enquanto a bitwise opera bit à bit. Lembrando que para linguagem C uma variável com valor 0
(zero) representa falso, e qualquer outro valor representa verdadeiro.
NOT 3.1
A operação NOT lógica retorna ’1’ (um) se o valor for ’0’ (zero) e ’0’ se o valor for ’1’.
23
A !A
0 1
1 0
A operação bitwise NOT (operador ˜) executa uma NOT lógica. Isso significa 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.
1 result = ~A;
1 char A = 12; 1 result = !A; 2 // result = 243
2 // A = 0b00001100 2 // result = 0 3 // A = 0b00001100
4 // r = 0b11110011
AND 3.2
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:
1 result = A & B;
1 char A = 8;
2 // result = 0
2 // A = 0b00001000 1 result = A && B;
3 // A = 0b00001000
3 char B = 5; 2 // result = 1
4 // B = 0b00000101
4 // B = 0b00000101
5 // r = 0b00000000
OR 3.3
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:
1 result = A | B;
1 char A = 8;
2 // result = 13
2 // A = 0b00001000 1 result = A || B;
3 // A = 0b00001000
3 char B = 5; 2 // result = 1
4 // B = 0b00000101
4 // B = 0b00000101
5 // r = 0b00001101
XOR 3.4
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:
1 result = A ^ B;
1 char A = 8;
2 // result = 13
2 // A = 0b00001000
1 // não existe em C 3 // A = 0b00001000
3 char B = 5;
4 // B = 0b00000101
4 // B = 0b00000101
5 // r = 0b00001101
Shift 3.5
A operação shift desloca os bits para a esquerda (operador <<) ou direita (operador >>). É
necessário indicar quantas casas serão deslocadas.
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 fixo ou flutuante nem
variáveis sinalizadas (signed).
Em diversas ocasiões é necessário que trabalhemos com os bits de maneira individual, princi-
palmente 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.
Ligar um bit (bit set) 3.6
Para ligar apenas um bit, utilizaremos uma operação OU. Supondo dois operandos A e B. Se A
é 1 o resultado de (A | B) é 1 independente de B. Se A é 0 o resultado é igual ao valor de B.
Se o objetivo é ligar apenas o bit da posição X devemos criar um valor onde todas as posições
são 0's com exceção da posição desejada. Para uma máscara binária de N bits temos (N>=X):
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
Para desligar apenas um bit o procedimento é similar ao utilizado para ligar. Ao invés de utili-
zarmos uma operação OU, utilizaremos uma operação AND. A operação AND tem a característica
de, dados A e B valores binários, se A é 1, a resposta de (A & B) será o próprio valor de B, se a A=0,
a resposta é zero, independente de B.
Novamente é necessário gerar uma máscara. Mas para esta situação ela deve possuir todos os
bits iguais a 1 com exceção de X, o bit que queremos desligar.
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.
É 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.
Para trocar o valor de um bit utilizaremos como artifício algébrico a operação XOR. Dado duas
variáveis binárias A e B , se A é 1, o valor resultante de A XOR B é o oposto do valor de B, se A=0,
a resposta se mantém igual ao valor de B.
Podemos perceber que para trocar o valor de apenas um bit a máscara será idêntica àquela
utilizada para ligar um bit:
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
Para verificar se o bit X está com o valor 1 utilizaremos novamente a mesma máscara utilizada
para bit set e bit toggle:
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 01 .
Exemplo: Testar o bit 2 da variável PORTD
1A maioria dos compiladores C adotam uma variável com valor diferente de zero como sendo verdadeiro.
Criando funções através de define’s 3.10
Uma opção no uso de define’s é criar funções simples que podem ser escritas em apenas uma
linha. Utilizando um pouco de algebrismo e parênteses, é possível escrever as quatro operações
anteriores numa única linha. De posse desta simplificação podemos criar uma função para facilitar
o uso destas operações através de um define conforme podemos ver nas tabelas 3.1, 3.2, 3.3 e 3.4.
Tabela 3.1: Operação bit set com define
1 char bit = 2;
2 char mascara;
3 mascara = 1 << bit;
4 arg = arg | mascara;
Passo a Passo 5 //em 1 linha
6 arg = arg | (1<<bit);
7 //ou
8 arg |= (1<<bit);
1 char bit = 2;
2 char mascara;
3 mascara = 1 << bit;
4 arg = arg & ~mascara;
Passo a Passo 5 //em 1 linha
6 arg = arg & ~(1<<bit);
7 //ou
8 arg &= ~(1<<bit);
1 char bit = 2;
2 char mascara;
3 mascara = 1 << bit;
4 arg = arg ^ mascara;
Passo a Passo 5 //em 1 linha
6 arg = arg ^ (1<<bit);
7 //ou
8 arg ^= (1<<bit);
1 char bit = 2;
2 char mascara;
3 mascara = 1 << bit;
Passo a Passo 4 if (arg & mascara)
5 //em 1 linha
6 if (arg & (1<<bit))
4
Debug de sistemas embarcados1
4.1 Externalizar as informações 33
4.2 Programação incremental 34
4.3 Checar possíveis pontos de memory-leak 34
4.4 Cuidado com a fragmentação da memória 34
4.5 Otimização de código 35
4.6 Reproduzir e isolar o erro 35
“If the code and the comments disagree, then both are
probably wrong.”
Norm Schryer
A verificação de sistemas embarcados apresenta algumas restrições e de modo geral não é pos-
sí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 advin-
dos 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.
1 #include "stdio.h"
2 #include "serial.h"
3 //início do programa
4 int main(int argc, char* argv[]) {
5 printf("Inicializando sistema");
6 if (CheckForData()) {
7 printf("Chegou informacao");
1 Mais informações sobre debug de sistemas embarcados referir ao artigo “The ten secrets of embedded debugging” de
33
8 } else {
9 printf("Problemas na comunicacao");
10 }
11 return 0;
12 }
Devemos ter em mente onde é necessário colocar estes alertas e lembrar de retirá-los do código
final.
Para a placa em questão utilizaremos o barramento de leds que está ligado à porta D. A operaçã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 configurar a porta
D. Isto é feito iniciando a variável TRISD com o valor 0x002 .
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”.
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 fica mais difícil saber onde ele está.
Se for necessário realizar alocação dinâmica, garantir que todas as alocações sejam liberadas em
algum ponto do programa.
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
profiler 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.
Quando houver algum erro deve-se primeiro entender como reproduzi-lo. Não é possível tentar
corrigir o erro se não houver maneira de verificar se ele foi eliminado.
No momento em que se consegue um procedimento de como reproduzir o erro podemos co-
meçar a visualizar onde ele pode estar. A partir deste momento devemos isolar onde o erro está
acontecendo. Uma maneira de se fazer isto em sistemas embarcados é colocar um loop infinito
dentro de um teste, que visa verificar alguma condição de anomalia. Se o sistema entrar neste teste
devemos sinalizar através dos meios disponíveis, ligar/desligar algum led por exemplo.
5 * Microcontrolador, 37
36
C APÍTULO
5
Microcontrolador
5.1 Acesso à memória 39
5.2 Clock e tempo de instrução 40
5.3 Registros de configuração do microcontrolador 41
37
Figura 5.1: Arquitetura do microcontrolador PIC 18f4520
Acesso à memória 5.1
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 5.2).
Suporte Existe
número: gaveta?
1 sim
2 sim
3 não
4 não
5 sim
6 não
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 definidos pelo clock do sistema. O clock define 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 tare-
fas. Por exemplo a soma.
A soma de números inteiros é feita de maneira direta enquanto para somar dois números fraci-
onários, que estão em notação científica1 , é necessário igualar as potências antes de realizar a soma.
Por este motivo a segunda operação é mais demorada que a primeira.
Exemplo:
1 Números fracionários podem ser armazenados de dois modos no ambiente digital. O modo mais comum é o ponto
1 A = 1.23456 x 10 ^ 5
2 B = 3.4567 x 10 ^ 4
3 C = A x B
1 A = 123456; 4 //C = 4.267503552 x 10 ^9
2 B = 34567; 5
3 C = A x B; 6 //1. Converter para o mesmo expoente
4 //C = 4267503552 7 // 12.3456 x 10 ^ 4
5 8 // 3.4567 x 10 ^ 4
6 //1. Multiplicar os números 9 //2. Multiplicar os números
7 // 123456 10 //e somar a mantissa
8 // ∗ 34567 11 // 12.3456 x 10 ^ 4
9 // 4267503552 12 // x 3.4567 x 10 ^ 4
13 // 42.67503552 x 10 ^ 8
14 //3. Corrigir quantidade de casas dec.
15 // 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 5.1.
Tabela 5.1: Quantidade de operações e tarefas
Operação com: Quantidade Total de tarefas
1 tarefa 104 104
2 tarefas 63 126
3 tarefas 21 63
4 tarefas 12 48
Total 200 341
O total de tarefas a serem realizadas é de 341 tarefas por milissegundo. Isso dá uma quantidade
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.
A maioria dos terminais dos microcontroladores podem ser configurados para trabalhar de
diversas maneiras. Esta configuração é realizada através de registros especiais. Estes registros são
posições de memória pré-definidas pelo fabricante. Para conhecer quais são e o que fazem é preciso
recorrer ao datasheet do componente.
Além dos registros de configuração dos terminais, existem registros que indicam como o mi-
crocontrolador deve operar. O microcontrolador PIC 18f4520 possui dez registros que controlam
seu modo de operação, velocidade, modo de gravação, etc. Estes registros são apresentados na
Figura 5.5.
Dos registros apresentados na Figura 5.5, quatro precisam necessariamente ser configurados
para que o sistema possa funcionar. Dois deles tem relação com a configuração do sistema de clock:
um especifica 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 configurar 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 correta-
Figura 5.5: Registros de configuração do microcontrolador PIC 18f4520
mente, o programa deve ser preparado para tal finalidade. Ele será explicado em detalhes na seção
17 e por isso será mantido desligado nos próximos exemplos.
A última configuraçã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 configurações para o compilador
SDCC.
1 // Pll desligado
2 code char at 0x300000 CONFIG1L = 0x01;
3 // Oscilador c/ cristal externo HS
4 code char at 0x300001 CONFIG1H = 0x0C;
5 // Watchdog controlado por software
6 code char at 0x300003 CONFIG2H = 0x00;
7 // Sem programação em baixa tensão
8 code char at 0x300006 CONFIG4L = 0x00;
Notar que as diretivas utilizadas são completamente diferentes, mas realizam o mesmo trabalho.
C APÍTULO
6
Esquema elétrico e circuitos
importantes
6.1 Multiplexação nos terminais do microcontrolador 44
Todos os componentes eletrônico possuem uma representação simbólica para serem utilizados
nos diagramas eletrônicos, também conhecidos como esquemáticos. O esquemático apresenta ao
projetista os componentes e suas ligações, dando uma melhor ideia de como o sistema deve operar.
A Figura 6.1 apresenta o esquemático básico para a utilização de um microcontrolador da família
pic18f4550.
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
18f4520, por exemplo, pode ser alimentado com qualquer tensão contínua entre 2 e 5,5 volts.
Para gerar o clock necessário, que definirá 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 auxiliam
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
43
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 final. Para isso, basta que o desenvolvedor
disponibilize o contato de alguns pinos com um conector. Este conector será ligado a um gravador
que facilitará o trabalho de gravação do programa. Para a família PIC esta tecnologia é denominada
ICSP (in circuit serial programming).
Conforme pode ser observado na Figura 6.1, alguns pinos/terminais possuem mais de uma
descrição. Por exemplo o terminal 8, a descrição deste terminal é “RE0/AN5/CK1SPP”. Isto indica
que dependendo da configuração escolhida ele pode ser um terminal:
• de entrada ou saída referente ao primeiro bit da porta E (RE0)
• de leitura analógica pertencente ao quinto conversor analógico - digital (AN5)
• utilizado para enviar um clock externo de comunicação paralela (CK1SPP)
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.
Programação dos Periféricos
Parte III
7 * Portas de E/S, 46
8 * Barramento de Leds, 51
9 * Display de 7 segmentos, 53
10 * Leitura de teclas, 58
12 * Comunicação serial, 72
13 * Conversor AD, 82
14 * Saídas PWM, 88
15 * Timer, 92
16 * Interrupção, 96
17 * Watchdog, 100
45
C APÍTULO
7
Portas de E/S
7.1 Acesso às portas do microcontrolador 47
7.2 Configuração dos periféricos 48
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
46
Acesso às portas do microcontrolador 7.1
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 configurá-las antes de sua utilização.
O microcontrolador PIC 18f4520 possui 5 portas:
• PORTA: bidirecional com 7 bits
• PORTB: bidirecional com 8 bits
• PORTC: bidirecional com 7 bits
• PORTD: bidirecional com 8 bits
• PORTE: 3 bits bidirecionais e 1 bit apenas entrada
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 configuração. O segundo
endereço realiza essa configuração indicando quais bits serão utilizados para entrada e quais serão
utilizados para saída (Tabela 7.1).
Tabela 7.1: Endereços de memória para as portas do PIC 18f4520
Porta Endereço dos dados Endereço de configuração (TRIS)
PORTA 0xF80 0xF92
PORTB 0xF81 0xF93
PORTC 0xF82 0xF94
PORTD 0xF83 0xF95
PORTE 0xF84 0xF96
1 //início do programa
2 void main(void) {
3 //Para que o ponteiro para a porta D e Tris D funcione
4 //eles são definidos como:
5 //a) unsigned char: pois os 8 bits representam valores
6 //b) volatile: as variáveis podem mudar a qualquer momento
7 //c) near: indica que o registro está na memória de dados
8 volatile near unsigned char *PORTD = 0xF83;
9 volatile near unsigned char *TRISD = 0xF95;
10 //configurando todos os pinos como saídas
11 // 0 = saída (Output)
12 // 1 = entrada (Input)
13 *TRISD = 0b00000000;
14 //liga apenas os quatro últimos leds
15 *PORTD = 0b11110000;
16 //mantém o sistema ligado indefinidamente
17 for(;;);
18 }
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 define’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.
Como estamos criando um define, é 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 define. É isto que permite que utilizemos o define 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 define) é 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
definição seja feita apenas uma vez e utilizada em todo o programa.
Em geral, os terminais das portas são multiplexados com outros dispositivos. Para saber como
configurar cada porta é preciso conhecer os registros de configuração que atuam sobre a porta
desejada. A Figura 7.1 apresenta todos os registros disponíveis do microcontrolador 18f4520.
Para a placa que estamos utilizando, a configuração dos terminais do PIC segue conforme a
Tabela 7.2. Esta configuração reflete 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 7.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 (18f4520) possui o encapsulamento
DIP. Para outros encapsulamentos favor considerar o datasheet.
Da Tabela 7.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.
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 configurados como saídas digitais. São utilizados
para controle de ativação dos displays e também como sinais de controle do LCD.
Tabela 7.2: Tabela de configuração do PIC para as experiências
Terminal Descrição do pino Função
Potenciômetro / Sensor de Tempera-
2 RA0/AN0
tura
4 RA2/AN2/VREF-/CVREF Display 2
7 RA5/AN4/SS/C2OUT Display 1
8 RE0/AN5/CK1SPP RS-LCD / Display 3
9 RE1/AN6/CK2SPP EN-LCD
10 RE2/AN7/OESPP RW-LCD / Display 4
13 OSC1/CLKI
Cristal
14 OSC2/CLKO/RA6
16 RC1/T1OSI/CCP2 Aquecedor
17 RC2/CCP1/P1A Ventilador / Buzzer
19 RD0/SPP0
20 RD1/SPP1 Barramento de dados para o
21 RD2/SPP2 LCD/7seg/Led
22 RD3/SPP3
25 RC6/TX/CK
RS232
26 RC7/RX/DT/SDO
27 RD4/SPP4
28 RD5/SPP5/P1B Barramento de dados para o
29 RD6/SPP6/P1C LCD/7seg/Led
30 RD7/SPP7/P1D
33 RB0/AN12/INT0/SDI
34 RB1/AN10/INT1/SCK
Saídas para alimentação do teclado
35 RB2/AN8/INT2/VMO
36 RB3/AN9/CCP2/VPO
37 RB4/AN11/KBI0/CSSPP
38 RB5/KBI1/PGM
Entradas para leitura do teclado
39 RB6/KBI2/PGC
40 RB7/KBI3/PGD
8
Barramento de Leds
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 8.1.
Podemos notar pela Figura 8.1 que existe um jumper (JP1) que habilita ou não o funcionamento
destes leds. Além disso percebemos que se o jumper estiver encaixado, os led's estão permanente-
mente 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.
51
Conforme visto é preciso configurar os pinos da porta D como saída, para isso basta escrever
zero em cada um deles no registro TRISD.
1 void main(void){
2 TRISD = 0x00; //configura os pinos da porta D como saída
3 PORTD = 0xF0; //liga apenas os quatro últimos bits.
4 for(;;); //mantém o sistema num loop infinito
5 }
Para realizar o acionamento individual de cada um dos leds é preciso realizar a manipulação
de cada um dos bits de modo individual. Isto pode ser feito através das operações bitwise ou das
funções de manipulação desenvolvidas: BitClr() e BitSet():
1 void main(void){
2 char i;
3 float t;
4 TRISD = 0x00; // configura cada um dos pinos como saída
5 PORTD = 0xFF; // começa todos ligados (lógica invertida)
6 for(;;){
7 for(i=0; i<8; i++)
8 BitClr(PORD, i); //acende um led por vez
9 for(t=0; t<100; t++); //intervalo de tempo
10 }
11 for(i=7; i<=0; i--)
12 BitSet(PORD, i); //apaga um led por vez
13 for(t=0; t<100; t++); //intervalo de tempo
14 }
15 }
16 }
C APÍTULO
9
Display de 7 segmentos
9.1 Multiplexação de displays 55
9.2 Criação da biblioteca 56
Os displays de 7 segmentos (Figura 9.1) são componentes optoeletrô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 9.2 podemos visualizar o esquema elétrico e a disposição
física de cada led no componente.
53
Figura 9.2: Diagrama elétrico para display de 7 segmentos com ânodo comum
http://www.hobbyprojects.com/the_diode/seven_segment_display.html
Pela Figura 9.2 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 9.1 são apresentados os valores em binário e em
hexadecimal para cada representação alfanumérica 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.
Tabela 9.1: Conversão binário - hexadecimal para displays de 7 segmentos
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.
Neste curso utilizaremos a ordem direta apresentada na Tabela 9.1. A utilização de uma ou
outra depende da ligação feita na placa. A Figura 9.3 apresenta o esquema elétrico disponível.
Para simplificar 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:
Figura 9.3: Ligação de 4 displays de 7 segmentos multiplexados
1 void main(void) {
2 //vetor que armazena a conversão dos algarismos para o display 7 seg
3 const char conv[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07,
4 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};
5 unsigned int var, time;
6 TRISD = 0x00;
7 TRISA = 0x00;
8 PORTA = 0xFF;
9 for (var = 0; var < 16; var++) {
10 //coloca os caracteres em sequência na saída
11 PORTD = conv[var];
12 //apenas para contar tempo
13 for (time = 0; time < 65000; time++);
14 }
15 }
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 custo1 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 ficam 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 suficientemente 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 9.3 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.
1 Microcontroladores com mais terminais possuem um custo superior, mesmo possuindo os mesmos periféricos interna-
mente.
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:
1. Colocar no barramento de dados o valor a ser mostrado no display X
2. Ligar o display X através da linha de comando
3. Esperar um tempo adequado para evitar flicker2
4. Desligar o display
5. Escolher o próximo display (X+1)
6. Voltar ao passo 1
O programa 9.2 apresenta um exemplo de código para criar uma biblioteca para os displays de
7 segmentos. O programa 9.3 apresenta o header da biblioteca. Já o programa 9.1 apresenta uma
demonstração de uso da biblioteca.
2 Sea 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 flicker.
10
Leitura de teclas
10.1 Debounce por software 60
10.2 Arranjo de leitura por matriz 61
10.3 Usando tristate ou terminais como entrada 62
10.4 Criação da biblioteca 63
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 10.1 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 flui de VCC para o terra passando por R1. Como
não existe nenhuma outra resistência no circuito, toda a tensão fica em cima de R1. Deste modo a
tensão de saída passa a ser zero.
58
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 10.2).
Estas oscilações indevidas podem gerar acionamentos acidentais, causando mau funcionamento
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 10.3.
O debounce por software em geral é utilizado em situações onde se deseja aumentar a robustez
de uma entrada que já possua um debounce por hardware ou reduzir o custo da placa utilizando
apenas a solução por software. A grande desvantagem deste tipo de sistema é inserir um atraso na
detecção da informação.
Para realizar o debounce por software precisamos ter uma noção do tempo que a chave precisa
para estabilizar. Da Figura 10.2 temos que este tempo, para uma determinada chave é de aproxima-
damente 150 (µs). Um ciclo de clock do sistema em questão (PIC 18f4520 com cristal de 8MHz) é
de 0,51 (µ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.
1 void main(void) {
2 unsigned char valTemp;
3 unsigned char valAtual;
4 unsigned char tempo;
5 TRISB = 0xF0; //mantém os 4 últimos bits como entrada
6 TRISD = 0x00; //Configura a porta D como saída
7 PORTB = 0x00; //liga os 4 primeiros bits
8 BitClr(INTCON2,7); //habilita pull−up
9 ADCON1 = 0b00001110; //configura todos os bits da porta B como digitais
10 for(;;) {
11 //aguarda uma mudança na porta B
12 while(valAtual==PORTB);
13 //quando acontecer alguma mudança, conta um tempo pra ver se é permanente
14 valTemp = PORTB;
15 tempo = 22;
16 while (tempo > 0){
1 void main(void) {
2 unsigned char i,j;
3 unsigned char chave[2][4] = {{0,0,0,0},{0,0,0,0}};
4 INTCON2 &= 0x7F; //habilita pull−up
5 ADCON1 = 0b00001110; //apenas AN0 é analógico,
6 TRISB = 0xF0; //os 4 últimos bits são entrada
7 TRISD = 0x00; //configura a porta D como saída
8 PORTD = 0xff;
9
10 for(;;) {
11 for(i = 0; i < 2; i++) {
12 PORTB = 0xff; //"desliga" todas as colunas
13 for(j = 0; j < 100; j++);
14 BitClr(PORTB,i); //"liga" o bit da coluna correspondente
15 //gasta tempo para garantir que o pino atingiu o nível alto
16 for(j = 0; j < 100; j++);
17 //realiza o teste para cada bit e atualiza a matriz.
18 for(j = 0; j < 4; j++) {
19 if (!BitTst(PORTB,j+4)) {
20 chave[i][j] = 1;
21 BitSet(PORTD,j+4*i);
22 } else {
23 chave[i][j] = 0;
24 BitClr(PORTD,j+4*i);
25 }
26 }
27 }
28 }
29 }
É 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 biblio-
teca para o teclado.
Tristate em eletrônica é o conceito de um terceiro estado além do estado de nível alto (1) e do
estado de nível baixo (0).
Um determinado fio, se estiver conectado à tensão de 5 volts de uma bateria, é comumente
definido como estando em nível alto. Se o mesmo fio for ligado ao terra, ele é definido como
estando em nível baixo. O tristate acontece quando o fio não está conectado em nenhum lugar.
Uma maneira de conseguir isso é através de um sistema de relés, ou transistores.
No exemplo do teclado anterior, ao invés de desligar a coluna em questão utilizando um valor
de nível baixo, podemos desligar completamente a coluna do terminal colocando-o em tristate.
Em microcontroladores que não possuem esse tipo de conexão é possível colocar o terminal como
entrada.
Sendo configurado como entrada, o terminal não fornece nenhum nível de tensão ao sistema,
removendo a coluna de modo mais eficiente que colocando-a com valor zero.
Figura 10.6: Circuito de tristate de uma porta de E/S
http://electronics.stackexchange.com/questions/250836/tristate-buffer
1 void main(void) {
2 unsigned char i,j;
3 unsigned char chave[2][4] = {{0,0,0,0},{0,0,0,0}};
4 INTCON2 &= 0x7F; //habilita pull−up
5 ADCON1 = 0b00001110; //apenas AN0 é analógico,
6 TRISB = 0xF0; //os 4 últimos bits são entrada
7 TRISD = 0x00; //configura a porta D como saída
8 PORTD = 0xff;
9
10 for(;;) {
11 for (i = 0; i < 4; i++) {
12 TRISB = ~((unsigned char)1<<i); // apenas a coluna desejada é saída
13 PORTB = ~((unsigned char)1<<i); // apenas a coluna desejada possui nível alto
14
15 //realiza o teste para cada bit e atualiza a variável
16 for (j = 0; j < 2; j++) {
17 if (!BitTst(PORTB,j+4)) {
18 chave[i][j] = 1;
19 BitSet(PORTD,j+4*i);
20 } else {
21 chave[i][j] = 0;
22 BitClr(PORTD,j+4*i);
23 }
24 }
25 }
26 }
27 }
O programa 10.1 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 10.2. Já o programa 10.3
apresenta uma demonstração de uso da biblioteca.
Código 10.1: teclado.c
1 #include "keypad.h"
2 #include "pic18f4520.h"
3 static unsigned char valor = 0x00;
4 unsigned char kpRead(void) {
5 return valor;
6 }
7 void kpDebounce(void) {
8 unsigned char i, j;
9 static unsigned char tempo;
10 static unsigned char valorNovo = 0x0000;
11 static unsigned char valorAntigo = 0x0000;
12
13 for (i = 0; i < 4; i++) {
14 TRISB = ~((unsigned char)1<<i);
15 PORTB = ~((unsigned char)1<<i);
16
17 //realiza o teste para cada bit e atualiza a variável
18 for (j = 0; j < 2; j++) {
19 if (!BitTst(PORTB, j + 4)) {
20 BitSet(valorNovo, (i * 2) + j);
21 } else {
22 BitClr(valorNovo, (i * 2) + j);
23 }
24 }
25 }
26 if (valorAntigo == valorNovo) {
27 tempo--;
28 } else {
29 tempo = 10;
30 valorAntigo = valorNovo;
31 }
32 if (tempo == 0) {
33 valor = valorAntigo;
34 }
35 }
36 void kpInit(void) {
37 TRISB = 0xF0; //quatro entradas e quatro saidas
38 BitClr(INTCON2, 7); //liga pull up
39 ADCON1 = 0b00001110; //apenas AN0 é analogico, demais terminais são digitais
40 }
11
Display LCD 2x16
11.1 Criação da biblioteca 70
O display de LCD utilizado neste curso possui duas linhas por 16 colunas de caracteres, compa-
tível com o HD44780. Na Figura 11.1 é apresentado um modelo genérico deste tipo de display. A
Figura 11.2 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 di-
ferentes, sendo os mais comuns de 1x8, 2x16 e 4x40. Pode ainda ter 16 ou 14 terminais, dependendo
65
Figura 11.2: Display Alfanumérico LCD 2x16 - verso
1. Terra 1. Bit 2
2. VCC (+5V) 2. Bit 3
3. Ajuste do contraste 3. Bit 4
4. Seleção de registro(RS) 4. Bit 5
5. Read/Write (RW) 5. Bit 6
6. Clock, Enable (EN) 6. Bit 7
7. Bit 0 7. Backlight + (opcional)
8. Bit 1 8. Backlight Gnd (opcional)
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 11.3, 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 11.2 e
11.1, respectivamente.
12
Comunicação serial
12.1 I2C 73
HT1380 74
12.2 RS 232 77
12.3 Criação da biblioteca 80
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 simplificação do hardware. Como os dados são enviados um a um, a quantidade
de fios 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 fios. 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 12.1.
Para sistemas embarcados em geral não existe grande necessidade de altas taxas de transmissão
mas de sistemas de baixo custo e confiáveis. Existem diversas alternativas entre elas estudaremos
duas das mais comuns: RS232 e I2C.
72
Tabela 12.1: Taxas de transmissão para diferentes protocolos
Protocolo Taxa (bits/s) Taxa (bytes/s)
Serial MIDI 31.25 kbit/s 3.9 kB/s
Serial EIA-232 max. 230.4 kbit/s 28.8 kB/s
Serial UART max 2.7648 Mbit/s 345.6 kB/s
I2c 3.4 Mbit/s 425 kB/s
Serial EIA-422 max. 10 Mbit/s 1.25 MB/s
SPI Bus (Up to 100MHz) 100 Mbit/s 12.5 MB/s
USB super speed (USB 3.0) 5 Gbit/s 625 MB/s
HDMI v. 1.3 10.2 Gbit/s 1.275 GB/s
Ultra DMA ATA 133 (16 bits) 1,064 Mbit/s 133 MB/s
Serial ATA 3 (SATA-600) 6,000 Mbit/s 600 MB/s
Ultra-640 SCSI (16 bits) 5,120 Mbit/s 640 MB/s
Serial Attached SCSI (SAS) 3 9,600 Mbit/s 1,200 MB/s
I2C 12.1
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 1 especifica
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 especificaçã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 bits
que permite identificá-lo de maneira única. Deste modo é possível construir um sistema integrado
com baixo custo de conexão. A figura 12.1 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, figura 12.2.
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 é
1 I2C Specification Version 4.0: http://www.nxp.com/documents/user_manual/UM10204.pdf
Figura 12.2: Conexão com coletor aberto
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 habili-
tar/desabilitar a comunicação do chip. A comunicação também não exige um bit de resposta como
no I2C. A figura 12.3 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 figura
12.4.
O RTC possui 8 registros internos, com endereços de 0b000 à 0b111. Os valores destes endereços
estão codificados em BCD para facilitar a passagem dos valores. Estes registros são apresentados
na figura 12.5.
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 12.4 apresenta duas funções de conversão e
acesso ao registro de segundos.
RS 232 12.2
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 eletro-
mecâ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 é codificada como 7610 e em binário como 110100102 . Na
maioria dos dispositivos primeiro se envia o bit menos significativo. 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 significativo é enviado para a saída do microcontrolador. Realiza-se então
um shift para direita e o “novo” bit menos significativo é “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 12.6 apresenta o sinal elétrico2 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á
definido. 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.
Para o correto funcionamento do protocolo devemos garantir compatibilidade no nível físico (do
hardware) e lógico (no software).
Para o hardware basta compatibilizar o tipo de conector, os níveis de tensão e a pinagem dos
conectores.
Para o nível de software temos que definir a codificação utilizada (ASCII, UTF-8, etc), especificar
o fluxo de caracteres (quantidade de bits por caractere, tamanho do start/stop bit, paridade) e a
taxa de transmissão desejada.
Estas configurações são realizadas através de 5 registros TXSTA, RCSTA, BAUDCON, SPBRGH
e SPBRG.
2 Para o protocolo RS232 o nível alto ou 1 (um) é aquele com tensões positivas entre +3 e +15. O nível lógico baixo ou 0
Os registros TXSTA e RCSTA são responsáveis por configurar o meio de transmissão: presen-
ça/ausência de bit de parada, tamanho da palavra de um caractere, transmissão síncrona/assín-
crona.
O registro BAUDCON é responsável por configurar o controle de velocidade de transmissão.
Os registros SPBRGH e SPBRG representam o byte mais alto e o mais baixo da palavra de 16
bits que indica a taxa de transmissão.
A taxa de transmissão pode ser calculada segundo a Tabela 12.2.
Tabela 12.2: Cálculo do valor da taxa de transmissão da porta serial
Bits de Configuração Precisão Taxa de transmissão
TXSTA:4 BAUDCON:3 TXSTA:2
FOSC
0 0 0 8bits F232 =
[64 ∗ (n + 1)]
0 0 1 8bits FOSC
F232 =
0 1 0 16bits [16 ∗ (n + 1)]
0 1 1 16bits
FOSC
1 0 x 8bits F232 =
[4 ∗ (n + 1)]
1 1 x 16bits
x – não importa, n – valor do par SPBRGH:SPBRG
Como visto na Tabela 12.2 existem três fórmulas diferentes para calcular a taxa de transmissão.
A melhor maneira de configurar a taxa de transmissão da porta serial é verificar 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:
• n1 = 1, F232 = 62.500, err = -7,64%
• n2 = 8, F232 = 55.555, err = 3,63%
• n3 = 32, F232 = 57.142, err = 0,79%
A equação que gera o menor erro é a terceira. Como queremos trabalhar com uma comuni-
cação assíncrona, da Tabela 12.2 obtemos que os bits de configuração devem ser: TXSTA(4) = 0,
BAUDCON(3) = 1 e TXSTA(2) = 1. A seguir temos todo o processo de configuração da porta serial
RS232.
1 BitClr(BAUDCON,0);//Desabilita auto detecção de velocidade
2 BitSet(BAUDCON,3);//Registro de geração de sinal com 16 bits
3 BitClr(BAUDCON,6);//Operação de recepção ativa
4 BitClr(RCSTA,1); //Desabilita bit de erro de overrun
5 BitClr(RCSTA,2); //Desabilita bit erro na comunicação
6 BitClr(RCSTA,4); //Habilita bit de recepção
7 BitClr(RCSTA,6); //Seleciona 8 bits
8 BitSet(RCSTA,7); //Configura RX/TX como pinos de comunicação
9 BitSet(TXSTA,2); //Modo de alta velocidade habilitado
10 BitSet(TXSTA,3); //Envia bit de parada (break character bit)
11 BitClr(TXSTA,4); //Modo assíncrono
12 BitSet(TXSTA,5); //Habilita transmissão
13 BitClr(TXSTA,6); //Seleciona 8 bits
14 SPBRGH = 0x00; //Configura para 56k (SPBRGH|SPBRG = 32)
15 SPBRG = 0x22; //Configura para 56k (SPBRGH|SPBRG = 32)
16 BitSet(TRISC,6); //Configura pino de recepção como entrada
17 BitClr(TRISC,7); //Configura pino de envio como saída
O procedimento de serialização dos bits é feito de maneira automática pelo hardware. Enquanto
ele está realizando este processo não devemos mexer no registro que armazena o byte a ser enviado.
Por isso devemos verificar 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 ficamos 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 fica travado em uma tarefa gastando tempo que seria útil para realizar outras ope-
rações. A melhor alternativa para resolver este problema é através de interrupções, que serão
abordadas apenas no tópico 16.
Criação da biblioteca 12.3
O programa 12.5 apresenta um exemplo de código para criar uma biblioteca para comunicação
serial. O arquivo de header é apresentado no programa 12.6 e o exemplo de uso demonstrado no
programa 12.7.
A seguir o arquivo de header.
13
Conversor AD
13.1 Elementos sensores 82
13.2 Processo de conversão AD 85
13.3 Criação da biblioteca 86
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 13.1).
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 fiaçã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 13.2).
1 Com uma precisão de 10 bits conseguimos representar 210 valores diferentes, ou 1024 valores.
82
Figura 13.1: Lâmpada incandescente
Deste modo a tensão aplicada em RL (supondo que RL é muito maior que R2) é:
VS ∗ R2 R
VRL = = VS ∗ ( 2 )
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: luminosidade
com LDR's, força com strain-gauges, deslocamento com potenciômetros lineares, etc.
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 2 N representações diferentes. É interessante então dividir a amplitude ini-
cial por 2 N divisões iguais. Para N = 2 temos 4 representações de 1.25v cada. É comum nestes
comparadores que a primeira tensão possua um offset.
Representação binária com 2 bits Valor em tensão Valor em Tensão com offset
00 0.000 0.625v
01 1.250 1.875v
10 2.500 3.125v
11 3.750 4.375v
A Figura 13.6 apresenta as faixas de valores e da necessidade de offset 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 13.7.
O circuito da Figura 13.7 é conhecido como conversor analógico digital do tipo flash 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.
Figura 13.7: Conversor analógico digital de 2 bits
http://en.wikipedia.org/wiki/File:Flash_ADC.png - Jon Guerber
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. Configurar o conversor;
2. Iniciar a conversão;
3. Monitorar o final da conversão;
4. Ler o valor.
Os programas 13.1 e 13.2 apresentam os arquivos de código e header de uma biblioteca exemplo
para conversores analógicos para digital no microcontrolador PIC. O programa 13.3 apresenta um
código exemplificando o uso da biblioteca criada.
Código 13.1: adc.c
1 #include "adc.h"
2 #include "pic18f4520.h"
3
4 void adcInit(void) {
5 BitSet(TRISA, 0); //seta o bit 0 como entrada
6
7 ADCON0 = 0b00000001; //seleciona o canal 0 e liga o ad
8 ADCON1 = 0b00001110; //apenas AN0 é analogico, a referencia é baseada na fonte
9 ADCON2 = 0b10101010; //FOSC /32, Alinhamento à direita e tempo de conv = 12 TAD
10 }
11
12 int adcRead(void) {
13 unsigned int ADvalor;
14 ADCON0 |= 0b00000010; //inicia conversao
15
16 while (BitTst(ADCON0, 1)); // espera terminar a conversão;
17
18 ADvalor = ADRESH; // le o resultado
19 ADvalor <<= 8;
20 ADvalor += ADRESL;
21 return ADvalor;
22 }
14
Saídas PWM
14.1 Criação da biblioteca 89
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 define o ciclo de trabalho, ou duty cycle, da saída. A Figura 14.1 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
88
aqueça ou esfrie. Assim é possível ajustar a quantidade de energia média dado uma frequência
suficientemente alta1 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 final deste modo, num motor podemos ajustar a velocidade de
rotação que queremos.
Como citado, a frequência do PWM tem que ser suficientemente alta. Esta frequência depende
do circuito implementado no microcontrolador. No caso do PIC 18f4520 é calculada segundo a
fórmula abaixo.
FOSC
Freq.PWM =
[(PR2) + 1] ∗ 4 ∗ (TMR2Prescaler )
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 definiçã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 configurar 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( FFOSC )
PWM
ResoluçãoPWM (max) = bits
log(2)
O PIC 18f4520 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
resolução de 8 bits a frequência máxima aumenta para 31.25 kHz.
Utilizando a primeira e segunda fórmulas podemos montar a Tabela 14.1.
Tabela 14.1: Faixa de frequências máximas e mínimas para cada configuração do prescaler
Prescaler Freq. máxima (PR2 = 0) Freq. mínima (PR2 = 0)
1 2.000.000 7.812,5
4 500.000 1.953,2
16 125.000 488,3
Para configurar as saídas PWM devemos especificar a frequência de trabalho através de PR2 e
TCON2, além do duty cycle em CCPR1L e CCPR2L. No registro TRISC é configurado o terminal
como uma saída e em CCP1CON e CCP2CON definimos que ele deve trabalhar como um PWM. O
prescaler foi configurado para 16 bits de modo a obter a maior faixa de frequência audível disponível
(Tabela 14.1). Notar que é importante realizar primeiro a multiplicação e somente depois a divisão,
para não haver perda de informação. No programa 14.1 é apresentado um código exemplo de como
criar as rotinas de operação do PWM. O header desta biblioteca é apresentado no programa 14.2.
Por fim, o programa 14.3 apresenta um exemplo de utilização desta biblioteca.
1 Para ser considerada suficientemente 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 é suficiente para que o
sistema não sinta esse impacto. No entanto cada caso deve ser analisado em particular
Código 14.1: pwm.c
1 #include "pwm.h"
2 #include "pic18f4520.h"
3
4 void pwmSet1(unsigned char porcento) {
5 //formula do duty cycle:
6 //DC_porcento = V / ((PR2+1)∗4;
7 //V = DC/100 ∗ (PR2+1) ∗ 4
8 //V = DC ∗ (PR2+1) /25
9
10 unsigned int val = ((unsigned int) porcento) *(PR2 + 1);
11 val = val / 25;
12 //garante que tem apenas 10 bits
13 val &= 0x03ff;
14 //os 8 primeiros bits sao colocados no CCPR1L
15 CCPR1L = val >> 2;
16 //os ultimos dois são colocados na posição 5 e 4 do CCP1CON
17 CCP1CON |= (val & 0x0003) << 4;
18 }
19
20 void pwmSet2(unsigned char porcento) {
21 unsigned int val = ((unsigned int) porcento) *(PR2 + 1);
22 val /= 25;
23 //garante que tem apenas 10 bits
24 val &= 0x03ff;
25 //os 8 primeiros bits sao colocados no CCPR1L
26 CCPR2L = val >> 2;
27 //os ultimos dois são colocados na posição 5 e 4 do CCP1CON
28 CCP2CON |= (val & 0x0003) << 4;
29 }
30
31 void pwmFrequency(unsigned int freq) {
32 //PR2 = fosc/(fpwm∗4∗prescaler)−1;
33 //PR2 = (8000000/(freq∗4∗16)) − 1;
34 PR2 = (125000 / (freq)) - 1;
35 }
36
37 void pwmInit(void) {
38 BitClr(TRISC, 1); //configura os pinos correspondentes como saídas
39 BitClr(TRISC, 2);
40
41 T2CON |= 0b00000011; //configura o prescale do timer 2 para 1:16
42 BitSet(T2CON, 2); //Liga o timer 2
43
44 CCP1CON |= 0b00001100; //configura CCP1 como um PWM
45 CCP2CON |= 0b00001100; //configura CCP2 como um PWM
46 }
15
Timer
15.1 Reprodução de Sons 94
Nos microcontroladores existem estruturas próprias para realizar a contagem de tempo, estas
estruturas são denominadas Timers.
O PIC 18f4520 possui quatro timers. Para utilizarmos a saída PWM temos que configurar 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 “overflow”1 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 configurado anteriormente já passou. Uma quarta
função “AguardaTimer()”, foi construída para facilitar o desenvolvimento de algumas rotinas co-
muns nos programas. Estas rotinas estão implementadas no programa 15.1 cujo header é apresen-
tado no programa 15.2. O modo de utilizar esta biblioteca é apresentado no programa 15.3.
1 Overflow é 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 overflow.
92
Código 15.1: timer.c
1 #include "pic18f4520.h"
2 #include "timer.h"
3
4 char timerEnded(void) {
5 return BitTst(INTCON, 2);
6 }
7
8 void timerWait(void) {
9 while (!BitTst(INTCON, 2));
10 }
11
12 //tempo em micro segundos
13 void timerReset(unsigned int tempo) {
14 //para placa com 8MHz 1 ms = 2 ciclos
15 unsigned ciclos = tempo * 2;
16 //overflow acontece com 2^15−1 = 65535 (max unsigned int)
17 ciclos = 65535 - ciclos;
18
19 ciclos -= 14; //subtrai tempo de overhead(experimental)
20 TMR0H = (ciclos >> 8); //salva a parte alta
21 TMR0L = (ciclos & 0x00FF); // salva a parte baixa
22
23 BitClr(INTCON, 2); //limpa a flag de overflow
24 }
25
26 void timerInit(void) {
27 T0CON = 0b00001000; //configura timer 0 sem prescaler
28 BitSet(T0CON, 7); //liga o timer 0
29 }
A seguir o exemplo de outra música codificada utilizando o sistema desenvolvido. Cada nota
é representada por sua duração (vetor tempo[]) e sua frequência (vetor notas[]). Esta informação
pode ser obtida facilmente através da partitura da música.
2 Esta é a máxima frequência possível para o PWM operado com prescaler de 16x.
66 //Super Mario theme
67 unsigned char tempo[] = {
68 15, 5, 15, 7, 30, 15, 30, 30, 30, 30,
69 30, 30, 15, 30, 15, 30, 15, 30, 30, 15,
70 30, 22, 15, 15, 30, 15, 30, 30, 15, 15,
71 30, 15, 30, 15, 30, 15, 30, 15, 30, 30,
72 15, 30, 22, 15, 15, 30, 15, 30, 30, 15,
73 15, 30, 30, 15, 15, 15, 15, 15, 15, 15,
74 15, 15, 15, 15, 15, 15, 15, 30, 15, 15,
75 5, 15, 15, 15, 15, 15, 15, 15, 15, 30,
76 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
77 15, 15, 15, 15, 30, 15, 30, 15, 30, 15};
78 unsigned int notas[] = {
79 E2, v, E2, v, E2, C2, E2, G2, v, G,
80 v, C2, v, G, v, E, v, A, B, AS,
81 A, G, E2, G2, A2, F2, G2, E2, C2, D2,
82 B, v, C2, v, G, v, E, v, A, B,
83 AS, A, G, E2, G2, A2, F2, G2, E2, C2,
84 D2, B, v, G2, F2S, F2, D2S, v, E2, v,
85 G2, A, C2, v, A, C2, D2, v, G2, F2S,
86 F2, D2S, v, E2, v, C3, v, C3, C3, v,
87 G2, F2S, F2, D2S, v, E2, v, GS, A, C2,
88 v, A, C2, D2, v, D2S, v, D2, v, C2};
O último elemento musical não apresentado é a intensidade da nota, ou seja, seu volume. Com
os dispositivos apresentados até agora não é possível implementar mais essa característica de modo
simples.
C APÍTULO
16
Interrupção
Até o momento todos os programas que foram desenvolvidos seguiam um fluxo 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 ficar 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 finalizada, 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 condição
do dispositivo for satisfeita (fim 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é-definida. 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 buffer de transmissão
TXREG estiver disponível.
• Conversor AD: quando o resultado da conversão estiver disponível para leitura.
• Porta B: quando algum dos bits configurados como entrada altera seu valor.
• Timer 0: quando acontece overflow em seu contador.
96
Para gerenciar a interrupção, deve-se criar uma rotina que irá verificar qual foi o hardware que ge-
rou 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.
1 void NomeDaFuncao(void)
2 {
3 //código ...
4 }
5
6 //Indicar a posição no vetor de interrupções
7 #pragma code high_vector=0x08
8 void interrupt_at_high_vector(void)
9 {
10 _asm GOTO Interrupcao _endasm
11 }
12 #pragma code
13 #pragma interrupt NomeDaFuncao
A função que irá tratar da interrupção não retorna nem recebe nenhum valor.
Existe uma correlação entre o número que vem depois da expressão “interrupt” para o compila-
dor SDCC e o número ao final 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 interrupção. Estas
posições estão numa área conhecida como vetor de interrupções.
Para o microcontrolador PIC 18f4520 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 verificação é
através das flags de controle, ou seja, bits que indicam a situação de cada periférico.
O programa 16.1 apresenta uma função que trata de todas as fontes possíveis de interrupção
para o PIC 18f4520.
Em geral não é necessário tratar todas as interrupções, apenas aquelas que influenciarão o sis-
tema. O programa 16.2 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 16.2 funcione corretamente devemos inicializar as
interrupções de modo adequado, conforme apresentado no programa 16.3.
Código 16.1: Fontes de Interrupção
1 void Interrupcao(void) interrupt 1
2 {
3 // não é necessário utilizar todos os if's, apenas
4 // aqueles das interrupções desejadas
5 if (BitTst(PIR1,0)) { /∗código∗/ } //Flag de overflow do TIMER1
6 if (BitTst(PIR1,1)) { /∗código∗/ } //Flag de comparação do TIMER2 com PR2
7 if (BitTst(PIR1,2)) { /∗código∗/ } //Flag de comparação do CCP1
8 if (BitTst(PIR1,3)) { /∗código∗/ } //Flag de fim de operação na porta paralela
9 if (BitTst(PIR1,4)) { /∗código∗/ } //Flag de fim de transmissão da Serial
10 if (BitTst(PIR1,5)) { /∗código∗/ } //Flag de recepção da Serial
11 if (BitTst(PIR1,6)) { /∗código∗/ } //Flag de fim de conversão do AD
12 if (BitTst(PIR1,7)) { /∗código∗/ } //Flag de leitura/escrita da porta paralela
13 if (BitTst(PIR2,0)) { /∗código∗/ } //Flag de comparação do CCP2
14 if (BitTst(PIR2,1)) { /∗código∗/ } //Flag de overflow do TIMER3
15 if (BitTst(PIR2,2)) { /∗código∗/ } //Flag de condição de Tensão Alta/Baixa
16 if (BitTst(PIR2,3)) { /∗código∗/ } //Flag de detecção de colisão no barramento
17 if (BitTst(PIR2,4)) { /∗código∗/ } //Flag de fim escrita na memória flash
18 if (BitTst(PIR2,5)) { /∗código∗/ } //Flag de interrupção da USB
19 if (BitTst(PIR2,6)) { /∗código∗/ } //Flag de mudança na entrada de comparação
20 if (BitTst(PIR2,7)) { /∗código∗/ } //Flag de falha no oscilador
21 if (BitTst(INTCON,0)) { /∗código∗/ } //Flag de mudança na PORTA B
22 if (BitTst(INTCON,1)) { /∗código∗/ } //Flag de interrupção externa INT0
23 if (BitTst(INTCON,2)) { /∗código∗/ } //Flag de overflow no TIMER0
24 if (BitTst(INTCON3,0)) { /∗código∗/ } //Flag de interrupção externa INT1
25 if (BitTst(INTCON3,1)) { /∗código∗/ } //Flag de interrupção externa INT2
26 }
17
Watchdog
Por algum motivo o software pode travar em algum ponto, seja por um loop infinito 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 infinito ou de um
pooling sem resposta.
Para habilitar o watchdog é necessário alterar os registros de configuração, especificamente 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 17.1.
Notar o #define criado na primeira linha do programa 17.1. 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
indefinidamente.
100
Código 17.1: Inicialização do sistema com interrupções
1 #define CLRWTD() _asm CLRWDT _endasm
2
3 //início do programa
4 void main(void) {
5 unsigned int i;
6 unsigned char temp;
7 TRISD=0x00;
8 PORTD=0x00;
9 BitSet(WDTCON,0); //liga o sistema de watchdog
10 for(;;){
11 PORTD++;
12 for(i = 0; i < 10000; i++){
13 CLRWTD();
14 }
15 }
16 }
Arquitetura de desenvolvimento de
Parte IV
software
21 * Anexos, 114
102
C APÍTULO
18
One single loop
103
Código 18.1: Exemplo de arquitetura single-loop
1 //seção de includes
2 #include "p18f4520.h"
3 #include "config.h"
4 #include "keypad.h"
5 #include "ssd.h"
6 //função principal
7 void main (void) {
8 //declaração das variáveis
9 int ia, ib, ic;
10 float fa, fb, fc;
11 //inicialização dos periféricos
12 kpInit();
13 ssdInit();
14 //loop principal
15 for(;;){
16 //chamada das tarefas
17 kpDebounce();
18 ia = kpRead();
19 ssdPrint(ia); //tem que ser executado pelo menos a cada 10(ms)
20 }
21 }
19
Interrupt control system
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 é suficiente. 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 fixo, por exemplo
a cada 5ms.
A grande vantagem da abordagem citada é que a inserção de mais código dentro do loop prin-
cipal não atrapalha a velocidade com que o display é atualizado, que está fixo em 5(ms).
105
Código 19.1: Exemplo de sistema Interrupt-driven
1 int ia;
2 //tratamento do teclado via interrupção
3 void Interrupcao(void) interrupt 1{
4 if (BitTst(INTCON,0)){ //PORTA B : mudou valor
5 ia = kpRead();
6 }
7 }
8
9 void main(void) {
10 //inicialização dos periféricos
11 kpInit();
12 ssdInit();
13 //inicialização da interrupção
14 BitClr(RCON,7); //desabilita IPEN (modo de compatibilidade)
15 BitSet(INTCON,3); // liga a interrupção para a porta B
16 BitSet(INTCON,7); // habilita todas as interrupções globais
17 BitSet(INTCON,6); // habilita todas as interrupções de periféricos
18 for(;;){ //loop principal
19 //chamada das tarefas
20 ssdPrint(ia);
21 }
22 }
20
Cooperative multitasking
20.1 Fixação de tempo para execução dos slots 111
20.2 Utilização do tempo livre para interrupções 113
107
Inicio
Ler Atualiza
Teclado Display
Atualiza Escreve
Display Serial
Atualiza
Ler Serial Display
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 20.1, percebemos
que é exatamente este o comportamento que queremos para a função ssdUpdate(). Deste modo,
podemos remodelar o código fazendo esta alteração.
Código 20.1: Exemplo de cooperative multitasking
1 void main(void) {
2 //declaração das variáveis
3 char slot;
4 //funções de inicialização
5 serialInit();
6 kpInit();
7 ssdInit();
8 for(;;){ //início do loop infinito
9 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ início do top−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
10 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ fim do top−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
11
12 //∗∗∗∗∗∗∗∗∗∗∗ início da máquina de estado ∗∗∗∗∗∗∗∗∗∗∗∗
13 switch(slot){
14 case 0:
15 ProcessaTeclado();
16 slot = 1;
17 break;
18 case 1:
19 ssdUpdate();
20 slot = 2;
21 break;
22 case 2:
23 serialRead();
24 slot = 3;
25 break;
26 case 3:
27 ssdUpdate();
28 slot = 4;
29 break;
30 case 4:
31 serialSend();
32 slot = 5;
33 break;
34 case 5:
35 ssdUpdate();
36 slot = 0;
37 break;
38 default:
39 slot = 0;
40 break;
41 }
42 //∗∗∗∗∗∗∗∗∗∗∗∗ fim da máquina de estado ∗∗∗∗∗∗∗∗∗∗∗∗∗∗
43
44 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗ início do bottom−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
45 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ fim do bottom−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
46 } //fim loop infinito (!?)
47 }
Código 20.2: Exemplo de cooperative multitasking com uso do top slot
1 void main(void){
2 //declaração das variáveis
3 char slot;
4 //funções de inicialização
5 serialInit();
6 kpInit();
7 ssdInit();
8 for(;;){ //início do loop infinito
9 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ início do top−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
10 ssdUpdate();
11 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ fim do top−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
12
13
14 //∗∗∗∗∗∗∗∗∗∗∗ início da máquina de estado ∗∗∗∗∗∗∗∗∗∗∗∗
15 switch(slot){
16 case 0:
17 ProcessaTeclado();
18 slot = 1;
19 break;
20 case 1:
21 serialRead();
22 slot = 2;
23 break;
24 case 2:
25 serialSend();
26 slot = 0;
27 break;
28 default:
29 slot = 0;
30 break;
31 }
32 //∗∗∗∗∗∗∗∗∗∗∗∗ fim da máquina de estado ∗∗∗∗∗∗∗∗∗∗∗∗∗∗
33
34
35 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗ início do bottom−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
36
37 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ fim do bottom−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
38
39 } //fim loop infinito (!?)
40 }
Fixação de tempo para execução dos slots 20.1
Do modo apresentado até agora, assim que uma função termina, o sistema automaticamente
passa para a próxima tarefa. Uma característica desejada é que estas funções possuam um tempo
determinado para funcionar. Deste modo, todo o sistema se torna mais previsível.
A maneira mais simples de realizar este procedimento é criar uma rotina de tempo. Toda vez
que um slot terminar, o sistema ficará aguardando o tempo escolhido para reiniciar o sistema.
No exemplo apresentado é inserida a função AguardaTimer() no bottom-slot de modo que a
próxima função só executará quando passar os 5 (ms).
Como este é um modo simples de implementar um sistema multitarefa podemos notar que se a
função ultrapassar 5 (ms) todo o cronograma será afetado. É necessário então garantir que todo e
cada slot será executado em menos de 5 (ms). Isto deve ser feito através de testes de bancada.
Na Figura 20.2 está um exemplo de como um sistema com 3 slots se comporta ao longo do
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).
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 fica “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).
Utilização do tempo livre para interrupções 20.2
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 20.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).
C APÍTULO
21
Anexos
114
Código 21.2: p18f4520.h
1 //função para limpar o watchdog
2 #define CLRWTD() _asm CLRWDT _endasm
3
4 //funções de bit
5 #define BitSet(arg,bit) ((arg) |= (1<<bit))
6 #define BitClr(arg,bit) ((arg) &= ~(1<<bit))
7 #define BitFlp(arg,bit) ((arg) ^= (1<<bit))
8 #define BitTst(arg,bit) ((arg) & (1<<bit))
9
10
11 //defines para registros especiais
12 #define PORTA (*(volatile __near unsigned char*)0xF80)
13 #define PORTB (*(volatile __near unsigned char*)0xF81)
14 #define PORTC (*(volatile __near unsigned char*)0xF82)
15 #define PORTD (*(volatile __near unsigned char*)0xF83)
16 #define PORTE (*(volatile __near unsigned char*)0xF84)
17
18 #define TRISA (*(volatile __near unsigned char*)0xF92)
19 #define TRISB (*(volatile __near unsigned char*)0xF93)
20 #define TRISC (*(volatile __near unsigned char*)0xF94)
21 #define TRISD (*(volatile __near unsigned char*)0xF95)
22 #define TRISE (*(volatile __near unsigned char*)0xF96)
23
24 #define INTCON (*(volatile __near unsigned char*)0xFF2)
25 #define INTCON2 (*(volatile __near unsigned char*)0xFF1)
26 #define PIE1 (*(volatile __near unsigned char*)0xF9D)
27 #define PIR1 (*(volatile __near unsigned char*)0xF9E)
28 #define PIR2 (*(volatile __near unsigned char*)0xFA1)
29
30 #define TMR0L (*(volatile __near unsigned char*)0xFD6)
31 #define TMR0H (*(volatile __near unsigned char*)0xFD7)
32 #define T0CON (*(volatile __near unsigned char*)0xFD5)
33
34 #define ADCON2 (*(volatile __near unsigned char*)0xFC0)
35 #define ADCON1 (*(volatile __near unsigned char*)0xFC1)
36 #define ADCON0 (*(volatile __near unsigned char*)0xFC2)
37 #define ADRESL (*(volatile __near unsigned char*)0xFC3)
38 #define ADRESH (*(volatile __near unsigned char*)0xFC4)
39
40 #define RCSTA (*(volatile __near unsigned char*)0xFAB)
41 #define TXSTA (*(volatile __near unsigned char*)0xFAC)
42 #define TXREG (*(volatile __near unsigned char*)0xFAD)
43 #define RCREG (*(volatile __near unsigned char*)0xFAE)
44 #define SPBRG (*(volatile __near unsigned char*)0xFAF)
45 #define SPBRGH (*(volatile __near unsigned char*)0xFB0)
46 #define BAUDCON (*(volatile __near unsigned char*)0xFB8)
47
48 #define RCON (*(volatile __near unsigned char*)0xFD0)
49 #define WDTCON (*(volatile __near unsigned char*)0xFD1)
50 #define T2CON (*(volatile __near unsigned char*)0xFCA)
51 #define PR2 (*(volatile __near unsigned char*)0xFCB)
52
53 #define CCP2CON (*(volatile __near unsigned char*)0xFBA)
54 #define CCPR2L (*(volatile __near unsigned char*)0xFBB)
55 #define CCP1CON (*(volatile __near unsigned char*)0xFBD)
56 #define CCPR1L (*(volatile __near unsigned char*)0xFBE)
57
58 #define SSPCON2 (*(volatile __near unsigned char*)0xFC5)
59 #define SSPCON1 (*(volatile __near unsigned char*)0xFC6)
60 #define SSPSTAT (*(volatile __near unsigned char*)0xFC7)
61 #define SSPAD (*(volatile __near unsigned char*)0xFC8)
62 #define SSPBUF (*(volatile __near unsigned char*)0xFC9)
63
64
65 #define OSCCON (*(volatile __near unsigned char*)0xFD3)