Вы находитесь на странице: 1из 118

Unifei - Universidade Federal de Itajubá

Notas de Aula ECOP04


Programação de sistemas embarcados

IESTI - Instituto de Engenharia de Sistemas e Tecnologia da Informação

AUTOR

Rodrigo Maximiano Antunes de Almeida


Sumário

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

3 * Operações com bits, 23

4 * Debug de sistemas embarcados, 33

2
C APÍTULO

1
Introdução
1.1 Hardware utilizado 4
1.2 Ambiente de programação 4
Instalação 4

“The real danger is not that computers will begin to


think like men, but that men will begin to think like com-
puters.”
Sydney J. Harris

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

Figura 1.1: Camadas de abstração de um sistema operacional

Para sistemas embarcados, é necessário programar especificamente para o hardware em questão.

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.

Hardware utilizado 1.1

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.

Ambiente de programação 1.2

O ambiente utilizado será o MPLABX(R). Este é um ambiente de desenvolvimento disponibili-


zado pela Microchip(R) gratuitamente. O compilador utilizado será o SDCC, os linkers e assemblers
serão disponibilizados pela biblioteca GPUtils.
Como o foco é a aprendizagem de conceitos sobre programação embarcada, poderá ser utilizada
qualquer plataforma de programação e qualquer compilador/linker. Caso seja utilizado qualquer
conjunto de compilador/linker diferentes deve-se prestar atenção apenas nas diretivas para grava-
ção.

Instalação

A Tabela 1.1 apresenta os softwares que serão utilizados no curso.


Tabela 1.1: Softwares utilizados no curso
Item Versão Licença
IDE MPLABX 3.00 Mista
Compilador SDCC 3.1.00 (win32) GPL
Linker/Assembler GPUtils 1.5.0 (win32) GPL
Plugin SDCC Toolchain 1.2 GPL
C APÍTULO

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

“C is quirky, flawed, and an enormous success.”


Dennis M. Ritchie

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.

Figura 2.1: Linguagens mais utilizadas


Fonte: http://www.tiobe.com

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

A descontinuidade depois de 2004 se dá devido à mudança de metodologia da pesquisa. Antes


de 2005, a pergunta formulada era: “Para o desenvolvimento da sua aplicação embarcada, quais
das linguagens você usou nos últimos 12 meses?”. Em 2005 a pergunta se tornou: “Meu projeto
embarcado atual é programado principalmente em ______”. Múltiplas seleções eram possíveis antes
de 2005, permitindo a soma superior a 100%, sendo o valor médio de 209%, o que implica que a
maioria das pessoas escolheu duas ou mais opções.
O maior impacto na pesquisa pode ser visualizado na linguagem assembler: até 2004, estava
presente em 62% das respostas (na média). O que comprova que praticamente todo projeto de
sistema embarcado exige um pouco de assembler. Do mesmo modo, percebemos que atualmente
poucos projetos são realizados totalmente ou em sua maioria em assembler, uma média de apenas
7%.

Indentação e padrão de escrita 2.1

É fundamental obedecer a um padrão para escrita de programas, de modo que a visualização


do código seja facilitada.
Na língua portuguesa utilizamos parágrafos para delimitar blocos de frases que possuem a
mesma ideia. Em linguagem C estes blocos são delimitados por chaves “{” e “}”.
Para demonstrar ao leitor que um parágrafo começou utilizamos um recuo à direita na primeira
linha. Quando é necessário realizar uma citação de itens coloca-se cada um destes itens numa linha
recuada à direita, algumas vezes com um identificador como um traço “-” ou seta “->” para facilitar
a identificação visual.
Com esse mesmo intuito, os recuos e espaçamentos são utilizados para que o código seja mais
facilmente entendido.
Como todo bloco de comandos é iniciado e terminado com uma chave, tornou-se comum que
estas (as chaves) estejam no mesmo nível e todo código interno a elas seja deslocado à direita. Se
existir um segundo bloco interno ao primeiro, este deve ser deslocado duas vezes para indicar a
hierarquia no fluxo do programa. Segue abaixo um exemplo de um mesmo código com diferença
apenas na indentação.
Código indentado Código não indentado

1 void main(void){ 1 void main(void) {


2 unsigned int i; 2 unsigned int i;
3 unsigned int temp; 3 unsigned int temp;
4 unsigned int teclanova=0; 4 unsigned int teclanova=0;
5 serialInit(); 5 serialInit();
6 ssdInit(); 6 ssdInit();
7 InicializaLCD(); 7 InicializaLCD();
8 InicializaAD(); 8 InicializaAD();
9 for(;;) { 9 for(;;) {
10 ssdUpdate(); 10 ssdUpdate();
11 if (teclanova != Tecla) { 11 if (teclanova != Tecla) {
12 teclanova = Tecla; 12 teclanova = Tecla;
13 for(i=0;i<16;i++) { 13 for(i=0;i<16;i++) {
14 if (BitTst(Tecla,i)){ 14 if (BitTst(Tecla,i)) {
15 EnviaDados(i+48); 15 EnviaDados(i+48);
16 } 16 }
17 } 17 }
18 } 18 }
19 for(i = 0; i < 1000; i++); 19 for(i = 0; i < 1000; i++);
20 } 20 }
21 } 21 }

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:

1 if (PORTA == 0x30){ PORTB = 0x10; }

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 }

Código 2.2: Resumo do ssd.h


1 #ifndef VAR_H
2 #define VAR_H
3 void MudaDigito(char val);
4 char LerDigito(void);
5 void ssdInit(void);
6 #endif //VAR_H

Diretivas de compilação 2.4

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 }

Função Original Opções de uso com o #define Resultado na Tela

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.

#ifdef, #ifndef, #else e #endif

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:

1 void ImprimirTemp(char valor){


2 #ifdef LCD
3 Imprime_LCD(valor)
4 #else
5 if (valor > 30) {
Código 2.3: Estrutura de header
1 #ifndef TAG_CONTROLE
2 #define TAG_CONTROLE
3 //todo o conteúdo do arquivo vem aqui.
4
5 #endif //TAG_CONTROLE

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);

O arquivo de controle da temperatura (temp.h) possui as funções apresentadas a seguir.

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);

Figura 2.3: Problema das Referências Circulares

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”

temp.h char LerSerial(void);


void EnviaSerial(char val);
#ifndef TEMP_H #endif

//tag já definida,
//pula o conteúdo

#endif

Figura 2.4: Solução das referências circulares com #ifndef

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.

Representação binária e hexadecimal

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 .

Modificadores de tamanho e sinal

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:

1 #define X (*(near unsigned char*)0xF83)


2 void main(void) {
3 while (X!=X);
4 }

Quando compilado apresenta o seguinte código em assembler:


1 // Starting pCode block
2 S_Teste__main code
3 _main:
4 .line 19 // Teste.c while (X!=X);
5
6 RETURN

Enquanto a variável “x” for diferente de “x” o programa não sai do loop. O compilador entende
que esta condição nunca irá acontecer e elimina o loop do código 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.

1 #define X (*(volatile near unsigned char*)0xF83)


2 void main(void) {
3 while (X!=X);
4 }

Gerando o código em assembler descrito abaixo:

1 // Starting pCode block


2 S_Teste__main code
3 _main:
4 _00105_DS_:
5 .line 19 // Teste.c while (X != X);
6 MOVLW 0x83 //primeira parte do endereço
7 MOVWF r0x00
8 MOVLW 0x0f //segunda parte do endereço
9 MOVWF r0x01
10 MOVFF r0x00, FSR0L
11 MOVFF r0x01, FSR0H
12 MOVFF INDF0, r0x00 //realiza primeira leitura
13 MOVLW 0x83 //primeira parte do endereço
14 MOVWF r0x01
15 MOVLW 0x0f //segunda parte do endereço
16 MOVWF r0x02
17 MOVFF r0x01, FSR0L
18 MOVFF r0x02, FSR0H
19 MOVFF INDF0, r0x01 //realiza segunda leitura
20 MOVF r0x00, W
21 XORWF r0x01, W
22 BNZ _00105_DS_ //faz o teste para igualdade
23 RETURN

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.

1 #define X (*(volatile const near unsigned char*)0xF83)


2 //início do programa
3 void main(void) {
4 X = 3;
5 }

Se tentarmos compilar este código aparecerá a seguinte mensagem de erro:

1 Teste.c: error 33: Attempt to assign value to a constant variable (=)

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.

1 //cria um contador persistente que é


2 //incrementado a cada chamada de função
3 int ContadorPersistente(int reseta){
4 static char variável_persistente;
5 if (reseta) {
6 variável_persistente = 0;
7 }else{
8 return (variável_persistente++);
9 }
10 return -1;
11 }

Operações aritméticas 2.6

Um cuidado a se tomar, na programação em C para sistemas embarcados, é o resultado de


operações aritméticas. Por padrão na linguagem C o resultado de uma operação aritmética possui
tamanho igual ao maior operando. Observando o Programa 2.4 notamos alguns exemplos.
No caso 1 (linha 8) uma variável char somada a um int gera como resultado um int (maior
operando). Não é possível armazenar esse resultado num char, haverá perda de informação.

1 var32 = var8 + var16; // 1 corrigido


Código 2.4: Operações aritméticas com tipos diferentes
1 void main (void){
2 char var08;
3 int var16;
4 long int var32;
5 float pont16;
6 double pont32;
7 var08 = var08 + var16; // 1
8 var08 = var08 + var08; // 2
9 var16 = var08 * var08; // 3
10 var32 = var32 / var16; // 4
11 var32 = pont32 * var32; // 5
12 pont16 = var08 / var16; // 6
13 pont16 = pont32 * var32; // 7
14 pont16 = 40 / 80; // 8
15 }

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.

1 var16 = var8 + var8; // 2 corrigido

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.

1 var16 = ((int)var8) * var8; // 3 corrigido

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.

1 pont16 = ((float)var8) / var16; // 6 corrigido

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.

1 pont16 = 40f / 80.0; // 8 corrigido

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.

Figura 2.5: Loop infinito de um device driver gerando erro no sistema

Função main() 2.7

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

1 void main (void){


2 //aqui entra o código do programa
3 }

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.

1 void main (void) { 1 void main (void) {


2 for(;;) { 2 while(1) {
3 //aqui entra o 3 //aqui entra o
4 //código principal 4 //código principal
5 } 5 }
6 } 6 }

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-

cução controlada através de interrupções.


2 Este método não é aconselhado em sistemas de maior porte.
1 unsigned char i;
2 for(i=0; i < 10; i++);

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.

1 //código em assembler equivalente à for(i=0; i<10; i++);


2 MOVF r0x00, W //inicializa W com 0 (1 ciclo)
3 SUBLW 0x0a //coloca o valor 10 (0x0a) no registro W (1 ciclo)
4 MOVWF r0x00 //muda o valor de W para F (1 ciclo)
5 _00107_DS_:
6 DECFSZ r0x00, F //decrementa F, se F > 0 executa a próxima linha (1 ciclo)
7 BRA _00107_DS_ //"pula" para o lugar marcado como _00107_DS_ (2 ciclos)

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

assembler será diferente e teremos que realizar uma nova análise.


4 Para 8MHz, 1 ciclo = 0.125µs. No PIC, cada instrução precisa de 4 ciclos de clock, portanto 0.5µs.
10 }
11 }

Ponteiros e endereços de memória 2.8

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:

1 //cria a variável a num endereço de memória a ser


2 //decidido pelo compilador
3 int a = 0;
4 a = a + 1;
5 printf( a ); //imprime o valor 1
6 printf( &a ); //imprime o endereço de a (por exemplo 157821)

Conhecer o endereço de uma variável é muito útil quando queremos criar um ponteiro para ela.
Ponteiro é uma variável que, ao invés de armazenar valores, armazena endereços de memória.
Através do ponteiro é possível manipular o que está dentro do lugar apontado por ele.
Para 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:

1 tipo * nome da variável;

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:

1 //definindo a variável ivar


2 int ivar;
3 //definindo o ponteiro iptr
4 int *iptr;
5 //o ponteiro iptr recebe o valor do endereço da variável ivar
6 iptr = &ivar;
7 // as próximas linhas são equivalentes
8 ivar = 421;
9 *iptr = 421;

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

“All of the books in the world contain no more informa-


tion than is broadcast as video in a single large American
city in a single year. Not all bits have equal value.”
Carl Sagan

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

Figura 3.1: Circuito equivalente de uma porta tipo NÃO

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.

Declaração Lógico Bitwise

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

Figura 3.2: Circuito equivalente de uma porta tipo E

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:

Declaração Lógico Bitwise

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

Figura 3.3: Circuito equivalente de uma porta tipo OU

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:

Declaração Lógico Bitwise

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

Figura 3.4: Circuito equivalente de uma porta tipo OU EXCLUSIVO

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:

Declaração Lógico Bitwise

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.

Declaração Shift Esquerda Shift Direita

1 result = A << 2; 1 result = A >> 2;


1 char A = 8; 2 // result = 32 2 // result = 2
2 // A = 0b00001000 3 // A = 0b00001000 3 // A = 0b00001000
4 // r = 0b00100000 4 // r = 0b00000010

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):

Posição N ... X+1 X X-1 ... 0


Valor 0 ... 0 1 0 ... 0

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

1 //define's para portas de entrada e saída


2 #define PORTD (*(volatile near unsigned char*)0xF83)
3 #define TRISD (*(volatile near unsigned char*)0xF95)
4 //início do programa
5 void main(void)
6 {
7 char mascara; //variável que guarda a máscara
8 TRISD = 0x00; //configura a porta D como saída
9 PORTD = 0x00; //liga todos os leds (lógica negativa)
10 //liga o primeiro bit da variável
11 mascara = 1; // bit = 0b00000001
12 // rotaciona−se a variável para que o bit 1 chegue na posição desejada
13 mascara = mascara << 2; // bit = 0b00000100
14 //Ligar o bit 2, desligando o 3o led
15 PORTD = PORTD | mascara;
16 //mantém o sistema ligado indefinidamente
17 for(;;);
18 }

Desligar um bit (bit clear) 3.7

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.

posição N ... X+1 X X-1 ... 0


Valor 1 ... 1 0 1 ... 1

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.

1 //define's para portas de entrada e saída


2 #define PORTD (*(volatile near unsigned char*)0xF83)
3 #define TRISD (*(volatile near unsigned char*)0xF95)
4 //início do programa
5 void main(void)
6 {
7 char mascara; //variável que guarda a máscara
8 TRISD = 0x00; //configura a porta D como saída
9 PORTD = 0xFF; //desliga todos os leds (lógica negativa)
10 //liga o primeiro bit da variável
11 mascara = 1; // mascara = 0b00000001
12 // rotaciona−se a variável para que o bit 1 chegue na posição desejada
13 mascara = mascara << 2; // mascara = 0b00000100
14 // inverte−se os valores de cada bit
15 mascara = ~mascara; // mascara = 0b11111011
16 //Desliga o bit 2, ligando o 3o led
17 PORTD = PORTD & mascara;
18 //mantém o sistema ligado indefinidamente
19 for(;;);
20 }

É 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.

Trocar o valor de um bit (bit flip) 3.8

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:

posição N ... X+1 X X-1 ... 0


Valor 0 ... 0 1 0 ... 0

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

1 //define's para portas de entrada e saída


2 #define PORTD (*(volatile near unsigned char*)0xF83)
3 #define TRISD (*(volatile near unsigned char*)0xF95)
4 //início do programa
5 void main(void)
6 {
7 char mascara; //variável que guarda a mascara
8 TRISD = 0x00; //configura a porta D como saída
9 PORTD = 0xF0; //desliga todos os 4 primeiros leds (lógica negativa)
10 //liga o primeiro bit da variável
11 mascara = 1; // mascara = 0b00000001
12 // rotaciona−se a variável para que o bit 1 chegue na posição desejada
13 mascara = mascara << 2; // mascara = 0b00000100
14 //Liga o bit 2, desligando o 3o led
15 PORTD = PORTD ^ mascara;
16 //liga o primeiro bit da variável
17 mascara = 1; // mascara = 0b00000001
18 // rotaciona−se a variável para que o bit 1 chegue na posição desejada
19 mascara = mascara << 6; // mascara = 0b01000000
20 //Desliga o bit 6, ligando o 7o led
21 PORTD = PORTD ^ mascara;
22 //mantém o sistema ligado indefinidamente
23 for(;;);
24 }

Percebemos através do exemplo que a utilização do procedimento apresentado troca o valor do


bit escolhido. Foi utilizado o mesmo procedimento duas vezes. Na primeira, um bit foi ligado e, na
segunda, outro foi desligado.

Verificar o estado de um bit (bit test) 3.9

Para verificar se o bit X está com o valor 1 utilizaremos novamente a mesma máscara utilizada
para bit set e bit toggle:

posição N ... X+1 X X-1 ... 0


Valor 0 ... 0 1 0 ... 0

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

1 //define's para portas de entrada e saída


2 #define PORTD (*(volatile near unsigned char*)0xF83)
3 #define TRISD (*(volatile near unsigned char*)0xF95)
4 //início do programa
5 void main(void) {
6 char mascara; //variável que guarda a mascara
7 char teste;
8 TRISD = 0x00; //configura a porta D como saída
9 teste = 0x00; //desliga todos os bits
10 //rodar depois o mesmo programa com os bits ligados.
11 //teste = 0xff;
12 // cria uma variável onde APENAS o primeiro bit é 1
13 mascara = 1; // mascara = 0b00000001
14 // rotaciona−se a variável para que o bit 1 chegue na posição desejada
15 mascara = mascara << 2; // mascara = 0b00000100
16 //Verifica apenas o bit 2
17 if (teste & mascara) {
18 PORTD = 0x00; //se o resultado for verdadeiro liga todos os leds
19 } else {
20 PORTD = 0xff; //se o resultado for falso desliga todos os leds
21 }
22 //mantém o sistema ligado indefinidamente
23 for(;;);
24 }

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

Operação Bit set

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 //Ligando o bit 2 da porta D


2 PORTD = PORTD | (1<<2);
Exemplo de uso 3 //ou
4 PORTD |= (1<<2);

1 #define BitSet(arg,bit) ((arg) |= (1<<bit))


Com define

1 //Ligando o bit 2 da porta D


Exemplo de uso com de-
2 BitSet(PORTD,2);
fine
Tabela 3.2: Operação bit clear com define

Operação Bit clear

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 //Desligando o bit 2 da porta D


2 PORTD = PORTD & ~(1<<2);
Exemplo de uso 3 //ou
4 PORTD &= ~(1<<2);

1 #define BitClr(arg,bit) ((arg) &= ~(1<<bit))


Com define

1 //Desligando o bit 2 da porta D


Exemplo de uso com de-
2 BitClr(PORTD,2);
fine

Tabela 3.3: Operação bit flip com define

Operação Bit flip

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 //Trocando o valor do bit 2 da porta D


2 PORTD = PORTD ^ (1<<2);
Exemplo de uso 3 //ou
4 PORTD ^= (1<<2);

1 #define BitFlp(arg,bit) ((arg) ^= (1<<bit))


Com define

1 //Trocando o valor do bit 2 da porta D


Exemplo de uso com de-
2 BitFlp(PORTD,2);
fine
Tabela 3.4: Operação bit test com define

Operação Bit test

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))

1 //Testando o bit 2 da porta D


2 if (PORTD | (1<<2)){
Exemplo de uso 3 //...
4 }

1 #define BitTst(arg,bit) ((arg) & (1<<bit))


Com define

1 //Testando o bit 2 da porta D


2 if (BitTst(PORTD,2)){
Exemplo de uso com de-
3 //...
fine 4 }
C APÍTULO

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.

Externalizar as informações 4.1

A primeira necessidade é conhecer o que está acontecendo em teu sistema. Na programação


tradicional para desktop é comum utilizarmos de mensagens no console avisando o estado do
programa.

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

Stan Schneider e Lori Fraleigh

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 .

1 //define's para portas de entrada e saída


2 #define PORTD (*(volatile near unsigned char*)0xF83)
3 #define TRISD (*(volatile near unsigned char*)0xF95)
4 //início do programa
5 void main(void) {
6 //configurando todos os pinos como saídas
7 TRISD = 0x00;
8 PORTD = 0xFF; //desliga todos os leds
9 //liga apenas o bit 1.
10 BitClr(PORTD,1);
11 //mantém o sistema ligado indefinidamente
12 for(;;);
13 }

Devemos utilizar os leds como sinais de aviso para entendermos o funcionamento do programa.
Isto pode ser feito através das seguintes ideias: “Se passar desta parte liga o led X”, “Se entrar no
IF liga o led Y, se não entrar liga o led Z”, “Assim que sair do loop liga o led W”.

Programação incremental 4.2

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á.

Checar possíveis pontos de memory-leak 4.3

Se for necessário realizar alocação dinâmica, garantir que todas as alocações sejam liberadas em
algum ponto do programa.

Cuidado com a fragmentação da memória 4.4

Sistemas com grande frequência na alocação/liberação de memória podem fragmentar a me-


mória até o ponto de inviabilizar os espaços livres disponíveis, eventualmente travando o sistema.
Quando trabalhar com rotinas de nível mais baixo, mais próximo ao hardware, tente utilizar apenas
mapeamento estático de memória.
2 As variáveis PORTD e TRISD são definidas como unsigned char e possuem portanto 8 bits.
Otimização de código 4.5

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.

Reproduzir e isolar o erro 4.6

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.

1 // aqui tem um monte de código...


2 if (PORTB >= 5){ //PORTB não deveria ser um valor maior que 5.
3 BitClr(PORTD,3); //liga o led 3
4 for(;;); //trava o programa
5 }
6 // aqui continua com um monte de código...
Arquitetura de microcontroladores
Parte II

5 * Microcontrolador, 37

6 * Esquema elétrico e circuitos importantes, 43

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

“Any sufficiently advanced technology is indistinguisha-


ble from magic.”
Arthur C. Clarke

Os microcontroladores são formados basicamente por um processador, memória e periféricos


interligados através de um barramento conforme Figura 5.1.
Em geral, os periféricos são tratados do mesmo modo que a memória, ou seja, para o processador
não existe diferença se estamos tratando com um valor guardado na memória RAM ou com o valor
da leitura de um conversor analógico digital. Isto acontece porque existem circuitos eletrônicos
que criam um nível de abstração em hardware. Deste modo todos os dispositivos aparecem como
endereços de memória.

37
Figura 5.1: Arquitetura do microcontrolador PIC 18f4520
Acesso à memória 5.1

A quantidade de memória disponível que um microcontrolador pode acessar depende de dois


fatores, os tamanhos das palavras de dados e das palavras de endereço.
O tamanho da palavra de dados representa quantos bits podem ser colocados numa única posi-
ção da memória.
O tamanho da palavra de endereço indica quantas posições de memória o processador consegue
enxergar.
Por exemplo, um microcontrolador cujo tamanho de dados e o tamanho da palavra de endereço
são ambos 8 bits possui uma possibilidade de acessar uma memória de até:
tamanho_da_palavra * 2^tamanho_do_endereco
8 * 2^8 = 2048 bytes ou 2 kbytes

O termo possibilidade foi usado pois apesar de poder alcançar toda essa extensão, nem sempre
existe memória física para armazenamento. Podemos imaginar a memória como um armário. Um
armário com 6 suportes pode abrigar até 6 gavetas. Depende do marceneiro fabricar e colocar as
gavetas neste armário. Podemos até indicar a posição onde queremos guardar algum objeto, mas
se a gaveta não foi colocada não é possível armazenar nada (Figura 5.2).

Suporte Existe
número: gaveta?
1 sim

2 sim

3 não

4 não

5 sim

6 não

Figura 5.2: Memória como um armário

Ao invés de gavetas o marceneiro pode pensar em colocar outros “sistemas de armazenamento”


nos espaços (Figura 5.3). Alguns destes sistemas podem permitir que o usuário enxergue o que está
dentro mas não mexa. Alguns vão permitir que o usuário coloque coisas mas não retire. Outros
ainda podem permitir que a pessoa retire objetos mas não possa repô-los.
Estes vários “sistemas de armazenamento” representam a variedade de tipos de memória e de
interfaces de periféricos que possuímos.
A memória é identificada através de um endereço. Por estarmos tratando de sistemas digitais,
o valor do endereço é codificado em binário. Como visto anteriormente, escrever em binário é
trabalhoso e muito propenso a gerar erros. Visando facilitar esse processo, é comumente adotado o
sistema hexadecimal para indicar o local de memória.
Os dispositivos são então ligados a um determinado número de endereço que passa a identificá-
lo de forma única. O mesmo acontece para a memória RAM e memória ROM. Elas estão ligadas a
uma série de endereços. Se, portanto, é preciso salvar uma variável na memória, tem-se que saber
quais endereços estão ligados à memória RAM. Se quisermos ler alguma informação gravada na
memória ROM, é preciso conhecer a localização exata antes de realizar a leitura.
A porta D do microcontrolador PIC 4520 por exemplo está no endereço 0xF83, destinado aos
registros de função especial ou special function register, SFR Figura 5.4).
Suporte Existe
número: gaveta?
1 Vitrine

2 Gaveta

3 Dispenser

4 Não

5 Gaveta

6 Cofre

Figura 5.3: Memória e periféricos como um armário

Stack 1 0x000
GPR1
... 0x0FF
Stack 31 0x100
GPR2
0x1FF
Interrupção

Reset 0x0000 0x200


Vetor de

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

Figura 5.4: Regiões de memórias disponíveis no PIC 18f4520

Clock e tempo de instrução 5.2

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

flutuante que se assemelha à notação científica.


Multiplicação de inteiros Multiplicação de fracionários

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.

Registros de configuração do microcontrolador 5.3

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;

A primeira coluna de números indica a posição do registro que armazena as configurações. A


segunda coluna representa os códigos utilizados para configuração. Para conhecer as demais opções
de configurações devemos consultar o manual do usuário.
Estas configurações são dependentes do compilador a ser usado. A seguir demonstramos os
códigos necessários para o compilador C18 da Microchip(R) ou para o próprio SDCC após a versão
3.0.

1 #pragma config MCLRE=ON // Master Clear desabilitado


2 #pragma config OSC=HS // Oscilador c/ cristal externo HS
3 #pragma config WDT=OFF // Watchdog controlado por software
4 #pragma config LVP=OFF // Sem programação em baixa tensão
5 #pragma config WDTPS = 1 // Configura prescaler do watchdog

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

“I don’t think people change; electronics change, the


things we have change, but the way we live doesn’t
change.”
Judy Blume

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.

Figura 6.1: Esquema elétrico: Microcontrolador PIC 18f4520

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).

Multiplexação nos terminais do microcontrolador 6.1

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

11 * Display LCD 2x16, 65

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

“People who are really serious about software should


make their own hardware.”
Alan Kay

Periféricos são os componentes, circuitos ou sistemas ligados ao microcontrolador, interna ou


externamente. Eles podem ser utilizados para realizar interface entre o homem e o equipamento ou
incluir funcionalidades extras ao sistema, como comunicação, segurança, etc.
Os periféricos de saída1 que serão abordados neste curso serão:
• Barramento de Led's(8)
• Display de 7 segmentos(9)
• Display LCD 2x16(11)
• Saídas PWM(14)
Entre os periféricos de entrada2 temos:
• Leitura de teclas(10)
• Conversor AD(13)
Além de sistemas de entrada e saída de dados será abordado um dispositivo de comunicação
serial (12), um tratador de Interrupção(16), um Timer(15) e um dispositivo de segurança: Watch-
dog(17).

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

Aqui o conceito de ponteiros se faz extremamente necessário. Já que conhecemos os endereços


fixos onde as portas se encontram, podemos criar ponteiros para tais endereços de forma que
possamos utilizar as portas como se fossem variáveis. Exemplo:

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.

1 //define's para portas de entrada e saída


2 #define PORTD (*(volatile near unsigned char*)0xF83)
3 #define TRISD (*(volatile near unsigned char*)0xF95)
4 //início do programa
5 void main(void) {
6 //configurando todos os pinos como saídas
7 TRISD = 0b00000000;
8 //liga apenas os quatro últimos leds
9 PORTD = 0b11110000;
10 //mantém o sistema ligado indefinidamente
11 for(;;);
12 }

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.

Configuração dos periféricos 7.2

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.

1 TRISA = 0b00000001; //configurando os terminais como entrada e saída


2 ADCON1 = 0b00001110; //apenas o primeiro terminal é analógico

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.

1 TRISB = 0b11110000; //configurando os terminais como entrada e saída


2 //configuração com interrupção habilitada
3 INTCON = 0b11000101;
4 INTCON2 = 0b00000001;
5 //configuração sem interrupção
6 INTCON = 0b00000000;
7 INTCON2 = 0b00000001;
8 SPPCFG = 0b00000000; //RB0 e RB4 são controlados pela porta B e não pelo SPP

A porta C possui o segundo e terceiro bit como saída PWM e o sétimo e oitavo como comunica-
ção serial.

1 TRISC = 0b10000000; //configurando os terminais como saída, apenas RC7 é entrada


Figura 7.1: Registros de configuração dos periféricos do PIC 18f4520

2 CCP1CON = 0b00001100; //configura o segundo terminal como PWM


3 CCP2CON = 0b00001100; //configura o terceiro terminal como PWM
4 TXTA = 0b00101100; //configura a transmissão de dados da serial
5 RCSTA = 0b10010000; //configura a recepção de dados da serial
6 BAUDCON = 0b00001000; //configura sistema de velocidade da serial
7 SPBRGH = 0b00000000; //configura para 56k
8 SPBRG = 0b00100010; //configura para 56k

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.

1 TRISD = 0b00000000; //configurando os terminais como saída

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

1 TRISE = 0b00000000; //configurando os terminais como saída


C APÍTULO

8
Barramento de Leds

“In theory, there is no difference between theory and


practice; In practice, there is.”
Chuck Reid

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.

Figura 8.1: Barramento de Led's

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

“The light in the world comes principally from two sour-


ces, —the sun, and the student’s lamp.”
Christian Nestell Bovee

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.

Figura 9.1: Display de 7 Segmentos


Fonte: Peter Halasz - http://commons.wikimedia.org/wiki/File:Seven_segment_02_Pengo.jpg

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

Ordem inversa Ordem direta


Display a b c d e f g Hex (0abcdefg) g f e d c b a Hex (0gfedcba)
0 1 1 1 1 1 1 0 7E 0 1 1 1 1 1 1 3F
1 0 1 1 0 0 0 0 30 0 0 0 0 1 1 0 06
2 1 1 0 1 1 0 1 6D 1 0 1 1 0 1 1 5B
3 1 1 1 1 0 0 1 79 1 0 0 1 1 1 1 4F
4 0 1 1 0 0 1 1 33 1 1 0 0 1 1 0 66
5 1 0 1 1 0 1 1 5B 1 1 0 1 1 0 1 6D
6 1 0 1 1 1 1 1 5F 1 1 1 1 1 0 1 7D
7 1 1 1 0 0 0 0 70 0 0 0 0 1 1 1 07
8 1 1 1 1 1 1 1 7F 1 1 1 1 1 1 1 7F
9 1 1 1 1 0 1 1 7B 1 1 0 1 1 1 1 6F
A 1 1 1 0 1 1 1 77 1 1 1 0 1 1 1 77
b 1 0 1 1 1 1 0 5E 1 1 1 1 1 0 0 7C
C 1 0 0 1 1 1 1 4F 0 1 1 1 0 0 1 39
d 0 1 1 1 1 0 1 3D 1 0 1 1 1 1 0 5E
E 1 0 0 1 1 1 1 4F 1 1 1 1 0 0 1 79
F 1 0 0 0 1 1 1 47 1 1 1 0 0 0 1 71

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 }

Multiplexação de displays 9.1

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

Criação da biblioteca 9.2

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.

Código 9.1: Utilizando a biblioteca ssd


1 #include "config.h"
2 #include "ssd.h"
3
4 //início do programa
5 void main(void) {
6 unsigned int tempo;
7 ssdInit();
8 ssdDigit(0,0);
9 ssdDigit(1,1);
10 ssdDigit(2,2);
11 ssdDigit(3,3);
12 for(;;) {
13 ssdUpdate();
14 //gasta um tempo para evitar o efeito flicker
15 for(tempo=0;tempo<1000;tempo++);
16 }
17 }
Código 9.3: ssd.h
1 #ifndef ssd_H
2 #define ssd_H
3 void ssdDigit(char val, char pos);
4 void ssdUpdate(void);
5 void ssdInit(void);
6 #endif

Código 9.2: ssd.c


1 #include "ssd.h"
2 #include "pic18f4520.h" 35 void ssdUpdate(void) {
3 36 //desliga todos os displays
4 //vetor para armazenar a conversão do display 37 PORTA = 0x00;
5 static const char valor[] = {0x3F, 0x06, ←- 38 PORTE = 0x00;
0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, ←- 39 //desliga todos os leds
0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, ←- 40 PORTD = 0x00;
0x79, 0x71}; 41
6 //armazena qual é o display disponivel 42 switch (display){ //liga apenas o ←-
7 static char display; display da vez
8 //armazena o valor a ser enviado ao display 43 case 0:
9 static char v0, v1, v2, v3; 44 PORTD = valor[v0];
10 45 BitSet(PORTA, 5);
11 void ssdDigit(char val, char pos) { 46 display = 1;
12 if (pos == 0) { 47 break;
13 v0 = val; 48 case 1:
14 } 49 PORTD = valor[v1];
15 if (pos == 1) { 50 BitSet(PORTA, 2);
16 v1 = val; 51 display = 2;
17 } 52 break;
18 if (pos == 2) { 53 case 2:
19 v2 = val; 54 PORTD = valor[v2];
20 } 55 BitSet(PORTE, 0);
21 if (pos == 3) { 56 display = 3;
22 v3 = val; 57 break;
23 } 58 case 3:
24 } 59 PORTD = valor[v3];
25 60 BitSet(PORTE, 2);
26 void ssdInit(void) { 61 display = 0;
27 //configuração dos pinos de controle 62 break;
28 BitClr(TRISA, 2); 63 default:
29 BitClr(TRISA, 5); 64 display = 0;
30 BitClr(TRISE, 0); 65 break;
31 BitClr(TRISE, 2); 66 }
32 ADCON1 = 0x0E; //apenas AN0 é analogico, ←- 67 }
a referencia é baseada na fonte
33 TRISD = 0x00; //Porta de dados
34 }
C APÍTULO

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

“It’s supposed to be automatic, but actually you have to


push this button.”
John Brunner

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.

Figura 10.1: Circuito de leitura de chave


http://www.labbookpages.co.uk/electronics/debounce.html - Dr. Andrew Greensted

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).

Figura 10.2: Oscilação do sinal no momento do chaveamento


http://www.labbookpages.co.uk/electronics/debounce.html - Dr. Andrew Greensted

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.

Figura 10.3: Circuito de debounce


http://www.ikalogic.com/debouncing.php - Ibrahim Kamal

Neste circuito, o capacitor desempenha o papel de amortecedor do sinal. Um circuito com um


resistor e um capacitor possui um tempo de atraso para o sinal. Este é o tempo necessário para
carregar o capacitor. Deste modo as alterações rápidas no sinal, devido à oscilação mecânica da
chave, são filtradas e não ocorre o problema dos chaveamentos indevidos, como pode ser visto na
Figura 10.4. Notar que o nível do sinal filtrado não chega a zero em nenhum momento, devido à
constante de tempo do filtro RC ser maior que o período de debounce.
Figura 10.4: Utilização de filtro RC para debounce do sinal
http://www.labbookpages.co.uk/electronics/debounce.html – A. Greensted (modificado)

Debounce por software 10.1

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 Lembrar que cada ciclo do PIC necessita de 4 ciclos de clock externo


17 if (valTemp == PORTB){ // se não mudar continua a contar
18 tempo--;
19 } else {
20 valTemp = PORTB; // se mudar, atualiza o sistema e reinicia o tempo
21 tempo = 22;
22 }
23 }
24 valAtual = valTemp; //valor atualizado;
25 PORTD = valAtual; //coloca o valor no barramento de leds
26 }
27 }

Arranjo de leitura por matriz 10.2

Para cada tecla/botão que é colocado no projeto, é necessário um terminal do microcontrolador.


Para um teclado maior é possível que o microcontrolador não possua terminais disponíveis em
quantidade suficiente. Do mesmo modo que no caso dos displays é possível multiplexar as chaves
aumentando a quantidade de chave por terminal. Novamente, este ganho, em termos de hardware,
aumenta a complexidade para o software e juntamente com este, o custo em termos de tempo de
computação.
Uma das técnicas mais eficientes para a geração deste teclado é o arranjo em formato matricial.
Com esta configuração podemos, com N terminais, ler até ( N/2)2 chaves.

Figura 10.5: Teclado em arranjo matricial


Conforme podemos ver na Figura 10.5, cada chave pode ser identificada unicamente pela sua
posição (linha, coluna). Os terminais ligados às linhas serão configurados como entrada, que servi-
rão para ler os valores das teclas. Os terminais ligados às colunas serão configurados como saídas,
fornecendo energia para as chaves. A leitura é realizada então por um processo conhecido como
varredura: liga-se uma coluna por vez e verifica-se quais chaves daquela coluna estão ligadas.

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.

Usando tristate ou terminais como entrada 10.3

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 }

Criação da biblioteca 10.4

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 }

Código 10.2: keypad.h


1 #ifndef KEYPAD_H
2 #define KEYPAD_H
3 unsigned char kpRead(void);
4 void kpDebounce(void);
5 void kpInit(void);
6 #endif

Código 10.3: Exemplo de uso da biblioteca teclado


1 #include "p18f4520.h"
2 #include "config.h"
3 #include "keypad.h"
4
5 void main(void) {
6 kpInit();
7 TRISD = 0x00; //Configura a porta D como saída
8 PORTD = 0xFF; //desliga todos os leds
9 for(;;){
10 kpDebounce();
11 PORTD = kpRead();
12 }
13 }
C APÍTULO

11
Display LCD 2x16
11.1 Criação da biblioteca 70

“What I was proud of was that I used very few parts


to build a computer that could actually speak words on a
screen and type words on a keyboard and run a program-
ming language that could play games. And I did all this
myself.”
Steve Wozniak

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.

Figura 11.1: Display Alfanumérico LCD 2x16

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

se existe ou não retro iluminação. Estes terminais são identificados como:

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)

Trabalharemos apenas com 11 terminais: os 3 de controle do display (RS,RW,EN) e os 8 para o


barramento de dados. Este tipo de display possui, integrado, um microcontrolador para realizar as
rotinas de manutenção do estado do display, controle da luminosidade e interface com o restante
do sistema eletrônico. A comunicação é realizada através de um barramento paralelo de 8 bits, por
onde são enviados os dados/comandos.
O terminal RS indica ao display se o barramento contém um comando a ser executado (0) ou
uma informação para ser exibida (1).
O terminal RW indica ao display se ele receberá um valor (0) ou se estamos requisitando alguma
informação (1).
O terminal EN indica ao display que ele pode ler/executar o que está no barramento de dados.
Atenção, o display utilizado apresenta os terminais colocados de maneira não sequencial, con-
forme pode ser visto na Figura 11.2. Deste modo não é qualquer display que é compatível.
As informações são enviadas através da codificação ASCII, sendo que os caracteres de 0 a 127 são
padronizados. Os caracteres de 128 a 255 dependem do fabricante do display. É possível também
criar algumas representações, símbolos definidos pelo usuário e armazenar na memória interna do
display. Para um display com a ROM do tipo A00 temos os caracteres definidos na Figura 11.3.
Para a ROM A02 temos a Figura 11.4.
Figura 11.3: Caracteres disponíveis para ROM A00
http://www.sparkfun.com/datasheets/LCD/HD44780.pdf - Datasheet Hitachi
Figura 11.4: Caracteres disponíveis para ROM A02
http://www.sparkfun.com/datasheets/LCD/HD44780.pdf - Datasheet Hitachi
Os comandos reconhecidos pelo display são apresentados na Tabela 11.1.
Tabela 11.1: Lista de comandos aceitos pelo o LCD

Instrução RS RW Barramento de dados (bit) Tempo


7 6 5 4 3 2 1 0
Limpa todo o display e configura o endereço para
0 0 0 0 0 0 0 0 0 1 37 us
0.
Configura o endereço para 0. Retorna o display
0 0 0 0 0 0 0 0 1 - 1.52 ms
para o início se houve alguma operação de shift.
Configura a movimentação do cursor e o modo
0 0 0 0 0 0 0 1 ID S 37 us
de shift do display
Configura o display (D) inteiro para desligado ou
ligado, cursor (C) ligado ou desligado e “blin- 0 0 0 0 0 0 1 D C B 37 us
king” (B) do cursor.
Move o cursor e/ou o display sem alterar o con-
0 0 0 0 0 1 SC RL - - 37 us
teúdo
Configura o tamanho da palavra (DL), número de
0 0 0 0 1 DL N F - - 37 us
linhas (N) e fonte dos caracteres (F)
Desloca o cursor para a posição desejada: linha e
0 0 1 X 0 0 Coluna 37 us
coluna.
Verifica se o display está disponível ou se está
0 1 BF - - - - - - - 10 us
ocupado com alguma operação interna.
Definições das opções
ID: 1 – Incrementa, 0 – Decrementa N: 1 – 2 linhas, 0 – 1 linha
S: 1 – O display acompanha o deslocamento F: 1 – 5x10 pontos, 0 – 5x8 pontos
SC: 1 – Desloca o display, 0 – Desloca o cursor BF: 1 – Ocupado, 0 – Disponível
RL: 1 – Move para direita, 0 – Move para esquerda X: 1 – 2a linha, 0 – 1a linha
DL: 1 – 8 bits, 0 – 4 bits Coluna: nible que indica a coluna
http://www.sparkfun.com/datasheets/LCD/HD44780.pdf - Datasheet Hitachi (modificado)

Os terminais de dados estão ligados à porta D, juntamente com o display de 7 segmentos e


barramento de dados. Para estes dispositivos funcionarem em conjunto é necessário multiplexa-los
no tempo. Os terminais de controle estão ligados à porta E conforme o esquema apresentado na
Figura 11.5.

Figura 11.5: Esquemático de ligação do display de LCD


Criação da biblioteca 11.1

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.

Código 11.1: Exemplo de uso da biblioteca de LCD


1 #include "p18f4520.h"
2 #include "config.h"
3 #include "lcd.h"
4
5 //início do programa
6 void main(void) {
7 unsigned int i,j;
8 char msg[] = "Hello World!";
9 lcdInit();
10 for(i=0;i<11;i++){
11 lcdData(msg[i]);
12 for(j = 0; j < 65000; j++);
13 }
14 for(;;);
15 }

Código 11.2: lcd.h


1 #ifndef LCD_H
2 #define LCD_H
3
4 void lcdCommand(unsigned char cmd);
5 void lcdData(unsigned char valor);
6 void lcdInit(void);
7
8 #endif
Código 11.3: lcd.c
1 #include "lcd.h"
2 #include "pic18f4520.h"
3
4 #define RS 0
5 #define EN 1
6 #define RW 2
7
8 void Delay40us(void) { 44 void lcdCommand(unsigned char cmd) {
9 unsigned char i; 45 BitClr(PORTE, RS); //comando
10 for (i = 0; i < 5; i++); //valor aproximado 46 BitClr(PORTE, RW); // habilita escrita
11 } 47
12 48 PORTD = cmd;
13 void Delay2ms(void) { 49
14 unsigned char i; 50 BitSet(PORTE, EN); //Pulso no Enable
15 for (i = 0; i < 50; i++) { 51 BitClr(PORTE, EN);
16 Delay40us(); 52
17 } 53 if (BitTst(cmd, 1)) { //o comando de ←-
18 } reset exige mais tempo
19 54 Delay2ms();
20 void lcdInit(void) { 55 } else {
21 // Inicializa o LCD 56 Delay40us();
22 57 }
23 // garante inicialização do LCD (+−10ms) 58 }
24 Delay2ms(); 59
25 Delay2ms(); 60 void lcdData(unsigned char valor) {
26 Delay2ms(); 61 BitSet(PORTE, RS); //dados
27 Delay2ms(); 62 BitClr(PORTE, RW); // habilita escrita
28 Delay2ms(); 63
29 Delay2ms(); 64 PORTD = valor;
30 65
31 // configurações de direção dos terminais 66 BitSet(PORTE, EN); //Pulso no Enable
32 BitClr(TRISE, RS); //RS 67 BitClr(PORTE, EN);
33 BitClr(TRISE, EN); //EN 68
34 BitClr(TRISE, RW); //RW 69 BitClr(PORTE, RS); //deixa em nivel ←-
35 TRISD = 0x00; //dados baixo por causa do display de 7 seg
36 ADCON1 = 0b00001110; //apenas 70 Delay40us();
37 71 }
38 //configura o display
39 lcdCommand(0x38); //0b0011 1000 8bits, 2 ←-
linhas, 5x8
40 lcdCommand(0x0F); //0b0000 1111 display ←-
e cursor on, com blink
41 lcdCommand(0x06); //0b0000 0110 modo ←-
incremental
42 lcdCommand(0x03); //0b0000 0011 zera tudo
43 }
C APÍTULO

12
Comunicação serial
12.1 I2C 73
HT1380 74
12.2 RS 232 77
12.3 Criação da biblioteca 80

“Companies spend millions of dollars on firewalls, en-


cryption, and secure access devices and it’s money was-
ted because none of these measures address the weakest
link in the security chain: the people who use, admi-
nister, operate and account for computer systems that
contain protected information.”
Kevin Mitnick

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.

Figura 12.1: Barramento I2C com vários dispositivos

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.

Figura 12.3: Envio de dados para o HT1380

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.

Figura 12.4: Byte de comando do HT1380


Código 12.1: Write RTC
1 void writeByte(char dados) {
2 unsigned char i;
3 // Escreve o comando
4 for (i = 0; i < 8; i++) {
5 //Analisa o bit a ser transmitido
6 if (dados & 0x01) {
7 RTC_IO_ON();
8 } else {
9 RTC_IO_OFF();
10 }
11 //pega o próximo bit
12 dados >>= 1;
13 //geração do pulso de clock
14 RTC_SCLK_ON();
15 DELAY();
16 //info valida
17 RTC_SCLK_OFF();
18 DELAY();
19 }
20 }

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.

Figura 12.5: Registros internos do HT1380

A implementação em software da rotina para enviar um byte é bastante simples:


1. Colocar o valor do bit a ser enviado na linha de DADOS
2. Gerar um pulso de clock na linha de CLOCK
3. Se houver mais bits para enviar voltar ao passo 1
O código que implementa a rotina acima está na listagem 12.1.
A recepção de valores possui apenas uma diferença. Como o valor dos dados é gerado pelo
escravo, a leitura deve ser feita com o clock em valor alto conforme pode ser observado no trecho
de código 12.2.
Como citado anteriormente, para enviar ou ler um valor é necessário escrever um byte indicando
qual é o registro a ser lido. Para facilitar essa tarefa duas funções foram desenvolvidas, para leitura
e escrita do RTC e são apresentadas no código 12.3. Note que na escrita é necessário inverter a
direção do terminal de dados, de saída para entrada.
Os registros deste RTC armazenam 8 valores em bcd: segundos, minutos, dias, meses, anos, dia
do mês e um registro de proteção para escrita. BCD é um formato utilizado em sistemas eletrônicos
Código 12.2: Read RTC
1 unsigned char readByte() {
2 unsigned char i;
3 unsigned char dados;
4 dados = 0;
5 for (i = 0; i < 8; i++) {
6 RTC_SCLK_ON();
7 DELAY();
8 // informação válida
9 // RTC_IO_STATUS(); //ler duas vezes, problema do HW de leitura
10 if (RTC_IO_STATUS()) {
11 dados |= 0x80;
12 }
13 dados >>= 1;
14 RTC_SCLK_OFF();
15 DELAY();
16 }
17 return dados;
18 }

Código 12.3: Rotinas de escrita e leitura no RTC


1 void ht1380write(char addr, char dados) {
2 RTC_RESET_OFF();
3 RTC_SCLK_OFF();
4 RTC_IO_OFF();
5 //liga o RTC
6 RTC_RESET_ON();
7
8 addr <<= 1;
9 addr |= 0x80; // write
10 writeByte(addr);
11 writeByte(dados);
12
13 RTC_RESET_OFF();
14 RTC_SCLK_OFF();
15 RTC_IO_OFF();
16 }
17
18 char ht1380read(char addr) {
19 unsigned char dados;
20 RTC_RESET_OFF();
21 RTC_SCLK_OFF();
22 RTC_IO_OFF();
23
24 RTC_RESET_ON();
25 addr <<= 1;
26 addr |= 0x81; //read
27 writeByte(addr);
28 //muda pino para ler sinais
29 RTC_IO_IN();
30 DELAY();DELAY();DELAY();DELAY();
31 dados = readByte();
32
33 RTC_RESET_OFF();
34 RTC_SCLK_OFF();
35 RTC_IO_OFF();
36 RTC_IO_OUT();
37 return dados;
38 }
Código 12.4: Rotinas de conversão BCD x inteiro
1 char rtcGetSeconds(void) {
2 char value;
3 value = ht1380read(0); // read seconds
4 return (((value >> 4)&0x07)*10 + (value & 0x0f));
5 }
6 void rtcPutSeconds(char seconds) {
7 ht1380write(0, (seconds % 10) | ((seconds / 10) << 4));
8 }
9 //demais funções são similares, apenas modificando o endereço

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

(zero) é interpretado entre -3 e -15 volts.


Figura 12.6: Sinal serializado para transmissão em RS232

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.

1 void serialSend(unsigned char c) {


2 while(!BitTst(PIR1,4)); //aguarda o registro ficar disponível
3 TXREG=c; //coloca o valor para ser enviado
4 }

O processo de desserialização também é realizado de modo automático pelo hardware do dis-


positivo. Assim que um byte estiver disponível o sistema seta o bit 5 do registro PIR1 e podemos
então ler o valor disponível no registro RCREG.
Nas atividades práticas é possível que o receptor sobrecarregue com mensagens, se elas chega-
rem mais rápido do que o microcontrolador seja capaz de processá-las. Nestes casos o PIC18f4520
pode travar a leitura de novas mensagens enquanto o erro não for lido. Isto é feito através da
mudança do bit 4 do registro RCSTA.

1 unsigned char serialRead(void) {


2 unsigned char resp = 0;
3 if (BitTst(RCSTA, 1)) { //Verifica se há erro de overrun e reseta a serial
4 BitClr(RCSTA, 4);
5 BitSet(RCSTA, 4);
6 }
7 if (BitTst(PIR1, 5)) { //Verifica se existe algum valor disponivel
8 resp = RCREG; //retorna o valor
9 }
10 return resp; //retorna zero
11 }

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.

Código 12.5: serial.c


1 #include "serial.h"
2 #include "pic18f4520.h"
3
4 void serialSend(unsigned char c) {
5 while (!BitTst(PIR1, 4)); //aguarda o registro ficar disponível
6 TXREG = c; //coloca o valor para ser enviado
7 }
8
9 unsigned char serialRead(void) {
10 unsigned char resp = 0;
11
12 if (BitTst(RCSTA, 1)) { //Verifica se há erro de overrun e reseta a serial
13 BitClr(RCSTA, 4);
14 BitSet(RCSTA, 4);
15 }
16 if (BitTst(PIR1, 5)) { //Verifica se existe algum valor disponivel
17 resp = RCREG; //retorna o valor
18 }
19 return resp; //retorna zero
20 }
21
22 void serialInit(void) {
23 TXSTA = 0b00101100; //configura a transmissão de dados da serial
24 RCSTA = 0b10010000; //configura a recepção de dados da serial
25 BAUDCON = 0b00001000; //configura sistema de velocidade da serial
26 SPBRGH = 0; //configura para 57.6k
27 SPBRG = 34; //configura para 57.6k
28 BitSet(TRISC, 6); //pino de recepção de dados
29 BitSet(TRISC, 7); //pino de envio de dados
30 }

Código 12.6: serial.h


1 #ifndef SERIAL_H
2 #define SERIAL_H
3
4 void serialSend(unsigned char c);
5 unsigned char serialRead(void);
6 void serialInit(void);
7
8 #endif
Código 12.7: Exemplo de uso da biblioteca de comunicação serial
1 #include "p18f4520.h"
2 #include "config.h"
3 #include "serial.h"
4
5 //início do programa
6 void main(void) {
7 unsigned int i,j;
8 char msg[] = "Hello World!";
9 unsigned char resp;
10 TRISD = 0x00; //acesso aos leds
11 serialInit();
12 j=0;
13 for(;;){
14 //delay
15 for(i = 0; i < 65000; i++);
16 //envia dados
17 serialSend(msg[j]);
18 j++;
19 if (j > 11){
20 j=0;
21 serialSend(13);
22 }
23 //recebe dados
24 resp = serialRead();
25 if (resp!=0)
26 {
27 PORTD = resp;
28 }
29 }
30 }
C APÍTULO

13
Conversor AD
13.1 Elementos sensores 82
13.2 Processo de conversão AD 85
13.3 Criação da biblioteca 86

“People are so into digital recording now they forgot how


easy analog recording can be.”
Dave Grohl

Um conversor de analógico para digital é um circuito capaz de transformar um valor de tensão


numa informação binária. O circuito que utilizaremos possui uma precisão de 10 bits, ou seja, ele é
capaz de sentir uma variação de praticamente um milésimo1 da excursão máxima do sinal. Para a
configuração que iremos utilizar, com uma fonte de 5v, isto significa uma sensibilidade de 4.88mV.

Elementos sensores 13.1

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

Figura 13.2: Potenciômetro

Se o potenciômetro estiver alimentado pelos terminais da extremidade, o terminal central fun-


ciona como um divisor de tensão. O valor de saída é proporcional à posição do cursor. Podemos
aproximar o potenciômetro como duas resistências conforme apresentado na Figura 13.3.

Figura 13.3: Potenciômetro como divisor de tensão


http://en.wikipedia.org/wiki/File:Potentiometer_with_load.png

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.

Figura 13.4: Circuito integrado LM35

Existem alguns sensores que possuem circuitos de amplificação e condicionamento do sinal


embutidos no mesmo envólucro que o elemento sensor. A estes tipos de sensores damos a denomi-
nação de ativos.
Um sensor ativo possui no mínimo 3 terminais: 2 para alimentação e 1 para saída do sinal.
Um exemplo deste tipo de sensor é o LM35 (Figura 13.4) que é utilizado para monitoramento de
temperatura.
Na Figura 13.5 é apresentado o diagrama de blocos do circuito integrado do LM35. O diodo é
utilizado como unidade sensora de temperatura.

Figura 13.5: Diagrama de blocos do LM35

Quando polarizado através de uma corrente constante, havendo mudança de temperatura a


tensão em cima do diodo muda. Os dois amplificadores e as respectivas realimentações estão
inseridas no circuito para amplificar e estabilizar as variações de tensão.
Processo de conversão AD 13.2

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

Figura 13.6: Definição de faixa de valores para AD de 2 bits

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

Criação da biblioteca 13.3

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 }

Código 13.2: adc.h


1 #ifndef ADC_H
2 #define ADC_H
3
4 void adcInit(void);
5 int adcRead(void);
6
7 #endif

Código 13.3: Exemplo de uso da biblioteca de conversores AD


1 #include "p18f4520.h"
2 #include "config.h"
3 #include "ssd.h"
4 #include "adc.h"
5 //início do programa
6 void main(void) {
7 unsigned int i;
8 int temp = 0;
9 ssdInit();
10 adcInit();
11 for(;;){
12 temp = adcRead();
13 ssdDigit((temp / 1000)%10,3);
14 ssdDigit((temp / 100 )%10,2);
15 ssdDigit((temp / 10 )%10,1);
16 ssdDigit((temp / 1 )%10,0);
17 ssdUpdate();
18 for(i = 0; i < 1000; i++);
19 }
20 }
C APÍTULO

14
Saídas PWM
14.1 Criação da biblioteca 89

“When a coil is operated with currents of very high fre-


quency, beautiful brush effects may be produced, even if
the coil be of comparatively small dimensions. The expe-
rimenter may vary them in many ways, and, if it were
nothing else, they afford a pleasing sight.”
Nikola Tesla

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.

Figura 14.1: Sinais PWM com variação do duty cycle

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

O duty cycle (em porcentagem) é calculado de acordo com a fórmula abaixo:

[CCPRxL : CCPxCON (5 : 4)]


DutyCyclePWM =
(PR2 + 1) ∗ 4

Criação da biblioteca 14.1

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 }

Código 14.2: pwm.h


1 #ifndef PWM_H
2 #define PWM_H
3
4 void pwmSet1(unsigned char porcento);
5 void pwmSet2(unsigned char porcento);
6 void pwmFrequency(unsigned int freq);
7 void pwmInit(void);
8
9
10 #endif //PWM_H
Código 14.3: Exemplo de uso da biblioteca das saídas PWM
1 #include "config.h"
2 #include "p18f4520.h"
3 #include "pwm.h"
4 #include "adc.h"
5
6 //início do programa
7 void main(void) {
8 int temp;
9 pwmInit();
10 adcInit();
11 for(;;) {
12 temp = adcRead();
13 //ajustando a frequência de acordo com entrada analógica
14 pwmFrequency(temp);
15 //ajustando o duty−cycle para 50%
16 pwmSet1(50);
17 }
18 }
C APÍTULO

15
Timer
15.1 Reprodução de Sons 94

“The only reason for time is so that everything doesn’t


happen at once.”
Albert Einstein

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 }

Código 15.2: timer.h


1 #ifndef TIMER_H
2 #define TIMER_H
3
4 char timerEnded(void);
5 void timerWait(void);
6 //tempo em mili segundos
7 void timerReset(unsigned int tempo);
8 void timerInit(void);
9 #endif //TIMER_H

Código 15.3: Exemplo de uso da biblioteca de um temporizador


1 //início do programa
2 void main(void) {
3 unsigned int cont;
4 TRISD=0x00;
5 timerInit();
6 cont = 0;
7 for(;;){
8 timerReset(10000);
9 cont ++;
10 //50 ∗ 10ms = 0,5s
11 if (cont >= 50){
12 PORTD ^= 0xFF;
13 cont = 0;
14 }
15 timerWait();
16 }
17 }
Reprodução de Sons 15.1

Se ligarmos à saída PWM um auto-falante é possível reproduzir sons. Conhecendo a frequência


de cada uma das notas musicais e a duração destas é possível reproduzir uma música. Para repro-
duzir o tempo com uma precisão melhor podemos utilizar o TIMER0 como unidade de tempo.
Conforme visto na seção 14, o PWM utilizado na placa consegue reproduzir as frequências
audíveis a partir de 488,3Hz. Por isso escolhemos começar a escala musical a partir do C5 (Dó
Tenor) que possui a frequência de 523 Hz. A segunda escala possui o dobro da frequência (uma
oitava acima). Para reproduzir a ausência de som escolhemos a frequência de 125.000 Hz2 , que é
inaudível. Isto simplifica o programa.

Código 15.4: Reprodução de sons


1 #include "config.h" 40 //início do programa
2 #include "p18f4520.h" 41 void main(void)
3 #include "pwm.h" 42 {
4 #include "timer.h" 43 unsigned char cont=0;
5 44 unsigned char pos=0;
6 //frequência das 45 //Imperial March (SW Episode V)
7 //notas musicais 46 unsigned char tempo[] = {50, 10, 50, 10, ←-
8 #define C 523 50, 10, 50, 5, 25, 5, 50, 5, 50, 5, ←-
9 #define CS 554 25, 5, 50, 50, 50, 10, 50, 10, 50, ←-
10 #define D 587 10, 50, 5, 25, 5, 50, 5, 50, 5, 25, ←-
11 #define DS 622 5, 50, 50, 100, 5, 25, 5, 25, 10, ←-
12 #define E 659 100, 5, 50, 5, 25, 2, 10, 2, 10, 2, ←-
13 #define F 698 100, 250};
14 #define FS 740 47 unsigned int notas[] = {G, v, G, v, G, ←-
15 #define G 784 v, E, v, B, v, G, v, E, v, B, v, G, ←-
16 #define GS 830 v, D2S, v, D2S, v, D2S, v, E2, v, ←-
17 #define A 880 B, v, FS, v, E, v, B, v, G, v, G2S, ←-
18 #define AS 932 v, G, v, G, v, G2S, v, G2, v, F2S, ←-
19 #define B 987 v, F2, v, E2, v, F2S, v};
20 48 InicializaPWM();
21 49 timerInit();
22 //segunda oitava 50 SetaFreqPWM(notas[0]);
23 #define C2 C*2 51 SetaPWM1(50); //garante duty−cycle de 50%
24 #define C2S CS*2 52 for(;;)
25 #define D2 D*2 53 {
26 #define D2S DS*2 54 AguardaTimer();
27 #define E2 E*2 55 timerReset(10000);
28 #define F2 F*2 56 cont ++;
29 #define F2S FS*2 57 if (cont >= tempo[pos])
30 #define G2 G*2 58 {
31 #define G2S GS*2 59 pos++;
32 #define A2 A*2 60 SetaFreqPWM(notas[pos]);
33 #define A2S AS*2 61 SetaPWM1(50);
34 #define B2 B*2 62 cont=0;
35 63 }
36 #define C3 C2*2 64 }
37 65 }
38 //sem som
39 #define v 125000

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

“When I go to a library and I see the librarian at her desk


reading, I’m afraid to interrupt her, even though she sits
there specifically so that she may be interrupted, even
though being interrupted for reasons like this by people
like me is her very job.”
Aaron Swartz

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.

1 void NomeDaFuncao(void) interrupt 1


2 {
3 //código ...
4 }

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 }

Código 16.2: Tratamento das interrupções


1 static unsigned int ADvalor;
2 static unsigned char Serial;
3 static unsigned int Tecla;
4 void Interrupcao(void) interrupt 1{
5 char i, j;
6 if (BitTst(PIR1,6)){ //AD : fim de conversão
7 ADvalor = ADRESH ; // lê o resultado
8 ADvalor <<= 8;
9 ADvalor += ADRESL;
10 BitClr(PIR1,6); //limpa a flag
11 }
12 if (BitTst(PIR1,5)){ //Serial: recepção
13 //BitClr(PIR1,5);
14 Serial = RCREG; //limpa sozinho quando lê
15 }
16 if (BitTst(INTCON,0)){ //PORTA B : mudou valor
17 for(i = 0; i < 4; i++){
18 PORTB |= 0xFF;
19 BitClr(PORTB,(i));
20 for(j=0;j<10;j++);
21 for(j = 0; j < 4; j++){
22 if (!BitTst(PORTB,j+4)){
23 BitSet(Tecla,(i*4)+j);
24 }else{
25 BitClr(Tecla,(i*4)+j);
26 }
27 }
28 }
29 PORTB = 0x00;
30 BitClr(INTCON,0);
31 }
32 if (BitTst(INTCON,2)){ //TIMER0: Overflow
33 //tempo máximo de interrupção do timer 0
34 BitClr(INTCON,2); //limpa a flag
35 TMR0H = 0x00; //reinicia contador de tempo
36 TMR0L = 0x00; //reinicia contador de tempo
37 ADCON0 |= 0b00000010; //inicia conversão
38 }
39 }
Código 16.3: Inicialização do sistema com interrupções
1 void main(void){
2 unsigned int i, temp, teclanova=0;
3 TRISD = 0x00;
4 TRISB = 0xF0; //mantém os 4 últimos bits como entrada
5 PORTB = 0x00; //mantém ligadas as 4 colunas
6
7 serialInit();
8 ssdInit();
9 lcdInit();
10 adcInit();
11 timerInit();
12
13 // Região de inicialização das interrupções
14 BitClr(RCON,7); // desabilita IPEN (modo de compatibilidade)
15 BitSet(PIE1,6); // liga a interrupção para o AD
16 BitSet(PIE1,5); // liga a interrupção para a recepção na serial
17 BitSet(INTCON,5); // liga a interrupção para o timer 0
18 BitSet(INTCON,3); // liga a interrupção para a porta B
19 BitSet(INTCON,7); // habilita todas as interrupções globais
20 BitSet(INTCON,6); // habilita todas as interrupções de periféricos
21
22 for(;;){
23 temp = adcRead();
24 ssdDigit((temp / 1000)%10,3);
25 ssdDigit((temp / 100 )%10,2);
26 ssdDigit((temp / 10 )%10,1);
27 ssdDigit((temp / 1 )%10,0);
28 ssdUpdate();
29 if (teclanova != Tecla){
30 teclanova = Tecla;
31 for(i=0;i<16;i++){
32 if (BitTst(Tecla,i)){
33 serialSend(i+48);
34 }
35 }
36 }
37 for(i = 0; i < 1000; i++);
38 }
39 }
C APÍTULO

17
Watchdog

“Technology is a word that describes something that do-


esn’t work yet.”
Douglas Adams

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

18 * One single loop, 103

19 * Interrupt control system, 105

20 * Cooperative multitasking, 107

21 * Anexos, 114

102
C APÍTULO

18
One single loop

“Writing in C or C++ is like running a chain saw with


all the safety guards removed.”
Bob Gray

No desenvolvimento de um sistema de maior porte, é importante definir o tipo de arquitetura


que iremos utilizar. Neste capítulo apresentamos três abordagens possíveis. A escolha deve ser
baseada no tipo de dispositivo a ser desenvolvido, na complexidade do sistema, na possibilidade
de gerar subprodutos e dos requisitos de tempo.
Esta é a estratégia utilizada até agora nos exemplos apresentados. Dentro da função principal é
colocado um loop infinito. Todas as tarefas são chamadas através de funções.
A vantagem de se utilizar esta abordagem é a facilidade de se iniciar um projeto. Para sis-
temas maiores começa a ficar complicado coordenar as tarefas e garantir a execução num tempo
determinístico. Outro problema é a modificação/ampliação do software. Geralmente a inserção de
uma função no meio do loop pode gerar erros em outras funções devido a restrições de tempo dos
periféricos associados.
No exemplo acima, a inserção da comunicação serial e os cálculos podem atrapalhar a escrita
no display de sete segmentos, gerando flicker.

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 }

Código 18.2: Problema na sincronia de tempo para o single-loop


1 //loop principal
2 for(;;)
3 {
4 //chamada das tarefas
5 kpDebounce();
6 ia = kpRead();
7 ssdPrint(ia); //tem que ser executado pelo menos a cada 10(ms)
8 ic = serialRead();
9 fa = 2.0 * ic / 3.14;
10 serialSend(fa & 0x00FF);
11 serialSend(fa >> 8);
12 }
C APÍTULO

19
Interrupt control system

“If people do not believe that mathematics is simple, it is


only because they do not realize how complicated life is.”
John Louis von Neumann

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 }

Código 19.2: Exemplo de sistema Interrupt-driven com base de tempo


1 int ia;
2 //existe apenas uma fonte de interrupção: Timer 0
3 void Interrupcao(void) interrupt 1{
4 timerReset(5000); // reinicia timer para próxima interrupção
5 ssdPrint(ia);
6 BitSet(INTCON,5); // religa a interrupção para o timer 0
7 }
8
9 void main(void)
10 {
11 //inicialização dos periféricos
12 kpInit();
13 ssdInit();
14 timerInit();
15 //inicialização da interrupção
16 BitClr(RCON,7); //desabilita IPEN (modo de compatibilidade)
17 BitSet(INTCON,5); // liga a interrupção para o timer 0
18 BitSet(INTCON,7); //habilita todas as interrupções globais
19 BitSet(INTCON,6); //habilita todas as interrupções de periféricos
20 timerReset(5000);
21 for(;;){ //loop principal
22 kpDebounce():
23 ia = kpRead();
24 }
25 }
C APÍTULO

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

“Constrained by memory limitations, performance re-


quirements, and physical and cost considerations, each
embedded system design requires a middleware platform
tailored precisely to its needs, unused features occupy
precious memory space, while missing capabilities must
be tacked on.”
Dr. Richard Soley

Em computação, multitarefa ou multitasking é um processo pelo qual diferentes tarefas com-


partilham um mesmo recurso, seja ele memória, processamento ou qualquer periférico disponível.
Uma maneira de realizar este compartilhamento é através de uma divisão do tempo: a tarefa A
possui um intervalo ao final do qual deve ceder os recursos para a tarefa B. Quando a mudança de
tarefa é feita pela própria tarefa, o sistema é dito cooperativo. Quando existe um sistema externo
que realiza essa troca o sistema é denominado preemptivo.
Se a mudança de tarefas for extremamente rápida o efeito resultante, para o ser humano, é de
que todas as tarefas estão sendo executadas simultaneamente. Uma das maneiras de se obter este
tipo de operação é através da criação de uma máquina de estados, como mostrado na Figura 20.1.
Nota-se que após a fase de inicialização o sistema entra num ciclo, como na abordagem one-
single-loop. Outra peculiaridade é que algumas tarefas podem ser executadas mais de uma vez
para garantir as restrições de tempo. No exemplo a tarefa de atualização dos displays é executada
três vezes.
A transposição de uma máquina de estado para o código em C é realizada através de um switch-
case.
É possível retirar todas as atribuições para a variável slot e colocar no “slot-bottom” a expressão
slot++. A abordagem apresentada foi escolhida por aumentar a robustez do sistema, já que a
variável slot controla todo o fluxo do programa.
A inserção de uma nova tarefa é realizada de maneira simples, basta adicionar outro slot, ou
seja, basta inserir um case/break com a tarefa desejada.
Como a máquina está dentro do loop infinito, a cada vez que o programa passar pelo case, ele
executará apenas um slot. Esta abordagem gera ainda outro efeito. Como pode ser visto no código,

107
Inicio

Ler Atualiza
Teclado Display

Atualiza Escreve
Display Serial

Atualiza
Ler Serial Display

Figura 20.1: Exemplo de máquina de estados

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-

Código 20.3: Exemplo de sistema Cooperative-multitasking com slot temporizado


1 void main(void){
2 //declaração das variáveis
3 char slot;
4 //funções de inicialização
5 ssdInit();
6 timerInit();
7 for(;;){ //início do loop infinito
8 //∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ início do top−slot ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
9 timerReset(5000); //5 ms para cada 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 AguardaTimer();
39 } //fim loop infinito (!?)
40 }
Top
S.1
S.2
S.3
Bottom
"vago"
0 5 10 15 20 25 30
Figura 20.2: Exemplo da mudança de slots no tempo

(0.5+1.2+0.3)=3.0 (ms).
Utilização do tempo livre para interrupções 20.2

Conforme visto anteriormente, dependendo do tempo escolhido para o slot e do “tamanho” da


função, podem existir espaços vagos na linha de tempo do processador. A Figura 20.3 apresenta
uma linha de tempo de um sistema que possui apenas 1 slot. Já a Figura 20.4 demonstra o mesmo
sistema sendo interrompido através de interrupções assíncronas.

Top 1 1 1
S.1 3 3 3
Bottom 1 1 1
"vago" 3 3 3

Figura 20.3: Linha de tempo de um sistema com 1 slot

Top 1 1
S.1 1 2 3 3
Bottom 1 1 1
"vago" 2 2 2
Interr. 1 1 1

Figura 20.4: Comportamento da linha de tempo com interrupções

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

O arquivo config.h possui as diretivas de compilação para configuração do microcontrolador.


O header p18f4520.h possui o endereço de todos os registros do microcontrolador PIC 18f4520
que são utilizados nesta apostila. Além disso contém alguns define’s importantes como as funções
inline para limpar a flag de watchdog e para manipulação de bits.

Código 21.1: config.h


1 //para o compilador SDCC + GPUtils
2 #ifndef CONFIG_H
3 #define CONFIG_H
4
5 //code char at 0x300000 CONFIG1L = 0x01; // Pll desligado
6 //code char at 0x300001 CONFIG1H = 0x0C; // Oscilador c/ cristal externo HS
7 //code char at 0x300003 CONFIG2H = 0x00; // Watchdog controlado por software
8 //code char at 0x300006 CONFIG4L = 0x00; // Sem programação em baixa tensão
9
10 //para o compilador SDCC versão >= 3.0
11 #pragma config MCLRE=ON // Master Clear desabilitado
12 #pragma config OSC=HS // Oscilador c/ cristal externo HS
13 #pragma config WDT=OFF // Watchdog controlado por software
14 #pragma config LVP=OFF // Sem programação em baixa tensão
15 #pragma config DEBUG = OFF // Desabilita debug
16 #pragma config WDTPS = 1 // Configura prescaler do watchdog
17
18 #endif //CONFIG_H

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)

Вам также может понравиться