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.
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: // 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): // Ligar interrupções globais // 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() // 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
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: (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:
Vamos usar para este efeito o pino 2 (podia ser feito com o pino 3 também, com poucas diferenças). O objectivo deste código vai ser mudar o estado de um LED quando se toca num botão ligado ao pino 2. O LED vai estar ligado ao pino digital 4 (PD4). Vamos começar por criar uma variável global, com o estado do pino e inicializar esse pino como output (vai começar desligado) (para mais informações sobre GPIOs, ler o tutorial que pus no primeiro post), e já vamos colocar o código necessário para fazer toggle ao pino na ISR. char output = 0; // Estado do led. DDRD = (1<<PD4); // Inicializar o pino digital 4 como output. PORTD &= ~(1<<PD4); // Inicializar o pino digital 4 como desligado. 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.
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: char output = 0; // Estado do led. 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 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: char output = 0; // Estado do led.
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); 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
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 // //... 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.