Prévia do material em texto
Programação em C no AVR Última revisão: 21/12/2010 1 Rodrigo Toste Gomes a.k.a Cynary Nuno João a.k.a Njay Senso Neste documento tentamos explicar ao leitor as várias funções que um micro-controlador AVR disponibiliza, e como pode controlar as mesmas, sem recorrer a bibliotecas de alto nível que muitas vezes roubam performance e acrescentam tamanho aos programas, como por exemplo as que o arduino disponibiliza. Assumimos que o leitor tem alguma experiência com programação em C, e que o contacto que tem com AVR é com o arduino (logo, todos os exemplos que temos aqui irão funcionar no mesmo. No entanto, podem funcionar noutros micro-controladores AVR com poucas ou nenhumas alterações). Todos os programas aqui podem ser compilados através dos seguintes comandos: avr-gcc -Wall prog.c -Os -mmcu=atmega168 -o prog.out -DF_CPU=16000000 avr-objcopy -O ihex -R .eeprom prog.out prog.hex (substituir prog.c pelo nome do ficheiro com o código) E podem ser transferidos para o arduino com o seguinte comando: avrdude -p m328p -c avrisp -P /dev/ttyUSB0 -b 57600 -F -U flash:w:prog.hex É muito fácil alterar estes comandos para funcionarem com outros micro-controladores e programadores, estando essa informação provavelmente na datasheet. Os programas necessários para os comandos acima funcionarem vêm instalados com o IDE do arduino, e podem ser utilizados para programar outros micro-controladores que não o usado pelo arduino. Para os utilizadores de windows, a utilização das ferramentas AVRStudio e WinAVR é recomendada, mas visto que está além do alcance deste documento, o leitor é incentivado a pesquisar, mas garantimos que os comandos acima funcionam. Qualquer software presente neste documento é oferecido com objectivos didácticos, e não é acompanhado de qualquer garantia de performance ou funcionalidade. Gostaria de agradecer a todos os membros da lusorobótica que comentaram no tópico respectivo a estes tutoriais, e em especial ao membro Njay, que me autorizou a usar o seu Micro-tutorial neste documento. Última revisão: 21/12/2010 2 Índice Programação em C no AVR..................................................................................................................1 Introdução.............................................................................................................................................5 Programação em C em micro-controladores........................................................................................6 Controlo da funcionalidade do micro-controlador – os registers....................................................6 Pseudo-código/código esqueleto......................................................................................................7 MACROS.........................................................................................................................................8 Variáveis volatile..............................................................................................................................8 Operações bit-wise em C.................................................................................................................8 GPIO – General Purpose Input/Output...............................................................................................14 Entrada Digital Normal..................................................................................................................14 Entrada com “pull-up” (“puxa para cima”)...................................................................................15 Entrada controlada por um periférico............................................................................................16 Saída Digital Normal.....................................................................................................................16 Saída em Colector Aberto (open colector).....................................................................................17 Saída controlada por um periférico................................................................................................17 GPIOs na Arquitectura AVR..........................................................................................................18 Configuração dos Portos em Linguagem C...................................................................................20 Interrupções........................................................................................................................................21 O que é uma interrupção?..............................................................................................................21 Como funciona uma interrupção no AVR?....................................................................................21 Como lidar com uma interrupção no AVR?...................................................................................22 Exemplo de interrupção através do pino digital 2 (INT0).............................................................23 Cuidados a ter na utilização de interrupções.................................................................................26 Timers.................................................................................................................................................28 O que são e como funcionam timers?............................................................................................28 Timers no AVR...............................................................................................................................28 Modos Normal e CTC....................................................................................................................29 Como usar um timer no AVR.........................................................................................................29 Eventos relacionados com timers..................................................................................................32 Interrupções e timers......................................................................................................................36 Timers – Parte 2, Pulse Width Modulation.........................................................................................39 O que é PWM?...............................................................................................................................39 Vários Modos de PWM.................................................................................................................39 Fast PWM......................................................................................................................................41 Phase and Frequency Correct PWM..............................................................................................46 Analog-to-Digital Converter..............................................................................................................52 Formato Analógico e Digital..........................................................................................................52 O que é o ADC?.............................................................................................................................52 Como funciona o ADC no AVR?...................................................................................................52 Como ligar o input ao AVR?..........................................................................................................56 Utilizar o ADC – construir um sensor de distância.......................................................................57 ADC8 – medir a temperatura interna.............................................................................................61 Comunicação Serial no AVR..............................................................................................................62 Como funciona acomunicação Serial?..........................................................................................62 O que é a USART?.........................................................................................................................63 Inicializando a USART do AVR....................................................................................................64 Enviando e Recebendo Dados através da USART........................................................................65 Exemplo de utilização do USART.................................................................................................68 Comunicação por I²C..........................................................................................................................72 O Protocolo I²C..............................................................................................................................72 Última revisão: 21/12/2010 3 I²C no AVR.....................................................................................................................................74 Bibliografia.........................................................................................................................................83 Última revisão: 21/12/2010 4 "Excerto do "Micro Tutorial AVR" de Njay (http://embeddeddreams.com/users/njay/Micro Tutorial AVR - Njay.pdf) com alterações/adaptações de Cynary (formatação e conteúdo)" Introdução "AVR" é o nome de uma família de micro-controladores de 8 bits comercializada pela ATMEL. A arquitectura do AVR foi desenvolvida por 2 estudantes de doutoramento noruegueses em 1992 e depois proposta à ATMEL para comercialização. Para quem souber inglês, podem ver uma pequeno vídeo sobre os AVR aqui: http://www.avrtv.com/2007/09/09/avrtv-special-005/ . O AVR consiste, tal como um PIC e outros micro-controladores, num processador (o "core"), memórias voláteis e não- voláteis e periféricos. Ao contrário do PIC, o core do AVR foi muito bem pensado e implementado desde o inicio, e o core que é usado nos chips desenhados hoje é o mesmo que saiu no 1o AVR há mais de 10 anos (o PIC teve "dores de crescimento" e o tamanho das instruções aumentou algumas vezes ao longo do tempo de forma a suportar mais funcionalidade). Assim de uma forma rápida podemos resumir a arquitectura do AVR nos seguintes pontos: – Consiste num core de processamento, memória de programa (não volátil, FLASH), memória volátil (RAM estática, SRAM), memória de dados persistentes (não volátil, EEPROM) e bits fuse/lock (permitem configurar alguns parâmetros especiais do AVR). – Arquitectura de memória Harvard (memória de programa e memória de dados separadas) – A memória volátil (SRAM) é contínua – A maior parte das instruções têm 16 bits de tamanho, e é este o tamanho de cada palavra na memória de programa (FLASH). – Execução de 1 instrução por ciclo de relógio para a maior parte das instruções. – Existem 32 registos de 8 bits disponíveis e há poucas limitações ao que se pode fazer com cada um. – Os registos do processador e os de configuração dos periféricos estão mapeados (são acessíveis) na SRAM. – Existe um vector de interrupção diferente por cada fonte de interrupção. – Existem instruções com modos de endereçamento complexo, como base + deslocamento seguido de auto- incremento/decremento do endereço. – O conjunto de instruções foi pensado para melhorar a conversão de código C em assembly. (A introdução do Micro tutorial do Njay mencionava mais alguns tópicos, que considerei como irrelevantes para este tutorial, logo cortei-os). Última revisão: 21/12/2010 5 Programação em C em micro-controladores. Neste conjunto de tutoriais, tentamos ensinar ao leitor como programar um micro-controlador AVR em C “low-level”. Para fazer isso, é especialmente necessário compreender como controlar as várias funções do micro-controlador. Os restantes tutoriais concentram-se nisso. No entanto, para compreender os exemplos dados, e poder aplicar o que é ensinado, o leitor necessita de compreender algumas coisas básicas primeiro, respectivamente: Controlo da funcionalidade do micro-controlador – os registers. Pseudo-código/código esqueleto MACROS Variáveis volatile Operações bit-wise em C. É assumido que o leitor sabe programar em C e que domina os seguintes conceitos: comentários, bibliotecas, variáveis, funções, ponteiros, ciclos, condições, lógica e bases numéricas. Controlo da funcionalidade do micro-controlador – os registers Os AVR têm várias funções: podem ser usados para comparar e ler diferenças de potencial, comunicar por serial, … Todas estas funções são controladas por registers … mas o que são registers? Todos os CPUs têm uma certa memória interna. Esta funciona quase como a memória ram, excepto no uso de ponteiros. O CPU tem acesso directo a esta memória, o que significa que em termos de performance é muito mais eficiente usar registers para armazenamento do que memória ram (o compilador em C optimiza automaticamente os programas, dando uso deste “boost” na performance sempre que possível – daí a importância de usar variáveis volatile quando se usam interrupções, estudadas mais à frente). No entanto, estes não são só usados para armazenamento, mas também para controlar várias funções dos micro-controladores. Certos bits em certos registers podem controlar o estado de um pino, ligar e desligar o ADC, … Nos AVR todos os registers têm o tamanho de 8 bits. Logo, quando é necessário armazenar valores maiores que 255, usam-se mais do que um register. No entanto, este pormenor é abstraído pelo compilador, visto que podemos muitas vezes aceder a um conjunto de registers como se fosse um só (como por exemplo, o register TCNT1 do timer1 que corresponde a dois registers, visto que pode conter um valor de 16 bits). Agora que sabemos o que é um register, vamos aprender como usá-los. As bibliotecas do avr dão-nos um header muito útil que nos permite aceder directamente aos Última revisão: 21/12/2010 6 registers e bits dos mesmos através dos seus nomes: avr/io.h Um exemplo: Para alterar o estado de um pino, alteramos o bit correspondente no register DDRx (em que x corresponde à porta. Por exemplo, o pino PB1 está na porta B, logo para alterar o seu estado, alteramos o bit PB1 no register DDRB). Logo, utilizamos o código seguinte: #include <avr/io.h> int main(void) { DDRB |= (1<<PB1); } (quando alteramos o bit para 1, estamos a colocar o pino em output) Se não compreende exactamente como alterámos um bit no register, não se preocupe, pois as operações bit-wise serão explicadas de seguida. Pseudo-código/código esqueleto O pseudo-código é basicamente uma representação abstracta do código, em linguagem natural. Muitas vezes começa-se por escrever o pseudo-código, e depois vai-se substituindo por linhas de código (muitas vezes o pseudo-código transforma-se nos comentários). Irei usar isto nos meus tutoriais para ir construindo os programas passo-a-passo. Por exemplo, o famoso programa “Hello World”, feito passo-a-passo: // Iniciar o programa // Escrever “Hello World no Ecrã” // Terminar o programa Primeiro, fazemos o mais simples: iniciar e terminar o programa. Como vamos precisar de funções Input/Output, parte da inicialização é incluir o header stdio.h, o resto é começar a função main(), e terminamos com return 0 (sair do programa com sucesso – visto que nos AVRs não existe sistema operativo, a função main nunca fará um return, apenas acabará num loop infinito): // Iniciar o programa: #include <stdio.h> int main(void) { // Escrever “Hello World” no Ecrã return 0; } // Terminar o programa Agora falta a parte funcionaldo programa: escrever o Hello World no ecrã: Última revisão: 21/12/2010 7 // Iniciar o programa: #include <stdio.h> int main(void) { printf(“Hello World”); // Escrever “Hello World” no Ecrã return 0; } // Terminar o programa MACROS Em quase todos os programas de C, temos instruções começadas por '#'. Estas não são instruções em C, mas sim instruções interpretadas apenas pelo pré-processador, antes da compilação. Por exemplo, quando fazemos “#include <qualquercoisa.h>”, estamos a indicar ao pré-processador para incluir o conteúdo do ficheiro qualquercoisa.h no nosso programa. Uma MACRO é uma instrução deste tipo, que se comporta como uma função. São úteis quando queremos realizar certas tarefas repetidamente, mas não se justifica o custo em performance de chamar uma função (para quem programa em C++, isto é equivalente ao inline). Por exemplo, duas macros que costumo usar são as seguintes: #define max(I,J) ((I)>(J)?(I):(J)) #define min(I,J) ((I)<(J)?(I):(J)) Antes da compilação, o pré-processador substitui todas as declarações de max(x,y) e min(x,y) pelo código correspondente, sem ser assim necessário chamar uma função (as macros são úteis para substituir principalmente funções com só uma linha de código). Há vários pormenores envolvidos na criação de macro (como por exemplo, abusar das parêntesis para proteger o código), mas não interessam para este tutorial. No entanto, visto que são muito úteis, aconselho os interessados a pesquisar sobre elas. Variáveis volatile Quando declaramos variáveis, podemos controlar certos aspectos de como o código deve acedê- las. Uma declaração importante quando se programa AVRs, devido à existência de interrupções, é a volatile. Mais à frente explicarei a importância disto, por agora é apenas importante reter que quando se declara uma variável como volatile, estamos a informar que o seu valor pode ser alterado de formas inesperadas, logo deve sempre ir buscar o seu valor actualizado. Operações bit-wise em C Muita da programação em micro-controladores consiste principalmente em manipular bits de certos registers. Para fazer isso, usamos as operações bit-wise que manipulam valores ao nível dos bits. Última revisão: 21/12/2010 8 Para quem não compreende bases numéricas, e não sabe o que significa manipular bits, aconselho a lerem algum livro/tutorial que trate deste assunto. No entanto, explicado de uma forma breve, é o seguinte: Normalmente usamos a base decimal. Isto significa que usamos 10 dígitos diferentes (do 0 ao 9). Com combinações deles, fazemos diferentes números. Quando queremos um valor acima do dígito maior, transportamos mais um para a posição seguinte (se contarmos desde a direita). Assim podemos dar valores a cada posição no número. Por exemplo, o número 29 tem um 9 na posição 0 e um 2 na posição 1. A posição 0 corresponde ao valor 10 (1), e a 1 ao valor 10¹. Assim, podemos chegar ao número através da conta: 2*10¹ +⁰ 9*10 .⁰ Números de base binária funcionam da mesma forma que os de base decimal, com a particularidade de apenas utilizarmos dois algarismos: o 0 e o 1. Assim, cada posição tem um valor diferente. Por convenção, chamam-se às posições de um número em base binária de bit. Assim, quando falamos em manipular bits, estamos a falar em manipular o valor (0 ou 1) de certas posições. Por exemplo, o número 1001 (para facilitar a leitura, costumam-se ler os dígitos separados. Assim, em vez de se ler “mil e um”, lê-se “um zero zero um”) corresponde ao número em decimal 9. Isto porque o bit 0 tem o valor de 1 (2 ) e o bit 3 tem o valor de 8 (2³). Logo, como⁰ esses são os únicos bits com dígitos lá, chegamos ao 9 através da conta: 1*2 +1*2³.⁰ Agora que já conhecemos a base binária, e o que significa manipular bits, vamos ver como podemos manipulá-los. Isto é feito através de operações bit-wise. Em C, existem cinco operações bit-wise: | – or & – and ~ – not ^ – xor << – shift left >> – shift right As duas primeiras operações funcionam como as operações lógicas ||, &&. No entanto, em vez de testarem a variável como um todo lógico, testam bit a bit, e o resultado corresponde a essa comparação bit a bit. Por isso, enquanto temos resultados bem definidos com as operações | e &, as Última revisão: 21/12/2010 9 operações || e && podem dar um valor aleatório para verdadeiro. Assim, quando se necessitam de valores lógicos, devem-se usar as operações || e &&, e para manipulação bit a bit, devem-se usar as operações | e & (nota: as operações & e && podem ter resultados diferentes). Vamos então começar por estudar essas duas operações: O or retorna 0 quando ambos os bits são 0, e 1 quando pelo menos um dos bits é 1. Olhemos para um exemplo: 111000 | 001110 = 111110 Vamos analisar isto bit a bit. Em ambos os números, o bit 0 tem o valor 0. 0 ou 0 = 0. Logo, o bit 0 do resultado será um 0. No bit 1, o primeiro número tem um 0, mas o segundo tem um 1. 0 ou 1 = 1. Logo, o resultado terá um 1 no bit 1. A mesma coisa ocorre com o bit 2. No bit 3, ambos os números têm um 1. 1 ou 1 = 1. Logo, o resultado terá um 1 no bit 3. Nos restantes bits, o primeiro número tem um 1, e o segundo tem um 0. 1 ou 0 = 1. Logo os restantes bits (bits 4 e 5) terão um 1 no resultado. Assim, chegamos ao número 111110. Podemos usar isto para colocar o valor 1 numa certa posição num número. Por exemplo, temos o número 1101 (em decimal é o número 13), e queremos preencher aquele 0 com um 1. Se fizermos um ou com o número 0010 (em decimal é o número 2), preenchemo-lo. Vejamos um exemplo: #include <stdio.h> int main(void) { int i = 13; i = i|2; // Equivalente a fazer i |= 2 printf(“%d\n”, i); // Imprime o número 15 – em binário 1111. return 0; } Vamos agora observar a operação &. O and retorna 0 quando pelo menos um dos bits é 0, e 1 quando os dois bits são 1. Por exemplo: 1101 & 0111 = 0101 A análise deste exemplo será deixada como um desafio ao leitor. Última revisão: 21/12/2010 10 O & é muitas vezes usado para colocar a 0 um certo bit. Por exemplo: se tivermos o número 10111 (em decimal 23), e quisermos a partir dele obter o número 10101 (em decimal 21), podemos fazer a seguinte operação: 10111 & 1101 = 10101: #include <stdio.h> int main(void) { int i = 23; i = i&13; // 13 – em binário 1101; equivalente a i &= 13; printf(“%d\n”, i); // Imprime 21 return 0; } A terceira operação, ~ (not), também tem um comportamento semelhante ao seu equivalente lógico, o !. No entanto, foi separado das outras duas operações, pois esta não pode ser usada como uma operação lógica, visto que ~(true) pode dar um valor verdadeiro à mesma (interessantemente, devido à forma como a aritmética dos CPUs funcionam, fazer o ~ de qualquer número positivo dá um número negativo e vice-versa, sendo a única excepção o -1, já que ~(-1) = 0. Não aprofundaremos mais isto, visto que não interessa muito para programar micro-controladores). Vejamos como funciona: ~1101 = 0010 Cada bit do número original é invertido, logo a partir de um número positivo (true), podemos não obter 0 (false), que é exactamente o que o ! lógico faz. O ~ é muitas vezes utilizado em conjunção com o & para pôr um valor 0 num bit. Vejamos porquê: 11101 & 10111 = 10101 Sabendo a posição do bit que queremos pôr a 0 (neste caso a posição 4), como chegamos ao seu inverso, de forma a manter o resto do número intacto. Usando o ~, claro! Neste caso, fazer: 11101 & 10111 = 10101 é igual a fazer: Última revisão: 21/12/2010 11 11101 & (~01000) = 11101 & 10111 = 10101 (mais à frente iremos estudar como criar um número com apenasum 1 na posição pretendida, sabendo apenas essa posição). Por exemplo, com código agora (reformulação do exemplo do &): #include <stdio.h> int main(void) { int i = 23; i &= ~(8); // 8 – 01000 printf(“%d\n”, i); // Imprime 21. return 0; } O “exclusive or”, ou como é melhor conhecido, o xor, não tem um equivalente lógico óbvio. É o mesmo que !=. O seu comportamento é o seguinte: retorna 0 quando ambos os números são iguais, e 1 quando são diferentes. Como o |, quando se procura um resultado lógico, é equivalente usar o ^ e o !=. Vejamos então um exemplo 1101 ^ 0101 = 1001 O xor é muitas vezes usado para fazer “toggle” (alterar o valor de 0 para 1 e vice-versa) de um certo bit. Por exemplo, se tivermos um número 11x1, e quisermos alterar o estado do bit 1, sem conhecermos o seu valor, basta fazer a seguinte operação: 11x1 ^ 0010 Isto porque quando fazermos um xor com 0, o resultado é sempre igual ao do outro número (1^0 = 1; 0^0 = 0), e quando fazemos um xor com 1, altera sempre (1^1 = 0; 1^0 = 1). (visto que o código de exemplo seria semelhante aos anteriores, iremos passar à frente desse passo). Agora só nos falta estudar os operadores de shift. Estes são muito úteis porque nos permitem pôr um valor em qualquer posição do número, ou seja, fazer shift para cima ou para baixo desse mesmo valor. Vamos utilizar o exemplo do ~ e do &. Sabendo apenas a posição em que queremos pôr o 0, e o Última revisão: 21/12/2010 12 número que tem essa posição a 1, e as restantes a 0, já sabemos que operação utilizar. Mas ainda nos falta uma coisa: como chegamos ao número que tem a posição desejada a 1? Para isso usam-se os operadores de shift. Por exemplo, se quisermos colocar o 1 na posição 3, fazemos o seguinte: 1<<3 = 1000 #include <stdio.h> int main(void) { int i = 23; i &= ~(1<<3); // 1<<3 = 8 – 01000 printf(“%d\n”, i); // Imprime 21. return 0; } Esta técnica também é utilizada para chegar aos valores utilizador com o or e o xor, sabendo apenas os bits que queremos, respectivamente, colocar a 1, ou alterar o valor. Também existe o operador de shift >>, que faz o contrário do <<. Por exemplo: 111>>2 = 1 Mas é menos usado quando se programa micro-controladores. É de notar que qualquer overflow é completamente esquecido. Por exemplo, se considerarmos um limite de 5 bits: 10111<<3 = 11000 10111>>3 = 00010 Uma pequena curiosidade: dadas as características das bases numéricas, fazer <<x, é equivalente a multiplicar por 2^x, e fazer >>x é equivalente a dividir por 2^x. E assim terminamos o nosso tutorial acerca das bases de programação necessárias para programar micro-controladores. Esperamos que o leitor esteja agora preparado para se aventurar no mundo da programação “low-level” dos mesmos! Última revisão: 21/12/2010 13 "Excerto do "Micro Tutorial AVR" de Njay (http://embeddeddreams.com/users/njay/Micro Tutorial AVR - Njay.pdf) com alterações/adaptações de Cynary (formatação e conteúdo)" GPIO – General Purpose Input/Output O conceito de GPIO surge como uma forma de se tornar um chip mais flexível, e deve ter surgido com os chips programáveis. Este conceito consiste em podermos configurar um pino de um chip para poder ter uma de entre várias funções, como por exemplo uma entrada ou uma saída. Isto tem vantagens óbvias na flexibilidade de um chip, pois o fabricante dá-vos um chip com N pinos em que vocês escolhem a função de cada um conforme as necessidades da vossa aplicação. Se a função de cada pino fosse sempre fixa, os chips seriam muito menos úteis, e provavelmente teríamos chips maiores, com muito mais pinos, numa tentativa de colmatar essa limitação, e não poderíamos alterar a função do pino durante o funcionamento do chip. A função de base que podemos escolher para um GPIO é se o respectivo o pino é uma entrada ou saída, mas não é a única. Existem outras funções que podem ser escolhidas, embora nem todos os chips suportem todas. As funções possíveis mais comuns são: Normalmente dizemos apenas "GPIO" quando nos estamos a referir a um "pino GPIO" e eu assim farei daqui para a frente. Vamos ver com mais detalhe cada função, a que às vezes também chamamos "tipo de pino". Entrada Digital Normal Esta é talvez a configuração mais simples que podemos ter. O pino funciona como uma entrada digital, ou seja, só podemos ler (em software) um de 2 valores: 0 ou 1. Na prática os valores 0 e 1 representam uma certa tensão que é aplicada ao pino, resultando numa leitura de 0 ou 1 por parte do software. As tensões mais comuns são 0V para representar um 0 e 5V para representar um 1, mas podem ser outras como por exemplo 3.3V ou 1.8V para representar um 1, dependendo da tensão de alimentação do chip (refiro apenas "chip" porque não são apenas os micro-controladores que têm GPIOs; por exemplo as FPGA, outro tipo de chip programável, também têm). Última revisão: 21/12/2010 14 Portanto, num sistema que funcione com uma tensão de alimentação de 5V, se aplicarmos 5V a um pino configurado como "entrada digital normal", o software irá ler um valor 1 desse pino. Se aplicarmos 0V, o software irá ler um 0. A leitura do "estado do pino" é habitualmente efectuada lendo-se um registo do chip. Falaremos mais sobre isto no final. Configurar um GPIO como entrada digital normal também serve como forma de desligar o pino do circuito. Neste caso não estamos interessados em ler valores. Ao configurá-lo como entrada, ele não afecta electricamente (de um ponto de vista digital) o circuito exterior ao chip e portanto é como se tivéssemos cortado o pino do chip. Diz-se que o pino está em "alta impedancia" ("high-Z" em inglês, pois o "Z" é muito usado para designar "impedancia"), "no ar", ou simplesmente "desligado do circuito". Normalmente dizemos apenas que um pino está "configurado como entrada" ou como input. Entrada com “pull-up” (“puxa para cima”) Então se tivermos um pino configurado como input mas não lhe aplicarmos nenhuma tensão, que valor lemos no software?... A resposta é: não podemos prever. Tomem bem atenção a isto, vou repetir: não podemos prever. Quando temos uma entrada que está no ar, não podemos prever que valor vamos ler; o valor pode estar estável em 0 ou 1 ou pode estar sempre a variar, ou mudar de vez em quando consoante é dia ou noite ou Marte está alinhado com Júpiter ou o vizinho deitar-se 2 minutos mais cedo ou mais tarde. Ele pode até mudar só de lhe tocarem com o dedo. Em algumas situações queremos ter sempre um valor estável na entrada. Um caso tipico é um interruptor (que pode ser um botão). Conectamos o interruptor entre a massa (o negativo da tensão de alimentação, "0V") e o pino. Aí, quando ligamos o interruptor (posição "ON"), o pino fica ligado aos 0V e portanto o chip lê um 0. Mas, e quando o interruptor está desligado? Aí o pino está no ar pois o interruptor desligado é um circuito aberto, e já sabemos que ler um input que está no ar dá- nos um valor aleatório e portanto nunca vamos saber se o interruptor está mesmo ON ou OFF. É aqui que o pull-up entra; ao configurarmos o pino com pull-up, o chip liga internamente uma resistência entre o pino e a tensão de alimentação, e portanto, quando não há nada electricamente ligado ao pino, o pino "vê" um 1. No caso do interruptor, quando este está OFF, o pull-up coloca um 1 estável à entrada do pino e fica resolvido o problema. Quando o interruptor está ON, o próprio interruptor força um 0 no pino, ligando-o à massa. Então mas... se o pull-up puxa o pino "para cima" e o interruptor (quando está ON) puxa para baixo, não há aqui uma espécie de conflito? Não há, por uma simples razão: o valor da resistência de pull-up é alto(tipicamente mais de 100 KOhms) e portanto tem "pouca força". Como o interruptor liga o pino directamente à massa, é esta que "ganha". Diz-se até que o pull-up é um Última revisão: 21/12/2010 15 "pull-up fraco", ou "weak pull-up" em inglês. Esta expressão pull-up ("puxa para cima") vem de estarmos a ligar à tensão de alimentação positiva, que é mais "alta" do que a massa, os 0V. Para este termo contribui ainda o facto de geralmente se desenhar a linha de alimentação positiva no topo dos esquemas, e a massa em baixo. Também podemos falar em pull-down ("puxa para baixo") quando nos referimos a ligar à massa. Podemos criar pull-downs ligando resistências à massa, mas tipicamente os chips não suportam este tipo de pull, por razões que fogem ao âmbito deste artigo que se quer simples. Entrada controlada por um periférico Neste caso deixamos de ter controlo sobre o GPIO, passando esse controle para um periférico interno do chip. Por exemplo a linha Rx (recepção) de uma porta série (UART). Aqui o pino funciona como uma entrada mas quem a controla é o periférico. Saída Digital Normal Na lógica CMOS, com a qual trabalhamos mais hoje em dia, as saídas digitais são baseadas numa topologia designada "totem-pole". Este tipo de saída é constituído por 2 interruptores electrónicos (transístores) um em cima do outro, e controlados de forma a que: 1. quando queremos ter um "zero" à saída, liga-se o transístor de baixo, que liga o pino à massa (0V); o transístor de cima mantém-se desligado 2. quando queremos ter um "um" à saída, liga-se o transístor de cima, que liga o pino à tensão se alimentação (+V); o transístor de baixo mantém-se desligado Na imagem acima podemos ver uma saída totem-pole num dos seus 2 estados mais habituais: quando tem um 0 e quando tem um 1. Por aqui podemos ver por exemplo porque é que não se devem ligar 2 (ou mais) saídas umas às outras. Se uma delas estiver com um "1" e a outra com um "0", estamos a criar um curto-circuito na alimentação, ligando +V à massa. Numa das saídas está ligado o interruptor de cima e na outra está Última revisão: 21/12/2010 16 ligado o de baixo. Mesmo que isto só aconteça durante um período de tempo muito pequeno (milisegundos, microsegundos ou menos), vão passar correntes elevadas, fora das especificações dos chips, e se acontecer regularmente, começa um processo de degradação que leva à falha do chip em segundos, horas, semanas, meses ou anos. Uma saída totem-pole tem ainda um 3o estado: "no ar". É outra forma de desligar um pino, mas que é usada quando o pino é sempre uma saída (não configurável). No caso de um GPIO, este pode ser configurado como entrada ficando assim desligado do circuito exterior, como vimos atrás. Saída em Colector Aberto (open colector) Este tipo de saída surgiu para resolver o problema de não se poder ter 2 ou mais saídas ligadas. Por esta altura vocês podem estar a pensar "mas porque raio é que haveríamos de querer 2 saídas ligadas uma à outra?!", e a resposta é simples: pensem por exemplo no I²C, em que temos linhas de dados bidireccionais. No I²C há vários chips ligados a um mesmo par de linhas e todos eles têm a possibilidade de transmitir dados nessas linhas. Daí que, de alguma forma, há várias saídas ligadas entre si. A saída em colector aberto consiste num simples interruptor electrónico (transístor) capaz de ligar o pino à massa. Quando o interruptor está ligado a saída é 0, e quando está desligado a saída é... não sabemos. O pino fica "no ar" e portanto qualquer outro dispositivo exterior ao chip que esteja ligado ao pino pode lá colocar a tensão que entender. Num bus I²C o que se passa é que existe uma resistência externa que mantém as linhas com tensão positiva quando nenhum dispositivo está a transmitir; ou seja, temos um "pull-up fraco". A partir daí, qualquer um dos dispositivos pode forçar uma das linhas I²C a ter 0V, se activar o interruptor electrónico na sua saída em colector aberto. Saída controlada por um periférico À semelhança do que se passa no caso da entrada controlado por um periférico, neste caso deixamos de ter controlo sobre o GPIO, passando esse controle para um periférico interno do chip. Por exemplo a linha Tx (transmissão) de uma porta série (UART). Aqui o pino funciona como uma saída mas quem a controla é o periférico (no caso da UART, uma saída normal). Última revisão: 21/12/2010 17 GPIOs na Arquitectura AVR Nos AVR os pinos estão agrupados em portos com no máximo 8 pinos cada. Os portos têm a designação de letras, A, B, C, etc, e cada AVR tem um conjunto de portos. Cada pino de um porto pode ser configurado num de 3 modos: 1. Entrada normal 2. Entrada com pull-up 3. Saída normal 4. Entrada ou saída controlada por um periférico Um pino entra no 4º modo quando o respectivo periférico é activado, pelo que não vamos debruçar-nos aqui sobre isso. A cada porto está associado um conjunto de 3 registos que são usados para configurar, ler e definir o estado de cada pino do porto individualmente. Cada bit de cada registo está associado ao respectivo pino do chip. 1. PINx - Lê o estado actual do pino 2. DDRx - Data Direction Register (registo de direcção dos dados) 3. PORTx - Define o estado da saída do porto O "x" depende da letra do porto, de modo a que temos por exemplo o registo DDRA para o porto A. Segue-se um diagrama simplificado da lógica associada a cada pino de um porto do AVR, neste caso exemplificado para o pino 3 do porto B (designado B3): Todos os pinos do chip têm uma lógica similar a este diagrama. O registo PINx apresenta sempre o valor lógico ("0" ou "1") que estiver presente num pino independentemente da configuração. É como se o registo estivesse ali a medir a tensão directamente Última revisão: 21/12/2010 18 no pino e a reportar o seu valor lógico ao software. O registo DDRx define, para cada pino do porto, se é uma entrada ou uma saída. Após o reset do chip, todos os pinos estão configurados como entradas, e portanto é como se todo o chip estivesse desligado do exterior, tem todos os pinos no ar. Para configurar um pino como saída temos que colocar a 1 o respectivo bit no registo DDRx. Se um pino estiver configurado como uma saída (se o respectivo bit no registo DDRx for 1), podemos então definir o estado da saída com o respectivo bit no registo PORTx. O PORTx controla ainda o pull-up interno quando um pino está configurado como entrada. Se o respectivo bit no PORTx estiver a 1, então o pull-up está activado. Cada AVR tem um certo número de portos. Cada porto pode não ter pinos físicos do chip associados a todos os seus bits. As datasheets da ATMEL (fabricante dos AVR) apresentam logo na 2ª página o pinout (a atribuição de funcionalidade aos pinos do chip). (A partir daqui, divergimos um pouco do tutorial original do Njay, visto esse tratar de outro micro-controlador AVR. Neste documento iremos tratar do atmega168/328 – o AVR do arduino) Vamos pegar na datasheet do atmega168/328, e o pinout é o seguinte (com a legenda para os pinos do arduino já incluída): Isto diz-nos que este modelo de AVR tem 4 portos, A, B, C e D. Logo, para configuração dos pinos, este AVR tem os registos DDRA, PORTA, PINA, DDRB, PORTB, PINB, DDRC, PORTC, PINC, DDRD, PORTD, e PIND. Os nomes entre parêntesis são os nomes associados aos periféricos Última revisão: 21/12/2010 19 do AVR quando estes estão ligados; vamos esquecê-los neste artigo. Configuração dos Portos em Linguagem C É muito fácil aceder aos registos de configuração dos GPIOs (e não só) com este compilador: eles têm exactamente os nomes dos registos e usam-se como variáveis. Assim, existe porexemplo a variável DDRA e podemos escrever instruções como: DDRA = 0xff; // configurar todos os GPIOs do porto A como saídas Se quisermos configurar apenas alguns GPIOs, temos disponível a macro _BV(index) que cria uma máscara de bits para um determinado bit do registo. Esta macro retorna um número de 8 bits em que apenas o bit de índice index é 1. Exemplos: _BV(0) é 1, _BV(7) é 128 (0x80), _BV(2) é 4. No entanto, ao longo deste documento, iremos principalmente usar o operador bit-wise de shift, já que o seu comportamento é igual ao da macro _BV. Por exemplo, _BV(4) = (1<<4). Agora seguem-se alguns exemplos de configuração. Para configurar apenas o GPIO PA3 como saída e todos os restantes como entradas, DDRA = _BV(PA3); // configurar apenas o GPIO PA3 (pino 17) como saída e para configurar vários GPIOs como saídas, basta efectuar um OU bit-a-bit DDRA = _BV(PA3) | _BV(PA6); // configurar os GPIOs PA3 e PA6 (pinos 17 e 12) como saídas Depois bastaria colocar no registo PORTB, no respectivo bit (3), o valor que queremos que "apareça" no pino. Para ligar o pull-up de um GPIO basta garantir que o respectivo bit está a zero no registo DDRx e depois colocar a 1 o bit no registo PORTx. Configurar o GPIO PB1 como entrada com pull-up seria assim: DDRB &= ~_BV(PB1); // configurar PB0 como entrada PORTB |= _BV(PB1); // ligar o pull-up Portanto é muito fácil configurar os GPIOs em C. Última revisão: 21/12/2010 20 Interrupções O que é uma interrupção? Irei agora começar a falar de interrupções a partir do mais básico – o que é uma interrupção? Uma interrupção é basicamente uma pausa no programa, enquanto o processador trata de outra coisa mais importante. Um exemplo da vida real: http://www.youtube.com/watch?v=A9EP6U0BBrA Neste caso a interrupção foi o toque com o pato que interrompeu o discurso. Como funciona uma interrupção no AVR? Nos AVRs, as interrupções têm várias particularidades, pois é necessário activar as interrupções globais, as interrupções particulares e criar uma rotina para lidar com cada interrupção. Cada microcontrolador AVR tem um conjunto de registers cujos bits controlam vários aspectos do seu funcionamento (por exemplo, no tutorial que coloquei no meu primeiro post, estão explicados os que controlam os pinos de INPUT/OUTPUT). O mesmo acontece com as interrupções. No caso do atmega328, o bit I do register SREG controla as interrupções a nível global. Quando o seu valor é 1, estas estão ligadas, e vice-versa. No entanto, há uma instrução mais simples para ligar as interrupções globais do que terem de se lembrar que é no bit I do register SREG, que é simplesmente “sei” (funciona tanto em assembly como em C, só que em C é uma função incluída no header <avr/interrupt.h>). Depois de ligadas as interrupções globais, ainda é necessário ligar interrupções individuais. Para isso, é necessário encontrar qual o bit que liga certa interrupção (encontra-se na datasheet facilmente na zona dos registers no capítulo acerca da funcionalidade procurada). Depois de ligadas as interrupções globais e particulares, o processador procura por mudanças de estado em bits de certos registers (flags), definidos pelas interrupções individuais. Quando esses bits tornam-se em 1, a interrupção é gerada (independentemente do que esteja a acontecer, o programa pára). Mas falta aqui uma coisa … o que acontece quando essa interrupção ocorre? A “Interrupt Service Routine” (ISR) definida para aquela interrupção é executada. No final disto tudo, a flag fica com o valor 0 novamente, e o programa continua a sua execução a partir do ponto em que estava. Nota: Enquanto o processador está a executar a interrupção, as interrupções globais estão desligadas. Quando acaba-se de executar a interrupção, as interrupções globais são ligadas novamente. Última revisão: 21/12/2010 21 Como lidar com uma interrupção no AVR? Agora que já sabemos como funciona uma interrupção, temos de aprender a programar de forma a lidar com as mesmas. Primeiro, iremos começar por definir o pseudo-código: // Iniciar o programa // … (código que inicialize variáveis, LEDs, etc. aqui) // Ligar interrupções particulares // Ligar interrupções globais // Definir ISR para lidar com as interrupções particulares ligadas. Para lidar com registos e interrupções, iremos precisar dos seguintes headers: <avr/io.h> e <avr/interrupt.h> (já podemos também adicionar a função main(), e um loop eterno): // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> int main(void) { // … (código que inicialize variáveis, LEDs, etc. aqui) // Ligar interrupções particulares // Ligar interrupções globais for(;;); } // Definir ISR para lidar com as interrupções particulares ligadas. (como a ISR é uma função, definimos fora do main). Como expliquei anteriormente, ligam-se as interrupções globais através da função sei() // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> int main(void) { // … (código que inicialize variáveis, LEDs, etc. aqui) // Ligar interrupções particulares sei(); for(;;); } // Definir ISR para lidar com as interrupções particulares ligadas. Neste tópico, vamos ignorar as interrupções individuais, pois ainda não falámos de nenhuma Última revisão: 21/12/2010 22 interessante, e vamos concentrar-nos nas ISR. A biblioteca do avr dá-nos uma macro muito útil para definir uma ISR, e tem o nome ISR() (espertos, não são? xD), com um argumento: o nome do vector da interrupção. O vector da interrupção é basicamente o que identifica qual a interrupção com que estamos a lidar. Esta informação encontra-se na página 57 do datasheet que eu tenho (início do capítulo sobre interrupções/capítulo 9), numa tabela, na coluna Source. Por exemplo, no tópico a seguir, vamos lidar com a interrupção que ocorre no pino digital 2. Este pino tem o nome de INT0. Ao olharmos para a tabela, vemos que a source da interrupção é INT0. Para usarmos isto como argumento para a macro ISR, basta adicionar “_vect”. Assim, o vector é: INT0_vect: // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> int main(void) { // … (código que inicialize variáveis, LEDs, etc. aqui) // Ligar interrupções particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupção } (para alguns, esta declaração da função ISR pode ser confusa, pois não tem tipo. No entanto, lembrem-se que é uma macro, e por isso ISR não é realmente o que fica no código final). Nota: Se notarem, alguns dos Vectores na datasheet têm espaços ou vírgulas no nome. Basta substituir esses por _. Por exemplo, para a interrupção gerada quando se recebem dados por serial, temos o seguinte Source: USART, RX. O argumento que usamos para a macro ISR é: USART_RX_vect. Exemplo de interrupção através do pino digital 2 (INT0) Vamos agora fazer algo mais interessante, e codificar uma interrupção. Comecemos com o código do tópico anterior: // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> Última revisão: 21/12/2010 23 int main(void) { // … (código que inicialize variáveis, LEDs, etc. aqui) // Ligar interrupções particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupção } Vamos usar para este efeito o pino 2 (podia ser feito com o pino 3 também, com poucas diferenças). O objectivo deste código vai ser mudar o estado de um LED quando se toca num botão ligado ao pino 2. O LED vai estar ligado ao pino digital 4 (PD4). Vamos começar por criar uma variávelglobal, com o estado do pino e inicializar esse pino como output (vai começar desligado) (para mais informações sobre GPIOs, ler o tutorial que pus no primeiro post), e já vamos colocar o código necessário para fazer toggle ao pino na ISR. // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> char output = 0; // Estado do led. int main(void) { DDRD |= (1<<PD4); // Inicializar o pino digital 4 como output. PORTD &= ~(1<<PD4); // Inicializar o pino digital 4 como desligado. // Ligar interrupções particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupção output = ~output; // Alterar o estado PORTD &= ~(1<<PD4); // Desligar o pino – isto é necessário para quando o output é 0 se poder desligar. PORTD |= ((output&1)<<PD4) // output&1 pois só nos interessa o primeiro bit, assim evitamos mexer nos outros pinos. } Agora só nos falta mesmo inicializar a interrupção particular para o INT0. Última revisão: 21/12/2010 24 Ao pesquisarmos na datasheet, podemos observar um pormenor acerca dos interrupts externos INT0 e INT1: eles podem ser ligados por 4 estados diferentes: quando o pino está low, quando o pino transita para high, quando o pino transita para low e quando o pino muda de estado (low-high e vice-versa). Como estamos a usar um botão, o mais fácil é que este ligue o pino à corrente, colocando-o em HIGH. Assim, queremos gerar o interrupt quando o pino transita para HIGH. O estado escolhido para o INT0 está nos bits ISC00 e ISC01 do register EICRA (para o pino INT1, está nesse mesmo register, mas nos bits ISC10 e ISC11). O estado que desejamos corresponde a colocar ambos os bits em 1. Logo, adicionamos isso ao código: // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> char output = 0; // Estado do led. int main(void) { DDRD |= (1<<PD4); // Inicializar o pino digital 4 como output. PORTD &= ~(1<<PD4); // Inicializar o pino digital 4 como desligado. EICRA |= ((1<<ISC00) | (1<<ISC01)); // Configurar interrupção no pino INT0 para quando este transita para HIGH // Ligar interrupções particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupção output = ~output; // Alterar o estado PORTD &= ~(1<<PD4); // Desligar o pino – isto é necessário para quando o output é 0 se poder desligar. PORTD |= ((output&1)<<PD4) // output&1 pois só nos interessa o primeiro bit, assim evitamos mexer nos outros pinos. } Agora só nos falta mesmo ligar a interrupção associada ao INT0. O bit que controla isto é o INT0 no register EIMSK. Assim, é só modificar o código, e fica completo: // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> char output = 0; // Estado do led. Última revisão: 21/12/2010 25 int main(void) { DDRD |= (1<<PD4); // Inicializar o pino digital 4 como output. PORTD &= ~(1<<PD4); // Inicializar o pino digital 4 como desligado. EICRA |= ((1<<ISC00) | (1<<ISC01)); // Configurar interrupção no pino INT0 para quando este transita para HIGH EIMSK |= (1<<INT0); // Ligar interrupções particulares sei(); for(;;); } ISR(INT0_vect) { // Definir o que fazer quando acontece esta interrupção output = ~output; // Alterar o estado PORTD &= ~(1<<PD4); // Desligar o pino – isto é necessário para quando o output é 0 se poder desligar. PORTD |= ((output&1)<<PD4) // output&1 pois só nos interessa o primeiro bit, assim evitamos mexer nos outros pinos. } Agora temos um código que, ao clicarmos num botão que liga o pino digital 2 e o pólo positivo, faz toggle do pino digital 4 (nota: visto que não fizemos nada para tratar do bouncing, podem haver resultados inesperados. No entanto, como isto é só para exemplo, não considerámos muito importante). Para experimentarem, podem montar o seguinte circuito: Cuidados a ter na utilização de interrupções Vou deixar aqui duas situações a terem em atenção quando estão a lidar com interrupções: 1. O compilador optimiza bastante o código. Isto quase sempre é uma vantagem, no entanto, por vezes não é. Utilizando o exemplo do tópico anterior, se tivéssemos de aceder a variável output Última revisão: 21/12/2010 26 no código do main(), não estaríamos a aceder ao valor correcto da variável. Isto acontece porque o compilador não considera que podemos aceder à função ISR só com aquele código, logo apenas carrega a variável da memória ram para os registers uma vez, e depois não actualiza o seu valor, que é alterado na ISR. Para resolver isto, dizemos ao código que a variável output é volatile, declarando-a assim: volatile char output; Isto indica ao compilador que a variável pode ser alterada de formas inesperadas, e por isso deve sempre actualizar o seu valor da ram. 2. O processador AVR do Arduino funciona a 8 bits. Isto quer dizer que ele só pode lidar com 8 bits de cada vez. Não pode, por exemplo, carregar um int da memória numa só instrução, pois estes têm 16 bits. Mas as interrupções podem ocorrer em qualquer parte do programa … logo o que pensam que acontece se tirarmos a primeira metade de um inteiro da memória, e antes de tirarmos a segunda metade ocorrer uma interrupção que altere essa variável? Resultados não previsíveis obviamente … Logo, o que podemos fazer para evitar isto? O que podemos fazer é desligar interrupções nesses blocos de código que não podem ser perturbados (nota: se estiverem a carregar um char não deve haver problemas, visto ter apenas 8 bits). Isto é feito facilmente com a função cli() (também no header <avr/interrupt.h>). Depois do código efectuado, basta ligar novamente as interrupções com sei(). Por exemplo: //... int i,j; for(;;) { cli(); j = i; // o i é alterado numa interrupção sei(); // … } //... E com isto acabamos a base das interrupções. Decidi começar com estas, pois nos próximos tutoriais, explicarei as interrupções individuais de várias funcionalidades. Última revisão: 21/12/2010 27 Timers O que são e como funcionam timers? Timers são, como o nome sugere, utilizados para contar o tempo. No mundo dos microprocessadores, funcionam da seguinte forma: a partir de uma fonte de pulsos (por exemplo: o clock do AVR), incrementam uma variável a cada pulso. Se usarmos uma fonte de pulsos com uma frequência conhecida (por exemplo, o clock do AVR tem uma frequência de 16MHz), conseguimos contar o tempo. Por exemplo, com um clock de 16MHz, sabemos que ao chegar ao valor 16000000, passou um segundo. Timers no AVR O AVR tem três timers: timer0, timer1 e timer2. Cada um difere em vários aspectos, mas o mais significativo é o número de bits: o timer0 e timer2 têm cada um 8 bits, e o timer1 16 bits. Os outros aspectos são os modos que suportam, portas que controlam, etc. No entanto, estes não afecta muito a sua funcionalidade, visto que para fazer uma mesma coisa em dois timers, só são necessárias algumas mudanças nos registers e pinos usados. Mas o número de bits usados afecta bastante a funcionalidade, pois limitam a resolução que cada timer tem. Com 8 bits, só podemos contar até 255, e com 16 até 65535. Isto quer dizer que, com um clock de 16MHz, não podemos contar até 1s com nenhum, mas por exemplo, com um clock de 65kHz, conseguimos contar até 1s com o de 16 bits, e não com o de 8 bits. No entanto, quando atingem o seu limite, os timers não param de contar, apenas começam novamente do zero (isto significa que é possível utilizar software para contar 1s tanto com os timers de 8 bits e de 16 bits. No entanto, isto é geralmente fora do ideal, e mais à frente iremos examinar técnicas decomo fazer isto sem necessitar de software). Os timers podem ser usados em diferentes modos. No AVR, existem três modos: Normal, CTC (Clear Timer on Compare Match) e PWM (Pulse-Width-Modulation – este tem alguns sub-modos associados). Neste tutorial iremo-nos concentrar nos modos normal e CTC, e deixaremos o PWM para o próximo tutorial Todos os timers oferecem estes três modos. No entanto, os modos CTC e PWM podem ter certos pormenores na sua utilização/configuração que diferem entre timers. O timer de 16 bits oferece a funcionalidade total destes modos, enquanto os outros dois oferecem um sub-conjunto dos mesmos. Cada timer funciona incrementando um valor num certo register (no caso do timer de 16 bits, esse valor é guardado em dois registers. No entanto, quando programamos em C, podemos acedê-lo Última revisão: 21/12/2010 28 como se fosse um), até atingir o seu máximo, e depois volta a 0. A frequência com que incrementa esse valor depende do clock utilizado. Cada timer tem um conjunto de clocks disponíveis. Neste tutorial vamos utilizar apenas o clock do sistema (clkI/O), e os seus prescalers (um prescaler corresponde a dividir a frequência original por um certo valor. Os valores disponíveis no AVR são: 1, 8, 64, 256 e 1024. A utilização de um prescaler para o timer não afecta o clock do sistema). Neste tutorial iremos analisar apenas o timer de 16 bits, visto que é o que nos oferece mais flexibilidade, tanto em resolução e modos. No entanto, a maior parte das coisas descritas aqui podem aplicar-se aos outros se tiverem o modo correspondente disponível, e ajustando-se o código à menor resolução que oferecem e aos seus registers. Modos Normal e CTC Antes de começar a explicar os modos, vou introduzir alguns conceitos: TOP, BOTTOM, MAX e overflow. MAX corresponde ao valor máximo que o timer aguenta. No caso do de 16 bits é 65535. TOP corresponde ao valor máximo que o timer atinge. Isto depende do modo. BOTTOM é o valor mínimo que o timer tem, e que é sempre 0 (no entanto, podemos mudar isto artificialmente, através de software. Não é o mais recomendado, sendo sempre preferível mudar o TOP, por ser mais eficiente). Overflow é o nome que se dá ao que acontece quando o timer chega a MAX: sobrecarrega o máximo suportado por 16 bits e volta a 0 Iremos começar pelo modo normal. O modo normal é o mais simples: o timer simplesmente incrementa o register correspondente ao mesmo até atingir o seu limite, e nesse momento o register volta a 0. Neste modo, o TOP corresponde ao MAX. Agora, o modo CTC. O modo CTC é um pouco mais complexo, mas também útil. Basicamente, a particularidade deste modo é que podemos ajustar o TOP. O timer1 permite-nos escolher dois registers como contendo o valor de TOP: OCR1A e ICR1. Iremos ver o impacto disto mais à frente, quando estudarmos eventos relacionados com os timers. No entanto, uma coisa a ter cuidado é o seguinte: quando se escreve um novo valor para os registers OCR1A e ICR1 devemos ou ter a certeza que é um valor maior que o anterior, ou que o valor do timer é menor que esse valor (cuidado quando se usa um clock alto, pois durante a escrita, o valor é actualizado) ou que fazemos reset ao timer, pois se alterarmos o valor de OCR1A ou ICR1, e o timer já tiver ultrapassado o novo valor, vai até MAX e faz overflow, e aí é que volta a funcionar normalmente. Como usar um timer no AVR No AVR, os timers são controlados por um conjunto de registers. Última revisão: 21/12/2010 29 Para poder explicar mais facilmente como usar um timer, vamos estabelecer um objectivo: criar uma função que funcione como a função _delay_ms(). Vamos começar com o pseudo-código: // Iniciar o programa // Iniciar a função new_delayms(x) // Iniciar o timer // Verificar o timer para ver se já se passaram x ms. // Terminar a função Já podemos inserir algumas coisas: a declaração da função, o loop em que se vai verificar o valor do timer e os headers necessários para aceder aos registers: <avr/io.h> (vamos esquecer a função main neste caso): //Iniciar o programa #include <avr/io.h> void new_delayms(int x) {// Iniciar a função new_delayms(x) //Iniciar o timer for(;;) { //Verificar o timer para ver se já se passaram x ms. } } // Terminar a função Para começar, vamos compreender como se sabe quanto tempo passou, tendo em conta o valor do timer: Sabemos que o timer incrementa a sua variável de acordo com uma certa frequência, e que a fórmula para calcular a frequência é: f = incrementos/s Neste caso, visto que queremos os milisegundos, podemos ajustar a fórmula: f/1000 = incrementos/ms Nesta fórmula, o único valor que podemos conhecer do início é a frequência (como vamos usar o clock do sistema, será 16MHz), logo podemos já ajustar essa parte da fórmula: 16000 = incrementos/ms O que queremos descobrir é quantos incrementos, logo ajustamos para: incrementos = 16000*ms E temos uma forma para descobrir quantos ms passam, se começarmos do 0 no timer. No entanto, muitos estão a pensar numa coisa, provavelmente: limites. Sabemos que o limite para 16 bits é: 65535, logo, no máximo podemos medir: 65535 = 16000*ms <=> ms = 65535/16000 Última revisão: 21/12/2010 30 <=> ms ~= 4 4ms (isto tem algum erro, mas vamos ignorá-lo para manter o exemplo simples)! Isso é longe do ideal, e se quisermos medir 6, 7 ou 8 ms? Temos duas formas para aumentar esta resolução: utilizar prescalers, que diminuem o clock, ou manipulação por software. A utilização de prescalers é apropriada para casos particulares. No entanto, a manipulação por software é mais apropriada aqui, visto que nos permite calcular o tempo passado para qualquer valor possível de int (para fazermos o mesmo com um timer de 16 bits, necessitaríamos de um prescaler de 16k, o que não existe). Vamos então fazer isto passo-a-passo: inicializar o timer. Os bits de configuração/inicialização do timer (mais respectivamente de selecção de modo e clock) encontram-se em dois registers: TCCR1A, TCCR1B e TCCR1C. Para seleccionar o modo, utilizamos os bits WGM10 a WGM13 (espalhados pelos dois registers ). Neste caso, queremos o modo normal, logo pomos esses 4 bits a 0 (como esse é o valor por defeito, não temos de fazer nada). Para seleccionar o clock, utilizamos os bits CS10 a CS12, no register TCCR1B. Neste caso queremos o clock do sistema, sem prescaler. Ao olharmos para a datasheet, vemos que temos de colocar o bit CS10 com o valor 1. Nota: O timer começa sem nenhum clock seleccionado, logo não está a incrementar o register correspondente, e esse é inicializado automaticamente a 0. Assim que seleccionamos um clock, o timer começa imediatamente a incrementar o register. Logo, já podemos inicializar o timer: //Iniciar o programa #include <avr/io.h> void new_delayms(int x) {// Iniciar a função new_delayms(x) TCCR1B |= (1<<CS10); //Iniciar o timer for(;;) { //Verificar o timer para ver se já se passaram x ms. } } // Terminar a função O método por software que vamos usar é o seguinte: numa variável guardamos o valor anterior do timer. Se o novo valor for menor, significa que houve um overflow, e incrementamos uma variável que nos indica quantas vezes passaram 4ms (aviso: isto apenas se aplica para uma frequência de 16MHz. Se querem manipular o vosso programa para se adaptar a outras frequências, podem usar a macro F_CPU, que indica a frequência, e utilizar as fórmulas acima, para descobrir Última revisão: 21/12/2010 31 quantos ms passam em cada overflow). Depois guardamos o novo valor na variável e vemos quanto tempo se passou até então (o valor da variável dos 4ms+os milissegundos passados,cuja fórmula é: ms = valor/16000), e caso tenha passado o tempo desejado ou mais (ao executarmos as instruções pode passar mais tempo do que o desejado), paramos o ciclo, saindo assim da função. Os registers onde o timer1 guarda o valor do timer são TCNT1H e TCNT1L (são dois por ter 16 bits). A datasheet descreve a ordem em que estes devem ser acedidos. No entanto em C não nos precisamos de preocupar com isso, pois o seu acesso é simplificado, utilizando-se o nome TCNT1 como se fosse um só register: //Iniciar o programa #include <avr/io.h> void new_delayms(int x) {// Iniciar a função new_delayms(x) int times_4ms = 0; int prev_value; TCCR1B |= (1<<CS10); //Iniciar o timer prev_value = TCNT1; for(;;) { if(prev_value > TCNT1) times_4ms++; // Incrementar a variável dos 4 ms caso tenha havido um overflow prev_value = TCNT1; if(prev_value/16000 + times_4ms*4 >= x) //Verificar o timer para ver se já se passaram x ms. break; // Se sim, sair. } } // Terminar a função E aí temos uma função de delay que funciona com 16MHz. Esta é a forma mais básica de se usar timers, e através do código usado, parecem-nos ineficientes e básico. Usados desta forma, timers não são muito úteis … tornam-se realmente úteis quando começamos a explorar a sua utilização com eventos e interrupções, que são os temas dos próximos tópicos. Eventos relacionados com timers. Os timers têm uma série de eventos e interrupções associados aos mesmos Com estes é que se tornam muito poderosos. Neste tópico, iremos usar principalmente o modo CTC, pois os exemplos utilizados são os que melhor demonstram a sua utilidade. Este tópico irá cobrir os eventos relacionados com os pinos OC1A, OC1B, e ICP1. Os timers permitem-nos escolher um evento que pode ocorrer nos pinos OC1A/OC1B, quando o valor do TCNT1 equivale a OCR1A/OCR1B. Os eventos possíveis dependem do modo escolhido. Os possíveis para os modos normal e CTC são equivalentes, logo serão os explorados neste tutorial. Para explorarmos o que podemos fazer com estes pinos, vamos primeiro estabelecer um Última revisão: 21/12/2010 32 objectivo: piscar um LED (ligado 1s, desligado 1s, ligado 1s, desligado 1s, …), sem utilizar qualquer controlo por software/interrupções, apenas as configurações dos pinos. O pino usado neste exemplo será o pino OC1A (pino digital 9). Logo, o nosso pseudo-código será assim: // Iniciar o programa // Configurar o pino digital 9 como output (iniciar como desligado – valor por defeito // Configurar o timer // Configurar os eventos do pino OC1A // Loop eterno Já podemos preencher algumas coisas: // Iniciar o programa #include <avr/io.h> int main(void) { DDRB |= (1<<PB1); // Configurar o pino digital 9 como output (iniciar como desligado – valor por defeito // Configurar o timer // Configurar os eventos do pino OC1A for(;;); // Loop eterno } Agora, para configurar o timer, temos de fazer algumas contas … Queremos que o LED se ligue e desligue a cada segundo. Para fazer isto, sem ajuda de software, temos de usar os eventos no pino OC1A. Cada vez que o timer chega ao valor OCR1A, ocorre um evento. Ao usarmos o modo normal, independentemente de qual o valor de OCR1A, a distância de tempo em que ocorre esse evento é constante – igual ao tempo que demora para incrementar a variável do 0 ao MAX (mesmo que ponhamos OCR1A no meio, ele continua a incrementar até chegar ao MAX, e depois vai do 0 até ao MAX/2). Se testarmos os prescalers, até encontramos um que faz com que esse tempo seja perto de 1s (com o prescaler 256, 1s = 62500 iterações, MAX = 65535, nota), mas com um erro grande que aumenta em cada evento, logo não é o ideal. As contas feitas são as seguintes: FREQ = F_CPU/prescaler F_CPU = 16000000 prescaler = 256 Última revisão: 21/12/2010 33 FREQ = 62500 t = 1s f = inc/t <=> 62500 = inc/1 <=> inc = 62500 Aqui é que entra a utilidade do CTC. Para fazer o que queremos, usando o prescaler de 256, queremos fazer reset ao timer, cada vez que chega aos 62500. O modo CTC faz exactamente isso. Neste caso, 62500 corresponderá ao valor de TOP. Se forem ver para trás, onde explico este modo, irão notar que eu digo que podemos usar dois registers como valor de TOP no modo CTC: ICR1 e OCR1A. Como queremos criar um evento no pino OC1A, usar o register OCR1A como TOP é exactamente o que precisamos (se quiséssemos gerar o evento no pino OC1B, teríamos de usar ICR1 ou OCR1A como TOP, e colocar o mesmo valor no register OCR1B). Então, já podemos configurar o timer: queremos que o clock utilizado seja o clock do sistema com um prescaler de 256, no modo CTC com OCR1A como TOP e com o valor 62500 como TOP: // Iniciar o programa #include <avr/io.h> int main(void) { DDRB |= (1<<PB1); // Configurar o pino digital 9 como output (iniciar como desligado – valor por defeito // Configurar o timer TCCR1B |= (1<<WGM12); // Modo: CTC com OCR1A como TOP TCCR1B |= (1<<CS12); // Clock do sistema com prescaler de 256 OCR1A = 62500; // Valor do TOP, de forma a passar-se um segundo. // Configurar os eventos do pino OC1A for(;;); // Loop eterno } Agora só nos falta configurar os eventos no pino OC1A. Os eventos nos pinos OC1A/OC1B são configurados através dos bits COM1A0/COM1B0 e COM1A1/COM1B1 no register TCCR1A. O evento que desejamos é um toggle do pino OC1A. Ao consultarmos a datasheet, vemos que esse evento corresponde a COM1A0 = 1 e COM1A1 = 0: // Iniciar o programa #include <avr/io.h> int main(void) { DDRB |= (1<<PB1); // Configurar o pino digital 9 como Última revisão: 21/12/2010 34 output (iniciar como desligado – valor por defeito // Configurar o timer TCCR1B |= (1<<WGM12); // Modo: CTC com OCR1A como TOP TCCR1B |= (1<<CS12); // Clock do sistema com prescaler de 256 OCR1A = 62500; // Valor do TOP, de forma a passar-se um segundo. TCCR1A |= (1<<COM1A0); // Configurar os eventos do pino OC1A for(;;); // Loop eterno } E aí têm: um programa que faz toggle do pino OC1A (pino digital 9) a cada 1s. Podem testar isto com um LED, como no esquema abaixo: Também podemos forçar o evento que ocorre nos pins OC1A/OC1B escrevendo um 1 para os bits FOC1A/FOC1B do register TCCR1C. Isto, no entanto, deve ser feito com cuidado, visto que só altera o estado de output do pino, de acordo com a configuração, não gerando quaisquer interrupções associadas com uma igualdade ao register OCR1A nem fazendo reset ao timer. O tipo de eventos que estudámos primeiro, foram os eventos de output. No entanto, também temos os eventos de input, associados ao pino ICP1 (pino digital 8; na verdade, podemos ter mais do que esse pino como fonte de input, visto que um evento no analog comparator pode gerar um evento de input relacionado com o timer. No entanto, visto que o evento comporta-se da mesma forma, independentemente do input, só considerarem eventos no pino ICP1). Estes eventos são simples de compreender: quando ou o input transita de HIGH para LOW ou de LOW para HIGH (este comportamento é definido ICES1 no register TCCR1B: 0 para HIGH-LOW e vice-versa), o valor no register TCNT1 é escrito no register ICR1. Atenção à forma como isto se funciona: devem lembrar-se que o register ICR1 pode ser usado como valor de TOP no modo CTC (e PWM também, mas fica para outro tutorial). Quando isto acontece, estes eventos de input são desligados. Se se lembram do tutorial anterior, demonstrei como usar um interrupt associado com um pino digital para detectar um clique num botão. Este tipo de eventos pode ser usado da mesma forma, mas com uma vantagem: ao colocarmos o bit ICNC1 como 1 no register TCCR1C, o hardware faz Última revisão: 21/12/2010 35controlo de bouncing automático, ao testar o valor do pino 4 vezes, a ver se é igual nessas 4 vezes (nota: esses quatro testes são independentes do prescaler do timer, visto que são feitos de acordo com o clock do sistema). No próximo tópico, em que falamos sobre interrupções e timers, demonstraremos uma forma de usar este filtro, em vez da forma básica demonstrada no tutorial anterior. E estes são os principais eventos associados aos timers, relacionados com hardware. Para acabar, só faltam agora as interrupções. Interrupções e timers. Existem 4 interrupções associadas com o timer1: uma que ocorre quando se recebe um input, outras duas que ocorrem quando o valor do register TCNT1 é igual aos valores dos registers OCR1A/OCR1B e uma que ocorre quando ocorre um overflow do timer (atenção, que no modo CTC, o overflow ocorre apenas se o timer chegar ao valor MAX, e não ao top, logo pode nunca ocorrer. Esta interrupção pode ser usada para, por exemplo, verificar a ocorrência de erros quando se muda o valor de TOP). Estas interrupções estão todas definidas no register TIMSK1. Os vectores associados às mesmas são: TIMER1_CAPT_vect TIMER1_COMPA_vect TIMER1_COMPB_vect TIMER1_OVF_vect O tutorial anterior mostrou como lidar com interrupções, por isso será deixado à imaginação do leitor o que fazer com estas. No entanto, a título de exemplo, iremos cumprir o prometido no tópico anterior: codificar um programa com a mesma função que o do tutorial anterior, só que com controlo de bouncing feito pelo hardware. Neste caso, não usamos nada relacionado directamente com o timer, excepto a interrupção de input. No entanto, isto, em conjunção com outros interrupts, pode ser usado para fazer um programa que registe a distância, em tempo entre dois inputs (para caber tudo em ints, pode-se criar um int separado para os segundos, minutos e horas – o leitor pode fazer isto ser mais eficiente de acordo com qualquer critério). Como não usamos a funcionalidade do timer, não chegamos a seleccionar um timer para o clock, logo, o valor escrito em ICR1 será sempre 0. Caso o leitor queira usar o timer, tem de seleccionar um clock e modo para o mesmo (o valor escrito em ICR1 será o valor de TCNT1 na altura do input). Comecemos com o pseudo-código: Última revisão: 21/12/2010 36 // Iniciar programa // Iniciar o pino digital 4 como output, e desligado – estado por defeito // Configurar o input para reconhecer eventos LOW-HIGH // Ligar o filtro para o input. // Ligar a interrupção para o input. // Ligar as interrupções globais. // Loop eterno // Definir a ISR para o input: TIMER1_CAPT_vect // Fazer toggle do pino digital 4 Vamos começar pelo mais básico: // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> int main(void) { DDRD |= (1<<PD4); // Iniciar o pino digital 4 como output, e desligado – estado por defeito // Configurar o input para reconhecer eventos LOW-HIGH // Ligar o filtro para o input. // Ligar a interrupção para o input. sei(); // Ligar as interrupções globais. for(;;); // Loop eterno } ISR(TIMER1_CAPT_vect) { // Definir a ISR para o input: TIMER1_CAPT_vect PORTD ^= (1<<PD4); // Fazer toggle do pino digital 4 } Se se lembram das minhas explicações anteriores, os bits usados para configurar o evento de input são ICNC1 (tratar do bouncing automaticamente) e ICES1 (que tipo de evento é registado, para LOW-HIGH, queremos o valor 1), no register TCCR1B: // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> int main(void) { DDRD |= (1<<PD4); // Iniciar o pino digital 4 como output, e desligado – estado por defeito TCCR1B |= (1<<ICES1); // Configurar o input para reconhecer eventos LOW-HIGH TCCR1B |= (1<<ICNC1); // Ligar o filtro para o input. // Ligar a interrupção para o input. sei(); // Ligar as interrupções globais. for(;;); // Loop eterno Última revisão: 21/12/2010 37 } ISR(TIMER1_CAPT_vect) { // Definir a ISR para o input: TIMER1_CAPT_vect PORTD ^= (1<<PD4); // Fazer toggle do pino digital 4 } Agora só nos ligar a interrupção particular para o input. Como disse antes, as interrupções dos timers são definidas no register TIMSK1, e o bit que procuramos é o ICIE1: // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> int main(void) { DDRD |= (1<<PD4); // Iniciar o pino digital 4 como output, e desligado – estado por defeito TCCR1B |= (1<<ICES1); // Configurar o input para reconhecer eventos LOW-HIGH TCCR1B |= (1<<ICNC1); // Ligar o filtro para o input. TIMSK1 |= (1<<ICIE1); // Ligar a interrupção para o input. sei(); // Ligar as interrupções globais. for(;;); // Loop eterno } ISR(TIMER1_CAPT_vect) { // Definir a ISR para o input: TIMER1_CAPT_vect PORTD ^= (1<<PD4); // Fazer toggle do pino digital 4 } E temos um programa que usa interrupções dos timers! O circuito que usa isto é o seguinte: Última revisão: 21/12/2010 38 Timers – Parte 2, Pulse Width Modulation O que é PWM? Microcontroladores, por si só, são incapazes de gerar sinais analógicos (voltagens variáveis), apenas podendo gerar dois sinais distintos: 0 e 1 (normalmente, 0V e 5V respectivamente). No entanto, muitas vezes estes sinais são necessários para controlar vários dispositivos: servos, colunas, … Então, como podemos criar esses sinais? A resposta simples: PWM. Pensem num carro a andar: se metade de um período de tempo andar a 5 km/h, e a outra metade estiver parado, qual é a sua velocidade média ao longo desse período? 2,5 km/h. Passem esta analogia para electricidade: 5V metade do tempo e 0V a outra metade dão 2,5V. Para “enganarmos” o circuito, de forma a pensar que é mesmo 2,5V constantes, fazemos isto a uma grande frequência (e se utilizarmos alguns componentes externos, até conseguimos estabilizar a corrente, mas isso não será discutido aqui). Vou agora introduzir um conceito ligado ao PWM: o duty cycle. Nem sempre queremos que a voltagem seja 2,5V, logo deixamos ligados os 5V por mais ou menos tempo de acordo com o que precisamos. À percentagem de tempo em que os 5V ficam ligados, chamamos de duty cycle. Assim, quanto maior o duty cycle, maior a voltagem média e vice-versa (assim, duty cycle de 100% = 5V, e de 0% = 0V). Então, como podemos utilizar o PWM no AVR? Existem duas formas de gerar um sinal de PWM no AVR: por software (através de delays ou com o auxílio de interrupções) ou por hardware (que é o que veremos nesta parte do tutorial). Um pormenor que convém notar acerca do PWM: ao utilizarem o modo CTC, aconselhei a que tivessem cuidado ao actualizar os registers ICR1 e OCR1A, quando os seus valores eram o TOP. No PWM, estes registers podem ser o TOP, mas no modo PWM, o register OCR1A tem um buffer duplo. Isto significa que ao alterarmo-lo, não o fazemos directamente, mas sim a um buffer. Num momento definido pelo modo PWM (bits WGM dos registers de controlo do timer), o register OCR1A é actualizado com o valor nesse buffer. Isto dá-nos maior controlo sobre a frequência, pois podemos alterar o TOP a qualquer altura (claro que ao utilizarmos o register OCR1A como TOP, estamos a sacrificar o pino OC1A como output de PWM). Logo, quando queremos variar a frequência do PWM, devemos usar OCR1A como top, e caso queiramos uma frequência fixa, podemos usar o ICR1. Vários Modos de PWM Ao utilizarmos o AVR, temos várias modos em termos de PWM: Fast PWM, Phase Correct Última revisão: 21/12/2010 39 PWM e Phase and Frequency Correct PWM. Os modos Fast PWM e Phase/Phase and Frequency Correct PWM são os que diferem mais. Fast PWM funciona da seguinte forma: O timer conta até OCR1x, e nesse momento actualiza o pinoOC1x (se o liga ou desliga depende da configuração dos bits COM1x0 e COM1x1). Depois continua a contar até TOP (que pode ter vários valores: ICR1, OCR1A, 10 bits (1023), 9 bits (511) e 8 bits (255)), e nesse momento volta ao BOTTOM e actualiza o pino OC1x, alterando o seu estado. Phase Correct e Phase and Frequency correct PWM são muito semelhantes, alterando apenas o momento em que o register OCR1x é actualizado com o novo buffer. Como o nome sugere, a vantagem do Phase and Frequency correct PWM é ser mais apropriado para alterar a frequência do PWM. De resto, estes modos são iguais (devido a isto, apenas discutirei o modo Phase and Frequency Correct PWM). Funcionam da seguinte forma: o timer conta até ao OCR1x, momento em que actualiza o pino OC1x (dependendo da configuração, ele liga-o ou desliga-o). Depois continua até chegar ao TOP, momento em que começa a contar para trás, até atingir novamente o register OCR1x, momento em que actualiza o pino OC1x, de acordo com a configuração. A vantagem destes modos é que as ondas geradas têm sempre a mesma fase (o que é necessário para controlar servos, por exemplo), ou seja, as cristas (parte mais alta da onda) e vales (parte mais baixa da onda) correspondem, independentemente do duty cycle. A desvantagem que isto traz é que a frequência mais alta deste modo é metade da possível no modo Fast PWM. Abaixo estão alguns gráficos que mostram como funcionam estes modos. No modo Fast PWM, o pino é ligado quando há um compare match, e desligado em bottom. No modo Phase and Frequency Correct PWM, o pino é ligado quando se conta para cima, e desligado quando se conta para cima (serão estas as configurações usadas neste tutorial): Última revisão: 21/12/2010 40 Vamos agora ver como podemos usar estes modos no AVR, através de exemplos: Fast PWM O modo Fast PWM é o mais simples de todos, e muito fácil de configurar. Vamos primeiro definir um objectivo: Fazer variar a luminosidade de um LED, aumentando-a em cada 100 ms (0,1s ), até chegar ao máximo, e depois diminuindo-a, e repetimos este processo até à eternidade. O pino que vamos usar para este programa é o OC1A (pino digital 9/PB1). Para isto, basta-nos definir uma interrupção a cada 100ms que altere o valor do register OCR1A, para definir um maior/menor duty cycle. Para saber se estamos a subir ou a descer, podemos guardar um 1 ou -1 numa variável, para depois multiplicar pela variação do OCR1A, e alteramos quando chegamos ao TOP/BOTTOM. Visto que é impossível gerar um interrupt a cada 100 ms num timer de 8 bits, e estamos a usar o timer1, podemos fazer isto de duas formas: ou configurar o período do PWM para 100ms, e assim aproveitar o timer1 para essas interrupções (o que não é a melhor solução visto que assim o LED piscaria 10 vezes por segundo, o que já se torna visível ao olho humano), ou podemos usar uma interrupção no timer0/timer2 (iremos usar o 0) a cada 10ms (o que é possível com um prescaler de 1024, com um certo erro que iremos ignorar), e só fazer uma acção à 10ª vez que essa interrupção ocorre (iremos manter um contador para verificar isso). O top do timer0 pode ser calculado da seguinte forma (o prescaler usado é 1024): 10ms = 0,01s TOP = 16000000*0,01/1024 156≃ Para calcular a variação necessária do OCR1A, iremos estabelecer um objectivo: o LED irá de desligado até luminosidade máxima em 2s. Para uma frequência alta, iremos usar o Fast PWM com TOP de 8 bits (255). Assim, OCR1A tem de ir de 0 a 255 em 2s. Visto que é actualizado a cada 0,1s, ele varia 2/0,1 = 20 vezes. Logo, a sua variação será: 255/20 12 (isto tem um erro associado, mas iremos ignorá-lo).≃ Vamos então começar com Pseudo-Código: //Iniciar o programa // Definir o pino digital 9 como OUTPUT Última revisão: 21/12/2010 41 // Configurar o timer1 com modo Fast PWM // Configurar PWM para ligar o pino OC1A no compare match, e desligar no BOTTOM // Seleccionar clock no timer1 (sem prescaler, para máxima frequência). // Configurar o timer0 com prescaler de 1024, TOP de 156, modo CTC // Ligar interrupções de compare match no timer0 // Ligar interrupções globais // Loop eterno // ISR de compare match no timer0 // incrementar contador // Se o contador for igual a 10 // Incrementar/decrementar OCR1A por um factor pré- estabelecido. // Caso tenhamos atingido o valor máximo de OCR1A/BOTTOM // Alterar a operação de incremento/decremento // Fazer reset ao contador. Vamos já começar a preencher o que já sabemos (visto que a configuração do timer0 é semelhante à do timer1, iremos também fazer isso): // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> #define VARIACAO_OCR1A 12 int mult = -1; int count; int main(void) { DDRB |= (1<<PB1); // Definir o pino digital 9 como OUTPUT // Configurar o timer1 com modo Fast PWM // Configurar PWM para ligar o pino OC1A no compare match, e desligar no BOTTOM TCCR1B |= (1<<CS10); // Seleccionar clock no timer1 (sem Última revisão: 21/12/2010 42 prescaler, para máxima frequência). TCCR0A |= (1<<WGM01); TCCR0B |= (1<<CS02) | (1<<CS00); OCR0A = 156; // Configurar o timer0 com prescaler de 1024, TOP de 156, modo CTC TIMSK0 |= (1<<OCIE0A); // Ligar interrupções de compare match no timer0 sei(); // Ligar interrupções globais for(;;); // Loop eterno } ISR(TIMER0_COMPA_vect) { // ISR de compare match no timer0 ++count; // incrementar contador if(count == 10) { // Se o contador for igual a 10 OCR1A += mult*VARIACAO_OCR1A; // Incrementar/Decrementar OCR1A por um factor pré- estabelecido. if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso tenhamos atingido o valor máximo de OCR1A/BOTTOM mult = -mult; // Alterar a operação de incremento/decremento } count = 0; // Fazer reset ao contador. } } Só nos falta mesmo configurar o PWM agora! Vamos então ver a datasheet … O que queremos é o modo Fast PWM, com uma resolução de 8 bits. Vemos lá que conseguimos isso colocando os bits WGM12 e WGM10, dos registers TCCR1A e TCCR1B respectivamente, a 1: // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> #define VARIACAO_OCR1A 12 int mult = -1; Última revisão: 21/12/2010 43 int count; int main(void) { DDRB |= (1<<PB1); // Definir o pino digital 9 como OUTPUT TCCR1A |= (1<<WGM10); TCCR1B |= (1<<WGM12); // Configurar o timer1 com modo Fast PWM // Configurar PWM para ligar o pino OC1A no compare match, e desligar no BOTTOM TCCR1B |= (1<<CS10); // Seleccionar clock no timer1 (sem prescaler, para máxima frequência). TCCR0A |= (1<<WGM01); TCCR0B |= (1<<CS02) | (1<<CS00); OCR0A = 156; // Configurar o timer0 com prescaler de 1024, TOP de 156, modo CTC TIMSK0 |= (1<<OCIE0A); // Ligar interrupções de compare match no timer0 sei(); // Ligar interrupções globais for(;;); // Loop eterno } ISR(TIMER0_COMPA_vect) { // ISR de compare match no timer0 ++count; // incrementar contador if(count == 10) { // Se o contador for igual a 10 OCR1A += mult*VARIACAO_OCR1A; // Incrementar/Decrementar OCR1A por um factor pré- estabelecido. if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso tenhamos atingido o valor máximo de OCR1A/BOTTOM mult = -mult; // Alterar a operação de incremento/decremento } count = 0; // Fazer reset ao contador. } } Agora só nos falta configurar o comportamento do Fast PWM. Queremos que ele ligue o pino no compare match, e desligue no BOTTOM. Assim, quanto maior o OCR1A, menor o duty cycle. Logo, como queremos começar com o LED desligado, iniciamos o Última revisão: 21/12/2010 44 OCR1A com o seu maior valor, e a variável de incremento/decrementocom o valor -1. Para definir o comportamento do PWM para o pin OC1A, temos de mexer nos bits COM1A0 e COM1A1 do register TCCR1A (colocar os dois a 1 para o que desejamos): // Iniciar o programa #include <avr/io.h> #include <avr/interrupt.h> #define VARIACAO_OCR1A 12 int mult = -1; int count; int main(void) { DDRB |= (1<<PB1); // Definir o pino digital 9 como OUTPUT TCCR1A |= (1<<WGM10); TCCR1B |= (1<<WGM12); // Configurar o timer1 com modo Fast PWM OCR1A = 20*VARIACAO_OCR1A; TCCR1A |= (1<<COM1A0) | (1<<COM1A1); // Configurar PWM para ligar o pino OC1A no compare match, e desligar no BOTTOM TCCR1B |= (1<<CS10); // Seleccionar clock no timer1 (sem prescaler, para máxima frequência). TCCR0A |= (1<<WGM01); TCCR0B |= (1<<CS02) | (1<<CS00); OCR0A = 156; // Configurar o timer0 com prescaler de 1024, TOP de 156, modo CTC TIMSK0 |= (1<<OCIE0A); // Ligar interrupções de compare match no timer0 sei(); // Ligar interrupções globais for(;;); // Loop eterno } ISR(TIMER0_COMPA_vect) { // ISR de compare match no timer0 ++count; // incrementar contador if(count == 10) { // Se o contador for igual a 10 OCR1A += mult*VARIACAO_OCR1A; // Incrementar/Decrementar OCR1A por um factor pré- estabelecido. Última revisão: 21/12/2010 45 if(OCR1A == 20*VARIACAO_OCR1A || OCR1A == 0) { // Caso tenhamos atingido o valor máximo de OCR1A/BOTTOM mult = -mult; // Alterar a operação de incremento/decremento } count = 0; // Fazer reset ao contador. } } E com isto, fizemos o nosso primeiro programa que usa PWM! Este programa pode ser usado com o circuito abaixo: Phase and Frequency Correct PWM O modo Phase and Frequency Correct PWM, como mencionei antes, é útil para controlar servos, devido a manter sempre a mesma fase. Por isso, iremos utilizá-lo para isso mesmo: controlar um servo. Primeiro, temos de compreender o que é e como funciona um servo: um servo é um conjunto de um motor e electrónica que abstraem muito do controlo de motores. Estes permitem-nos, através de um sinal, especificar para onde queremos que o motor se mova, em graus, ou no caso de servos de rotação contínua, controlar a sua direcção e velocidade. Logo, esta electrónica abstrai muito daquilo que precisamos para controlar um motor, como pontes-H e leitura e interpretação do feedback do motor. Estes têm três fios: Vcc, GND e controlo. O Vcc e GND devem ser ligados respectivamente aos terminais positivos e negativos da fonte de alimentação. O controlo será ligado a um pino do AVR, Última revisão: 21/12/2010 46 que neste caso será o pino digital 9 (OC1A, pois temos um output de PWM nesse). Para controlarmos o servo, precisamos de compreender que tipo de sinais lê. No geral, os servos usam um sinal de 50Hz, cujo período em HIGH determina a o ângulo ou direcção e sentido para controlar o motor. O período em HIGH pode ser entre 1ms e 2ms, sendo 1ms completamente para a esquerda e 2ms completamente para a direita, e 1.5ms meio. Isto corresponderá a respectivamente 0º, 180º e 90º num servo com capacidade de rodar até 180º (isto não é constante, e pode variar de servo para servo). Este sinal pode parecer confuso, mas a imagem abaixo pode ajudar a esclarecer (para um sinal de 1ms – completamente para a esquerda): Para exemplificar isto, vou definir um objectivo: a criação de uma função que altera a posição do servo, tendo em conta que este se encontra ligado ao pino OC1A, e que o PWM já foi inicializado para uma frequência fixa de 50Hz, no timer1 (neste caso, irá receber um valor entre 0 e 100, para definir a posição entre 1ms e 2ms). // Iniciar o programa // Colocar o pino digital 9/OC1A como output. // Iniciar o timer1 no modo Phase and Frequency Correct PWM // Configurar o timer para ligar o pino OC1A na subida, e desligá-la na descida. // Seleccionar o clock do timer1. // Configurar para uma frequência de 50Hz // Chamar a função rodar, para ir todo para a esquerda (valor dado: 0). // Loop eterno. Última revisão: 21/12/2010 47 // Função rodar(x); x – valor entre 0 e 100 // Converter o valor entre 0 e 100 para um valor em ms // Converter o valor em ms para um valor de OCR1A, de acordo com o prescaler e clock. Como devem ter reparado, este código implica muita matemática. Primeiro, temos de descobrir como fazer uma frequência de 50Hz: O clock é 16MHz, logo, a onda terá de ocorrer a cada 16M/50 = 320K ciclos para ter 50Hz. No entanto, isto não cabe num register de 16 bits, logo usaremos um prescaler. Neste caso 8 é suficiente: 320K/8 = 40K No entanto, temos de ter um cuidado especial quando usamos Phase and Frequency Correct PWM: visto que ele primeiro conta para cima, e depois para baixo, a frequência será metade daquela obtida em Fast PWM (que nesse caso teria um TOP de 40K para 50Hz). Logo, o TOP será 40K/2 = 20K, para termos a frequência 50Hz. Como nunca mudamos a frequência, iremos usar um TOP fixo, logo é seguro usar o register ICR1. Para converter o valor entre 0 e 100, para um valor entre 1 e 2, basta-nos usar a seguinte fórmula: (x+100)/100 No entanto, isto não é suficiente para colocar no OCR1A. Visto que iremos usar a configuração em que o pino é ligado na subida, e desligado na descida, temos de ver quanto tempo demora o timer a chegar ao TOP desde OCR1A, e a descer novamente, até chegar a OCR1A. Assim, para x ms, temos: 1 ms = 16000000/8/1000 = 2000 x ms = 2000*x. OCR1A = 20000-2000*x/2 Logo, podemos chegar à fórmula geral (x estando entre 0 e 100): OCR1A = 20000-2000*(x+100)/100/2 = 20000-10*(x+100) Assim, já podemos criar a nossa função rodar (e já agora, adicionamos aquilo que já sabemos fazer): Última revisão: 21/12/2010 48 // Iniciar o programa #include <avr/io.h> void rodar(int); int main(void) { DDRB |= (1<<PB1); // Colocar o pino digital 9/OC1A como output. // Iniciar o timer1 no modo Phase and Frequency Correct PWM // Configurar o timer para ligar o pino OC1A na subida, e desligá-la na descida. TCCR1B |= (1<<CS11); // Seleccionar o clock do timer1. ICR1 = 20000; // Configurar para uma frequência de 50Hz rodar(0); // Chamar a função rodar, para ir todo para a esquerda (valor dado: 0). for(;;); // Loop eterno. } void rodar(int x) {// Função rodar(x); x – valor entre 0 e 100 // Converter o valor entre 0 e 100 para um valor em ms // Converter o valor em ms para um valor de OCR1A, de acordo com o prescaler e clock. OCR1A = 20000-10*(x+100); } Agora vamos iniciar o timer1 no modo Phase and Frequency Correct PWM com o TOP em ICR1 (neste caso, bastava o modo Phase Correct PWM, mas são o dois semelhantes o suficiente para não fazer diferença em nada :P Caso queiram usar, basta ver na datasheet as diferenças), e segundo a datasheet, faz-se isso colocando o bit WGM13 a 1, no register TCCR1B. // Iniciar o programa #include <avr/io.h> void rodar(int); Última revisão: 21/12/2010 49 int main(void) { DDRB |= (1<<PB1); // Colocar o pino digital 9/OC1A como output. TCCR1B |= (1<<WGM13); // Iniciar o timer1 no modo Phase and Frequency Correct PWM // Configurar o timer para ligar o pino OC1A na subida, e desligá-la na descida. TCCR1B |= (1<<CS11); // Seleccionar o clock do timer1. ICR1 = 20000; // Configurar para uma frequência de 50Hz rodar(0); // Chamar a função rodar, para ir todo para a esquerda (valor dado: 0). for(;;); // Loop eterno. } void rodar(int x) {// Função rodar(x); x – valor entre 0 e 100 // Converter o valor entre 0 e 100 para um valor em ms // Converter o valor em ms para um valor de OCR1A, de acordo com o prescaler e clock. OCR1A = 20000-10*(x+100); } E para terminar, configurar o comportamento dos pinos. O que desejamos é alcançado colocandoambos os bits COM1A0 e COM1A1 a 1, no register TCCR1A: // Iniciar o programa #include <avr/io.h> void rodar(int); int main(void) { DDRB |= (1<<PB1); // Colocar o pino digital 9/OC1A como output. TCCR1B |= (1<<WGM13); // Iniciar o timer1 no modo Phase and Frequency Correct PWM TCCR1A |= (1<<COM1A0) | (1<<COM1A1); // Configurar o timer para ligar o pino OC1A na subida, e desligá-la na descida. TCCR1B |= (1<<CS11); // Seleccionar o clock do timer1. ICR1 = 20000; // Configurar para uma frequência de 50Hz Última revisão: 21/12/2010 50 rodar(0); // Chamar a função rodar, para ir todo para a esquerda (valor dado: 0). for(;;); // Loop eterno. } void rodar(int x) {// Função rodar(x); x – valor entre 0 e 100 // Converter o valor entre 0 e 100 para um valor em ms // Converter o valor em ms para um valor de OCR1A, de acordo com o prescaler e clock. OCR1A = 20000-10*(x+100); } E aqui temos, uma função para controlar servos! Podem usar isto com o circuito abaixo: E com isto fica concluído o tutorial sobre timers. Última revisão: 21/12/2010 51 Analog-to-Digital Converter Formato Analógico e Digital No mundo da electrónica, podemos identificar dois tipos de sinais: os digitais e os analógicos. Os sinais digitais distinguem-se por poderem ter apenas dois estados (normalmente designados por desligado/ligado, 0/1, e muitas vezes representados pelas diferenças de potencial 0V e 5V, respectivamente). Os sinais analógicos, no entanto, podem ter vários estados. Por exemplo, todos os estados entre 0V e 5V (podem incluir 1V, 2V, …). Ao usar o PWM, já convertemos de uma certa forma, um sinal digital para um sinal analógico, já que a diferença de potencial resultante é alterada pelo duty cycle do sinal em formato PWM. Neste tutorial, iremos estudar o ADC, que basicamente converte um sinal digital para um sinal analógico. O que é o ADC? O ADC (Analog-to-Digital Converter) é uma funcionalidade do micro-controlador AVR que permite converter uma certa diferença de potencial (entre 0 e um valor de referência) num valor numérico, de acordo com a resolução pretendida. O ADC do AVR tem uma resolução máxima de 10 bits. Isto significa que nos pode dar valores entre 0 e 1023. Se utilizarmos como valor de referência AVcc (o mais comum – um pino do ADC que deve estar sempre ligado a uma diferença de potencial muito próxima do Vcc do micro- controlador, normalmente 5V), significa que temos uma precisão de 5/1023 0,0049, ou seja, de≃ cerca de 4,9 mV. Por exemplo, se o ADC tiver como input uma diferença de potencial de 2,5V, irá retornar o valor decimal 510. Como funciona o ADC no AVR? No AVR, o ADC tem vários pormenores com que nos temos de preocupar: um multiplexer/mux que nos permite escolher o input (permite escolher até 8 inputs, no entanto no atmega328 utilizado só temos 6), a possibilidade de utilizarmos auto-triggers (gatilhos automáticos), a escolha da diferença de potencial de referência e a frequência com que se faz conversões, utilizando um prescaler. O atmega328 dá-nos 6 pinos que podemos utilizar como input para o ADC – ADC0 (PC0) a Última revisão: 21/12/2010 52 ADC5 (PC5). Para seleccionar um destes manipulamos os bits MUXn no register ADMUX (podemos também ler o input ADC8, que consiste num sensor de temperatura interno, que discutiremos noutro tópico) – consultar datasheet para saber quais os valores a usar. Para escolher qual a diferença de potencial utilizada como referência, temos primeiro de conhecer as opções: AREF (uma diferença de potencial externa, ligada ao pino com esse nome), AVcc (uma referência interna, que tem de estar próxima do valor de Vcc, normalmente 5V) e uma referência interna de 1.1V (apesar de parecer inútil para a maior parte dos usos, é utilizada para o sensor de temperatura interno que explicaremos mais à frente). Podemos seleccionar qual das opções a usar, manipulando os bits REFSn no register ADMUX – consultar datasheet para saber quais os valores a usar. Outro pormenor que temos de ter em atenção é que não podemos usar como referência as diferenças de potencial internas quando aplicamos uma diferença de potencial no pino AREF. Com o ADC, para fazer uma leitura, temos de dar uma “ordem” para o fazer. Isto consegue-se colocando o valor 1 no bit ADSC do register ADCSRA. No entanto, nem sempre queremos ter de fazer isso para saber o valor de um input, ainda por cima porque essa conversão não ocorre em apenas um ciclo. Por isso é-nos útil definir gatilhos que iniciem a conversão – as opções que temos são flags de interrupções, ou seja, funciona como se fosse uma interrupção que ocorre separadamente do nosso código. Atenção que, caso não lidemos com a interrupção utilizada como gatilho, nem que seja só definir uma ISR que não faça nada, de forma a desabilitar a flag de interrupção, a conversão só ocorre na primeira vez que a flag mudar de 0 para 1 (já que nunca mudamos de 1 para 0 posteriormente) – excepto quando utilizamos free-running mode. Estes gatilhos funcionam mesmo que não liguemos as interrupções globais e particulares. Neste documento apenas iremos utilizar o gatilho free-running mode, já que o funcionamento das interrupções já foi bem explicado, e estes gatilhos funcionam da mesma forma. Para escolher o gatilho, alteramos os bits ADTSn do register ADCSRB. O ADC tem mais um pormenor com que nos temos de preocupar: a frequência do clock do ADC. Para seleccionarmos esta frequência, temos de trabalhar com um prescaler, que funciona muito como o dos timers: F_CPU/PRESCALER. Segundo a datasheet, para ter a máxima resolução, temos de uma frequência entre 50kHz e 200kHz. Visto que F_CPU = 16000000, o único valor disponível que nos dá uma frequência adequada é o 128, dando-nos uma frequência de 125kHz (atenção: se não desejarem a frequência máxima de 10bits, podem usar um valor mais alto para a frequência, no entanto, a datasheet não diz mais nada além disto), logo é esse que utilizaremos neste documento. Agora que já tirámos estes pormenores do caminho, vamos criar um programa de exemplo que faz uma coisa muito simples: ler um valor do ADC e guardá-lo numa variável (não irá fazer nada com ele … mais à frente, iremos explorar como fazer um programa mais útil, construindo um Última revisão: 21/12/2010 53 pequeno sensor de distância analógico) – neste caso iremos usar o pino ADC0. Comecemos com o pseudo-código: // Iniciar programa // Configurar o ADC: // Prescaler de 128 // Referência AVcc // Configurar o multiplexer para usar o ADC0 // Ligar o ADC // Iniciar uma conversão no ADC // Esperar até que a conversão esteja terminada // Ler o valor do ADC e guardá-lo numa variável // Ciclo eterno Podemos já preencher algumas coisas: // Iniciar programa #include <avr/io.h> int main(void) { int adc_value; // Configurar o ADC: // Prescaler de 128 // Referência AVcc // Configurar o multiplexer para usar o ADC0 // Ligar o ADC // Iniciar uma conversão no ADC // Esperar até que a conversão esteja terminada // Ler o valor do ADC e guardá-lo numa variável for(;;); // Ciclo eterno } Vamos começar por configurar o ADC. Visto que não iremos usar nenhum gatilho automático, não nos temos de preocupar com isso. Para configurar o prescaler como 128, temos de colocar os bits ADPS2, ADPS1 e ADPS0 todos a 1 (register ADCSRA). Para seleccionar como diferença de potencial de referência o AVcc temos de colocar o bit REFS0 a 1 (register ADMUX). Visto que para seleccionar o pino ADC0, só precisamos de colocar um 0 nos bits MUX3..0(register ADMUX), e é esse o seu valor por defeito, não temos de fazer nada quanto à selecção do pino: // Iniciar programa #include <avr/io.h> int main(void) { int adc_value; // Configurar o ADC: ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Última revisão: 21/12/2010 54 Prescaler de 128 ADMUX |= (1<<REFS0); // Referência AVcc // Configurar o multiplexer para usar o ADC0 – feito por defeito // Ligar o ADC // Iniciar uma conversão no ADC // Esperar até que a conversão esteja terminada // Ler o valor do ADC e guardá-lo numa variável for(;;); // Ciclo eterno } Para ligar o ADC, temos de colocar um 1 no bit ADEN do register ADCSRA: // Iniciar programa #include <avr/io.h> int main(void) { int adc_value; // Configurar o ADC: ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referência AVcc // Configurar o multiplexer para usar o ADC0 – feito por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC // Iniciar uma conversão no ADC // Esperar até que a conversão esteja terminada // Ler o valor do ADC e guardá-lo numa variável for(;;); // Ciclo eterno } Para iniciar uma conversão, temos de colocar um 1 no bit ADSC (register ADCSRA). Este bit é lido como 1 enquanto se está a processar uma conversão. Quando fica com o valor 0, significa que conversão terminou. Assim, para esperarmos que a conversão termine, basta testar o valor deste bit (esta não é a melhor forma de fazer isto – especialmente neste código – em que o melhor seria utilizar uma variável global e uma interrupção, mas discutiremos isso mais para a frente): // Iniciar programa #include <avr/io.h> int main(void) { int adc_value; // Configurar o ADC: ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referência AVcc // Configurar o multiplexer para usar o ADC0 – feito por defeito Última revisão: 21/12/2010 55 ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADSC); // Iniciar uma conversão no ADC while(ADCSRA&(1<<ADSC)); // Esperar até que a conversão esteja terminada // Ler o valor do ADC e guardá-lo numa variável for(;;); // Ciclo eterno } Agora só nos falta ler o valor que resultou da conversão. Este valor é guardado em dois registers: ADCL e ADCH. Estes têm de ser lidos numa ordem específica (primeiro o ADCL e depois o ADH). Mas como usamos C, isto é abstraído pelo compilador, bastando-nos ler o register ADC: // Iniciar programa #include <avr/io.h> int main(void) { int adc_value; // Configurar o ADC: ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referência AVcc // Configurar o multiplexer para usar o ADC0 – feito por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADSC); // Iniciar uma conversão no ADC while(ADCSRA&(1<<ADSC)); // Esperar até que a conversão esteja terminada adc_value = ADC; // Ler o valor do ADC e guardá-lo numa variável for(;;); // Ciclo eterno } Como ligar o input ao AVR? Apesar de parecer uma pergunta simples, foi uma das coisas com que tive mais dificuldade quando comecei a utilizar o ADC. A técnica que utilizei, foi pensar no ADC como se fosse um voltímetro quando o utilizo, que mede a diferença de potencial entre o ponto onde ligamos o input e o GND (atenção: para se poder utilizar o ADC correctamente, o input analógico e o AVR têm de partilhar o GND). Isto significa que não podemos, por exemplo, medir a diferença de potencial de uma resistência, se esta está mesmo no início do circuito, e ligarmos o pino ao espaço entre o terminal positivo e a resistência. Se queremos medir a diferença de potencial da resistência, ou colocamo-la no final do circuito ou medimos a diferença de potencial do resto do circuito (ligado o pino depois da resistência), e depois é só subtrair à diferença de potencial total (o que não funcionará se não a medirmos/conhecermos). Última revisão: 21/12/2010 56 Utilizar o ADC – construir um sensor de distância Neste tópico iremos tentar utilizar o ADC de uma forma mais prática – através da construção de um sensor de distância analógico. Para este sensor, utilizaremos um LED emissor de infravermelhos, um receptor de infravermelhos (foto-transístor) e duas resistências, uma de 220 ohms e uma de 10k ohms, resultando no seguinte circuito: AIN0 significa o analog pin 0 do arduino, que corresponde ao pino ADC0 do AVR. Não irei discutir como funciona este sensor de distância, deixando-o como um desafio ao leitor (nota: isto só funciona para detectar distâncias, quando a cor é constante, e funciona para detectar cores/reflectividade quando a distância é constante). Vamos começar por definir um objectivo: queremos que o ADC meça constantemente o input do sensor de distância, e guarde o valor que esse nos dá numa variável (mais à frente iremos fazer alguma coisa com esse valor). Para isso, iremos configurar o ADC em free-running mode (este modo é definido por um gatilho automático em que o ADC está constantemente a realizar conversões, e em que não nos precisamos de preocupar em colocar flags de interrupções a 0. No entanto, enquanto nos outros modos não precisamos de iniciar as conversões, neste temos de “ordenar” ao ADC que faça uma conversão primeiro), e utilizaremos interrupções associadas ao ADC para actualizar a variável. Visto que não precisamos de uma resolução de 10 bits, iremos apenas utilizar uma de 8 bits. Podemos realizar isto de duas formas: ou lemos o valor do ADC de 10 bits, e retiramos os dois bits da direita, ou podemos utilizar uma função que o ADC nos dá, que é a de alinhar o resultado à esquerda, guardando os 8 bits mais significativos no register ADCH, e os dois menos significativos no register ADCL, fazendo com que apenas necessitemos de ler o register ADCH. Para ligar esta funcionalidade, temos de colocar o valor 1 no bit ADLAR do register ADMUX (nota: visto que apenas necessitamos de 8 bits, poderíamos utilizar uma frequência maior que 125kHz, mudando para isso o prescaler, mas não o faremos neste caso). Visto que ligamos o sensor ao terminal positivo de 5V do arduino, não é necessário utilizar uma Última revisão: 21/12/2010 57 referência de diferença de potencial externa, utilizando-se assim o AVcc. Comecemos com o pseudo-código: // Iniciar programa // Criar uma variável com o valor do sensor de distância do ADC // Configurar o ADC: // Resolução de 8 bits // Prescaler de 128 // Referência: AVcc // Gatilho automático: Free-running mode // Input: ADC0 – seleccionado por defeito // Ligar o ADC // Ligar a interrupção particular do ADC // Ligar as interrupções globais // Iniciar as conversões no ADC // Ciclo eterno // Fazer algo com o valor da variável do sensor de distância // Definir uma interrupção que ocorre a cada ocorrência de uma conversão // Ler o valor do ADC, e guardá-lo numa variável Já podemos preencher algumas coisas: // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> // Criar uma variável com o valor do sensor de distância do ADC volatile unsigned char adc_distance; // char porque só precisamos de 8 bits int main(void) { // Configurar o ADC: // Resolução de 8 bits ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referência: AVcc // Gatilho automático: Free-running mode // Input: ADC0 – seleccionado por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC // Ligar a interrupção particular do ADC sei(); // Ligar as interrupções globais ADCSRA |= (1<<ADSC); // Iniciar as conversões no ADC for(;;) {// Ciclo eterno // Fazer algocom o valor da variável do sensor de distância } Última revisão: 21/12/2010 58 } ISR() {// Definir uma interrupção que ocorre a cada ocorrência de uma conversão adc_distance = ADCH; // Ler o valor do ADC, e guardá-lo numa variável } Para resolução de 8 bits, colocamos o bit ADLAR a 1 no register ADMUX. A interrupção que utilizaremos é a única relacionada com o ADC – a que ocorre quando se realiza uma conversão. O seu vector é: ADC. Para a ligar, colocamos o valor 1 no bit ADIE do register ADCSRA. Para seleccionar o gatilho automático de free-running mode, necessitamos primeiro de ligar o modo de gatilho automático (colocar o valor 1 no bit ADATE do register ADCSRA), e depois necessitamos de manipular os bits ADTSn no register ADCSRB (para Free-running mode não precisamos de de fazer nada já que para este modo só temos de colocar os seus valores a 0 – o seu valor por defeito): // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> // Criar uma variável com o valor do sensor de distância do ADC volatile unsigned char adc_distance; // char porque só precisamos de 8 bits int main(void) { // Configurar o ADC: ADMUX |= (1<<ADLAR); // Resolução de 8 bits ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referência: AVcc ADCSRA |= (1<<ADATE); // Gatilho automático: Free- running mode // Input: ADC0 – seleccionado por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADIE); // Ligar a interrupção particular do ADC sei(); // Ligar as interrupções globais ADCSRA |= (1<<ADSC); // Iniciar as conversões no ADC for(;;) {// Ciclo eterno // Fazer algo com o valor da variável do sensor de distância } } ISR(ADC_vect) {// Definir uma interrupção que ocorre a cada ocorrência de uma conversão Última revisão: 21/12/2010 59 adc_distance = ADCH; // Ler o valor do ADC, e guardá-lo numa variável } E com isto temos um programa que utiliza o ADC em quase todo o seu potencial! O que foi explicado até agora já permite ao leitor utilizar o ADC. No entanto, os exemplos apresentados não têm nenhum efeito observável, o que impede o leitor de experimentar. Por isso vamos preencher o interior do ciclo eterno para fazer alguma coisa com o valor do sensor de distância. Basicamente, vamos fazer com que um LED esteja ligado quando o valor de input seja maior que 2,5V (~128, visto que usamos uma resolução de 8 bits), e desligado quando é menor. Para começar, vamos adicionar ao circuito anterior, estas ligações: E agora, o código: // Iniciar programa #include <avr/io.h> #include <avr/interrupt.h> // Criar uma variável com o valor do sensor de distância do ADC volatile unsigned char adc_distance; // char porque só precisamos de 8 bits int main(void) { DDRB |= (1<<PB1); // Configurar o pino digital 9 como output // Configurar o ADC: ADMUX |= (1<<ADLAR); // Resolução de 8 bits ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // Prescaler de 128 ADMUX |= (1<<REFS0); // Referência: AVcc ADCSRA |= (1<<ADATE); // Gatilho automático: Free- running mode // Input: ADC0 – seleccionado por defeito ADCSRA |= (1<<ADEN); // Ligar o ADC ADCSRA |= (1<<ADIE); // Ligar a interrupção particular do ADC sei(); // Ligar as interrupções globais ADCSRA |= (1<<ADSC); // Iniciar as conversões no ADC for(;;) {// Ciclo eterno if(adc_distance > 128) // Testar o valor da Última revisão: 21/12/2010 60 distância, e ligar o pino caso seja maior que 128 e vice versa. PORTB |= (1<<PB1); else PORTB &= ~(1<<PB1); } } ISR(ADC_vect) {// Definir uma interrupção que ocorre a cada ocorrência de uma conversão adc_distance = ADCH; // Ler o valor do ADC, e guardá-lo numa variável } Nota: o ligar e desligar o pino podia ter sido feito na interrupção. Visto que já tínhamos destinado antes o ciclo para isso, e que as interrupções devem ter o mínimo de código possível, achámos melhor pôr no ciclo. ADC8 – medir a temperatura interna Como mencionámos anteriormente, existe um input extra para o ADC – o ADC8. Este está ligado a um termómetro interno que nos permite ver a temperatura interna do micro-controlador. Não iremos discutir este input em pormenor, visto que tem maior utilidade em casos de utilizações em condições extremas, ou com grandes velocidades de relógio. Para utilizar este input, é necessário seleccionar a referência interna de 1.1V. O valor medido tem uma sensibilidade de 1 mV/ºC, com um erro de +-10ºC. Valores típicos, como descritos na datasheet são: 242mV para -45ºC, 314mV para 25ºC e 380mV para 85ºC, logo podemos estabelecer um valor de 290mV para 0ºC. No entanto, na datasheet também vemos que depende da calibração do chip, podendo esta ser alterada na EEPROM. E assim terminamos o nosso tutorial sobre o ADC. Última revisão: 21/12/2010 61 "Excerto do "Introdução ao avr-gcc usando o AvrStudio – Segunda Parte" de Senso (http://lusorobotica.com/index.php?topic=2838.15) com alterações/adaptações de Cynary (formatação e conteúdo)" Comunicação Serial no AVR Neste tutorial iremos utilizar um esqueleto básico para iniciar todos os programas, que tem os includes e a declaração do main: #define F_CPU 16000000UL //permite usar os delays calibrados #include <avr/io.h> //definições gerais de pinos e registos #include <util/delay.h> int main(void){ // inicio da função main // O vosso programa fica entre as chavetas return 0; // toda a função não void tem de ter um return } Sempre que quiserem programar podem usar este esqueleto que são sempre uns caracteres a menos que têm de escrever. Como funciona a comunicação Serial? Antes de aprendermos a usar a comunicação serial no AVR, convém compreendermos como funciona para sermos capazes de entender o que todas as opções que nos estão disponíveis na datasheet significam. A comunicação serial consiste em enviar bits sequencialmente. Esta é utilizada para permitir comunicação entre dispositivos (um periférico, como um modem ou micro-controlador, e um PC, dois PCs, ou até dois periféricos). Para que os dispositivos se possam entender, foram definidos vários standards para a comunicação serial. No entanto, estes standards também exigem que os dispositivos estejam pré-programados para certos parâmetros. A comunicação serial tem várias características: bits de paridade, stop bits, start bit, e sincronismo/assincronismo. Dados enviados por comunicação serial têm o seguinte formato: Start Bit – Dados – bit de paridade – Stop bit(s). O start bit indica o início da comunicação. Enquanto o dispositivo não está a receber/enviar dados a linha de comunicação tem sempre o valor 1 (o que define os valores 0/1 depende do hardware … o AVR usa os valores 0V/Vcc (normalmente 5V), respectivamente. No entanto, o standard RS232 define os valores 3V/-3V, respectivamente). O start bit tem sempre o valor 0, para sinalizar que estamos a começar uma transmissão. Os dados podem ter entre 7 e 9 bits no caso do AVR. Isto é um dos pormenores que tem de ser Última revisão: 21/12/2010 62 pré-programado nos dispositivos que estão a comunicar. Neste documento, iremos utilizar sempre 8 bits. O bit de paridade é utilizado para detectar erros. Normalmente não se usa paridade para isso, pois é muito ineficiente. No caso do bit de paridade estar configurado para odd, é utilizado para termos um número ímpar de 1s. Por exemplo, no caso de enviarmos os bits: 1111000, o bit de paridade seria 1, para termos 5 bits. No caso de estar configurado para even, é utilizado para termos um número par de 1s. No entanto, visto que podemos ter erros no próprio bit de paridade ouem mais do que um bit, este método é muito ineficiente e não utilizaremos (não iremos abordar a detecção de erros na comunicação serial neste documento). O stop bit tem sempre o valor de 1, e indica o fim da transmissão. Um ou dois podem ser usados, mas não faz diferença na realidade, portanto iremos sempre configurar o AVR para usar um. A comunicação serial pode ser síncrona ou assíncrona. Na comunicação assíncrona, os dispositivos são pré-programados com uma velocidade de transmissão (denominada de baud rate), de forma a saberem a distância entre os bits. No caso da comunicação síncrona, uma linha extra é utilizada para indicar um clock comum a ambos os dispositivos. Basicamente, este clock indica aos dispositivos quando é que um novo bit é transferido, e também inicia e termina a comunicação. A comunicação síncrona dá-nos a possibilidade de maiores velocidades de transferência, e também retira a necessidade de se utilizar um start e stop bit. No entanto, tem a desvantagem de ser necessário um fio extra para indicar o clock. Nesta primeira parte acerca de comunicação serial, iremos apenas abordar comunicação assíncrona. Quando falarmos de I²C, um tipo especial de protocolo de comunicação serial, iremos utilizar sincronidade. Para possibilitar a comunicação serial, necessitamos de três fios: transferência, recepção e ground. O fio de ground é utilizado de forma a que os GND de ambos os dispositivos seja igual (de forma a que 5V num, seja 5V no outro). Os outros dois fios têm de se ligar de uma forma cruzada – o fio ligado à transmissão de um dispositivo (pino TXD/PD1 no AVR/pino digital 1 no arduino) tem de estar ligado à recepção do outro dispositivo (pino RXD/PD0 no AVR/pino digital 0 no arduino). Agora que já sabemos como funciona a comunicação serial, vamos aprender a programar o AVR para usá-la! O que é a USART? A USART é um módulo de hardware que está dentro dos nossos atmegas, que permite ao nosso chip comunicar com outros dispositivos usando um protocolo serial, isto quer dizer que com apenas dois fios podemos enviar e receber dados. Um dos maiores usos da USART é a comunicação serial com o nosso computador, e para isso temos de configurar a USART para ela fazer exactamente aquilo que queremos. Última revisão: 21/12/2010 63 Inicializando a USART do AVR Falando agora especificamente de como activar e usar a nossa USART para falar com o computador, neste tutorial em vez de deixar um monte de linhas de código perdidas no meio no nosso main vou antes criar algumas funções, pois assim podem copiar as funções e usar noutros programas, até porque fica tudo mais limpinho e separado. Em pseudo-código eis o que temos de fazer: // Iniciar e configurar a usart // Criar uma função para enviar um byte/caracter // Criar uma função para receber um byte/caracter // Fazer um simples eco na função main Como não faço ideia de como a USAR funciona o que devo fazer é pegar no datasheet e começar a ler, e como as pessoas na atmel até fizeram um bom trabalho a fazer este datasheet está tudo muito bem organizado com um índice e marcadores e facilmente descobrimos que a secção sobre a USART começa na página 177- Secção 19, e temos até código em C e assembly para configurar a USART, quer então dizer que o nosso trabalho está facilitado, e pouco mais temos que fazer que ler e passar para o nosso programa o código dado. Vamos começar pela inicialização da nossa USART, tal como está no datasheet: void USART_init(void){ UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8); UBRR0L = (uint8_t)(BAUD_PRESCALLER); UCSR0B = (1<<RXEN0)|(1<<TXEN0); UCSR0C = (3<<UCSZ00); } UBRR0H e UBRR0L são os registos onde colocamos um valor que depende do baud-rate e da frequência do oscilador que o nosso chip tem, temos duas opções para determinar este valor, ou vemos a tabela que está no datasheet ou usamos uma pequena fórmula que juntamos ao cabeçalho do nosso programa onde estão os outros includes e o compilador determina o valor BAUD_PRESCALLER baseado no baudrate que queremos e no valor de F_CPU, sendo esta fórmula a seguinte: #define BAUD 9600 //o baudrate que queremos usar #define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) – 1) // a formula que faz as contas para determinar o valor a colocar nos dois registos Última revisão: 21/12/2010 64 Isto não é magia nem nada que se pareça, no datasheet são dadas duas fórmulas, se juntarmos as duas é esta a formula com que ficamos. UCSR0B é o registo que nos permite activar os canais de recepção e transmissão de dados da USART assim como activar interrupts, mas isso não nos interessa por agora. E em UCSR0C definimos que queremos 8 bits de dados, sem paridade e um stop bit, em vez de (3<<UCSZ00) podemos fazer ((1<<UCSZ00)|(1<<UCSZ01)) o resultado é precisamente o mesmo. Agora para inicializar-mos a nossa USART basta chamar a função USART_init no nosso main e temos a USART pronta a usar, mas ainda não somos capaz nem de enviar nem de receber dados. Enviando e Recebendo Dados através da USART Mais uma vez a datasheet tem uma solução funcional, que é a seguinte: void USART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } A primeira de linha de código pode parecer estranha, mas tudo tem a sua razão e a razão desta linha é que o atmega tem um buffer de 3 bytes em hardware e esta linha verifica se ainda existe espaço no buffer para colocar mais dados, se não espera que haja espaço, se sim, coloca os dados no buffer coisa que é tratada na segunda linha da função. E com apenas duas linhas estamos prontos a enviar dados por serial! Agora falta-nos apenas a função para receber dados, sendo esta mais uma função de duas linhas: unsigned char USART_receive(void){ while(!(UCSR0A & (1<<RXC0))); return UDR0; } Na primeira linha, usamos o while para esperar que existam dados recebidos no registo de recepção, quando esses dados chegam ao registo, simplesmente devolvemos os dados e temos a nossa USART a ler dados por serial. Agora como extra, vou mostrar uma pequena função que permite enviar strings, pois muitas vezes queremos enviar mais que apenas um byte de informação de cada vez. A função para enviar strings tira partido do facto de em C uma string ser terminada por um carácter nulo(/null) e que é feita de muitos caracteres individuais. Assim com um pequeno loop que é executado enquanto os dados a enviar não são nulos enviamos um carácter de cada vez. Última revisão: 21/12/2010 65 void USART_putstring(char* StringPtr){ while(*StringPtr != 0x00){ // Aqui fazemos a verificação de que não chegamos ao fim da string, verificando para isso se o carácter é um null USART_send(*StringPtr); // Aqui usamos a nossa função de enviar um caracter para enviar um dos caracteres da string StringPtr++; } // Aumentamos o indice do array de dados que contem a string } O char* no inicio da função pode parecer estranho e chama-se um ponteiro, que é algo bastante útil em C, mas por agora vamos simplificar as coisas, e imaginar as strings como arrays de caracteres, que é isso mesmo que elas são em C e que o ponteiro não é mais que o inicio desse mesmo array. Nota (Cynary): Esta função não envia o carácter de terminação de string (NULL), logo, caso o leitor queira escrever um programa no lado do cliente que processe strings enviadas por esta função, deve ter isto em conta, e, ou alterá-la, de forma a enviar o NULL, ou enviar pela linha serial um número que indique o número de caracteres na string, antes de enviar a própria string. Agora, pegamos no nosso programa inicial em branco e juntamos tudo, ficando assim: #define F_CPU16000000UL #include <avr/io.h> #include <util/delay.h> #define BAUDRATE 9600 #define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1) int main(void){ return 0; } void USART_init(void){ UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8); UBRR0L = (uint8_t)(BAUD_PRESCALLER); UCSR0B = (1<<RXEN0)|(1<<TXEN0); UCSR0C = (3<<UCSZ00); } unsigned char USART_receive(void){ while(!(UCSR0A & (1<<RXC0))); Última revisão: 21/12/2010 66 return UDR0; } void USART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } void USART_putstring(char* StringPtr){ while(*StringPtr != 0x00){ USART_send(*StringPtr); StringPtr++;} } Se tentar-mos usar as nossas funções o compilador vai dizer que elas não estão definidas e nós ficamos a olhar para ele com cara espantada, porque as nossas funções estão mesmo ali, por baixo do main, e é precisamente esse o problema, no arduino podemos declarar funções onde bem nos apetecer, e em C tambem, mas temos que declarar as funções, ou seja a primeira linha da função que tem o nome dela e que tipo de dados é o seu retorno e quais os seus argumentos têm de estar antes do main, para o compilador saber que as funções existem, ficando assim o nosso código com as declarações das funções antes do main: #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> #define BAUDRATE 9600 #define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1) //declaração das nossas funções void USART_init(void); unsigned char USART_receive(void); void USART_send( unsigned char data); void USART_putstring(char* StringPtr); int main(void){ return 0; } void USART_init(void){ UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8); Última revisão: 21/12/2010 67 UBRR0L = (uint8_t)(BAUD_PRESCALLER); UCSR0B = (1<<RXEN0)|(1<<TXEN0); UCSR0C = (3<<UCSZ00); } unsigned char USART_receive(void){ while(!(UCSR0A & (1<<RXC0))); return UDR0; } void USART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } void USART_putstring(char* StringPtr){ while(*StringPtr != 0x00){ USART_send(*StringPtr); StringPtr++;} } Nota (Cynary): Havia outra forma de ultrapassar o erro do compilador acerca das funções não estarem definidas, que era declará-las noutra ordem (as funções de que o main depende, devem estar acima deste, e assim progressivamente). No entanto, para evitar quaisquer confusões, e até facilitar a programação (visto que as declarações das funções estão todas no topo, e assim não temos de as procurar no código), a melhor forma de o fazer é esta. Exemplo de utilização do USART Agora que sabemos usar a comunicação serial do AVR, vamos fazer alguma coisa com ela! Vamos usar as nossas funções para escrever a frase "Olá mundo!!!", mas falta-nos umas coisa, não temos um terminal serial, ou seja um programa que receba os dados da porta serial e nos mostre no ecrã do computador o que recebeu. No meu caso, recomendo usar este terminal: http://www.smileymicros.com/download/term20040714.zip?&MMN_position=42:42 Nota (Cynary): O IDE do arduino também inclui um terminal serial que funciona perfeitamente bem. Por isso, caso o leitor não queira ter de fazer download e instalar um novo programa, ou não use windows (apesar de parecer que este terminal funciona bem com o wine), pode sempre utilizar o terminal do IDE do arduino. Última revisão: 21/12/2010 68 Foi feito por um menbro do AvrFreaks e acho-o simples de usar, também vai do gosto e existem milhares de terminais pela internet fora, escolham o que mais gostarem. Neste caso basta fazer o download e executar o ficheiro, podem já fazer isso e deixar o terminal aberto que vamos usa-lo mais tarde. Vamos lá pegar então no nosso programa e completá-lo para o nosso "Olá mundo": #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> #define BAUDRATE 9600 #define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1) //declaração das nossas funções void USART_init(void); unsigned char USART_receive(void); void USART_send( unsigned char data); void USART_putstring(char* StringPtr); char String[]="Olá mundo!!!"; //String[] que dizer que é um array, mas ao colocar-mos o texto entre "" indicamos ao compilador que é uma string e ele coloca automáticamente o terminador null e temos assim uma string de texto usavel int main(void){ USART_init(); //Inicializar a usart while(1){ //Loop infinito USART_putstring(String); //Passamos a nossa string á função que a escreve via serial _delay_ms(5000); //E a cada 5s re-enviamos o texto } return 0; } void USART_init(void){ UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8); UBRR0L = (uint8_t)(BAUD_PRESCALLER); UCSR0B = (1<<RXEN0)|(1<<TXEN0); UCSR0C = (3<<UCSZ00); } unsigned char USART_receive(void){ while(!(UCSR0A & (1<<RXC0))); return UDR0; } Última revisão: 21/12/2010 69 void USART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } void USART_putstring(char* StringPtr){ while(*StringPtr != 0x00){ USART_send(*StringPtr); StringPtr++;} } E tão simples como isto temos o nosso atmega a enviar dados. Agora, para testar, enviamos o nosso programa para o AVR e vamos agora abrir o nosso Terminal, e tal como com o avrdude têm de ter atenção á porta com que o vosso arduino usa, assim como ao baudrate escolhido, no caso deste exemplo é 9600 e a forma como a USART está configurada, no nosso caso, 8 bits de dados, 1 bit de stop e sem bit de paridade, nesta imagem mostro como configurar o terminal para receber dados do arduino: Agora basta carregar em "Connect" e carregar no botão de reset do arduino para sincronizar o programa com arduino e deverão ver algo do género: Última revisão: 21/12/2010 70 E está a comunicação serial a funcionar! Agora deixo um desafio em aberto, usando o outro tutorial e este, desafio-vos a criarem um programa que acende o led do arduino quando recebe o caracter "a" e que o apague quando receber outro carácter qualquer, é algo bastante simples de se fazer, não precisam de nenhum hardware extra para além do arduino e assim aprendem como controlar algo usando dados via serial, para enviar um carácter usando este terminal basta escrever o carácter na caixa marcada a verde da imagem de cima e carregar no ENTER. Boa programação!!! Última revisão: 21/12/2010 71 Comunicação por I²C O Protocolo I²C Antes de começarmos a aprender como comunicar por I²C no AVR, temos de compreender exactamente como este protocolo funciona. O protocolo I²C (Inter-Integrated Circuit) é muito útil, pois permite que, em teoria, até 127 aparelhos comuniquem entre si, usando apenas dois fios (na prática, este limite pode não ser muito real, pois depende das características eléctricas do sistema, e também porque alguns endereços podem estar reservados limitando o número de dispositivos ainda mais). Isto é possível, pois os aparelhos comunicam através de endereços, num sistema mestre/escravo. Começando pelas características físicas do protocolo, usam-se duas linhas: a SCL (clock) e a SDA (dados) – onde ligamos estas linhas num aparelho depende da sua construção. No caso do AVR, ligamos, respectivamente nos pinos PC5 (analog in 5) e PC4 (analog in 4). Devido a haver apenas uma linha de dados, a comunicação usando o protocolo I²C é designada como sendo half- duplex, visto que só podemos estar a enviar ou receber dados num certo ponto no tempo, e não os dois ao mesmo tempo. A linha SCL é utilizada para sincronizar os diferentes aparelhos. Funciona da seguinte forma: quando está em low, o sinal da linha SDA pode ser alterado, e quando está em high, o sinal da linha SDA não pode ser alterado, logo está pronto a ser lido (logo, uma transiçãode low para high sinaliza um novo bit na linha SDA). Normalmente apenas um aparelho controla esta linha, mas os outros aparelhos, caso não sejam rápidos o suficiente para utilizarem a frequência desse aparelho podem controlar a linha, deixando-a em low pelo tempo que quiserem. A linha SDA contém a informação, que é lida de acordo com o estado da linha SCL, como explicado no parágrafo anterior. No estado high, esta linha tem um bit 1, e no estado low, esta linha contém um bit 0. Ambas estas linhas têm uma regra especial: os aparelhos que as usam não as podem pôr no estado high, apenas em low. Por isso, para se ter um estado high, colocam-se duas resistências pull- up entre o terminal positivo e a linha (uma resistência para cada linha é suficiente). Assim, quando os aparelhos “largam” a linha, esta está em high, e são responsáveis por a pôr em low. Isto é muito útil para, por exemplo, aparelhos que funcionem a 5V poderem comunicar com aparelhos que funcionam a 3.3V – 3.3V é normalmente aceite como um estado high válido, e como os outros aparelhos não suportam uma corrente de 5V, isto impede que tenham problemas, sem afectar a comunicação. A regra descrita acima sobre a alteração do estado da linha SDA de acordo com a SCL tem duas excepções: bits de início e de fim de comunicação. Nem sempre existe um aparelho a usar as linhas Última revisão: 21/12/2010 72 do I²C, por isso é necessário conseguir-se distinguir quando é que esta está a ser usada ou não. Quando não está a ser usada, ambas as linhas estão no estado high. Para sinalizar o início de comunicação, a linha SDA vai para um estado low, enquanto a linha SCL está em high. Para sinalizar o fim da comunicação, a linha SDA vai para um estado high enquanto SCL está em high. Agora que compreendemos como funciona o protocolo a um nível de hardware, vamos aprender como funciona a um nível de software. Já começámos a falar disso ao mencionarmos a existência de bits de início e de fim (neste documento, iremos referir-nos a eles como start bits e stop bits, respectivamente, pois é essa a convenção). O protocolo I²C é muito simples de compreender e utilizar. Existem duas categorias de aparelhos – os master e os slave. Os master é que têm controlo sobre a linha. São estes que “chamam” os outros aparelhos, através do seu endereço único, e enviam ou recebem dados dos mesmos. Em qualquer altura, só podem haver dois dispositivos a usar os fios para comunicação, e um desses tem de ser um master. Existe um endereço especial, chamado de general call, que permite ao master falar com todos os slaves. É útil para, por exemplo, configurar o endereço de um aparelho com um endereço desconhecido, ou escrever um valor para vários aparelhos diferentes ao mesmo tempo. Este endereço é o 0. No entanto, nem todos os aparelhos responderão a este endereço, visto que depende da sua configuração. Podem haver mais do que um master. Para evitar problemas, quando dois master tentam controlar os fios, ocorre um processo denominado arbitration, em que os masters “lutam” pelo controlo. Após a arbitration, o aparelho que ganhar é o master, e todos os outros funcionam como slaves. Neste documento iremos apenas cobrir o uso de um master. Para mais informações sobre utilização de vários masters, pode-se consultar a datasheet do AVR. O I²C define também o formato da mensagem na linha: a mensagem começa com um start bit, seguido do endereço do slave e um bit read/write que indica se o master quer ler (bit=1) ou escrever (bit=0) dados para o slave. Isto é seguido de um sinal ACK/NACK do slave, a dizer que recebeu a mensagem, e se pode ou não continuar a comunicação (um ACK – acknowledge – tem o valor 0, e significa que podemos continuar a comunicação; um NACK é o contrário, tendo o valor 1, e significando que a comunicação deve parar, e que o master deve enviar um stop bit). Para cada byte enviado, o aparelho receptor deve enviar um ACK/NACK (visto que um endereço tem 7 bits, ao adicionarmos o bit de leitura/escrita, enviámos 8 bits – um byte). De acordo com o valor do bit read/write, o master transforma-se num receptor ou num transmissor de dados. Após esta primeira parte da mensagem, os aparelhos continuam a comunicar entre si através de bytes e ACK/NACK. O master pode ainda realizar um repeated start, que basicamente consiste em enviar um novo bit de start, e um novo endereço, sem ter de perder controlo da linha, devido a não Última revisão: 21/12/2010 73 enviar o bit de stop. Isto é útil se o master quer endereçar outro aparelho, ou mudar o valor do bit read/write (por exemplo, começa por escrever para um aparelho, enviando um comando, e depois lê os resultados desse comando). Para finalizar a comunicação, o master envia um bit de stop. Alguns aparelhos não mencionam que trabalham em I²C, mas sim que usam SMBus, ou TWI (Two-Wire Interface – o caso do AVR). No entanto, estas designações, apesar de terem algumas diferenças, são semelhantes em funcionalidade ao I²C, por isso podem, na prática, ser interpretadas como sendo este protocolo. Agora que já compreendemos como funciona o I²C, vamos aprender como usá-lo no AVR, e iremos usar este protocolo para dois AVRs comunicarem entre si. I²C no AVR No AVR, em vez de I²C, temos uma interface TWI (two wire serial interface). No entanto, podemos usar como se fosse I²C. Quando utilizamos o AVR como master, devemos começar por gerar o clock na linha SCL. Isto é feito alterando o register TWBR. Segundo a datasheet, a fórmula para o clock é a seguinte: SCL Frequency= CPU Frequency 162 TWBR∗Prescaler Value Ao isolarmos o TWBR, ficamos com isto: TWBR= CPU Frequency SCL Frequency −16 2 Prescaler Value Neste documento iremos utilizar a frequência 100kHz, visto que é uma frequência standard do I²C, e a maior parte dos aparelhos suporta-a. O valor do prescaler aqui é definido no register TWSR (que iremos descrever mais à frente). Iremos usar o prescaler 1, visto que, com uma frequência de 100kHz para o I²C, e uma frequência do CPU de 16 MHz, o valor do TWBR não ultrapassa a capacidade de 8 bits (neste caso, esse valor será 72), e é o valor por defeito (assim não necessitamos de alterar o register TWSR). Atenção que a datasheet recomenda um valor mínimo de 10 para o TWBR, logo para frequências da SCL maiores, será necessário um prescaler maior. Agora que já sabemos como gerar o clock, temos de compreender como realizar acções no I²C. Para isto utilizamos o register TWCR. Este register em particular tem muitas particularidades na forma como funciona. Primeiro, para o I²C funcionar, temos de colocar o valor 1 no bit TWEN. Em segundo lugar, o bit TWINT determina quando ocorrem acções ou não no I²C: quando este tem o valor 0, as acções definidas pelos restantes bits ocorrem, e quando tem o valor 1, não podem ocorrer acções. No entanto, a forma como este assume os valores 0 e 1 é diferente dos outros bits: para colocar o valor 0 neste bit, temos de escrever um 1 para lá, enquanto ele apenas assume o valor 1 Última revisão: 21/12/2010 74 após ocorrer algum evento relacionado com o I²C. Os bits TWEA, TWSTA e TWSTO indicam que acção o microcontrolador deve tomar nas linhas I²C quando o bit TWINT é 0. O bit TWWC constitui uma flag que indica se estamos a realizar uma certa acção proibida – mexer no register TWDR quando o bit TWINT é 0 (iremos ignorar esta flag). O bit TWIE liga a interrupção ligada ao I²C – esta interrupção ocorre sempre que o bit TWINT é 1. Atenção que esta interrupção não coloca esse bit a 0, logo para evitar que esta se repita eternamente, ou colocamo-lo a 0, ou desligamos ainterrupção temporariamente – não iremos usar esta interrupção neste documento, no entanto destacamos a sua utilidade enquanto ferramenta para realizar acções relacionadas com o protocolo I²C, sem a necessidade de ocupar o microprocessador enquanto se espera que cada operação fique completa; para saber qual a operação a realizar, usam-se os códigos de estados, discutidos brevemente. Até agora, temos usado sempre a operações binária OR para colocar valores em bits específicos nos registers. No entanto, não é prático fazer isso com o register TWCR, visto que este tem bits que determinam a acção a seguir, e que, se não os apagarmos, farão com que ocorram acções repetidas. Assim, de cada vez que mexermos no register TWCR, faremos um reset a todos os seus valores. Isto obriga a ter sempre em atenção o facto de termos de colocar sempre o bit TWEN com o valor 1. Com o que já sabemos até agora, já somos capazes de enviar um sinal start para as linhas I²C! O bit que determina esta acção é o TWSTA. Vamos então definir uma função que faz isto mesmo, bem como uma que inicia o clock: #define FREQ 100000 #define PRES 1 #define TWBR_value ((F_CPU/FREQ)-16)/(2*PRES) void set_clk() { TWBR = TWBR_value; } void send_start() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA); } Note que, além do bit TWSTA, também mexemos nos bits TWINT e TWEN. Isto é para garantir que o I²C está ligado (1 no bit TWEN), e que podemos efectuar acções neste (0 no bit TWINT – como foi dito antes, este bit fica com o valor 0 quando escrevemos o valor 1 nele). O código para o envio de um start, no entanto, está incompleto, pois ficamos com o problema de não saber quando o start acabou de ser enviado, e se tivemos sucesso ou não a enviá-lo (demora um pouco a enviar um start, e por vezes pode falhar, por exemplo, devido a um processo de arbitration que o nosso AVR perdeu)! Última revisão: 21/12/2010 75 Como foi dito antes, o bit TWINT fica com o valor 1 assim que uma acção é completada nas linhas I²C. Assim, para saber quando se acabou de enviar o start, temos de esperar que este bit fique com o valor 1. No entanto, isto não nos indica se o start foi enviado com sucesso ou não. Para sabermos isto, observamos o register TWSR. Já falámos deste register anteriormente quando mencionámos o prescaler. Além do prescaler, este register contém informação acerca do estado do I²C, tal como se o start foi enviado com sucesso – visto que as operações para enviar um start e um repeated start são iguais em termos do register TWCR, mas originam diferentes códigos de estado no register TWSR, temos de verificar ambos esses códigos. Estes códigos de estado podem ser encontrados na datasheet do AVR em quatro tabelas (páginas 229, 232, 235 e 238), respectivamente, para os modos de master transmitter, master receiver, slave receiver e slave transmitter. Apenas o master pode enviar um sinal de start/repeated start, e os códigos para esses sinais são iguais tanto no modo transmitter como receiver, e são, respectivamente, 0x08 e 0x10. É de notar que estes códigos de estado assumem que os bits do prescaler são ambos 0. No entanto, nem sempre o são. Estes bits são os dois menos significativos. Para os retirar, realizamos a operação AND com o valor 0b11111100, ou 0xF8. Para sabermos se o o envio do start bit foi realizado com sucesso, esta função devolverá um valor verdadeiro ou falso, dependendo respectivamente se teve sucesso ou não. Assim, já podemos completar o código para a função que envia um sinal de start: unsigned char send_start() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA); // Enviar o sinal de start while(!(TWCR&(1<<TWINT))); // Esperar que a operação termine return ((TWSR&0xF8) == 0x08 || (TWSR&0xF8) == 0x10); } Depois do start bit, temos de enviar o endereço do aparelho com quem queremos comunicar e o bit read/write. Isto constitui um byte (visto o endereço ser 7 bits + 1 bit read/write). O AVR não distingue entre enviar o endereço/bit read/write, e qualquer outro byte no I²C, quando está em modo master. Para fazer isto, apenas colocamos o byte a enviar no register TWDR e ligamos as acções I²C. Atenção que, como mencionado anteriormente, não podemos colocar qualquer informação no register TWDR, quando o bit TWINT é 0! Vamos então criar uma função que envia um byte para as linhas I²C. Novamente, verificaremos o estado do I²C para saber se tivemos sucesso ou não. Visto que os códigos de estado para o envio e recepção (ACK devolvido) com sucesso de um endereço de slave e o bit read/write são diferentes dos códigos de sucesso de um byte enviado (com ack ou nack devolvido), iremos criar uma função separada para “chamar” um slave. Os códigos devolvidos são ainda diferentes quando enviamos um bit read ou um bit write, portanto, temos de testar para ambos os casos (respectivamente, 0x40 e 0x18): Última revisão: 21/12/2010 76 unsigned char send_slave(unsigned char addr) { TWDR = addr; TWCR = (1<<TWINT) | (1<<TWEN); while(!(TWCR&(1<<TWINT))); return ((TWSR&0xF8) == 0x18 || (TWSR&0xF8) == 0x40); } Quando queremos endereçar um slave de endereço addr, com bit read/write B, apenas enviamos o byte ((addr<<1)|B). É de notar que algumas datasheets listam dois endereços para um aparelho. Isto significa que, em vez de darem os 7 bits do endereço, dão o byte completo, com o bit de read/write. Com o que já sabemos, podemos já definir uma função para enviar dados, visto que basta alterar os códigos de estado que verificamos. Como foi explicado antes, quando uma transmissão ocorre com sucesso, o receptor devolve um ACK. O receptor também pode devolver um NACK quando a recepção ocorre com sucesso, mas este sinal significa que devemos parar a transmissão e enviar um stop bit imediatamente. Assim, temos de verificar se a transmissão ocorreu com sucesso e se um ACK foi devolvido. Como podemos enviar dados tanto em modo slave como em modo master, temos ainda de verificar os códigos de estado para estes dois modos. Assim, temos de verificar os códigos de estado 0xB8 e 0x28. Outra particularidade do modo slave é que em muitas operações, para garantir a sua funcionalidade, devemos colocar o bit TWEA a 1, inclusive na operação de envio de dados. Explicaremos o porquê mais à frente, mas iremos incluir este pormenor nesta função: unsigned char send_data(unsigned char data) { TWDR = data; TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); while(!(TWCR&(1<<TWINT))); return ((TWSR&0xF8) == 0xB8 || (TWSR&0xF8) == 0x10); } Até agora só nos concentrámos em enviar dados. No entanto, tanto aparelhos master como slave têm de ser capazes de receber dados. Atenção que apenas podemos recebe dados em master quando enviamos o bit de read, e em slave quando recebemos o bit de write! O processo é semelhante ao de enviar dados. Tem apenas os pormenores de lermos os dados do register TWDR, em vez de os escrevermos lá, e de termos de enviar um ACK/NACK, para sinalizar que recebemos os dados com sucesso e se queremos continuar ou não com a comunicação, e dos estados diferirem de acordo com o que devolvemos (ACK/NACK), e de acordo com o endereço utilizado para o slave (general call ou o endereço específico). Assim, temos 6 códigos de estado que temos de verificar, 3 para o caso Última revisão: 21/12/2010 77 de devolvermos um ACK (0x50, 0x80, 0x90), e 3 para o caso de devolvermos um NACK (0x58, 0x88, 0x98). O bit que define se devolvemos um ACK ou um NACK é o TWEA (1 para ACK, 0 para NACK). A função que irá receber dados recebe um parâmetro lógico: verdadeiro no caso de querermos devolver um ACK, e falso no caso de querermos devolver um NACK. O valor devolvido será 0 no casode haver um erro na recepção, ou o valor recebido caso contrário (isto não é óptimo devido o valor recebido poder ser 0! Apenas fazemos assim para simplificar. No entanto, o ideal seria usar uma flag externa à função para sinalizar um erro ou devolver uma estrutura com duas variáveis: uma flag para sinalizar erros e o valor recebido): unsigned char receive_data(unsigned char ack) { TWCR = (1<<TWINT) | (1<<TWEN) | (ack?(1<<TWEA):0); while(!(TWCR&(1<<TWINT))); if((ack && (TWSR&0xF8) != 0x50 && (TWSR&0xF8) != 0x80 && (TWSR&0xF8) != 0x90) || ((!ack) && (TWSR&0xF8) != 0x58 && (TWSR&0xF8) != 0x88 && (TWSR&0xF8) != 0x98)) { return 0; } return TWDR; } Até agora já enviámos start bits, “chamamos” slaves, enviamos dados, e recebemos dados, tanto em modo slave como master. Para completarmos a funcionalidade do modo master, apenas nos falta enviar stop bits. Isto é feito colocando a 1 o bit TWSTO. Esta acção em particular não tem nenhum código de estado, nem coloca o bit TWINT a 1, logo, podemos criar uma função muito simples para a efectuar: void send_stop() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO); } Agora já temos toda a funcionalidade disponível ao master! Apenas nos faltam alguns pormenores acerca do slave. Primeiro, vamos compreender a relevância do bit TWEA. Quando este bit tem o valor 1, e o aparelho ainda não foi endereçado, este irá responder quando o seu endereço ou uma general call (caso esteja configurado para responder a uma general call) aparecerem nas linhas I²C, com um ACK. Se este bit for 0, então ignorará estes bytes, desligando-se assim do I²C. Anteriormente, mencionei que quando um slave envia dados, deve colocar o bit TWEA a 1. Isto não é estritamente necessário, mas é aconselhável. Quando se coloca o bit TWEA a 0 após enviar dados, o aparelho está à espera de receber um NACK, e não irá enviar mais dados. No entanto, para Última revisão: 21/12/2010 78 simplificar, colocamos sempre o bit TWEA a 1, visto que não faz nenhuma diferença nos dados transmitidos, e podemos simplesmente terminar aí a conexão como se estivéssemos à espera do NACK. Além disto, se não colocarmos o bit TWEA a 1, não podemos enviar mais dados. Após realizarmos operações no modo slave, devemos esperar por um stop/start/repeated start do master – de acordo com a documentação da datasheet, no modo slave transmitter, os stop bits são ignorados. Assim, para fazer isto, ligamos o I²C, à espera de que algum evento ocorra. Caso seja um stop bit (código de estado 0xA0), ligamos novamente o I²C, para o aparelho esperar que o seu endereço seja enviado para a linha. Caso não seja, um stop bit, significa que um start bit foi enviado, seguido do endereço do aparelho em questão – sai-se da função para o código principal tratar de processar esse endereçamento (veremos de seguida como fazer isso). Assim, podemos definir a função para esperar por um stop/start/repeated start bit da seguinte forma: void wait_stop() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); while(!(TWCR&(1<<TWINT))); if((TWSR&0xF8) == 0xA0) { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); } } Para podermos ter o nosso aparelho a funcionar como slave, apenas nos falta configurar o seu endereço, saber como configurá-lo para responder à general call e saber quando o aparelho é endereçado, e saber se foi enviado um bit de read ou write. Vamos começar por atribuir um endereço ao aparelho e configurar se ele responde à general call ou não. Para isto, apenas temos de de escrever o endereço pretendido nos 7 bits superiores do register TWAR, e o bit inferior, caso seja 0, o aparelho ignora a general call, e caso seja 1, o aparelho responde à general call. Vamos então configurar o nosso aparelho para responder tanto à general call como ao endereço 0x10, através de uma função (esta função, além de configurar o endereço, também fará com que o aparelho “escute” as linhas I²C, à espera de ser endereçado): #define GC 1 #define ADDR 0x10 void set_slave() { TWAR = (ADDR<<1)|GC; TWCR = (1<<TWEN) | (1<<TWEA); } Agora, o que nos falta para ter um slave funcional, é saber como verificar quando este é Última revisão: 21/12/2010 79 endereçado. Para isto, iremos colocar dois aparelhos a comunicar um com o outro. Basicamente, um AVR terá o papel de master transmitter, e o outro de slave receiver. O slave irá mudar o estado do LED incoroporado o arduino (pino digital 13/PB5) assim que receber dados do master receiver. Neste documento, não iremos usar os modos master receiver e slave transmitter, mas são idêntidos, apenas trocando-se as funções de transmissão/recepção, e no caso do slave, os códigos de estado (para o slave transmitter, são 0xA8 e 0xB0). Iremos começar com o aparelho master. Este começa por enviar um start bit, depois “chama” o slave (neste caso com o endereço 10, e o bit de write), e envia um byte para o slave (neste caso iremos enviar o byte 'a'), e envia um sinal de stop. Para o efeito no LED ser visível, iremos enviar dados em intervalos de 1 segundo, recorrendo à função _delay_ms na biblioteca util/delay.h: #include <avr/io.h> #include <util/delay.h> #define FREQ 100000 #define PRES 1 #define TWBR_value ((F_CPU/FREQ)-16)/(2*PRES) void set_clk() { TWBR = TWBR_value; } unsigned char send_start() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA); // Enviar o sinal de start while(!(TWCR&(1<<TWINT))); // Esperar que a operação termine return ((TWSR&0xF8) == 0x08 || (TWSR&0xF8) == 0x10); } unsigned char send_slave(unsigned char addr) { TWDR = addr; TWCR = (1<<TWINT) | (1<<TWEN); while(!(TWCR&(1<<TWINT))); return ((TWSR&0xF8) == 0x18 || (TWSR&0xF8) == 0x40); } unsigned char send_data(unsigned char data) { TWDR = data; TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); while(!(TWCR&(1<<TWINT))); return ((TWSR&0xF8) == 0xB8 || (TWSR&0xF8) == 0x10); } void send_stop() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO); } #define W 0 Última revisão: 21/12/2010 80 #define R 1 int main(void) { set_clk(); for(;;) { send_start(); send_slave((0x10<<1)|W); send_data('a'); send_stop(); _delay_ms(1000); } } Agora iremos programar o aparelho slave. Este espera até que seja endereçado com um bit de read (código de estado 0x60, 0x68, 0x70 ou 0x78). Assim que o é, este recebe os dados, faz toggle do LED, e espera pelo stop bit: #include <avr/io.h> #include <util/delay.h> unsigned char receive_data(unsigned char ack) { TWCR = (1<<TWINT) | (1<<TWEN) | (ack?(1<<TWEA):0); while(!(TWCR&(1<<TWINT))); if((ack && (TWSR&0xF8) != 0x50 && (TWSR&0xF8) != 0x80 && (TWSR&0xF8) != 0x90) || ((!ack) && (TWSR&0xF8) != 0x58 && (TWSR&0xF8) != 0x88 && (TWSR&0xF8) != 0x98)) { return 0; } return TWDR; } void wait_stop() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); while(!(TWCR&(1<<TWINT))); if((TWSR&0xF8) == 0xA0) { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); } } #define GC 1 #define ADDR 0x10 void set_slave() { TWAR = (ADDR<<1)|GC; TWCR = (1<<TWEN) | (1<<TWEA); } int main(void) { DDRB |= (1<<PB5); for(;;) { Última revisão: 21/12/2010 81 while((TWSR&0xF8) != 0x60 && (TWSR&0xF8) != 0x68 && (TWSR&0xF8) != 0x70 && (TWSR&0xF8) != 0x78); receive_data(0); PORTB ^= (1<<PB5); wait_stop(); } } E assim completamos o tutorial acerca do protocolo I²C e sobre como usá-lo com o AVR. Este é um dos componentes mais complexos de utilizar no AVR, por isso aconselhamos que experimente várias vezes com o código, e que consulte a datasheet e este documento sempre que tiver dúvidas. Última revisão: 21/12/2010 82 Bibliografia http://www.avrfreaks.net/ http://www.atmel.com/dyn/resources/prod_documents/doc8025.pdfhttp://embeddeddreams.com/users/njay/Micro Tutorial AVR – Njay.pdf http://www.smileymicros.com/index.php? module=pagemaster&PAGE_user_op=view_page&PAGE_id=70&MMN_position=117:117 http://lusorobotica.com/index.php?topic=2838.15 Última revisão: 21/12/2010 83 Programação em C no AVR Introdução Programação em C em micro-controladores. Controlo da funcionalidade do micro-controlador – os registers Pseudo-código/código esqueleto MACROS Variáveis volatile Operações bit-wise em C GPIO – General Purpose Input/Output Entrada Digital Normal Entrada com “pull-up” (“puxa para cima”) Entrada controlada por um periférico Saída Digital Normal Saída em Colector Aberto (open colector) Saída controlada por um periférico GPIOs na Arquitectura AVR Configuração dos Portos em Linguagem C Interrupções O que é uma interrupção? Como funciona uma interrupção no AVR? Como lidar com uma interrupção no AVR? Exemplo de interrupção através do pino digital 2 (INT0) Cuidados a ter na utilização de interrupções Timers O que são e como funcionam timers? Timers no AVR Modos Normal e CTC Como usar um timer no AVR Eventos relacionados com timers. Interrupções e timers. Timers – Parte 2, Pulse Width Modulation O que é PWM? Vários Modos de PWM Fast PWM Phase and Frequency Correct PWM Analog-to-Digital Converter Formato Analógico e Digital O que é o ADC? Como funciona o ADC no AVR? Como ligar o input ao AVR? Utilizar o ADC – construir um sensor de distância ADC8 – medir a temperatura interna Comunicação Serial no AVR Como funciona a comunicação Serial? O que é a USART? Inicializando a USART do AVR Enviando e Recebendo Dados através da USART Exemplo de utilização do USART Comunicação por I²C O Protocolo I²C I²C no AVR Bibliografia