• Nenhum resultado encontrado

ELT024-NotasAulaLimpo

N/A
N/A
Protected

Academic year: 2021

Share "ELT024-NotasAulaLimpo"

Copied!
112
0
0

Texto

(1)

Rodrigo Maximiano Antunes de Almeida

Instituto de Engenharia de Sistemas e Tecnologia da Informação,

Universidade Federal de Itajubá,

Minas Gerais,

Brasil

rodrigomax @ unifei.edu.br

5 de Outubro de 2010

(2)

Conteúdo

1 Introdução 1

1.1 Linguagem C . . . 1

1.2 Hardware utilizado . . . 2

1.3 Ambiente de programação . . . 3

2 Programação em C para sistemas embarcados 11 2.1 Indentação e padrão de escrita . . . 11

2.2 Comentários . . . 13 2.3 Arquivos .c e .h . . . 13 2.4 Diretivas de compilação . . . 15 2.5 Tipos de dados em C . . . 19 2.6 Operações aritméticas . . . 23 2.7 Função main() . . . 24 2.8 Rotinas de tempo . . . 26

2.9 Operações com bits . . . 28

2.10 Debug de sistemas embarcados . . . 33

2.11 Ponteiros e endereços de memória . . . 37

3 Arquitetura de microcontroladores 39 3.1 Acesso à memória . . . 40

3.2 Clock e tempo de instrução . . . 42

3.3 Esquema elétrico e circuitos importantes . . . 42

3.4 Registros de conguração do microcontrolador . . . 44

4 Programação dos Periféricos 46 4.1 Acesso às "portas" do microcontrolador . . . 46

4.2 Conguração dos periféricos . . . 48

4.3 Barramento de Led's . . . 50 4.4 Display de 7 segmentos . . . 52 4.5 Leitura de teclas . . . 59 4.6 Display LCD 2x16 . . . 64 4.7 Comunicação serial . . . 73 4.8 Conversor AD . . . 78 4.9 Saídas PWM . . . 80 4.10 Timer . . . 86 4.11 Reprodução de Sons . . . 88 4.12 Interrupção . . . 88 4.13 Watchdog . . . 93

(3)

5 Arquitetura de desenvolvimento de software 94 5.1 One single loop . . . 94 5.2 Interrupt control system . . . 94 5.3 Cooperative multitasking . . . 97

6 Anexos 103

6.1 cong.h . . . 103 6.2 basico.h . . . 103 6.3 Instalar gravadores/depuradores de PIC em sistemas x64 . . . 105

(4)

Lista de Figuras

1.1 Camadas de abstração de um sistema operacional . . . 1

1.2 Pesquisa sobre linguagens utilizadas para projetos de software embarcado . . . . 2

1.3 Conguração das ferramentas de compilação . . . 5

1.4 Instalação do ICD2 . . . 6

1.5 Resumo das congurações do ICD2 no MPLAB . . . 7

1.6 Project Explorer do MPLAB . . . 8

1.7 Comparativo de características da família PIC 18fxx5x . . . 10

2.1 Problema das Referências Circulares . . . 17

2.2 Solução das referências circulares com #ifndef . . . 18

2.3 Loop innito de um device driver gerando erro no sistema . . . 25

2.4 Exemplo de funcionamento do vetor de interrupção . . . 25

3.1 Arquitetura do microcontrolador PIC 18F4550 . . . 39

3.2 Memória como um armário . . . 40

3.3 Memória e periféricos como um armário . . . 41

3.4 Regiões de memórias disponíveis no PIC18F4550 . . . 41

3.5 Esquema elétrico: Microcontrolador PIC 18F4550 . . . 43

3.6 Registros de conguração do microcontrolador PIC 18F4550 . . . 45

4.1 Registros de conguração dos periféricos do PIC 18F4550 . . . 49

4.2 Barramento de Led's . . . 52

4.3 Display de 7 Segmentos . . . 53

4.4 Diagrama elétrico para display de 7 segmentos com anodo comum . . . 53

4.5 Ligação de 4 displays de 7 segmentos multiplexados . . . 55

4.6 Circuito de leitura de chave . . . 59

4.7 Oscilação do sinal no momento do chaveamento . . . 60

4.8 Circuito de debounce . . . 60

4.9 Utilização de ltro RC para debounce do sinal . . . 61

4.10 Teclado em arranjo matricial . . . 63

4.11 Caracteres disponíveis para ROM A00 . . . 68

4.12 Caracteres disponíveis para ROM A02 . . . 69

4.13 Sinal serializado para transmissão em RS232 . . . 75

4.14 Lâmpada incandescente . . . 78

4.15 Potenciômetro . . . 78

4.16 Potenciômetro como divisor de tensão . . . 79

4.17 Circuito integrado LM35 . . . 79

4.18 Diagrama de blocos do LM35 . . . 80

(5)

5.1 Exemplo de máquina de estados . . . 97

5.2 Exemplo da mudança de slots no tempo . . . 101

5.3 Linha de tempo de um sistema com 1 slot . . . 101

(6)

Lista de Tabelas

1.1 Softwares utilizados no curso . . . 3

2.1 Tipos de dados e faixa de valores . . . 19

2.2 Representação decimal - binária - hexadecimal . . . 20

2.3 Alteração de tamanho e sinal dos tipos básicos . . . 21

2.4 Operação bit set com dene . . . 34

2.5 Operação bit clear com dene . . . 34

2.6 Operação bit ip com dene . . . 34

2.7 Operação bit test com dene . . . 35

3.1 Quantidade de operações e tarefas . . . 43

4.1 Endereços de memória para as portas do PIC 18F4550 . . . 47

4.2 Resumo de registros do PIC 18F4550 . . . 48

4.3 Tabela de conguração do PIC para as experiências . . . 51

4.4 Conversão binário - hexadecimal para displays de 7 segmentos . . . 54

4.5 Lista de comandos aceitos pelo o LCD . . . 70

4.6 Taxas de transmissão para diferentes protocolos . . . 73

4.7 Cálculo do valor da taxa de transmissão da porta serial . . . 74

(7)

Capítulo 1

Introdução

The real danger is not that computers will begin to think like men, but that men will begin to think like computers. - Sydney J. Harris

Programação para sistemas embarcados exige uma série de cuidados especiais, pois estes sis-temas geralmente possuem restrições de memória e processamento. Por se tratar de sissis-temas com funções especícas, as rotinas e técnicas de programação diferem daquelas usadas para projetos de aplicativos para desktops.

Também é necessário conhecer mais a fundo o hardware que será utilizado, pois cada micro-processador possui uma arquitetura diferente, com quantidade e tipos de instruções diversos. Programadores voltados para desktops não precisam se ater tanto a estes itens, pois eles pro-gramam para um sistema operacional que realiza o papel de tradutor, disponibilizando uma interface comum, independente do hardware utilizado(Figura 1.1).

Firmware Hardware Sistema Operacional

Aplicação

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

Para sistemas embarcados, é necessário programar especicamente para o hardware em questão. Uma opção para se obter articialmente 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 lingua-gem de alto nível em uma lingualingua-gem que o microcontrolador consegue entender. A biblioteca de funções, ou framework, em geral, é disponibilizada pelos fabricantes do microcontrolador.

1.1 Linguagem C

(8)

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.

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

Figura 1.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%.

1.2 Hardware utilizado

People who are really serious about software should make their own hard-ware. - Alan Kay

Como o enfoque deste curso é a programação de sistemas embarcados e não a eletrônica, utilizaremos 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

(9)

ˆ 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 ligado ventilador 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.

1.3 Ambiente de programação

First, solve the problem. Then, write the code. - John Johnson

O ambiente utilizado será o MPLAB(R). Este é um ambiente de desenvolvimento disponibi-lizado pela Microchip(R) gratuitamente. O compilador utidisponibi-lizado 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 uti-lizada 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.

Para a programação em ambiente Linux recomenda-se o uso da suíte PIKLAB 15.10. Este programa foi desenvolvido para KDE 3.5. Além de permitir a integração com o mesmo compila-dor utilizado neste curso permite a programação do microcontrolacompila-dor utilizando o programacompila-dor ICD2 via USB.

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 MPLAB 8.50 Proprietário

Compilador SDCC 2.9.00 (win32) GPL

Linker/Assembler GPUtils 0.13.7 (win32) GPL

Plugin MPLAB sdcc-mplab 0.1 GPL

Todos os softwares são gratuitos e estão disponíveis na internet. Para correta instalação deve-se instalar os softwares segundo a sequência apresentada na Tabela 1.1. Anote o diretório onde cada software foi instalado.

Após a instalação dos softwares deve-se abrir o arquivo pic16devices.txt (de preferência no wordpad) que foi instalado no diretório do SDCC dentro da pasta include\pic16 (por padrão C:\Arquivos de programas\SDCC\include\pic16). Procure a seguintes linhas:

(10)

name 18f4550 using 18f2455

Trocar a letra f minúscula da primeira linha, apenas do 18f4550, para um F maiúsculo:

name 18F4550

using 18f2455

Após isto abrir a pasta onde foi instalado o MPLAB (por padrão: C:\Arquivos de progra-mas\Microchip\MPLAB IDE). Abrir a pasta Core\MTC Suites. Abrir os arquivos sdc-clink.mtc e gplink.mtc num editor de texto. Apagar o conteúdo do arquivo sdcsdc-clink.mtc. Copiar todo conteúdo do arquivo gplink.mtc para o arquivo sdcclink.mtc. Salvar.

Após o passo acima o arquivo sdcclink.mtc deverá conter o seguinte texto:

// Microchip Language Tools Configuration File // gplink // Craig Franklin [Tool] Tool=gplink ScriptExtension=lkr DefaultOptions= MultipleNodes=1 SpaceBetweenSwitchAndData=1 [0] Description=Output filename Switch=-o Data=1 MultipleOptions=0 OutputNameSwitch=Switch Hidden=1 [1] Description=Map file Switch=-m Data=0 MultipleOptions=0 [2] Description=COFF File Switch=-c Data=0 MultipleOptions=0 [3] Description=Hex Format OptionList=INHX8M;INHX8S;INHX32 INHX8M=-a INHX8M INHX8S=-a INHX8S INHX32=-a INHX32 Data=0 [4] Description=Quiet mode Switch=-q Data=0 [5] Description=Library directories Switch=-I Data=1 MultipleOptions=0 LibrarySwitch=Switch Hidden=1 [6]

Description=Linker script directories Switch=-I Data=1 MultipleOptions=0 LinkerScriptSwitch=Switch Hidden=1 [7]

Description=Use Shared Memory Switch=-r Data=0 [8] Description=Fill Value Switch=-f MultipleOptions=0 Data=1 [9] Description=Stack Size Switch=-t MultipleOptions=0 Data=1 [10]

Description=No List File switch=-l

Data=0

Em seguida abrir o programa MPLAB e ir ao menu Projects -> Set Language Tool Locations. Será apresentada uma tela similar a da Figura 1.3.

(11)
(12)

Selecione a ferramenta Small Device C Compiler for PIC16 (SDCC16). Expanda a opção Executables. A ferramenta gpasm e gplink são obtidas no diretório bin dentro de onde foi instalado o GPUtils (por padrão: C:\Arquivos de programas\gputils\bin). A ferramenta sdcc16 é encontrada no diretório bin dentro do diretório onde foi instalado o SDCC (por padrão: C:\Arquivos de programas\SDCC\bin\). Clicar em OK. Após estes passos a suíte MPLAB está pronta para trabalhar com o compilador SDCC+GPUtils.

Conguração do gravador ICD2

Após instalar o MPLAB já é possível fazer a instalação e conguração do gravador/depurador ICD2. Conecte-o a qualquer porta USB e aguarde a tela de instalação do Windows. Em algumas versões do windows pode acontecer de você ser perguntado se deseja instalar um software não assinado digitalmente, certique-se que a versão do rmware é pelo menos 1.0.0.0 da fabricante Microchip, conforme pode ser visto na Figura 1.4 e avance.

Figura 1.4: Instalação do ICD2

Após o termino da instalação abra o programa MPLAB para congurar o gravador/depurador. Vá ao menu Programmer -> Select Programmer -> MPLAB ICD 2. Vá novamente ao menu Programmer mas desta vez escolha a opção  MPLAB ICD 2 Setup Wizard.

No wizard, escolha a comunicação como USB e depois diga que a placa possui alimentação independente Target has own power supply. Deixe as outras opções na seleção padrão. Antes de clicar em concluir verique ao nal se o resumo se parece com o da Figura 1.5.

Criação de um novo projeto

Recomenda-se a utilização do assistente disponível para a criação de um novo projeto (menu Project -> Project Wizard). Ele irá questionar sobre (entre parênteses os valores adotados neste curso):

(13)
(14)

2. A suíte de compilação (SDCC 16) 3. O diretório e nome do projeto

4. Arquivos já existentes cujo programador deseja incluir no projeto

Após estes passos o projeto estará criado. Caso a lista de arquivos do projeto não esteja visível vá ao menu View -> Project.

Para a criação de um novo arquivo vá até o menu File -> New. Neste novo arquivo digite alguma coisa e salve-o. Caso seja o arquivo que conterá a função principal (main) é costume salvá-lo com o nome de ”main.c”.

A cada novo arquivo criado é necessário inserí-lo no projeto. Para isso deve-se clicar na pasta correspondente ao tipo de arquivo que se deseja incluir e em seguida ”Add Files” como pode ser visualizado na Figura 1.6.

Figura 1.6: Project Explorer do MPLAB

Além dos arquivos criados pelo programador, existem 3 que devem ser adicionados ao pro-jeto: um linker e duas bibliotecas.

1. Linker

(a) C:\Arquivos de programas\gputils\lkr\18f4550.lkr 2. Bibliotecas

(a) C:\Arquivos de programas\SDCC\lib\pic16\libdev18f4550.lib (b) C:\Arquivos de programas\SDCC\lib\pic16\18f4550.lkr

O arquivo de linker é o responsável por indicar quais são os espaços de memória disponíveis no chip utilizado, onde começam e de que tipo são (RAM, ROM, Flash) etc.

(15)

// File: 18f4550.lkr

// Sample linker script for the PIC18F4550 processor // Not intended for use with MPLAB C18. For C18 projects, // use the linker scripts provided with that product. LIBPATH .

CODEPAGE NAME=page START=0x0 END=0x7FFF

CODEPAGE NAME=idlocs START=0x200000 END=0x200007 PROTECTED CODEPAGE NAME=config START=0x300000 END=0x30000D PROTECTED CODEPAGE NAME=devid START=0x3FFFFE END=0x3FFFFF PROTECTED CODEPAGE NAME=eedata START=0xF00000 END=0xF000FF PROTECTED ACCESSBANK NAME=accessram START=0x0 END=0x5F

DATABANK NAME=gpr0 START=0x60 END=0xFF DATABANK NAME=gpr1 START=0x100 END=0x1FF DATABANK NAME=gpr2 START=0x200 END=0x2FF DATABANK NAME=gpr3 START=0x300 END=0x3FF

DATABANK NAME=usb4 START=0x400 END=0x4FF PROTECTED DATABANK NAME=usb5 START=0x500 END=0x5FF PROTECTED DATABANK NAME=usb6 START=0x600 END=0x6FF PROTECTED DATABANK NAME=usb7 START=0x700 END=0x7FF PROTECTED ACCESSBANK NAME=accesssfr START=0xF60 END=0xFFF PROTECTED

Percebemos pelo linker acima que existem 256 bytes de memória eeprom, não volátil, que foi denominada eedata. Para a memória RAM está reservado um total de 2 kbytes, divididos1em 4

bancos de memória, sendo que o primeiro foi dividido em duas seções. Estes foram denominados (acessram-gpr0), gpr1, gpr2, gpr32.

Para o programa temos disponível uma região de 32 kbytes de memória ash, que vai da posição 0x0000 até 0x7FFF. Este é o mesmo endereço da memória RAM. Não existe conito, pois estamos trabalhando, no caso do PIC, com uma arquitetura Harvard. Nesta existem dois barramentos e duas memórias diferentes: uma para o programa, denominada CODEPAGE no linker, e uma para os dados, denominada DATABANK. Notar que apesar da memória eeprom ser utilizada para armazenamento não volátil de dados, ela está mapeada no barramento de código. Isto se deve a construção interna do microcontrolador.

Os dados apresentados no linker e descorridos anteriormente podem ser vericados e com-parados com outros modelos observando a Figura 1.7.

1Uma das maiores diculdades encontradas em se construir um compilador de linguagem C é o gasto em

termos de recursos computacionais que é dispendido para tratar estes quatro bancos como sequênciais. Na realidade eles estão todos sobre um mesmo endereço de memória. Para acessar cada um deles é necessário atuar sobre um registro no PIC, indicando qual banco estará ativo naquele momento.

(16)
(17)

Capítulo 2

Programação em C para sistemas

embarcados

C is quirky, awed, and an enormous success. - Dennis M. Ritchie

A programação para sistemas embarcados possui diversas características diferentes da progra-mação voltada para desktop. Do mesmo modo, existem alguns conceitos que geralmente não são explorados nos cursos de linguagens de programação em C mas que são essenciais para o bom desenvolvimento deste curso. Estes conceitos serão explanados neste capítulo.

2.1 Indentação e padrão de escrita

Good programmers use their brains, but good guidelines save us having to think out every case. - Francis Glassborow

É fundamental obedecer 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 e quando vamos fazer uma citação de itens colocamos cada um numa linha recuada à direita, algumas vezes com traços - para facilitar a identicação visual.

Com esse mesmo intuito utilizamos recuos, ou espaços, para facilitar que um outro progra-mador entenda o nosso código.

Como todo bloco de comandos é iniciado e terminado com uma chave tornou-se comum que estas 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 uxo do programa. Segue abaixo um exemplo de um mesmo código com diferença apenas na indentação.

(18)

Código indentado Código não indentado void main(void) interrupt 0

{

unsigned int i;

unsigned int temp;

unsigned int teclanova=0; InicializaSerial(); InicializaDisplays(); InicializaLCD(); InicializaAD(); for(;;) { AtualizaDisplay(); if (teclanova != Tecla) { teclanova = Tecla;

for(i=0;i<16;i++) { if (BitTst(Tecla,i)) { EnviaDados(i+48); } } } for(i = 0; i < 1000; i++); } }

void main(void) interrupt 0 {

unsigned int i;

unsigned int temp;

unsigned int teclanova=0; InicializaSerial(); InicializaDisplays(); InicializaLCD(); InicializaAD(); for(;;) { AtualizaDisplay(); if (teclanova != Tecla) { teclanova = Tecla;

for(i=0;i<16;i++) { if (BitTst(Tecla,i)) { EnviaDados(i+48); } } } for(i = 0; i < 1000; i++); } }

Outra característica de padronização esta 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 nome de variáveis sejam escritos apenas em minúsculas. Quando o nome é composto, se utiliza uma maiúscula para diferenciá-los como por exemplo as variáveis contPos e contTotal.

Nomes de função serão escritos com a primeira letra maiúscula e no caso de nome composto, cada inicial será grafada em maiúsculo: InicializaTeclado(), ParaSistema().

Tags de denições (utilizados em conjunto com a diretiva #dene) serão grafados exclusi-vamente em maiúsculo: NUMERODEVOLTAS, CONSTGRAVITACIONAL.

Cada chave será colocada numa única linha, conforme exemplo anterior, evitando-se cons-truções do tipo:

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

Ou

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

(19)

As regras apresentadas visam fornecer uma identidade visual ao código. Tais regras não são absolutas, servem apenas para o contexto desta apostila. Em geral, cada instituição ou projeto possui seu próprio conjunto de normas. É importante ter conhecimento deste conjunto e aplicá-lo em seu código.

O estilo adotado nesta apostila é conhecido também como estilo Allman, bsd (no emacs) ou ANSI, já que todos os documentos do padrão ANSI C utilizam este estilo. Apesar disto o padrão ANSI C não especica um estilo para ser usado.

2.2 Comentários

If the code and the comments disagree, then both are probably wrong. -Norm Schryer

Comentários são textos que introduzimos no meio do programa fonte com a intenção de torná-lo mais claro. É uma boa prática em programação inserir comentários no meio dos nossos programas. Pode-se comentar apenas uma linha usando o símbolo `//' (duas barras). Para comentar mais de uma linha usa-se o símbolo `/*' (barra e asterisco) antes do comentário e `*/' (asterisco e barra) para indicar o nal do comentário.

#include <stdio.h>

#define DIST 260 // distancia entre SP e Ita

int main(int argc, char* argv[]) {

/* esse programa serve para

mostrar como se insere comentários */

printf ("São Paulo está %d Km de Itajubá", DIST);

return 0; }

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, denimos as variáveis e realizamos a programação do código. Se existem dois arquivos .c no projeto e queremos que um deles possa usar as funções do outro arquivo, é necessário realizar um #include.

Os arquivos .h (header) tem como função ser um espelho dos arquivos .c disponibilizando as funções de um arquivo .c para serem utilizadas em outros arquivos. Nele colocamos todos os protótipos das funções que queremos que os outros arquivos usem.

Se quisermos que uma função só possa ser utilizada dentro do próprio arquivo, por motivo de segurança ou organização, basta declarar seu protótipo APENAS no arquivo .c.

Se for necessário que um arquivo leia e/ou grave numa variável de outro arquivo é recomen-dado criar funções especícas para tal nalidade.

O programa 1 apresenta um exemplo de um arquivo de código .c e o programa 2 apresenta o respectivo arquivo de header .h.

Podemos notar que no arquivo .h a função AtualizaDisplay() não está presente, deste modo ela não estará disponível para os outros arquivos. Podemos notar também que para ler ou gravar a variável digito é necessário utilizar as funções MudarDigito() e LerDigito(). Notar que não existe acesso direto às variáveis. Este tipo de abordagem insere atrasos no processamento devido à um efeito conhecido como overhead de funções, podendo inclusive causar travamentos no sistema caso não exista espaço suciente no stack.

(20)

Programa 1 Resumo do disp7seg.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 {

8 valor = val;

9 }

10 char LerDigito(void) 11 {

12 return valor;

13 }

14 void InicializaDisplays(void) 15 {

16 //código da função 17 }

18 //função usada apenas dentro deste arquivo 19 void AtualizaDisplay(void)

20 {

21 //código da função

22 }

Programa 2 Resumo do disp7seg.h

#ifndef VAR_H #define VAR_H

void MudaDigito(char val);

char LerDigito(void);

void InicializaDisplays(void);

(21)

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 de um arquivo que indique os protótipos das funções (como elas devem ser chamadas) e possuir a função disponível em sua forma compilada.

Em geral um arquivo que possui apenas protótipos de funções é denominado de Header e possui a extensão .h.

#dene

Outra diretiva muito conhecida é a #dene. Geralmente é utilizada para denir uma cons-tante mas pode ser utilizada para que o código fonte seja modicado antes de ser compilado.

Original Compilado Resultado na Tela

#define CONST 15

void main(void) {

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

void main(void) {

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

45

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

void MostraSaidaPadrao() {

#ifdef PADRAO Serial

char * msg = "SERIAL"; #else char * msg = "LCD"; #endif printf(msg); } #include <stdio.h> #define PADRAO Serial

void main(void) { MostraSaidaPadrao(); } SERIAL #include <stdio.h> #define PADRAO LCD

void main(void) {

MostraSaidaPadrao(); }

LCD

Pelo código apresentado percebemos que a mesma função MostraSaidaPadrao(), apresenta re-sultados diferentes dependendo de como foi denida a opção PADRAO.

Os denes também ajudam a facilitar a localização dos dispositivos e ajustar as congurações no microcontrolador. Todo periférico possui um ou mais endereços para os quais ele responde.

(22)

Estes endereços podem variar inclusive dentro de uma mesma família. Por exemplo o endereço da porta D (onde estão ligados os leds) é 0xF83. Para ligar ou desligar um led é preciso alterar o valor que esta dentro do endereço 0xF83. Para facilitar este procedimento, é denido um ponteiro para este endereço e rotulado com o nome PORTD. Denir OFF como 0 e ON como 1 facilita a leitura do código.

#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 siste-mas 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

void ImprimirTemp(char valor) { #ifdef LCD Imprime_LCD(valor) #else if (valor > 30) { led = 1; } else { led = 0; } #endif //LCD }

No momento da compilação o pré-compilador irá vericar se a tag LCD foi denida 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 aquivo da porta serial (serial.h) tem as seguintes funções, apresentadas a seguir.

char LerSerial(void);

void EnviaSerial(char val);

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

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 EnviaSerial() com o código 0x30. Para isso o arquivo temp.h deve incluir o arquivo serial.h.

#include "serial.h"

char LerTemperatura(void);

(23)

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

#include "temp.h"

char LerSerial(void);

void EnviaSerial(char val);

O problema é que deste modo é criada uma referência circular sem m: o compilador lê o arquivo serial.h e percebe que tem que inserir o arquivo temp.h. Inserindo o arquivo temp.h percebe que tem que inserir o arquivo serial.h, conforme pode ser visto na Figura 2.1.

#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

Figura 2.1: Problema das Referências Circulares

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 3. Programa 3 Estrutura de header

#ifndef TAG_CONTROLE #define TAG_CONTROLE

//todo o conteúdo do arquivo vem aqui.

#endif //TAG_CONTROLE

Segundo o código acima, o conteúdo que estiver entre o #ifndef e o #endif, só será mantido se a a tag TAG_CONTROLE NÃO estiver denida. Como isto é verdade durante a primeira leitura, o pré-compilador lê o arquivo normalmente. Se acontecer uma referência cíclica, na segunda vez que o arquivo for lido, a tag TAG_CONTROLE já estará denida impedindo assim que o processo cíclico continue, conforme pode ser visto na Figura 2.2.

Geralmente se utiliza como tag de controle o nome do arquivo. Esta tag deve ser única para cada arquivo.

(24)

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

char LerTemperatura(void); void AjustaCalor(char val); #endif temp.h #infdef SERIAL_H #define SERIAL_H #include “temp.h” char LerSerial(void); void EnviaSerial(char val); #endif serial.h #infdef TEMP_H //tag já definida, //pula o conteúdo #endif temp.h

(25)

2.5 Tipos de dados em C

19 Jan 2038 at 3:14:07 AM. The end of the world according to Unix (232

seconds after Jan 1st 1970) - Unix date system

O tipo de uma variável, informa a quantidade de memória, em bytes, que esta irá ocupar e como esta deve ser interpretada: com ou sem fração (vírgula). Os tipos básicos de dados na linguagem 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 -127 à 127

int 16 2 -32.768 à 32.767

oat 32 4 3,4 x 10-38 à 3,4 x 1038

double 64 8 3,4 x 1010308 -308 à 3,4 x

Podemos notar que as variáveis que possuem maior tamanho podem armazenar valores maiores. Notamos também que apenas os tipos oat 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 a grande utilização de números binários na programação de baixo nível é muito comum escrevemos 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.

(26)

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.

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

Para converter de binário para hexadecimal basta dividir o número em grupos de 4 em 4, da esquerda para a direita, 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: 12 = 116 00102 = 216. Logo: 100102. = 1216.

Modicadores de tamanho e sinal

Um modicador de tipo altera o signicado dos tipos base e produz um novo tipo. Existem quatro tipos de modicadores, dois para o tamanho (long e short) e dois para sinal (unsigned e signed). Um tipo declarado com o modicador 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 o valor máximo que podem atingir é menor. Os tipos declarados como unsigned não podem assumir valores 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.

Na linguagem C, por padrão os tipos são sinalizados, ou seja, possuem parte positiva e negativa. Por isso é raro encontrar o modicador signed.

Modicadores de acesso

Durante o processo de compilação, existe uma etapa de otimização do programa. Durante esta etapa, o compilador pode retirar partes do código ou desfazer loops com períodos xos. Por exemplo o código abaixo:

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

(27)

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

{

while (X!=X); }

Quando compilado apresenta o seguinte código em assembler:

// Starting pCode block

S_Teste__main code _main:

.line 19 // Teste.c while (X!=X);

RETURN

Enquanto a variável x for diferente de x o programa não sai do loop. O compilador entende que esta condição nunca irá acontecer e elimina o loop do código nal como podemos 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 à um periférico físico, seu valor pode mudar independentemente do uxo do programa. Para indicar esta situação ao programa utilizamos a palavra reservada volatile.

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

void main(void) interrupt 0 {

while (X!=X); }

Gerando o código em assembler descrito abaixo:

// Starting pCode block

S_Teste__main code _main:

_00105_DS_:

.line 19 // Teste.c while (X != X);

MOVLW 0x83 //primeira parte do endereço

MOVWF r0x00

(28)

MOVWF r0x01

MOVFF r0x00, FSR0L MOVFF r0x01, FSR0H

MOVFF INDF0, r0x00 //realiza primeira leitura

MOVLW 0x83 //primeira parte do endereço

MOVWF r0x01

MOVLW 0x0f //segunda parte do endereço

MOVWF r0x02

MOVFF r0x01, FSR0L MOVFF r0x02, FSR0H

MOVFF INDF0, r0x01 //realiza segunda leitura

MOVF r0x00, W

XORWF r0x01, W

BNZ _00105_DS_ //faz o teste para igualdade

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 modicador para indicar que a variável representa um local que apenas pode ser lido e não modicado, por exemplo uma porta para entrada de dados. Nesta situação é comum utilizar as palavras volatile e const junto.

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

//inicio do programa

void main(void) interrupt 0 {

X = 3; }

Se tentarmos compilar este código aparecerá a seguinte mensagem de erro: Teste.c: error 33: Attempt to assign value to a constant variable (=) Modicadores de posicionamento

As variáveis podem ser declaradas utilizando os modicadores near e far. Estes modica-dores indicam ao compilador em qual região de memória devem ser colocadas as variáveis.

A região near geralmente se refere à zero page. É uma região mais fácil de ser acessada. A região far exige mais tempo para executar a mesma função que a near.

Podemos pensar nestas regiões como a memória RAM e a memória Cache do computador. A segunda é mais rápida, mas possui um alto custo e por isso geralmente é menor. Em algumas situações é interessante que algumas variáveis nunca saiam do cache, pois são utilizadas com grande frequência ou são críticas para o sistema.

Modicador de persistência

Em geral, as variáveis utilizadas dentro das funções perdem seu valor ao término da função. Para que este valor não se perca podemos utilizar um modicador de persistência: static. Com esse modicador a variável passa a possuir um endereço xo de memória dado pelo compila-dor. 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.

//cria um contador persistente que é //incrementado a cada chamada de função

int ContadorPersistente(int reseta) {

(29)

if (reseta) { variavel_persistente = 0; } else { return (variavel_persistente++); } return -1; }

2.6 Operações aritméticas

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

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 4 notamos alguns exemplos. Programa 4 Operações aritméticas com tipos diferentes

1 void main (void) 2 {

3 char var08; 4 int var16;

5 long int var32;

6 float pont16;

7 double pont32;

8 var8 = var8 + var16; // 1 9 var8 = var8 + var8; // 2 10 var16 = var8 * var8; // 3 11 var32 = var32 / var16; // 4 12 var32 = pont32 * var32; // 5 13 pont16 = var8 / var16; // 6 14 pont16 = pont32 * var32; // 7 15 pont16 = 40 / 80; // 8 16 }

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.

var32 = var8 + var16; // 1 corrigido

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.

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.

(30)

O quarto caso (linha 11) pode apresentar um problema de precisão. A divisão de dois inteiros não armazena parte fracionária. Se isto não for crítico para o sistema está correto. Lembrar que a divisão de números inteiros é mais rápida que de números fracionários.

O quinto caso (linha 12) pode apresentar um problema de precisão. O resultado da conta de um número inteiro com um ponto utuante é um ponto utuante. Armazenar esse valor num outro número inteiro gera perda de informação.

O sexto caso (linha 13) apresenta um problema muito comum. A divisão de dois números inteiros gera um outro número inteiro. Não importa se armazenaremos o valor numa variável de ponto utuante haverá perda de informação pois os operandos são inteiros. Para evitar esse problema é necessário um typecast.

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

O oitavo caso (linha 15) é similar ao sexto. Estamos realizando uma conta com dois números inteiros esperando que o resultado seja 0,5. Como os operandos são inteiros a expressão será avaliada como resultante em Zero. Uma boa prática é sempre usar ".0" ou "f" após o número para indicar operações com vírgula.

pont16 = 40f / 80.0; // 8 corrigido

Devemos tomar cuidado também com comparações envolvendo números com ponto utu-ante. float x = 0.1; while (x != 1.1) { printf("x = %f\n", x); x = x + 0.1; }

O trecho de código acima apresenta um loop innito. Como existem restrições de precisão nos números de ponto utuante (oat e double) nem todos os números são representados -elmente. Os erros de arredondamento podem fazer com que a condição (x !=1.1) nunca seja satisfeita. Sempre que houver a necessidade de comparação com números de ponto utuante utilizar maior, menor ou variações.

float x = 0.1;

while (x < 1.1) { printf("x = %f\n", x); x = x + 0.1;

}

Apesar de sutis este tipo de erro pode causar um mau funcionamento do sistema como visto na Figura 2.3.

2.7 Função main()

Todo sistema necessita de iniciar em algum lugar. Em geral, os microcontroladores, assim que ligados, procuram por suas instruções no primeiro ou último endereço de memória, dependendo da arquitetura utilizada. O espaço de memória disponível neste endereço é geralmente muito pequeno, apenas o necessário para inserir uma instrução de pulo e o endereço onde está a função

(31)

Figura 2.3: Loop innito de um device driver gerando erro no sistema 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

(32)

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

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.

Para o compilador SDCC/GPUtils no MPLAB é necessário indicar que queremos que a função main() seja chamada toda vez que o sistema for iniciado. Por isso é necessário que a posição de reset dentro do vetor de interrupção aponte para a função main. Isto é feito através do atributo interrupt 0 logo após o nome da função conforme pode ser visto no código abaixo. void main (void) interrupt 0

{

//aqui entra o código do programa

}

Outra coisa interessante é que para sistemas embarcados a função principal não recebe nem retorna nada. Como ela é a primeira a ser chamada não há como enviar algum valor por parâmetro. Ela também não retorna nada pois ao término desta o sistema não está mais operativo.

Em geral sistemas embarcados são projetados para começarem a funcionar assim que ligados e apenas parar sua tarefa quando desligados. Como todas as funcionalidades são chamadas dentro da função main()1 espera-se que o programa continue executando as instruções dentro

dela até ser desligado ou receber um comando para desligar. Este comportamento pode ser obtido através de um loop innito. Abaixo estão as duas alternativas mais utilizadas.

void main (void) interrupt 0 { for(;;) { //aqui entra o //código principal } }

void main (void) interrupt 0 { while(1) { //aqui entra o //código principal } }

2.8 Rotinas de tempo

Time is an illusion, lunchtime doubly so. - Ford Prefect

É muito comum necessitar que o microcontrolador que um tempo sem fazer nada. Uma maneira de atingir esse objetivo é utilizar um laço FOR2.

unsigned char i;

for(i=0; i < 10; i++);

1Em sistemas mais complexos algumas tarefas são executadas independentemente da função principal, tendo

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

(33)

Notar que não estamos utilizando os colchetes. Logo após fechar os parênteses já existe um ponto e virgula. Para entender como esse procedimento funciona, e estimar o tempo de espera é preciso entender como o compilador traduz essa função para assembler.

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

MOVF r0x00, W //inicializa W com 0 (1 ciclo)

SUBLW 0x0a //coloca o valor 10 (0x0a) no registro W (1 ciclo)

MOVWF r0x00 //muda o valor de W para F (1 ciclo)

_00107_DS_:

DECFSZ r0x00, F //decrementa F, se F > 0 executa a próxima linha (1 ciclo)

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 inicia-lização. Cada iteração exige 2 passos: uma comparação e um pulo3, 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.5us.4

Para termos um tempo de espera de 0.5s precisamos de 1 milhão de instruções. Se colocarmos loops encadeados podemos multiplicar a quantidade de instruções que serão executadas. Para obtermos um valor de 1 milhão de instruções devemos utilizar pelo menos 3 loops encadeados. Os valores dos loops são obtidos de maneira iterativa.

unsigned char i, j, k;

for(i=0; i < 34; i++) //3 + 34 * (30.003 + 3) = 1.020.207 instruções

{ for(j=0; j < 100; j++) //3 + 100 * (297 + 3) = 30.003 instruções { for(k=0; k < 98; k++); // 3 + 98 * (3) = 297 instruções } }

O código acima foi projetado para gerar um atraso de tempo de meio segundo. Compilando e realizando testes práticos podemos conrmar que o tempo real é aproximadamente 0.51 (s). Esta discrepância acontece porque agora temos 3 loops encadeados e cada qual com sua variável de controle. Deste modo o compilador precisa salvar e carregar cada variável para realizar a comparação.

Percebemos assim que para conhecer corretamente o funcionamento do sistema é necessário, em algumas situações, abrir o código em assembler gerado pelo compilador para entender como o este é executado. Nem sempre o compilador toma as mesmas decisões que nós. Além disso ele pode gerar otimizações no código. Existem dois tipos de otimização: uma visando diminuir o tempo de execução do sistema, deixando-o mais rápido e outra que reduz o tamanho do código nal, poupando espaço na memória.

A seguir apresentamos um exemplo de função que gera delays com tempo parametrizado. void delay(unsigned int DL)

{

unsigned char i, j, k;

while(DL--) //executa DL vezes.

{

for(i=0; i < 34; i++) //3 + 34 * (30.003 + 3) = 1.020.207 instruções

{

3Este valor só é valido 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.

(34)

for(j=0; j < 100; j++) //3 + 100 * (297 + 3) = 30.003 instruções { for(k=0; k < 98; k++); // 3 + 98 * (3) = 297 instruções } } } }

2.9 Operações com bits

All of the books in the world contain no more information than is broadcast as video in a single large American city in a single year. Not all bits have equal value. - Carl Sagan

A opção de trabalhar com os números em notação binária ou hexadecimal facilita a interpretação das saídas e entradas digitais dos microcontroladores. Cada terminal representa uma posição na palavra digital. Deste modo a mudança de um bit de zero para um aciona uma saída. Do mesmo modo, o pressionamento de uma chave muda apenas um bit.

Existem operações em C que nos permitem trabalhar com as variáveis levando em conta os valores individuais de cada bit. Estas operações são chamadas de bitwise operation.

Ao contrário das operações lógicas que retornam falso ou verdadeiro para as variáveis de entrada, as operações bitwise retornam 1 ou 0 para cada bit da palavra em questão.

NOT

A operação NOT lógica (operador !) retorna 1 se o valor for zero e 0 se o valor for um.

A !A

0 1

1 0

A operação bitwise NOT () executa uma NOT lógica para cada um dos bits:

Declaração Lógico Bitwise

char A = 12; // A = 0b00001100 result = !A;// result = 0

result = ~A;

// result = 243 // A = 0b00001100 // r = 0b11110011

AND

A operação AND lógica (operador &&) retorna 0 se algum dos valores for zero, e 1 se os dois valores forem diferentes de zero.

A B A&&B

0 0 0

0 1 0

1 0 0

(35)

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

char A = 8; // A = 0b00001000

char B = 5; // B = 0b00000101 result = A && B;// result = 1

result = A & B; // result = 0 // A = 0b00001000 // B = 0b00000101 // r = 0b00000000 OR

A operação OR lógica (operador ||) retorna 1 se algum dos valores for um, 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

char A = 8; // A = 0b00001000

char B = 5; // B = 0b00000101 result = A || B;// result = 1

result = A | B; // result = 13 // A = 0b00001000 // B = 0b00000101 // r = 0b00001101 XOR

A operação XOR não possui correspondente lógica na linguagem C. Esta operação pode ser representada como A XOR B = (A && !B)||(!A && B)

A B A ⊕B

0 0 0

0 1 1

1 0 1

1 1 0

A operação bitwise XOR (operador ) executa uma XOR lógica para cada par de bits e coloca o resultado na posição correspondente:

(36)

Declaração Lógico Bitwise char A = 8; // A = 0b00001000

char B = 5; // B = 0b00000101 // não existe em C

result = A ^ B; // result = 15 // A = 0b00001000 // B = 0b00000101 // r = 0b00001101 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

char A = 8; // A = 0b00001000 result = A << 2; // result = 32 // A = 0b00001000 // r = 0b00100000 result = A >> 2; // result = 2 // A = 0b00001000 // 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 xo ou utuante nem variáveis sinalizadas (signed).

Em diversas ocasiões é necessário que trabalhemos com os bits de maneira individual, prin-cipalmente quando estes bits representam saídas ou entradas digitais, por exemplo chaves ou leds.

Supondo que temos 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)

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

(37)

//defines para portas de entrada e saída

#define PORTD (*(volatile near unsigned char*)0xF83) #define TRISD (*(volatile near unsigned char*)0xF95)

//inicio do programa

void main(void) interrupt 0 {

char mascara; //variável que guarda a máscara

TRISD = 0x00; //configura a porta D como saída

PORTD = 0x00; //liga todos os leds (lógica negativa) //liga o primeiro bit da variável

mascara = 1; // bit = 0b00000001

// rotaciona-se a variável para que o bit 1 chegue na posição desejada

mascara = mascara << 2; // bit = 0b00000100 //Ligar o bit 2, desligando o 2o led

PORTD = PORTD | mascara;

//mantém o sistema ligado indefinidamente

for(;;); }

Desligar um bit (bit clear)

Para desligar apenas um bit o procedimento é similar ao utilizado para ligar. Ao invés de utilizarmos 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 à um 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.

//defines para portas de entrada e saída

#define PORTD (*(volatile near unsigned char*)0xF83) #define TRISD (*(volatile near unsigned char*)0xF95)

//inicio do programa

void main(void) interrupt 0 {

char mascara; //variável que guarda a máscara

TRISD = 0x00; //configura a porta D como saída

PORTD = 0xFF; //desliga todos os leds (lógica negativa) //liga o primeiro bit da variável

mascara = 1; // mascara = 0b00000001

// rotaciona-se a variável para que o bit 1 chegue na posição desejada

mascara = mascara << 2; // mascara = 0b00000100 // inverte-se os valores de cada bit

mascara = ~mascara; // mascara = 0b11111011 //Desliga o bit 2, ligando o 2o led

PORTD = PORTD & mascara;

//mantém o sistema ligado indefinidamente

for(;;); }

(38)

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

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

//defines para portas de entrada e saída

#define PORTD (*(volatile near unsigned char*)0xF83) #define TRISD (*(volatile near unsigned char*)0xF95)

//inicio do programa

void main(void) interrupt 0 {

char mascara; //variável que guarda a mascara

TRISD = 0x00; //configura a porta D como saída

PORTD = 0xF0; //desliga todos os 4 primeiros leds (lógica negativa) //liga o primeiro bit da variável

mascara = 1; // mascara = 0b00000001

// rotaciona-se a variável para que o bit 1 chegue na posição desejada

mascara = mascara << 2; // mascara = 0b00000100 // inverte-se os valores de cada bit da mascara

mascara = ~mascara; // mascara = 0b11111011 //Liga o bit 2, desligando o led 2

PORTD = PORTD ^ mascara;

//liga o primeiro bit da variável

mascara = 1; // mascara = 0b00000001

// rotaciona-se a variável para que o bit 1 chegue na posição desejada

mascara = mascara << 6; // mascara = 0b01000000 // inverte-se os valores de cada bit da mascara

mascara = ~mascara; // mascara = 0b11111011 //Desliga o bit 6, ligando o led 6

PORTD = PORTD ^ mascara;

//mantém o sistema ligado indefinidamente

for(;;); }

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.

(39)

Para vericar se o bit X está um 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 um a resposta será diferente de zero5.

Exemplo: Testar o bit 3 e 4 da variável PORTD

//defines para portas de entrada e saída

#define PORTD (*(volatile near unsigned char*)0xF83) #define TRISD (*(volatile near unsigned char*)0xF95)

//inicio do programa

void main(void) interrupt 0 {

char mascara; //variável que guarda a mascara

char teste;

TRISD = 0x00; //configura a porta D como saída

teste = 0x00; //desliga todos os bits

//rodar depois o mesmo programa com os bits ligados. //teste = 0xff;

// cria uma variável onde APENAS o primeiro bit é 1

mascara = 1; // mascara = 0b00000001

// rotaciona-se a variável para que o bit 1 chegue na posição desejada

mascara = mascara << 2; // mascara = 0b00000100 //Verifica apenas o bit 2

if (teste & mascara) {

PORTD = 0x00; //se o resultado for verdadeiro liga todos os leds

}

else

{

PORTD = 0xff; //se o resultado for falso desliga todos os leds

}

//mantém o sistema ligado indefinidamente

for(;;); }

Criando funções através de denes

Uma opção no uso de denes é 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 simplicação podemos criar uma função para facilitar o uso destas operações através de um dene conforme podemos ver nas tabelas 2.4, 2.5, 2.6 e 2.7.

2.10 Debug de sistemas embarcados

6

In the beginner's mind there are many possibilities; in the expert's mind there are few. - Shunryu Suzuki

5A maioria dos compiladores C adotam uma variável com valor diferente de zero como sendo verdadeiro. 6Mais informações sobre debug de sistemas embarcados referir ao artigo The ten secrets of embedded

(40)

Tabela 2.4: Operação bit set com dene

Operação Bit set

Passo a Passo

char bit = 2;

char mascara; mascara = 1 << bit; arg = arg | mascara;

Uma linha arg = arg | (1<<bit)

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

Tabela 2.5: Operação bit clear com dene

Operação Bit clear

Passo a Passo

char bit = 2;

char mascara; mascara = 1 << bit; arg = arg & ~mascara;

Uma linha arg = arg & ~(1<<bit)

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

Tabela 2.6: Operação bit ip com dene

Operação Bit ip

Passo a Passo

char bit = 2;

char mascara; mascara = 1 << bit; arg = arg ^ mascara;

Uma linha arg = arg ^ (1<<bit)

(41)

Tabela 2.7: Operação bit test com dene

Operação Bit clear

Passo a Passo

char bit = 2;

char mascara; mascara = 1 << bit; arg = arg & mascara;

Uma linha arg = arg & (1<<bit)

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

A vericação de sistemas embarcados apresenta algumas restrições, de modo geral não é possível inferir sobre a operação do sistema sem paralisá-lo. Como este tipo de sistema possui vários dispositivos agregados, que funcionam independentemente do processador, é necessário utilizar abordagens diferentes para realizar o debug.

Devemos lembrar que além do software devemos levar em conta possíveis problemas advindos do hardware. Debounce, tempo de chaveamento, limite do barramento de comunicação são exemplos de pontos a serem considerados no momento de depuração.

Externalizar as informações.

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

#include "stdio.h" #include "serial.h"

//inicio do programa

int main(int argc, char* argv[]) {

printf("Inicializando sistema");

if (CheckForData()) {

printf("Chegou informação"); }

else

{

printf("Problemas na comunicação"); }

return 0; }

Devemos ter em mente onde é necessário colocar estes alertas e lembrar de retirá-los do código nal.

Para a placa em questão utilizaremos o barramento de leds que está ligado à porta D. A operação deste dispositivo será estudada posteriormente em detalhes. Por enquanto basta sabermos que cada bit da variável PORTD está ligada à 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).

(42)

Além disso temos que congurar a porta D. Isto é feito iniciando a variável TRISD com o valor 0x007.

//defines para portas de entrada e saída

#define PORTD (*(volatile near unsigned char*)0xF83) #define TRISD (*(volatile near unsigned char*)0xF95)

//inicio do programa

void main(void) interrupt 0 {

//configurando todos os pinos como saídas

TRISD = 0x00;

PORTD = 0xFF; //desliga todos os leds //liga apenas o bit 1.

BitClr(PORTD,1);

//mantém o sistema ligado indefinidamente

for(;;); }

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

Programação incremental

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

Checar possíveis pontos de Memory-leak

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

Cuidado com a fragmentação da memória

Sistemas com grande frequência na alocação/liberação de memória podem fragmentar a memó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.

Otimização de código

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

Caso seja necessário otimizar o código analise antes o local de realizar a otimização. Não adianta otimizar uma função grande se ela é chamada apenas uma vez. Utilize-se de ferramentas do tipo proler sempre que possível. Isto evita a perda de tempo e auxilia o programador a visualizar a real necessidade de otimização de código.

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 vericar se ele foi eliminado.

No momento em que se consegue um procedimento de como reproduzir o erro podemos começ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 innito dentro de um teste, que visa vericar 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.

(43)

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

if (PORTB >= 5) //PORTB não deveria ser um valor maior que 5.

{

BitClr(PORTD,3); //liga o led 3

for(;;); //trava o programa

}

// aqui continua com um monte de código...

2.11 Ponteiros e endereços de memória

Writing in C or C++ is like running a chain saw with all the safety guards removed. - Bob Gray

Toda variável criada é armazenada em algum lugar da memória. Este lugar é denido de maneira única através de um endereço.

Para conhecermos o endereço de uma variável podemos utilizar o operador &. Cuidado! Este operador também é utilizado para realização da operação bitwise AND. Exemplo:

//cria a variável a num endereço de memória a ser //decidido pelo compilador

int a = 0; a = a + 1;

printf( a ); //imprime o valor 1

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 denir um ponteiro também precisamos indicar ao compilador um tipo. A diferença é que o tipo indica "quanto" cabe no local apontado pelo ponteiro e não o próprio ponteiro.

Sintaxe:

tipo * nome da variavel;

Exemplo: int *apint;

float *apfloat;

Deve-se tomar cuidado, pois nos exemplos acima, apint e apoat são variáveis que armaze-nam endereços de memória e não valores tipo int ou oat. O lugar APONTADO pela variável apint é que armazena um inteiro, do mesmo modo que o lugar apontado por oat armazena um valor fracionário.

Se quisermos manipular o valor do endereço utilizaremos apint e apoat mas se quisermos manipular o valor que esta dentro deste endereço devemos usar um asterisco antes do nome da variável. Exemplo:

apfloat = 3.2; *apfloat = 3.2;

Referências

Documentos relacionados

O tratamento específico para a doença ainda não foi determinado, pois ele pode variar de acordo com o estágio em que o paciente se encontre, porém o mesmo esta baseado

estamos tratando de um problema restrito, isto é, o número de retângulos menores possui uma demanda que deve ser respeitada. Neste estudo a quantidade de

Os interessados em adquirir quaisquer dos animais inscritos nos páreos de claiming deverão comparecer à sala da Diretoria Geral de Turfe, localizada no 4º andar da Arquibancada

Acreditamos que o estágio supervisionado na formação de professores é uma oportunidade de reflexão sobre a prática docente, pois os estudantes têm contato

•   O  material  a  seguir  consiste  de  adaptações  e  extensões  dos  originais  gentilmente  cedidos  pelo 

hospitalizados, ou de lactantes que queiram solicitar tratamento especial deverão enviar a solicitação pelo Fale Conosco, no site da FACINE , até 72 horas antes da realização

Na fachada posterior da sede, na parte voltada para um pátio interno onde hoje há um jardim, as esquadrias não são mais de caixilharia de vidro, permanecendo apenas as de folhas

d) os dados obtidos na avaliação fonoaudiológica foram, na maioria da vezes, suficientes para definir a conduta fonoaudiológica quanto à necessidade de avaliação abrangente ou