Bitwise Linguagem C - AVR
Bitwise Linguagem C - AVR
Bitwise Linguagem C - AVR
Rodrigo Toste Gomes a.k.a Cynary Nuno Joo a.k.a Njay Senso
Neste documento tentamos explicar ao leitor as vrias funes que um micro-controlador AVR disponibiliza, e como pode controlar as mesmas, sem recorrer a bibliotecas de alto nvel que muitas vezes roubam performance e acrescentam tamanho aos programas, como por exemplo as que o arduino disponibiliza. Assumimos que o leitor tem alguma experincia com programao em C, e que o contacto que tem com AVR com o arduino (logo, todos os exemplos que temos aqui iro funcionar no mesmo. No entanto, podem funcionar noutros micro-controladores AVR com poucas ou nenhumas alteraes). Todos os programas aqui podem ser compilados atravs 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 cdigo) 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 fcil alterar estes comandos para funcionarem com outros micro-controladores e programadores, estando essa informao provavelmente na datasheet. Os programas necessrios para os comandos acima funcionarem vm instalados com o IDE do arduino, e podem ser utilizados para programar outros micro-controladores que no o usado pelo arduino. Para os utilizadores de windows, a utilizao das ferramentas AVRStudio e WinAVR recomendada, mas visto que est alm do alcance deste documento, o leitor incentivado a pesquisar, mas garantimos que os comandos acima funcionam. Qualquer software presente neste documento oferecido com objectivos didcticos, e no acompanhado de qualquer garantia de performance ou funcionalidade. Gostaria de agradecer a todos os membros da lusorobtica que comentaram no tpico respectivo a estes tutoriais, e em especial ao membro Njay, que me autorizou a usar o seu Micro-tutorial neste documento.
ndice
Programao em C no AVR..................................................................................................................1 Introduo.............................................................................................................................................5 Programao em C em micro-controladores........................................................................................6 Controlo da funcionalidade do micro-controlador os registers....................................................6 Pseudo-cdigo/cdigo esqueleto......................................................................................................7 MACROS.........................................................................................................................................8 Variveis volatile..............................................................................................................................8 Operaes 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 perifrico............................................................................................16 Sada Digital Normal.....................................................................................................................16 Sada em Colector Aberto (open colector).....................................................................................17 Sada controlada por um perifrico................................................................................................17 GPIOs na Arquitectura AVR..........................................................................................................18 Configurao dos Portos em Linguagem C...................................................................................20 Interrupes........................................................................................................................................21 O que uma interrupo?..............................................................................................................21 Como funciona uma interrupo no AVR?....................................................................................21 Como lidar com uma interrupo no AVR?...................................................................................22 Exemplo de interrupo atravs do pino digital 2 (INT0).............................................................23 Cuidados a ter na utilizao de interrupes.................................................................................26 Timers.................................................................................................................................................28 O que so 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 Interrupes e timers......................................................................................................................36 Timers Parte 2, Pulse Width Modulation.........................................................................................39 O que PWM?...............................................................................................................................39 Vrios Modos de PWM.................................................................................................................39 Fast PWM......................................................................................................................................41 Phase and Frequency Correct PWM..............................................................................................46 Analog-to-Digital Converter..............................................................................................................52 Formato Analgico 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 distncia.......................................................................57 ADC8 medir a temperatura interna.............................................................................................61 Comunicao Serial no AVR..............................................................................................................62 Como funciona a comunicao Serial?..........................................................................................62 O que a USART?.........................................................................................................................63 Inicializando a USART do AVR....................................................................................................64 Enviando e Recebendo Dados atravs da USART........................................................................65 Exemplo de utilizao do USART.................................................................................................68 Comunicao por IC..........................................................................................................................72 O Protocolo IC..............................................................................................................................72 ltima reviso: 21/12/2010 3
IC no AVR.....................................................................................................................................74 Bibliografia.........................................................................................................................................83
"Excerto do "Micro Tutorial AVR" de Njay (http://embeddeddreams.com/users/njay/Micro Tutorial AVR - Njay.pdf) com alteraes/adaptaes de Cynary (formatao e contedo)"
Introduo
"AVR" o nome de uma famlia 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 comercializao. Para quem souber ingls, podem ver uma pequeno vdeo 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"), memrias volteis e no- volteis e perifricos. Ao contrrio 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 instrues aumentou algumas vezes ao longo do tempo de forma a suportar mais funcionalidade). Assim de uma forma rpida podemos resumir a arquitectura do AVR nos seguintes pontos: Consiste num core de processamento, memria de programa (no voltil, FLASH), memria voltil (RAM esttica, SRAM), memria de dados persistentes (no voltil, EEPROM) e bits fuse/lock (permitem configurar alguns parmetros especiais do AVR). Arquitectura de memria Harvard (memria de programa e memria de dados separadas) A memria voltil (SRAM) contnua A maior parte das instrues tm 16 bits de tamanho, e este o tamanho de cada palavra na Execuo de 1 instruo por ciclo de relgio para a maior parte das instrues. Existem 32 registos de 8 bits disponveis e h poucas limitaes ao que se pode fazer com Os registos do processador e os de configurao dos perifricos esto mapeados (so Existe um vector de interrupo diferente por cada fonte de interrupo. Existem instrues com modos de endereamento complexo, como base + deslocamento O conjunto de instrues foi pensado para melhorar a converso de cdigo C em assembly.
seguido de auto- incremento/decremento do endereo. (A introduo do Micro tutorial do Njay mencionava mais alguns tpicos, que considerei como irrelevantes para este tutorial, logo cortei-os).
Programao 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 necessrio compreender como controlar as vrias funes 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 bsicas primeiro, respectivamente: Controlo da funcionalidade do micro-controlador os registers. Pseudo-cdigo/cdigo esqueleto MACROS Variveis volatile Operaes bit-wise em C. assumido que o leitor sabe programar em C e que domina os seguintes conceitos: comentrios, bibliotecas, variveis, funes, ponteiros, ciclos, condies, lgica e bases numricas.
registers e bits dos mesmos atravs 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 cdigo 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 no compreende exactamente como altermos um bit no register, no se preocupe, pois as operaes bit-wise sero explicadas de seguida.
Pseudo-cdigo/cdigo esqueleto
O pseudo-cdigo basicamente uma representao abstracta do cdigo, em linguagem natural. Muitas vezes comea-se por escrever o pseudo-cdigo, e depois vai-se substituindo por linhas de cdigo (muitas vezes o pseudo-cdigo transforma-se nos comentrios). 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 funes Input/Output, parte da inicializao incluir o header stdio.h, o resto comear a funo main(), e terminamos com return 0 (sair do programa com sucesso visto que nos AVRs no existe sistema operativo, a funo 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:
// 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 instrues comeadas por '#'. Estas no so instrues em C, mas sim instrues interpretadas apenas pelo pr-processador, antes da compilao. Por exemplo, quando fazemos #include <qualquercoisa.h>, estamos a indicar ao pr-processador para incluir o contedo do ficheiro qualquercoisa.h no nosso programa. Uma MACRO uma instruo deste tipo, que se comporta como uma funo. So teis quando queremos realizar certas tarefas repetidamente, mas no se justifica o custo em performance de chamar uma funo (para quem programa em C++, isto equivalente ao inline). Por exemplo, duas macros que costumo usar so as seguintes: #define max(I,J) ((I)>(J)?(I):(J)) #define min(I,J) ((I)<(J)?(I):(J)) Antes da compilao, o pr-processador substitui todas as declaraes de max(x,y) e min(x,y) pelo cdigo correspondente, sem ser assim necessrio chamar uma funo (as macros so teis para substituir principalmente funes com s uma linha de cdigo). H vrios pormenores envolvidos na criao de macro (como por exemplo, abusar das parntesis para proteger o cdigo), mas no interessam para este tutorial. No entanto, visto que so muito teis, aconselho os interessados a pesquisar sobre elas.
Variveis volatile
Quando declaramos variveis, podemos controlar certos aspectos de como o cdigo deve acedlas. Uma declarao importante quando se programa AVRs, devido existncia de interrupes, a volatile. Mais frente explicarei a importncia disto, por agora apenas importante reter que quando se declara uma varivel como volatile, estamos a informar que o seu valor pode ser alterado de formas inesperadas, logo deve sempre ir buscar o seu valor actualizado.
Operaes bit-wise em C
Muita da programao em micro-controladores consiste principalmente em manipular bits de certos registers. Para fazer isso, usamos as operaes bit-wise que manipulam valores ao nvel dos bits. ltima reviso: 21/12/2010 8
Para quem no compreende bases numricas, e no 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 dgitos diferentes (do 0 ao 9). Com combinaes deles, fazemos diferentes nmeros. Quando queremos um valor acima do dgito maior, transportamos mais um para a posio seguinte (se contarmos desde a direita). Assim podemos dar valores a cada posio no nmero. Por exemplo, o nmero 29 tem um 9 na posio 0 e um 2 na posio 1. A posio 0 corresponde ao valor 10 (1), e a 1 ao valor 10. Assim, podemos chegar ao nmero atravs da conta: 2*10 + 9*10. Nmeros de base binria funcionam da mesma forma que os de base decimal, com a particularidade de apenas utilizarmos dois algarismos: o 0 e o 1. Assim, cada posio tem um valor diferente. Por conveno, chamam-se s posies de um nmero em base binria de bit. Assim, quando falamos em manipular bits, estamos a falar em manipular o valor (0 ou 1) de certas posies. Por exemplo, o nmero 1001 (para facilitar a leitura, costumam-se ler os dgitos separados. Assim, em vez de se ler mil e um, l-se um zero zero um) corresponde ao nmero 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 so os nicos bits com dgitos l, chegamos ao 9 atravs da conta: 1*2+1*2. Agora que j conhecemos a base binria, e o que significa manipular bits, vamos ver como podemos manipul-los. Isto feito atravs de operaes bit-wise. Em C, existem cinco operaes bit-wise: | or & and ~ not ^ xor << shift left >> shift right As duas primeiras operaes funcionam como as operaes lgicas ||, &&. No entanto, em vez de testarem a varivel como um todo lgico, testam bit a bit, e o resultado corresponde a essa comparao bit a bit. Por isso, enquanto temos resultados bem definidos com as operaes | e &, as
operaes || e && podem dar um valor aleatrio para verdadeiro. Assim, quando se necessitam de valores lgicos, devem-se usar as operaes || e &&, e para manipulao bit a bit, devem-se usar as operaes | e & (nota: as operaes & e && podem ter resultados diferentes). Vamos ento comear por estudar essas duas operaes: O or retorna 0 quando ambos os bits so 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 nmeros, 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 nmero 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 nmeros tm um 1. 1 ou 1 = 1. Logo, o resultado ter um 1 no bit 3. Nos restantes bits, o primeiro nmero tem um 1, e o segundo tem um 0. 1 ou 0 = 1. Logo os restantes bits (bits 4 e 5) tero um 1 no resultado. Assim, chegamos ao nmero 111110. Podemos usar isto para colocar o valor 1 numa certa posio num nmero. Por exemplo, temos o nmero 1101 (em decimal o nmero 13), e queremos preencher aquele 0 com um 1. Se fizermos um ou com o nmero 0010 (em decimal o nmero 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 nmero 15 em binrio 1111. return 0; }
Vamos agora observar a operao &. O and retorna 0 quando pelo menos um dos bits 0, e 1 quando os dois bits so 1. Por exemplo: 1101 & 0111 = 0101 A anlise deste exemplo ser deixada como um desafio ao leitor. ltima reviso: 21/12/2010 10
O & muitas vezes usado para colocar a 0 um certo bit. Por exemplo: se tivermos o nmero 10111 (em decimal 23), e quisermos a partir dele obter o nmero 10101 (em decimal 21), podemos fazer a seguinte operao: 10111 & 1101 = 10101:
#include <stdio.h> int main(void) { int i = 23; i = i&13; // 13 em binrio 1101; equivalente a i &= 13; printf(%d\n, i); // Imprime 21 return 0; }
A terceira operao, ~ (not), tambm tem um comportamento semelhante ao seu equivalente lgico, o !. No entanto, foi separado das outras duas operaes, pois esta no pode ser usada como uma operao lgica, visto que ~(true) pode dar um valor verdadeiro mesma (interessantemente, devido forma como a aritmtica dos CPUs funcionam, fazer o ~ de qualquer nmero positivo d um nmero negativo e vice-versa, sendo a nica excepo o -1, j que ~(-1) = 0. No aprofundaremos mais isto, visto que no interessa muito para programar micro-controladores). Vejamos como funciona: ~1101 = 0010 Cada bit do nmero original invertido, logo a partir de um nmero positivo (true), podemos no obter 0 (false), que exactamente o que o ! lgico faz. O ~ muitas vezes utilizado em conjuno com o & para pr um valor 0 num bit. Vejamos porqu: 11101 & 10111 = 10101 Sabendo a posio do bit que queremos pr a 0 (neste caso a posio 4), como chegamos ao seu inverso, de forma a manter o resto do nmero intacto. Usando o ~, claro! Neste caso, fazer: 11101 & 10111 = 10101 igual a fazer:
11
11101 & (~01000) = 11101 & 10111 = 10101 (mais frente iremos estudar como criar um nmero com apenas um 1 na posio pretendida, sabendo apenas essa posio). Por exemplo, com cdigo agora (reformulao 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, no tem um equivalente lgico bvio. o mesmo que !=. O seu comportamento o seguinte: retorna 0 quando ambos os nmeros so iguais, e 1 quando so diferentes. Como o |, quando se procura um resultado lgico, equivalente usar o ^ e o !=. Vejamos ento 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 nmero 11x1, e quisermos alterar o estado do bit 1, sem conhecermos o seu valor, basta fazer a seguinte operao: 11x1 ^ 0010 Isto porque quando fazermos um xor com 0, o resultado sempre igual ao do outro nmero (1^ 0 = 1; 0^0 = 0), e quando fazemos um xor com 1, altera sempre (1^1 = 0; 1^0 = 1). (visto que o cdigo de exemplo seria semelhante aos anteriores, iremos passar frente desse passo). Agora s nos falta estudar os operadores de shift. Estes so muito teis porque nos permitem pr um valor em qualquer posio do nmero, ou seja, fazer shift para cima ou para baixo desse mesmo valor. Vamos utilizar o exemplo do ~ e do &. Sabendo apenas a posio em que queremos pr o 0, e o
12
nmero que tem essa posio a 1, e as restantes a 0, j sabemos que operao utilizar. Mas ainda nos falta uma coisa: como chegamos ao nmero que tem a posio desejada a 1? Para isso usam-se os operadores de shift. Por exemplo, se quisermos colocar o 1 na posio 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 tcnica tambm 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. Tambm existe o operador de shift >>, que faz o contrrio 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 caractersticas das bases numricas, 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 programao necessrias para programar micro-controladores. Esperamos que o leitor esteja agora preparado para se aventurar no mundo da programao low-level dos mesmos!
13
"Excerto do "Micro Tutorial AVR" de Njay (http://embeddeddreams.com/users/njay/Micro Tutorial AVR - Njay.pdf) com alteraes/adaptaes de Cynary (formatao e contedo)"
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 funo, a que s vezes tambm chamamos "tipo de pino".
14
Portanto, num sistema que funcione com uma tenso de alimentao 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 tambm serve como forma de desligar o pino do circuito. Neste caso no estamos interessados em ler valores. Ao configur-lo como entrada, ele no afecta electricamente (de um ponto de vista digital) o circuito exterior ao chip e portanto como se tivssemos cortado o pino do chip. Diz-se que o pino est em "alta impedancia" ("high-Z" em ingls, 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.
"pull-up fraco", ou "weak pull-up" em ingls. Esta expresso pull-up ("puxa para cima") vem de estarmos a ligar tenso de alimentao positiva, que mais "alta" do que a massa, os 0V. Para este termo contribui ainda o facto de geralmente se desenhar a linha de alimentao positiva no topo dos esquemas, e a massa em baixo. Tambm podemos falar em pull-down ("puxa para baixo") quando nos referimos a ligar massa. Podemos criar pull-downs ligando resistncias massa, mas tipicamente os chips no suportam este tipo de pull, por razes que fogem ao mbito deste artigo que se quer simples.
(0V); o transstor de cima mantm-se desligado 2. quando queremos ter um "um" sada, liga-se o transstor de cima, que liga o pino tenso
Na imagem acima podemos ver uma sada 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 no se devem ligar 2 (ou mais) sadas umas s outras. Se uma delas estiver com um "1" e a outra com um "0", estamos a criar um curto-circuito na alimentao, ligando +V massa. Numa das sadas est ligado o interruptor de cima e na outra est ltima reviso: 21/12/2010 16
ligado o de baixo. Mesmo que isto s acontea durante um perodo de tempo muito pequeno (milisegundos, microsegundos ou menos), vo passar correntes elevadas, fora das especificaes dos chips, e se acontecer regularmente, comea um processo de degradao que leva falha do chip em segundos, horas, semanas, meses ou anos. Uma sada totem-pole tem ainda um 3o estado: "no ar". outra forma de desligar um pino, mas que usada quando o pino sempre uma sada (no configurvel). No caso de um GPIO, este pode ser configurado como entrada ficando assim desligado do circuito exterior, como vimos atrs.
A sada em colector aberto consiste num simples interruptor electrnico (transstor) capaz de ligar o pino massa. Quando o interruptor est ligado a sada 0, e quando est desligado a sada ... no sabemos. O pino fica "no ar" e portanto qualquer outro dispositivo exterior ao chip que esteja ligado ao pino pode l colocar a tenso que entender. Num bus IC o que se passa que existe uma resistncia externa que mantm as linhas com tenso positiva quando nenhum dispositivo est a transmitir; ou seja, temos um "pull-up fraco". A partir da, qualquer um dos dispositivos pode forar uma das linhas IC a ter 0V, se activar o interruptor electrnico na sua sada em colector aberto.
17
Um pino entra no 4 modo quando o respectivo perifrico activado, pelo que no vamos debruar-nos aqui sobre isso. A cada porto est associado um conjunto de 3 registos que so 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. 2. 3. PINx - L o estado actual do pino DDRx - Data Direction Register (registo de direco dos dados) PORTx - Define o estado da sada 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 lgica 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 tm uma lgica similar a este diagrama. O registo PINx apresenta sempre o valor lgico ("0" ou "1") que estiver presente num pino independentemente da configurao. como se o registo estivesse ali a medir a tenso directamente
18
no pino e a reportar o seu valor lgico ao software. O registo DDRx define, para cada pino do porto, se uma entrada ou uma sada. Aps o reset do chip, todos os pinos esto 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 sada temos que colocar a 1 o respectivo bit no registo DDRx. Se um pino estiver configurado como uma sada (se o respectivo bit no registo DDRx for 1), podemos ento definir o estado da sada 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, ento o pull-up est activado. Cada AVR tem um certo nmero de portos. Cada porto pode no ter pinos fsicos do chip associados a todos os seus bits. As datasheets da ATMEL (fabricante dos AVR) apresentam logo na 2 pgina o pinout (a atribuio 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 includa):
Isto diz-nos que este modelo de AVR tem 4 portos, A, B, C e D. Logo, para configurao dos pinos, este AVR tem os registos DDRA, PORTA, PINA, DDRB, PORTB, PINB, DDRC, PORTC, PINC, DDRD, PORTD, e PIND. Os nomes entre parntesis so os nomes associados aos perifricos ltima reviso: 21/12/2010 19
20
Interrupes
O que uma interrupo?
Irei agora comear a falar de interrupes a partir do mais bsico o que uma interrupo? Uma interrupo 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 interrupo foi o toque com o pato que interrompeu o discurso.
21
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, no so? xD), com um argumento: o nome do vector da interrupo. O vector da interrupo basicamente o que identifica qual a interrupo com que estamos a lidar. Esta informao encontra-se na pgina 57 do datasheet que eu tenho (incio do captulo sobre interrupes/captulo 9), numa tabela, na coluna Source. Por exemplo, no tpico a seguir, vamos lidar com a interrupo que ocorre no pino digital 2. Este pino tem o nome de INT0. Ao olharmos para a tabela, vemos que a source da interrupo 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) { // (cdigo que inicialize variveis, LEDs, etc. aqui) // Ligar interrupes particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupo } (para alguns, esta declarao da funo ISR pode ser confusa, pois no tem tipo. No entanto, lembrem-se que uma macro, e por isso ISR no realmente o que fica no cdigo final). Nota: Se notarem, alguns dos Vectores na datasheet tm espaos ou vrgulas no nome. Basta substituir esses por _. Por exemplo, para a interrupo gerada quando se recebem dados por serial, temos o seguinte Source: USART, RX. O argumento que usamos para a macro ISR : USART_RX_vect.
int main(void) { // (cdigo que inicialize variveis, LEDs, etc. aqui) // Ligar interrupes particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupo } Vamos usar para este efeito o pino 2 (podia ser feito com o pino 3 tambm, com poucas diferenas). O objectivo deste cdigo vai ser mudar o estado de um LED quando se toca num boto ligado ao pino 2. O LED vai estar ligado ao pino digital 4 (PD4). Vamos comear por criar uma varivel global, com o estado do pino e inicializar esse pino como output (vai comear desligado) (para mais informaes sobre GPIOs, ler o tutorial que pus no primeiro post), e j vamos colocar o cdigo necessrio 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 interrupes particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupo output = ~output; // Alterar o estado PORTD &= ~(1<<PD4); // Desligar o pino isto necessrio 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 interrupo particular para o INT0.
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 boto, o mais fcil 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 cdigo: // 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 interrupo no pino INT0 para quando este transita para HIGH // Ligar interrupes particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupo output = ~output; // Alterar o estado PORTD &= ~(1<<PD4); // Desligar o pino isto necessrio 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 interrupo associada ao INT0. O bit que controla isto o INT0 no register EIMSK. Assim, s modificar o cdigo, e fica completo: // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> char output = 0; // Estado do led. ltima reviso: 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 interrupo no pino INT0 para quando este transita para HIGH EIMSK |= (1<<INT0); // Ligar interrupes particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupo output = ~output; // Alterar o estado PORTD &= ~(1<<PD4); // Desligar o pino isto necessrio 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 cdigo que, ao clicarmos num boto que liga o pino digital 2 e o plo positivo, faz toggle do pino digital 4 (nota: visto que no fizemos nada para tratar do bouncing, podem haver resultados inesperados. No entanto, como isto s para exemplo, no considermos muito importante). Para experimentarem, podem montar o seguinte circuito:
no cdigo do main(), no estaramos a aceder ao valor correcto da varivel. Isto acontece porque o compilador no considera que podemos aceder funo ISR s com aquele cdigo, logo apenas carrega a varivel da memria ram para os registers uma vez, e depois no actualiza o seu valor, que alterado na ISR. Para resolver isto, dizemos ao cdigo que a varivel output volatile, declarando-a assim: volatile char output; Isto indica ao compilador que a varivel 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. No pode, por exemplo, carregar um int da memria numa s instruo, pois estes tm 16 bits. Mas as interrupes podem ocorrer em qualquer parte do programa logo o que pensam que acontece se tirarmos a primeira metade de um inteiro da memria, e antes de tirarmos a segunda metade ocorrer uma interrupo que altere essa varivel? Resultados no previsveis obviamente Logo, o que podemos fazer para evitar isto? O que podemos fazer desligar interrupes nesses blocos de cdigo que no podem ser perturbados (nota: se estiverem a carregar um char no deve haver problemas, visto ter apenas 8 bits). Isto feito facilmente com a funo cli() (tambm no header <avr/interrupt.h>). Depois do cdigo efectuado, basta ligar novamente as interrupes com sei(). Por exemplo: //... int i,j; for(;;) { cli(); j = i; // o i alterado numa interrupo sei(); // } //... E com isto acabamos a base das interrupes. Decidi comear com estas, pois nos prximos tutoriais, explicarei as interrupes individuais de vrias funcionalidades.
27
Timers
O que so e como funcionam timers?
Timers so, 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 varivel a cada pulso. Se usarmos uma fonte de pulsos com uma frequncia conhecida (por exemplo, o clock do AVR tem uma frequncia 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 trs timers: timer0, timer1 e timer2. Cada um difere em vrios aspectos, mas o mais significativo o nmero de bits: o timer0 e timer2 tm cada um 8 bits, e o timer1 16 bits. Os outros aspectos so os modos que suportam, portas que controlam, etc. No entanto, estes no afecta muito a sua funcionalidade, visto que para fazer uma mesma coisa em dois timers, s so necessrias algumas mudanas nos registers e pinos usados. Mas o nmero de bits usados afecta bastante a funcionalidade, pois limitam a resoluo 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, no podemos contar at 1s com nenhum, mas por exemplo, com um clock de 65kHz, conseguimos contar at 1s com o de 16 bits, e no com o de 8 bits. No entanto, quando atingem o seu limite, os timers no param de contar, apenas comeam novamente do zero (isto significa que possvel 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 tcnicas de como fazer isto sem necessitar de software). Os timers podem ser usados em diferentes modos. No AVR, existem trs 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 prximo tutorial Todos os timers oferecem estes trs modos. No entanto, os modos CTC e PWM podem ter certos pormenores na sua utilizao/configurao 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 reviso: 21/12/2010 28
como se fosse um), at atingir o seu mximo, e depois volta a 0. A frequncia com que incrementa esse valor depende do clock utilizado. Cada timer tem um conjunto de clocks disponveis. Neste tutorial vamos utilizar apenas o clock do sistema (clkI/O), e os seus prescalers (um prescaler corresponde a dividir a frequncia original por um certo valor. Os valores disponveis no AVR so: 1, 8, 64, 256 e 1024. A utilizao de um prescaler para o timer no 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 resoluo e modos. No entanto, a maior parte das coisas descritas aqui podem aplicar-se aos outros se tiverem o modo correspondente disponvel, e ajustando-se o cdigo menor resoluo que oferecem e aos seus registers.
Para poder explicar mais facilmente como usar um timer, vamos estabelecer um objectivo: criar uma funo que funcione como a funo _delay_ms(). Vamos comear com o pseudo-cdigo: // Iniciar o programa // Iniciar a funo new_delayms(x) // Iniciar o timer // Verificar o timer para ver se j se passaram x ms. // Terminar a funo J podemos inserir algumas coisas: a declarao da funo, o loop em que se vai verificar o valor do timer e os headers necessrios para aceder aos registers: <avr/io.h> (vamos esquecer a funo main neste caso): //Iniciar o programa #include <avr/io.h> void new_delayms(int x) {// Iniciar a funo new_delayms(x) //Iniciar o timer for(;;) { //Verificar o timer para ver se j se passaram x ms. } } // Terminar a funo Para comear, vamos compreender como se sabe quanto tempo passou, tendo em conta o valor do timer: Sabemos que o timer incrementa a sua varivel de acordo com uma certa frequncia, e que a frmula para calcular a frequncia : f = incrementos/s Neste caso, visto que queremos os milisegundos, podemos ajustar a frmula: f/1000 = incrementos/ms Nesta frmula, o nico valor que podemos conhecer do incio a frequncia (como vamos usar o clock do sistema, ser 16MHz), logo podemos j ajustar essa parte da frmula: 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 comearmos do 0 no timer. No entanto, muitos esto a pensar numa coisa, provavelmente: limites. Sabemos que o limite para 16 bits : 65535, logo, no mximo podemos medir: 65535 = 16000*ms ltima reviso: 21/12/2010 <=> ms = 65535/16000 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 resoluo: utilizar prescalers, que diminuem o clock, ou manipulao por software. A utilizao de prescalers apropriada para casos particulares. No entanto, a manipulao por software mais apropriada aqui, visto que nos permite calcular o tempo passado para qualquer valor possvel de int (para fazermos o mesmo com um timer de 16 bits, necessitaramos de um prescaler de 16k, o que no existe). Vamos ento fazer isto passo-a-passo: inicializar o timer. Os bits de configurao/inicializao do timer (mais respectivamente de seleco 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, no 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 comea sem nenhum clock seleccionado, logo no est a incrementar o register correspondente, e esse inicializado automaticamente a 0. Assim que seleccionamos um clock, o timer comea 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 funo new_delayms(x) TCCR1B |= (1<<CS10); //Iniciar o timer for(;;) { //Verificar o timer para ver se j se passaram x ms. } } // Terminar a funo O mtodo por software que vamos usar o seguinte: numa varivel guardamos o valor anterior do timer. Se o novo valor for menor, significa que houve um overflow, e incrementamos uma varivel que nos indica quantas vezes passaram 4ms (aviso: isto apenas se aplica para uma frequncia de 16MHz. Se querem manipular o vosso programa para se adaptar a outras frequncias, podem usar a macro F_CPU, que indica a frequncia, e utilizar as frmulas acima, para descobrir ltima reviso: 21/12/2010 31
quantos ms passam em cada overflow). Depois guardamos o novo valor na varivel e vemos quanto tempo se passou at ento (o valor da varivel dos 4ms+os milissegundos passados, cuja frmula : ms = valor/16000), e caso tenha passado o tempo desejado ou mais (ao executarmos as instrues pode passar mais tempo do que o desejado), paramos o ciclo, saindo assim da funo. Os registers onde o timer1 guarda o valor do timer so TCNT1H e TCNT1L (so dois por ter 16 bits). A datasheet descreve a ordem em que estes devem ser acedidos. No entanto em C no 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 funo 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 varivel 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 funo E a temos uma funo de delay que funciona com 16MHz. Esta a forma mais bsica de se usar timers, e atravs do cdigo usado, parecem-nos ineficientes e bsico. Usados desta forma, timers no so muito teis tornam-se realmente teis quando comeamos a explorar a sua utilizao com eventos e interrupes, que so os temas dos prximos tpicos.
objectivo: piscar um LED (ligado 1s, desligado 1s, ligado 1s, desligado 1s, ), sem utilizar qualquer controlo por software/interrupes, apenas as configuraes dos pinos. O pino usado neste exemplo ser o pino OC1A (pino digital 9). Logo, o nosso pseudo-cdigo 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 distncia de tempo em que ocorre esse evento constante igual ao tempo que demora para incrementar a varivel 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 iteraes, MAX = 65535, nota), mas com um erro grande que aumenta em cada evento, logo no o ideal. As contas feitas so as seguintes: FREQ = F_CPU/prescaler F_CPU = 16000000 prescaler = 256
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 trs, onde explico este modo, iro 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 quisssemos gerar o evento no pino OC1B, teramos de usar ICR1 ou OCR1A como TOP, e colocar o mesmo valor no register OCR1B). Ento, 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 so configurados atravs 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 reviso: 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 tm: um programa que faz toggle do pino OC1A (pino digital 9) a cada 1s. Podem testar isto com um LED, como no esquema abaixo:
Tambm podemos forar 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 configurao, no gerando quaisquer interrupes associadas com uma igualdade ao register OCR1A nem fazendo reset ao timer. O tipo de eventos que estudmos primeiro, foram os eventos de output. No entanto, tambm 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 so 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. Ateno forma como isto se funciona: devem lembrar-se que o register ICR1 pode ser usado como valor de TOP no modo CTC (e PWM tambm, mas fica para outro tutorial). Quando isto acontece, estes eventos de input so desligados. Se se lembram do tutorial anterior, demonstrei como usar um interrupt associado com um pino digital para detectar um clique num boto. 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 reviso: 21/12/2010 35
controlo de bouncing automtico, ao testar o valor do pino 4 vezes, a ver se igual nessas 4 vezes (nota: esses quatro testes so independentes do prescaler do timer, visto que so feitos de acordo com o clock do sistema). No prximo tpico, em que falamos sobre interrupes e timers, demonstraremos uma forma de usar este filtro, em vez da forma bsica demonstrada no tutorial anterior. E estes so os principais eventos associados aos timers, relacionados com hardware. Para acabar, s faltam agora as interrupes.
Interrupes e timers.
Existem 4 interrupes 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 (ateno, que no modo CTC, o overflow ocorre apenas se o timer chegar ao valor MAX, e no ao top, logo pode nunca ocorrer. Esta interrupo pode ser usada para, por exemplo, verificar a ocorrncia de erros quando se muda o valor de TOP). Estas interrupes esto todas definidas no register TIMSK1. Os vectores associados s mesmas so: TIMER1_CAPT_vect TIMER1_COMPA_vect TIMER1_COMPB_vect TIMER1_OVF_vect O tutorial anterior mostrou como lidar com interrupes, por isso ser deixado imaginao do leitor o que fazer com estas. No entanto, a ttulo de exemplo, iremos cumprir o prometido no tpico anterior: codificar um programa com a mesma funo que o do tutorial anterior, s que com controlo de bouncing feito pelo hardware. Neste caso, no usamos nada relacionado directamente com o timer, excepto a interrupo de input. No entanto, isto, em conjuno com outros interrupts, pode ser usado para fazer um programa que registe a distncia, 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 critrio). Como no usamos a funcionalidade do timer, no 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-cdigo:
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 interrupo para o input. // Ligar as interrupes globais. // Loop eterno // Definir a ISR para o input: TIMER1_CAPT_vect // Fazer toggle do pino digital 4 Vamos comear pelo mais bsico: // 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 interrupo para o input. sei(); // Ligar as interrupes 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 explicaes anteriores, os bits usados para configurar o evento de input so 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 interrupo para o input. sei(); // Ligar as interrupes globais. for(;;); // Loop eterno ltima reviso: 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 interrupo particular para o input. Como disse antes, as interrupes dos timers so 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 interrupo para o input. sei(); // Ligar as interrupes 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 interrupes dos timers! O circuito que usa isto o seguinte:
38
PWM e Phase and Frequency Correct PWM. Os modos Fast PWM e Phase/Phase and Frequency Correct PWM so 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 configurao dos bits COM1x0 e COM1x1). Depois continua a contar at TOP (que pode ter vrios 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 so 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 frequncia do PWM. De resto, estes modos so 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 configurao, ele liga-o ou desliga-o). Depois continua at chegar ao TOP, momento em que comea a contar para trs, at atingir novamente o register OCR1x, momento em que actualiza o pino OC1x, de acordo com a configurao. A vantagem destes modos que as ondas geradas tm sempre a mesma fase (o que necessrio 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 frequncia mais alta deste modo metade da possvel no modo Fast PWM.
Abaixo esto alguns grficos 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 (sero estas as configuraes usadas neste tutorial):
40
Vamos agora ver como podemos usar estes modos no AVR, atravs de exemplos:
Fast PWM
O modo Fast PWM o mais simples de todos, e muito fcil 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 mximo, 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 interrupo 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 varivel, para depois multiplicar pela variao do OCR1A, e alteramos quando chegamos ao TOP/BOTTOM. Visto que impossvel 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 perodo do PWM para 100ms, e assim aproveitar o timer1 para essas interrupes (o que no a melhor soluo visto que assim o LED piscaria 10 vezes por segundo, o que j se torna visvel ao olho humano), ou podemos usar uma interrupo no timer0/timer2 (iremos usar o 0) a cada 10ms (o que possvel com um prescaler de 1024, com um certo erro que iremos ignorar), e s fazer uma aco 10 vez que essa interrupo 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 variao necessria do OCR1A, iremos estabelecer um objectivo: o LED ir de desligado at luminosidade mxima em 2s. Para uma frequncia 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 variao ser: 255/20 12 (isto tem um erro associado, mas iremos ignor-lo). Vamos ento comear com Pseudo-Cdigo:
//Iniciar o programa // Definir o pino digital 9 como OUTPUT ltima reviso: 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 mxima frequncia). // Configurar o timer0 com prescaler de 1024, TOP de 156, modo CTC // Ligar interrupes de compare match no timer0 // Ligar interrupes globais // Loop eterno // ISR de compare match no timer0 // incrementar contador // Se o contador for igual a 10 // Incrementar/decrementar OCR1A por um factor prestabelecido. // Caso tenhamos atingido o valor mximo de OCR1A/BOTTOM // Alterar a operao de incremento/decremento // Fazer reset ao contador.
Vamos j comear a preencher o que j sabemos (visto que a configurao do timer0 semelhante do timer1, iremos tambm 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 reviso: 21/12/2010 42
prescaler, para mxima frequncia). 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 interrupes de compare match no timer0 sei(); // Ligar interrupes 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 prestabelecido. if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso tenhamos atingido o valor mximo de OCR1A/BOTTOM mult = -mult; // Alterar a operao de incremento/decremento } count = 0; // Fazer reset ao contador. } }
S nos falta mesmo configurar o PWM agora! Vamos ento ver a datasheet O que queremos o modo Fast PWM, com uma resoluo 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 reviso: 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 mxima frequncia). 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 interrupes de compare match no timer0 sei(); // Ligar interrupes 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 prestabelecido. if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso tenhamos atingido o valor mximo de OCR1A/BOTTOM mult = -mult; // Alterar a operao 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 comear com o LED desligado, iniciamos o ltima reviso: 21/12/2010 44
OCR1A com o seu maior valor, e a varivel 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 mxima frequncia). 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 interrupes de compare match no timer0 sei(); // Ligar interrupes 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 prestabelecido. ltima reviso: 21/12/2010 45
if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso tenhamos atingido o valor mximo de OCR1A/BOTTOM mult = -mult; // Alterar a operao 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:
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 perodo em HIGH determina a o ngulo ou direco e sentido para controlar o motor. O perodo 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 no 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 criao de uma funo que altera a posio do servo, tendo em conta que este se encontra ligado ao pino OC1A, e que o PWM j foi inicializado para uma frequncia fixa de 50Hz, no timer1 (neste caso, ir receber um valor entre 0 e 100, para definir a posio 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 frequncia de 50Hz // Chamar a funo rodar, para ir todo para a esquerda (valor dado: 0). // Loop eterno.
47
// Funo 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 cdigo implica muita matemtica. Primeiro, temos de descobrir como fazer uma frequncia de 50Hz: O clock 16MHz, logo, a onda ter de ocorrer a cada 16M/50 = 320K ciclos para ter 50Hz. No entanto, isto no 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 frequncia 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 frequncia 50Hz. Como nunca mudamos a frequncia, 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 frmula: (x+100)/100 No entanto, isto no suficiente para colocar no OCR1A. Visto que iremos usar a configurao 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 frmula geral (x estando entre 0 e 100): OCR1A = 20000-2000*(x+100)/100/2 = 20000-10*(x+100)
Assim, j podemos criar a nossa funo rodar (e j agora, adicionamos aquilo que j sabemos fazer): ltima reviso: 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 frequncia de 50Hz rodar(0); // Chamar a funo rodar, para ir todo para a esquerda (valor dado: 0). for(;;); // Loop eterno. } void rodar(int x) {// Funo 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 so o dois semelhantes o suficiente para no fazer diferena em nada :P Caso queiram usar, basta ver na datasheet as diferenas), e segundo a datasheet, faz-se isso colocando o bit WGM13 a 1, no register TCCR1B.
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 frequncia de 50Hz rodar(0); // Chamar a funo rodar, para ir todo para a esquerda (valor dado: 0). for(;;); // Loop eterno. } void rodar(int x) {// Funo 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 alcanado 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 frequncia de 50Hz ltima reviso: 21/12/2010 50
rodar(0); // Chamar a funo rodar, para ir todo para a esquerda (valor dado: 0). for(;;); // Loop eterno. } void rodar(int x) {// Funo 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 funo para controlar servos! Podem usar isto com o circuito abaixo:
51
Analog-to-Digital Converter
Formato Analgico e Digital
No mundo da electrnica, podemos identificar dois tipos de sinais: os digitais e os analgicos. Os sinais digitais distinguem-se por poderem ter apenas dois estados (normalmente designados por desligado/ligado, 0/1, e muitas vezes representados pelas diferenas de potencial 0V e 5V, respectivamente). Os sinais analgicos, no entanto, podem ter vrios 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 analgico, j que a diferena 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 analgico.
O que o ADC?
O ADC (Analog-to-Digital Converter) uma funcionalidade do micro-controlador AVR que permite converter uma certa diferena de potencial (entre 0 e um valor de referncia) num valor numrico, de acordo com a resoluo pretendida. O ADC do AVR tem uma resoluo mxima de 10 bits. Isto significa que nos pode dar valores entre 0 e 1023. Se utilizarmos como valor de referncia AVcc (o mais comum um pino do ADC que deve estar sempre ligado a uma diferena de potencial muito prxima do Vcc do microcontrolador, normalmente 5V), significa que temos uma preciso de 5/1023 0,0049, ou seja, de cerca de 4,9 mV. Por exemplo, se o ADC tiver como input uma diferena de potencial de 2,5V, ir retornar o valor decimal 510.
ADC5 (PC5). Para seleccionar um destes manipulamos os bits MUXn no register ADMUX (podemos tambm ler o input ADC8, que consiste num sensor de temperatura interno, que discutiremos noutro tpico) consultar datasheet para saber quais os valores a usar. Para escolher qual a diferena de potencial utilizada como referncia, temos primeiro de conhecer as opes: AREF (uma diferena de potencial externa, ligada ao pino com esse nome), AVcc (uma referncia interna, que tem de estar prxima do valor de Vcc, normalmente 5V) e uma referncia interna de 1.1V (apesar de parecer intil para a maior parte dos usos, utilizada para o sensor de temperatura interno que explicaremos mais frente). Podemos seleccionar qual das opes 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 ateno que no podemos usar como referncia as diferenas de potencial internas quando aplicamos uma diferena 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 converso no ocorre em apenas um ciclo. Por isso -nos til definir gatilhos que iniciem a converso as opes que temos so flags de interrupes, ou seja, funciona como se fosse uma interrupo que ocorre separadamente do nosso cdigo. Ateno que, caso no lidemos com a interrupo utilizada como gatilho, nem que seja s definir uma ISR que no faa nada, de forma a desabilitar a flag de interrupo, a converso 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 no liguemos as interrupes globais e particulares. Neste documento apenas iremos utilizar o gatilho free-running mode, j que o funcionamento das interrupes 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 frequncia do clock do ADC. Para seleccionarmos esta frequncia, temos de trabalhar com um prescaler, que funciona muito como o dos timers: F_CPU/PRESCALER. Segundo a datasheet, para ter a mxima resoluo, temos de uma frequncia entre 50kHz e 200kHz. Visto que F_CPU = 16000000, o nico valor disponvel que nos d uma frequncia adequada o 128, dando-nos uma frequncia de 125kHz (ateno: se no desejarem a frequncia mxima de 10bits, podem usar um valor mais alto para a frequncia, no entanto, a datasheet no diz mais nada alm disto), logo esse que utilizaremos neste documento. Agora que j tirmos 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 varivel (no ir fazer nada com ele mais frente, iremos explorar como fazer um programa mais til, construindo um ltima reviso: 21/12/2010 53
pequeno sensor de distncia analgico) neste caso iremos usar o pino ADC0. Comecemos com o pseudo-cdigo: // Iniciar programa // Configurar o ADC: // Prescaler de 128 // Referncia AVcc // Configurar o multiplexer para usar o ADC0 // Ligar o ADC // Iniciar uma converso no ADC // Esperar at que a converso esteja terminada // Ler o valor do ADC e guard-lo numa varivel // 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 // Referncia AVcc // Configurar o multiplexer para usar o ADC0 // Ligar o ADC // Iniciar uma converso no ADC // Esperar at que a converso esteja terminada // Ler o valor do ADC e guard-lo numa varivel for(;;); // Ciclo eterno } Vamos comear por configurar o ADC. Visto que no iremos usar nenhum gatilho automtico, no 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 diferena de potencial de referncia 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, no temos de fazer nada quanto seleco 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 reviso: 21/12/2010 54
Prescaler de 128 ADMUX |= (1<<REFS0); // Referncia AVcc // Configurar o multiplexer para usar o ADC0 feito por defeito // Ligar o ADC // Iniciar uma converso no ADC // Esperar at que a converso esteja terminada // Ler o valor do ADC e guard-lo numa varivel 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); // Referncia AVcc // Configurar o multiplexer para usar o ADC0 feito por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC // Iniciar uma converso no ADC // Esperar at que a converso esteja terminada // Ler o valor do ADC e guard-lo numa varivel for(;;); // Ciclo eterno } Para iniciar uma converso, temos de colocar um 1 no bit ADSC (register ADCSRA). Este bit lido como 1 enquanto se est a processar uma converso. Quando fica com o valor 0, significa que converso terminou. Assim, para esperarmos que a converso termine, basta testar o valor deste bit (esta no a melhor forma de fazer isto especialmente neste cdigo em que o melhor seria utilizar uma varivel global e uma interrupo, 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); // Referncia AVcc // Configurar o multiplexer para usar o ADC0 feito por defeito ltima reviso: 21/12/2010 55
ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADSC); // Iniciar uma converso no ADC while(ADCSRA&(1<<ADSC)); // Esperar at que a converso esteja terminada // Ler o valor do ADC e guard-lo numa varivel for(;;); // Ciclo eterno } Agora s nos falta ler o valor que resultou da converso. Este valor guardado em dois registers: ADCL e ADCH. Estes tm de ser lidos numa ordem especfica (primeiro o ADCL e depois o ADH). Mas como usamos C, isto abstrado 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); // Referncia AVcc // Configurar o multiplexer para usar o ADC0 feito por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADSC); // Iniciar uma converso no ADC while(ADCSRA&(1<<ADSC)); // Esperar at que a converso esteja terminada adc_value = ADC; // Ler o valor do ADC e guard-lo numa varivel for(;;); // Ciclo eterno }
56
AIN0 significa o analog pin 0 do arduino, que corresponde ao pino ADC0 do AVR. No irei discutir como funciona este sensor de distncia, deixando-o como um desafio ao leitor (nota: isto s funciona para detectar distncias, quando a cor constante, e funciona para detectar cores/reflectividade quando a distncia constante). Vamos comear por definir um objectivo: queremos que o ADC mea constantemente o input do sensor de distncia, e guarde o valor que esse nos d numa varivel (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 automtico em que o ADC est constantemente a realizar converses, e em que no nos precisamos de preocupar em colocar flags de interrupes a 0. No entanto, enquanto nos outros modos no precisamos de iniciar as converses, neste temos de ordenar ao ADC que faa uma converso primeiro), e utilizaremos interrupes associadas ao ADC para actualizar a varivel. Visto que no precisamos de uma resoluo 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 funo 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, poderamos utilizar uma frequncia maior que 125kHz, mudando para isso o prescaler, mas no o faremos neste caso). Visto que ligamos o sensor ao terminal positivo de 5V do arduino, no necessrio utilizar uma ltima reviso: 21/12/2010 57
referncia de diferena de potencial externa, utilizando-se assim o AVcc. Comecemos com o pseudo-cdigo: // Iniciar programa // Criar uma varivel com o valor do sensor de distncia do ADC // Configurar o ADC: // Resoluo de 8 bits // Prescaler de 128 // Referncia: AVcc // Gatilho automtico: Free-running mode // Input: ADC0 seleccionado por defeito // Ligar o ADC // Ligar a interrupo particular do ADC // Ligar as interrupes globais // Iniciar as converses no ADC // Ciclo eterno // Fazer algo com o valor da varivel do sensor de distncia // Definir uma interrupo que ocorre a cada ocorrncia de uma converso // Ler o valor do ADC, e guard-lo numa varivel J podemos preencher algumas coisas: // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> // Criar uma varivel com o valor do sensor de distncia do ADC volatile unsigned char adc_distance; // char porque s precisamos de 8 bits int main(void) { // Configurar o ADC: // Resoluo de 8 bits ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referncia: AVcc // Gatilho automtico: Free-running mode // Input: ADC0 seleccionado por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC // Ligar a interrupo particular do ADC sei(); // Ligar as interrupes globais ADCSRA |= (1<<ADSC); // Iniciar as converses no ADC for(;;) {// Ciclo eterno // Fazer algo com o valor da varivel do sensor de distncia } ltima reviso: 21/12/2010 58
} ISR() {// Definir uma interrupo que ocorre a cada ocorrncia de uma converso adc_distance = ADCH; // Ler o valor do ADC, e guard-lo numa varivel } Para resoluo de 8 bits, colocamos o bit ADLAR a 1 no register ADMUX. A interrupo que utilizaremos a nica relacionada com o ADC a que ocorre quando se realiza uma converso. O seu vector : ADC. Para a ligar, colocamos o valor 1 no bit ADIE do register ADCSRA. Para seleccionar o gatilho automtico de free-running mode, necessitamos primeiro de ligar o modo de gatilho automtico (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 no 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 varivel com o valor do sensor de distncia do ADC volatile unsigned char adc_distance; // char porque s precisamos de 8 bits int main(void) { // Configurar o ADC: ADMUX |= (1<<ADLAR); // Resoluo de 8 bits ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referncia: AVcc ADCSRA |= (1<<ADATE); // Gatilho automtico: Freerunning mode // Input: ADC0 seleccionado por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADIE); // Ligar a interrupo particular do ADC sei(); // Ligar as interrupes globais ADCSRA |= (1<<ADSC); // Iniciar as converses no ADC for(;;) {// Ciclo eterno // Fazer algo com o valor da varivel do sensor de distncia } } ISR(ADC_vect) {// Definir uma interrupo que ocorre a cada ocorrncia de uma converso ltima reviso: 21/12/2010 59
adc_distance = ADCH; // Ler o valor do ADC, e guard-lo numa varivel } 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 no tm nenhum efeito observvel, 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 distncia. Basicamente, vamos fazer com que um LED esteja ligado quando o valor de input seja maior que 2,5V (~128, visto que usamos uma resoluo de 8 bits), e desligado quando menor. Para comear, vamos adicionar ao circuito anterior, estas ligaes:
E agora, o cdigo: // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> // Criar uma varivel com o valor do sensor de distncia 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); // Resoluo de 8 bits ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referncia: AVcc ADCSRA |= (1<<ADATE); // Gatilho automtico: Freerunning mode // Input: ADC0 seleccionado por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADIE); // Ligar a interrupo particular do ADC sei(); // Ligar as interrupes globais ADCSRA |= (1<<ADSC); // Iniciar as converses no ADC for(;;) {// Ciclo eterno if(adc_distance > 128) // Testar o valor da ltima reviso: 21/12/2010 60
distncia, e ligar o pino caso seja maior que 128 e vice versa. PORTB |= (1<<PB1); else PORTB &= ~(1<<PB1); } } ISR(ADC_vect) {// Definir uma interrupo que ocorre a cada ocorrncia de uma converso adc_distance = ADCH; // Ler o valor do ADC, e guard-lo numa varivel } Nota: o ligar e desligar o pino podia ter sido feito na interrupo. Visto que j tnhamos destinado antes o ciclo para isso, e que as interrupes devem ter o mnimo de cdigo possvel, achmos melhor pr no ciclo.
61
"Excerto do "Introduo ao avr-gcc usando o AvrStudio Segunda Parte" de Senso (http://lusorobotica.com/index.php?topic=2838.15) com alteraes/adaptaes de Cynary (formatao e contedo)"
pr-programado nos dispositivos que esto a comunicar. Neste documento, iremos utilizar sempre 8 bits. O bit de paridade utilizado para detectar erros. Normalmente no se usa paridade para isso, pois muito ineficiente. No caso do bit de paridade estar configurado para odd, utilizado para termos um nmero 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 nmero par de 1s. No entanto, visto que podemos ter erros no prprio bit de paridade ou em mais do que um bit, este mtodo muito ineficiente e no utilizaremos (no iremos abordar a deteco de erros na comunicao serial neste documento). O stop bit tem sempre o valor de 1, e indica o fim da transmisso. Um ou dois podem ser usados, mas no faz diferena na realidade, portanto iremos sempre configurar o AVR para usar um. A comunicao serial pode ser sncrona ou assncrona. Na comunicao assncrona, os dispositivos so pr-programados com uma velocidade de transmisso (denominada de baud rate), de forma a saberem a distncia entre os bits. No caso da comunicao sncrona, 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 tambm inicia e termina a comunicao. A comunicao sncrona d-nos a possibilidade de maiores velocidades de transferncia, e tambm retira a necessidade de se utilizar um start e stop bit. No entanto, tem a desvantagem de ser necessrio um fio extra para indicar o clock. Nesta primeira parte acerca de comunicao serial, iremos apenas abordar comunicao assncrona. Quando falarmos de IC, um tipo especial de protocolo de comunicao serial, iremos utilizar sincronidade. Para possibilitar a comunicao serial, necessitamos de trs fios: transferncia, recepo 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 tm de se ligar de uma forma cruzada o fio ligado transmisso de um dispositivo (pino TXD/PD1 no AVR/pino digital 1 no arduino) tem de estar ligado recepo do outro dispositivo (pino RXD/PD0 no AVR/pino digital 0 no arduino). Agora que j sabemos como funciona a comunicao serial, vamos aprender a programar o AVR para us-la!
O que a USART?
A USART um mdulo 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 comunicao serial com o nosso computador, e para isso temos de configurar a USART para ela fazer exactamente aquilo que queremos.
63
Como no fao ideia de como a USAR funciona o que devo fazer pegar no datasheet e comear 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 seco sobre a USART comea na pgina 177- Seco 19, e temos at cdigo em C e assembly para configurar a USART, quer ento dizer que o nosso trabalho est facilitado, e pouco mais temos que fazer que ler e passar para o nosso programa o cdigo dado. Vamos comear pela inicializao 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 so os registos onde colocamos um valor que depende do baud-rate e da frequncia do oscilador que o nosso chip tem, temos duas opes para determinar este valor, ou vemos a tabela que est no datasheet ou usamos uma pequena frmula que juntamos ao cabealho do nosso programa onde esto os outros includes e o compilador determina o valor BAUD_PRESCALLER baseado no baudrate que queremos e no valor de F_CPU, sendo esta frmula 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 reviso: 21/12/2010 64
Isto no magia nem nada que se parea, no datasheet so dadas duas frmulas, se juntarmos as duas esta a formula com que ficamos. UCSR0B o registo que nos permite activar os canais de recepo e transmisso de dados da USART assim como activar interrupts, mas isso no 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 funo USART_init no nosso main e temos a USART pronta a usar, mas ainda no somos capaz nem de enviar nem de receber dados.
void USART_putstring(char* StringPtr){ while(*StringPtr != 0x00){ // Aqui fazemos a verificao de que no chegamos ao fim da string, verificando para isso se o carcter um null USART_send(*StringPtr); // Aqui usamos a nossa funo 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 funo 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 so em C e que o ponteiro no mais que o inicio desse mesmo array. Nota (Cynary): Esta funo no envia o carcter de terminao de string (NULL), logo, caso o leitor queira escrever um programa no lado do cliente que processe strings enviadas por esta funo, deve ter isto em conta, e, ou alter-la, de forma a enviar o NULL, ou enviar pela linha serial um nmero que indique o nmero de caracteres na string, antes de enviar a prpria 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 UBRR0L UCSR0B UCSR0C } = = = = (uint8_t)(BAUD_PRESCALLER>>8); (uint8_t)(BAUD_PRESCALLER); (1<<RXEN0)|(1<<TXEN0); (3<<UCSZ00);
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 funes o compilador vai dizer que elas no esto definidas e ns ficamos a olhar para ele com cara espantada, porque as nossas funes esto mesmo ali, por baixo do main, e precisamente esse o problema, no arduino podemos declarar funes onde bem nos apetecer, e em C tambem, mas temos que declarar as funes, ou seja a primeira linha da funo que tem o nome dela e que tipo de dados o seu retorno e quais os seus argumentos tm de estar antes do main, para o compilador saber que as funes existem, ficando assim o nosso cdigo com as declaraes das funes 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) //declarao das nossas funes 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 reviso: 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 funes no estarem definidas, que era declar-las noutra ordem (as funes de que o main depende, devem estar acima deste, e assim progressivamente). No entanto, para evitar quaisquer confuses, e at facilitar a programao (visto que as declaraes das funes esto todas no topo, e assim no temos de as procurar no cdigo), a melhor forma de o fazer esta.
68
Foi feito por um menbro do AvrFreaks e acho-o simples de usar, tambm 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 ento 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) //declarao das nossas funes 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 automticamente 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 funo que a escreve via serial _delay_ms(5000); //E a cada 5s re-enviamos o texto } return 0; } void USART_init(void){ UBRR0H UBRR0L UCSR0B UCSR0C } = = = = (uint8_t)(BAUD_PRESCALLER>>8); (uint8_t)(BAUD_PRESCALLER); (1<<RXEN0)|(1<<TXEN0); (3<<UCSZ00);
unsigned char USART_receive(void){ while(!(UCSR0A & (1<<RXC0))); return UDR0; } ltima reviso: 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 to 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 tm de ter ateno 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 boto de reset do arduino para sincronizar o programa com arduino e devero ver algo do gnero:
70
E est a comunicao 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 carcter qualquer, algo bastante simples de se fazer, no precisam de nenhum hardware extra para alm do arduino e assim aprendem como controlar algo usando dados via serial, para enviar um carcter usando este terminal basta escrever o carcter na caixa marcada a verde da imagem de cima e carregar no ENTER. Boa programao!!!
71
Comunicao por IC
O Protocolo IC
Antes de comearmos a aprender como comunicar por IC no AVR, temos de compreender exactamente como este protocolo funciona. O protocolo IC (Inter-Integrated Circuit) muito til, pois permite que, em teoria, at 127 aparelhos comuniquem entre si, usando apenas dois fios (na prtica, este limite pode no ser muito real, pois depende das caractersticas elctricas do sistema, e tambm porque alguns endereos podem estar reservados limitando o nmero de dispositivos ainda mais). Isto possvel, pois os aparelhos comunicam atravs de endereos, num sistema mestre/escravo. Comeando pelas caractersticas fsicas do protocolo, usam-se duas linhas: a SCL (clock) e a SDA (dados) onde ligamos estas linhas num aparelho depende da sua construo. 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 comunicao usando o protocolo IC designada como sendo halfduplex, visto que s podemos estar a enviar ou receber dados num certo ponto no tempo, e no 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 no pode ser alterado, logo est pronto a ser lido (logo, uma transio de low para high sinaliza um novo bit na linha SDA). Normalmente apenas um aparelho controla esta linha, mas os outros aparelhos, caso no sejam rpidos o suficiente para utilizarem a frequncia desse aparelho podem controlar a linha, deixando-a em low pelo tempo que quiserem. A linha SDA contm a informao, que lida de acordo com o estado da linha SCL, como explicado no pargrafo anterior. No estado high, esta linha tem um bit 1, e no estado low, esta linha contm um bit 0. Ambas estas linhas tm uma regra especial: os aparelhos que as usam no as podem pr no estado high, apenas em low. Por isso, para se ter um estado high, colocam-se duas resistncias pullup entre o terminal positivo e a linha (uma resistncia para cada linha suficiente). Assim, quando os aparelhos largam a linha, esta est em high, e so responsveis por a pr 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 vlido, e como os outros aparelhos no suportam uma corrente de 5V, isto impede que tenham problemas, sem afectar a comunicao. A regra descrita acima sobre a alterao do estado da linha SDA de acordo com a SCL tem duas excepes: bits de incio e de fim de comunicao. Nem sempre existe um aparelho a usar as linhas ltima reviso: 21/12/2010 72
do IC, por isso necessrio conseguir-se distinguir quando que esta est a ser usada ou no. Quando no est a ser usada, ambas as linhas esto no estado high. Para sinalizar o incio de comunicao, a linha SDA vai para um estado low, enquanto a linha SCL est em high. Para sinalizar o fim da comunicao, a linha SDA vai para um estado high enquanto SCL est em high. Agora que compreendemos como funciona o protocolo a um nvel de hardware, vamos aprender como funciona a um nvel de software. J comemos a falar disso ao mencionarmos a existncia de bits de incio e de fim (neste documento, iremos referir-nos a eles como start bits e stop bits, respectivamente, pois essa a conveno). O protocolo IC muito simples de compreender e utilizar. Existem duas categorias de aparelhos os master e os slave. Os master que tm controlo sobre a linha. So estes que chamam os outros aparelhos, atravs do seu endereo nico, e enviam ou recebem dados dos mesmos. Em qualquer altura, s podem haver dois dispositivos a usar os fios para comunicao, e um desses tem de ser um master. Existe um endereo especial, chamado de general call, que permite ao master falar com todos os slaves. til para, por exemplo, configurar o endereo de um aparelho com um endereo desconhecido, ou escrever um valor para vrios aparelhos diferentes ao mesmo tempo. Este endereo o 0. No entanto, nem todos os aparelhos respondero a este endereo, visto que depende da sua configurao. 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. Aps 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 informaes sobre utilizao de vrios masters, pode-se consultar a datasheet do AVR. O IC define tambm o formato da mensagem na linha: a mensagem comea com um start bit, seguido do endereo 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 no continuar a comunicao (um ACK acknowledge tem o valor 0, e significa que podemos continuar a comunicao; um NACK o contrrio, tendo o valor 1, e significando que a comunicao 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 endereo tem 7 bits, ao adicionarmos o bit de leitura/escrita, envimos 8 bits um byte). De acordo com o valor do bit read/write, o master transforma-se num receptor ou num transmissor de dados. Aps esta primeira parte da mensagem, os aparelhos continuam a comunicar entre si atravs 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 endereo, sem ter de perder controlo da linha, devido a no ltima reviso: 21/12/2010 73
enviar o bit de stop. Isto til se o master quer enderear outro aparelho, ou mudar o valor do bit read/write (por exemplo, comea por escrever para um aparelho, enviando um comando, e depois l os resultados desse comando). Para finalizar a comunicao, o master envia um bit de stop. Alguns aparelhos no mencionam que trabalham em IC, mas sim que usam SMBus, ou TWI (Two-Wire Interface o caso do AVR). No entanto, estas designaes, apesar de terem algumas diferenas, so semelhantes em funcionalidade ao IC, por isso podem, na prtica, ser interpretadas como sendo este protocolo. Agora que j compreendemos como funciona o IC, vamos aprender como us-lo no AVR, e iremos usar este protocolo para dois AVRs comunicarem entre si.
IC no AVR
No AVR, em vez de IC, temos uma interface TWI (two wire serial interface). No entanto, podemos usar como se fosse IC. Quando utilizamos o AVR como master, devemos comear por gerar o clock na linha SCL. Isto feito alterando o register TWBR. Segundo a datasheet, a frmula para o clock a seguinte: SCL Frequency= CPU Frequency 162TWBR Prescaler Value
Ao isolarmos o TWBR, ficamos com isto: CPU Frequency 16 SCL Frequency TWBR= 2Prescaler Value Neste documento iremos utilizar a frequncia 100kHz, visto que uma frequncia standard do IC, 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 frequncia de 100kHz para o IC, e uma frequncia do CPU de 16 MHz, o valor do TWBR no ultrapassa a capacidade de 8 bits (neste caso, esse valor ser 72), e o valor por defeito (assim no necessitamos de alterar o register TWSR). Ateno que a datasheet recomenda um valor mnimo de 10 para o TWBR, logo para frequncias da SCL maiores, ser necessrio um prescaler maior. Agora que j sabemos como gerar o clock, temos de compreender como realizar aces no IC. Para isto utilizamos o register TWCR. Este register em particular tem muitas particularidades na forma como funciona. Primeiro, para o IC funcionar, temos de colocar o valor 1 no bit TWEN. Em segundo lugar, o bit TWINT determina quando ocorrem aces ou no no IC: quando este tem o valor 0, as aces definidas pelos restantes bits ocorrem, e quando tem o valor 1, no podem ocorrer aces. 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 reviso: 21/12/2010 74
aps ocorrer algum evento relacionado com o IC. Os bits TWEA, TWSTA e TWSTO indicam que aco o microcontrolador deve tomar nas linhas IC quando o bit TWINT 0. O bit TWWC constitui uma flag que indica se estamos a realizar uma certa aco proibida mexer no register TWDR quando o bit TWINT 0 (iremos ignorar esta flag). O bit TWIE liga a interrupo ligada ao IC esta interrupo ocorre sempre que o bit TWINT 1. Ateno que esta interrupo no coloca esse bit a 0, logo para evitar que esta se repita eternamente, ou colocamo-lo a 0, ou desligamos a interrupo temporariamente no iremos usar esta interrupo neste documento, no entanto destacamos a sua utilidade enquanto ferramenta para realizar aces relacionadas com o protocolo IC, sem a necessidade de ocupar o microprocessador enquanto se espera que cada operao fique completa; para saber qual a operao a realizar, usam-se os cdigos de estados, discutidos brevemente. At agora, temos usado sempre a operaes binria OR para colocar valores em bits especficos nos registers. No entanto, no prtico fazer isso com o register TWCR, visto que este tem bits que determinam a aco a seguir, e que, se no os apagarmos, faro com que ocorram aces repetidas. Assim, de cada vez que mexermos no register TWCR, faremos um reset a todos os seus valores. Isto obriga a ter sempre em ateno 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 IC! O bit que determina esta aco o TWSTA. Vamos ento definir uma funo 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, alm do bit TWSTA, tambm mexemos nos bits TWINT e TWEN. Isto para garantir que o IC est ligado (1 no bit TWEN), e que podemos efectuar aces neste (0 no bit TWINT como foi dito antes, este bit fica com o valor 0 quando escrevemos o valor 1 nele). O cdigo para o envio de um start, no entanto, est incompleto, pois ficamos com o problema de no saber quando o start acabou de ser enviado, e se tivemos sucesso ou no 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)!
75
Como foi dito antes, o bit TWINT fica com o valor 1 assim que uma aco completada nas linhas IC. Assim, para saber quando se acabou de enviar o start, temos de esperar que este bit fique com o valor 1. No entanto, isto no nos indica se o start foi enviado com sucesso ou no. Para sabermos isto, observamos o register TWSR. J falmos deste register anteriormente quando mencionmos o prescaler. Alm do prescaler, este register contm informao acerca do estado do IC, tal como se o start foi enviado com sucesso visto que as operaes para enviar um start e um repeated start so iguais em termos do register TWCR, mas originam diferentes cdigos de estado no register TWSR, temos de verificar ambos esses cdigos. Estes cdigos de estado podem ser encontrados na datasheet do AVR em quatro tabelas (pginas 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 cdigos para esses sinais so iguais tanto no modo transmitter como receiver, e so, respectivamente, 0x08 e 0x10. de notar que estes cdigos de estado assumem que os bits do prescaler so ambos 0. No entanto, nem sempre o so. Estes bits so os dois menos significativos. Para os retirar, realizamos a operao AND com o valor 0b11111100, ou 0xF8. Para sabermos se o o envio do start bit foi realizado com sucesso, esta funo devolver um valor verdadeiro ou falso, dependendo respectivamente se teve sucesso ou no. Assim, j podemos completar o cdigo para a funo 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 operao termine return ((TWSR&0xF8) == 0x08 || (TWSR&0xF8) == 0x10); } Depois do start bit, temos de enviar o endereo do aparelho com quem queremos comunicar e o bit read/write. Isto constitui um byte (visto o endereo ser 7 bits + 1 bit read/write). O AVR no distingue entre enviar o endereo/bit read/write, e qualquer outro byte no IC, quando est em modo master. Para fazer isto, apenas colocamos o byte a enviar no register TWDR e ligamos as aces IC. Ateno que, como mencionado anteriormente, no podemos colocar qualquer informao no register TWDR, quando o bit TWINT 0! Vamos ento criar uma funo que envia um byte para as linhas IC. Novamente, verificaremos o estado do IC para saber se tivemos sucesso ou no. Visto que os cdigos de estado para o envio e recepo (ACK devolvido) com sucesso de um endereo de slave e o bit read/write so diferentes dos cdigos de sucesso de um byte enviado (com ack ou nack devolvido), iremos criar uma funo separada para chamar um slave. Os cdigos devolvidos so ainda diferentes quando enviamos um bit read ou um bit write, portanto, temos de testar para ambos os casos (respectivamente, 0x40 e 0x18): ltima reviso: 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 enderear um slave de endereo addr, com bit read/write B, apenas enviamos o byte ((addr<<1)|B). de notar que algumas datasheets listam dois endereos para um aparelho. Isto significa que, em vez de darem os 7 bits do endereo, do o byte completo, com o bit de read/write. Com o que j sabemos, podemos j definir uma funo para enviar dados, visto que basta alterar os cdigos de estado que verificamos. Como foi explicado antes, quando uma transmisso ocorre com sucesso, o receptor devolve um ACK. O receptor tambm pode devolver um NACK quando a recepo ocorre com sucesso, mas este sinal significa que devemos parar a transmisso e enviar um stop bit imediatamente. Assim, temos de verificar se a transmisso 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 cdigos de estado para estes dois modos. Assim, temos de verificar os cdigos de estado 0xB8 e 0x28. Outra particularidade do modo slave que em muitas operaes, para garantir a sua funcionalidade, devemos colocar o bit TWEA a 1, inclusive na operao de envio de dados. Explicaremos o porqu mais frente, mas iremos incluir este pormenor nesta funo: 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 concentrmos em enviar dados. No entanto, tanto aparelhos master como slave tm de ser capazes de receber dados. Ateno 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 no com a comunicao, e dos estados diferirem de acordo com o que devolvemos (ACK/NACK), e de acordo com o endereo utilizado para o slave (general call ou o endereo especfico). Assim, temos 6 cdigos de estado que temos de verificar, 3 para o caso ltima reviso: 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 funo que ir receber dados recebe um parmetro lgico: 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 recepo, ou o valor recebido caso contrrio (isto no ptimo devido o valor recebido poder ser 0! Apenas fazemos assim para simplificar. No entanto, o ideal seria usar uma flag externa funo para sinalizar um erro ou devolver uma estrutura com duas variveis: 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 envimos 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 aco em particular no tem nenhum cdigo de estado, nem coloca o bit TWINT a 1, logo, podemos criar uma funo muito simples para a efectuar: void send_stop() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO); } Agora j temos toda a funcionalidade disponvel ao master! Apenas nos faltam alguns pormenores acerca do slave. Primeiro, vamos compreender a relevncia do bit TWEA. Quando este bit tem o valor 1, e o aparelho ainda no foi endereado, este ir responder quando o seu endereo ou uma general call (caso esteja configurado para responder a uma general call) aparecerem nas linhas IC, com um ACK. Se este bit for 0, ento ignorar estes bytes, desligando-se assim do IC. Anteriormente, mencionei que quando um slave envia dados, deve colocar o bit TWEA a 1. Isto no estritamente necessrio, mas aconselhvel. Quando se coloca o bit TWEA a 0 aps enviar dados, o aparelho est espera de receber um NACK, e no ir enviar mais dados. No entanto, para
78
simplificar, colocamos sempre o bit TWEA a 1, visto que no faz nenhuma diferena nos dados transmitidos, e podemos simplesmente terminar a a conexo como se estivssemos espera do NACK. Alm disto, se no colocarmos o bit TWEA a 1, no podemos enviar mais dados. Aps realizarmos operaes no modo slave, devemos esperar por um stop/start/repeated start do master de acordo com a documentao da datasheet, no modo slave transmitter, os stop bits so ignorados. Assim, para fazer isto, ligamos o IC, espera de que algum evento ocorra. Caso seja um stop bit (cdigo de estado 0xA0), ligamos novamente o IC, para o aparelho esperar que o seu endereo seja enviado para a linha. Caso no seja, um stop bit, significa que um start bit foi enviado, seguido do endereo do aparelho em questo sai-se da funo para o cdigo principal tratar de processar esse endereamento (veremos de seguida como fazer isso). Assim, podemos definir a funo 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 endereo, saber como configur-lo para responder general call e saber quando o aparelho endereado, e saber se foi enviado um bit de read ou write. Vamos comear por atribuir um endereo ao aparelho e configurar se ele responde general call ou no. Para isto, apenas temos de de escrever o endereo 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 ento configurar o nosso aparelho para responder tanto general call como ao endereo 0x10, atravs de uma funo (esta funo, alm de configurar o endereo, tambm far com que o aparelho escute as linhas IC, espera de ser endereado): #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 reviso: 21/12/2010 79
endereado. 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, no iremos usar os modos master receiver e slave transmitter, mas so idntidos, apenas trocando-se as funes de transmisso/recepo, e no caso do slave, os cdigos de estado (para o slave transmitter, so 0xA8 e 0xB0). Iremos comear com o aparelho master. Este comea por enviar um start bit, depois chama o slave (neste caso com o endereo 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 visvel, iremos enviar dados em intervalos de 1 segundo, recorrendo funo _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 operao 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 reviso: 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 endereado com um bit de read (cdigo 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 reviso: 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 IC e sobre como us-lo com o AVR. Este um dos componentes mais complexos de utilizar no AVR, por isso aconselhamos que experimente vrias vezes com o cdigo, e que consulte a datasheet e este documento sempre que tiver dvidas.
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
83