Professional Documents
Culture Documents
Ver na Pag. 19 a tabela de pinos de equivalência dos pinos do arduino com os pinos do ATMEGA328
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:
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.
Qualquer software presente neste documento é oferecido com objectivos didácticos, e não é
acompanhado de qualquer garantia de performance ou funcionalidade.
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).
É 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.
#include <avr/io.h>
int main(void) {
DDRB |= (1<<PB1); }
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
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:
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.
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.
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.
| – or
& – and
~ – not
^ – xor
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
O or retorna 0 quando ambos os bits são 0, e 1 quando pelo menos um dos bits é 1. Olhemos para
um exemplo:
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; }
O and retorna 0 quando pelo menos um dos bits é 0, e 1 quando os dois bits são 1.
Por exemplo:
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; }
~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ê:
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!
é igual a fazer:
(mais à frente iremos estudar como criar um número com apenas um 1 na posição pretendida,
sabendo apenas essa posição).
#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 !=.
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).
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
Mas ainda nos falta uma coisa: como chegamos ao número que tem a posição desejada a 1?
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
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!
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".
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.
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
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.
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á
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.
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.
1. Entrada normal
3. Saída normal
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.
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):
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
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
Agora seguem-se alguns exemplos de configuração. Para configurar apenas o GPIO PA3 como
saída e todos os restantes como entradas,
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:
// 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.
// 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
// 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.
// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>
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>
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.
}
// Iniciar o programa
#include <avr/io.h>
#include <avr/interrupt.h>
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>
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:
//...
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.
Timers no AVR
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.
// 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>
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
//Iniciar o programa
#include <avr/io.h>
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
//Iniciar o programa
#include <avr/io.h>
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.
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
// 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
// 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
}
// 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
}
// Iniciar o programa
#include <avr/io.h>
int main(void) {
DDRB |= (1<<PB1); // Configurar o pino digital 9 como
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
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:
// 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
}
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
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
}
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.
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):
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
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).
Logo, a sua variação será: 255/20 ≃ 12 (isto tem um erro associado, mas iremos ignorá-lo).
//Iniciar o programa
// Definir o pino digital 9 como OUTPUT
// 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
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;
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
}
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
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
}
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,
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.
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.
1 ms = 16000000/8/1000 = 2000
x ms = 2000*x.
OCR1A = 20000-2000*x/2
Assim, já podemos criar a nossa função rodar (e já agora, adicionamos aquilo que já sabemos
fazer):
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.
}
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);
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
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?
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
// 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
// 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); //
// 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
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
}
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).
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
// 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
// Iniciar programa
#include <avr/io.h>
#include <avr/interrupt.h>
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
}
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>
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
}
}
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>
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
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.
Sempre que quiserem programar podem usar este esqueleto que são sempre uns caracteres a
menos que têm de escrever.
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.
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:
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:
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.
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:
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);
}
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:
int main(void){
return 0;
}
void USART_init(void){
UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
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.
int main(void){
USART_init(); //Inicializar a usart
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);
}
while(*StringPtr != 0x00){
USART_send(*StringPtr);
StringPtr++;}
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:
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
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=
162TWBR∗ Prescaler Value
Ao isolarmos o TWBR, ficamos com isto:
CPU Frequency
−16
SCL Frequency
TWBR=
2Prescaler 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
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)!
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):
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:
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
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
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 é
#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;
}
void send_stop() {
TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO);
}
#define W 0
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>
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(;;) {
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.
http://www.atmel.com/dyn/resources/prod_documents/doc8025.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