• Nenhum resultado encontrado

ELT024-NotasAulaLimpo

N/A
N/A
Protected

Academic year: 2021

Share "ELT024-NotasAulaLimpo"

Copied!
111
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 20 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 Instalação . . . 3

Conguração do gravador ICD2 . . . 4

Criação de um novo projeto . . . 6

2 Linguagem C para sistemas embarcados 9 2.1 Indentação e padrão de escrita . . . 9

2.2 Comentários . . . 11

2.3 Arquivos .c e .h . . . 11

2.4 Diretivas de compilação . . . 11

#include . . . 13

#dene . . . 13

#ifdef, #ifndef, #else e #endif . . . 14

2.5 Tipos de dados em C . . . 16

Representação binária e hexadecimal . . . 17

Modicadores de tamanho e sinal . . . 18

Modicadores de acesso . . . 18 Modicadores de posicionamento . . . 20 Modicador de persistência . . . 20 2.6 Operações aritméticas . . . 20 2.7 Função main() . . . 22 2.8 Rotinas de tempo . . . 24

2.9 Operações com bits . . . 25

NOT . . . 25

AND . . . 26

OR . . . 26

XOR . . . 27

Shift . . . 27

Ligar um bit (bit set) . . . 27

Desligar um bit (bit clear) . . . 28

Trocar o valor de um bit (bit ip) . . . 29

Vericar o estado de um bit (bit test) . . . 30

Criando funções através de denes . . . 30

2.10 Debug de sistemas embarcados . . . 30

Externalizar as informações. . . 33

Programação incremental . . . 33

Checar possíveis pontos de Memory-leak . . . 34

Cuidado com a fragmentação da memória . . . 34

Otimização de código . . . 34

(3)

3 Arquitetura de microcontroladores 36

3.1 Acesso à memória . . . 36

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

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

Multiplexação nos terminais do microcontrolador . . . 42

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

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

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

4.3 Barramento de Led's . . . 49

4.4 Display de 7 segmentos . . . 50

Multiplexação de displays . . . 52

Criação da biblioteca . . . 53

4.5 Leitura de teclas . . . 55

Debounce por software . . . 55

Arranjo de leitura por matriz . . . 58

Criação da biblioteca . . . 60 4.6 Display LCD 2x16 . . . 60 Criação da biblioteca . . . 67 4.7 Comunicação serial . . . 71 RS 232 . . . 71 Criação da biblioteca . . . 74 4.8 Conversor AD . . . 76 Elementos sensores . . . 76 Criação da biblioteca . . . 78 4.9 Saídas PWM . . . 80 Criação da biblioteca . . . 81 4.10 Timer . . . 84 4.11 Reprodução de Sons . . . 86 4.12 Interrupção . . . 87 4.13 Watchdog . . . 91

5 Arquitetura de desenvolvimento de software 92 5.1 One single loop . . . 92

5.2 Interrupt control system . . . 93

5.3 Cooperative multitasking . . . 93

Fixação de tempo para execução dos slots . . . 98

Utilização do "tempo livre" para interrupções . . . 99

6 Anexos 101 6.1 cong.h . . . 101

6.2 basico.h . . . 101

(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 . . . 5

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

1.6 Project Explorer do MPLAB . . . 7

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

2.1 Problema das Referências Circulares . . . 15

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

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

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

3.1 Arquitetura do microcontrolador PIC 18F4550 . . . 37

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

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

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

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

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

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

4.2 Barramento de Led's . . . 49

4.3 Display de 7 Segmentos . . . 50

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

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

4.6 Circuito de leitura de chave . . . 55

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

4.8 Circuito de debounce . . . 56

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

4.10 Teclado em arranjo matricial . . . 59

4.11 Display Alfanumérico LCD 2x16 . . . 62

4.12 Display Alfanumérico LCD 2x16 - verso . . . 63

4.13 Caracteres disponíveis para ROM A00 . . . 65

4.14 Caracteres disponíveis para ROM A02 . . . 66

4.15 Esquemático de ligação do display de LCD . . . 68

4.16 Sinal serializado para transmissão em RS232 . . . 73

4.17 Lâmpada incandescente . . . 76

4.18 Potenciômetro . . . 77

4.19 Potenciômetro como divisor de tensão . . . 77

4.20 Circuito integrado LM35 . . . 77

4.21 Diagrama de blocos do LM35 . . . 78

4.22 Sinais PWM com variação do duty cycle . . . 80

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

(5)
(6)

Lista de Tabelas

1.1 Softwares utilizados no curso . . . 3

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

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

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

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

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

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

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

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

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

4.2 Tabela de conguração do PIC para as experiências . . . 48

4.3 Conversão binário - hexadecimal para displays de 7 segmentos . . . 51

4.4 Lista de comandos aceitos pelo o LCD . . . 67

4.5 Taxas de transmissão para diferentes protocolos . . . 71

4.6 Cálculo do valor da taxa de transmissão da porta serial . . . 72

(7)

Listings

2.1 Resumo do disp7seg.c . . . 12

2.2 Resumo do disp7seg.h . . . 12

2.3 Estrutura de header . . . 16

2.4 Operações aritméticas com tipos diferentes . . . 21

4.1 disp7seg.c . . . 53

4.2 disp7seg.h . . . 54

4.3 Utilizando a biblioteca disp7seg . . . 54

4.4 teclado.c . . . 61

4.5 teclado.h . . . 61

4.6 Exemplo de uso da biblioteca teclado . . . 62

4.8 lcd.h . . . 69

4.7 lcd.c . . . 69

4.9 Exemplo de uso da biblioteca de LCD . . . 70

4.10 serial.c . . . 74

4.11 serial.h . . . 74

4.12 Exemplo de uso da biblioteca de comunicação serial . . . 75

4.13 adc.c . . . 79

4.14 adc.h . . . 79

4.15 Exemplo de uso da biblioteca de conversores AD . . . 79

4.16 pwm.c . . . 82

4.17 pwm.h . . . 82

4.18 Exemplo de uso da biblioteca das saídas PWM . . . 83

4.19 timer.c . . . 84

4.20 timer.h . . . 85

4.21 Exemplo de uso da biblioteca de um temporizador . . . 85

4.22 Reprodução de sons . . . 86

4.23 Fontes de Interupção . . . 88

4.24 Tratamento das interrupções . . . 89

4.25 Inicialização do sistema com interrupções . . . 90

4.26 Inicialização do sistema com interrupções . . . 91

5.1 Exemplo de arquitetura single-loop . . . 92

5.2 Problema na sincronia de tempo para o single-loop . . . 93

5.3 Exemplo de sistema Interrupt-driven . . . 94

5.4 Exemplo de sistema Interrupt-driven com base de tempo . . . 94

5.5 Exemplo de cooperative multitasking . . . 96

5.6 Exemplo de cooperative multitasking com uso do top slot . . . 97

5.7 Exemplo de sistema Cooperative-multitasking com slot temporizado . . . 98

6.1 cong.h . . . 101

(8)

Introdução

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

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

Também é necessário conhecer mais a fundo o hardware que será utilizado, pois cada mi-croprocessador possui uma arquitetura diferente, com quantidade e tipos de instruções diversos. Programadores voltados para desktops não precisam se ater tanto a estes itens, pois eles progra-mam 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 ques-tã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 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 Linguagem C

C is quirky, awed, 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.

(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 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, utili-zaremos um kit de desenvolvimento pronto, baseado num microcontrolador PIC.

Como periféricos disponíveis temos:

ˆ 1 display LCD 2 linhas por 16 caracteres (compatível com HD77480) ˆ 4 displays de 7 segmentos com barramento de dados compartilhados ˆ 8 leds ligados ao mesmo barramento dos displays

ˆ 16 mini switches organizadas em formato matricial 4x4 ˆ 1 sensor de temperatura LM35C

ˆ 1 resistência de aquecimento ligada a uma saída PWM ˆ 1 motor DC tipo ligado ventilador a uma saída PWM ˆ 1 buzzer ligado a uma saída PWM

(10)

ˆ 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 disponibilizado pela Microchip(R) gratuitamente. O compilador utilizado será o SDCC, os linkers e assemblers serão disponibilizados pela biblioteca GPUtils.

Como o foco é a aprendizagem de conceitos sobre programação embarcada poderá ser 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 compilador utilizado neste curso permite a programação do microcontrolador utilizando o programador 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:

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 pro-gramas\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.

(11)

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

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

Após o termino da instalação abra o programa MPLAB para congurar o gravador ou depu-rador. 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.

(12)

Figura 1.3: Conguração das ferramentas de compilação

(13)

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.

Figura 1.5: Resumo das congurações do ICD2 no MPLAB

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

1. O microcontrolador a ser utilizado (PIC18F4550) 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.

Além dos arquivos criados pelo programador, existem três arquivos que devem ser adicionados ao projeto: um de linker e dois de bibliotecas.

(14)

Figura 1.6: Project Explorer do MPLAB

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

// 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, divididos1 em 4

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

(15)

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 compa-rados com outros modelos observando a Figura 1.7.

Figura 1.7: Comparativo de características da família PIC 18fxx5x

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)

Linguagem 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 pri-meira linha. Quando é necessário realizar uma citação de itens coloca-se cada um destes itens numa linha recuada à direita, algumas vezes com um identicador como um traço - ou seta -> para facilitar a identicação visual.

Com esse mesmo intuito utiliza-se recuos e espaçamentos para que o código seja mais facil-mente entendido.

Como todo bloco de comandos é iniciado e terminado com uma chave, tornou-se comum que estas (as chaves) estejam no mesmo nível e todo código interno a elas seja deslocado à direita. Se existir um segundo bloco interno ao primeiro, este deve ser deslocado duas vezes para indicar a hierarquia no uxo do programa. Segue abaixo um exemplo de um mesmo código com diferença apenas na indentação.

(17)

Código indentado Código não indentado

1 void main (void) interrupt 0

{

unsigned int i ; unsigned int temp ;

unsigned int teclanova =0; InicializaSerial ( ) ; InicializaDisplays ( ) ; InicializaLCD ( ) ; InicializaAD ( ) ; for( ; ; ) { AtualizaDisplay ( ) ; i f ( teclanova != Tecla ) { teclanova = Tecla ; for( i=0;i <16;i++) { i f ( 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 ( ) ; i f ( teclanova != Tecla ) { teclanova = Tecla ; for( i=0;i <16;i++) { i f ( BitTst ( Tecla , i ) ) { EnviaDados ( i+48) ; } } } for( i = 0 ; i < 1000; i++); } }

Podemos notar pelo código anterior que aquele que possui identação facilita na vericação de quais instruções/rotinas estão subordinadas às demais.

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 exclusiva-mente em maiúsculo: NUMERODEVOLTAS, CONSTGRAVITACIONAL.

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

i f ( PORTA == 0x30 ) { PORTB = 0x10 ; } Ou

i f ( PORTA == 0x30 ) { PORTB = 0x10 ; }

As regras apresentadas visam fornecer uma identidade visual ao código. Tais regras não são absolutas, servem apenas para o contexto desta apostila. Em geral, cada instituição ou projeto

(18)

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 <s t d i o . h>

#define DIST 260 // d i s t a n c i a e n t r e SP e I t a

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

/* e s s e programa s e r v e para

mostrar como se i n s e r e 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 2.1 apresenta um exemplo de um arquivo de código .c e o programa 2.2 apre-senta o respectivo arquivo de header .h.

Podemos notar que no arquivo .h a função AtualizaDisplay() não está presente, deste modo ela não estará disponível para os outros arquivos. Podemos notar também que para ler ou gravar a variável digito é necessário utilizar as funções 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.

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 executa-das. Todas as diretivas de compilação começam com um sinal #, conhecido como jogo da velha ou hash.

(19)

Listing 2.1: Resumo do disp7seg.c

1 // v a r i á v e l usada apenas dentro d e s t e arquivo

2 static char temp ;

3 // v a r i á v e l que será usada também f o r a do arquivo

4 static char valor ;

5 // funções usadas dentro e f o r a 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 d e s t e arquivo

19 void AtualizaDisplay (void)

20 {

21 // código da função

22 }

Listing 2.2: Resumo do disp7seg.h

1 #ifndef VAR_H

2 #define VAR_H

3 void MudaDigito (char val ) ;

4 char LerDigito (void) ;

5 void InicializaDisplays (void) ;

(20)

#include

A diretiva de compilação #include é a responsável por permitir que o programador utilize no seu código funções que foram implementadas em outros arquivos, seja por ele próprio ou por outras pessoas. Não é necessário possuir o código fonte das funções que se deseja utilizar. É necessário apenas de um arquivo que indique os protótipos das funções (como elas devem ser chamadas) e possuir a função disponível em sua forma compilada.

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

#dene

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

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 <s t d i o . h> #define PADRAO S e r i a l

void main (void) { MostraSaidaPadrao ( ) ; } SERIAL #include <s t d i o . 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. 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.

(21)

#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

void ImprimirTemp (char valor )

{ #ifdef LCD Imprime_LCD ( valor ) #else i f ( 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) ; 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

#include "temp.h"

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

(22)

Listing 2.3: Estrutura de header

1 #ifndef TAG_CONTROLE

2 #define TAG_CONTROLE

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

5 #endif //TAG_CONTROLE

O problema é que deste modo é criada uma referência circular sem m: o compilador lê o arquivo serial.h e percebe que tem que inserir o arquivo temp.h. Inserindo o arquivo temp.h percebe que tem que inserir o arquivo serial.h, conforme pode ser visto na Figura 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 2.3.

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.

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

(23)

#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

(24)

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 10-308 à 3,4 x 10308

Podemos notar que as variáveis que possuem maior tamanho podem armazenar valores mai-ores. Notamos também que apenas os tipos oat e double possuem casas decimais.

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.

A base hexadecimal possui 16 "unidades"diferentes. Como existem apenas 10 algarismos no sistema de numeração arábico (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) utilizamos 6 letras para complementá-los (A, B, C, D, E, F). A conversão entre valores binários, decimais e hexadecimais é apresentada na Tabela 2.2.

Para converter de binário para hexadecimal basta dividir o número em grupos de 4 em 4, da 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

(25)

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

Tabela 2.3: Alteração de tamanho e sinal dos tipos básicos

Tipo Bytes Excursão máxima

unsigned char 1 0 à 255

signed char 1 -128 à 127

unsigned int 2 0 à 65.535

signed int 2 -32.768 à 32.767

long int 4 -2.147.483.648 à 2.147.483.647 unsigned long int 4 0 à 4.294.967.295

short int 2 -32.768 à 32.767

Na linguagem C, por padrão os tipos são sinalizados, ou seja, possuem parte positiva e negativa. Por isso é raro encontrar o 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:

(26)

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

void main (void) interrupt 0

{

while ( X!=X ) ; }

Quando compilado apresenta o seguinte código em assembler:

// S t a r t i n g pCode b l o c k

S_Teste__main code _main :

. line 19 // Teste . c w h i l e (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:

// S t a r t i n g pCode b l o c k

S_Teste__main code _main :

_00105_DS_ :

. line 19 // Teste . c w h i l e (X != X) ;

MOVLW 0x83 // primeira p a r t e do endereço

MOVWF r0x00

MOVLW 0x0f // segunda p a r t e do endereço

MOVWF r0x01

MOVFF r0x00 , FSR0L

MOVFF r0x01 , FSR0H

MOVFF INDF0 , r0x00 // r e a l i z a primeira l e i t u r a

MOVLW 0x83 // primeira p a r t e do endereço

MOVWF r0x01

MOVLW 0x0f // segunda p a r t e do endereço

MOVWF r0x02

MOVFF r0x01 , FSR0L

MOVFF r0x02 , FSR0H

MOVFF INDF0 , r0x01 // r e a l i z a segunda l e i t u r a

MOVF r0x00 , W

XORWF r0x01 , W

BNZ _00105_DS_ // f a z o t e s t e para i g u a l d a d e

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

(27)

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 )

// i n i c i o do programa

void main (void) interrupt 0

{

X = 3 ; }

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

Modicadores de posicionamento

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

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

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

Modicador de persistência

Em geral, as variáveis utilizadas dentro das funções perdem seu valor ao término da função. Para que este valor não se perca podemos utilizar um modicador de persistência: static. Com esse modicador a variável passa a possuir um endereço xo de memória dado pelo compilador. Além disso o compilador não reutiliza este endereço em nenhuma outra parte do código, garantindo que na próxima vez que a função for chamada o valor continue o mesmo.

// c r i a um contador p e r s i s t e n t e que é // incrementado a cada chamada de função

int ContadorPersistente (int reseta )

{

static char variavel_persistente ; i f ( 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 2.4 notamos alguns exemplos.

(28)

Listing 2.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 / 8 0 ; // 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 c o r r i g i d o

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 c o r r i g i d o

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.

var16 = ( (int) var8 ) * var8 ; // 3 c o r r i g i d o

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 c o r r i g i d o

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.

(29)

pont16 = 40f / 8 0 . 0 ; // 8 c o r r i g i d o

Devemos tomar cuidado também com comparações envolvendo números com ponto utuante.

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 estes tipos de erro podem causar um mau funcionamento do sistema. Na Figura 2.3 é apresentado um erro gerado através de um loop innito.

Figura 2.3: Loop innito de um device driver gerando erro no sistema

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

(30)

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

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

Figura 2.4: Exemplo de funcionamento do vetor de interrupção

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.

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

(31)

void main (void) interrupt 0 { for( ; ; ) { // aqui entra o // código p r i n c i p a l } }

void main (void) interrupt 0

{ while( 1 ) { // aqui entra o // código p r i n c i p a l } }

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 < 1 0 ; i++);

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 e q u i v a l e n t e à f o r ( i =0; i <10; i++);

MOVF r0x00 , W // i n i c i a l i z a W com 0 (1 c i c l o )

SUBLW 0x0a // c o l o c a o v a l o r 10 (0 x0a ) no r e g i s t r o W (1 c i c l o )

MOVWF r0x00 //muda o v a l o r de W para F (1 c i c l o )

_00107_DS_ :

DECFSZ r0x00 , F // decrementa F, se F > 0 executa a próxima l i n h a (1 c i c l o )

BRA _00107_DS_ //" pula " para o l u g a r marcado como _00107_DS_ (2 c i c l o s )

Percebemos pelo código acima que para realizar um for precisamos de 3 passos de inicialização. Cada iteração exige 2 passos: uma comparação e um 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 < 3 4 ; i++) //3 + 34 * (30.003 + 3) = 1.020.207 i n s t r u ç õ e s

{ for( j=0; j < 100; j++) //3 + 100 * (297 + 3) = 30.003 i n s t r u ç õ e s { for( k=0; k < 9 8 ; k++); // 3 + 98 * (3) = 297 i n s t r u ç õ e s } }

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

2Este método não é aconselhado em sistemas de maior porte.

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.

(32)

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 v e z e s .

{

for( i=0; i < 3 4 ; i++) //3 + 34 * (30.003 + 3) = 1.020.207 i n s t r u ç õ e s

{ for( j=0; j < 100; j++) //3 + 100 * (297 + 3) = 30.003 i n s t r u ç õ e s { for( k=0; k < 9 8 ; k++); // 3 + 98 * (3) = 297 i n s t r u ç õ e s } } } }

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

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 modiquem apenas os bits desejados, mantendo o restante dos bits da variável inalterados.

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

É importante ressaltar que as operações de bitwise possuem funcionalidade semelhante a suas respectivas operações lógicas. A diferença é que a lógica opera em cima da variável como um todo5 enquanto a bitwise opera bit à bit.

NOT

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

0 1 1 0

A operação bitwise NOT (operador ) executa uma NOT lógica. Isso signica que a operação é realizada para cada um dos bits da variável, não mais para a variável como um todo. Na tabela seguinte é apresentada a diferença entre as duas operações.

5Lembrar que para linguagem C uma variável com valor 0 (zero) representa falso, e qualquer outro valor

(33)

Declaração Lógico Bitwise char A = 1 2 ; // A = 0 b00001100 // r e s u l t = 0result = ! A ; result = ~A ; // r e s u l t = 243 // A = 0 b00001100 // r = 0 b11110011 AND

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

A B A&&B

0 0 0

0 1 0

1 0 0

1 1 1

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

Declaração Lógico Bitwise

char A = 8 ; // A = 0 b00001000 char B = 5 ; // B = 0 b00000101 result = A && B ; // r e s u l t = 1 result = A & B ; // r e s u l t = 0 // A = 0 b00001000 // B = 0 b00000101 // r = 0 b00000000 OR

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

A B A||B

0 0 0

0 1 1

1 0 1

1 1 1

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

Declaração Lógico Bitwise

char A = 8 ; // A = 0 b00001000 char B = 5 ; // B = 0 b00000101 result = A | | B ; // r e s u l t = 1 result = A | B ; // r e s u l t = 13 // A = 0 b00001000 // B = 0 b00000101 // r = 0 b00001101

(34)

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:

Declaração Lógico Bitwise

char A = 8 ; // A = 0 b00001000 char B = 5 ; // B = 0 b00000101 // não e x i s t e em C result = A ^ B ; // r e s u l t = 15 // A = 0 b00001000 // B = 0 b00000101 // r = 0 b00001101 Shift

A operação shift desloca os bits para a esquerda (operador <<) ou direita (operador >>). É necessário indicar quantas casas serão deslocadas.

Declaração Shift Esquerda Shift Direita

char A = 8 ; // A = 0 b00001000 result = A << 2 ; // r e s u l t = 32 // A = 0 b00001000 // r = 0 b00100000 result = A >> 2 ; // r e s u l t = 2 // A = 0 b00001000 // r = 0 b00000010

Para variáveis unsigned e inteiras, esta operação funciona como a multiplicação/divisão por potência de dois. Cada shift multiplica/divide por 2 o valor. Esta é uma prática muito comum para evitar a divisão que na maioria dos sistemas embarcados é uma operação cara do ponto de vista de tempo de processamento.

Não utilizar esta operação com o intuito de multiplicar/dividir variáveis com ponto xo ou utuante nem variáveis sinalizadas (signed).

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

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

(35)

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

// d e f i n e s para p o r t a s de entrada e saíd a

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

// i n i c i o do programa

void main (void) interrupt 0

{

char mascara ; // v a r i á v e l que guarda a máscara

TRISD = 0x00 ; // c o n f i g u r a a porta D como saíd a

PORTD = 0x00 ; // l i g a todos os l e d s ( l ó g i c a n e g a t i v a ) // l i g a o primeiro b i t da v a r i á v e l

mascara = 1 ; // b i t = 0 b00000001

// rotaciona −se a v a r i á v e l para que o b i t 1 chegue na posição desejada

mascara = mascara << 2 ; // b i t = 0 b00000100 // Ligar o b i t 2 , d e s l i g a n d o o 2o l e d

PORTD = PORTD | mascara ;

//mantém o sistema l i g a d o i nd e f i ni d a me n t e

for( ; ; ) ; }

Desligar um bit (bit clear)

Para desligar apenas um bit o procedimento é similar ao utilizado para ligar. Ao invés de utilizar-mos uma operação OU, utilizareutilizar-mos 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.

// d e f i n e s para p o r t a s de entrada e saíd a

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

// i n i c i o do programa

void main (void) interrupt 0

{

char mascara ; // v a r i á v e l que guarda a máscara

TRISD = 0x00 ; // c o n f i g u r a a porta D como saíd a

PORTD = 0xFF ; // d e s l i g a todos os l e d s ( l ó g i c a n e g a t i v a ) // l i g a o primeiro b i t da v a r i á v e l

mascara = 1 ; // mascara = 0 b00000001

// rotaciona −se a v a r i á v e l para que o b i t 1 chegue na posição desejada

mascara = mascara << 2 ; // mascara = 0 b00000100 // i n v e r t e −se os v a l o r e s de cada b i t

mascara = ~mascara ; // mascara = 0 b11111011

// D e s l i g a o b i t 2 , l i g a n d o o 2o l e d

(36)

//mantém o sistema l i g a d o i nd e f i ni d a me n t e

for( ; ; ) ; }

É 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

// d e f i n e s para p o r t a s de entrada e saíd a

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

// i n i c i o do programa

void main (void) interrupt 0

{

char mascara ; // v a r i á v e l que guarda a mascara

TRISD = 0x00 ; // c o n f i g u r a a porta D como saída

PORTD = 0xF0 ; // d e s l i g a todos os 4 primeiros l e d s ( l ó g i c a n e g a t i v a ) // l i g a o primeiro b i t da v a r i á v e l

mascara = 1 ; // mascara = 0 b00000001

// rotaciona −se a v a r i á v e l para que o b i t 1 chegue na posição desejada

mascara = mascara << 2 ; // mascara = 0 b00000100 // i n v e r t e −se os v a l o r e s de cada b i t da mascara

mascara = ~mascara ; // mascara = 0 b11111011

// Liga o b i t 2 , d e s l i g a n d o o l e d 2

PORTD = PORTD ^ mascara ;

// l i g a o primeiro b i t da v a r i á v e l

mascara = 1 ; // mascara = 0 b00000001

// rotaciona −se a v a r i á v e l para que o b i t 1 chegue na posição desejada

mascara = mascara << 6 ; // mascara = 0 b01000000 // i n v e r t e −se os v a l o r e s de cada b i t da mascara

mascara = ~mascara ; // mascara = 0 b11111011

// D e s l i g a o b i t 6 , l i g a n d o o l e d 6

PORTD = PORTD ^ mascara ;

//mantém o sistema l i g a d o i nd e f i ni d a me n t e

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.

(37)

Vericar o estado de um bit (bit test)

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

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

// d e f i n e s para p o r t a s de entrada e saíd a

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

// i n i c i o do programa

void main (void) interrupt 0

{

char mascara ; // v a r i á v e l que guarda a mascara

char teste ;

TRISD = 0x00 ; // c o n f i g u r a a porta D como saída

teste = 0x00 ; // d e s l i g a todos os b i t s

// rodar d e p o i s o mesmo programa com os b i t s l i g a d o s . // t e s t e = 0 x f f ;

// c r i a uma v a r i á v e l onde APENAS o primeiro b i t é 1

mascara = 1 ; // mascara = 0 b00000001

// rotaciona −se a v a r i á v e l para que o b i t 1 chegue na posição desejada

mascara = mascara << 2 ; // mascara = 0 b00000100 // V e r i f i c a apenas o b i t 2

i f ( teste & mascara ) { PORTD = 0x00 ; // se o r e s u l t a d o f o r v e r d a d e i r o l i g a todos os l e d s } else { PORTD = 0xff ; // se o r e s u l t a d o f o r f a l s o d e s l i g a todos os l e d s } //mantém o sistema l i g a d o i nd e f i ni d a me n t e 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

7

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

A vericação de sistemas embarcados apresenta algumas restrições, 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

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

(38)

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 , b i t ) ( ( arg ) |= (1<< b i t ) )

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 )

(39)

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 )

Com dene #define BitSet ( arg , b i t ) ( ( arg ) ^= (1<< b i t ) )

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 )

(40)

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"

// i n i c i o do programa

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

printf (" Inicializando sistema") ; i f ( 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 ope-raçã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). Além disso temos que congurar a porta D. Isto é feito iniciando a variável TRISD com o valor 0x008.

// d e f i n e s para p o r t a s de entrada e saíd a

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

// i n i c i o do programa

void main (void) interrupt 0

{

// configurando todos os pinos como s a í d a s

TRISD = 0x00 ; PORTD = 0xFF ; // d e s l i g a todos os l e d s // l i g a apenas o b i t 1 . BitClr ( PORTD , 1 ) ; //mantém o sistema l i g a d o i nd e f i ni d a me n t e for( ; ; ) ; }

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.

Referências

Documentos relacionados

Assim, a inserção de novo médico para dar continuidade ao pré-natal, mediante pagamento privado ou planos de saúde, como vem ocorrendo no Estado, pode então ser um

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

•   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