Baixe o app para aproveitar ainda mais
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 pino
Compartilhar