You are on page 1of 83

Programação em C no AVR

Rodrigo Toste Gomes a.k.a Cynary


Nuno João a.k.a Njay
Senso

Ver na Pag. 19 a tabela de pinos de equivalência dos pinos do arduino com os pinos do ATMEGA328

Compilador AVR 6.0 free: http://www.atmel.com/microsite/atmel_studio6/

Última revisão: 21/12/2010 1


Neste documento tentamos explicar ao leitor as várias funções que um micro-controlador AVR
disponibiliza, e como pode controlar as mesmas, sem recorrer a bibliotecas de alto nível que muitas
vezes roubam performance e acrescentam tamanho aos programas, como por exemplo as que o
arduino disponibiliza.

Assumimos que o leitor tem alguma experiência com programação em C, e que o contacto que
tem com AVR é com o arduino (logo, todos os exemplos que temos aqui irão funcionar no mesmo.
No entanto, podem funcionar noutros micro-controladores AVR com poucas ou nenhumas
alterações).

Todos os programas aqui podem ser compilados através dos seguintes comandos:

avr-gcc -Wall prog.c -Os -mmcu=atmega168 -o prog.out -DF_CPU=16000000


avr-objcopy -O ihex -R .eeprom prog.out prog.hex
(substituir prog.c pelo nome do ficheiro com o código)

E podem ser transferidos para o arduino com o seguinte comando:

avrdude -p m328p -c avrisp -P /dev/ttyUSB0 -b 57600 -F -U flash:w:prog.hex


É muito fácil alterar estes comandos para funcionarem com outros micro-controladores e
programadores, estando essa informação provavelmente na datasheet.

Os programas necessários para os comandos acima funcionarem vêm instalados com o IDE do
arduino, e podem ser utilizados para programar outros micro-controladores que não o usado pelo
arduino.

Para os utilizadores de windows, a utilização das ferramentas AVRStudio e WinAVR é


recomendada, mas visto que está além do alcance deste documento, o leitor é incentivado a
pesquisar, mas garantimos que os comandos acima funcionam.

Qualquer software presente neste documento é oferecido com objectivos didácticos, e não é
acompanhado de qualquer garantia de performance ou funcionalidade.

Gostaria de agradecer a todos os membros da lusorobótica que comentaram no tópico respectivo


a estes tutoriais, e em especial ao membro Njay, que me autorizou a usar o seu Micro-tutorial neste
documento.

Última revisão: 21/12/2010 2


Índice
Programação em C no AVR..................................................................................................................1
Introdução.............................................................................................................................................5
Programação em C em micro-controladores........................................................................................6
Controlo da funcionalidade do micro-controlador – os registers....................................................6
Pseudo-código/código esqueleto......................................................................................................7
MACROS.........................................................................................................................................8
Variáveis volatile..............................................................................................................................8
Operações bit-wise em C.................................................................................................................8
GPIO – General Purpose Input/Output...............................................................................................14
Entrada Digital Normal..................................................................................................................14
Entrada com “pull-up” (“puxa para cima”)...................................................................................15
Entrada controlada por um periférico............................................................................................16
Saída Digital Normal.....................................................................................................................16
Saída em Colector Aberto (open colector).....................................................................................17
Saída controlada por um periférico................................................................................................17
GPIOs na Arquitectura AVR..........................................................................................................18
Configuração dos Portos em Linguagem C...................................................................................20
Interrupções........................................................................................................................................21
O que é uma interrupção?..............................................................................................................21
Como funciona uma interrupção no AVR?....................................................................................21
Como lidar com uma interrupção no AVR?...................................................................................22
Exemplo de interrupção através do pino digital 2 (INT0).............................................................23
Cuidados a ter na utilização de interrupções.................................................................................26
Timers.................................................................................................................................................28
O que são e como funcionam timers?............................................................................................28
Timers no AVR...............................................................................................................................28
Modos Normal e CTC....................................................................................................................29
Como usar um timer no AVR.........................................................................................................29
Eventos relacionados com timers..................................................................................................32
Interrupções e timers......................................................................................................................36
Timers – Parte 2, Pulse Width Modulation.........................................................................................39
O que é PWM?...............................................................................................................................39
Vários Modos de PWM.................................................................................................................39
Fast PWM......................................................................................................................................41
Phase and Frequency Correct PWM..............................................................................................46
Analog-to-Digital Converter..............................................................................................................52
Formato Analógico e Digital..........................................................................................................52
O que é o ADC?.............................................................................................................................52
Como funciona o ADC no AVR?...................................................................................................52
Como ligar o input ao AVR?..........................................................................................................56
Utilizar o ADC – construir um sensor de distância.......................................................................57
ADC8 – medir a temperatura interna.............................................................................................61
Comunicação Serial no AVR..............................................................................................................62
Como funciona a comunicação Serial?..........................................................................................62
O que é a USART?.........................................................................................................................63
Inicializando a USART do AVR....................................................................................................64
Enviando e Recebendo Dados através da USART........................................................................65
Exemplo de utilização do USART.................................................................................................68
Comunicação por I²C..........................................................................................................................72
O Protocolo I²C..............................................................................................................................72

Última revisão: 21/12/2010 3


I²C no AVR.....................................................................................................................................74
Bibliografia.........................................................................................................................................83

Última revisão: 21/12/2010 4


"Excerto do "Micro Tutorial AVR" de Njay (http://embeddeddreams.com/users/njay/Micro Tutorial
AVR - Njay.pdf) com alterações/adaptações de Cynary (formatação e conteúdo)"

Introdução
"AVR" é o nome de uma família de micro-controladores de 8 bits comercializada pela ATMEL.
A arquitectura do AVR foi desenvolvida por 2 estudantes de doutoramento noruegueses em 1992 e
depois proposta à ATMEL para comercialização. Para quem souber inglês, podem ver uma pequeno
vídeo sobre os AVR aqui:
http://www.avrtv.com/2007/09/09/avrtv-special-005/ .
O AVR consiste, tal como um PIC e outros micro-controladores, num processador (o "core"),
memórias voláteis e não- voláteis e periféricos. Ao contrário do PIC, o core do AVR foi muito bem
pensado e implementado desde o inicio, e o core que é usado nos chips desenhados hoje é o mesmo
que saiu no 1o AVR há mais de 10 anos (o PIC teve "dores de crescimento" e o tamanho das
instruções aumentou algumas vezes ao longo do tempo de forma a suportar mais funcionalidade).
Assim de uma forma rápida podemos resumir a arquitectura do AVR nos seguintes pontos:
– Consiste num core de processamento, memória de programa (não volátil, FLASH), memória
volátil (RAM estática, SRAM), memória de dados persistentes (não volátil, EEPROM) e bits
fuse/lock (permitem configurar alguns parâmetros especiais do AVR).
– Arquitectura de memória Harvard (memória de programa e memória de dados separadas)
– A memória volátil (SRAM) é contínua
– A maior parte das instruções têm 16 bits de tamanho, e é este o tamanho de cada palavra na
memória de programa (FLASH).
– Execução de 1 instrução por ciclo de relógio para a maior parte das instruções.
– Existem 32 registos de 8 bits disponíveis e há poucas limitações ao que se pode fazer com
cada um.
– Os registos do processador e os de configuração dos periféricos estão mapeados (são
acessíveis) na SRAM.
– Existe um vector de interrupção diferente por cada fonte de interrupção.
– Existem instruções com modos de endereçamento complexo, como base + deslocamento
seguido de auto- incremento/decremento do endereço.
– O conjunto de instruções foi pensado para melhorar a conversão de código C em assembly.
(A introdução do Micro tutorial do Njay mencionava mais alguns tópicos, que considerei como
irrelevantes para este tutorial, logo cortei-os).

Última revisão: 21/12/2010 5


Programação em C em micro-controladores.
Neste conjunto de tutoriais, tentamos ensinar ao leitor como programar um micro-controlador
AVR em C “low-level”. Para fazer isso, é especialmente necessário compreender como controlar as
várias funções do micro-controlador. Os restantes tutoriais concentram-se nisso.
No entanto, para compreender os exemplos dados, e poder aplicar o que é ensinado, o leitor
necessita de compreender algumas coisas básicas primeiro, respectivamente:

Controlo da funcionalidade do micro-controlador – os registers.


Pseudo-código/código esqueleto
MACROS
Variáveis volatile
Operações bit-wise em C.

É assumido que o leitor sabe programar em C e que domina os seguintes conceitos: comentários,
bibliotecas, variáveis, funções, ponteiros, ciclos, condições, lógica e bases numéricas.

Controlo da funcionalidade do micro-controlador – os registers


Os AVR têm várias funções: podem ser usados para comparar e ler diferenças de potencial,
comunicar por serial, …
Todas estas funções são controladas por registers … mas o que são registers?
Todos os CPUs têm uma certa memória interna. Esta funciona quase como a memória ram,
excepto no uso de ponteiros.
O CPU tem acesso directo a esta memória, o que significa que em termos de performance é
muito mais eficiente usar registers para armazenamento do que memória ram (o compilador em C
optimiza automaticamente os programas, dando uso deste “boost” na performance sempre que
possível – daí a importância de usar variáveis volatile quando se usam interrupções, estudadas mais
à frente). No entanto, estes não são só usados para armazenamento, mas também para controlar
várias funções dos micro-controladores. Certos bits em certos registers podem controlar o estado de
um pino, ligar e desligar o ADC, … Nos AVR todos os registers têm o tamanho de 8 bits. Logo,
quando é necessário armazenar valores maiores que 255, usam-se mais do que um register. No
entanto, este pormenor é abstraído pelo compilador, visto que podemos muitas vezes aceder a um
conjunto de registers como se fosse um só (como por exemplo, o register TCNT1 do timer1 que
corresponde a dois registers, visto que pode conter um valor de 16 bits).
Agora que sabemos o que é um register, vamos aprender como usá-los.
As bibliotecas do avr dão-nos um header muito útil que nos permite aceder directamente aos

Última revisão: 21/12/2010 6


registers e bits dos mesmos através dos seus nomes: avr/io.h
Um exemplo:
Para alterar o estado de um pino, alteramos o bit correspondente no register DDRx (em que x
corresponde à porta. Por exemplo, o pino PB1 está na porta B, logo para alterar o seu estado,
alteramos o bit PB1 no register DDRB). Logo, utilizamos o código seguinte:

#include <avr/io.h>

int main(void) {
DDRB |= (1<<PB1); }

(quando alteramos o bit para 1, estamos a colocar o pino em output)


Se não compreende exactamente como alterámos um bit no register, não se preocupe, pois as
operações bit-wise serão explicadas de seguida.

Pseudo-código/código esqueleto
O pseudo-código é basicamente uma representação abstracta do código, em linguagem natural.
Muitas vezes começa-se por escrever o pseudo-código, e depois vai-se substituindo por linhas de
código (muitas vezes o pseudo-código transforma-se nos comentários). Irei usar isto nos meus
tutoriais para ir construindo os programas passo-a-passo.
Por exemplo, o famoso programa “Hello World”, feito passo-a-passo:

// Iniciar o programa
// Escrever “Hello World no Ecrã”
// Terminar o programa

Primeiro, fazemos o mais simples: iniciar e terminar o programa. Como vamos precisar de
funções Input/Output, parte da inicialização é incluir o header stdio.h, o resto é começar a função
main(), e terminamos com return 0 (sair do programa com sucesso – visto que nos AVRs não existe
sistema operativo, a função main nunca fará um return, apenas acabará num loop infinito):

// Iniciar o programa:
#include <stdio.h>

int main(void) {
// Escrever “Hello World” no Ecrã
return 0; } // Terminar o programa

Agora falta a parte funcional do programa: escrever o Hello World no ecrã:

Última revisão: 21/12/2010 7


// Iniciar o programa:
#include <stdio.h>

int main(void) {
printf(“Hello World”); // Escrever “Hello World” no Ecrã
return 0; } // Terminar o programa

MACROS
Em quase todos os programas de C, temos instruções começadas por '#'. Estas não são instruções
em C, mas sim instruções interpretadas apenas pelo pré-processador, antes da compilação. Por
exemplo, quando fazemos “#include <qualquercoisa.h>”, estamos a indicar ao pré-processador para
incluir o conteúdo do ficheiro qualquercoisa.h no nosso programa.
Uma MACRO é uma instrução deste tipo, que se comporta como uma função. São úteis quando
queremos realizar certas tarefas repetidamente, mas não se justifica o custo em performance de
chamar uma função (para quem programa em C++, isto é equivalente ao inline).
Por exemplo, duas macros que costumo usar são as seguintes:

#define max(I,J) ((I)>(J)?(I):(J))


#define min(I,J) ((I)<(J)?(I):(J))

Antes da compilação, o pré-processador substitui todas as declarações de max(x,y) e min(x,y)


pelo código correspondente, sem ser assim necessário chamar uma função (as macros são úteis para
substituir principalmente funções com só uma linha de código). Há vários pormenores envolvidos
na criação de macro (como por exemplo, abusar das parêntesis para proteger o código), mas não
interessam para este tutorial. No entanto, visto que são muito úteis, aconselho os interessados a
pesquisar sobre elas.

Variáveis volatile
Quando declaramos variáveis, podemos controlar certos aspectos de como o código deve acedê-
las. Uma declaração importante quando se programa AVRs, devido à existência de interrupções, é a
volatile. Mais à frente explicarei a importância disto, por agora é apenas importante reter que
quando se declara uma variável como volatile, estamos a informar que o seu valor pode ser alterado
de formas inesperadas, logo deve sempre ir buscar o seu valor actualizado.

Operações bit-wise em C
Muita da programação em micro-controladores consiste principalmente em manipular bits de
certos registers. Para fazer isso, usamos as operações bit-wise que manipulam valores ao nível dos
bits.

Última revisão: 21/12/2010 8


Para quem não compreende bases numéricas, e não sabe o que significa manipular bits,
aconselho a lerem algum livro/tutorial que trate deste assunto. No entanto, explicado de uma forma
breve, é o seguinte:

Normalmente usamos a base decimal. Isto significa que usamos 10 dígitos diferentes (do 0 ao 9).
Com combinações deles, fazemos diferentes números. Quando queremos um valor acima do dígito
maior, transportamos mais um para a posição seguinte (se contarmos desde a direita). Assim
podemos dar valores a cada posição no número.

Por exemplo, o número 29 tem um 9 na posição 0 e um 2 na posição 1. A posição 0 corresponde


ao valor 10⁰ (1), e a 1 ao valor 10¹. Assim, podemos chegar ao número através da conta: 2*10¹ +
9*10⁰.

Números de base binária funcionam da mesma forma que os de base decimal, com a
particularidade de apenas utilizarmos dois algarismos: o 0 e o 1. Assim, cada posição tem um valor
diferente. Por convenção, chamam-se às posições de um número em base binária de bit. Assim,
quando falamos em manipular bits, estamos a falar em manipular o valor (0 ou 1) de certas
posições. Por exemplo, o número 1001 (para facilitar a leitura, costumam-se ler os dígitos
separados. Assim, em vez de se ler “mil e um”, lê-se “um zero zero um”) corresponde ao número
em decimal 9. Isto porque o bit 0 tem o valor de 1 (2⁰) e o bit 3 tem o valor de 8 (2³). Logo, como
esses são os únicos bits com dígitos lá, chegamos ao 9 através da conta: 1*2⁰+1*2³.

Agora que já conhecemos a base binária, e o que significa manipular bits, vamos ver como
podemos manipulá-los.

Isto é feito através de operações bit-wise.

Em C, existem cinco operações bit-wise:

| – or

& – and

~ – not

^ – xor

<< – shift left

>> – shift right

As duas primeiras operações funcionam como as operações lógicas ||, &&. No entanto, em vez
de testarem a variável como um todo lógico, testam bit a bit, e o resultado corresponde a essa
comparação bit a bit. Por isso, enquanto temos resultados bem definidos com as operações | e &, as

Última revisão: 21/12/2010 9


operações || e && podem dar um valor aleatório para verdadeiro. Assim, quando se necessitam de
valores lógicos, devem-se usar as operações || e &&, e para manipulação bit a bit, devem-se usar as
operações | e & (nota: as operações & e && podem ter resultados diferentes).

Vamos então começar por estudar essas duas operações:

O or retorna 0 quando ambos os bits são 0, e 1 quando pelo menos um dos bits é 1. Olhemos para
um exemplo:

111000 | 001110 = 111110

Vamos analisar isto bit a bit. Em ambos os números, o bit 0 tem o valor 0. 0 ou 0 = 0. Logo, o bit
0 do resultado será um 0. No bit 1, o primeiro número tem um 0, mas o segundo tem um 1. 0 ou 1 =
1. Logo, o resultado terá um 1 no bit 1. A mesma coisa ocorre com o bit 2. No bit 3, ambos os
números têm um 1. 1 ou 1 = 1. Logo, o resultado terá um 1 no bit 3. Nos restantes bits, o primeiro
número tem um 1, e o segundo tem um 0. 1 ou 0 = 1. Logo os restantes bits (bits 4 e 5) terão um 1
no resultado. Assim, chegamos ao número 111110.

Podemos usar isto para colocar o valor 1 numa certa posição num número.

Por exemplo, temos o número 1101 (em decimal é o número 13), e queremos preencher aquele 0
com um 1. Se fizermos um ou com o número 0010 (em decimal é o número 2), preenchemo-lo.
Vejamos um exemplo:

#include <stdio.h>

int main(void) {
int i = 13;
i = i|2; // Equivalente a fazer i |= 2
printf(“%d\n”, i); // Imprime o número 15 – em binário
1111.
return 0; }

Vamos agora observar a operação &.

O and retorna 0 quando pelo menos um dos bits é 0, e 1 quando os dois bits são 1.

Por exemplo:

1101 & 0111 = 0101

A análise deste exemplo será deixada como um desafio ao leitor.

Última revisão: 21/12/2010 10


O & é muitas vezes usado para colocar a 0 um certo bit.

Por exemplo: se tivermos o número 10111 (em decimal 23), e quisermos a partir dele obter o
número 10101 (em decimal 21), podemos fazer a seguinte operação: 10111 & 1101 = 10101:

#include <stdio.h>

int main(void) {
int i = 23;
i = i&13; // 13 – em binário 1101; equivalente a i &= 13;
printf(“%d\n”, i); // Imprime 21
return 0; }

A terceira operação, ~ (not), também tem um comportamento semelhante ao seu equivalente


lógico, o !. No entanto, foi separado das outras duas operações, pois esta não pode ser usada como
uma operação lógica, visto que ~(true) pode dar um valor verdadeiro à mesma (interessantemente,
devido à forma como a aritmética dos CPUs funcionam, fazer o ~ de qualquer número positivo dá
um número negativo e vice-versa, sendo a única excepção o -1, já que ~(-1) = 0. Não
aprofundaremos mais isto, visto que não interessa muito para programar micro-controladores).

Vejamos como funciona:

~1101 = 0010

Cada bit do número original é invertido, logo a partir de um número positivo (true), podemos
não obter 0 (false), que é exactamente o que o ! lógico faz.

O ~ é muitas vezes utilizado em conjunção com o & para pôr um valor 0 num bit. Vejamos
porquê:

11101 & 10111 = 10101

Sabendo a posição do bit que queremos pôr a 0 (neste caso a posição 4), como chegamos ao seu
inverso, de forma a manter o resto do número intacto.

Usando o ~, claro!

Neste caso, fazer:

11101 & 10111 = 10101

é igual a fazer:

Última revisão: 21/12/2010 11


11101 & (~01000) = 11101 & 10111 = 10101

(mais à frente iremos estudar como criar um número com apenas um 1 na posição pretendida,
sabendo apenas essa posição).

Por exemplo, com código agora (reformulação do exemplo do &):

#include <stdio.h>

int main(void) {
int i = 23;
i &= ~(8); // 8 – 01000
printf(“%d\n”, i); // Imprime 21.
return 0; }

O “exclusive or”, ou como é melhor conhecido, o xor, não tem um equivalente lógico óbvio. É o
mesmo que !=. O seu comportamento é o seguinte: retorna 0 quando ambos os números são iguais,
e 1 quando são diferentes. Como o |, quando se procura um resultado lógico, é equivalente usar o ^
e o !=.

Vejamos então um exemplo

1101 ^ 0101 = 1001

O xor é muitas vezes usado para fazer “toggle” (alterar o valor de 0 para 1 e vice-versa) de um
certo bit. Por exemplo, se tivermos um número 11x1, e quisermos alterar o estado do bit 1, sem
conhecermos o seu valor, basta fazer a seguinte operação:

11x1 ^ 0010

Isto porque quando fazermos um xor com 0, o resultado é sempre igual ao do outro número (1^ 0
= 1; 0^0 = 0), e quando fazemos um xor com 1, altera sempre (1^1 = 0; 1^0 = 1).

(visto que o código de exemplo seria semelhante aos anteriores, iremos passar à frente desse
passo).

Agora só nos falta estudar os operadores de shift.

Estes são muito úteis porque nos permitem pôr um valor em qualquer posição do número, ou
seja, fazer shift para cima ou para baixo desse mesmo valor.

Vamos utilizar o exemplo do ~ e do &. Sabendo apenas a posição em que queremos pôr o 0, e o

Última revisão: 21/12/2010 12


número que tem essa posição a 1, e as restantes a 0, já sabemos que operação utilizar.

Mas ainda nos falta uma coisa: como chegamos ao número que tem a posição desejada a 1?

Para isso usam-se os operadores de shift.

Por exemplo, se quisermos colocar o 1 na posição 3, fazemos o seguinte:

1<<3 = 1000

#include <stdio.h>

int main(void) {
int i = 23;
i &= ~(1<<3); // 1<<3 = 8 – 01000
printf(“%d\n”, i); // Imprime 21.
return 0; }

Esta técnica também é utilizada para chegar aos valores utilizador com o or e o xor, sabendo
apenas os bits que queremos, respectivamente, colocar a 1, ou alterar o valor.

Também existe o operador de shift >>, que faz o contrário do <<. Por exemplo:

111>>2 = 1

Mas é menos usado quando se programa micro-controladores.

É de notar que qualquer overflow é completamente esquecido.

Por exemplo, se considerarmos um limite de 5 bits:

10111<<3 = 11000

10111>>3 = 00010

Uma pequena curiosidade: dadas as características das bases numéricas, fazer <<x, é equivalente
a multiplicar por 2^x, e fazer >>x é equivalente a dividir por 2^x.

E assim terminamos o nosso tutorial acerca das bases de programação necessárias para
programar micro-controladores. Esperamos que o leitor esteja agora preparado para se aventurar no
mundo da programação “low-level” dos mesmos!

Última revisão: 21/12/2010 13


"Excerto do "Micro Tutorial AVR" de Njay (http://embeddeddreams.com/users/njay/Micro Tutorial
AVR - Njay.pdf) com alterações/adaptações de Cynary (formatação e conteúdo)"

GPIO – General Purpose Input/Output


O conceito de GPIO surge como uma forma de se tornar um chip mais flexível, e deve ter
surgido com os chips programáveis. Este conceito consiste em podermos configurar um pino de um
chip para poder ter uma de entre várias funções, como por exemplo uma entrada ou uma saída. Isto
tem vantagens óbvias na flexibilidade de um chip, pois o fabricante dá-vos um chip com N pinos
em que vocês escolhem a função de cada um conforme as necessidades da vossa aplicação. Se a
função de cada pino fosse sempre fixa, os chips seriam muito menos úteis, e provavelmente
teríamos chips maiores, com muito mais pinos, numa tentativa de colmatar essa limitação, e não
poderíamos alterar a função do pino durante o funcionamento do chip.

A função de base que podemos escolher para um GPIO é se o respectivo o pino é uma entrada ou
saída, mas não é a única. Existem outras funções que podem ser escolhidas, embora nem todos os
chips suportem todas. As funções possíveis mais comuns são:

Normalmente dizemos apenas "GPIO" quando nos estamos a referir a um "pino GPIO" e eu
assim farei daqui para a frente. Vamos ver com mais detalhe cada função, a que às vezes também
chamamos "tipo de pino".

Entrada Digital Normal


Esta é talvez a configuração mais simples que podemos ter. O pino funciona como uma entrada
digital, ou seja, só podemos ler (em software) um de 2 valores: 0 ou 1. Na prática os valores 0 e 1
representam uma certa tensão que é aplicada ao pino, resultando numa leitura de 0 ou 1 por parte do
software. As tensões mais comuns são 0V para representar um 0 e 5V para representar um 1, mas
podem ser outras como por exemplo 3.3V ou 1.8V para representar um 1, dependendo da tensão de
alimentação do chip (refiro apenas "chip" porque não são apenas os micro-controladores que têm
GPIOs; por exemplo as FPGA, outro tipo de chip programável, também têm).

Última revisão: 21/12/2010 14


Portanto, num sistema que funcione com uma tensão de alimentação de 5V, se aplicarmos 5V a
um pino configurado como "entrada digital normal", o software irá ler um valor 1 desse pino. Se
aplicarmos 0V, o software irá ler um 0. A leitura do "estado do pino" é habitualmente efectuada
lendo-se um registo do chip. Falaremos mais sobre isto no final.

Configurar um GPIO como entrada digital normal também serve como forma de desligar o pino
do circuito. Neste caso não estamos interessados em ler valores. Ao configurá-lo como entrada, ele
não afecta electricamente (de um ponto de vista digital) o circuito exterior ao chip e portanto é
como se tivéssemos cortado o pino do chip. Diz-se que o pino está em "alta impedancia" ("high-Z"
em inglês, pois o "Z" é muito usado para designar "impedancia"), "no ar", ou simplesmente
"desligado do circuito".

Normalmente dizemos apenas que um pino está "configurado como entrada" ou como input.

Entrada com “pull-up” (“puxa para cima”)


Então se tivermos um pino configurado como input mas não lhe aplicarmos nenhuma tensão, que
valor lemos no software?... A resposta é: não podemos prever. Tomem bem atenção a isto, vou
repetir: não podemos prever. Quando temos uma entrada que está no ar, não podemos prever que
valor vamos ler; o valor pode estar estável em 0 ou 1 ou pode estar sempre a variar, ou mudar de
vez em quando consoante é dia ou noite ou Marte está alinhado com Júpiter ou o vizinho deitar-se 2
minutos mais cedo ou mais tarde. Ele pode até mudar só de lhe tocarem com o dedo.

Em algumas situações queremos ter sempre um valor estável na entrada. Um caso tipico é um
interruptor (que pode ser um botão). Conectamos o interruptor entre a massa (o negativo da tensão
de alimentação, "0V") e o pino. Aí, quando ligamos o interruptor (posição "ON"), o pino fica ligado
aos 0V e portanto o chip lê um 0. Mas, e quando o interruptor está desligado? Aí o pino está no ar
pois o interruptor desligado é um circuito aberto, e já sabemos que ler um input que está no ar dá-
nos um valor aleatório e portanto nunca vamos saber se o interruptor está mesmo ON ou OFF. É
aqui que o pull-up entra; ao configurarmos o pino com pull-up, o chip liga internamente uma
resistência entre o pino e a tensão de alimentação, e portanto, quando não há nada electricamente
ligado ao pino, o pino "vê" um 1. No caso do interruptor, quando este está OFF, o pull-up coloca um
1 estável à entrada do pino e fica resolvido o problema. Quando o interruptor está ON, o próprio
interruptor força um 0 no pino, ligando-o à massa.

Então mas... se o pull-up puxa o pino "para cima" e o interruptor (quando está ON) puxa para
baixo, não há aqui uma espécie de conflito? Não há, por uma simples razão: o valor da resistência
de pull-up é alto (tipicamente mais de 100 KOhms) e portanto tem "pouca força". Como o
interruptor liga o pino directamente à massa, é esta que "ganha". Diz-se até que o pull-up é um

Última revisão: 21/12/2010 15


"pull-up fraco", ou "weak pull-up" em inglês.

Esta expressão pull-up ("puxa para cima") vem de estarmos a ligar à tensão de alimentação
positiva, que é mais "alta" do que a massa, os 0V. Para este termo contribui ainda o facto de
geralmente se desenhar a linha de alimentação positiva no topo dos esquemas, e a massa em baixo.
Também podemos falar em pull-down ("puxa para baixo") quando nos referimos a ligar à massa.
Podemos criar pull-downs ligando resistências à massa, mas tipicamente os chips não suportam este
tipo de pull, por razões que fogem ao âmbito deste artigo que se quer simples.

Entrada controlada por um periférico


Neste caso deixamos de ter controlo sobre o GPIO, passando esse controle para um periférico
interno do chip. Por exemplo a linha Rx (recepção) de uma porta série (UART). Aqui o pino
funciona como uma entrada mas quem a controla é o periférico.

Saída Digital Normal


Na lógica CMOS, com a qual trabalhamos mais hoje em dia, as saídas digitais são baseadas
numa topologia designada "totem-pole". Este tipo de saída é constituído por 2 interruptores
electrónicos (transístores) um em cima do outro, e controlados de forma a que:

1. quando queremos ter um "zero" à saída, liga-se o transístor de baixo, que liga o pino à massa
(0V); o transístor de cima mantém-se desligado

2. quando queremos ter um "um" à saída, liga-se o transístor de cima, que liga o pino à tensão
se alimentação (+V); o transístor de baixo mantém-se desligado

Na imagem acima podemos ver uma saída totem-pole num dos seus 2 estados mais habituais:
quando tem um 0 e quando tem um 1.

Por aqui podemos ver por exemplo porque é que não se devem ligar 2 (ou mais) saídas umas às
outras. Se uma delas estiver com um "1" e a outra com um "0", estamos a criar um curto-circuito na
alimentação, ligando +V à massa. Numa das saídas está ligado o interruptor de cima e na outra está

Última revisão: 21/12/2010 16


ligado o de baixo. Mesmo que isto só aconteça durante um período de tempo muito pequeno
(milisegundos, microsegundos ou menos), vão passar correntes elevadas, fora das especificações
dos chips, e se acontecer regularmente, começa um processo de degradação que leva à falha do chip
em segundos, horas, semanas, meses ou anos.

Uma saída totem-pole tem ainda um 3o estado: "no ar". É outra forma de desligar um pino, mas
que é usada quando o pino é sempre uma saída (não configurável). No caso de um GPIO, este pode
ser configurado como entrada ficando assim desligado do circuito exterior, como vimos atrás.

Saída em Colector Aberto (open colector)


Este tipo de saída surgiu para resolver o problema de não se poder ter 2 ou mais saídas ligadas.
Por esta altura vocês podem estar a pensar "mas porque raio é que haveríamos de querer 2 saídas
ligadas uma à outra?!", e a resposta é simples: pensem por exemplo no I²C, em que temos linhas de
dados bidireccionais. No I²C há vários chips ligados a um mesmo par de linhas e todos eles têm a
possibilidade de transmitir dados nessas linhas. Daí que, de alguma forma, há várias saídas ligadas
entre si.

A saída em colector aberto consiste num simples interruptor electrónico (transístor) capaz de
ligar o pino à massa. Quando o interruptor está ligado a saída é 0, e quando está desligado a saída
é... não sabemos. O pino fica "no ar" e portanto qualquer outro dispositivo exterior ao chip que
esteja ligado ao pino pode lá colocar a tensão que entender. Num bus I²C o que se passa é que existe
uma resistência externa que mantém as linhas com tensão positiva quando nenhum dispositivo está
a transmitir; ou seja, temos um "pull-up fraco". A partir daí, qualquer um dos dispositivos pode
forçar uma das linhas I²C a ter 0V, se activar o interruptor electrónico na sua saída em colector
aberto.

Saída controlada por um periférico


À semelhança do que se passa no caso da entrada controlado por um periférico, neste caso
deixamos de ter controlo sobre o GPIO, passando esse controle para um periférico interno do chip.
Por exemplo a linha Tx (transmissão) de uma porta série (UART). Aqui o pino funciona como uma
saída mas quem a controla é o periférico (no caso da UART, uma saída normal).

Última revisão: 21/12/2010 17


GPIOs na Arquitectura AVR
Nos AVR os pinos estão agrupados em portos com no máximo 8 pinos cada. Os portos têm a
designação de letras, A, B, C, etc, e cada AVR tem um conjunto de portos. Cada pino de um porto
pode ser configurado num de 3 modos:

1. Entrada normal

2. Entrada com pull-up

3. Saída normal

4. Entrada ou saída controlada por um periférico

Um pino entra no 4º modo quando o respectivo periférico é activado, pelo que não vamos
debruçar-nos aqui sobre isso.

A cada porto está associado um conjunto de 3 registos que são usados para configurar, ler e
definir o estado de cada pino do porto individualmente. Cada bit de cada registo está associado ao
respectivo pino do chip.

1. PINx - Lê o estado actual do pino

2. DDRx - Data Direction Register (registo de direcção dos dados)

3. PORTx - Define o estado da saída do porto

O "x" depende da letra do porto, de modo a que temos por exemplo o registo DDRA para o porto
A. Segue-se um diagrama simplificado da lógica associada a cada pino de um porto do AVR, neste
caso exemplificado para o pino 3 do porto B (designado B3):

Todos os pinos do chip têm uma lógica similar a este diagrama.

O registo PINx apresenta sempre o valor lógico ("0" ou "1") que estiver presente num pino
independentemente da configuração. É como se o registo estivesse ali a medir a tensão directamente

Última revisão: 21/12/2010 18


no pino e a reportar o seu valor lógico ao software.

O registo DDRx define, para cada pino do porto, se é uma entrada ou uma saída. Após o reset do
chip, todos os pinos estão configurados como entradas, e portanto é como se todo o chip estivesse
desligado do exterior, tem todos os pinos no ar. Para configurar um pino como saída temos que
colocar a 1 o respectivo bit no registo DDRx.

Se um pino estiver configurado como uma saída (se o respectivo bit no registo DDRx for 1),
podemos então definir o estado da saída com o respectivo bit no registo PORTx.

O PORTx controla ainda o pull-up interno quando um pino está configurado como entrada. Se o
respectivo bit no PORTx estiver a 1, então o pull-up está activado.

Cada AVR tem um certo número de portos. Cada porto pode não ter pinos físicos do chip
associados a todos os seus bits. As datasheets da ATMEL (fabricante dos AVR) apresentam logo na
2ª página o pinout (a atribuição de funcionalidade aos pinos do chip). (A partir daqui, divergimos
um pouco do tutorial original do Njay, visto esse tratar de outro micro-controlador AVR. Neste
documento iremos tratar do atmega168/328 – o AVR do arduino) Vamos pegar na datasheet do
atmega168/328, e o pinout é o seguinte (com a legenda para os pinos do arduino já incluída):

Isto diz-nos que este modelo de AVR tem 4 portos, A, B, C e D. Logo, para configuração dos
pinos, este AVR tem os registos DDRA, PORTA, PINA, DDRB, PORTB, PINB, DDRC, PORTC,
PINC, DDRD, PORTD, e PIND. Os nomes entre parêntesis são os nomes associados aos periféricos

Última revisão: 21/12/2010 19


do AVR quando estes estão ligados; vamos esquecê-los neste artigo.

Configuração dos Portos em Linguagem C


É muito fácil aceder aos registos de configuração dos GPIOs (e não só) com este compilador:
eles têm exactamente os nomes dos registos e usam-se como variáveis. Assim, existe por exemplo a
variável DDRA e podemos escrever instruções como:

DDRA = 0xff; // configurar todos os GPIOs do porto A como saídas


Se quisermos configurar apenas alguns GPIOs, temos disponível a macro _BV(index) que cria
uma máscara de bits para um determinado bit do registo. Esta macro retorna um número de 8 bits
em que apenas o bit de índice index é 1. Exemplos: _BV(0) é 1, _BV(7) é 128 (0x80), _BV(2) é 4.
No entanto, ao longo deste documento, iremos principalmente usar o operador bit-wise de shift, já
que o seu comportamento é igual ao da macro _BV. Por exemplo, _BV(4) = (1<<4).

Agora seguem-se alguns exemplos de configuração. Para configurar apenas o GPIO PA3 como
saída e todos os restantes como entradas,

DDRA = _BV(PA3); // configurar apenas o GPIO PA3 (pino 17)


como saída
e para configurar vários GPIOs como saídas, basta efectuar um OU bit-a-bit

DDRA = _BV(PA3) | _BV(PA6); // configurar os GPIOs PA3 e


PA6 (pinos 17 e 12) como saídas
Depois bastaria colocar no registo PORTB, no respectivo bit (3), o valor que queremos que
"apareça" no pino.

Para ligar o pull-up de um GPIO basta garantir que o respectivo bit está a zero no registo DDRx
e depois colocar a 1 o bit no registo PORTx. Configurar o GPIO PB1 como entrada com pull-up
seria assim:

DDRB &= ~_BV(PB1); // configurar PB0 como entrada


PORTB |= _BV(PB1); // ligar o pull-up
Portanto é muito fácil configurar os GPIOs em C.

Última revisão: 21/12/2010 20


Interrupções

O que é uma interrupção?


Irei agora começar a falar de interrupções a partir do mais básico – o que é uma interrupção?
Uma interrupção é basicamente uma pausa no programa, enquanto o processador trata de outra
coisa mais importante.
Um exemplo da vida real:
http://www.youtube.com/watch?v=A9EP6U0BBrA
Neste caso a interrupção foi o toque com o pato que interrompeu o discurso.

Como funciona uma interrupção no AVR?


Nos AVRs, as interrupções têm várias particularidades, pois é necessário activar as interrupções
globais, as interrupções particulares e criar uma rotina para lidar com cada interrupção.
Cada microcontrolador AVR tem um conjunto de registers cujos bits controlam vários aspectos
do seu funcionamento (por exemplo, no tutorial que coloquei no meu primeiro post, estão
explicados os que controlam os pinos de INPUT/OUTPUT). O mesmo acontece com as
interrupções.
No caso do atmega328, o bit I do register SREG controla as interrupções a nível global. Quando
o seu valor é 1, estas estão ligadas, e vice-versa. No entanto, há uma instrução mais simples para
ligar as interrupções globais do que terem de se lembrar que é no bit I do register SREG, que é
simplesmente “sei” (funciona tanto em assembly como em C, só que em C é uma função incluída
no header <avr/interrupt.h>).
Depois de ligadas as interrupções globais, ainda é necessário ligar interrupções individuais. Para
isso, é necessário encontrar qual o bit que liga certa interrupção (encontra-se na datasheet
facilmente na zona dos registers no capítulo acerca da funcionalidade procurada).
Depois de ligadas as interrupções globais e particulares, o processador procura por mudanças de
estado em bits de certos registers (flags), definidos pelas interrupções individuais. Quando esses bits
tornam-se em 1, a interrupção é gerada (independentemente do que esteja a acontecer, o programa
pára). Mas falta aqui uma coisa … o que acontece quando essa interrupção ocorre? A “Interrupt
Service Routine” (ISR) definida para aquela interrupção é executada. No final disto tudo, a flag fica
com o valor 0 novamente, e o programa continua a sua execução a partir do ponto em que estava.
Nota: Enquanto o processador está a executar a interrupção, as interrupções globais estão
desligadas. Quando acaba-se de executar a interrupção, as interrupções globais são ligadas
novamente.

Última revisão: 21/12/2010 21


Como lidar com uma interrupção no AVR?
Agora que já sabemos como funciona uma interrupção, temos de aprender a programar de forma
a lidar com as mesmas.
Primeiro, iremos começar por definir o pseudo-código:

// Iniciar o programa
// … (código que inicialize variáveis, LEDs, etc. aqui)
// Ligar interrupções particulares
// Ligar interrupções globais
// Definir ISR para lidar com as interrupções
particulares ligadas.

Para lidar com registos e interrupções, iremos precisar dos seguintes headers: <avr/io.h> e
<avr/interrupt.h> (já podemos também adicionar a função main(), e um loop eterno):

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void) {
// … (código que inicialize variáveis, LEDs, etc. aqui)
// Ligar interrupções particulares
// Ligar interrupções globais
for(;;);
}
// Definir ISR para lidar com as interrupções
particulares ligadas.

(como a ISR é uma função, definimos fora do main).


Como expliquei anteriormente, ligam-se as interrupções globais através da função sei()

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void) {
// … (código que inicialize variáveis, LEDs, etc. aqui)
// Ligar interrupções particulares
sei();
for(;;);
}
// Definir ISR para lidar com as interrupções
particulares ligadas.

Neste tópico, vamos ignorar as interrupções individuais, pois ainda não falámos de nenhuma

Última revisão: 21/12/2010 22


interessante, e vamos concentrar-nos nas ISR.
A biblioteca do avr dá-nos uma macro muito útil para definir uma ISR, e tem o nome ISR()
(espertos, não são? xD), com um argumento: o nome do vector da interrupção. O vector da
interrupção é basicamente o que identifica qual a interrupção com que estamos a lidar. Esta
informação encontra-se na página 57 do datasheet que eu tenho (início do capítulo sobre
interrupções/capítulo 9), numa tabela, na coluna Source.
Por exemplo, no tópico a seguir, vamos lidar com a interrupção que ocorre no pino digital 2. Este
pino tem o nome de INT0. Ao olharmos para a tabela, vemos que a source da interrupção é INT0.
Para usarmos isto como argumento para a macro ISR, basta adicionar “_vect”. Assim, o vector é:
INT0_vect:

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void) {
// … (código que inicialize variáveis, LEDs, etc. aqui)
// Ligar interrupções particulares
sei();
for(;;);
}

ISR(INT0_vect) {
// Definir o que fazer quando acontece esta interrupção
}

(para alguns, esta declaração da função ISR pode ser confusa, pois não tem tipo. No entanto,
lembrem-se que é uma macro, e por isso ISR não é realmente o que fica no código final).
Nota: Se notarem, alguns dos Vectores na datasheet têm espaços ou vírgulas no nome. Basta
substituir esses por _. Por exemplo, para a interrupção gerada quando se recebem dados por serial,
temos o seguinte Source: USART, RX. O argumento que usamos para a macro ISR é:
USART_RX_vect.

Exemplo de interrupção através do pino digital 2 (INT0)


Vamos agora fazer algo mais interessante, e codificar uma interrupção.

Comecemos com o código do tópico anterior:

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

Última revisão: 21/12/2010 23


int main(void) {
// … (código que inicialize variáveis, LEDs, etc. aqui)
// Ligar interrupções particulares
sei();
for(;;);
}

ISR(INT0_vect) {
// Definir o que fazer quando acontece esta interrupção
}

Vamos usar para este efeito o pino 2 (podia ser feito com o pino 3 também, com poucas
diferenças).
O objectivo deste código vai ser mudar o estado de um LED quando se toca num botão ligado ao
pino 2. O LED vai estar ligado ao pino digital 4 (PD4).
Vamos começar por criar uma variável global, com o estado do pino e inicializar esse pino como
output (vai começar desligado) (para mais informações sobre GPIOs, ler o tutorial que pus no
primeiro post), e já vamos colocar o código necessário para fazer toggle ao pino na ISR.

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

char output = 0; // Estado do led.

int main(void) {
DDRD |= (1<<PD4); // Inicializar o pino digital 4 como
output.
PORTD &= ~(1<<PD4); // Inicializar o pino digital 4 como
desligado.
// Ligar interrupções particulares
sei();
for(;;);
}

ISR(INT0_vect) {
// Definir o que fazer quando acontece esta interrupção
output = ~output; // Alterar o estado
PORTD &= ~(1<<PD4); // Desligar o pino – isto é
necessário para quando o output é 0 se poder desligar.
PORTD |= ((output&1)<<PD4) // output&1 pois só nos
interessa o primeiro bit, assim evitamos mexer nos outros
pinos.
}

Agora só nos falta mesmo inicializar a interrupção particular para o INT0.

Última revisão: 21/12/2010 24


Ao pesquisarmos na datasheet, podemos observar um pormenor acerca dos interrupts externos
INT0 e INT1: eles podem ser ligados por 4 estados diferentes: quando o pino está low, quando o
pino transita para high, quando o pino transita para low e quando o pino muda de estado (low-high e
vice-versa). Como estamos a usar um botão, o mais fácil é que este ligue o pino à corrente,
colocando-o em HIGH. Assim, queremos gerar o interrupt quando o pino transita para HIGH.
O estado escolhido para o INT0 está nos bits ISC00 e ISC01 do register EICRA (para o pino
INT1, está nesse mesmo register, mas nos bits ISC10 e ISC11). O estado que desejamos
corresponde a colocar ambos os bits em 1. Logo, adicionamos isso ao código:

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

char output = 0; // Estado do led.

int main(void) {
DDRD |= (1<<PD4); // Inicializar o pino digital 4 como
output.
PORTD &= ~(1<<PD4); // Inicializar o pino digital 4 como
desligado.
EICRA |= ((1<<ISC00) | (1<<ISC01)); // Configurar
interrupção no pino INT0 para quando este transita para
HIGH
// Ligar interrupções particulares
sei();
for(;;);
}

ISR(INT0_vect) {
// Definir o que fazer quando acontece esta interrupção
output = ~output; // Alterar o estado
PORTD &= ~(1<<PD4); // Desligar o pino – isto é
necessário para quando o output é 0 se poder desligar.
PORTD |= ((output&1)<<PD4) // output&1 pois só nos
interessa o primeiro bit, assim evitamos mexer nos outros
pinos.
}

Agora só nos falta mesmo ligar a interrupção associada ao INT0. O bit que controla isto é o
INT0 no register EIMSK. Assim, é só modificar o código, e fica completo:

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

char output = 0; // Estado do led.

Última revisão: 21/12/2010 25


int main(void) {
DDRD |= (1<<PD4); // Inicializar o pino digital 4 como
output.
PORTD &= ~(1<<PD4); // Inicializar o pino digital 4 como
desligado.
EICRA |= ((1<<ISC00) | (1<<ISC01)); // Configurar
interrupção no pino INT0 para quando este transita para
HIGH
EIMSK |= (1<<INT0); // Ligar interrupções particulares
sei();
for(;;);
}

ISR(INT0_vect) {
// Definir o que fazer quando acontece esta interrupção
output = ~output; // Alterar o estado
PORTD &= ~(1<<PD4); // Desligar o pino – isto é
necessário para quando o output é 0 se poder desligar.
PORTD |= ((output&1)<<PD4) // output&1 pois só nos
interessa o primeiro bit, assim evitamos mexer nos outros
pinos.
}

Agora temos um código que, ao clicarmos num botão que liga o pino digital 2 e o pólo positivo,
faz toggle do pino digital 4 (nota: visto que não fizemos nada para tratar do bouncing, podem haver
resultados inesperados. No entanto, como isto é só para exemplo, não considerámos muito
importante).
Para experimentarem, podem montar o seguinte circuito:

Cuidados a ter na utilização de interrupções


Vou deixar aqui duas situações a terem em atenção quando estão a lidar com interrupções:
1. O compilador optimiza bastante o código. Isto quase sempre é uma vantagem, no entanto,
por vezes não é. Utilizando o exemplo do tópico anterior, se tivéssemos de aceder a variável output

Última revisão: 21/12/2010 26


no código do main(), não estaríamos a aceder ao valor correcto da variável. Isto acontece porque o
compilador não considera que podemos aceder à função ISR só com aquele código, logo apenas
carrega a variável da memória ram para os registers uma vez, e depois não actualiza o seu valor, que
é alterado na ISR. Para resolver isto, dizemos ao código que a variável output é volatile,
declarando-a assim:
volatile char output;
Isto indica ao compilador que a variável pode ser alterada de formas inesperadas, e por isso deve
sempre actualizar o seu valor da ram.
2. O processador AVR do Arduino funciona a 8 bits. Isto quer dizer que ele só pode lidar com 8
bits de cada vez. Não pode, por exemplo, carregar um int da memória numa só instrução, pois estes
têm 16 bits. Mas as interrupções podem ocorrer em qualquer parte do programa … logo o que
pensam que acontece se tirarmos a primeira metade de um inteiro da memória, e antes de tirarmos a
segunda metade ocorrer uma interrupção que altere essa variável? Resultados não previsíveis
obviamente … Logo, o que podemos fazer para evitar isto? O que podemos fazer é desligar
interrupções nesses blocos de código que não podem ser perturbados (nota: se estiverem a carregar
um char não deve haver problemas, visto ter apenas 8 bits). Isto é feito facilmente com a função
cli() (também no header <avr/interrupt.h>). Depois do código efectuado, basta ligar novamente as
interrupções com sei(). Por exemplo:

//...
int i,j;
for(;;) {
cli();
j = i; // o i é alterado numa interrupção
sei();
// …
}
//...

E com isto acabamos a base das interrupções. Decidi começar com estas, pois nos próximos
tutoriais, explicarei as interrupções individuais de várias funcionalidades.

Última revisão: 21/12/2010 27


Timers

O que são e como funcionam timers?

Timers são, como o nome sugere, utilizados para contar o tempo.


No mundo dos microprocessadores, funcionam da seguinte forma: a partir de uma fonte de
pulsos (por exemplo: o clock do AVR), incrementam uma variável a cada pulso.
Se usarmos uma fonte de pulsos com uma frequência conhecida (por exemplo, o clock do AVR
tem uma frequência de 16MHz), conseguimos contar o tempo. Por exemplo, com um clock de
16MHz, sabemos que ao chegar ao valor 16000000, passou um segundo.

Timers no AVR

O AVR tem três timers: timer0, timer1 e timer2.


Cada um difere em vários aspectos, mas o mais significativo é o número de bits: o timer0 e
timer2 têm cada um 8 bits, e o timer1 16 bits.
Os outros aspectos são os modos que suportam, portas que controlam, etc. No entanto, estes não
afecta muito a sua funcionalidade, visto que para fazer uma mesma coisa em dois timers, só são
necessárias algumas mudanças nos registers e pinos usados.
Mas o número de bits usados afecta bastante a funcionalidade, pois limitam a resolução que cada
timer tem. Com 8 bits, só podemos contar até 255, e com 16 até 65535. Isto quer dizer que, com um
clock de 16MHz, não podemos contar até 1s com nenhum, mas por exemplo, com um clock de
65kHz, conseguimos contar até 1s com o de 16 bits, e não com o de 8 bits. No entanto, quando
atingem o seu limite, os timers não param de contar, apenas começam novamente do zero (isto
significa que é possível utilizar software para contar 1s tanto com os timers de 8 bits e de 16 bits.
No entanto, isto é geralmente fora do ideal, e mais à frente iremos examinar técnicas de como fazer
isto sem necessitar de software).
Os timers podem ser usados em diferentes modos. No AVR, existem três modos: Normal, CTC
(Clear Timer on Compare Match) e PWM (Pulse-Width-Modulation – este tem alguns sub-modos
associados). Neste tutorial iremo-nos concentrar nos modos normal e CTC, e deixaremos o PWM
para o próximo tutorial
Todos os timers oferecem estes três modos. No entanto, os modos CTC e PWM podem ter certos
pormenores na sua utilização/configuração que diferem entre timers. O timer de 16 bits oferece a
funcionalidade total destes modos, enquanto os outros dois oferecem um sub-conjunto dos mesmos.
Cada timer funciona incrementando um valor num certo register (no caso do timer de 16 bits,
esse valor é guardado em dois registers. No entanto, quando programamos em C, podemos acedê-lo

Última revisão: 21/12/2010 28


como se fosse um), até atingir o seu máximo, e depois volta a 0. A frequência com que incrementa
esse valor depende do clock utilizado. Cada timer tem um conjunto de clocks disponíveis. Neste
tutorial vamos utilizar apenas o clock do sistema (clkI/O), e os seus prescalers (um prescaler
corresponde a dividir a frequência original por um certo valor. Os valores disponíveis no AVR são:
1, 8, 64, 256 e 1024. A utilização de um prescaler para o timer não afecta o clock do sistema).
Neste tutorial iremos analisar apenas o timer de 16 bits, visto que é o que nos oferece mais
flexibilidade, tanto em resolução e modos. No entanto, a maior parte das coisas descritas aqui
podem aplicar-se aos outros se tiverem o modo correspondente disponível, e ajustando-se o código
à menor resolução que oferecem e aos seus registers.

Modos Normal e CTC

Antes de começar a explicar os modos, vou introduzir alguns conceitos: TOP, BOTTOM, MAX e
overflow. MAX corresponde ao valor máximo que o timer aguenta. No caso do de 16 bits é 65535.
TOP corresponde ao valor máximo que o timer atinge. Isto depende do modo. BOTTOM é o valor
mínimo que o timer tem, e que é sempre 0 (no entanto, podemos mudar isto artificialmente, através
de software. Não é o mais recomendado, sendo sempre preferível mudar o TOP, por ser mais
eficiente). Overflow é o nome que se dá ao que acontece quando o timer chega a MAX:
sobrecarrega o máximo suportado por 16 bits e volta a 0
Iremos começar pelo modo normal.
O modo normal é o mais simples: o timer simplesmente incrementa o register correspondente ao
mesmo até atingir o seu limite, e nesse momento o register volta a 0.
Neste modo, o TOP corresponde ao MAX.
Agora, o modo CTC.
O modo CTC é um pouco mais complexo, mas também útil. Basicamente, a particularidade deste
modo é que podemos ajustar o TOP. O timer1 permite-nos escolher dois registers como contendo o
valor de TOP: OCR1A e ICR1. Iremos ver o impacto disto mais à frente, quando estudarmos
eventos relacionados com os timers. No entanto, uma coisa a ter cuidado é o seguinte: quando se
escreve um novo valor para os registers OCR1A e ICR1 devemos ou ter a certeza que é um valor
maior que o anterior, ou que o valor do timer é menor que esse valor (cuidado quando se usa um
clock alto, pois durante a escrita, o valor é actualizado) ou que fazemos reset ao timer, pois se
alterarmos o valor de OCR1A ou ICR1, e o timer já tiver ultrapassado o novo valor, vai até MAX e
faz overflow, e aí é que volta a funcionar normalmente.

Como usar um timer no AVR

No AVR, os timers são controlados por um conjunto de registers.

Última revisão: 21/12/2010 29


Para poder explicar mais facilmente como usar um timer, vamos estabelecer um objectivo: criar
uma função que funcione como a função _delay_ms().
Vamos começar com o pseudo-código:

// Iniciar o programa
// Iniciar a função new_delayms(x)
// Iniciar o timer
// Verificar o timer para ver se já se passaram x ms.
// Terminar a função

Já podemos inserir algumas coisas: a declaração da função, o loop em que se vai verificar o valor
do timer e os headers necessários para aceder aos registers: <avr/io.h> (vamos esquecer a função
main neste caso):

//Iniciar o programa
#include <avr/io.h>

void new_delayms(int x) {// Iniciar a função new_delayms(x)


//Iniciar o timer
for(;;) {
//Verificar o timer para ver se já se passaram x ms.
}
} // Terminar a função

Para começar, vamos compreender como se sabe quanto tempo passou, tendo em conta o valor
do timer:
Sabemos que o timer incrementa a sua variável de acordo com uma certa frequência, e que a
fórmula para calcular a frequência é:
f = incrementos/s
Neste caso, visto que queremos os milisegundos, podemos ajustar a fórmula:
f/1000 = incrementos/ms
Nesta fórmula, o único valor que podemos conhecer do início é a frequência (como vamos usar o
clock do sistema, será 16MHz), logo podemos já ajustar essa parte da fórmula:
16000 = incrementos/ms
O que queremos descobrir é quantos incrementos, logo ajustamos para:
incrementos = 16000*ms
E temos uma forma para descobrir quantos ms passam, se começarmos do 0 no timer.
No entanto, muitos estão a pensar numa coisa, provavelmente: limites.
Sabemos que o limite para 16 bits é: 65535, logo, no máximo podemos medir:
65535 = 16000*ms <=> ms = 65535/16000

Última revisão: 21/12/2010 30


<=> ms ~= 4
4ms (isto tem algum erro, mas vamos ignorá-lo para manter o exemplo simples)! Isso é longe do
ideal, e se quisermos medir 6, 7 ou 8 ms?
Temos duas formas para aumentar esta resolução: utilizar prescalers, que diminuem o clock, ou
manipulação por software.
A utilização de prescalers é apropriada para casos particulares. No entanto, a manipulação por
software é mais apropriada aqui, visto que nos permite calcular o tempo passado para qualquer
valor possível de int (para fazermos o mesmo com um timer de 16 bits, necessitaríamos de um
prescaler de 16k, o que não existe).
Vamos então fazer isto passo-a-passo: inicializar o timer.
Os bits de configuração/inicialização do timer (mais respectivamente de selecção de modo e
clock) encontram-se em dois registers: TCCR1A, TCCR1B e TCCR1C.
Para seleccionar o modo, utilizamos os bits WGM10 a WGM13 (espalhados pelos dois registers
). Neste caso, queremos o modo normal, logo pomos esses 4 bits a 0 (como esse é o valor por
defeito, não temos de fazer nada).
Para seleccionar o clock, utilizamos os bits CS10 a CS12, no register TCCR1B. Neste caso
queremos o clock do sistema, sem prescaler. Ao olharmos para a datasheet, vemos que temos de
colocar o bit CS10 com o valor 1.
Nota: O timer começa sem nenhum clock seleccionado, logo não está a incrementar o register
correspondente, e esse é inicializado automaticamente a 0. Assim que seleccionamos um clock, o
timer começa imediatamente a incrementar o register.
Logo, já podemos inicializar o timer:

//Iniciar o programa
#include <avr/io.h>

void new_delayms(int x) {// Iniciar a função new_delayms(x)


TCCR1B |= (1<<CS10); //Iniciar o timer
for(;;) {
//Verificar o timer para ver se já se passaram x ms.
}
} // Terminar a função

O método por software que vamos usar é o seguinte: numa variável guardamos o valor anterior
do timer. Se o novo valor for menor, significa que houve um overflow, e incrementamos uma
variável que nos indica quantas vezes passaram 4ms (aviso: isto apenas se aplica para uma
frequência de 16MHz. Se querem manipular o vosso programa para se adaptar a outras frequências,
podem usar a macro F_CPU, que indica a frequência, e utilizar as fórmulas acima, para descobrir

Última revisão: 21/12/2010 31


quantos ms passam em cada overflow). Depois guardamos o novo valor na variável e vemos quanto
tempo se passou até então (o valor da variável dos 4ms+os milissegundos passados, cuja fórmula é:
ms = valor/16000), e caso tenha passado o tempo desejado ou mais (ao executarmos as instruções
pode passar mais tempo do que o desejado), paramos o ciclo, saindo assim da função.
Os registers onde o timer1 guarda o valor do timer são TCNT1H e TCNT1L (são dois por ter 16
bits). A datasheet descreve a ordem em que estes devem ser acedidos. No entanto em C não nos
precisamos de preocupar com isso, pois o seu acesso é simplificado, utilizando-se o nome TCNT1
como se fosse um só register:

//Iniciar o programa
#include <avr/io.h>

void new_delayms(int x) {// Iniciar a função new_delayms(x)


int times_4ms = 0;
int prev_value;
TCCR1B |= (1<<CS10); //Iniciar o timer
prev_value = TCNT1;
for(;;) {
if(prev_value > TCNT1) times_4ms++; // Incrementar a
variável dos 4 ms caso tenha havido um overflow
prev_value = TCNT1;
if(prev_value/16000 + times_4ms*4 >= x) //Verificar o
timer para ver se já se passaram x ms.
break; // Se sim, sair.
}
} // Terminar a função

E aí temos uma função de delay que funciona com 16MHz. Esta é a forma mais básica de se usar
timers, e através do código usado, parecem-nos ineficientes e básico.
Usados desta forma, timers não são muito úteis … tornam-se realmente úteis quando começamos
a explorar a sua utilização com eventos e interrupções, que são os temas dos próximos tópicos.

Eventos relacionados com timers.

Os timers têm uma série de eventos e interrupções associados aos mesmos Com estes é que se
tornam muito poderosos. Neste tópico, iremos usar principalmente o modo CTC, pois os exemplos
utilizados são os que melhor demonstram a sua utilidade.
Este tópico irá cobrir os eventos relacionados com os pinos OC1A, OC1B, e ICP1.
Os timers permitem-nos escolher um evento que pode ocorrer nos pinos OC1A/OC1B, quando o
valor do TCNT1 equivale a OCR1A/OCR1B. Os eventos possíveis dependem do modo escolhido.
Os possíveis para os modos normal e CTC são equivalentes, logo serão os explorados neste tutorial.
Para explorarmos o que podemos fazer com estes pinos, vamos primeiro estabelecer um

Última revisão: 21/12/2010 32


objectivo: piscar um LED (ligado 1s, desligado 1s, ligado 1s, desligado 1s, …), sem utilizar
qualquer controlo por software/interrupções, apenas as configurações dos pinos. O pino usado neste
exemplo será o pino OC1A (pino digital 9).
Logo, o nosso pseudo-código será assim:

// Iniciar o programa
// Configurar o pino digital 9 como output (iniciar como desligado – valor por defeito
// Configurar o timer
// Configurar os eventos do pino OC1A
// Loop eterno

Já podemos preencher algumas coisas:

// Iniciar o programa
#include <avr/io.h>

int main(void) {
DDRB |= (1<<PB1); // Configurar o pino digital 9 como
output (iniciar como desligado – valor por defeito
// Configurar o timer
// Configurar os eventos do pino OC1A
for(;;); // Loop eterno
}

Agora, para configurar o timer, temos de fazer algumas contas …


Queremos que o LED se ligue e desligue a cada segundo. Para fazer isto, sem ajuda de software,
temos de usar os eventos no pino OC1A. Cada vez que o timer chega ao valor OCR1A, ocorre um
evento. Ao usarmos o modo normal, independentemente de qual o valor de OCR1A, a distância de
tempo em que ocorre esse evento é constante – igual ao tempo que demora para incrementar a
variável do 0 ao MAX (mesmo que ponhamos OCR1A no meio, ele continua a incrementar até
chegar ao MAX, e depois vai do 0 até ao MAX/2).
Se testarmos os prescalers, até encontramos um que faz com que esse tempo seja perto de 1s
(com o prescaler 256, 1s = 62500 iterações, MAX = 65535, nota), mas com um erro grande que
aumenta em cada evento, logo não é o ideal.
As contas feitas são as seguintes:
FREQ = F_CPU/prescaler
F_CPU = 16000000
prescaler = 256

Última revisão: 21/12/2010 33


FREQ = 62500
t = 1s
f = inc/t <=> 62500 = inc/1
<=> inc = 62500
Aqui é que entra a utilidade do CTC. Para fazer o que queremos, usando o prescaler de 256,
queremos fazer reset ao timer, cada vez que chega aos 62500. O modo CTC faz exactamente isso.
Neste caso, 62500 corresponderá ao valor de TOP.
Se forem ver para trás, onde explico este modo, irão notar que eu digo que podemos usar dois
registers como valor de TOP no modo CTC: ICR1 e OCR1A. Como queremos criar um evento no
pino OC1A, usar o register OCR1A como TOP é exactamente o que precisamos (se quiséssemos
gerar o evento no pino OC1B, teríamos de usar ICR1 ou OCR1A como TOP, e colocar o mesmo
valor no register OCR1B).
Então, já podemos configurar o timer: queremos que o clock utilizado seja o clock do sistema
com um prescaler de 256, no modo CTC com OCR1A como TOP e com o valor 62500 como TOP:

// Iniciar o programa
#include <avr/io.h>

int main(void) {
DDRB |= (1<<PB1); // Configurar o pino digital 9 como
output (iniciar como desligado – valor por defeito
// Configurar o timer
TCCR1B |= (1<<WGM12); // Modo: CTC com OCR1A como TOP
TCCR1B |= (1<<CS12); // Clock do sistema com prescaler de
256
OCR1A = 62500; // Valor do TOP, de forma a passar-se um
segundo.
// Configurar os eventos do pino OC1A
for(;;); // Loop eterno
}

Agora só nos falta configurar os eventos no pino OC1A.


Os eventos nos pinos OC1A/OC1B são configurados através dos bits COM1A0/COM1B0 e
COM1A1/COM1B1 no register TCCR1A. O evento que desejamos é um toggle do pino OC1A.
Ao consultarmos a datasheet, vemos que esse evento corresponde a COM1A0 = 1 e COM1A1 =
0:

// Iniciar o programa
#include <avr/io.h>

int main(void) {
DDRB |= (1<<PB1); // Configurar o pino digital 9 como

Última revisão: 21/12/2010 34


output (iniciar como desligado – valor por defeito
// Configurar o timer
TCCR1B |= (1<<WGM12); // Modo: CTC com OCR1A como TOP
TCCR1B |= (1<<CS12); // Clock do sistema com prescaler de
256
OCR1A = 62500; // Valor do TOP, de forma a passar-se um
segundo.
TCCR1A |= (1<<COM1A0); // Configurar os eventos do pino
OC1A
for(;;); // Loop eterno
}

E aí têm: um programa que faz toggle do pino OC1A (pino digital 9) a cada 1s. Podem testar isto
com um LED, como no esquema abaixo:

Também podemos forçar o evento que ocorre nos pins OC1A/OC1B escrevendo um 1 para os
bits FOC1A/FOC1B do register TCCR1C. Isto, no entanto, deve ser feito com cuidado, visto que só
altera o estado de output do pino, de acordo com a configuração, não gerando quaisquer
interrupções associadas com uma igualdade ao register OCR1A nem fazendo reset ao timer.
O tipo de eventos que estudámos primeiro, foram os eventos de output. No entanto, também
temos os eventos de input, associados ao pino ICP1 (pino digital 8; na verdade, podemos ter mais
do que esse pino como fonte de input, visto que um evento no analog comparator pode gerar um
evento de input relacionado com o timer. No entanto, visto que o evento comporta-se da mesma
forma, independentemente do input, só considerarem eventos no pino ICP1). Estes eventos são
simples de compreender: quando ou o input transita de HIGH para LOW ou de LOW para HIGH
(este comportamento é definido ICES1 no register TCCR1B: 0 para HIGH-LOW e vice-versa), o
valor no register TCNT1 é escrito no register ICR1. Atenção à forma como isto se funciona: devem
lembrar-se que o register ICR1 pode ser usado como valor de TOP no modo CTC (e PWM também,
mas fica para outro tutorial). Quando isto acontece, estes eventos de input são desligados.
Se se lembram do tutorial anterior, demonstrei como usar um interrupt associado com um pino
digital para detectar um clique num botão. Este tipo de eventos pode ser usado da mesma forma,
mas com uma vantagem: ao colocarmos o bit ICNC1 como 1 no register TCCR1C, o hardware faz

Última revisão: 21/12/2010 35


controlo de bouncing automático, ao testar o valor do pino 4 vezes, a ver se é igual nessas 4 vezes
(nota: esses quatro testes são independentes do prescaler do timer, visto que são feitos de acordo
com o clock do sistema). No próximo tópico, em que falamos sobre interrupções e timers,
demonstraremos uma forma de usar este filtro, em vez da forma básica demonstrada no tutorial
anterior.
E estes são os principais eventos associados aos timers, relacionados com hardware. Para acabar,
só faltam agora as interrupções.

Interrupções e timers.

Existem 4 interrupções associadas com o timer1: uma que ocorre quando se recebe um input,
outras duas que ocorrem quando o valor do register TCNT1 é igual aos valores dos registers
OCR1A/OCR1B e uma que ocorre quando ocorre um overflow do timer (atenção, que no modo
CTC, o overflow ocorre apenas se o timer chegar ao valor MAX, e não ao top, logo pode nunca
ocorrer. Esta interrupção pode ser usada para, por exemplo, verificar a ocorrência de erros quando
se muda o valor de TOP).
Estas interrupções estão todas definidas no register TIMSK1.
Os vectores associados às mesmas são:
TIMER1_CAPT_vect
TIMER1_COMPA_vect
TIMER1_COMPB_vect
TIMER1_OVF_vect
O tutorial anterior mostrou como lidar com interrupções, por isso será deixado à imaginação do
leitor o que fazer com estas. No entanto, a título de exemplo, iremos cumprir o prometido no tópico
anterior: codificar um programa com a mesma função que o do tutorial anterior, só que com
controlo de bouncing feito pelo hardware. Neste caso, não usamos nada relacionado directamente
com o timer, excepto a interrupção de input. No entanto, isto, em conjunção com outros interrupts,
pode ser usado para fazer um programa que registe a distância, em tempo entre dois inputs (para
caber tudo em ints, pode-se criar um int separado para os segundos, minutos e horas – o leitor pode
fazer isto ser mais eficiente de acordo com qualquer critério). Como não usamos a funcionalidade
do timer, não chegamos a seleccionar um timer para o clock, logo, o valor escrito em ICR1 será
sempre 0. Caso o leitor queira usar o timer, tem de seleccionar um clock e modo para o mesmo (o
valor escrito em ICR1 será o valor de TCNT1 na altura do input).
Comecemos com o pseudo-código:

Última revisão: 21/12/2010 36


// Iniciar programa
// Iniciar o pino digital 4 como output, e desligado –
estado por defeito
// Configurar o input para reconhecer eventos LOW-HIGH
// Ligar o filtro para o input.
// Ligar a interrupção para o input.
// Ligar as interrupções globais.
// Loop eterno
// Definir a ISR para o input: TIMER1_CAPT_vect
// Fazer toggle do pino digital 4

Vamos começar pelo mais básico:

// Iniciar programa
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void) {
DDRD |= (1<<PD4); // Iniciar o pino digital 4 como
output, e desligado – estado por defeito
// Configurar o input para reconhecer eventos LOW-HIGH
// Ligar o filtro para o input.
// Ligar a interrupção para o input.
sei(); // Ligar as interrupções globais.
for(;;); // Loop eterno
}

ISR(TIMER1_CAPT_vect) { // Definir a ISR para o input:


TIMER1_CAPT_vect
PORTD ^= (1<<PD4); // Fazer toggle do pino digital 4
}

Se se lembram das minhas explicações anteriores, os bits usados para configurar o evento de
input são ICNC1 (tratar do bouncing automaticamente) e ICES1 (que tipo de evento é registado,
para LOW-HIGH, queremos o valor 1), no register TCCR1B:

// Iniciar programa
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void) {
DDRD |= (1<<PD4); // Iniciar o pino digital 4 como
output, e desligado – estado por defeito
TCCR1B |= (1<<ICES1); // Configurar o input para
reconhecer eventos LOW-HIGH
TCCR1B |= (1<<ICNC1); // Ligar o filtro para o input.
// Ligar a interrupção para o input.
sei(); // Ligar as interrupções globais.
for(;;); // Loop eterno

Última revisão: 21/12/2010 37


}

ISR(TIMER1_CAPT_vect) { // Definir a ISR para o input:


TIMER1_CAPT_vect
PORTD ^= (1<<PD4); // Fazer toggle do pino digital 4
}

Agora só nos ligar a interrupção particular para o input. Como disse antes, as interrupções dos
timers são definidas no register TIMSK1, e o bit que procuramos é o ICIE1:

// Iniciar programa
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void) {
DDRD |= (1<<PD4); // Iniciar o pino digital 4 como
output, e desligado – estado por defeito
TCCR1B |= (1<<ICES1); // Configurar o input para
reconhecer eventos LOW-HIGH
TCCR1B |= (1<<ICNC1); // Ligar o filtro para o input.
TIMSK1 |= (1<<ICIE1); // Ligar a interrupção para o
input.
sei(); // Ligar as interrupções globais.
for(;;); // Loop eterno
}

ISR(TIMER1_CAPT_vect) { // Definir a ISR para o input:


TIMER1_CAPT_vect
PORTD ^= (1<<PD4); // Fazer toggle do pino digital 4
}

E temos um programa que usa interrupções dos timers!


O circuito que usa isto é o seguinte:

Última revisão: 21/12/2010 38


Timers – Parte 2, Pulse Width Modulation

O que é PWM?
Microcontroladores, por si só, são incapazes de gerar sinais analógicos (voltagens variáveis),
apenas podendo gerar dois sinais distintos: 0 e 1 (normalmente, 0V e 5V respectivamente). No
entanto, muitas vezes estes sinais são necessários para controlar vários dispositivos: servos, colunas,

Então, como podemos criar esses sinais? A resposta simples: PWM.
Pensem num carro a andar: se metade de um período de tempo andar a 5 km/h, e a outra metade
estiver parado, qual é a sua velocidade média ao longo desse período? 2,5 km/h.
Passem esta analogia para electricidade: 5V metade do tempo e 0V a outra metade dão 2,5V.
Para “enganarmos” o circuito, de forma a pensar que é mesmo 2,5V constantes, fazemos isto a
uma grande frequência (e se utilizarmos alguns componentes externos, até conseguimos estabilizar
a corrente, mas isso não será discutido aqui).
Vou agora introduzir um conceito ligado ao PWM: o duty cycle. Nem sempre queremos que a
voltagem seja 2,5V, logo deixamos ligados os 5V por mais ou menos tempo de acordo com o que
precisamos. À percentagem de tempo em que os 5V ficam ligados, chamamos de duty cycle. Assim,
quanto maior o duty cycle, maior a voltagem média e vice-versa (assim, duty cycle de 100% = 5V, e
de 0% = 0V).
Então, como podemos utilizar o PWM no AVR? Existem duas formas de gerar um sinal de PWM
no AVR: por software (através de delays ou com o auxílio de interrupções) ou por hardware (que é o
que veremos nesta parte do tutorial).
Um pormenor que convém notar acerca do PWM: ao utilizarem o modo CTC, aconselhei a que
tivessem cuidado ao actualizar os registers ICR1 e OCR1A, quando os seus valores eram o TOP. No
PWM, estes registers podem ser o TOP, mas no modo PWM, o register OCR1A tem um buffer
duplo. Isto significa que ao alterarmo-lo, não o fazemos directamente, mas sim a um buffer. Num
momento definido pelo modo PWM (bits WGM dos registers de controlo do timer), o register
OCR1A é actualizado com o valor nesse buffer. Isto dá-nos maior controlo sobre a frequência, pois
podemos alterar o TOP a qualquer altura (claro que ao utilizarmos o register OCR1A como TOP,
estamos a sacrificar o pino OC1A como output de PWM). Logo, quando queremos variar a
frequência do PWM, devemos usar OCR1A como top, e caso queiramos uma frequência fixa,
podemos usar o ICR1.

Vários Modos de PWM


Ao utilizarmos o AVR, temos várias modos em termos de PWM: Fast PWM, Phase Correct

Última revisão: 21/12/2010 39


PWM e Phase and Frequency Correct PWM.

Os modos Fast PWM e Phase/Phase and Frequency Correct PWM são os que diferem mais.

Fast PWM funciona da seguinte forma: O timer conta até OCR1x, e nesse momento actualiza o
pino OC1x (se o liga ou desliga depende da configuração dos bits COM1x0 e COM1x1). Depois
continua a contar até TOP (que pode ter vários valores: ICR1, OCR1A, 10 bits (1023), 9 bits (511) e
8 bits (255)), e nesse momento volta ao BOTTOM e actualiza o pino OC1x, alterando o seu estado.

Phase Correct e Phase and Frequency correct PWM são muito semelhantes, alterando apenas o
momento em que o register OCR1x é actualizado com o novo buffer. Como o nome sugere, a
vantagem do Phase and Frequency correct PWM é ser mais apropriado para alterar a frequência do
PWM. De resto, estes modos são iguais (devido a isto, apenas discutirei o modo Phase and
Frequency Correct PWM). Funcionam da seguinte forma: o timer conta até ao OCR1x, momento
em que actualiza o pino OC1x (dependendo da configuração, ele liga-o ou desliga-o). Depois
continua até chegar ao TOP, momento em que começa a contar para trás, até atingir novamente o
register OCR1x, momento em que actualiza o pino OC1x, de acordo com a configuração. A
vantagem destes modos é que as ondas geradas têm sempre a mesma fase (o que é necessário para
controlar servos, por exemplo), ou seja, as cristas (parte mais alta da onda) e vales (parte mais baixa
da onda) correspondem, independentemente do duty cycle. A desvantagem que isto traz é que a
frequência mais alta deste modo é metade da possível no modo Fast PWM.

Abaixo estão alguns gráficos que mostram como funcionam estes modos. No modo Fast PWM, o
pino é ligado quando há um compare match, e desligado em bottom. No modo Phase and Frequency
Correct PWM, o pino é ligado quando se conta para cima, e desligado quando se conta para cima
(serão estas as configurações usadas neste tutorial):

Última revisão: 21/12/2010 40


Vamos agora ver como podemos usar estes modos no AVR, através de exemplos:

Fast PWM
O modo Fast PWM é o mais simples de todos, e muito fácil de configurar. Vamos primeiro
definir um objectivo: Fazer variar a luminosidade de um LED, aumentando-a em cada 100 ms (0,1s
), até chegar ao máximo, e depois diminuindo-a, e repetimos este processo até à eternidade.

O pino que vamos usar para este programa é o OC1A (pino digital 9/PB1).

Para isto, basta-nos definir uma interrupção a cada 100ms que altere o valor do register OCR1A,
para definir um maior/menor duty cycle. Para saber se estamos a subir ou a descer, podemos
guardar um 1 ou -1 numa variável, para depois multiplicar pela variação do OCR1A, e alteramos
quando chegamos ao TOP/BOTTOM.

Visto que é impossível gerar um interrupt a cada 100 ms num timer de 8 bits, e estamos a usar o
timer1, podemos fazer isto de duas formas: ou configurar o período do PWM para 100ms, e assim
aproveitar o timer1 para essas interrupções (o que não é a melhor solução visto que assim o LED
piscaria 10 vezes por segundo, o que já se torna visível ao olho humano), ou podemos usar uma
interrupção no timer0/timer2 (iremos usar o 0) a cada 10ms (o que é possível com um prescaler de
1024, com um certo erro que iremos ignorar), e só fazer uma acção à 10ª vez que essa interrupção
ocorre (iremos manter um contador para verificar isso).

O top do timer0 pode ser calculado da seguinte forma (o prescaler usado é 1024):

10ms = 0,01s

TOP = 16000000*0,01/1024 ≃ 156

Para calcular a variação necessária do OCR1A, iremos estabelecer um objectivo: o LED irá de
desligado até luminosidade máxima em 2s.

Para uma frequência alta, iremos usar o Fast PWM com TOP de 8 bits (255).

Assim, OCR1A tem de ir de 0 a 255 em 2s.

Visto que é actualizado a cada 0,1s, ele varia 2/0,1 = 20 vezes.

Logo, a sua variação será: 255/20 ≃ 12 (isto tem um erro associado, mas iremos ignorá-lo).

Vamos então começar com Pseudo-Código:

//Iniciar o programa
// Definir o pino digital 9 como OUTPUT

Última revisão: 21/12/2010 41


// Configurar o timer1 com modo Fast PWM
// Configurar PWM para ligar o pino OC1A no compare
match, e desligar no BOTTOM
// Seleccionar clock no timer1 (sem prescaler, para
máxima frequência).
// Configurar o timer0 com prescaler de 1024, TOP de 156,
modo CTC
// Ligar interrupções de compare match no timer0
// Ligar interrupções globais
// Loop eterno

// ISR de compare match no timer0


// incrementar contador
// Se o contador for igual a 10
// Incrementar/decrementar OCR1A por um factor pré-
estabelecido.
// Caso tenhamos atingido o valor máximo de
OCR1A/BOTTOM
// Alterar a operação de incremento/decremento
// Fazer reset ao contador.

Vamos já começar a preencher o que já sabemos (visto que a configuração do timer0 é


semelhante à do timer1, iremos também fazer isso):

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

#define VARIACAO_OCR1A 12
int mult = -1;
int count;

int main(void) {
DDRB |= (1<<PB1); // Definir o pino digital 9 como OUTPUT
// Configurar o timer1 com modo Fast PWM
// Configurar PWM para ligar o pino OC1A no compare
match, e desligar no BOTTOM
TCCR1B |= (1<<CS10); // Seleccionar clock no timer1 (sem

Última revisão: 21/12/2010 42


prescaler, para máxima frequência).
TCCR0A |= (1<<WGM01);
TCCR0B |= (1<<CS02) | (1<<CS00);
OCR0A = 156; // Configurar o timer0 com prescaler de
1024, TOP de 156, modo CTC
TIMSK0 |= (1<<OCIE0A); // Ligar interrupções de compare
match no timer0
sei(); // Ligar interrupções globais
for(;;); // Loop eterno
}

ISR(TIMER0_COMPA_vect) { // ISR de compare match no timer0


++count; // incrementar contador
if(count == 10) { // Se o contador for igual a 10
OCR1A += mult*VARIACAO_OCR1A; //
Incrementar/Decrementar OCR1A por um factor pré-
estabelecido.
if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso
tenhamos atingido o valor máximo de OCR1A/BOTTOM
mult = -mult; // Alterar a operação de
incremento/decremento
}
count = 0; // Fazer reset ao contador.
}
}

Só nos falta mesmo configurar o PWM agora!

Vamos então ver a datasheet …

O que queremos é o modo Fast PWM, com uma resolução de 8 bits. Vemos lá que conseguimos
isso colocando os bits WGM12 e WGM10, dos registers TCCR1A e TCCR1B respectivamente, a 1:

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

#define VARIACAO_OCR1A 12
int mult = -1;

Última revisão: 21/12/2010 43


int count;

int main(void) {
DDRB |= (1<<PB1); // Definir o pino digital 9 como OUTPUT
TCCR1A |= (1<<WGM10);
TCCR1B |= (1<<WGM12); // Configurar o timer1 com modo
Fast PWM
// Configurar PWM para ligar o pino OC1A no compare
match, e desligar no BOTTOM
TCCR1B |= (1<<CS10); // Seleccionar clock no timer1 (sem
prescaler, para máxima frequência).
TCCR0A |= (1<<WGM01);
TCCR0B |= (1<<CS02) | (1<<CS00);
OCR0A = 156; // Configurar o timer0 com prescaler de
1024, TOP de 156, modo CTC
TIMSK0 |= (1<<OCIE0A); // Ligar interrupções de compare
match no timer0
sei(); // Ligar interrupções globais
for(;;); // Loop eterno
}

ISR(TIMER0_COMPA_vect) { // ISR de compare match no timer0


++count; // incrementar contador
if(count == 10) { // Se o contador for igual a 10
OCR1A += mult*VARIACAO_OCR1A; //
Incrementar/Decrementar OCR1A por um factor pré-
estabelecido.
if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso
tenhamos atingido o valor máximo de OCR1A/BOTTOM
mult = -mult; // Alterar a operação de
incremento/decremento
}
count = 0; // Fazer reset ao contador.
}
}

Agora só nos falta configurar o comportamento do Fast PWM.

Queremos que ele ligue o pino no compare match, e desligue no BOTTOM. Assim, quanto maior
o OCR1A, menor o duty cycle. Logo, como queremos começar com o LED desligado, iniciamos o

Última revisão: 21/12/2010 44


OCR1A com o seu maior valor, e a variável de incremento/decremento com o valor -1.

Para definir o comportamento do PWM para o pin OC1A, temos de mexer nos bits COM1A0 e
COM1A1 do register TCCR1A (colocar os dois a 1 para o que desejamos):

// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>

#define VARIACAO_OCR1A 12
int mult = -1;
int count;

int main(void) {
DDRB |= (1<<PB1); // Definir o pino digital 9 como OUTPUT
TCCR1A |= (1<<WGM10);
TCCR1B |= (1<<WGM12); // Configurar o timer1 com modo
Fast PWM
OCR1A = 20*VARIACAO_OCR1A;
TCCR1A |= (1<<COM1A0) | (1<<COM1A1); // Configurar PWM
para ligar o pino OC1A no compare match, e desligar no
BOTTOM
TCCR1B |= (1<<CS10); // Seleccionar clock no timer1 (sem
prescaler, para máxima frequência).
TCCR0A |= (1<<WGM01);
TCCR0B |= (1<<CS02) | (1<<CS00);
OCR0A = 156; // Configurar o timer0 com prescaler de
1024, TOP de 156, modo CTC
TIMSK0 |= (1<<OCIE0A); // Ligar interrupções de compare
match no timer0
sei(); // Ligar interrupções globais
for(;;); // Loop eterno
}

ISR(TIMER0_COMPA_vect) { // ISR de compare match no timer0


++count; // incrementar contador
if(count == 10) { // Se o contador for igual a 10
OCR1A += mult*VARIACAO_OCR1A; //
Incrementar/Decrementar OCR1A por um factor pré-
estabelecido.

Última revisão: 21/12/2010 45


if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso
tenhamos atingido o valor máximo de OCR1A/BOTTOM
mult = -mult; // Alterar a operação de
incremento/decremento
}
count = 0; // Fazer reset ao contador.
}
}

E com isto, fizemos o nosso primeiro programa que usa PWM!

Este programa pode ser usado com o circuito abaixo:

Phase and Frequency Correct PWM


O modo Phase and Frequency Correct PWM, como mencionei antes, é útil para controlar servos,
devido a manter sempre a mesma fase. Por isso, iremos utilizá-lo para isso mesmo: controlar um
servo.

Primeiro, temos de compreender o que é e como funciona um servo: um servo é um conjunto de


um motor e electrónica que abstraem muito do controlo de motores. Estes permitem-nos, através de
um sinal, especificar para onde queremos que o motor se mova, em graus, ou no caso de servos de
rotação contínua, controlar a sua direcção e velocidade. Logo, esta electrónica abstrai muito daquilo
que precisamos para controlar um motor, como pontes-H e leitura e interpretação do feedback do
motor.

Estes têm três fios: Vcc, GND e controlo. O Vcc e GND devem ser ligados respectivamente aos
terminais positivos e negativos da fonte de alimentação. O controlo será ligado a um pino do AVR,

Última revisão: 21/12/2010 46


que neste caso será o pino digital 9 (OC1A, pois temos um output de PWM nesse).

Para controlarmos o servo, precisamos de compreender que tipo de sinais lê. No geral, os servos
usam um sinal de 50Hz, cujo período em HIGH determina a o ângulo ou direcção e sentido para
controlar o motor. O período em HIGH pode ser entre 1ms e 2ms, sendo 1ms completamente para a
esquerda e 2ms completamente para a direita, e 1.5ms meio. Isto corresponderá a respectivamente
0º, 180º e 90º num servo com capacidade de rodar até 180º (isto não é constante, e pode variar de
servo para servo).

Este sinal pode parecer confuso, mas a imagem abaixo pode ajudar a esclarecer (para um sinal de
1ms – completamente para a esquerda):

Para exemplificar isto, vou definir um objectivo: a criação de uma função que altera a posição do
servo, tendo em conta que este se encontra ligado ao pino OC1A, e que o PWM já foi inicializado
para uma frequência fixa de 50Hz, no timer1 (neste caso, irá receber um valor entre 0 e 100, para
definir a posição entre 1ms e 2ms).

// Iniciar o programa
// Colocar o pino digital 9/OC1A como output.
// Iniciar o timer1 no modo Phase and Frequency Correct
PWM
// Configurar o timer para ligar o pino OC1A na subida, e
desligá-la na descida.
// Seleccionar o clock do timer1.
// Configurar para uma frequência de 50Hz
// Chamar a função rodar, para ir todo para a esquerda
(valor dado: 0).
// Loop eterno.

Última revisão: 21/12/2010 47


// Função rodar(x); x – valor entre 0 e 100
// Converter o valor entre 0 e 100 para um valor em ms
// Converter o valor em ms para um valor de OCR1A, de
acordo com o prescaler e clock.

Como devem ter reparado, este código implica muita matemática.

Primeiro, temos de descobrir como fazer uma frequência de 50Hz:

O clock é 16MHz, logo, a onda terá de ocorrer a cada 16M/50 = 320K ciclos para ter 50Hz.

No entanto, isto não cabe num register de 16 bits, logo usaremos um prescaler. Neste caso 8 é
suficiente: 320K/8 = 40K

No entanto, temos de ter um cuidado especial quando usamos Phase and Frequency Correct
PWM: visto que ele primeiro conta para cima, e depois para baixo, a frequência será metade
daquela obtida em Fast PWM (que nesse caso teria um TOP de 40K para 50Hz). Logo, o TOP será
40K/2 = 20K, para termos a frequência 50Hz.

Como nunca mudamos a frequência, iremos usar um TOP fixo, logo é seguro usar o register
ICR1.

Para converter o valor entre 0 e 100, para um valor entre 1 e 2, basta-nos usar a seguinte fórmula:

(x+100)/100

No entanto, isto não é suficiente para colocar no OCR1A. Visto que iremos usar a configuração
em que o pino é ligado na subida, e desligado na descida, temos de ver quanto tempo demora o
timer a chegar ao TOP desde OCR1A, e a descer novamente, até chegar a OCR1A.

Assim, para x ms, temos:

1 ms = 16000000/8/1000 = 2000

x ms = 2000*x.

OCR1A = 20000-2000*x/2

Logo, podemos chegar à fórmula geral (x estando entre 0 e 100):

OCR1A = 20000-2000*(x+100)/100/2 = 20000-10*(x+100)

Assim, já podemos criar a nossa função rodar (e já agora, adicionamos aquilo que já sabemos
fazer):

Última revisão: 21/12/2010 48


// Iniciar o programa
#include <avr/io.h>

void rodar(int);

int main(void) {
DDRB |= (1<<PB1); // Colocar o pino digital 9/OC1A como
output.
// Iniciar o timer1 no modo Phase and Frequency Correct
PWM
// Configurar o timer para ligar o pino OC1A na subida, e
desligá-la na descida.
TCCR1B |= (1<<CS11); // Seleccionar o clock do timer1.
ICR1 = 20000; // Configurar para uma frequência de 50Hz
rodar(0); // Chamar a função rodar, para ir todo para a
esquerda (valor dado: 0).
for(;;); // Loop eterno.
}

void rodar(int x) {// Função rodar(x); x – valor entre 0 e


100
// Converter o valor entre 0 e 100 para um valor em ms
// Converter o valor em ms para um valor de OCR1A, de
acordo com o prescaler e clock.
OCR1A = 20000-10*(x+100); }

Agora vamos iniciar o timer1 no modo Phase and Frequency Correct PWM com o TOP em ICR1
(neste caso, bastava o modo Phase Correct PWM, mas são o dois semelhantes o suficiente para não
fazer diferença em nada :P Caso queiram usar, basta ver na datasheet as diferenças), e segundo a
datasheet, faz-se isso colocando o bit WGM13 a 1, no register TCCR1B.

// Iniciar o programa
#include <avr/io.h>

void rodar(int);

Última revisão: 21/12/2010 49


int main(void) {
DDRB |= (1<<PB1); // Colocar o pino digital 9/OC1A como
output.
TCCR1B |= (1<<WGM13); // Iniciar o timer1 no modo Phase
and Frequency Correct PWM
// Configurar o timer para ligar o pino OC1A na subida, e
desligá-la na descida.
TCCR1B |= (1<<CS11); // Seleccionar o clock do timer1.
ICR1 = 20000; // Configurar para uma frequência de 50Hz
rodar(0); // Chamar a função rodar, para ir todo para a
esquerda (valor dado: 0).
for(;;); // Loop eterno.
}

void rodar(int x) {// Função rodar(x); x – valor entre 0 e


100
// Converter o valor entre 0 e 100 para um valor em ms
// Converter o valor em ms para um valor de OCR1A, de
acordo com o prescaler e clock.
OCR1A = 20000-10*(x+100); }

E para terminar, configurar o comportamento dos pinos. O que desejamos é alcançado colocando
ambos os bits COM1A0 e COM1A1 a 1, no register TCCR1A:

// Iniciar o programa
#include <avr/io.h>

void rodar(int);

int main(void) {
DDRB |= (1<<PB1); // Colocar o pino digital 9/OC1A como
output.
TCCR1B |= (1<<WGM13); // Iniciar o timer1 no modo Phase
and Frequency Correct PWM
TCCR1A |= (1<<COM1A0) | (1<<COM1A1); // Configurar o
timer para ligar o pino OC1A na subida, e desligá-la na
descida.
TCCR1B |= (1<<CS11); // Seleccionar o clock do timer1.
ICR1 = 20000; // Configurar para uma frequência de 50Hz

Última revisão: 21/12/2010 50


rodar(0); // Chamar a função rodar, para ir todo para a
esquerda (valor dado: 0).
for(;;); // Loop eterno.
}

void rodar(int x) {// Função rodar(x); x – valor entre 0 e


100
// Converter o valor entre 0 e 100 para um valor em ms
// Converter o valor em ms para um valor de OCR1A, de
acordo com o prescaler e clock.
OCR1A = 20000-10*(x+100); }

E aqui temos, uma função para controlar servos!

Podem usar isto com o circuito abaixo:

E com isto fica concluído o tutorial sobre timers.

Última revisão: 21/12/2010 51


Analog-to-Digital Converter

Formato Analógico e Digital


No mundo da electrónica, podemos identificar dois tipos de sinais: os digitais e os analógicos.

Os sinais digitais distinguem-se por poderem ter apenas dois estados (normalmente designados
por desligado/ligado, 0/1, e muitas vezes representados pelas diferenças de potencial 0V e 5V,
respectivamente).

Os sinais analógicos, no entanto, podem ter vários estados. Por exemplo, todos os estados entre
0V e 5V (podem incluir 1V, 2V, …).

Ao usar o PWM, já convertemos de uma certa forma, um sinal digital para um sinal analógico, já
que a diferença de potencial resultante é alterada pelo duty cycle do sinal em formato PWM.

Neste tutorial, iremos estudar o ADC, que basicamente converte um sinal digital para um sinal
analógico.

O que é o ADC?

O ADC (Analog-to-Digital Converter) é uma funcionalidade do micro-controlador AVR que


permite converter uma certa diferença de potencial (entre 0 e um valor de referência) num valor
numérico, de acordo com a resolução pretendida.
O ADC do AVR tem uma resolução máxima de 10 bits. Isto significa que nos pode dar valores
entre 0 e 1023. Se utilizarmos como valor de referência AVcc (o mais comum – um pino do ADC
que deve estar sempre ligado a uma diferença de potencial muito próxima do Vcc do micro-
controlador, normalmente 5V), significa que temos uma precisão de 5/1023 ≃ 0,0049, ou seja, de
cerca de 4,9 mV.
Por exemplo, se o ADC tiver como input uma diferença de potencial de 2,5V, irá retornar o valor
decimal 510.

Como funciona o ADC no AVR?

No AVR, o ADC tem vários pormenores com que nos temos de preocupar: um multiplexer/mux
que nos permite escolher o input (permite escolher até 8 inputs, no entanto no atmega328 utilizado
só temos 6), a possibilidade de utilizarmos auto-triggers (gatilhos automáticos), a escolha da
diferença de potencial de referência e a frequência com que se faz conversões, utilizando um
prescaler.
O atmega328 dá-nos 6 pinos que podemos utilizar como input para o ADC – ADC0 (PC0) a

Última revisão: 21/12/2010 52


ADC5 (PC5). Para seleccionar um destes manipulamos os bits MUXn no register ADMUX
(podemos também ler o input ADC8, que consiste num sensor de temperatura interno, que
discutiremos noutro tópico) – consultar datasheet para saber quais os valores a usar.
Para escolher qual a diferença de potencial utilizada como referência, temos primeiro de
conhecer as opções: AREF (uma diferença de potencial externa, ligada ao pino com esse nome),
AVcc (uma referência interna, que tem de estar próxima do valor de Vcc, normalmente 5V) e uma
referência interna de 1.1V (apesar de parecer inútil para a maior parte dos usos, é utilizada para o
sensor de temperatura interno que explicaremos mais à frente). Podemos seleccionar qual das
opções a usar, manipulando os bits REFSn no register ADMUX – consultar datasheet para saber
quais os valores a usar. Outro pormenor que temos de ter em atenção é que não podemos usar como
referência as diferenças de potencial internas quando aplicamos uma diferença de potencial no pino
AREF.
Com o ADC, para fazer uma leitura, temos de dar uma “ordem” para o fazer. Isto consegue-se
colocando o valor 1 no bit ADSC do register ADCSRA. No entanto, nem sempre queremos ter de
fazer isso para saber o valor de um input, ainda por cima porque essa conversão não ocorre em
apenas um ciclo. Por isso é-nos útil definir gatilhos que iniciem a conversão – as opções que temos
são flags de interrupções, ou seja, funciona como se fosse uma interrupção que ocorre
separadamente do nosso código. Atenção que, caso não lidemos com a interrupção utilizada como
gatilho, nem que seja só definir uma ISR que não faça nada, de forma a desabilitar a flag de
interrupção, a conversão só ocorre na primeira vez que a flag mudar de 0 para 1 (já que nunca
mudamos de 1 para 0 posteriormente) – excepto quando utilizamos free-running mode. Estes
gatilhos funcionam mesmo que não liguemos as interrupções globais e particulares. Neste
documento apenas iremos utilizar o gatilho free-running mode, já que o funcionamento das
interrupções já foi bem explicado, e estes gatilhos funcionam da mesma forma. Para escolher o
gatilho, alteramos os bits ADTSn do register ADCSRB.
O ADC tem mais um pormenor com que nos temos de preocupar: a frequência do clock do ADC.
Para seleccionarmos esta frequência, temos de trabalhar com um prescaler, que funciona muito
como o dos timers: F_CPU/PRESCALER. Segundo a datasheet, para ter a máxima resolução, temos
de uma frequência entre 50kHz e 200kHz. Visto que F_CPU = 16000000, o único valor disponível
que nos dá uma frequência adequada é o 128, dando-nos uma frequência de 125kHz (atenção: se
não desejarem a frequência máxima de 10bits, podem usar um valor mais alto para a frequência, no
entanto, a datasheet não diz mais nada além disto), logo é esse que utilizaremos neste documento.
Agora que já tirámos estes pormenores do caminho, vamos criar um programa de exemplo que
faz uma coisa muito simples: ler um valor do ADC e guardá-lo numa variável (não irá fazer nada
com ele … mais à frente, iremos explorar como fazer um programa mais útil, construindo um

Última revisão: 21/12/2010 53


pequeno sensor de distância analógico) – neste caso iremos usar o pino ADC0.
Comecemos com o pseudo-código:

// Iniciar programa
// Configurar o ADC:
// Prescaler de 128
// Referência AVcc
// Configurar o multiplexer para usar o ADC0
// Ligar o ADC
// Iniciar uma conversão no ADC
// Esperar até que a conversão esteja terminada
// Ler o valor do ADC e guardá-lo numa variável
// Ciclo eterno

Podemos já preencher algumas coisas:

// Iniciar programa
#include <avr/io.h>

int main(void) {
int adc_value;
// Configurar o ADC:
// Prescaler de 128
// Referência AVcc
// Configurar o multiplexer para usar o ADC0
// Ligar o ADC
// Iniciar uma conversão no ADC
// Esperar até que a conversão esteja terminada
// Ler o valor do ADC e guardá-lo numa variável
for(;;); // Ciclo eterno
}

Vamos começar por configurar o ADC. Visto que não iremos usar nenhum gatilho automático,
não nos temos de preocupar com isso. Para configurar o prescaler como 128, temos de colocar os
bits ADPS2, ADPS1 e ADPS0 todos a 1 (register ADCSRA). Para seleccionar como diferença de
potencial de referência o AVcc temos de colocar o bit REFS0 a 1 (register ADMUX). Visto que para
seleccionar o pino ADC0, só precisamos de colocar um 0 nos bits MUX3..0 (register ADMUX), e é
esse o seu valor por defeito, não temos de fazer nada quanto à selecção do pino:

// Iniciar programa
#include <avr/io.h>

int main(void) {
int adc_value;
// Configurar o ADC:
ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); //

Última revisão: 21/12/2010 54


Prescaler de 128
ADMUX |= (1<<REFS0); // Referência AVcc
// Configurar o multiplexer para usar o ADC0 – feito
por defeito
// Ligar o ADC
// Iniciar uma conversão no ADC
// Esperar até que a conversão esteja terminada
// Ler o valor do ADC e guardá-lo numa variável
for(;;); // Ciclo eterno
}

Para ligar o ADC, temos de colocar um 1 no bit ADEN do register ADCSRA:

// Iniciar programa
#include <avr/io.h>

int main(void) {
int adc_value;
// Configurar o ADC:
ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); //
Prescaler de 128
ADMUX |= (1<<REFS0); // Referência AVcc
// Configurar o multiplexer para usar o ADC0 – feito
por defeito
ADCSRA |= (1<<ADEN); // Ligar o ADC
// Iniciar uma conversão no ADC
// Esperar até que a conversão esteja terminada
// Ler o valor do ADC e guardá-lo numa variável
for(;;); // Ciclo eterno
}

Para iniciar uma conversão, temos de colocar um 1 no bit ADSC (register ADCSRA). Este bit é
lido como 1 enquanto se está a processar uma conversão. Quando fica com o valor 0, significa que
conversão terminou. Assim, para esperarmos que a conversão termine, basta testar o valor deste bit
(esta não é a melhor forma de fazer isto – especialmente neste código – em que o melhor seria
utilizar uma variável global e uma interrupção, mas discutiremos isso mais para a frente):

// Iniciar programa
#include <avr/io.h>

int main(void) {
int adc_value;
// Configurar o ADC:
ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); //
Prescaler de 128
ADMUX |= (1<<REFS0); // Referência AVcc
// Configurar o multiplexer para usar o ADC0 – feito
por defeito

Última revisão: 21/12/2010 55


ADCSRA |= (1<<ADEN); // Ligar o ADC
ADCSRA |= (1<<ADSC); // Iniciar uma conversão no ADC
while(ADCSRA&(1<<ADSC)); // Esperar até que a conversão
esteja terminada
// Ler o valor do ADC e guardá-lo numa variável
for(;;); // Ciclo eterno
}

Agora só nos falta ler o valor que resultou da conversão. Este valor é guardado em dois registers:
ADCL e ADCH. Estes têm de ser lidos numa ordem específica (primeiro o ADCL e depois o ADH).
Mas como usamos C, isto é abstraído pelo compilador, bastando-nos ler o register ADC:
// Iniciar programa
#include <avr/io.h>

int main(void) {
int adc_value;
// Configurar o ADC:
ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); //
Prescaler de 128
ADMUX |= (1<<REFS0); // Referência AVcc
// Configurar o multiplexer para usar o ADC0 – feito
por defeito
ADCSRA |= (1<<ADEN); // Ligar o ADC
ADCSRA |= (1<<ADSC); // Iniciar uma conversão no ADC
while(ADCSRA&(1<<ADSC)); // Esperar até que a conversão
esteja terminada
adc_value = ADC; // Ler o valor do ADC e guardá-lo numa
variável
for(;;); // Ciclo eterno
}

Como ligar o input ao AVR?

Apesar de parecer uma pergunta simples, foi uma das coisas com que tive mais dificuldade
quando comecei a utilizar o ADC.
A técnica que utilizei, foi pensar no ADC como se fosse um voltímetro quando o utilizo, que
mede a diferença de potencial entre o ponto onde ligamos o input e o GND (atenção: para se poder
utilizar o ADC correctamente, o input analógico e o AVR têm de partilhar o GND). Isto significa
que não podemos, por exemplo, medir a diferença de potencial de uma resistência, se esta está
mesmo no início do circuito, e ligarmos o pino ao espaço entre o terminal positivo e a resistência.
Se queremos medir a diferença de potencial da resistência, ou colocamo-la no final do circuito ou
medimos a diferença de potencial do resto do circuito (ligado o pino depois da resistência), e depois
é só subtrair à diferença de potencial total (o que não funcionará se não a medirmos/conhecermos).

Última revisão: 21/12/2010 56


Utilizar o ADC – construir um sensor de distância

Neste tópico iremos tentar utilizar o ADC de uma forma


mais prática – através da construção de um sensor de distância
analógico.
Para este sensor, utilizaremos um LED emissor de
infravermelhos, um receptor de infravermelhos (foto-transístor)
e duas resistências, uma de 220 ohms e uma de 10k ohms,
resultando no seguinte circuito:

AIN0 significa o analog pin 0 do arduino, que corresponde ao pino ADC0 do AVR.
Não irei discutir como funciona este sensor de distância, deixando-o como um desafio ao leitor
(nota: isto só funciona para detectar distâncias, quando a cor é constante, e funciona para detectar
cores/reflectividade quando a distância é constante).
Vamos começar por definir um objectivo: queremos que o ADC meça constantemente o input do
sensor de distância, e guarde o valor que esse nos dá numa variável (mais à frente iremos fazer
alguma coisa com esse valor). Para isso, iremos configurar o ADC em free-running mode (este
modo é definido por um gatilho automático em que o ADC está constantemente a realizar
conversões, e em que não nos precisamos de preocupar em colocar flags de interrupções a 0. No
entanto, enquanto nos outros modos não precisamos de iniciar as conversões, neste temos de
“ordenar” ao ADC que faça uma conversão primeiro), e utilizaremos interrupções associadas ao
ADC para actualizar a variável.
Visto que não precisamos de uma resolução de 10 bits, iremos apenas utilizar uma de 8 bits.
Podemos realizar isto de duas formas: ou lemos o valor do ADC de 10 bits, e retiramos os dois bits
da direita, ou podemos utilizar uma função que o ADC nos dá, que é a de alinhar o resultado à
esquerda, guardando os 8 bits mais significativos no register ADCH, e os dois menos significativos
no register ADCL, fazendo com que apenas necessitemos de ler o register ADCH. Para ligar esta
funcionalidade, temos de colocar o valor 1 no bit ADLAR do register ADMUX (nota: visto que
apenas necessitamos de 8 bits, poderíamos utilizar uma frequência maior que 125kHz, mudando
para isso o prescaler, mas não o faremos neste caso).
Visto que ligamos o sensor ao terminal positivo de 5V do arduino, não é necessário utilizar uma

Última revisão: 21/12/2010 57


referência de diferença de potencial externa, utilizando-se assim o AVcc.
Comecemos com o pseudo-código:

// Iniciar programa
// Criar uma variável com o valor do sensor de distância
do ADC
// Configurar o ADC:
// Resolução de 8 bits
// Prescaler de 128
// Referência: AVcc
// Gatilho automático: Free-running mode
// Input: ADC0 – seleccionado por defeito
// Ligar o ADC
// Ligar a interrupção particular do ADC
// Ligar as interrupções globais
// Iniciar as conversões no ADC
// Ciclo eterno
// Fazer algo com o valor da variável do sensor de
distância
// Definir uma interrupção que ocorre a cada ocorrência de
uma conversão
// Ler o valor do ADC, e guardá-lo numa variável

Já podemos preencher algumas coisas:

// Iniciar programa
#include <avr/io.h>
#include <avr/interrupt.h>

// Criar uma variável com o valor do sensor de distância


do ADC
volatile unsigned char adc_distance; // char porque só
precisamos de 8 bits

int main(void) {
// Configurar o ADC:
// Resolução de 8 bits
ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); //
Prescaler de 128
ADMUX |= (1<<REFS0); // Referência: AVcc
// Gatilho automático: Free-running mode
// Input: ADC0 – seleccionado por defeito
ADCSRA |= (1<<ADEN); // Ligar o ADC
// Ligar a interrupção particular do ADC
sei(); // Ligar as interrupções globais
ADCSRA |= (1<<ADSC); // Iniciar as conversões no ADC
for(;;) {// Ciclo eterno
// Fazer algo com o valor da variável do sensor de
distância
}

Última revisão: 21/12/2010 58


}

ISR() {// Definir uma interrupção que ocorre a cada


ocorrência de uma conversão
adc_distance = ADCH; // Ler o valor do ADC, e guardá-lo
numa variável
}

Para resolução de 8 bits, colocamos o bit ADLAR a 1 no register ADMUX. A interrupção que
utilizaremos é a única relacionada com o ADC – a que ocorre quando se realiza uma conversão. O
seu vector é: ADC. Para a ligar, colocamos o valor 1 no bit ADIE do register ADCSRA.
Para seleccionar o gatilho automático de free-running mode, necessitamos primeiro de ligar o
modo de gatilho automático (colocar o valor 1 no bit ADATE do register ADCSRA), e depois
necessitamos de manipular os bits ADTSn no register ADCSRB (para Free-running mode não
precisamos de de fazer nada já que para este modo só temos de colocar os seus valores a 0 – o seu
valor por defeito):

// Iniciar programa
#include <avr/io.h>
#include <avr/interrupt.h>

// Criar uma variável com o valor do sensor de distância


do ADC
volatile unsigned char adc_distance; // char porque só
precisamos de 8 bits

int main(void) {
// Configurar o ADC:
ADMUX |= (1<<ADLAR); // Resolução de 8 bits
ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); //
Prescaler de 128
ADMUX |= (1<<REFS0); // Referência: AVcc
ADCSRA |= (1<<ADATE); // Gatilho automático: Free-
running mode
// Input: ADC0 – seleccionado por defeito
ADCSRA |= (1<<ADEN); // Ligar o ADC
ADCSRA |= (1<<ADIE); // Ligar a interrupção particular
do ADC
sei(); // Ligar as interrupções globais
ADCSRA |= (1<<ADSC); // Iniciar as conversões no ADC
for(;;) {// Ciclo eterno
// Fazer algo com o valor da variável do sensor de
distância
}
}

ISR(ADC_vect) {// Definir uma interrupção que ocorre a


cada ocorrência de uma conversão

Última revisão: 21/12/2010 59


adc_distance = ADCH; // Ler o valor do ADC, e guardá-lo
numa variável
}

E com isto temos um programa que utiliza o ADC em quase todo o seu potencial!
O que foi explicado até agora já permite ao leitor utilizar o ADC. No entanto, os exemplos
apresentados não têm nenhum efeito observável, o que impede o leitor de experimentar. Por isso
vamos preencher o interior do ciclo eterno para fazer alguma coisa com o valor do sensor de
distância.
Basicamente, vamos fazer com que um LED esteja ligado
quando o valor de input seja maior que 2,5V (~128, visto que
usamos uma resolução de 8 bits), e desligado quando é menor.
Para começar, vamos adicionar ao circuito anterior, estas
ligações:

E agora, o código:

// Iniciar programa
#include <avr/io.h>
#include <avr/interrupt.h>

// Criar uma variável com o valor do sensor de distância


do ADC
volatile unsigned char adc_distance; // char porque só
precisamos de 8 bits

int main(void) {
DDRB |= (1<<PB1); // Configurar o pino digital 9 como
output
// Configurar o ADC:
ADMUX |= (1<<ADLAR); // Resolução de 8 bits
ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); //
Prescaler de 128
ADMUX |= (1<<REFS0); // Referência: AVcc
ADCSRA |= (1<<ADATE); // Gatilho automático: Free-
running mode
// Input: ADC0 – seleccionado por defeito
ADCSRA |= (1<<ADEN); // Ligar o ADC
ADCSRA |= (1<<ADIE); // Ligar a interrupção particular
do ADC
sei(); // Ligar as interrupções globais
ADCSRA |= (1<<ADSC); // Iniciar as conversões no ADC
for(;;) {// Ciclo eterno
if(adc_distance > 128) // Testar o valor da

Última revisão: 21/12/2010 60


distância, e ligar o pino caso seja maior que 128 e vice
versa.
PORTB |= (1<<PB1);
else
PORTB &= ~(1<<PB1);
}
}

ISR(ADC_vect) {// Definir uma interrupção que ocorre a


cada ocorrência de uma conversão
adc_distance = ADCH; // Ler o valor do ADC, e guardá-lo
numa variável
}

Nota: o ligar e desligar o pino podia ter sido feito na interrupção. Visto que já tínhamos destinado
antes o ciclo para isso, e que as interrupções devem ter o mínimo de código possível, achámos
melhor pôr no ciclo.

ADC8 – medir a temperatura interna


Como mencionámos anteriormente, existe um input extra para o ADC – o ADC8. Este está
ligado a um termómetro interno que nos permite ver a temperatura interna do micro-controlador.
Não iremos discutir este input em pormenor, visto que tem maior utilidade em casos de
utilizações em condições extremas, ou com grandes velocidades de relógio.
Para utilizar este input, é necessário seleccionar a referência interna de 1.1V. O valor medido tem
uma sensibilidade de 1 mV/ºC, com um erro de +-10ºC. Valores típicos, como descritos na datasheet
são: 242mV para -45ºC, 314mV para 25ºC e 380mV para 85ºC, logo podemos estabelecer um valor
de 290mV para 0ºC. No entanto, na datasheet também vemos que depende da calibração do chip,
podendo esta ser alterada na EEPROM.

E assim terminamos o nosso tutorial sobre o ADC.

Última revisão: 21/12/2010 61


"Excerto do "Introdução ao avr-gcc usando o AvrStudio – Segunda Parte" de Senso
(http://lusorobotica.com/index.php?topic=2838.15) com alterações/adaptações de Cynary
(formatação e conteúdo)"

Comunicação Serial no AVR


Neste tutorial iremos utilizar um esqueleto básico para iniciar todos os programas, que tem os
includes e a declaração do main:

#define F_CPU 16000000UL //permite usar os delays calibrados


#include <avr/io.h> //definições gerais de pinos e registos
#include <util/delay.h>

int main(void){ // inicio da função main


// O vosso programa fica entre as chavetas
return 0; // toda a função não void tem de ter um return
}

Sempre que quiserem programar podem usar este esqueleto que são sempre uns caracteres a
menos que têm de escrever.

Como funciona a comunicação Serial?


Antes de aprendermos a usar a comunicação serial no AVR, convém compreendermos como
funciona para sermos capazes de entender o que todas as opções que nos estão disponíveis na
datasheet significam.
A comunicação serial consiste em enviar bits sequencialmente. Esta é utilizada para permitir
comunicação entre dispositivos (um periférico, como um modem ou micro-controlador, e um PC,
dois PCs, ou até dois periféricos). Para que os dispositivos se possam entender, foram definidos
vários standards para a comunicação serial. No entanto, estes standards também exigem que os
dispositivos estejam pré-programados para certos parâmetros.
A comunicação serial tem várias características: bits de paridade, stop bits, start bit, e
sincronismo/assincronismo.
Dados enviados por comunicação serial têm o seguinte formato:
Start Bit – Dados – bit de paridade – Stop bit(s).
O start bit indica o início da comunicação. Enquanto o dispositivo não está a receber/enviar
dados a linha de comunicação tem sempre o valor 1 (o que define os valores 0/1 depende do
hardware … o AVR usa os valores 0V/Vcc (normalmente 5V), respectivamente. No entanto, o
standard RS232 define os valores 3V/-3V, respectivamente). O start bit tem sempre o valor 0, para
sinalizar que estamos a começar uma transmissão.
Os dados podem ter entre 7 e 9 bits no caso do AVR. Isto é um dos pormenores que tem de ser

Última revisão: 21/12/2010 62


pré-programado nos dispositivos que estão a comunicar. Neste documento, iremos utilizar sempre 8
bits.
O bit de paridade é utilizado para detectar erros. Normalmente não se usa paridade para isso,
pois é muito ineficiente. No caso do bit de paridade estar configurado para odd, é utilizado para
termos um número ímpar de 1s. Por exemplo, no caso de enviarmos os bits: 1111000, o bit de
paridade seria 1, para termos 5 bits. No caso de estar configurado para even, é utilizado para termos
um número par de 1s. No entanto, visto que podemos ter erros no próprio bit de paridade ou em
mais do que um bit, este método é muito ineficiente e não utilizaremos (não iremos abordar a
detecção de erros na comunicação serial neste documento).
O stop bit tem sempre o valor de 1, e indica o fim da transmissão. Um ou dois podem ser usados,
mas não faz diferença na realidade, portanto iremos sempre configurar o AVR para usar um.
A comunicação serial pode ser síncrona ou assíncrona. Na comunicação assíncrona, os
dispositivos são pré-programados com uma velocidade de transmissão (denominada de baud rate),
de forma a saberem a distância entre os bits. No caso da comunicação síncrona, uma linha extra é
utilizada para indicar um clock comum a ambos os dispositivos. Basicamente, este clock indica aos
dispositivos quando é que um novo bit é transferido, e também inicia e termina a comunicação. A
comunicação síncrona dá-nos a possibilidade de maiores velocidades de transferência, e também
retira a necessidade de se utilizar um start e stop bit. No entanto, tem a desvantagem de ser
necessário um fio extra para indicar o clock. Nesta primeira parte acerca de comunicação serial,
iremos apenas abordar comunicação assíncrona. Quando falarmos de I²C, um tipo especial de
protocolo de comunicação serial, iremos utilizar sincronidade.
Para possibilitar a comunicação serial, necessitamos de três fios: transferência, recepção e
ground. O fio de ground é utilizado de forma a que os GND de ambos os dispositivos seja igual (de
forma a que 5V num, seja 5V no outro). Os outros dois fios têm de se ligar de uma forma cruzada –
o fio ligado à transmissão de um dispositivo (pino TXD/PD1 no AVR/pino digital 1 no arduino) tem
de estar ligado à recepção do outro dispositivo (pino RXD/PD0 no AVR/pino digital 0 no arduino).
Agora que já sabemos como funciona a comunicação serial, vamos aprender a programar o AVR
para usá-la!

O que é a USART?
A USART é um módulo de hardware que está dentro dos nossos atmegas, que permite ao nosso
chip comunicar com outros dispositivos usando um protocolo serial, isto quer dizer que com apenas
dois fios podemos enviar e receber dados.
Um dos maiores usos da USART é a comunicação serial com o nosso computador, e para isso
temos de configurar a USART para ela fazer exactamente aquilo que queremos.

Última revisão: 21/12/2010 63


Inicializando a USART do AVR
Falando agora especificamente de como activar e usar a nossa USART para falar com o
computador, neste tutorial em vez de deixar um monte de linhas de código perdidas no meio no
nosso main vou antes criar algumas funções, pois assim podem copiar as funções e usar noutros
programas, até porque fica tudo mais limpinho e separado.
Em pseudo-código eis o que temos de fazer:

// Iniciar e configurar a usart


// Criar uma função para enviar um byte/caracter
// Criar uma função para receber um byte/caracter
// Fazer um simples eco na função main

Como não faço ideia de como a USAR funciona o que devo fazer é pegar no datasheet e
começar a ler, e como as pessoas na atmel até fizeram um bom trabalho a fazer este datasheet está
tudo muito bem organizado com um índice e marcadores e facilmente descobrimos que a secção
sobre a USART começa na página 177- Secção 19, e temos até código em C e assembly para
configurar a USART, quer então dizer que o nosso trabalho está facilitado, e pouco mais temos que
fazer que ler e passar para o nosso programa o código dado.
Vamos começar pela inicialização da nossa USART, tal como está no datasheet:

void USART_init(void){
UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
UBRR0L = (uint8_t)(BAUD_PRESCALLER);
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
UCSR0C = (3<<UCSZ00);
}

UBRR0H e UBRR0L são os registos onde colocamos um valor que depende do baud-rate e da
frequência do oscilador que o nosso chip tem, temos duas opções para determinar este valor, ou
vemos a tabela que está no datasheet ou usamos uma pequena fórmula que juntamos ao cabeçalho
do nosso programa onde estão os outros includes e o compilador determina o valor
BAUD_PRESCALLER baseado no baudrate que queremos e no valor de F_CPU, sendo esta
fórmula a seguinte:

#define BAUD 9600 //o baudrate que queremos usar


#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) – 1)
// a formula que faz as contas para determinar o valor a
colocar nos dois registos

Última revisão: 21/12/2010 64


Isto não é magia nem nada que se pareça, no datasheet são dadas duas fórmulas, se juntarmos as
duas é esta a formula com que ficamos.
UCSR0B é o registo que nos permite activar os canais de recepção e transmissão de dados da
USART assim como activar interrupts, mas isso não nos interessa por agora.
E em UCSR0C definimos que queremos 8 bits de dados, sem paridade e um stop bit, em vez de
(3<<UCSZ00) podemos fazer ((1<<UCSZ00)|(1<<UCSZ01)) o resultado é precisamente o mesmo.
Agora para inicializar-mos a nossa USART basta chamar a função USART_init no nosso main e
temos a USART pronta a usar, mas ainda não somos capaz nem de enviar nem de receber dados.

Enviando e Recebendo Dados através da USART


Mais uma vez a datasheet tem uma solução funcional, que é a seguinte:

void USART_send( unsigned char data){


while(!(UCSR0A & (1<<UDRE0)));
UDR0 = data;
}

A primeira de linha de código pode parecer estranha, mas tudo tem a sua razão e a razão desta
linha é que o atmega tem um buffer de 3 bytes em hardware e esta linha verifica se ainda existe
espaço no buffer para colocar mais dados, se não espera que haja espaço, se sim, coloca os dados no
buffer coisa que é tratada na segunda linha da função.
E com apenas duas linhas estamos prontos a enviar dados por serial!
Agora falta-nos apenas a função para receber dados, sendo esta mais uma função de duas linhas:

unsigned char USART_receive(void){


while(!(UCSR0A & (1<<RXC0)));
return UDR0;
}

Na primeira linha, usamos o while para esperar que existam dados recebidos no registo de
recepção, quando esses dados chegam ao registo, simplesmente devolvemos os dados e temos a
nossa USART a ler dados por serial.
Agora como extra, vou mostrar uma pequena função que permite enviar strings, pois muitas
vezes queremos enviar mais que apenas um byte de informação de cada vez. A função para enviar
strings tira partido do facto de em C uma string ser terminada por um carácter nulo(/null) e que é
feita de muitos caracteres individuais. Assim com um pequeno loop que é executado enquanto os
dados a enviar não são nulos enviamos um carácter de cada vez.

Última revisão: 21/12/2010 65


void USART_putstring(char* StringPtr){
while(*StringPtr != 0x00){ // Aqui fazemos a verificação de
que não chegamos ao fim da string, verificando para isso se
o carácter é um null
USART_send(*StringPtr); // Aqui usamos a nossa função de
enviar um caracter para enviar um dos caracteres da
string
StringPtr++; } // Aumentamos o indice do array de dados
que contem a string
}

O char* no inicio da função pode parecer estranho e chama-se um ponteiro, que é algo bastante
útil em C, mas por agora vamos simplificar as coisas, e imaginar as strings como arrays de
caracteres, que é isso mesmo que elas são em C e que o ponteiro não é mais que o inicio desse
mesmo array.
Nota (Cynary): Esta função não envia o carácter de terminação de string (NULL), logo, caso o
leitor queira escrever um programa no lado do cliente que processe strings enviadas por esta função,
deve ter isto em conta, e, ou alterá-la, de forma a enviar o NULL, ou enviar pela linha serial um
número que indique o número de caracteres na string, antes de enviar a própria string.
Agora, pegamos no nosso programa inicial em branco e juntamos tudo, ficando assim:

#define F_CPU 16000000UL


#include <avr/io.h>
#include <util/delay.h>

#define BAUDRATE 9600


#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)

int main(void){

return 0;
}

void USART_init(void){

UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
UBRR0L = (uint8_t)(BAUD_PRESCALLER);
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
UCSR0C = (3<<UCSZ00);
}

unsigned char USART_receive(void){

while(!(UCSR0A & (1<<RXC0)));

Última revisão: 21/12/2010 66


return UDR0;

void USART_send( unsigned char data){

while(!(UCSR0A & (1<<UDRE0)));


UDR0 = data;

void USART_putstring(char* StringPtr){

while(*StringPtr != 0x00){
USART_send(*StringPtr);
StringPtr++;}

Se tentar-mos usar as nossas funções o compilador vai dizer que elas não estão definidas e nós
ficamos a olhar para ele com cara espantada, porque as nossas funções estão mesmo ali, por baixo
do main, e é precisamente esse o problema, no arduino podemos declarar funções onde bem nos
apetecer, e em C tambem, mas temos que declarar as funções, ou seja a primeira linha da função
que tem o nome dela e que tipo de dados é o seu retorno e quais os seus argumentos têm de estar
antes do main, para o compilador saber que as funções existem, ficando assim o nosso código com
as declarações das funções antes do main:

#define F_CPU 16000000UL


#include <avr/io.h>
#include <util/delay.h>

#define BAUDRATE 9600


#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)

//declaração das nossas funções


void USART_init(void);
unsigned char USART_receive(void);
void USART_send( unsigned char data);
void USART_putstring(char* StringPtr);

int main(void){

return 0;
}

void USART_init(void){

UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);

Última revisão: 21/12/2010 67


UBRR0L = (uint8_t)(BAUD_PRESCALLER);
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
UCSR0C = (3<<UCSZ00);
}

unsigned char USART_receive(void){

while(!(UCSR0A & (1<<RXC0)));


return UDR0;

void USART_send( unsigned char data){

while(!(UCSR0A & (1<<UDRE0)));


UDR0 = data;

void USART_putstring(char* StringPtr){

while(*StringPtr != 0x00){
USART_send(*StringPtr);
StringPtr++;}

Nota (Cynary): Havia outra forma de ultrapassar o erro do compilador acerca das funções não
estarem definidas, que era declará-las noutra ordem (as funções de que o main depende, devem
estar acima deste, e assim progressivamente). No entanto, para evitar quaisquer confusões, e até
facilitar a programação (visto que as declarações das funções estão todas no topo, e assim não
temos de as procurar no código), a melhor forma de o fazer é esta.

Exemplo de utilização do USART


Agora que sabemos usar a comunicação serial do AVR, vamos fazer alguma coisa com ela!
Vamos usar as nossas funções para escrever a frase "Olá mundo!!!", mas falta-nos umas coisa, não
temos um terminal serial, ou seja um programa que receba os dados da porta serial e nos mostre no
ecrã do computador o que recebeu.
No meu caso, recomendo usar este terminal:
http://www.smileymicros.com/download/term20040714.zip?&MMN_position=42:42
Nota (Cynary): O IDE do arduino também inclui um terminal serial que funciona perfeitamente
bem. Por isso, caso o leitor não queira ter de fazer download e instalar um novo programa, ou não
use windows (apesar de parecer que este terminal funciona bem com o wine), pode sempre utilizar
o terminal do IDE do arduino.

Última revisão: 21/12/2010 68


Foi feito por um menbro do AvrFreaks e acho-o simples de usar, também vai do gosto e existem
milhares de terminais pela internet fora, escolham o que mais gostarem. Neste caso basta fazer o
download e executar o ficheiro, podem já fazer isso e deixar o terminal aberto que vamos usa-lo
mais tarde.
Vamos lá pegar então no nosso programa e completá-lo para o nosso "Olá mundo":

#define F_CPU 16000000UL


#include <avr/io.h>
#include <util/delay.h>

#define BAUDRATE 9600


#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)

//declaração das nossas funções


void USART_init(void);
unsigned char USART_receive(void);
void USART_send( unsigned char data);
void USART_putstring(char* StringPtr);

char String[]="Olá mundo!!!"; //String[] que dizer que é um


array, mas ao colocar-mos o texto entre "" indicamos ao
compilador que é uma string e ele coloca automáticamente o
terminador null e temos assim uma string de texto usavel

int main(void){
USART_init(); //Inicializar a usart

while(1){ //Loop infinito


USART_putstring(String); //Passamos a nossa string á função
que a escreve via serial
_delay_ms(5000); //E a cada 5s re-enviamos o texto
}

return 0;
}

void USART_init(void){

UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
UBRR0L = (uint8_t)(BAUD_PRESCALLER);
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
UCSR0C = (3<<UCSZ00);
}

unsigned char USART_receive(void){

while(!(UCSR0A & (1<<RXC0)));


return UDR0;

Última revisão: 21/12/2010 69


void USART_send( unsigned char data){

while(!(UCSR0A & (1<<UDRE0)));


UDR0 = data;

void USART_putstring(char* StringPtr){

while(*StringPtr != 0x00){
USART_send(*StringPtr);
StringPtr++;}

E tão simples como isto temos o nosso atmega a enviar dados.


Agora, para testar, enviamos o nosso programa para o AVR e vamos agora abrir o nosso
Terminal, e tal como com o avrdude têm de ter atenção á porta com que o vosso arduino usa, assim
como ao baudrate escolhido, no caso deste exemplo é 9600 e a forma como a USART está
configurada, no nosso caso, 8 bits de dados, 1 bit de stop e sem bit de paridade, nesta imagem
mostro como configurar o terminal para receber dados do arduino:

Agora basta carregar em "Connect" e carregar no botão de reset do arduino para sincronizar o
programa com arduino e deverão ver algo do género:

Última revisão: 21/12/2010 70


E está a comunicação serial a funcionar!
Agora deixo um desafio em aberto, usando o outro tutorial e este, desafio-vos a criarem um
programa que acende o led do arduino quando recebe o caracter "a" e que o apague quando receber
outro carácter qualquer, é algo bastante simples de se fazer, não precisam de nenhum hardware extra
para além do arduino e assim aprendem como controlar algo usando dados via serial, para enviar
um carácter usando este terminal basta escrever o carácter na caixa marcada a verde da imagem de
cima e carregar no ENTER.
Boa programação!!!

Última revisão: 21/12/2010 71


Comunicação por I²C

O Protocolo I²C
Antes de começarmos a aprender como comunicar por I²C no AVR, temos de compreender
exactamente como este protocolo funciona.
O protocolo I²C (Inter-Integrated Circuit) é muito útil, pois permite que, em teoria, até 127
aparelhos comuniquem entre si, usando apenas dois fios (na prática, este limite pode não ser muito
real, pois depende das características eléctricas do sistema, e também porque alguns endereços
podem estar reservados limitando o número de dispositivos ainda mais).
Isto é possível, pois os aparelhos comunicam através de endereços, num sistema mestre/escravo.
Começando pelas características físicas do protocolo, usam-se duas linhas: a SCL (clock) e a
SDA (dados) – onde ligamos estas linhas num aparelho depende da sua construção. No caso do
AVR, ligamos, respectivamente nos pinos PC5 (analog in 5) e PC4 (analog in 4). Devido a haver
apenas uma linha de dados, a comunicação usando o protocolo I²C é designada como sendo half-
duplex, visto que só podemos estar a enviar ou receber dados num certo ponto no tempo, e não os
dois ao mesmo tempo.
A linha SCL é utilizada para sincronizar os diferentes aparelhos. Funciona da seguinte forma:
quando está em low, o sinal da linha SDA pode ser alterado, e quando está em high, o sinal da linha
SDA não pode ser alterado, logo está pronto a ser lido (logo, uma transição de low para high
sinaliza um novo bit na linha SDA). Normalmente apenas um aparelho controla esta linha, mas os
outros aparelhos, caso não sejam rápidos o suficiente para utilizarem a frequência desse aparelho
podem controlar a linha, deixando-a em low pelo tempo que quiserem.
A linha SDA contém a informação, que é lida de acordo com o estado da linha SCL, como
explicado no parágrafo anterior. No estado high, esta linha tem um bit 1, e no estado low, esta linha
contém um bit 0.
Ambas estas linhas têm uma regra especial: os aparelhos que as usam não as podem pôr no
estado high, apenas em low. Por isso, para se ter um estado high, colocam-se duas resistências pull-
up entre o terminal positivo e a linha (uma resistência para cada linha é suficiente). Assim, quando
os aparelhos “largam” a linha, esta está em high, e são responsáveis por a pôr em low. Isto é muito
útil para, por exemplo, aparelhos que funcionem a 5V poderem comunicar com aparelhos que
funcionam a 3.3V – 3.3V é normalmente aceite como um estado high válido, e como os outros
aparelhos não suportam uma corrente de 5V, isto impede que tenham problemas, sem afectar a
comunicação.
A regra descrita acima sobre a alteração do estado da linha SDA de acordo com a SCL tem duas
excepções: bits de início e de fim de comunicação. Nem sempre existe um aparelho a usar as linhas

Última revisão: 21/12/2010 72


do I²C, por isso é necessário conseguir-se distinguir quando é que esta está a ser usada ou não.
Quando não está a ser usada, ambas as linhas estão no estado high. Para sinalizar o início de
comunicação, a linha SDA vai para um estado low, enquanto a linha SCL está em high. Para
sinalizar o fim da comunicação, a linha SDA vai para um estado high enquanto SCL está em high.
Agora que compreendemos como funciona o protocolo a um nível de hardware, vamos aprender
como funciona a um nível de software. Já começámos a falar disso ao mencionarmos a existência
de bits de início e de fim (neste documento, iremos referir-nos a eles como start bits e stop bits,
respectivamente, pois é essa a convenção).
O protocolo I²C é muito simples de compreender e utilizar.
Existem duas categorias de aparelhos – os master e os slave. Os master é que têm controlo sobre
a linha. São estes que “chamam” os outros aparelhos, através do seu endereço único, e enviam ou
recebem dados dos mesmos. Em qualquer altura, só podem haver dois dispositivos a usar os fios
para comunicação, e um desses tem de ser um master. Existe um endereço especial, chamado de
general call, que permite ao master falar com todos os slaves. É útil para, por exemplo, configurar o
endereço de um aparelho com um endereço desconhecido, ou escrever um valor para vários
aparelhos diferentes ao mesmo tempo. Este endereço é o 0. No entanto, nem todos os aparelhos
responderão a este endereço, visto que depende da sua configuração.
Podem haver mais do que um master. Para evitar problemas, quando dois master tentam
controlar os fios, ocorre um processo denominado arbitration, em que os masters “lutam” pelo
controlo. Após a arbitration, o aparelho que ganhar é o master, e todos os outros funcionam como
slaves. Neste documento iremos apenas cobrir o uso de um master. Para mais informações sobre
utilização de vários masters, pode-se consultar a datasheet do AVR.
O I²C define também o formato da mensagem na linha: a mensagem começa com um start bit,
seguido do endereço do slave e um bit read/write que indica se o master quer ler (bit=1) ou escrever
(bit=0) dados para o slave. Isto é seguido de um sinal ACK/NACK do slave, a dizer que recebeu a
mensagem, e se pode ou não continuar a comunicação (um ACK – acknowledge – tem o valor 0, e
significa que podemos continuar a comunicação; um NACK é o contrário, tendo o valor 1, e
significando que a comunicação deve parar, e que o master deve enviar um stop bit). Para cada byte
enviado, o aparelho receptor deve enviar um ACK/NACK (visto que um endereço tem 7 bits, ao
adicionarmos o bit de leitura/escrita, enviámos 8 bits – um byte).
De acordo com o valor do bit read/write, o master transforma-se num receptor ou num
transmissor de dados.
Após esta primeira parte da mensagem, os aparelhos continuam a comunicar entre si através de
bytes e ACK/NACK. O master pode ainda realizar um repeated start, que basicamente consiste em
enviar um novo bit de start, e um novo endereço, sem ter de perder controlo da linha, devido a não

Última revisão: 21/12/2010 73


enviar o bit de stop. Isto é útil se o master quer endereçar outro aparelho, ou mudar o valor do bit
read/write (por exemplo, começa por escrever para um aparelho, enviando um comando, e depois lê
os resultados desse comando). Para finalizar a comunicação, o master envia um bit de stop.
Alguns aparelhos não mencionam que trabalham em I²C, mas sim que usam SMBus, ou TWI
(Two-Wire Interface – o caso do AVR). No entanto, estas designações, apesar de terem algumas
diferenças, são semelhantes em funcionalidade ao I²C, por isso podem, na prática, ser interpretadas
como sendo este protocolo.
Agora que já compreendemos como funciona o I²C, vamos aprender como usá-lo no AVR, e
iremos usar este protocolo para dois AVRs comunicarem entre si.

I²C no AVR
No AVR, em vez de I²C, temos uma interface TWI (two wire serial interface). No entanto,
podemos usar como se fosse I²C.
Quando utilizamos o AVR como master, devemos começar por gerar o clock na linha SCL. Isto é
feito alterando o register TWBR. Segundo a datasheet, a fórmula para o clock é a seguinte:
CPU Frequency
SCL Frequency=
162TWBR∗ Prescaler Value
Ao isolarmos o TWBR, ficamos com isto:
CPU Frequency
−16
SCL Frequency
TWBR=
2Prescaler Value 
Neste documento iremos utilizar a frequência 100kHz, visto que é uma frequência standard do
I²C, e a maior parte dos aparelhos suporta-a.
O valor do prescaler aqui é definido no register TWSR (que iremos descrever mais à frente).
Iremos usar o prescaler 1, visto que, com uma frequência de 100kHz para o I²C, e uma frequência
do CPU de 16 MHz, o valor do TWBR não ultrapassa a capacidade de 8 bits (neste caso, esse valor
será 72), e é o valor por defeito (assim não necessitamos de alterar o register TWSR). Atenção que a
datasheet recomenda um valor mínimo de 10 para o TWBR, logo para frequências da SCL maiores,
será necessário um prescaler maior.
Agora que já sabemos como gerar o clock, temos de compreender como realizar acções no I²C.
Para isto utilizamos o register TWCR. Este register em particular tem muitas particularidades na
forma como funciona. Primeiro, para o I²C funcionar, temos de colocar o valor 1 no bit TWEN. Em
segundo lugar, o bit TWINT determina quando ocorrem acções ou não no I²C: quando este tem o
valor 0, as acções definidas pelos restantes bits ocorrem, e quando tem o valor 1, não podem ocorrer
acções. No entanto, a forma como este assume os valores 0 e 1 é diferente dos outros bits: para
colocar o valor 0 neste bit, temos de escrever um 1 para lá, enquanto ele apenas assume o valor 1

Última revisão: 21/12/2010 74


após ocorrer algum evento relacionado com o I²C. Os bits TWEA, TWSTA e TWSTO indicam que
acção o microcontrolador deve tomar nas linhas I²C quando o bit TWINT é 0. O bit TWWC
constitui uma flag que indica se estamos a realizar uma certa acção proibida – mexer no register
TWDR quando o bit TWINT é 0 (iremos ignorar esta flag). O bit TWIE liga a interrupção ligada ao
I²C – esta interrupção ocorre sempre que o bit TWINT é 1. Atenção que esta interrupção não coloca
esse bit a 0, logo para evitar que esta se repita eternamente, ou colocamo-lo a 0, ou desligamos a
interrupção temporariamente – não iremos usar esta interrupção neste documento, no entanto
destacamos a sua utilidade enquanto ferramenta para realizar acções relacionadas com o protocolo
I²C, sem a necessidade de ocupar o microprocessador enquanto se espera que cada operação fique
completa; para saber qual a operação a realizar, usam-se os códigos de estados, discutidos
brevemente.
Até agora, temos usado sempre a operações binária OR para colocar valores em bits específicos
nos registers. No entanto, não é prático fazer isso com o register TWCR, visto que este tem bits que
determinam a acção a seguir, e que, se não os apagarmos, farão com que ocorram acções repetidas.
Assim, de cada vez que mexermos no register TWCR, faremos um reset a todos os seus valores.
Isto obriga a ter sempre em atenção o facto de termos de colocar sempre o bit TWEN com o valor 1.
Com o que já sabemos até agora, já somos capazes de enviar um sinal start para as linhas I²C! O
bit que determina esta acção é o TWSTA. Vamos então definir uma função que faz isto mesmo, bem
como uma que inicia o clock:

#define FREQ 100000


#define PRES 1
#define TWBR_value ((F_CPU/FREQ)-16)/(2*PRES)

void set_clk() {
TWBR = TWBR_value;
}
void send_start() {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA);
}

Note que, além do bit TWSTA, também mexemos nos bits TWINT e TWEN. Isto é para garantir
que o I²C está ligado (1 no bit TWEN), e que podemos efectuar acções neste (0 no bit TWINT –
como foi dito antes, este bit fica com o valor 0 quando escrevemos o valor 1 nele).
O código para o envio de um start, no entanto, está incompleto, pois ficamos com o problema de
não saber quando o start acabou de ser enviado, e se tivemos sucesso ou não a enviá-lo (demora um
pouco a enviar um start, e por vezes pode falhar, por exemplo, devido a um processo de arbitration
que o nosso AVR perdeu)!

Última revisão: 21/12/2010 75


Como foi dito antes, o bit TWINT fica com o valor 1 assim que uma acção é completada nas
linhas I²C. Assim, para saber quando se acabou de enviar o start, temos de esperar que este bit fique
com o valor 1. No entanto, isto não nos indica se o start foi enviado com sucesso ou não. Para
sabermos isto, observamos o register TWSR. Já falámos deste register anteriormente quando
mencionámos o prescaler. Além do prescaler, este register contém informação acerca do estado do
I²C, tal como se o start foi enviado com sucesso – visto que as operações para enviar um start e um
repeated start são iguais em termos do register TWCR, mas originam diferentes códigos de estado
no register TWSR, temos de verificar ambos esses códigos. Estes códigos de estado podem ser
encontrados na datasheet do AVR em quatro tabelas (páginas 229, 232, 235 e 238), respectivamente,
para os modos de master transmitter, master receiver, slave receiver e slave transmitter. Apenas o
master pode enviar um sinal de start/repeated start, e os códigos para esses sinais são iguais tanto no
modo transmitter como receiver, e são, respectivamente, 0x08 e 0x10. É de notar que estes códigos
de estado assumem que os bits do prescaler são ambos 0. No entanto, nem sempre o são. Estes bits
são os dois menos significativos. Para os retirar, realizamos a operação AND com o valor
0b11111100, ou 0xF8. Para sabermos se o o envio do start bit foi realizado com sucesso, esta função
devolverá um valor verdadeiro ou falso, dependendo respectivamente se teve sucesso ou não.
Assim, já podemos completar o código para a função que envia um sinal de start:

unsigned char send_start() {


TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA); // Enviar o
sinal de start
while(!(TWCR&(1<<TWINT))); // Esperar que a operação termine
return ((TWSR&0xF8) == 0x08 || (TWSR&0xF8) == 0x10);
}

Depois do start bit, temos de enviar o endereço do aparelho com quem queremos comunicar e o
bit read/write. Isto constitui um byte (visto o endereço ser 7 bits + 1 bit read/write). O AVR não
distingue entre enviar o endereço/bit read/write, e qualquer outro byte no I²C, quando está em modo
master. Para fazer isto, apenas colocamos o byte a enviar no register TWDR e ligamos as acções
I²C. Atenção que, como mencionado anteriormente, não podemos colocar qualquer informação no
register TWDR, quando o bit TWINT é 0! Vamos então criar uma função que envia um byte para as
linhas I²C. Novamente, verificaremos o estado do I²C para saber se tivemos sucesso ou não. Visto
que os códigos de estado para o envio e recepção (ACK devolvido) com sucesso de um endereço de
slave e o bit read/write são diferentes dos códigos de sucesso de um byte enviado (com ack ou nack
devolvido), iremos criar uma função separada para “chamar” um slave. Os códigos devolvidos são
ainda diferentes quando enviamos um bit read ou um bit write, portanto, temos de testar para ambos
os casos (respectivamente, 0x40 e 0x18):

Última revisão: 21/12/2010 76


unsigned char send_slave(unsigned char addr) {
TWDR = addr;
TWCR = (1<<TWINT) | (1<<TWEN);
while(!(TWCR&(1<<TWINT)));
return ((TWSR&0xF8) == 0x18 || (TWSR&0xF8) == 0x40);
}

Quando queremos endereçar um slave de endereço addr, com bit read/write B, apenas enviamos
o byte ((addr<<1)|B). É de notar que algumas datasheets listam dois endereços para um aparelho.
Isto significa que, em vez de darem os 7 bits do endereço, dão o byte completo, com o bit de
read/write.
Com o que já sabemos, podemos já definir uma função para enviar dados, visto que basta alterar
os códigos de estado que verificamos. Como foi explicado antes, quando uma transmissão ocorre
com sucesso, o receptor devolve um ACK. O receptor também pode devolver um NACK quando a
recepção ocorre com sucesso, mas este sinal significa que devemos parar a transmissão e enviar um
stop bit imediatamente. Assim, temos de verificar se a transmissão ocorreu com sucesso e se um
ACK foi devolvido. Como podemos enviar dados tanto em modo slave como em modo master,
temos ainda de verificar os códigos de estado para estes dois modos. Assim, temos de verificar os
códigos de estado 0xB8 e 0x28. Outra particularidade do modo slave é que em muitas operações,
para garantir a sua funcionalidade, devemos colocar o bit TWEA a 1, inclusive na operação de
envio de dados. Explicaremos o porquê mais à frente, mas iremos incluir este pormenor nesta
função:

unsigned char send_data(unsigned char data) {


TWDR = data;
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA);
while(!(TWCR&(1<<TWINT)));
return ((TWSR&0xF8) == 0xB8 || (TWSR&0xF8) == 0x10);
}

Até agora só nos concentrámos em enviar dados. No entanto, tanto aparelhos master como slave
têm de ser capazes de receber dados. Atenção que apenas podemos recebe dados em master quando
enviamos o bit de read, e em slave quando recebemos o bit de write! O processo é semelhante ao de
enviar dados. Tem apenas os pormenores de lermos os dados do register TWDR, em vez de os
escrevermos lá, e de termos de enviar um ACK/NACK, para sinalizar que recebemos os dados com
sucesso e se queremos continuar ou não com a comunicação, e dos estados diferirem de acordo com
o que devolvemos (ACK/NACK), e de acordo com o endereço utilizado para o slave (general call
ou o endereço específico). Assim, temos 6 códigos de estado que temos de verificar, 3 para o caso

Última revisão: 21/12/2010 77


de devolvermos um ACK (0x50, 0x80, 0x90), e 3 para o caso de devolvermos um NACK (0x58,
0x88, 0x98). O bit que define se devolvemos um ACK ou um NACK é o TWEA (1 para ACK, 0
para NACK). A função que irá receber dados recebe um parâmetro lógico: verdadeiro no caso de
querermos devolver um ACK, e falso no caso de querermos devolver um NACK. O valor devolvido
será 0 no caso de haver um erro na recepção, ou o valor recebido caso contrário (isto não é óptimo
devido o valor recebido poder ser 0! Apenas fazemos assim para simplificar. No entanto, o ideal
seria usar uma flag externa à função para sinalizar um erro ou devolver uma estrutura com duas
variáveis: uma flag para sinalizar erros e o valor recebido):

unsigned char receive_data(unsigned char ack) {


TWCR = (1<<TWINT) | (1<<TWEN) | (ack?(1<<TWEA):0);
while(!(TWCR&(1<<TWINT)));
if((ack && (TWSR&0xF8) != 0x50 && (TWSR&0xF8) != 0x80 &&
(TWSR&0xF8) != 0x90) || ((!ack) && (TWSR&0xF8) != 0x58 &&
(TWSR&0xF8) != 0x88 && (TWSR&0xF8) != 0x98)) {
return 0;
}
return TWDR;
}

Até agora já enviámos start bits, “chamamos” slaves, enviamos dados, e recebemos dados, tanto
em modo slave como master. Para completarmos a funcionalidade do modo master, apenas nos falta
enviar stop bits. Isto é feito colocando a 1 o bit TWSTO. Esta acção em particular não tem nenhum
código de estado, nem coloca o bit TWINT a 1, logo, podemos criar uma função muito simples para
a efectuar:

void send_stop() {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}

Agora já temos toda a funcionalidade disponível ao master! Apenas nos faltam alguns
pormenores acerca do slave.
Primeiro, vamos compreender a relevância do bit TWEA. Quando este bit tem o valor 1, e o
aparelho ainda não foi endereçado, este irá responder quando o seu endereço ou uma general call
(caso esteja configurado para responder a uma general call) aparecerem nas linhas I²C, com um
ACK. Se este bit for 0, então ignorará estes bytes, desligando-se assim do I²C.
Anteriormente, mencionei que quando um slave envia dados, deve colocar o bit TWEA a 1. Isto
não é estritamente necessário, mas é aconselhável. Quando se coloca o bit TWEA a 0 após enviar
dados, o aparelho está à espera de receber um NACK, e não irá enviar mais dados. No entanto, para

Última revisão: 21/12/2010 78


simplificar, colocamos sempre o bit TWEA a 1, visto que não faz nenhuma diferença nos dados
transmitidos, e podemos simplesmente terminar aí a conexão como se estivéssemos à espera do
NACK. Além disto, se não colocarmos o bit TWEA a 1, não podemos enviar mais dados.
Após realizarmos operações no modo slave, devemos esperar por um stop/start/repeated start do
master – de acordo com a documentação da datasheet, no modo slave transmitter, os stop bits são
ignorados. Assim, para fazer isto, ligamos o I²C, à espera de que algum evento ocorra. Caso seja um
stop bit (código de estado 0xA0), ligamos novamente o I²C, para o aparelho esperar que o seu
endereço seja enviado para a linha. Caso não seja, um stop bit, significa que um start bit foi
enviado, seguido do endereço do aparelho em questão – sai-se da função para o código principal
tratar de processar esse endereçamento (veremos de seguida como fazer isso). Assim, podemos
definir a função para esperar por um stop/start/repeated start bit da seguinte forma:

void wait_stop() {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA);
while(!(TWCR&(1<<TWINT)));
if((TWSR&0xF8) == 0xA0) {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA);
}
}

Para podermos ter o nosso aparelho a funcionar como slave, apenas nos falta configurar o seu
endereço, saber como configurá-lo para responder à general call e saber quando o aparelho é
endereçado, e saber se foi enviado um bit de read ou write.
Vamos começar por atribuir um endereço ao aparelho e configurar se ele responde à general call
ou não. Para isto, apenas temos de de escrever o endereço pretendido nos 7 bits superiores do
register TWAR, e o bit inferior, caso seja 0, o aparelho ignora a general call, e caso seja 1, o
aparelho responde à general call.
Vamos então configurar o nosso aparelho para responder tanto à general call como ao endereço
0x10, através de uma função (esta função, além de configurar o endereço, também fará com que o
aparelho “escute” as linhas I²C, à espera de ser endereçado):

#define GC 1
#define ADDR 0x10

void set_slave() {
TWAR = (ADDR<<1)|GC;
TWCR = (1<<TWEN) | (1<<TWEA);
}

Agora, o que nos falta para ter um slave funcional, é saber como verificar quando este é

Última revisão: 21/12/2010 79


endereçado. Para isto, iremos colocar dois aparelhos a comunicar um com o outro. Basicamente, um
AVR terá o papel de master transmitter, e o outro de slave receiver. O slave irá mudar o estado do
LED incoroporado o arduino (pino digital 13/PB5) assim que receber dados do master receiver.
Neste documento, não iremos usar os modos master receiver e slave transmitter, mas são idêntidos,
apenas trocando-se as funções de transmissão/recepção, e no caso do slave, os códigos de estado
(para o slave transmitter, são 0xA8 e 0xB0).
Iremos começar com o aparelho master. Este começa por enviar um start bit, depois “chama” o
slave (neste caso com o endereço 10, e o bit de write), e envia um byte para o slave (neste caso
iremos enviar o byte 'a'), e envia um sinal de stop. Para o efeito no LED ser visível, iremos enviar
dados em intervalos de 1 segundo, recorrendo à função _delay_ms na biblioteca util/delay.h:

#include <avr/io.h>
#include <util/delay.h>
#define FREQ 100000
#define PRES 1
#define TWBR_value ((F_CPU/FREQ)-16)/(2*PRES)

void set_clk() {
TWBR = TWBR_value;
}

unsigned char send_start() {


TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA); // Enviar o sinal
de start
while(!(TWCR&(1<<TWINT))); // Esperar que a operação termine
return ((TWSR&0xF8) == 0x08 || (TWSR&0xF8) == 0x10);
}

unsigned char send_slave(unsigned char addr) {


TWDR = addr;
TWCR = (1<<TWINT) | (1<<TWEN);
while(!(TWCR&(1<<TWINT)));
return ((TWSR&0xF8) == 0x18 || (TWSR&0xF8) == 0x40);
}

unsigned char send_data(unsigned char data) {


TWDR = data;
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA);
while(!(TWCR&(1<<TWINT)));
return ((TWSR&0xF8) == 0xB8 || (TWSR&0xF8) == 0x10);
}

void send_stop() {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}

#define W 0

Última revisão: 21/12/2010 80


#define R 1

int main(void) {
set_clk();
for(;;) {
send_start();
send_slave((0x10<<1)|W);
send_data('a');
send_stop();
_delay_ms(1000);
}
}

Agora iremos programar o aparelho slave. Este espera até que seja endereçado com um bit de
read (código de estado 0x60, 0x68, 0x70 ou 0x78). Assim que o é, este recebe os dados, faz toggle
do LED, e espera pelo stop bit:

#include <avr/io.h>
#include <util/delay.h>

unsigned char receive_data(unsigned char ack) {


TWCR = (1<<TWINT) | (1<<TWEN) | (ack?(1<<TWEA):0);
while(!(TWCR&(1<<TWINT)));
if((ack && (TWSR&0xF8) != 0x50 && (TWSR&0xF8) != 0x80 &&
(TWSR&0xF8) != 0x90) || ((!ack) && (TWSR&0xF8) != 0x58 &&
(TWSR&0xF8) != 0x88 && (TWSR&0xF8) != 0x98)) {
return 0;
}
return TWDR;
}

void wait_stop() {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA);
while(!(TWCR&(1<<TWINT)));
if((TWSR&0xF8) == 0xA0) {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA);
}
}

#define GC 1
#define ADDR 0x10

void set_slave() {
TWAR = (ADDR<<1)|GC;
TWCR = (1<<TWEN) | (1<<TWEA);
}

int main(void) {
DDRB |= (1<<PB5);
for(;;) {

Última revisão: 21/12/2010 81


while((TWSR&0xF8) != 0x60 && (TWSR&0xF8) != 0x68 &&
(TWSR&0xF8) != 0x70 && (TWSR&0xF8) != 0x78);
receive_data(0);
PORTB ^= (1<<PB5);
wait_stop();
}
}

E assim completamos o tutorial acerca do protocolo I²C e sobre como usá-lo com o AVR. Este é
um dos componentes mais complexos de utilizar no AVR, por isso aconselhamos que experimente
várias vezes com o código, e que consulte a datasheet e este documento sempre que tiver dúvidas.

Última revisão: 21/12/2010 82


Bibliografia
http://www.avrfreaks.net/

http://www.atmel.com/dyn/resources/prod_documents/doc8025.pdf

http://embeddeddreams.com/users/njay/Micro Tutorial AVR – Njay.pdf

http://www.smileymicros.com/index.php?
module=pagemaster&PAGE_user_op=view_page&PAGE_id=70&MMN_position=117:117

http://lusorobotica.com/index.php?topic=2838.15

Última revisão: 21/12/2010 83

You might also like