You are on page 1of 96

1

1 Tutorial de Linguagem C Embarcada


Essa apostila tem como intuito auxiliar os estudantes da disciplina de Microcontroladores
no estudo da linguagem C, que será utilizada para o desenvolvimento dos softwares que
deverão ser executados na disciplina.

1.1 Objetivos

• Definir, descrever e identificar tipos de constantes e variáveis, seus usos e objetivos;

• Declarar variáveis e constantes para dados numéricos e strings;

• Usar enumerações para declarar variáveis;

• Atribuir valores para variáveis e constantes por meio do operador de atribuição;

• Avaliar o resultado das operações usadas em C;

• Explicar os resultados de cada declaração de controle do fluxo do programa;

• Criação de funções com variáveis, constantes e declarações de controle para resolver


problemas;

• Usar ponteiros, vetores, estruturas e uniões como variáveis de funções;

• Criar programas usando linguagem C para resolver problemas propostos.

1.2 Introdução

Esse capı́tulo introduz a linguagem de programação C voltada para aplicações usando mi-
crocontroladores embarcados. Também serão vistos extensões da linguagem C que fazem
parte da linguagem CodeVisionAVR
R C. Serão apresentados desde conceitos básicos até

progarmas completos, com exemplos que podem ser implementados no microcontrolador


1.3 Conceitos Iniciais 2
que será utilizado em sala de aula. As informações básicas que todo programador deve
saber são:

• Como declarar variáveis e constantes;

• I/O simples, afim de que os programas possam usar as portas seriais do microcon-
trolador;

• Atribuir valores às variáveis e constantes.

• Realizar operações aritméticas com variáveis e constantes;

• Estruturas de C e declarações de controle para criarem um programa completo em


linguagem C.

Também iremos ver tópicos mais avançados como ponteiros, vetores, estruturas e uniões,
bem como seus usos em programas que utilizam linguagem C. Além disso, também deve-
remos abordar programação em tempo real e interrupções.

1.3 Conceitos Iniciais

Escrever um programa em C é como construir uma casa. Primeiro é feita a fundação,


então são levantadas as colunas, depois são feitas as paredes e, por fim, é construı́do o
telhado, finalizando a obra.
Num programa feito em C embarcado, instruções são reunidas para formarem
funções. Funções são tratadas como operações de alto nı́vel e combinadas para formarem
o programa desejado.
A principal função de um programa em C é a main(). Essa função é a primeira a
ser chamada pelo sistema. Nessa função são executadas todas as operações do programa.
Inclusive as funções criadas são chamadas no main(). Na Figura 1.1 temos um exemplo
simples da função main().
1.3 Conceitos Iniciais 3

Figura 1.1: Hello World

No exemplo o programa irá imprimir a expressão “HELLO WORLD”na saı́da


padrão (neste caso ela se assemelha a uma porta serial). Então o microcontrolador irá
esperar até que algum outro comando seja dado ou até que ele seja resetado. Nesse
momento pode-se perceber uma grande diferença entre um programa de computador e
um programa feito para um microcontrolador embarcado: o microcontrolador possui um
loop infinito, enquanto computadores possuem um sistema operacional (SO) que requisita
um retorno do programa, quando este finaliza a sua execução. Um microcontrolador não
possui nenhum SO e não pode sair do programa em momento algum. Por isso todo
microcontrolador possui um loop infinito em alguma parte do código (no exemplo acima,
while(1) faz o papel desse loop). Dessa forma, é possı́vel evitar que o programa não
execute determinadas ações ou execute atividades indesejadas. O exemplo também nos
mostra a instância da primeira diretiva do compilador do processador: #include. Essa
instância diz ao compilador para incluir um arquivo chamado stdio.h ao programa. A
função printf() faz parte de uma biblioteca externa. Podemos usá-la pois ela faz parte do
arquivo stdio.h. A seguir temos alguns elementos importantes que aparecem no código-
exemplo usado acima:

; O ponto-e-vı́rgula é usado para indicar o fim de


uma expressão.
{} As chaves são usadas para demarcar o inı́cio e o fim
de uma função. Também podem ser usadas para
indicar quando uma série deve ser tratada como
um bloco único.
“algum texto” Aspas duplas são usadas para marcar o inı́cio e o
fim de um texto ou de uma string
// ou /* ... */ Barra-barra ou barra-estrela/estrela-barra delimi-
tam comentários feitos no código
1.3 Conceitos Iniciais 4
Comentários são úteis para proporcionar um melhor entendimento do programa
(tanto para quem irá lê-lo pela primeira vez, quanto para quem já o conhece). Nessa
apostila, os comentários serão usados para explicar a função de cada linha dos códigos-
exemplos. Comentários devem explicar a função de cada linha e não repetir as instruções
usadas na linha que se comenta. Os delimitadores barra-estrela ( /* ) e estrela-barra (
*/ ) são usados para criar blocos de comentários. Quando o compilador encontra uma
barra-estrela ( /* ) ele ignora todo o texto que vier a seguir, até que seja encontrada uma
estrela-barra ( */ ).
Já os delimitadores barra-barra ( \\ ) são usados para se criar paenas uma linha
de comentário. Ao encontrar um barra-barra ( // ), o compilador ignora todo o texto que
vier a seguir, até o final da linha. A seguir veremos uma breve revisão de algumas regras
e terminologias básicas:

• Um identificador é uma variável ou o nome de uma função constituı́do de uma letra


ou underline ( ).

• Identificadores diferenciam letras maiúsculas e minúsculas.

• Identificadores podem ser de qualquer tamanho, mas alguns compiladores identifi-


cam um número limitado de caracteres (os 32 primeiros, por exemplo). CUIDADO!

• Algumas palavras possuem um significado especı́fico para o compilador e, por conta


disso, são consideradas reservadas. Essas palavras reservadas devem ser usadas em
letra minúsculas e nunca podem ser usadas como identificadores. A Figura 1.2 trás
uma lista de algumas dessas palavras.
1.4 Variáveis e Constantes 5

Figura 1.2: Lista de Palavras Reservadas

• Em C, espaços em branco são ignorados (a não ser que estejam entre aspas). Ou
seja, espaços, tabulação e nova linha (criada por meio de um enter e/ou um line
feed) serão ignorados.

1.4 Variáveis e Constantes

Nessa seção será estudado o armazenamento de dados através de constantes e variáveis.


Variáveis, assim como na álgebra, são valores que podem ser alterados. Já as constantes
são valores fixos. As variáveis e as constantes se apresentam de diversas variedades de
forma e tamanho; elas são armazenadas na memória de um programa numa variedade de
formas que serão abordadas no decorrer dessa seção.

1.4.1 Tipos de Variáveis

Uma variável é declarada por uma palavra reservada, que indica seu tipo e tamanho,
seguida por um identificador:

Figura 1.3: Declaração de Variáveis

Variáveis e constantes são armazenadas na memória limitada do microcontrolador


1.4 Variáveis e Constantes 6
e o compilador precisa saber o quanto de memória que deve ser alocada de tal maneira
que não aja desperdı́cio. Consequentemente, um programador deve declarar as variáveis
especificando tanto o tipo quanto o tamanho das mesmas. A Figura 1.4 mostra uma
tabela com os tipos de variáveis e os valores associados a eles.

Figura 1.4: Tipos e Tamanhos de Variáveis

1.4.2 Escopo de Variáveis

Como notado anteriormente, constantes e variáveis precisam ser declaradas antes de serem
utilizadas. O escopo de uma variável, é a acessibilidade da mesma dentro de um programa.
Uma variável pode ser declarada tanto em escopo local quanto global.

Variáveis Locais

Variáveis locais são espaços de memória alocados por uma função quando essa função é
chamada, tipicamente na pilha do programa ou no heap criado pelo compilador. Essas
variáveis não são acessáveis por outras funções, isso significa que seu escopo é limitado às
funções em que elas são declaradas. A declaração local de variáveis pode ser utilizada em
múltiplas funções sem causar conflito uma vez que o compilador enxerga essas variáveis
como pertencentes àquela função somente.
1.4 Variáveis e Constantes 7
Variáveis Globais

Uma variável global ou externa é um espaço de memória alocado pelo compilador e pode
ser acessada por todas as funções em um programa (escopo ilimitado). A variável global
pode ser modificada por qualquer função e irá manter o valor modificado para ser utilizado
por outras funções.
Variáveis globais são normalmente inicializadas (setadas para zero) quando o
método main() é inicializado. Essa operação é normalmente feito por códigos de inicia-
lização gerados pelo compilador, invisı́veis ao programador. A Figura 1.5 representa um
exemplo de declaração de variáveis globais e locais.

Figura 1.5: Declaração Local e Global de Variáveis

Quando variáveis são utilizadas dento de uma função, se a variável local tiver o
mesmo nome de uma variável global, a variável que será utilizada é a local. O valor da
variável global, para esse caso, será inacessı́vel à essa função e permanecerá intocado.

1.4.3 Constantes

Como anteriormente descrito, constantes são valores fixos -elas não podem ser modificadas
no decorrer do programa. Em vários casos, costantes são partes do próprio programa
compilado, localizadas em ROM (read-only memory), ao invés de serem alocadas em
1.4 Variáveis e Constantes 8
memória mutável RAM (random access memory). Na atribuição x = 3 + y, o número 3 é
uma constante e será codificada diretamente pelo compilador para operação de adição. As
constantes podem também aparecer na forma de caracteres e literais, como demonstrado
pela Figura 1.6:

Figura 1.6: Exemplo de constantes literais.

Pode-se ainda declarar uma constante usando a palavra reservada const junto
aos identificadores de tipo e tamanho. Um identificador e valor são necessários para
completar a declaração:

Figura 1.7: Declaração com a palavra reservada const.

Identificar uma variável como uma constante fará com que essa variável seja
armazenada no espaço de memória reservado ao código do programa ao invés do espaço
limitado de armazenamento da memória RAM, o que ajuda a preservar o tamanho dessa
memória.

Contantes Numéricas

Constantes numéricas podem ser declaradas de várias maneiras através da indicação de


sua base numérica, tornando o programa mais legı́vel. Constantes dos tipos int e long int
podem ser representadas das seguintes formas:

• Forma decimal sem prefixo (ex.: 1234)

• Forma binária com prefixo 0b (ex.: 0b101001)

• Forma hexadecimal com prefixo 0x (ex.: 0xff)

• Forma octal com prefixo 0 (ex.: 0777)


1.4 Variáveis e Constantes 9
Ainda há a possibilidade do uso de modificadores para melhor representar o ta-
manho desejado de uma constante:

• Constantes do tipo unsigned int podem ter o sufixo U (ex.: 10000U)

• Constantes do tipo long int podem ter o sufixo L (ex.: 99L)

• Constantes do tipo unsigned long int podem ter o sufixo UL (ex.: 99UL)

• Constantes do tipo float podem ter o sufixo F. (ex.: 1.234F)

• Constantes do tipo char devem ser limitadas por aspas simples. (‘a’ ou ‘A’)

Constantes de Caracter

Constantes de caractere podem ser imprimı́veis (como 0-9, A-Z), ou não-imprimı́veis


(como caracteres de nova linha, tabulação, fim de linha). Constantes de caracteres im-
primı́veis podem ser representadas com utilização de aspas simples (‘a’ ou ‘A’). Um backs-
lash (“\”) seguido de um valor octal ou hexadecimal ambos entre aspas, também pode
representar constantes de caracter:

Figura 1.8: Representação de constantes de caracter.

A Figura 1.9 representa caracteres não imprimı́veis que são reconhecidos pela
linguagem C.

Figura 1.9: Lista de caracteres não imprimı́veis reconhecidos pela linguagem C.


1.4 Variáveis e Constantes 10
Os caracteres backslash (\) e aspas simples (’) devem ser precedidos por outro
backslash em sua representação para evitar confusão por parte do compilador. Por exem-
plo ‘\” é um caracter de aspas simples e ‘\\’ é um caracter de backslash. BEL é o caracter
de sinalização e irá ativar um som quando for recebido pelo terminal de um computador
ou emulador de terminal.

1.4.4 Enumerações e Definições

Legibilidade em programas C é um requisito muito importante. Enumerações e definições


são recursos disponı́veis para que o programador possa substituir números por nomes ou
por alguma frase mais significativa ao contexto do programa. Enumerações são listas de
constantes. A palavra reservada enum é usada para fazer a atribuição de lista de inteiros
à uma lista de identificadores. A Figura 1.10 nos mostra um exemplo disso.

Figura 1.10: Uso de enumeração.

Para o nome zero val é atribuı́do o valor 0, para o one val o valor 1, para o
two val o valor 2 e assim sucessivamente. Um valor inicial pode ser forçado como por
exemplo:

Figura 1.11: Uso de enumeração com valor inicial modificado.

Tal inicialização fará com que start tenha o valor 10, e os nomes subsquentes
(next1, next2, end val), valores incrementais sequenciais (11, 12 e 13 respectivamente).
Enumerações são utilizadas para substituir números puros, tal processo facilita o
entendimento do programador dado que o nome ou a frase atribuı́da ao valor facilitam o
entendimento do uso daquele número.
1.4 Variáveis e Constantes 11
Definições são usadas de maneira similar às enumerações de maneira tal que
permitem a substituição de valores de uma string de texto por outra. Um exemplo dessa
utilização pode ser encontrado na Figura 1.12:

Figura 1.12: Exemplo de uso de definições.

A linha “#def ine leds P ORT A”faz com que o compilador substitua o valor do
rótulo PORTA aonde quer que encontre a palavra leds. Note que a linha “#def ine”não
é terminada por um ponto-e-vı́rgula e não pode ter comentários. A enumeração atribui
os valores de red led on para 1, green led on para 2 e both leds on para 3. Isso pode
ser usado em um programa para controlar os LED’s verde e vermelho de tal forma que
uma saı́da 1 acende o LED vermelho, 2 acende o LED verde e 3 acende os 2 LEDs. A
questão é: “leds = red led on”é muito mais fácil de entender no contexto do programa
que “PORTA = 0x01 ”.
#def ine é uma diretiva pré-processada. Diretivas pré-processadas não fazem
parte da sintaxe da linguagem C, mas são aceitas como tal dado seu uso e familiaridade.
O pré-processamento é um passo separado da verdadeira compilação de um programa,
que acontece antes da verdadeira compilação ser iniciada.

1.4.5 Classes de Armazenamento

Variáveis podem ser declaradas em 3 classes de armazenamento: auto, static e register.


Auto, ou automatic é a classe padrão, o que significa que a palavra auto não é necessária.

Automática

Uma variável local de classe automática é desinicializada quando é alocada, então fica a
cargo do programador se certificar de que ela não contém nenhum dado válido armazenado
antes de utilizá-la. Esse espaço de memória é liberado quando se sai da função em que a
1.4 Variáveis e Constantes 12
variável se encontra, o que significa que os valores serão perdidos e não serão válidos se
a função for reutilizada. A declaração de uma variável de classe automática tem forma
como representado pela Figura 1.13.

Figura 1.13: Declaração de variáveis da classe auto.

Estática

Uma variável local estática possui escopo apenas na função em que é definida (não é
acessı́vel por outras funções), mas é alocada no espaço de memória global. A variável
estática é inicializada para 0 na primeira vez que a função é acessada, mas mantém o valor
quando se sai da função. Isso permite que a variável seja válida e seu valor atualizado
(último valor atribuı́do) toda vez que a função for executada. A Figura 1.14 exemplifica
a declaração desse tipo de variável.

Figura 1.14: Declaração de variáveis da classe static.

Registrador

Uma variável local do tipo registrador é similar à uma variável local automática no sentido
de que também é desinicializada quando alocada e perde o valor ao se sair da função. A
diferença é que o compilador tentará utilizar um registrador fı́sico do microprocessador
como variável com o intuito de reduzir o número de ciclos para acessar a variável. Existem
muito poucos registradores disponı́veis comparado com a memória total de uma máquina
comum. No entanto, essa seria uma classe utilizada com moderação com intuito de acelerar
um processo em particular. A Figura 1.15 exemplifica a declaração desse tipo de variável.

Figura 1.15: Declaração de variáveis da classe register.


1.4 Variáveis e Constantes 13
1.4.6 Conversão de Tipo

No processo de programação, há situações em que o programador possa vir a querer


temporariamente forçar o tipo e tamanho de uma variável. A conversão de tipos permite
que o tipo declarado anteriormente seja substituı́do pelo perı́odo de execução de uma
determinada operação. O cast (conversão), chamado com parênteses, é aplicado ao valor
que ele precede.
Dadas as seguinte declarações e atribuições da Figura 1.16:

Figura 1.16: Declaração e atribuição de variáveis.

as operações de conversão de tipos poderiam aparecer como na Figura 1.17:

Figura 1.17: Conversão de variáveis.

A conversão de tipos é particularmente importante quando operações aritméticas


são realizadas com vaiáveis de diferentes tamanhos. Em muitos casos, a precisão do
procedimento será ditada pela variável de menor tipo. Considere as declarações da Figura
1.18:

Figura 1.18: Outra declaração de variáveis.

Enquanto o compilador processa o lado direito da equação, ele vai olhar para o
tamanho de y e assumir que y * 10 é uma multiplicação de caractere (8-bits). O resultado
1.5 Operações de Entrada e Saı́da 14
alocado na pilha irá exceder o tamanho do espaço de alocação (1 byte ou o valor 255).
Vai ocorrer então o truncamento desse resultado para o valor 118 (0x76) ao invés do valor
correto de 630 (0x276). Na próxima fase de avaliação, o compilador determinaria que o
tamanho da operação é inteiro (16-bits) e 118 seria extendido a um inteiro e então somado
a x. Finalmente seria atribuı́do a z o valor 268, resultado que está completamente errado.
A conversão de tipos deve ser utilizada para controlar esse tipo de situação. A
mesma aritmética escrita na Figura 1.17:

Figura 1.19: Conversão de variáveis.

indicaria ao compilador que y deve ser tratado como um inteiro (16-bits) para
essa operação, essa suposição fará com que o valor 630 seja obtido e colocado na pilha
de execução como resultado de uma multiplicação de 16-bits. O valor inteiro de x será
então adicionado ao valor inteiro da pilha para criar o resultado inteiro 780 (0x30C). z
terá então o valor 780 atribuı́do a ele.
C é uma linguagem muito flexı́vel. Ela vai te dar o suporte que você necessita.
A suposição que o compilador vai fazer é de que você, programador, sabe o que quer. No
exemplo acima se o valor de y fosse 6 ao invés de 63, nenhum erro ocorreria. Ao escrever
expressões, deve-se sempre levar em consideração os valores máximos que poderiam ser
obtidos dessa expressão e que resultados de soma e produto seriam obtidos.
Uma boa regra a ser seguida é: “Na dúvida, coverta”. Sempre converta variáveis
a menos que você tenha absoluta certeza de que não é necessário.

1.5 Operações de Entrada e Saı́da

Microcontroladores embarcados devem interagir diretamente com outros componentes de


hardware. Portanto várias de suas operações de entrada e saı́da são realizadas utilizando
as portas paralelas nativas do microcontrolador.
Muitos compiladores de C proporcionam um método conveniente de trabalhar
com portas paralelas através de bibliotecas (library) ou arquivos de cabeçalho (header )
1.6 Operadores e Expressões 15
que utilizam os comandos de compilação sfrb e sfrw para atribuir rótulos a cada porta
paralela e outros dispositivos de entrada e saı́da. Isso é importante pois o microcontrolador
necessita interagir com outros periféricos ou componentes.
A Figura 1.20 representa exemplo de uso de portas paralelas.

Figura 1.20: Exemplo de Operações de Entrada/Saı́da.

Esse exemplo mostra métodos tanto para ler quanto para escrever dados em uma
porta parelela. z é declarado como uma variável do tipo unsigned char de 8-bits uma vez
que a porta paralela também possui esse valor de armazenamento. Note que os rótulos
de para os pinos da porta A e porta de saı́da B estão em letra maiúscula pelo fato de que
esses rótulos precisam ter o mesmo nome definido no cabeçalho MEGA8535.h.

1.6 Operadores e Expressões

Uma expressão é uma afirmação onde um operador liga identificadores que quando ava-
liados, o resultado poderá ser verdadeiro, falso ou um valor numérico. Operadores são
sı́mbolos que indicam ao compilador qual o tipo de operação deverá ser feita com os iden-
tificadores. Existem regras de prioridade que determinam como os operadores irão atuar.
Quando operadores são combinados em uma única expressão, essas regras de prioridade
devem ser lembradas para que o resultado obtido seja o desejado.
1.6 Operadores e Expressões 16
1.6.1 Atribuição e Operadores Aritméticos

Uma vez que as variáveis tenham sido declaradas, podemos utilizar o operador de igual-
dade ( = ) para promovermos operações nessas variáveis. O valor atribuı́do a uma variável
pode ser uma constante, outra variável ou uma expressão. Uma expressão, em linguagem
C é uma combinação de operandos e operadores. Na Figura 1.21 podemos encontrar um
exemplo simples de aribuição.

Figura 1.21: Exemplo de atribuição e operações.

O programa compilado processa operações aritméticas da mesma maneira que se


estivéssemos fazendo na mão. Começa de dentro dos parênteses e trabalha da esquerda
para a direita: na expressão acima y =(m * x) + b, o m e o x serão multiplicados
primeiro e então adicionados ao b; por fim, será atribuı́do a y o valor resultante. Além
dos parênteses, existe uma ordem hierárquica de prioridade para os próprios operadores.
Multiplicação e divisão são feitas primeiro, seguidas da adição e da subtração. Portanto,
a afirmação y = m * x + b será avaliada ma mesma maneira que a expressão y =(m * x)
+ b.
Logo, a ordem de precedência de operadores aritméticos é:

Multiplicação *
Divisão /
Módulo %
Adição +
Subtração -

Existem outros tipos de operadores além dos aritméticos e de atribuição. Isso


inclui operadores de bit a bit, lógicos, relações, incrementos, decrementos, atribuições
compostas e condicionais.
1.6 Operadores e Expressões 17
Operadores de Bit

Operações Bit a Bit operam funções que irão afetar o operando em nı́vel binário. Fun-
cionam apenas para variáveis dos tipos char, int e long. A ordem de precedência dos
operadores está a seguir:

Complemento de Um ˜
Shift para esquerda <<
Shift para direita >>
AND (E) &
XOR (XOU) ˆ
OR (OU) —

A seguir temos as descrições de cada operação Bit a Bit:

• Ones Complemet (Complemento a um): converte o bit em que se está operando em


1 (um) se ele for 0 (zero), e em 0 (zero) se o bit for 1 (um).

• Left Shift (Deslocamento a esquerda): irá deslocar para a esquerda os bits da es-
querda do operando, na forma binária, o número de vezes especificado pelo operando
da direita. Num deslocamento a esquerda, 0 (zero) sempre é deslocado para repor
as posições binárias inferiores que ficarem vazias. Cada deslocamento a esquerda,
na prática, multiplica o operando por 2 (dois).

• Right Shift (Deslocamento a direita): irá deslocar para a direita os bits da esquerda
do operando, na forma binária, o número de vezes especificado pelo operando da
direita. Cada deslocamento a direita, na prática, provoca uma divisão por 2 (dois)
no operando. Quando um deslocamento a direita é realizado, variáveis com sinal e
sem sinal são tratadas de maneiras diferentes. O bit do sinal (da esquerda ou o mais
significante) num inteiro sinalado será replicado. Essa extensão do sinal permite
que um número positivo ou negativo seja deslocado para a direita, mantendo o seu
sinal. Quando uma variável sem sinal é deslocada para a direita, 0 (zeros) serão
sempre deslocados para a esquerda.

• AND (E): esse operador irá fornecer 1 (um) para cada posição binária em que os
dois operando sejam 1 (um).
1.6 Operadores e Expressões 18
• Exclusive OR (OU exclusivo): esse operador irá fornecer 1 (um) para cada posição
binária em que os operandos difiram (0 em um operando e 1 em outro).

• OR (OU): esse operador irá fornecer 1 (um) em cada posição onde qualquer um dos
operando seja 1(um).

Assumindo um caractere sem sinal y = 0xC9, a tabela abaixo exemplifica algumas


operações com bits:

Operation Result
x = ˜y; x = 0x36
x = y << 3; x = 0x48
x = y >> 4; x = 0x0C
x = y & 0x3F; x = 0x09
x = yˆ1; x = 0xC8
x = y | 0x10; x = 0xD9

As operações AND e OR são úteis quando se trabalha com portas paralelas. Um


exemplo pode ser encontrado na Figura 1.22.

Figura 1.22: Exemplo de uso de AND e OR.

O exemplo acima demonstra as técnicas de mascarar e controle por bit. Mascarar


(fazer uma máscara) é uma técnica usada para se determinar o valor de um certo número
de bits de um valor binário. No exemplo, a máscara é realizada aplicando-se, nos bits
não desejados, o operador AND com 0 (‘x’ AND 0 sempre dará 0). Nos bits desejados,
1.6 Operadores e Expressões 19
é aplicada o operador AND com 1 (‘x’ AND 1 sempre dará ‘x’). Dessa forma, todos os
bits do nibble, excetuando-se os dois centrais, são eliminados. Controlar portas bit a bit
é um método de mudar um ou mais bits de uma porta paralela, sem afetar os outros bits
da porta. O primeiro PORTB demonstra o uso do operador OR para colocar em alto (1)
dois bits da porta, sem afetar os outros bits da porta. O secundo PORTB mostra o uso
do operador AND para colocar em baixo (0) um bit da porta, usando no bit desejado o
operador AND com 0 (‘x’ AND 0).

1.6.2 Operadores Lógicos e Relacionais

Operadores lógicos e relacionais são sempre operadores binários, mas produzem um resul-
tado VERDADEIRO (TRUE) ou FALSO (FALSE). TRUE é representado por um valor
diferente de zero e FALSE por um valor igual a zero. Essas operações são normalmente
usadas para controle, a fim de guiar a execução do programa.

Operadores Lógicos

A Tabela abaixo mostra os operadores lógicos, em ordem de prioridade.

E (AND) &&
OU (OR) ||

Esses operadores diferem muito dos operadores bit a bit, pois eles tratam com
operandos no sentido de VERDADEIRO (TRUE) ou FALSO (FALSE). O operador lógico
AND gera TRUE se os dois operandos forem TRUE, de outra maneira, o resultado será
FALSE. O operador lógico OR, gera TRUE se um dos operandos forem TRUE. No caso
do operador OR, ambos os operandos devem ser FALSE para o resultado ser FALSE. Na
Figura 1.23 temos um exemplo dessa diferenciação.
1.6 Operadores e Expressões 20

Figura 1.23: Exemplo de uso de AND e OR.

Operadores Relacionais

Operadores relacionais promovem operações de comparação. Assim como nos operadores


lógicos, os operandos são avaliados da esquerda para a direita e um resultado TRUE ou
FALSE é gerado. Eles efetivamente ?perguntam? sobre a relação entre as duas expressões
a fim de receberem uma resposta TRUE ou FALSE. A tabela a seguir mostra os operadores
relacionais:

Significado Sı́mbolo
É Igual a ==
Não É Igual a !=
Menor que <
Menor ou Igual a <=
Maior ou igual a >=
Maior que >

Assumindo x=3 e y=5, a Figura 1.24 trás alguns exemplos de operações relacio-
nais e seus resultados.

Figura 1.24: Exemplo de uso Operadores Relacionais.


1.6 Operadores e Expressões 21
1.6.3 Operadores de Incremento, Decremento e Atribuição Com-

posta

Quando a linguagem C foi desenvolvida, foi feito um grande esforço para mantê-la sim-
plificada, mas clara. Alguns “operadores-atalhos”foram criados na linguagem , a fim de
simplificar a geração de sentenças e reduzir a digitação durante o desenvolvimento de pro-
gramas. Essas operações incluem o incremento e o decremento, assim como operadores
para atribuição composta.

Operadores de Incremento

Operadores de Incremento permitem a modificação de um identificador, no local, de ma-


neira pré-incremental ou pós-incremental. Exemplo na Figura 1.25.

Figura 1.25: Operador de incremento.

Neste exemplo, o valor é incrementado em 1. ++x é uma operação pré-incremental,


enquanto x++ é uma operação pós-incremental. Isso significa que durante a avaliação da
expressão do código compilado, o valor será mudado antes ou depois da avaliação. Exem-
plo na Figura 1.26.

Figura 1.26: Exemplo de uso de operador de incremento.

No primeiro caso, i é incrementado após a expressão ser resolvida. No secundo


caso, i é incrementado antes da expressão ser resolvida.
1.6 Operadores e Expressões 22
Operadores de Decremento

Operadores de decremento funcionam de maneira similar aos de incremento. Os operado-


res de decremento subtraem uma unidade, de maneira pré-decremental ou pós-decremental.
Exemplo na Figura 1.27.

Figura 1.27: Operador de decremento.

Operadores de Atribuição Composta

Operadores de atribuição composta são outro método para reduzir a quantidade de sintaxe
necessária durante a construção do programa. Uma atribuição composta é apenas uma
combinação de um operador de atribuição (=) com um operador aritmético ou lógico.
A expressão é processada da direita para a esquerda e, sintaticamente, é construı́da de
maneira parecida com os operadores de incremento e decremento. Na Figura 1.28 temos
um exemplo de utilização desse tipo de atribuição.

Figura 1.28: Atribuição Composta.

Essa combinação de um operador de atribuição e outro operador funciona também


com operadores de módulo e bit a bit (%, >>, <<, &, |, ˆ) assim como com operadores
aritméticos (+, -, *, /), como mostrado na Figura 1.29.
1.6 Operadores e Expressões 23

Figura 1.29: Outro exemplo de Atribuição Composta.

1.6.4 A Expressão Condicional

A expressão condicional é, provavelmente, um dos mais crı́ticos e infrequentemente uso


de operadores. Ela, definitivamente, foi usada para poupar tempo de digitação, mas em
geral não é fácil de ser usada por programadores iniciantes. A expressão condicional é
vista aqui na seção “Operadores e Expressões”devido à sua construção fı́sica, mas ela está
mais associada com estruturas de controle. Considerando a sequência da Figura 1.30:

Figura 1.30: Uso da expressão condicional If/Else.

Também podemos escrevê-la de maneira representada pela Figura 1.31. Se a


expressão A estiver certa, fará a expressão B; senão, fará a expressão C.

Figura 1.31: Alternativa de uso de expressão condicional.

1.6.5 Prioridade de Operadores

Quando múltiplas expressões são usadas em uma mesma sentença, o operador de priori-
dade determina em que ordem cada expressão será avaliada pelo compilador. Em todos
os casos de atribuições e expressões, a prioridade, ou a ordem de prioridade, deve ser
lembrada. Quando estiver em dúvida, o ideal é colocar as expressões entre parênteses -
1.6 Operadores e Expressões 24
para garantir a ordem de procedimento - ou olhar a prioridade da expressão em questão.
Alguns dos operadores listados na tabela abaixo compartilham do mesmo nı́vel de priori-
dade. A tabela a seguir mostra alguns operadores, sua prioridade e a ordem em que são
tratados (direita para esquerda ou esquerda pra direita). A Figura 1.32 nos da a ordem
de prioridade:

Figura 1.32: Lista de prioridade de operadores.

A Figura 1.33 nos mostra um exemplo de uso de prioridade entre operandos.

Figura 1.33: Exemplo de prioridade de operadores.

Devido à prioridade dos operadores, caso tenha alguma dúvida, use quantos
parênteses forem necessários para assegurar que a matemática será feita na ordem de-
sejada. Parênteses não aumentam o tamanho do código e facilitam a leitura e melhoram
1.7 Estruturas de Controle 25
a aparência do mesmo. Além de garantir que a missão seu cumprida de maneira satis-
fatória.

1.7 Estruturas de Controle

As estruturas de controle são usadas para o controle do fluxo de execução do programa.


Estas estruturas permitem ao programador alterar a sequencia de execução do programa.
As instruções if/else são utilizadas para orientar ou ramificar a operação em uma de duas
direções. As instruções while, do/while e for são utilizadas para controlar a repetição de
uma instrução. Os comandos switch/case são utilizado para permitir que uma decisão
direcione o programa para um dos muitos blocos de instrução de forma clara e concisa.

1.7.1 While

O loop while é um dos mais básicos elementos de controle e o formato dessa estrutura é
a seguinte:

Figura 1.34: Estrutura While.

Quando o programa é executado a estrutura while testa uma condição e se o


resultado for verdadeiro (diferente de 0) então as estruturas dentro do loop while são
executadas. O loop associado para as instruções do while são as linhas de código dentro
das chaves que seguem o comando while, ou no caso de uma única instrução ela fica depois
do comando while. Quando a execução da estrutura chega ao seu final, o programa
retorna ao inicio das instruções do loop while onde a expressão é testada novamente.
Sempre que a expressão é VERDADEIRA o loop é executado novamente, mas para o
caso da expressão ser FALSA o loop é ignorado e as instruções após o loop passam a ser
1.7 Estruturas de Controle 26
executadas. Considere o exemplo a seguir:

Figura 1.35: Exemplo While.

Neste exemplo c é inicializado em 0 e a mensagem “Start of program”é mostrada.


O loop será executado e o valor de c será mostrado a cada vez que c for incrementado.
Quando o valor de c é 100 então o loop while é finalizado. A mensagem “End of pro-
gram”mostrada e o programa fica para sempre na instrução while(1). A instrução while(1)
mostra que uma vez que “1”é a expressão a ser avaliada e 1 é constante, a expressão é
considerada sempre verdadeira e ocorre um loop infinito. O loop while também pode ser
usado quando se espera que um evento ocorra em uma porta paralela.

Figura 1.36: Exemplo While porta paralela.

Neste exemplo o while é usado para esperar enquanto o bit vai para baixo. A
expressão analisada “PINA&0x02”vai monitorar o segundo bit da porta A e enquanto
este bit estiver em estado lógico 1 a expressão vai ser verdadeira e então o programa irá
rodar novamente no loop até que o estado da porta vá para o estado baixo.
1.7 Estruturas de Controle 27
1.7.2 Do While

O comando do/while é bem parecido com o while tirando o fato de que a expressão é
testada depois de o loop ter sido executado uma vez. As instruções loop do/while são
sempre executadas uma vez antes do teste feito para determinar se permanece-se ou não
no loop.

Figura 1.37: Estrutura Do While.

Quando a execução chega ao final da construção do do/while constructo, a ex-


pressão é avaliada. Se o resultado da expressão for verdadeiro (diferente de zero) então
o programa retorna para o inicio do loop. Enquanto a expressão é verdadeira o loop vai
sendo executado, mas quando a expressão for falsa o programa continua com as instruções
que seguem na construção.
Veja um exemplo da construção do/while:

Figura 1.38: Exemplo Do While.


1.7 Estruturas de Controle 28
Neste exemplo c é inicializado em zero e o texto ?Start of program ? é mostrado
e então o loop do/while é executado e o valor de c é mostrado cada vez que ele é incre-
mentado de 0 a 100. Quando o valor de c chega a 100 o loop é ignorado e a frase ?End of
program? é exibida e o programa fica para sempre na instrução while(1).

1.7.3 For

O loop for é tipicamente utilizado para executar uma instrução ou um bloco de instruções
um numero especı́fico de vezes. Este loop pode ser descrito como uma inicialização, teste
ou uma ação que conduz a satisfação do teste. O formato do loop for pode ser descrito
da seguinte forma:

Figura 1.39: Estrutura For.

expr1 será executada apenas uma vez na entrada do for. expr1 é, normalmente,
uma sentença que pode ser usada para inicializar as condições para expr2. expr2 é uma
condição de controle usada para determinar até quando o loop vai durar. expr3 é uma
outra sentença que pode ser usada para satisfazer as condições da expr2. Quando a
execução do programa entra no inicio do for, a expr1 é executada. expr2 é avaliada
e se o resultado da expr2 é TRUE (diferente de 0), então as sentenças dentro do for
são executadas ? o programa continua no loop. Quando a execução chega ao final da
construção do loop, expr3 é executada e o programa retorna ao inı́cio do for, onde a expr2
é verificada novamente. Enquanto a expr2 for TRUE, o loop é executado. Quando a
expr2 foi FALSE (falsa), o loop é encerrado completamente. A estrutura do for pode ser
representada com um while no seu modelo:
1.7 Estruturas de Controle 29

Figura 1.40: Estrutura While como For.

A seguir, temos um exemplo do uso do for:

Figura 1.41: Exemplo For.

Neste exemplo, a mensagem “Star the program”é impressa. c é inicializada com


0, de acordo com a construção do for. O loop é então executado, imprimindo o valor
de c, em cada passagem, enquanto c é incrementado de 0 a 100 (também de acordo
com a construção do for). Quando c alcança o valor de 100, o loop é encerrado. A
mensagem “End of program”é, então, impressa e o programa espera para sempre na
sentença while(1).

1.7.4 If/Else

Declarações If/Else são usadas para orientar ou separar a operação do programa, baseado
na avaliação de uma expressão condicional.
1.7 Estruturas de Controle 30
Operadores de atribuição composta

Uma declaração If possui a seguinte forma:

Figura 1.42: Estrutura If.

Se a expressão resultante é TRUE (diferente de zero), então a sentença ou bloco


de sentenças é executado. De outra forma, se o resultado é FALSE, então a sentença ou
bloco de sentenças é pulado.

If/Else

Uma declaração do tipo If/Else possui a seguinte forma:

Figura 1.43: Estrutura If Else.

A sentença ou bloco de sentenças associados ao Else somente serão executadas se


a resposta ao If acabar sendo FALSE. Caso a resposta seja TRUE, a sentença ou bloco de
sentenças serão pulados. Uma declaração Else sempre deve der precedida por um If. Um
técnica comum de programação é cascatear declarações If/Else para criar uma árvore de
seleção:
1.7 Estruturas de Controle 31

Figura 1.44: Estrutura If Else - If Else.

Esta sequencia de If/Else fará o programa selecionar e executar apenas uma das
declarações. Se a primeira expressão, expr1, é TRUE, então statement1 será executado
e as outras declarações (os outros else if) serão deixados de lado. Se a expr1 é FALSE,
então a próxima declaração, else if (expr2), será executada. Se expr2 seja TRUE, então
statement2 será executado e as outras declarações (os outros else if) serão deixados de lado.
E assim sucessivamente. Se expr1, expr2 e expr3 forem todas FALSE, então statement4
será executado.
A seguir temos um exemplo de uma operação If/Else:

Figura 1.45: Exemplo If Else.

Neste exemplo, a mensagem “Start of Program”é impressa. A variável c é inicia-


lizada com 0, de acordo com a construção do for. O loop de for é executado, imprimindo
1.7 Estruturas de Controle 32
o valor de c em cada passagem, assim como incrementando c de 0 a 100 (também de
acordo com a construção do for). Se o valor de c é menor que 33, então a mensagem
“0 < c < 33”é impressa. Se o valor de c está entre 32 e 65, então a mensagem “33 < c
< 65”é impressa. Se o valor de c se enquadra em nenhum dos casos anteriores, então a
mensagem “66 < c < 100”é impressa. Quando c atinge o valor de 100, o for é encerrado.
A mensagem “End of program”é impressa e o programa fica esperando no while(1).
Usando as construções e técnicas mostradas até aqui, é possı́vel criar um programa
que testa, eficientemente, cada bit de uma porta entrada e imprime uma mensagem para
dizer o estado do bit.

Figura 1.46: Exemplo Técnicas.

O exemplo acima faz uso de um for que é setado para ser percorrer as sentenças
8 vezes, uma para cada bit a ser testado. A variável bit mask começa valendo 1 e é
usada para mascarar todos os bits, exceto o bit que será testado. Após ser usada como
máscara, ela é deslocada 1 bit para a esquerda, usando a notação de atribuição, afim de
testar o próximo bit na próxima passagem pelo for. Durante a execução de cada laço,
a construção if/else é usada para imprimir a informação sobre o estado de cada bit. A
declaração condicional na construção do if é um uso do AND para comparação bit a bit
1.7 Estruturas de Controle 33
com a variável bit mask para mascarar os bits indesejados durante o teste.

Expressão Condicional

Outra versão do if/else é a expressão condicional, desenvolvida para poupar tempo do


programador e simplificar a sintaxe. A sequência if/else:

Figura 1.47: If condicional.

pode ser reduzida para uma expressão condicional que ficaria da seguinte forma:

Figura 1.48: Expressão Condicional.

Nos dois casos mostrados acima, a expressão lógica expression A é avaliada e,


se o resultado for TRUE, então statement1 é executado. De outra maneira, statement2
é executado. Em um programa, uma expressão condicional pode ser escrita da seguinte
maneira:

Figura 1.49: Exemplo Condicional.

1.7.5 Switch/Case

A declaração switch/case é usada para executar outras declarações, ou grupos de de-


clarações, selecionando através do valor de uma expressão. A forma básica dessa de-
claração é:
1.7 Estruturas de Controle 34

Figura 1.50: Estrutura Switch Case.

A sentença expression é avaliada e seu valor é comparado com as constantes


(const1, const2, ... , constx). A execução começa no inı́cio da declaração após a constante
que é igual ao valor de expression. As constantes devem ter valores inteiros ou caracteres.
Todas as declarações após essa constante serão executadas, até o final da construção do
switch. Como isso não é, normalmente, uma operação desejada, o comando break pode
ser usado no final de cada bloco de declarações a fim de parar a execução (evitando que
declarações indesejadas sejam executadas) e permitir ao programa continuar a execução
após o switch. O caso default é opcional, mas ele permite a execução de declarações que
devem ser executadas quando nenhum valor corresponde ao encontrado em expression.
Abaixo temos um programa que exemplifica o que foi falado acima:
1.7 Estruturas de Controle 35

Figura 1.51: Exemplo Switch Case.

Este programa lê o valor em uma porta A e mascara o 4 bits mais significativos.
Ele compara o valor dos nibble menos significativo da porta A com as constantes nas
declarações case. Se algum caractere é 0, 1, 2 ou 3, o texto “c é um número menos que
4”(“c is a number less than 4”) será impresso na saı́da padrão. Se o caractere é um 5, o
texto “c é um 5”(“c is a 5”) será impresso na saı́da padrão. Se o caractere não é nenhum
dos anteriores (um 4 ou algum número maior que 5), a declaração default será executada
e a mensagem “c é 4 ou > 5”(“c is 4 or > 5”) será impressa. Assim que o caso apropriado
for executado, o programa irá retornar ao inı́cio do while e irá repetir.

1.7.6 Break, Continue e Goto

As declarações break, continue e goto são usadas para modificar a execução das declarações
for, do/while e switch.
1.7 Estruturas de Controle 36
Break

A declaração break é usada para sair de um laço (break, continue e goto). Se as declarações
estão aninhadas uma dentro da outra, o comando break ira sair apenas do bloco de
declarações atual.
O programa abaixo irá imprimir o valor da variável c na saı́da padrão além de
continuar incrementando de 0 a 100, e então reinicializará para 0. No laço interno, c é
incrementado até chegar em 100, então o comando break é executado. O break faz o
programa sair do while interno e continuar a execução do while externo No laço externo,
c é setado como 0, e uma mensagem de controle é enviada para o while interno. Esse
procedimento é repetido infinitamente:

Figura 1.52: Exemplo Break.

Continue

A declaração continue permitirá ao programa iniciar a próxima iteração de um laço (break,


continue e goto). continue é semelhante a um break, pois ambos param a execução de
um laço em algum ponto. A diferença entre eles consiste no fato do comando continue
começar o laço outra vez, desde o inicio, enquanto que o break sai do laço por completo.
1.7 Estruturas de Controle 37

Figura 1.53: Exemplo Continue.

Neste exemplo, o valor de c será mostrado até que ele chegue em 100. O programa
irá parecer parado nesse ponto, mas ele estará em funcionamento. O que aconteceu foi
que o programa pulou o incremento e a declaração printf().

1.7.7 Goto

A declaração goto é usada para, realmente, pular a execução para um trecho determinado.
Essa declaração é pouco estruturada e insuportável para os puritanos, mas em um sistema
embarcado ela pode ser uma boa maneira de reduzir o tamanho do código e o uso de
memória. A marca, para onde a execução deve seguir, pode estar antes ou depois da
declaração do goto em uma função. A seguir temos as formas de declarar o goto:

Figura 1.54: Estrutura Go To.

A marca, para onde a execução deve seguir, pode ser se um nome válido em C
ou um identificador, seguido de dois-pontos (:), como mostrado a seguir:
1.8 Funções 38

Figura 1.55: Exemplo Go To.

No exemplo cima, c e d são inicializadas de maneiras diferentes. A declaração


if(d==c) verifica os valores, enquanto eles não forem iguais, a execução passa para a
declaração que incrementa o c. O valor de c é impresso e o laço while recomeça. Os
valores de c e d continuam diferindo em uma unidade até que c seja maior que 100. Com
c valendo 101, o comando continue fará com que a declaração printf() e o incremento
de c não sejam executados. O if(d==c) passará a ser TRUE, pois agora d e c possuem
o mesmo valor, e o comando goto fará o programa pular para a marca start again. O
resultado desse programa será a impressão dos valores de c, de 0 a 100, várias vezes.

1.8 Funções

Uma função é o encapsulamento de um bloco de expressões que pode ser usado mais de
uma vez em um programa. Algumas linguagens de programação se referem às funções
como sub-rotinas ou procedimentos. Funções são muito importantes em qualquer pro-
grama (independente da linguagem de programação utilizada). Elas são escritas, em
geral, para quebra um procedimento de um programa em partes menores e mais fáceis de
1.8 Funções 39
serem analisadas. Dessa maneira, o programador pode analisar cada elemento (função)
e utilizá-lo mais de uma vez. Uma das maiores vantagens do uso de funções está na
possibilidade da criação de bibliotecas. Funções criadas e que desempenhem determina-
das tarefas podem ser salvas e utilizadas por diferentes aplicações ou mesmo por outro
programador. Isso poupa tempo e ajuda a manter a estabilidade das funções, pois elas
acabam por serem testadas e retestadas. Uma função pode executar uma tarefa isolada,
sem requisitar qualquer parâmetro. Funções podem aceitar parâmetros, afim de que sejam
guiadas para realizarem uma tarefa determinada. Uma função pode não apenas receber
parâmetros, mas também retornar um valor. Embora as funções possam receber mais
de um parâmetro, elas podem retornar apenas um valor. Na Figura 1.56 temos alguns
exemplos de funções:

Figura 1.56: Exemplo de Funções

A forma padrão de uma função é descrita na Figura 1.57

Figura 1.57: Forma Padrão de uma função

Uma função ou seus parâmetros podem ser de qualquer tipo (ex.: int, char, float).
O tipo pode, inclusive, ser vazio ou void. O tipo void é usado para indicar um parâmetro
ou um valor de retorno com tamanho zero. A declaração tı́pica de um função pode ser
vista na 1.58
1.8 Funções 40

Figura 1.58: Declaração de uma função

Neste exemplo, getchar() é uma função que não requer parâmetros e retorna um
unsigned char ao completar sua execução. A função getchar() é uma das muitas funções
de bibliotecas existentes no compilador da linguagem C. Esta função está disponı́vel para
o uso do programar e será discutida no futuro.

1.8.1 Protótipos e Organização de Funções

Assim como no caso de variáveis e constantes, o tipo da função e de seus parâmetros deve
ser declarado antes de serem chamados. Isto pode ser realizado de duas maneiras: uma é
a ordem da declaração das funções, a outra é o uso de protótipos de funções. Ordenar as
funções é sempre uma boa ideia. Isso permite ao compilador obter todas as informações
sobre a função antes do seu uso. Dessa maneira, todos os programas terão o formato de
acordo com a Figura 1.59

Figura 1.59: Organização de programa e funções

Isso é muito bom e ordenado, porém nem sempre é possı́vel de ser implementado.
1.8 Funções 41
Em muitas ocasiões as funções usam outras para realizar suas tarefas, o que torna im-
possı́vel declará-las de forma que elas fiquem na ordem top-down. Protótipos de funções
são usados para permitir ao compilador saber o tipo da função e o que esta requer antes
que ela seja declarada. Isso reduz a necessidade de uma arquitetura top-down. O exemplo
anterior poderia ser organizado de maneira representada pela Figura 1.60

Figura 1.60: Exemplo Alternativo sem Protótipos

Da forma como foi feito, o compilador não terá informações suficientes sobre as
funções que foram chamadas no main(), dessa forma o código não será gerado correta-
mente o que retornará uma mensagem de erro por parte do compilador. Para evitar essa
mensagem de erro, deve-se adicionar os protótipos das funções no topo do código, como
na Figura 1.61
1.8 Funções 42

Figura 1.61: Exemplo com Protótipos

Dessa maneira, o compilador possui todas as informações básicas necessárias so-


bre cada função. O compilador da linguagem C possui muitas bibliotecas com funções
prontas. Podemos torná-las disponı́veis para o programa através da declaração #include
<filename.h>, onde filename.h é o arquivo que contém os protótipos das funções da bi-
blioteca de funções. Nos exemplos anteriores, usamos a função printf.h. Essa função é
declarado em qualquer lugar, porém seu protótipo está no arquivo de cabeçalho stdio.h
(declarado no inı́cio do código através do comando #include <stdio.h>). Isso permite
que o programador simplesmente inclua um arquivo de cabeçalho no inı́cio do programa
e comece a escrever o código.

1.8.2 Funções que Retornam Valores

Em vários casos, uma função é desenvolvida para realizar uma tarefa e retornar um calor
ou status para a tarefa realizada. A palavra de controle return é usada para indicar um
ponto de saı́da na função ou, no caso de uma função cujo tipo é diferente de void, para
selecionar um valor que deve ser retornado para quem chamou a função.
OBS.: Em uma função do tipo void, representada pela Figura 1.62
1.8 Funções 43

Figura 1.62: Função void

O comando return está implicito no final da função. Caso return seja inserido,
numa função do tipo void, como na Figura 1.63

Figura 1.63: return em uma Função void

O return que foi inserido faz com que a execução do programa retorno para o
ponto onde a função foi chamada e todas as tarefas após o return não serão executadas.
Em uma função cujo tipo não é void, o comando return irá fazer com que a execução
do programa também retorne para o ponto em que a função foi chamada. Além disso, o
return irá colocar o valor da expressão, que está à direita do return, na pilha de execução
no formato do tipo especificado na declaração da função que contém o return. Na Figura
1.64 temos uma função do tipo float. Logo, um valor do tipo float será colocado na pilha
de execuções quando o comando return for executado:

Figura 1.64: return em uma Função float

A capacidade de retornar um valor permite que uma função seja usada como
parte de uma expressão, semelhante a uma atribuição. Como exemplo, temos o programa
da Figura 1.65
1.8 Funções 44

Figura 1.65: Exemplo de uso de uma Função com return

No programa acima, será atribuı́do à variável a o valor retornado pela função


cube(b). O resultado que deverá aparecer será correspondente à Figura 1.66

Figura 1.66: Resultado do uso de cube

1.8.3 Recursão

Uma função recursiva é aquela que chama a si mesma. A capacidade de gerar códigos
recursivos é um dos mais poderosos aspectos da linguagem C. Quando uma função é
chamada, suas variáveis locais são colocadas na pilha de execução, juntamente com o
endereço de quem a chamou. Dessa forma, a função saberá como voltar ao fluxo de
execução. Cada vez que a função é chamada, essas alocações são realizadas. Isso faz com
que a função seja reentrante (ela pode ser interrompida durante sua execução e, então,
chamada outra vez). A caracterı́stica reentrante da função é garantida pois os valores
da chamada anterior foram deixados juntamente com as alocações anteriores. O maior
exemplo de uma operação recursiva é o cálculo de fatoriais. O fatorial de um número
é produto de todos os inteiros positivos menores ou iguais a este número. Um exemplo
pode ser encontrado na Figura 1.67.
1.8 Funções 45

Figura 1.67: Exemplo de Fatorial

Na Figura 1.68 temos o procedimeno algébrico.

Figura 1.68: Cálculo de Fatorial Algébrico

Na Figura 1.69 temos um programa para demonstrar essa operação:

Figura 1.69: Exemplo de Programa de Cálculo de Fatorial

Quando a função fact() é chamada com o argumento n, ela chama a si mesma


n-1 vezes. Cada vez que ela se chama, n é reduzido em 1. Quando n PE igual a 0 (zero),
a função retorna 1 (um) ao invés de se chamar outra vez. Isso provoca uma “reação em
cadeia”ou um “efeito dominó”de retornos, levando de volta à chamada feita no main(),
onde a função será impressa. Da mesma maneira que a recursão é poderosa para realizar
fatoriais, classificações e buscas em listas encadeadas, ela também é uma operação com
alto consumo de memória. Cada vez que a função chama a si mesma, ela aloca memória
1.9 Ponteiros e Vetores 46
para as variáveis locais da função, para o valor de return, para o endereço de retorno
da função e para qualquer parâmetro que foi passado durante a chamada. No exemplo
anterior, isso iria incluir os gastos representados pela Figura 1.70.

Figura 1.70: Exemplo de Gasto de Memória em Recursão

No caso do “5!”, pelo menos 30 bytes de memória seria alocados durante a


operação de fatorial e a desalocados após o retorno. Isso torna as operações recursivas
muito perigosas e impraticáveis para microcontroladores pequenos. Se a profundidade da
alocação devido à recursão fosse maior, a pilha de execução poderia estourar (overflow) e
a operação do programa ficaria imprevisı́vel.

1.9 Ponteiros e Vetores

Ponteiros e vetores são estruturas amplamente utilizadas em C uma vez que permitem
ao programador a realização de operações mais eficientes e generalizadas. Operações que
necessitam de um agrupamento de um conjunto de dados podem usar um desses métodos
para facilmente acessar e manipular os dados sem ter que movimentá-los na memória.
Essas estruturas ainda permitem o agrupamento de vaiáveis associadas como buffers e de
comunicação e cadeia de caracteres (strings).

1.9.1 Ponteiros

Ponteiros são variáveis que contém o endereço ou localização de uma variável, constante,
função ou objeto. Uma variável é declarada como ponteiro a partir da inserção do operador
de desreferenciamento “*”. Exemplo: int *p // ponteiro para um inteiro.
O tipo ponteiro aloca um espaço na memória de tamanho suficiente para armaze-
nar o endereço de uma variável. Por exemplo, o endereço de memória em um microcontro-
lador normalmente será descrito em 16 bits. Então, em um microcontrolador comum, um
1.9 Ponteiros e Vetores 47
ponteio para um caracter terá tamanho 16, mesmo que a variável char possa armazenar
8-bits somente.
Uma vez que um ponteiro seja declarado, o programador estará lidando com o
endereço que a variável ocupa na memória e não com o valor nela armazenado. Deve-se
pensar em termos de localidade e conteúdo de uma localidade. O operador de endereço
“&”é utilizado para se ter acesso ao endereço de uma variável. Esse endereço pode ser
atribuı́do ao ponteiro e é correspondente ao valor armazenado por esse ponteiro. Na
Figura 1.71 temos um exemplo desse tipo de atribuição de ponteiros.

Figura 1.71: Exemplo de Ponteiros

Nesse exemplo, o endereço de a é atribuı́do ao ponteiro p, então “ p aponta para


a”.
Para acessar o valor da variável que é apontada por p (variável a no caso) utiliza-se
o operador de desreferenciamento “*”. Quando executado, o operador de desreferenci-
amento faz com que o valor de p, que é um endereço, seja utilizado para acessar uma
posição na memória. O valor nessa posição é então lido ou escrito de acordo com a ex-
pressão que contém esse operador. Por exemplo, no código b = *p;, *p faria com que
o valor localizado no endereço armazenado por p fosse lido e atribuı́do a b. Portanto,
ao analisar esta atribuição e o exemplo na Figura 1.71, podemos constatar que b = *p;
é equivalente a b = a;. O operador “*”pode também aparecer do lado direito de uma
atribuição, no exemplo da Figura 1.72, a localização na memória do endereço armazenado
em p é atribuı́do ao valor b, o que iria produzir mesmo resultado que a = b;.
1.9 Ponteiros e Vetores 48

Figura 1.72: Exemplo de Ponteiros

Toda vez que se lê esse tipo de atribuição, tente ler como “b é atribuı́do com o
valor apontado por p”e “p é atribuı́do com o endereço de a”. Isso ajuda a evitar os erros
mais comuns de ponteiro expressos na Figura 1.73.

Figura 1.73: Exemplo de erro no uso de Ponteiros

Essas duas atribuições são permitidas pois estão sintaticamente corretas. Seman-
ticamente falando, elas geram um resultado muito provavelmente indesejado.
Com poder e simplicidade vem a chance de cometer erros simples e poderosos.
A manipulação de ponteiros é uma das principais causas de erros de programação. Mas
trabalhando com cuidado, lendo a sintaxe em voz alta, pode reduzir drasticamente o risco
de cometê-los.
Ponteiros são também excelentes métodos para se acessar um periférico de um
sistema, como uma porta de entrada e saı́da. Por exemplo, se tivermos uma saı́da de
porta paralela de 8-bits localizada na posição 0x1010 da memória, essa porta poderia ser
acessada por indirecionamento como demonstrado na Figura 1.74.

Figura 1.74: Exemplo de uso de Ponteiros para acesso a porta paralela


1.9 Ponteiros e Vetores 49
Nesse código, a localização apontada por out port teria o valor 0xAA atribuı́do.
O que pode ser entendido como: “qualquer valor atribuı́do a out port será escrito no
endereço de memória 0x1010 ”.
Por causa da estrutura da linguagem C, é possı́vel fazer com que um ponteiro
aponte para outro ponteiro. De fato, não há limite para a profundidade desse tipo de
relação, exceto para a confusão que isso pode causar. O exemplo da Figura 1.75 exempli-
fica essa estrutura.

Figura 1.75: Exemplo de uso de Ponteiros para Ponteiro

A atribuição acima equivaleria a fazer j = i.


Uma vez que são efetivamente endereços, eles oferecem a possibilidade de mover,
copiar e modificar a memória de várias maneiras com muito poucas instruções. Em se
tratando de aritmética de endereços, o compilador C se assegura de que o endereço correto
seja computado baseado no tipo de variável ou constante a ser endereçada. Por exemplo,
na Figura 1.76 ptr e lptr são incrementados de 1 localização, o que na realidade são 2 bytes
para o ponteio ptr e 4 bytes para o ponteiro lptr uma vez que são tipos subsequentes. Isso
também é válido ao se utilizar os operadores de incremento de decremento.

Figura 1.76: Aritmética de Ponteiro


1.9 Ponteiros e Vetores 50
Uma vez que os operadores de indireção (*) e endereço (&) são operadores unários
e possuem a precedência mais alta, eles vão ter sempre a maior prioridade sobre as outros
operações em um expressão.
Uma vez que incremento e decremento são também operadores unários e dividem a
mesma prioridade, expressões contendo esses operadores serão avaliadas da esquerda para
a direita. Por exemplo, na Figura 1.77 são listadas operações de pré e pós incremento
como parte de uma atribuição. Preste atenção nos comentários para entender como o
mesmo nı́vel de precedência afeta o resultado da operação.

Figura 1.77: Mais Aritmética de Ponteiro

Ponteiros podem ser utilizados para extender a quantidade de informação retor-


nada por uma função. Uma vez que uma função internamente pode retorna apenas um va-
lor através do uso da palavra return, passando ponteiros como parâmetros de uma função
permite que a mesma seja capaz de retornar valores adicionais. Considere a função swap2
da Figura 1.78, essa função simplesmente troca os valores de a e b. O chamador provê
um ponteiro com as variáveis que ele deseja transpor da seguinte maneira: swap2(&v1,
&v2).
1.9 Ponteiros e Vetores 51

Figura 1.78: Mais Aritmética de Ponteiro

Uma vez que a função swap2() está utilizando os endereços que foram passados
a ela, ela realiza a troca das variáveis v1 e v2 diretamente. Esse processo de passagem
de ponteiros é frequentemente utilizado e pode ser encontrado em funções da biblioteca
padrão como o scanf(). A função scanf() (definida em stdio.h) permite que múltiplos
parâmetros sejam reúnidos de uma entrada padrão de maneira formatada e as armazena
em locais especı́ficos de memória. Um scanf() tı́pico se parece com scanf(“%d %d %d”,
&x, &y, &z), que vai pegar 3 inteiros da entrada padrão e armazenar nas variáveis x, y
e z.

1.9.2 Vetores

Um vetor é outro sistema de indireção. É um conjunto de dados ordenados do mesmo


tipo. Um vetor é declarado da mesma maneira que uma variável ou constante, exceto
pelo número de elementos, conforme Figura 1.79.

Figura 1.79: Exemplo de declaração de vetores

A referência a um elemento do vetor é dado através de um ı́ndice. O ı́ndice pode


variar de 0 até o tamanho declarado menos uma unidade. A Figura 1.80 mostra como
fazer esse referência.
1.9 Ponteiros e Vetores 52

Figura 1.80: Fazendo referência a um determinado elemento de um vetor

Declarações de vetores podem conter inicializadores. Em vários vetores, a inici-


alização de valores é feita na memória do programa e então copiada para o vetor antes
do main() ser executado. Um vetor de constantes é diferenciado pois os valores serão
alocados na memória do programa, poupando memória RAM, o que normalmente falta
em um microcontrolador. Uma inicialização tı́pica é mostrada na Figura 1.81:

Figura 1.81: Inicialização tı́pica de um vetor

Na Figura 1.81 array[0]=12 , array[1]=15 , array[2]=27 , array[3]=56 e


array[4]=94 . A linguagem C não tem como varificar os limites de um vetor. Se um
valor é atribuı́do a um ı́ndice que excede os limites de um vetor, a memória pode ser
alterada de maneira inesperada, o que levará a resultados imprevisı́veis. Na Figura 1.82
temos um exemplo o que foi falado anteriormente:

Figura 1.82: Acessando dados fora dos limites de um vetor

Vetores são alocados, na memória, de maneira sequencial. Lendo digits[5] no


mostrado na Figura 1.82 irá fazer com que o processador vá no local do primeiro ı́ndice
do vetor e então leia os dados das 5 posições acima da posição inicial. Portanto, a segunda
linha de código acima fará com que o processador percorra 12 espaços de dados acima do
ponto de partida. Qualquer dado que estiver nessa posição será atribuı́do à variável numb
o que pode causar vários resultados estranhos. Então, assim como em muitas áreas da
programação, alguma cautela e previsão devem ser tomadas.
1.9 Ponteiros e Vetores 53
A primeira diferença entre o uso de vetores e ponteiros está no fato de que para
um vetor, uma área da memória é alocada para os dados. Com um ponteiro, apenas um
endereço de referência é alocado, ficando por conta do programador definir a área que
será acessada.
O tipo de vetor mais comum é o de caracteres. Ele é chamado de string ou
vetor de string. Uma string variável é definida como um vetor de caracteres. Em um
string constante, o compilador do C irá colocar um zero no final da string, ou terminá-lo
em NULL. Quando você declarar uma string de caracteres, constantes ou variáveis, o
tamanho declarado deverá ser uma unidade maior que o tamanho necessário, a fim de
permitir a presença do NULL no final da string. A Figura 1.83 mostra um exemplo
dessa declaração

Figura 1.83: Exemplo de declaração de uma string

cstr, na Figura 1.83, está setado para armazenar 16 valores, pois a string contém
15 caracteres e 1 espaço deve estar reservado para o NULL.
O nome de um vetor seguido de um ı́ndice faz referência a elementos individuais
do vetor, independente do tipo. Usando apenas o nome do vetor estaremos fazendo
referência apenas ao primeiro elemento deste.
Fazendo as declarações mostradas na Figura 1.84

Figura 1.84: Declarações

A atribuição:
1.9 Ponteiros e Vetores 54

Figura 1.85: Atribuição 1

É o mesmo que:

Figura 1.86: Atribuição 2

Strings de caracteres normalmente precisam ser trabalhadas caractere por carac-


tere. Enviando uma mensagem para um dispositivo serial ou um LCD (Liquid Crystal
Display) são exemplos desse tipo de necessidade. O exemplo da Figura 1.87 mostra como
um vetor indexado e um ponteiro funcionam de maneira quase indistinta. O exemplo usa
a função putchar() para enviar uma caractere por vez para a saı́da padrão, praticamente
como uma porta serial:

Figura 1.87: Usando a função putchar()

A primeira parte do programa faz uso do for para mandar, individualmente, cada
caractere do vetor para a saı́da. O contador desse laço é usado como ı́ndice para recuperar
cada caractere do vetor, de maneira que isso possa ser passado para a função putchar(). A
segunda parte do programa faz uso de um ponteiro para acessar cada elemento do vetor.
1.9 Ponteiros e Vetores 55
A linha p=s seta o ponteiro com o endereço do primeiro caractere do vetor. O for a seguir
faz uso do ponteiro (pós-incrementado cada vez) para recuperar os elementos do vetor e
passá-los para a putchar().

1.9.3 Vetores Multidimensionais

A linguagem C suporta vetores multidimensionais. Quando um vetor multidimensional


é declarado, ele deve ser imaginado como um vetor de vetor. Vetores multidimensionais
podem ser feitos paa terem duas, três ou mais dimensões. Os endereços da memória
adjacente são sempre referenciados pelo ı́ndice mais a direita da declaração.
Um vetor de inteiros com duas dimensões tı́pico é declarado conforme mostrado
na Figura 1.88.

Figura 1.88: Exemplo de declaração de vetor com duas dimensões

Na memória, os elementos do vetor serão guardados em linhas sequenciais, como


mostrado na Figura 1.89

Figura 1.89: Exemplo de vetor com duas dimensões (matriz)

Vetores multidimensionais são úteis em operações como matrizes, filtros e look-


up-tables (LUTs).
Pela instância criada na Figura 1.90, supõe-se que temos um teclado de telefone
que gera uma linha e uma coluna, ou um código de verificação, sempre que uma tecla é
pressionada. Um vetor de duas dimensões pode ser usado como uma LUT para converter
o código de verificação para um caractere ASCII através da tecla. Nós estamos assumindo,
para esse exemplo, que a rotina getkeycode() pesquisa as letras e seta os valores de row e
col para indicar a posição de cada tecla pressionada.
1.9 Ponteiros e Vetores 56

Figura 1.90: Código usando uma LUT

Outra forma, mais comum, de vetor bi-dimensional é um vetor de strings. A


Figura 1.91 declara um vetor de caracteres inicializado com os dias da semana.

Figura 1.91: Vetor de strings

No vetor da Figura 1.91, as strings possuem comprimentos diferentes. O com-


1.9 Ponteiros e Vetores 57
pilador coloca NULL após o último caractere da string, não importa o quão longa ela
seja. Qualquer local desperdiçado na memória é deixado inutilizado. Funções como o
printf() imprimem a string até que elas encontrem o caractere NULL, dessa forma ela
se torna capaz de imprimir strings de tamanhos variados. Usaremos printf() para acessar
o quarto dia da semana e imprimir a string associada.printf() precisa do endereço do
primeiro caractere da string, conforme pode ser visto na Figura 1.92.

Figura 1.92: Função printf() mostrando como imprimir um dado de um vetor de strings

O nome da string é considerado como sendo o endereço do primeiro caractere


da string. Apenas a primeira dimensão está, efetivamente, referenciando a string inteira
assim como a segunda dimensão. Então, a mesma string, no vetor mostrado na figura
1.91, pode ser acessada conforme a Figura 1.93.

Figura 1.93: Acessando um dado do vetor de strings usando referência

1.9.4 Vetores para funções

Um dos mais poderosos aspectos de ponteiros é o fato de também poderem ser usados
em funções. Usar um ponteiro em uma função permite que esta seja chamada a partir do
resultado da LUT. Ponteiros em funções também permitem que as funções sejam passadas
como referência para outras. Isso permite a criação de um fluxo dinâmico de execução, o
qual é chamado de código “auto-modificador”.
Relembrando a seção sobre funções, a forma padrão de uma função é:
1.9 Ponteiros e Vetores 58

Figura 1.94: Forma padrão de uma função

Considere um exemplo que chama uma função de uma tabela de ponteiros para
funções. No exemplo mostrado na Figura 1.96, a função scanf() pega um valor de uma
lista de entrada padrão. O valor é verificado para ter certeza de que está no intervalo
determinado (1 a 6). Se for um valor apropriado, func number é usada como um ı́ndice em
um array de ponteiros de funções. O valor do vetor no ı́ndice de func number é atribuı́do
a fp, o qual é um ponteiro para uma função do tipo void.
Na Figura 1.96, fp possui o endereço de uma função. O operador de indireção é
adicionado para obter o endereço de função a partir do ponteiro fp. Agora a função pode
ser chamada simplesmente adicionando o operador de função (), conforme pode ser visto
na Figura 1.95.

Figura 1.95: Declaração de ponteiro para uma função

O exemplo citado é mostrado na Figura 1.96.


1.9 Ponteiros e Vetores 59

Figura 1.96: Exemplo de código usando ponteiros para funções


1.10 Estruturas e Uniões 60
1.10 Estruturas e Uniões

Estruturas e uniões são utilizadas para agrupar variáveis sob um cabeçalho ou nome.
Uma vez que a palavra “objeto”em C se refira a um grupo ou associação de membros de
dados, as estruturas e uniões são elementos fundamentais à programação orientada a
objeto.
A programação orientada a objeto (OOP ? object-oriented programming) se refere
ao método em que o programa lida com dados de maneira relacional. Uma estrutura ou
união podem ser pensadas como um objeto. Os membros dessa estrutura ou união são
as propriedades (variáveis) desse objeto. O nome do objeto, então, fornece um meio para
identificar a associação das propriedades do mesmo ao longo do programa.

1.10.1 Estruturas

Uma estrutura é um método de criação de um conjunto de dados únicos a partir de


uma ou mais variáveis. As variáveis dentro da estrutura são chamadas de membros. Este
método permite que uma coleção de membros seja referenciada a partir de um único nome.
Algumas linguagens de alto nı́vel se referem a esse tipo de objeto como uma gravação ou
variável-base. Ao contrário de uma matriz, as variáveis contidas nesta estrutura não
precisam ser do mesmo tipo.
Uma declaração de estrutura segue o modelo mostrado na Figura 1.97.

Figura 1.97: Declaração padrão de uma estrutura

Uma vez que a estrutura modelo foi definida, a structure tag name serve como
uma descrição comum e pode ser usada para declarar estruturas deste tipo por todo o
programa.
Na Figura 1.98 são declaradas duas estruturas, var1, var2 e um arranjo de estru-
1.10 Estruturas e Uniões 61
turas var3

Figura 1.98: Exemplo de declaração de duas estruturas de um arranjo

Estruturas modelo podem conter todo tipo de variáveis, incluindo outras estrutu-
ras, ponteiros para funções e ponteiros para estruturas. É interessante notar que quando
um modelo é definido, nenhuma memória é alocada. A memória é alocada quando a
variável da estrutura atual é declarada.
Membros em uma estrutura são acessados utilizando o operador de membro (.).
O operador de membro conecta o nome do membro com a estrutura que ele faz parte. Na
Figura 1.99.

Figura 1.99: Acessando um mebro de uma estrutura

Da mesma maneira que arranjos, estruturas podem ser inicializadas seguindo o


nome da estrutura com uma lista de inicializadores em chaves.

Figura 1.100: Inicialização de uma estrutura

Isso nos dá o mesmo resultado que as atribuições da Figura 1.101

Figura 1.101: Inicialização dos membros de uma estrutura


1.10 Estruturas e Uniões 62
Uma vez que as estruturas são de um tipo válido, não há limite de membros a
serem colocadas dentro delas. Por exemplo, se um conjunto de estruturas for declarado
como o mostrado na Figura 1.102.

Figura 1.102: Exemplo de membros em um estrutura

Para acessar a localização da estrutura “widget”(OBS.: widget é o novo moe


da estrutura PART ), você deverá fornecer uma referência (como mostrado na Figura
1.103.

Figura 1.103: Acessando a estrutura através de seu novo nome “widget”

Para acessar o local de widget as mesmas regras se aplicam.

Figura 1.104: Determinando a localização de um widget

Uma estrutura pode ser passada para função como um parâmetro bem como
retornada de uma função. A função da Figura 1.105:
1.10 Estruturas e Uniões 63

Figura 1.105: Exemplo de função envolvendo estruturas

Retornaria a estrutura PART com os membors locais bin.x e bin.y atribuı́dos


aos parâmetros passados para a função. Os membros sku e part name também seriam
apagados antes que a estrutura retornasse. Então, a função mostrada na Figura 1.105
poderia ser usada em uma atribuição:

Figura 1.106: Atribuição do retorno da função da Figura 1.105

Como resultado, part name e sku seriam apagadas e widget.bin.x e widget.bin.y


seriam definidas para o valor 10.

1.10.2 Vetores de Estruturas

Assim como qualquer outro tipo de variável, vetores de estruturas também podem ser
declarados. A declaração de um vetor de estruturas está mostrada na Figura 1.107.

Figura 1.107: Declaração de um vetor de estruturas

O acesso a um membro continua o mesmo, a única diferença é que a indexação da


estrutura varia. Então, para acessar uma “localização particular da variável widget”uma
referência como a mostrada na Figura 1.108
1.10 Estruturas e Uniões 64

Figura 1.108: Referência para acessar uma região particular da estrutura widget

Neste exemplo existe uma string de caracteres, part name, que pode ser acessada
normalmente como qualquer string

Figura 1.109: Acesso a uma string de caracteres dentro de um vetor de estrutura

Vetores de estruturas podem ser inicializados através do nome da estrutura se-


guido de uma lista de inicializadores entre chaves; deve existir um inicializador para cada
elemento da estrutura que está dentro de cada elemento do vetor. A Figura 1.110.

Figura 1.110: Inicialização de uma string de um vetor de estrutura

1.10.3 Ponteiros para Estrutura

As vezes é desejável manipular membros de uma estrutura de forma generalizada. Um


método para se fazer isso é usando um ponteiro para referenciar a estrutura, por exemplo,
passando um ponteiro para uma estrutura em uma função em vez de passar a estrutura
inteira.
1.10 Estruturas e Uniões 65
Um ponteiro para uma estrutura é declarado como mostrado na Figura 1.111.

Figura 1.111: Ponteiro para uma estrutura

O operador de ponteiro () mostra que structure var name é um ponteiro para


uma estrutura do tipo structure tag name. Assim como em qualquer outro tipo, a um
ponteiro deve ser atribuı́do um valor que aponta para algo tangı́vel, como uma variável
que já foi declarada. A declaração de variáveis garante que memória foi alocada para um
propósito.
O código da Figura 1.112 será usado para declarar a variável de estrutura widget
e um ponteiro para a variável de estrutura this widget. A última linha do exemplo atribui
o endereço do widget ao ponteiro this widget.

Figura 1.112: Ponteiro recebendo endereço de uma estrutura

Quando um ponteiro é utilizado para referenciar uma estrutura, o operador de


ponteiro (–>) é utilizado para acessar membros da estrutura através da indireção¿

Figura 1.113: Uso do operador de ponteiro para realizar uma atribuição


1.10 Estruturas e Uniões 66
Isso também poderia ser feito usando o operador de indireção para primeiro alocar
a estrutura e depois usando o operador de membro para acessar o membro sku.

Figura 1.114: Outra forma de realizar uma atribuição usandp o operador de ponteiro

Desde que this widget é um ponteiro para widget, os dois métodos de atribuição
mostrados anteriormente são válidos. O uso dos parênteses é necessário porque o operador
de membro tem uma prioridade maior do que o operador de indireção (* ). Se os parênteses
fossem omitidos a expressão poderia ser interpretada de maneira errada, como mostrado
na Figura 1.115

Figura 1.115: Problema causado pela falta dos parênteses

Na Figura 1.115 estamos nos referindo, na verdade, ao endereço de widget(&widget).


Estruturas podem conter ponteiros para outras estruturas, mas podem conter
ponteiros para estruturas do mesmo tipo. Uma estrutura não pode conter ela mesma como
membro, porque isso seria uma declaração recursiva, e o compilador não teria informação
para resolver a declaração. Ponteiros são sempre do mesmo tamanho independentemente
do que eles estejam apontando. Portanto, apontando para uma estrutura do mesmo tipo,
a estrutura pode ser feita auto-referenciada. A Figura 1.116 é um exemplo básico disso.

Figura 1.116: Exemplo básico de estrutura auto- refrenciada


1.10 Estruturas e Uniões 67
Na Figura 1.116 item.next item–>position é o membro position da estrutura
apontada por next item. isto é equivalente a item2.position.
Estruturas auto-refereciadas são tipicamente usadas para manipulação de dados
como listas encadeadas e classificações rápidas (quick sorts).

1.10.4 Uniões

Uma união é declarada e acessada da mesma maneira que uma estrutura. Uma declaração
de uma união é parecida com o mostrado na Figura 1.117

Figura 1.117: Declaração padrão de uma união

A diferença primária entre uma estrutura e uma união é no local de memória


alocado. Os membros de uma união, na verdade, compartilham a memória alocada para
o maior dos membros daquela união.

Figura 1.118: Exemplo de declaração de uma união

Na Figura 1.119, o quantidade total de memória alocada para my space é equi-


valente ao tamanho de long int long one(4 bytes). Se um valor é atribuı́do para long one.

Figura 1.119: Atibuindo um valor para long one


1.10 Estruturas e Uniões 68
Então o valor de my space.character e my space.integer também serão modifica-
dos. Nesse caso, seus valores seriam os mostrados na Figura 1.120.

Figura 1.120: Novos valores de my space.character e my space.integer

Uniões são usadas as vezes como uma método de preservar espaço de memória.
Se existem variáveis que são utilizadas como base temporariamente e depois não existe a
menor chance de que elas sejam usadas ao mesmo tempo, uma união é um método para
definir um “espaço de rascunho”na memória.
Com maior frequência uma união é utilizada para extrair pequenas partes de um
dado vindo de um grande objeto de dados. Isto é mostrado no exemplo anterior. A
posição real dos dados depende do tipo de dado usados e de como o compilador lida com
um número maior do que o tipo char (8 bits). O exemplo anterior assume que o byte
mais significativo vem antes na armazenagem. Compiladores variam na forma de como
eles rmazenam os dados. Os dados podem ser trocados por ordem de bytes, por ordem
de palavra ou pelos dois. Este exemplo pode ser usado em um compilador para se achar
como os dados são armazenados na memória.
Declarações de união podem salvar passos na codificação para converter uma
organização para outra. Nas Figuras 1.121 e 1.122 são mostrados dois exemplos onde duas
portas de entrada de 8 bits são combinadas formando uma porta de 16 bits. Primeiro
usaremos o método de deslocamento e combinação (Figura ??).
1.10 Estruturas e Uniões 69

Figura 1.121: Tranformando duas portas de 8 bits em uma porta de 16 bits usando
deslocamento

Na Figura 1.122 iremos obter o mesmo resultado, mas fazendo uso de uma de-
claração de união.

Figura 1.122: Tranformando duas portas de 8 bits em uma porta de 16 bits usando união
1.10 Estruturas e Uniões 70
1.10.5 Operador typedef

A linguagem C suporta uma operação que permite a criação de novos tipos de nomes.
O operador typedef permite que um nome seja declarado como sinônimo de um tipo
existente. A Figura 1.123 mostra um exemplo de uso do operador typedef.

Figura 1.123: Exemplos de usos do typedef

Agora o pseudônimo byte e word podem ser usados para declarar outras variáveis
que são na verdade do tipo unsigned char e unsigned int, respectivamente.

Figura 1.124: Declarando variáveis usando os pseudônimos byte e word criados

Este método também funciona para estruturas e uniões.

Figura 1.125: Renomeando estruturas com o comando typedef

A declaração #define as vezes é usada para efetuar esta operação através da


substituição de um texto no compilador do processador. typedef é avaliado diretamente
pelo compilador e pode funcionar com declarações, modelagens e usos que excederiam a
capacidade do processador.
1.10 Estruturas e Uniões 71
1.10.6 Bit e Campo de Bit (Bit e Bitfield

Bits e bitfields são frequentemente utilizados quando espaço de memória é muito grande.
Alguns compiladores suportam o tipo bit, o qual é automaticamente alocado pelo com-
pilador e referenciado como uma variável dele. A Figura 1.126 nos mostra um exemplo
disso:

Figura 1.126: Declarando uma variável do tipo bit

Ao contrario dos bits, os bitfields são mais comuns em sistemas maiores, mais
gerais, e não são sempre suportados por compiladores embarcados. Bitfields são associados
com estruturas por causa de sua forma e declaração, como mostrado na Figura 1.127.

Figura 1.127: Formas de criação de um bitfield

Um bitfield é especificado pelo nome de um membro (apenas do tipo unsigned


int, seguido por dois-pontos (:) e o numero de bits necessários para o valor. O tamanho
do membro pode ser de 1 até o tamanho do tipo unsigned int(16 bits). Isso permite
que vários bitfields dentro de uma estrutura sejam representados por um único local de
memória inteiro sem sinal.
1.10 Estruturas e Uniões 72

Figura 1.128: Declaração de uma estrutura usando a operação de bitfield

Esses bitfields podem ser acessados pelo nome de um membro, assim como você
faria para uma estrutura

Figura 1.129: Acesso a membros de uma estrutura criada usando o bitfield

As vezes, em sistemas embarcados, bitfields são usados para definir pinos de I/O
(entrada e saı́da).

Figura 1.130: Usando bitfield para descrever os pinos de entrada e saı́da

A declaração #define na Figura 1.131 indica o conteúdo da posição de memória


0x38 (lembre-se #define é tratada como uma substituição diretiva textual pelo proces-
sador). Isso permite que o programador acesse a posição de memória 0x38 pelo nome
PORTB, como se fosse uma variável, em todo o programa.
1.10 Estruturas e Uniões 73

Figura 1.131: Comando #define determinado parâmetros de PORTB

O tipos bits, de um bitfield, permitem que bits da porta I/O PORTB sejam aces-
sados independentemente (isso costuma ser chamado de “bit banged ”(“bit batido”).

Figura 1.132: Exemplo de “bit banged”

1.10.7 Operador sizeof

A linguagem C suporta um operador unário chamado de sizeof. Este operador é um


recurso que cria um valor constante relacionado com o tamanho de um conjunto de objetos
de dados ou seu tipo. As formas de operação do sizeof são mostradas na Figura 1.133.

Figura 1.133: Formas de operação do sizeof

Essas operações geram um inteiro que revela o tamanho (em bytes) do tipo ou o
objeto de dados localizado entre os parênteses.
Considere as declarações mostradas na Figura 1.134
1.11 Tipos de Memória 74

Figura 1.134: Declarações para usar o sizeof

Na Figura 1.135 temos alguns possiveis resultados das declarações feitas na Figura
1.134.

Figura 1.135: Possı́veis reultados das declarações feitas na Figura 1.134

1.11 Tipos de Memória

A arquitetura de um microprocessador pode requerer que variáveis e constantes estejam


guardadas em diferentes tipos de memórias. Dados que não mudarão deverão ser guarda-
das em um tipo de memória, enquanto dados que precisão ser lidos e escritos repetitiva-
mente no programam deverão ser guardados em outro tipo de memória. Um terceiro tipo
de memória pode ser usado para guardar variáveis de dados que precisam ser mantidas
mesmo quando a alimentação é removida do sistema. Quando tipos de memórias espe-
ciais como ponteiros e variáveis de registradores são acessadas, fatores adicionais devem
1.11 Tipos de Memória 75
ser considerados.

1.11.1 Constanstes e Variáveis

O micro controlador foi desenvolvido usando a arquitetura Harvard, com endereços separa-
dos para dados (SRAM), programa (FLASH), e memória EEPROM. O CodeVisionAVR
R

e outros compiladores implementam três tipos de descritores de memória para permitir


fácil acesso para esses diferentes tipos de memória.
A alocação de variável padrão ou automática, onde nenhuma palavra-chave de
descritor de memória é usada, é na SRAM. Constantes podem ser colocadas na memória
FLASH (espaço de programa) com as palavras-chave flash ou const. Para variáveis serem
colocadas na EEPROM, a palavra-chave eeprom é usada.
Quando declarações são realizadas, as posições das palavras-chave flash e eeprom
se tornam parte do significado. Se const, flash ou eeprom aparecerem antes, isso sinaliza
para o compilador que a atual alocação do armazenamento ou localização do dado é nessa
área de memória. Se o tipo é declarado seguido das palavras-chave flash ou eeprom, isso
indica que uma variável que faz referência à FLASH ou EEPROM, mas a variável em si é
fisicamente localizada na SRAM. Esse cenário é usado quando declara-se ponteiros dentro
da FLASH ou EEPROM.
As declarações feitas na Figura 1.136 vão colocar o dado fı́sico diretamente na
memória de programa (FLASH). Esses valores dos dados são todos constantes e não
podem ser modificados de modo algum pela execução do programa:
1.11 Tipos de Memória 76

Figura 1.136: Colocando um dado diretamente na memória de programa

O espaço EEPROM é uma área de variável de memória não volátil. Variáveis


podem ser colocadas na memória EEPROM simplesmente pela declaração, conforme po-
demos ver na Figura 1.137.

Figura 1.137: Colocando variáveis na memória EEPROM

Essas áreas de memória permanentes (FLASH) e semi-permanentes (EEPROM)


tem muitas aplicações especificas de sistemas no mundo embarcado. O espaço da FLASH
é uma área excelente para um dado que não muda. O código do programa em si reside
nessa região. Declarando itens como textos strings e look-up-tables, diretamente nessa
região, libera-se espaço na SRAM.
Se a string é declarada com um inicializador como na Figura 1.138.

Figura 1.138: Delcarando string com um inicializador


1.11 Tipos de Memória 77
30 bytes da SRAM serão alocados, e o texto “This string is palced in SRAM ”é
fisicamente colocado na memória FLASH com o programa. Na inicialização, esse dado
residente na FLASH é copiado para SRAM e o programa trabalha na SRAM sempre
que e acessa mystring. Isso é um desperdı́cio de 30 bytes da SRAM a não ser que a
string tenha a intenção de sofrer alterações pelo programa durante o tempo que trabalha.
Para prevenir essa perda de espaço da SRAM, a string pode ser armazenada na memória
FLASH diretamente na declaração. Isso é mostrado na Figura 1.140

Figura 1.139: Declarando uma string e já colocando-a na FLASH

A área EEPROM é chamada não-volátil, significando que quando a alimentação é


removida do microprocessador o dado vai se mater intacto, mas ela é semi-permanente de
modo que o programa pode alterar o dado localizado nessa região. EEPROM também tem
tempo de vida ? tem um número máximo de ciclos de escrita que pode ser realizado antes
de falhar eletricamente. Isso se deve em razão da construção da memória EEPROM em
si, uma função de eletro-quı́mica. Em vários casos, essa área de memória terá uma taxa
de 10.000 operações de escrita, no máximo. Novas tecnologias estão sendo desenvolvidas
a todo momento, aumentando o número de operações de escrita para cem mil, ou até
mesmo milhões. Não existe limitações para a quantidade de vezes que pode ser lida.
É importante entender essa limitação fı́sica quando se está desenvolvedo um pro-
grama que usa EEPROM. Dados que precisam ser mantidos e não serão mudados frequen-
tementes podem ser armazenados nessa área. Essa região é ótima para logging de dados
de baixa velocidade, tabelas de calibração, medidores de tempo de execução, configuração
de software e valores de configuração.

1.11.2 Ponteiros

Ponteiros para regiões de memórias especiais são tratados cada um de forma diferente
durante a execução. Mesmo os ponteiros podendo apontar para áreas de memórias FLASH
e EEPROM, eles, em si, são sempre armazenados na SRAM. Nesses casos, as alocações
1.11 Tipos de Memória 78
são normais (char, int, etc.) mas o tipo de memória que está sendo referenciado tem
que ser descrito para que o compilador possa gerar o código correto para acessar a região
desejada. As palavras-chave flash e eeprom nesses casos são usadas para discorer no nome,
como na Figura

Figura 1.140: Exemplo de utilização de um ponteiro apontando para uma string localizada
na SRAM

1.11.3 Variáveis de Registro

A área da SRAM do microcontrolador AVR inclui a região chamada de “Register File”(Arquivo


de Resgistro). Essa região contém portas I/O, temporizadores, e outros periféricos, bem
como alguma área de “trabalho”ou “scratch pad”. Para instruir o compilador a alocar a
variável em um registrador ou registradores, o modificador de classe de armazenamento
register tem de ser usada (vide Figura 1.141).

Figura 1.141: Modificador register

O compilador pode escolher automaticamente alocar a variável para um ou mais


registradores, mesmo se o modificador não for usado. Com o intuito de prevenir que a
variável seja alocada em um registrador, o modificador volatile tem de ser usado. Isso
avisa o compilador que a variável pode estar sujeita a variação externa durante a avaliação.

Figura 1.142: Modificador volatile


1.11 Tipos de Memória 79
O modificador volatile é frequentemente usado em aplicações onde variáveis são
armazenadas na SRAM enquanto o microcontrolador dorme (Dormir é uma parada, mode
de baixa potência usado tipicamente em aplicações com bateria). Isso permite que o valor
na SRAM seja válido toda vez que o microcontrolador é acordado e retornado a sua
operação normal.
Variáveis Globais que não tenham sido alocadas em registradores são guadadas
na área de Variável Geral ou Global da SRAM. Variáveis locais que não foram alocadas
em registradores , são armazanadas espaço de alocação dinâmica na Data Stack ou Heap
Space da SRAM.

Sfrb e sfrw

As portas I/O e periféricos são localizados no arquivo de registro. Portanto, instruções


especiais são usadas para indicar ao compilador a diferença entre a localização de uma
SRAM, uma porta I/O, ou outro periférico no arquivo de registro.
As palavras-chave sfrb e sfrw indica ao compilador que uma intrução assembly
IN e OUT são usadas para acessar os registradores I/O do microcontrolador AVR.

Figura 1.143: Exemplo usando as funções sfrb e sfrw

O acesso, em nı́vel binário, ao registrador I/O é permitido usando um slecionador


de bit anexado depois do nome do registrador I/O. Como o acesso em nı́vel binário ao
registrador I/O é feito usando o as intruções de linguagem assembly CBI,SBI,SBIC e
SBIS, o endereço do registrador tem que estar entre 0x00 e 0x1F para declarações usando
sfrb, e 0x00 até 0x1E para declarações usando sfrw. Na Figura 1.144 temos um exemplo
de acesso em nı́vel binário usando sfrb e sfrw
1.11 Tipos de Memória 80

Figura 1.144: Aceso em nı́vel binário usando sfrb e sfrw

Usualmente as plavras-chave sfrb e sfrw são encontradas nos arquivos de cabeçalho


inclusos no topo do programa através da diretiva de pré-processamento #include. Esse
arquivos de cabeçalho são tipicamente relacionados a um processador particular. Os ar-
quivos de cabeçalho providenciam nomes para registradores I/O ou outros tipo úteis no
microcontrolador que está sendo usado na aplicação em particular.
Está listado na Figura 1.145 um arquivo de inclusão tı́pico que pode ser gerado
com um compliador (MEGA8515.h)
1.12 Métodos de Tempo Real 81

Figura 1.145: Arquivo de inclusão tı́pico usando o MEGA8515.h

1.12 Métodos de Tempo Real

Programação em tempo real às vezes é mal interpretada como um processo complexo e
mágico que pode ser realizado apenas em máquinas grandes com sistemas operacionais
Linux ou Unix. Não é assim. Sistemas embarcados podem, em muitos casos, rodar mais
em uma base em tempo real do que um sistema grande. Um simples programa pode
executar seu curso mais e mais. Pode ser capaz de responder às mudanças no ambiente
do hardware que se opera, mas fará isso em seu próprio tempo. O termo “tempo real”é
usado para indicar que uma função do programa é capaz de realizar todas as suas funções
em uma forma arregimentada dentro de um determinado lote de tempo. O termo também
pode indicar que o programa tem a habilidade de responder imediatamente a um estimulo
externo (hardware de entrada). O AVR, com seu rico conjunto de periféricos, tem a
habilidade não só de responder à hardware de temporizadores, mas também à mudanças
de entrada. A habilidade do programa de responder à essa mudança do mundo real é
chamada interruptor (interrupt) ou processamento de exceção (exception processing).
1.12 Métodos de Tempo Real 82
1.12.1 Usando Interruptores

Um interruptor é apenas isso, um exceção, mudança de fluxo ou interrupção na operação


do programa causado por uma fonte de hardware externa ou interna. Um interruptor é,
na realidade, uma chamada de uma função gerada pelo hadware. O resultado é que o
interruptor vai causar um fluxo de execução para pausar enquanto a função interruptora,
chamada de rotina de serviço de interrupção (ISR), é executada. Após a conclusão do
ISR, o fluxo do programa vai resumir continuando de onde foi interrompido. Em um
AVR, um interruptor fará com que o registrador de status e o contador do programa
sejam alocados na pilha, e, baseado na fonte do interruptor, o contador do programa
vai ser atribuı́do um valor da tabela de endereços. Esses endereços são referidos como
vetores. Uma vez que o programa tenha sido redirecionado por um interrupt vectornig,
pode ser retornando à operação normal através da instrução da máquina RETI (Retorno
de uma Interrupção). A instrução RETI restaura o registrador de status ao seu valor
pré-interrupção e seta o contador do programa para a próxima instrução da máquina que
se segue da que foi interrompida. Existem muitas fontes de interrupções disponı́veis no
microcontrolador AVR. Quanto maior o AVR, mais dessas fontes são disponı́veis. A lista
abaixo possui algumas das possibilidades. Essas definições são normalmente encontradas
em um arquivo de cabeçalho, no topo do programa, e são especı́ficos a um microproces-
sador dado. Isso pode ser encontrado no cabeçalho para um ATMega128, Mega128.h em
definições conforme explı́cito na Figura 1.146.
1.12 Métodos de Tempo Real 83

Figura 1.146: Arquivo .h com definição de interrupções

Essa lista contém uma série de ı́ndices de vetores associados a um nome de des-
crição da fonte de interrupção. Para criar um ISR, a função que é chamada pelo sistema de
interrupção, o ISR, é declarada usando a palavra reservada interrupt como uma função
modificadora de tipo. A Figura 1.147 exemplifica esse tipo de declaração.
1.12 Métodos de Tempo Real 84

Figura 1.147: Declaração de interrupções

A palavra-chave interrupt é seguida por um ı́ndice, que é uma localização do vetor


da fonte de interrupção. O número de vetores começa com [1], mas já que o primeiro
vetor é o vetor de reset, os atuais vetores de interrupção disponı́veis ao programador
começam com [2]. ISRs pode ser executado a qualquer momento, uma vez que as fontes
de interrupção são inicializadas e os interruptores globais são ativados. O ISR não pode
retornar nenhum valor, pois não tem nenhum “chamador”, e é sempre declarado como
tipo void. É pela mesma razão que nada pode ser passado para o ISR. Interrupções em
um ambiente embarcado podem genuinamente criar uma execução em tempo real. Não é
incomum em um sistema rico em periférico ver um loop while(1) vazio na função main().
Nesses casos, main() simplesmente inicializa o hardware e o interruptor realiza todas as
tarefas necessárias quando elas precisam acontecer.
1.12 Métodos de Tempo Real 85

Figura 1.148: Programa com while vazio e funcionamento a base de interrupções

No exemplo da 1.148, main() é usada para inicializar o sistema e ativar o inter-


ruptor overflow TIMER1. A função de interrupção timer overflow pisca o LED a cada
meio segundo. Note que main() está em um loop while(1) enquanto o piscar continua.

1.12.2 Executor em Tempo Real

Um Executor em Tempo Real (RTX) ou Sistema Operante em Tempo Real (RTOS) é


um programa, comumente referido como “kernel”, que coordena a gestão e a execução
de múltiplos subprogramas ou “tarefas”. Um RTX estritamente coordena operações de
execução de programa, enquanto um RTOS é usualmente associado com uma funciona-
1.12 Métodos de Tempo Real 86
lidade extra como gestão de arquivos e outras operações I/O genéricas. Existem muitos
executivos em tempo real disponı́veis para o AVR. Alguns são de domı́nio público e li-
vre de taxas. Outros são licenciadas comercialmente e suportada mas continua barato.
Nessa seção, nós vamos tocar no RTX, especialmente o Recurso Progressivo - PR RTX -
que é prontamente ativo e simples de configurar e usar. Um RTX utiliza um interruptor
baseado no tempo (ex: TIMER0 ) para garantir que várias execuções de subprogramas
aconteçam no tempo e sempre que requer resultados de operações desejadas. A beleza de
um RTX é que a operação do programa, em uma perspectiva de tempo, é definida em um
bloco de cabeçalho e as tarefas são usualmente configuradas com uma chamada dentro de
uma função de inicialização RTX. Caracterı́sticas como o número de tarefas permitidas
a serem executadas dentro do sistema, qual a frequência que as operações tem que cha-
vear de uma tarefa para outra, e se tarefas são para executar em um estilo round-robin
ou top-to-bottom, são definidas pelo cabeçalho. Uma vez que cada subprograma está de
certa forma sozinho, para cada é dado sua própria pilha para se trabalhar, mantendo os
processos realmente independentes. Tome como exemplo a Figura 1.149.

Figura 1.149: Exemplo de uso de RTX

Com as caracterı́sticas operacionais definidas, o RTX é inicializado com definições


1.12 Métodos de Tempo Real 87
de quais tarefas você quer rodando e a prioridade que de cada tarefa. Prioridade é
importante se por algum motivo uma tarefa não pode ter sua função completada dentro
de uma quantidade alocada de tempo, o RTX pode pegar a operação de volta para a
tarefa de máxima prioridade e iniciar novamente. Isso permite que tarefas importantes
e geralmente crı́tica temporalmente pegue tempo de processamento não se importando o
que está ocorrendo com operações de baixa prioridade ou menos importantes. No exemplo
da Figura 1.150 dois LEDs estão sendo acesos por duas tarefas diferentes. Você vai ver
que cada tarefa é simplesmente escrita com uma função C do tipo void e que cada função
contém um loop while(1). Isso é porque não retorna a um “chamador”, cada um está
rodando como programas independentes.
1.12 Métodos de Tempo Real 88

Figura 1.150: Exemplo de uso de RTX para acendimento de LEDS

O loop while(1) na função main() se torna o “padrão”ou tarefa de mı́nima pri-


oridade. Uma vez que cada tarefa é responsável pela liberação de tempo de volta para
o sistema usando a função PR Wait Ticks(), esse tempo não usado no sistema é pas-
sado para main(). Em um tı́pico desenvolvimento de aplicação, as operações lentas como
printf() ou scanf() seriam executadas no main() e variáveis globais seriam usadas para
comunicar informações necessárias com outras tarefas no programa.
1.12 Métodos de Tempo Real 89
1.12.3 Máquinas de Estado

Máquinas de estados são um método comum de estruturar o programa de forma que ele
nunca fica ocioso esperando uma entrada. Máquinas de estados são geralmente codificadas
na forma de uma construção com switch/case, e bandeiras(flags) são usadas para indicar
quando o processo muda do estado atual para o próximo. Máquinas de estados também
oferecem um melhor oportunidade de mudar a função e o fluxo do programa sem uma
reescrita, simplesmente porque estados podem ser adicionados, modificados, e movidos
sem impactar os outros estados que os rodeiam. Máquinas de estados permitem para a
operação primária lógica de um programa acontecer um pouco em segundo plano. Como
tipicamente um tempo muito pequeno é gasto trocando cada estado, mais tempo livre da
CPU é deixado disponı́vel para tarefas temporalmente crı́ticas como coletar informação
analógica, processando comunicações seriais, e realizando operações matemática comple-
xas. O tempo adicional da CPU é geralmente usado para comunicação com humanos:
interfaces de usuários, displays, teclado, serviços, entrada de dado, alarmes, e parâmetro
de edição. As figuras abaixo mostram um exemplo de máquinas de estados usado para
controlar um sinal de trânsito “imaginário”. A máquina de estados nesse exemplo usa
PORTB para ascender as luzes vermelha, verde e amarela nas direções Note-Sul e Oeste-
Leste. Note que no main() a função delay ms() é usada para controlar o tempo. Isso
mantém o exemplo simples. Na vida real, esse tempo pode ser usado para inúmeras de
outras tarefas e funções, e o tempo ligado das lâmpadas pode ser controlado por um in-
terruptor de um hardware temporizador. A máquina de estados Do States() é chamada a
cada segundo, mas é executada apenas por poucas instruções antes de retornar ao main().
A variável global curent state controla o fluxo da execução através de Do States(). As
entradas PED ING EW, PED XING NS, e FOUR WAY STOP cria exceções ao fluxo
normal da máquina alterando tanto o momento, o caminho da máquina ou os dois. O
exemplo do sistema funciona como um semáforo normal. O sinal do Norte-Sul é verde
quando o sinal de Leste-Oeste é vermelho. O tráfego é permitido ao fluxo por um perı́odo
de trinta segundos em cada direção. Existe uma luz amarela de aviso por 5 segundos
durante a transição do verde para o vermelho. Se o pedestre quiser atravessar a rua,
pressionando o botão PED XING EW ou PED XING NS irá diminuir o tempo restante
1.12 Métodos de Tempo Real 90
para o fluxo de tráfego para dez segundos. Se o switch FOUR WAY STOP estiver no on,
todas as luzes serão convertidas para vermelho. A conversão acontecerá apenas durante
o aviso (amarela) para tornar a transição segura para o tráfego. Interfaces de usuários,
displays, teclado, serviços, entrada de dados, alarmes, e parâmetros de edição podem
ser implementados usando máquinas de estados. Quanto mais detalhado o programa é,
usando flags e máquinas de estados, mais instantâneo é. Mais coisas são trabalhadas
continuamente sem travar o sistema, esperando uma condição para mudar.
1.12 Métodos de Tempo Real 91

Figura 1.151: Exemplo de uso de Máquina de Estados para simulação de um sistema de


semáforos - Esquemático do sistema
1.12 Métodos de Tempo Real 92

Figura 1.152: Exemplo de uso de Máquina de Estados para simulação de um sistema de


semáforos
1.12 Métodos de Tempo Real 93

Figura 1.153: Exemplo de uso de Máquina de Estados para simulação de um sistema de


semáforos (continuação)
1.12 Métodos de Tempo Real 94

Figura 1.154: Exemplo de uso de Máquina de Estados para simulação de um sistema de


semáforos (continuação)
1.13 Estilos, Padrão e Diretrizes ao programar 95

Figura 1.155: Exemplo de uso de Máquina de Estados para simulação de um sistema de


semáforos (Fim do exemplo)

1.13 Estilos, Padrão e Diretrizes ao programar

Usar a linguagem C para escrever um código fonte é uma parte do processo de desenvol-
vimento de software. Existem várias considerações que estão fora da escrita do código e
desejo de operação do programa. Essas considerações são:

• Habilidade de manutenção e leitura do software

• Desenvolvimento da documentação do processo

• Gestão do projeto

• Controle de qualidade e atender requisitos externos como ISO9001 e ISO9003

• Administração das Configurações e controle de revisão

• Regras de design e estilo de código requerido pela sua companhia ou organização

• Verificação e validação do processo para atender requerimentos médicos e industriais


1.14 Bibliografia 96
• Análise de riscos

Assim que se começa a desenvolver um código para produtos e serviços que são
lançados no mercado, esses aspectos farão parte do processo de desenvolvimento. Muitas
companhias possuem software com estilos próprios que definem como o software deve
ser fisicamente estruturado. Item como formato de bloco de cabeçalho, nı́vel de chaves e
parênteses, convenção de nomes para variáveis e definições, e regras para tipos de variáveis
e uso serão delineadas. Isso pode soar um pouco ameaçador, mas assim que começa
a escrever um estilo definido e um critério de desenvolvimento, você achará mais fácil
colaborar e dividir esforços com outros em sua organização.
Organizações como MISRA criaram documentos que mostras como seguir algu-
mas regras básicas e diretrizes durante desenvolvimento de software que podem melhorar
a segurança confiabilidade do desenvolvimento. Você pode encontrar mais informações
sobre essas diretrizes em: HTTP://www.misra.org.uk/.

1.13.1 Sumário

Essa apostila forneceu uma base para o começo de programas em linguagem C.


Os conceitos iniciais demonstram a estrutura básica do programa em C. Variáveis,
constantes, enumerações, o escopo e construção, vetores, estruturas e união foram usadas
para definir como memória é alocada e como o dado na memória é interpretado pelo
programam em C.
Expressões e seus operadores, incluindo operações de I/O, foram discutidos para
prover a base para operações aritméticas e condições lógicas. Essas operações e expressões
também são usadas para construção de controle como por exemplo os laços de while e
do/while, for, estados de switch/case e IF/else para formar funções que guia o fluxo da
execução do programa.

1.14 Bibliografia

• Barnett, Cox, O’Cull. Embedded C Programming and the Atmel AVR. 2nd ed.

You might also like