1.1 Objetivos
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é
• I/O simples, afim de que os programas possam usar as portas seriais do microcon-
trolador;
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.
• 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.
Uma variável é declarada por uma palavra reservada, que indica seu tipo e tamanho,
seguida por um identificador:
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.
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:
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:
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 do tipo unsigned long int podem ter o sufixo UL (ex.: 99UL)
• Constantes do tipo char devem ser limitadas por aspas simples. (‘a’ ou ‘A’)
Constantes de Caracter
A Figura 1.9 representa caracteres não imprimı́veis que são reconhecidos pela
linguagem C.
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:
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:
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.
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.
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.
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.
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:
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.
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.
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.
Multiplicação *
Divisão /
Módulo %
Adição +
Subtração -
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) —
• 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).
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
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
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
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.
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 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.
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:
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.1 While
O loop while é um dos mais básicos elementos de controle e o formato dessa estrutura é
a seguinte:
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.
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:
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
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
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:
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
pode ser reduzida para uma expressão condicional que ficaria da seguinte forma:
1.7.5 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.
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:
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:
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
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:
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
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.
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
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
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
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
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
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:
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
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
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.
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.
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.
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
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
A atribuição:
1.9 Ponteiros e Vetores 54
É o mesmo que:
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().
Figura 1.92: Função printf() mostrando como imprimir um dado de um vetor de strings
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
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.
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 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
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.
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
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.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.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
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
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.
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.
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:
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.
Esses bitfields podem ser acessados pelo nome de um membro, assim como você
faria para uma estrutura
As vezes, em sistemas embarcados, bitfields são usados para definir pinos de I/O
(entrada e saı́da).
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”).
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
Na Figura 1.135 temos alguns possiveis resultados das declarações feitas na Figura
1.134.
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
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
Sfrb e sfrw
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
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
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
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:
• Gestão do projeto
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
1.14 Bibliografia
• Barnett, Cox, O’Cull. Embedded C Programming and the Atmel AVR. 2nd ed.