A compreensão do funcionamento de interrupções é essencial para uma programação eficiente de microcontroladores. Entretanto existem muitas dúvidas em torno desse tópico. Nesse post veremos uma pequena explicação de como interrupções funcionam e em seguida focaremos na interrupção por mudança de estado (PCINT). Na abordagem dos conceitos a seguir o microcontrolador atmega328 presente na placa Arduino Uno será utilizado. Apesar de utilizarmos o atmega328, os conceitos abordados aqui podem ser estendidos para outros atmegas. Imagine que está em casa extremamente focado em escrever um texto no computador, porém de repente seu celular toca. Mesmo sendo uma ligação inesperada, é capaz de parar a escrita do texto, atender a ligação e voltar para onde parou quando o telefone tocou.
Desta maneira que funcionam as a interrupções em um microcontrolador. Nós podemos programá-lo para observar eventos externos, como um pino mudando de estado, enquanto o microcontrolador executa as instruções do código principal. Quando um evento ocorrer, o microcontrolador interromperá a execução do código principal, tratará o evento chamando uma função especificada por nós e retornará a execução do código principal. Dessa maneira nosso programa ganha flexibilidade, uma vez que não é necessário mais ficar a observar a todo momento se o evento ocorreu. Nós podemos simplesmente configurar a interrupção e o de tratamento e assim que o evento ocorrer, ele será tratado.
É importante lembrar que nós não estamos fazendo duas coisas ao mesmo tempo. O programa principal será parado enquanto a função de tratamento estiver sendo executada. Esta função de tratamento é denominada ISR (Interrupt Service Routine). Cada interrupção terá seu vetor de interrupção que nada mais é que um índice em uma tabela de interrupções que apontará para nossa rotina de tratamento.
PCINT – Pin Change Interrupt
Como o nome indica este tipo de interrupção ocorrerá quando houver uma mudança no estado do pino escolhido. No atmega328 existem 3 vetores de interrupção para esse tipo de interrupção. Cada um deles está ligado a um PORT. Consequentemente, caso dois pinos de um mesmo PORT estejam utilizando essa interrupção ambos compartilharão a mesma rotina de interrupção. Cabe a nós então, implementar a rotina de interrupção de forma que esta seja capaz de identificar em qual dos dois pinos ocorreu a interrupção. O diagrama a seguir nos ajudará no entendimento das explicações a seguir.
PCICR – Pin Change Interrupt Control Register
Este registador é responsável por habilitar a interrupção em um determinado PORT quando o respectivo bit PCIEx for setado para 1.
PCMSK – Pin Change Mask Register
Este registador é responsável por habilitar a interrupção de um pino em um determinado PORT. Logo, existem 3 registadores desse tipo PCMSK0, PCMSK1 e PCMSK2 referentes aos PORTS B, C e D respectivamente.
SREG – Global Interrupt Flag
Essa flag é responsável por controlar as interrupções de todo o microcontrolador. Funciona como uma chave geral. Uma maneira de modifica-la é através das macros sei() e cli().
- sei() – Habilita as interrupções globalmente;
- cli() – Bloqueia as interrupções globalmente.
Analogia com Chaves
Para facilitar a visualização do papel de cada registador nós preparamos o diagrama a seguir. Ele mostra o caminho que a interrupção deve fazer até chegar ao respectivo vetor de interrupção. As chaves representam cada bit do registador especificado. Chave fechada representa que o bit está setado (1) e chave aberta que ele está limpo (0).
Exemplo 1
Neste exemplo nós veremos como habilitar a interrupção por mudança de estado no pino D12. Como podemos ver no diagrama o pino D12 é equivalente ao pino PB4 do microcontrolador atmega328. Começamos o programa configurando este pino como uma entrada. Durante a execução das configurações devemos desligar as interrupções globalmente. O pino PB4 possui a interrupção PCINT4 essa interrupção é habilitada por PCMSK0 que por sua vez é habilitado por PCIE0. Logo, devemos setar todos esses registradores. Finalmente devemos configurar a função que será chamada quando a interrupção ocorrer. Isso é feito com o auxílio da macro ISR() que recebe como parâmetro o vetor de interrupção que desejamos configurar. No nosso caso o vetor é o PCINT0_vect.
void setup() { cli(); // Equivalente a pinMode(12, INPUT_PULLUP); DDRB &= ~(1 << DDB4); // Seta D12 como entrada; PORTB |= (1 << PORTB4); // Liga Pull-up; // Seta as "chaves" necessárias para que a interrupção chegue a seu vetor; PCICR |= (1 << PCIE0); PCMSK0 |= (1 << PCINT4); sei(); } void loop() { //... } /* Função de Tratamento de Interrupção Como somente o pino D12 foi configurado para chamar esta função, não precisamos realizar checagens complexas para entender o que ocorreu. */ ISR(PCINT0_vect) { if (PINB & (1 << PINB4)) { // D12 mudou de LOW para HIGH; } else { // D12 mudou de HIGH para LOW; } }
Exemplo 2
Neste exemplo nós veremos como habilitar a interrupção por mudança de estado nos pinos D12, D11 e D10. Como podemos ver no diagrama estes pinos são equivalentes aos pinos PB4, PB3 e PB2 respectivamente do microcontrolador atmega328. Repetimos todos os passos anteriores, só que agora para todos os pinos. Diferentemente do exemplo anterior agora um mesmo vetor será chamado por vários pinos. Para definir qual pino causou a interrupção devemos guardar um histórico do ultimo estado de todo o PORTB.
void setup() { cli(); /* Equivalente a pinMode(12, INPUT_PULLUP); pinMode(11, INPUT_PULLUP); pinMode(10, INPUT_PULLUP); */ DDRB &= ~( (1 << DDB4) | (1 << DDB3) | (1 << DDB2) ); PORTB |= ( (1 << PORTB4) | (1 << PORTB3) | (1 << PORTB2) ); // Seta as "chaves" necessárias para que as interrupções cheguem ao vetor; PCICR |= (1 << PCIE0); PCMSK0 |= ( (1 << PCINT4) | (1 << PCINT3) | (1 << PCINT2) ); sei(); } void loop() { //... } // Variáveis globais que são acessadas por interrupções devem ser declaradas volatile; volatile uint8_t last_PINB = PINB; /* Função de Tratamento de Interrupção */ ISR(PCINT0_vect) { uint8_t changed_bits; changed_bits = PINB ^ last_PINB; last_PINB = PINB; if (changed_bits & (1 << PINB4)) { if (PINB & (1 << PINB4)) { // D12 mudou de LOW para HIGH; } else { // D12 mudou de HIGH para LOW; } } else if (changed_bits & (1 << PINB3)) { if (PINB & (1 << PINB3)) { // D11 mudou de LOW para HIGH; } else { // D11 mudou de HIGH para LOW; } } else if (changed_bits & (1 << PINB2)) { if (PINB & (1 << PINB2)) { // D10 mudou de LOW para HIGH; } else { // D10 mudou de HIGH para LOW; } } }
Artigo gentilmente cedido por Vida de Silicio
Todos os produtos utilizados neste artigo podem ser encontrados na Loja de Eletrónica e Robótica – ElectroFun.
Gostaram deste artigo? Deixem o vosso comentário no formulário a baixo e partilhem com os vossos amigos.
Não se esqueçam de fazer like na nossa Página no Facebook.
Podem ainda colocar as vossas dúvidas no nosso Forum da Comunidade Arduino em Portugal ou no nosso Grupo no Facebook Arduino Portugal – Qual o teu projeto?