Notas de Aula ECOP04

118  52 

Texto

(1)

U

nifei

- U

niversidade

F

ederal de

I

tajubá

Notas de Aula ECOP04

Programação de sistemas embarcados

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

AUTOR

(2)

Sumário

ii

Sumário

Parte I

Programação Embarcada

3

Capítulo 1Introdução

1.1 Hardware utilizado 4 1.2 Ambiente de programação 4

5

Capítulo 2Linguagem 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

23

Capítulo 3Operaçõ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

33

Capítulo 4Debug 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

(3)

Parte II

Arquitetura de microcontroladores

37

Capítulo 5Microcontrolador

5.1 Acesso à memória 39 5.2 Clock e tempo de instrução 40

5.3 Registros de configuração do microcontrolador 41

43

Capítulo 6Esquema elétrico e circuitos importantes

6.1 Multiplexação nos terminais do microcontrolador 44

Parte III

Programação dos Periféricos

46

Capítulo 7

Portas de E/S

7.1 Acesso às portas do microcontrolador 47 7.2 Configuração dos periféricos 48

51

Capítulo 8

Barramento de Leds

53

Capítulo 9Display de 7 segmentos

9.1 Multiplexação de displays 55 9.2 Criação da biblioteca 56

58

Capítulo 10Leitura 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

65

Capítulo 11Display LCD 2x16

11.1 Criação da biblioteca 70

72

Capítulo 12Comunicação serial

12.1 I2C 73 12.2 RS 232 77

12.3 Criação da biblioteca 80

82

Capítulo 13Conversor AD

(4)

88

Capítulo 14

Saídas PWM

14.1 Criação da biblioteca 89

92

Capítulo 15

Timer

15.1 Reprodução de Sons 94

96

Capítulo 16Interrupção

100

Capítulo 17Watchdog

Parte IV

Arquitetura de desenvolvimento de software

103

Capítulo 18One single loop

105

Capítulo 19Interrupt control system

107

Capítulo 20Cooperative multitasking

20.1 Fixação de tempo para execução dos slots 111 20.2 Utilização do tempo livre para interrupções 113

(5)

P

ar

te

I

Programação Embarcada

1⇀Introdução,3

2⇀Linguagem C,5

3⇀Operações com bits,23

4⇀Debug de sistemas embarcados,33

(6)

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 sissis-temas 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(Figura1.1).

Firmware

Hardware

Sistema Operacional

Aplicação

Figura1.1: Camadas de abstração de um sistema operacional

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

(7)

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.

1.1

Hardware utilizado

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:

• 1display LCD2linhas por16caracteres (compatível com HD77480)

• 4displays de7segmentos com barramento de dados compartilhados

• 8leds ligados ao mesmo barramento dos displays

• 16mini switches organizadas em formato matricial4x4

• 1sensor de temperatura LM35C

• 1resistência de aquecimento ligada a uma saída PWM

• 1motor DC tipo ventilador ligado a uma saída PWM • 1buzzer ligado a uma saída PWM

• 1canal de comunicação serial padrão RS-232

Cada componente terá seu funcionamento básico explicado para permitir o desenvolvimento de rotinas para estes.

1.2

Ambiente de programação

O ambiente utilizado será o MPLABX(R). Este é um ambiente de desenvolvimento disponibili-zado pela Microchip(R) gratuitamente. O compilador utilidisponibili-zado 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 Tabela1.1apresenta os softwares que serão utilizados no curso.

Tabela1.1: Softwares utilizados no curso

Item Versão Licença

IDE MPLABX 3.00 Mista

Compilador SDCC 3.1.00(win32) GPL

(8)

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 Figura2.1.

Figura2.1: Linguagens mais utilizadas

Fonte: http://www.tiobe.com

(9)

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 Figura2.2.

Figura2.2: Pesquisa sobre linguagens utilizadas para projetos de software embarcado

Fonte: http://www.embedded.com/design/218600142

A descontinuidade depois de2004se 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 12meses?”. Em 2005a pergunta se tornou: “Meu projeto

embarcado atual é programado principalmente em ______”. Múltiplas seleções eram possíveis antes de 2005, permitindo a soma superior a100%, 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%.

2.1

Indentação e padrão de escrita

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

(10)

Código indentado Código não indentado

1 void main(void){

2 unsigned int i;

3 unsigned int temp;

4 unsigned int teclanova=0;

5 serialInit();

6 ssdInit();

7 InicializaLCD();

8 InicializaAD();

9 for(;;) {

10 ssdUpdate();

11 if (teclanova != Tecla) {

12 teclanova = Tecla;

13 for(i=0;i<16;i++) {

14 if (BitTst(Tecla,i)){

15 EnviaDados(i+48);

16 }

17 }

18 }

19 for(i = 0; i < 1000; i++);

20 }

21 }

1 void main(void) {

2 unsigned int i;

3 unsigned int temp;

4 unsigned int teclanova=0;

5 serialInit();

6 ssdInit();

7 InicializaLCD();

8 InicializaAD();

9 for(;;) {

10 ssdUpdate();

11 if (teclanova != Tecla) {

12 teclanova = Tecla;

13 for(i=0;i<16;i++) {

14 if (BitTst(Tecla,i)) {

15 EnviaDados(i+48);

16 }

17 }

18 }

19 for(i = 0; i < 1000; i++);

20 }

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

(11)

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.

2.2

Comentários

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 }

2.3

Arquivos .c e .h

Na programação em linguagem C utilizamos dois tipos de arquivos com funções distintas. Toda implementação de código é feita no arquivo com extensão “.c” (code). É nele que criamos as funções,

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 programa2.1apresenta um exemplo de um arquivo de código “.c” e o programa2.2apresenta

o respectivo arquivo de header “.h”.

(12)

Código2.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ódigo2.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

2.4

Diretivas de compilação

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

(13)

Original Compilado Resultado na Tela

1 #define CONST 15

2 void main(void){

3 printf("%d", CONST * 3);

4 }

1 void main(void){

2 printf("%d", 15 * 3);

3 }

1 45

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

1 void MostraSaidaPadrao(){

2 #ifdef PADRAO Serial

3 char * msg = "SERIAL";

4 #else

5 char * msg = "LCD";

6 #endif

7 printf(msg);

8 }

1 #include <stdio.h>

2 #define PADRAO Serial

3 void main(void){

4 MostraSaidaPadrao();

5 }

1 SERIAL

1 #include <stdio.h>

2 #define PADRAO LCD

3 void main(void){

4 MostraSaidaPadrao();

5 }

1 LCD

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 0e ON como1 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

(14)

Código2.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ódigo0x30. 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 Figura2.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 programa2.3.

(15)

#include “serial.h”

char LerTemperatura(void); void AjustaCalor(char val);

temp.h

#include “temp.h”

char LerSerial(void); void EnviaSerial(char val);

serial.h

#include “serial.h”

char LerTemperatura(void); void AjustaCalor(char val);

temp.h

Figura2.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 Figura2.4.

#ifndef TEMP_H #define TEMP_H #include “serial.h”

char LerTemperatura(void); void AjustaCalor(char val); #endif

temp.h

#ifndef SERIAL_H #define SERIAL_H #include “temp.h” char LerSerial(void); void EnviaSerial(char val); #endif

serial.h

#ifndef TEMP_H //tag já definida, //pula o conteúdo #endif

temp.h

Figura2.4: Solução das referências circulares com #ifndef

(16)

2.5

Tipos de dados em C

O tipo de uma variável informa a quantidade de memória, embytes, 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 Tabela2.1.

Tabela2.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,4x10-38à3,4x1038

double 64 8 3,4x10-308à3,4x10308

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 base2. Um valor do tipo char que possui8bits será representado por um número de8

algarismos, todos0(zeros) ou1(uns). Para realizarmos a conversão de um número na base decimal

para a base2podemos seguir o seguinte algoritmo: 1. Dividir o número por2

2. Anotar o valor do resto (0ou1)

3. Se o valor é maior que0voltar ao número1

4. Escrever os valores obtidos através do passo2de trás para frente. 5. Apresentar o resultado

Por exemplo o número18. 18/2=9, resto0 9/2=4, resto1 4/2=2, resto0 2/2=1, resto0 1/2=0, resto1

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 apenas10 algarismos no sistema de numeração arábico (0,1, 2,3,4, 5,6,7,8,9) utilizamos6letras para complementá-los

(A, B, C, D, E, F). A conversão, entre valores binários, decimais e hexadecimais, é apresentada na Tabela2.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 de4em4algarismos temos: 1-0010

(17)

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

Tabela2.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 }

(18)

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

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.

(19)

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 modificadoresnearefar. Estes modificadores

indicam ao compilador em qual região de memória devem ser colocadas as variáveis.

A regiãoneargeralmente se refere à “zero page”. É uma região mais fácil de ser acessada. A

regiãofarexige mais tempo para executar a mesma função que anear.

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 }

2.6

Operações aritméticas

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 Programa2.4notamos 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.

(20)

Código2.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 linha9, 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 (linha11) 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 (linha12) 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 operandossã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 (linha15) é 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);

(21)

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 Figura2.5é apresentado um erro gerado através de um loop infinito.

Figura2.5: Loop infinito de um device driver gerando erro no sistema

2.7

Função main()

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 (Figura2.6).

(22)

0x58

Testa A

0x57

30

0x56

A recebe

0x55

Limpa A

0x59

...

0x8D

Porta B

0x8C

Salva em

0x8B

50

0x8A

A recebe

0x8E

...

0x03

0x55

0x02

Pulo

0x01

0x8A

0x04

...

0x00

Pulo

Endereço Instrução

Figura2.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()1espera-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) {

2 for(;;) {

3 //aqui entra o

4 //código principal

5 }

6 }

1 void main (void) {

2 while(1) {

3 //aqui entra o

4 //código principal

5 }

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çoFOR2.

1Em sistemas mais complexos algumas tarefas são executadas independentemente da função principal, tendo sua exe-cução controlada através de interrupções.

(23)

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 de3passos de inicialização.

Cada iteração exige2passos: uma comparação e um “pulo”3, totalizando3ciclos de inicialização e 3ciclos 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 de1 milhão de instruções devemos utilizar pelo menos3 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 temos3loops 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 }

3Este 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.

(24)

10 }

11 }

2.8

Ponteiros e endereços de memória

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úmero3.2, que não existe, gerando um erro. Se quisermos guardar o valor

3.2no endereço de memória apontado por apfloat devemos utilizar a segunda expressão.

(25)

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

(26)

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, equalqueroutro valor representa verdadeiro.

3.1

NOT

Figura3.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’.

(27)

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 char A = 12;

2 // A = 0b00001100

1 result = !A;

2 // result = 0

1 result = ~A;

2 // result = 243

3 // A = 0b00001100

4 // r = 0b11110011

3.2

AND

Figura3.2: Circuito equivalente de uma porta tipo E

A operação AND lógica (operador &&) retorna0 se algum dos valores for zero, e1se 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 char A = 8;

2 // A = 0b00001000

3 char B = 5;

4 // B = 0b00000101

1 result = A && B;

2 // result = 1

1 result = A & B;

2 // result = 0

3 // A = 0b00001000

4 // B = 0b00000101

(28)

3.3

OR

Figura3.3: Circuito equivalente de uma porta tipo OU

A operação OR lógica (operador ||) retorna1se algum dos valores for diferente de zero, e0se

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 char A = 8;

2 // A = 0b00001000

3 char B = 5;

4 // B = 0b00000101

1 result = A || B;

2 // result = 1

1 result = A | B;

2 // result = 13

3 // A = 0b00001000

4 // B = 0b00000101

5 // r = 0b00001101

3.4

XOR

Figura3.4: Circuito equivalente de uma porta tipo OU EXCLUSIVO

(29)

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 char A = 8;

2 // A = 0b00001000

3 char B = 5;

4 // B = 0b00000101

1 // não existe em C

1 result = A ^ B;

2 // result = 13

3 // A = 0b00001000

4 // B = 0b00000101

5 // r = 0b00001101

3.5

Shift

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 char A = 8;

2 // A = 0b00001000

1 result = A << 2;

2 // result = 32

3 // A = 0b00001000

4 // r = 0b00100000

1 result = A >> 2;

2 // result = 2

3 // A = 0b00001000

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 por2o 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 possua8 leds ligados ao microcontrolador. Cada led é

representado através de 1 bit de uma variável. Para ligarmos ou desligarmos apenas um led por

(30)

3.6

Ligar um bit (bit set)

Para ligar apenas um bit, utilizaremos uma operação OU. Supondo dois operandos A e B. Se A é1o resultado de (A | B) é1independente de B. Se A é0o 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ão0'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á valor1na posição X e manterá os valores antigos para as demais posições. Exemplo: Ligar apenas o bit2da 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 // rotacionase 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 }

3.7

Desligar um bit (bit clear)

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 a1com 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 bit2

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)

(31)

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 // rotacionase a variável para que o bit 1 chegue na posição desejada

13 mascara = mascara << 2; // mascara = 0b00000100

14 // invertese 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.

3.8

Trocar o valor de um bit (bit flip)

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 bit2e6da 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 // rotacionase 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;

(32)

17 mascara = 1; // mascara = 0b00000001

18 // rotacionase 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.

3.9

Verificar o estado de um bit (bit test)

Para verificar se o bit X está com o valor1utilizaremos 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 for1a resposta será diferente de01.

Exemplo: Testar o bit2da 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 // rotacionase 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 }

(33)

3.10

Criando funções através de define’s

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 tabelas3.1,3.2,3.3e3.4.

Tabela3.1: Operação bit set com define

Operação Bit set

Passo a Passo

1 char bit = 2;

2 char mascara;

3 mascara = 1 << bit;

4 arg = arg | mascara;

5 //em 1 linha

6 arg = arg | (1<<bit);

7 //ou

8 arg |= (1<<bit);

Exemplo de uso

1 //Ligando o bit 2 da porta D

2 PORTD = PORTD | (1<<2);

3 //ou

4 PORTD |= (1<<2);

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

Exemplo de uso com de-fine

1 //Ligando o bit 2 da porta D

(34)

Tabela3.2: Operação bit clear com define

Operação Bit clear

Passo a Passo

1 char bit = 2;

2 char mascara;

3 mascara = 1 << bit;

4 arg = arg & ~mascara;

5 //em 1 linha

6 arg = arg & ~(1<<bit);

7 //ou

8 arg &= ~(1<<bit);

Exemplo de uso

1 //Desligando o bit 2 da porta D

2 PORTD = PORTD & ~(1<<2);

3 //ou

4 PORTD &= ~(1<<2);

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

Exemplo de uso com de-fine

1 //Desligando o bit 2 da porta D

2 BitClr(PORTD,2);

Tabela3.3: Operação bit flip com define

Operação Bit flip

Passo a Passo

1 char bit = 2;

2 char mascara;

3 mascara = 1 << bit;

4 arg = arg ^ mascara;

5 //em 1 linha

6 arg = arg ^ (1<<bit);

7 //ou

8 arg ^= (1<<bit);

Exemplo de uso

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

2 PORTD = PORTD ^ (1<<2);

3 //ou

4 PORTD ^= (1<<2);

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

Exemplo de uso com de-fine

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

(35)

Tabela3.4: Operação bit test com define

Operação Bit test

Passo a Passo

1 char bit = 2;

2 char mascara;

3 mascara = 1 << bit;

4 if (arg & mascara)

5 //em 1 linha

6 if (arg & (1<<bit))

Exemplo de uso

1 //Testando o bit 2 da porta D

2 if (PORTD | (1<<2)){

3 //...

4 }

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

Exemplo de uso com de-fine

1 //Testando o bit 2 da porta D

2 if (BitTst(PORTD,2)){

3 //...

(36)

C

APÍTULO

4

Debug de sistemas embarcados

1

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.

4.1

Externalizar as informações

A primeira necessidade é conhecer o que está acontecendo em teu sistema. Na programação tradicional para desktop é comum utilizarmos de mensagens no console avisando o estado do programa.

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

1Mais informações sobre debug de sistemas embarcados referir ao artigo “The ten secrets of embedded debugging” de Stan Schneider e Lori Fraleigh

(37)

8 } else {

9 printf("Problemas na comunicacao");

10 }

11 return 0;

12 }

Devemos ter em mente onde é necessário colocar estes alertas e lembrar deretirá-losdo 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 valor0(zero) e desligado com o valor1(um). Além disso temos que configurar a porta D. Isto é feito iniciando a variável TRISD com o valor0x002.

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

4.2

Programação incremental

Ao invés de escrever todo o código e tentar compilar, é interessante realizar testes incrementais. A cada alteração no código realizar um novo teste. Evitar alterar o código em muitos lugares simultaneamente, no caso de aparecer um erro fica mais difícil saber onde ele está.

4.3

Checar possíveis pontos de memory-leak

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

4.4

Cuidado com a fragmentação da memória

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.

(38)

4.5

Otimização de código

Apenas se preocupe com otimização se estiver tendo problemas com o cumprimento de tarefas. Mesmo assim considere em migrar para uma plataforma mais poderosa. Sistemas embarcados preconizam segurança e não velocidade.

Caso seja necessário otimizar o código, analise antes o local de realizar a otimização. Não adianta otimizar uma função grande se ela é chamada apenas uma vez. Utilize-se de ferramentas do tipo 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.

4.6

Reproduzir e isolar o erro

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 }

(39)

P

ar

te

II

Arquitetura de microcontroladores

5⇀Microcontrolador,37

6⇀Esquema elétrico e circuitos importantes,43

(40)

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

(41)

Imagem

Referências