Como as interrupções funcionam no Arduino Uno e em placas semelhantes?

11

Por favor, explique como as interrupções funcionam no Arduino Uno e nas placas relacionadas usando o processador ATmega328P. Placas como:

  • Uno
  • Mini
  • Nano
  • Pro Mini
  • Vitória Régia

Em particular, discuta:

  • Para que usar interrupções
  • Como escrever uma rotina de serviço de interrupção (ISR)
  • Problemas de tempo
  • Seções críticas
  • Acesso atômico aos dados

Nota: esta é uma pergunta de referência .

Nick Gammon
fonte

Respostas:

25

TL; DR:

Ao escrever uma rotina de serviço de interrupção (ISR):

  • Seja breve
  • Não use delay ()
  • Não faça impressões seriais
  • Tornar variáveis ​​compartilhadas com o código principal voláteis
  • Variáveis ​​compartilhadas com o código principal podem precisar ser protegidas por "seções críticas" (veja abaixo)
  • Não tente ativar ou desativar as interrupções

O que são interrupções?

A maioria dos processadores possui interrupções. As interrupções permitem que você responda a eventos "externos" enquanto faz outra coisa. Por exemplo, se você estiver preparando o jantar, coloque as batatas para cozinhar por 20 minutos. Em vez de olhar o relógio por 20 minutos, você pode definir um cronômetro e depois assistir à TV. Quando o timer toca, você "interrompe" a exibição da TV para fazer algo com as batatas.


Exemplo de interrupções

const byte LED = 13;
const byte SWITCH = 2;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // end of switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // so we can update the LED
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  // loop doing nothing
}

Este exemplo mostra como, mesmo que o loop principal não esteja fazendo nada, você pode ativar ou desativar o LED no pino 13 se o interruptor do pino D2 estiver pressionado.

Para testar isso, basta conectar um fio (ou comutador) entre D2 e ​​o terra. O pullup interno (ativado na configuração) força o pino HIGH normalmente. Quando aterrado, torna-se BAIXO. A mudança no pino é detectada por uma interrupção CHANGE, que faz com que a rotina de serviço de interrupção (ISR) seja chamada.

Em um exemplo mais complicado, o loop principal pode estar fazendo algo útil, como fazer leituras de temperatura, e permitir que o manipulador de interrupções detecte um botão pressionado.


Convertendo números de pinos para interromper números

Para simplificar a conversão de números de vetor de interrupção em números fixos, você pode chamar a função digitalPinToInterrupt(), passando um número PIN. Retorna o número de interrupção apropriado, ou NOT_AN_INTERRUPT(-1).

Por exemplo, no Uno, o pino D2 na placa é interrompido 0 (INT0_vect da tabela abaixo).

Portanto, essas duas linhas têm o mesmo efeito:

  attachInterrupt (0, switchPressed, CHANGE);    // that is, for pin D2
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);

No entanto, o segundo é mais fácil de ler e mais portátil para diferentes tipos de Arduino.


Interrupções disponíveis

Abaixo está uma lista de interrupções, em ordem de prioridade, para o Atmega328:

 1  Reset
 2  External Interrupt Request 0  (pin D2)          (INT0_vect)
 3  External Interrupt Request 1  (pin D3)          (INT1_vect)
 4  Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
 5  Pin Change Interrupt Request 1 (pins A0 to A5)  (PCINT1_vect)
 6  Pin Change Interrupt Request 2 (pins D0 to D7)  (PCINT2_vect)
 7  Watchdog Time-out Interrupt                     (WDT_vect)
 8  Timer/Counter2 Compare Match A                  (TIMER2_COMPA_vect)
 9  Timer/Counter2 Compare Match B                  (TIMER2_COMPB_vect)
10  Timer/Counter2 Overflow                         (TIMER2_OVF_vect)
11  Timer/Counter1 Capture Event                    (TIMER1_CAPT_vect)
12  Timer/Counter1 Compare Match A                  (TIMER1_COMPA_vect)
13  Timer/Counter1 Compare Match B                  (TIMER1_COMPB_vect)
14  Timer/Counter1 Overflow                         (TIMER1_OVF_vect)
15  Timer/Counter0 Compare Match A                  (TIMER0_COMPA_vect)
16  Timer/Counter0 Compare Match B                  (TIMER0_COMPB_vect)
17  Timer/Counter0 Overflow                         (TIMER0_OVF_vect)
18  SPI Serial Transfer Complete                    (SPI_STC_vect)
19  USART Rx Complete                               (USART_RX_vect)
20  USART, Data Register Empty                      (USART_UDRE_vect)
21  USART, Tx Complete                              (USART_TX_vect)
22  ADC Conversion Complete                         (ADC_vect)
23  EEPROM Ready                                    (EE_READY_vect)
24  Analog Comparator                               (ANALOG_COMP_vect)
25  2-wire Serial Interface  (I2C)                  (TWI_vect)
26  Store Program Memory Ready                      (SPM_READY_vect)

Os nomes internos (que você pode usar para configurar retornos de chamada ISR) estão entre colchetes.

Aviso: Se você digitar incorretamente o nome do vetor de interrupção, mesmo errando a capitalização (uma coisa fácil), a rotina de interrupção não será chamada e você não receberá um erro do compilador.


Razões para usar interrupções

Os principais motivos pelos quais você pode usar interrupções são:

  • Para detectar alterações nos pinos (por exemplo, codificadores rotativos, pressionamentos de botão)
  • Temporizador de vigilância (por exemplo, se nada acontecer após 8 segundos, interrompa-me)
  • Interrupções do temporizador - usadas para comparar / transbordar temporizadores
  • Transferências de dados SPI
  • Transferências de dados I2C
  • Transferências de dados USART
  • Conversões ADC (analógico para digital)
  • EEPROM pronta para uso
  • Pronto para memória flash

As "transferências de dados" podem ser usadas para permitir que um programa faça outra coisa enquanto os dados estão sendo enviados ou recebidos na porta serial, porta SPI ou porta I2C.

Acorde o processador

Interrupções externas, interrupções de troca de pinos e interrupção do cronômetro do watchdog também podem ser usadas para ativar o processador. Isso pode ser muito útil, pois no modo de suspensão, o processador pode ser configurado para usar muito menos energia (por exemplo, cerca de 10 microamperes). Uma interrupção crescente, decrescente ou de baixo nível pode ser usada para ativar um gadget (por exemplo, se você pressionar um botão) ou uma interrupção "watchdog timer" pode ativá-lo periodicamente (por exemplo, para verificar a hora ou temperatura).

Interrupções de troca de pinos podem ser usadas para ativar o processador se uma tecla for pressionada em um teclado ou similar.

O processador também pode ser despertado por uma interrupção do cronômetro (por exemplo, um cronômetro atingindo um determinado valor ou transbordando) e certos outros eventos, como uma mensagem I2C recebida.


Ativando / desativando interrupções

A interrupção "reset" não pode ser desativada. No entanto, as outras interrupções podem ser temporariamente desabilitadas limpando o sinalizador de interrupção global.

Ativar interrupções

Você pode ativar as interrupções com a chamada de função "interrupções" ou "sei", assim:

interrupts ();  // or ...
sei ();         // set interrupts flag

Desativar interrupções

Se você precisar desativar as interrupções, pode "limpar" o sinalizador de interrupção global como este:

noInterrupts ();  // or ...
cli ();           // clear interrupts flag

Qualquer um dos métodos tem o mesmo efeito, usar interrupts/ noInterruptsé um pouco mais fácil de lembrar de que maneira eles estão.

O padrão no Arduino é que as interrupções sejam ativadas. Não desative-os por longos períodos, ou coisas como temporizadores não funcionarão corretamente.

Por que desativar interrupções?

Pode haver partes críticas do tempo que você não deseja que sejam interrompidas, por exemplo, por uma interrupção do timer.

Além disso, se os campos de vários bytes estiverem sendo atualizados por um ISR, talvez seja necessário desativar as interrupções para obter os dados "atomicamente". Caso contrário, um byte poderá ser atualizado pelo ISR enquanto você estiver lendo o outro.

Por exemplo:

noInterrupts ();
long myCounter = isrCounter;  // get value set by ISR
interrupts ();

Desligar temporariamente as interrupções garante que o isrCounter (um contador definido dentro de um ISR) não seja alterado enquanto estivermos obtendo seu valor.

Aviso: se você não tiver certeza se as interrupções já estão ativadas ou não, será necessário salvar o estado atual e restaurá-lo posteriormente. Por exemplo, o código da função millis () faz isso:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- save status register

  // disable interrupts while we read timer0_millis or we might get an
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- restore status register including interrupt flag

  return m;
}

Observe que as linhas indicadas salvam o SREG (registro de status) atual, que inclui o sinalizador de interrupção. Depois de obtermos o valor do timer (que tem 4 bytes), colocamos o registro de status de volta como estava.


Dicas

Nomes de funções

As funções cli/ seie o registro SREG são específicos para os processadores AVR. Se você estiver usando outros processadores, como o ARM, as funções podem ser um pouco diferentes.

Desativando globalmente x desabilitando uma interrupção

Se você usar, cli()desabilitará todas as interrupções (incluindo interrupções do timer, interrupções seriais, etc.).

No entanto, se você quiser apenas desativar uma interrupção específica , limpe o sinalizador de ativação de interrupção para essa fonte de interrupção específica. Por exemplo, para interrupções externas, ligue detachInterrupt().


O que é prioridade de interrupção?

Como existem 25 interrupções (além da redefinição), é possível que mais de um evento de interrupção ocorra ao mesmo tempo ou pelo menos antes que o anterior seja processado. Também pode ocorrer um evento de interrupção enquanto as interrupções estão desativadas.

A ordem de prioridade é a sequência na qual o processador verifica eventos de interrupção. Quanto maior a lista, maior a prioridade. Portanto, por exemplo, uma solicitação de interrupção externa 0 (pino D2) seria atendida antes da solicitação de interrupção externa 1 (pino D3).


Podem ocorrer interrupções enquanto as interrupções estão desativadas?

Eventos de interrupção (ou seja, perceber o evento) podem ocorrer a qualquer momento, e a maioria é lembrada definindo um sinalizador de "evento de interrupção" dentro do processador. Se as interrupções estiverem desabilitadas, essa interrupção será tratada quando forem ativadas novamente, em ordem de prioridade.


Como você usa interrupções?

  • Você escreve um ISR (rotina de serviço de interrupção). Isso é chamado quando a interrupção ocorre.
  • Você diz ao processador quando deseja que a interrupção seja acionada.

Escrevendo um ISR

As rotinas de serviço de interrupção são funções sem argumentos. Algumas bibliotecas do Arduino são projetadas para chamar suas próprias funções, então você apenas fornece uma função comum (como nos exemplos acima), por exemplo.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
 flag = true;
}  // end of switchPressed

No entanto, se uma biblioteca ainda não forneceu um "gancho" para um ISR, você pode criar seu próprio, assim:

volatile char buf [100];
volatile byte pos;

// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

Nesse caso, você usa a macro "ISR" e fornece o nome do vetor de interrupção relevante (da tabela anterior). Nesse caso, o ISR está lidando com a conclusão de uma transferência SPI. (Observe que alguns códigos antigos usam SIGNAL em vez de ISR; no entanto, SIGNAL está obsoleto).

Conectando um ISR a uma interrupção

Para interrupções já tratadas por bibliotecas, você apenas usa a interface documentada. Por exemplo:

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // do something with the incoming byte
    }
}  // end of receiveEvent

void setup ()
  {
  Wire.onReceive(receiveEvent);
  }

Nesse caso, a biblioteca I2C foi projetada para manipular internamente os bytes I2C recebidos e, em seguida, chame sua função fornecida no final do fluxo de dados recebidos. Nesse caso, o receiveEvent não é estritamente um ISR (ele possui um argumento), mas é chamado por um ISR embutido.

Outro exemplo é a interrupção do "pino externo".

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
}  // end of switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // attach interrupt handler for D2
}  // end of setup

Nesse caso, a função attachInterrupt adiciona a função switchPressed a uma tabela interna e, além disso, configura os sinalizadores de interrupção apropriados no processador.

Configurando o Processador para Manipular uma Interrupção

O próximo passo, depois de ter um ISR, é dizer ao processador que você deseja que essa condição específica aconteça uma interrupção.

Como exemplo, para a interrupção externa 0 (a interrupção D2), você pode fazer algo assim:

EICRA &= ~3;  // clear existing flags
EICRA |= 2;   // set wanted flags (falling level interrupt)
EIMSK |= 1;   // enable it

Mais legível seria usar os nomes definidos, assim:

EICRA &= ~(bit(ISC00) | bit (ISC01));  // clear existing flags
EICRA |= bit (ISC01);    // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0);     // enable it

O EICRA (Registro de controle de interrupção externa A) seria definido de acordo com esta tabela na folha de dados do Atmega328. Isso define o tipo exato de interrupção que você deseja:

  • 0: O nível baixo de INT0 gera uma solicitação de interrupção (interrupção LOW).
  • 1: Qualquer alteração lógica no INT0 gera uma solicitação de interrupção (CHANGE interrupt).
  • 2: A borda descendente de INT0 gera uma solicitação de interrupção (interrupção FALLING).
  • 3: A borda ascendente do INT0 gera uma solicitação de interrupção (interrupção RISING).

O EIMSK (Registro de máscara de interrupção externa) realmente permite a interrupção.

Felizmente, você não precisa se lembrar desses números, porque o attachInterrupt faz isso por você. No entanto, é isso o que realmente está acontecendo e, para outras interrupções, você pode ter que "manualmente" definir sinalizadores de interrupção.


ISRs de baixo nível vs. ISRs de biblioteca

Para simplificar sua vida, alguns manipuladores de interrupção comuns estão realmente dentro do código da biblioteca (por exemplo, INT0_vect e INT1_vect) e, em seguida, é fornecida uma interface mais amigável (por exemplo, attachInterrupt). O que o attachInterrupt realmente faz é salvar o endereço do manipulador de interrupção desejado em uma variável e, em seguida, chamar isso de INT0_vect / INT1_vect quando necessário. Ele também define os sinalizadores de registro apropriados para chamar o manipulador quando necessário.


Os ISRs podem ser interrompidos?

Em resumo, não, não, a menos que você queira que eles sejam.

Quando um ISR é inserido, as interrupções são desativadas . Naturalmente, eles devem ter sido ativados em primeiro lugar, caso contrário, o ISR não seria inserido. No entanto, para evitar que um ISR seja interrompido, o processador desativa as interrupções.

Quando um ISR sai, as interrupções são ativadas novamente . O compilador também gera código dentro de um ISR para salvar registros e sinalizadores de status, para que o que você estava fazendo quando a interrupção ocorreu não seja afetado.

No entanto, você pode ativar as interrupções dentro de um ISR se for absolutamente necessário, por exemplo.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
  interrupts ();  // allow more interrupts

}  // end of switchPressed

Normalmente, você precisaria de um bom motivo para fazer isso, pois outra interrupção agora pode resultar em uma chamada recursiva ao pinChange, com resultados possivelmente indesejáveis.


Quanto tempo leva para executar um ISR?

De acordo com a folha de dados, o tempo mínimo para atender uma interrupção é de 4 ciclos de clock (para empurrar o contador do programa atual para a pilha) seguido pelo código agora em execução no local do vetor de interrupção. Isso normalmente contém um salto para onde realmente está a rotina de interrupção, ou seja, mais 3 ciclos. O exame do código produzido pelo compilador mostra que um ISR feito com a declaração "ISR" pode levar cerca de 2.625 µs para ser executado, mais o que o próprio código faz. A quantidade exata depende de quantos registros precisam ser salvos e restaurados. A quantidade mínima seria 1,1875 µs.

As interrupções externas (onde você usa o attachInterrupt) fazem um pouco mais e levam cerca de 5,125 µs no total (funcionando com um relógio de 16 MHz).


Quanto tempo antes do processador começar a inserir um ISR?

Isso varia um pouco. Os números citados acima são os ideais onde a interrupção é processada imediatamente. Alguns fatores podem atrasar que:

  • Se o processador estiver no modo de espera, existem horários designados de "ativação", que podem levar alguns milissegundos, enquanto o relógio é colocado no spool de volta à velocidade. Esse tempo dependeria das configurações dos fusíveis e da profundidade do sono.

  • Se uma rotina de serviço de interrupção já estiver em execução, outras interrupções não poderão ser inseridas até que ela termine ou ative a interrupção. É por isso que você deve manter cada rotina de serviço de interrupção curta, pois a cada microssegundo gasto em um, você está potencialmente atrasando a execução de outro.

  • Algum código desativa as interrupções. Por exemplo, chamar millis () rapidamente desliga as interrupções. Portanto, o tempo para uma interrupção ser atendida seria estendido pelo tempo que as interrupções foram desativadas.

  • As interrupções só podem ser reparadas no final de uma instrução; portanto, se uma instrução em particular leva três ciclos de relógio e acaba de iniciar, a interrupção atrasará pelo menos alguns ciclos de relógio.

  • Um evento que ativa novamente as interrupções (por exemplo, retornando de uma rotina de serviço de interrupção) garante a execução de pelo menos mais uma instrução. Portanto, mesmo que um ISR termine e sua interrupção esteja pendente, ele ainda precisará aguardar mais uma instrução antes de ser reparado.

  • Como as interrupções têm uma prioridade, uma interrupção de maior prioridade pode ser atendida antes da interrupção em que você está interessado.


Considerações de desempenho

As interrupções podem aumentar o desempenho em muitas situações, porque você pode continuar com o "trabalho principal" do seu programa sem precisar testar constantemente para ver se as chaves foram pressionadas. Dito isto, a sobrecarga de manutenção de uma interrupção, como discutido acima, seria realmente mais do que fazer um "loop apertado" pesquisando uma única porta de entrada. Você mal pode responder a um evento em, digamos, um microssegundo. Nesse caso, você pode desativar as interrupções (por exemplo, temporizadores) e apenas procurar o pino para mudar.


Como as interrupções estão na fila?

Existem dois tipos de interrupções:

  • Alguns definem um sinalizador e são tratados em ordem de prioridade, mesmo que o evento que os causou tenha parado. Por exemplo, uma subida, descida ou mudança de nível no pino D2.

  • Outros são testados apenas se estiverem ocorrendo "agora". Por exemplo, uma interrupção de baixo nível no pino D2.

Os que configuram um sinalizador podem ser considerados em fila, pois o sinalizador de interrupção permanece definido até o momento em que a rotina de interrupção é inserida, momento em que o processador limpa o sinalizador. Obviamente, como existe apenas um sinalizador, se a mesma condição de interrupção ocorrer novamente antes que o primeiro seja processado, ele não será atendido duas vezes.

Algo a ter em atenção é que esses sinalizadores podem ser definidos antes de você anexar o manipulador de interrupções. Por exemplo, é possível que uma interrupção de nível crescente ou decrescente no pino D2 seja "sinalizada" e, assim que você fizer um anexo. Interromper a interrupção é acionada imediatamente, mesmo se o evento ocorreu uma hora atrás. Para evitar isso, você pode limpar manualmente a bandeira. Por exemplo:

EIFR = bit (INTF0);  // clear flag for interrupt 0
EIFR = bit (INTF1);  // clear flag for interrupt 1

No entanto, as interrupções de "baixo nível" são verificadas continuamente; portanto, se você não tomar cuidado, elas continuarão disparando, mesmo após a interrupção ter sido chamada. Ou seja, o ISR sairá e a interrupção será acionada imediatamente novamente. Para evitar isso, você deve interromper a interrupção imediatamente após saber que a interrupção foi acionada.


Dicas para escrever ISRs

Em resumo, mantenha-os curtos! Enquanto um ISR estiver executando, outras interrupções não podem ser processadas. Assim, você pode facilmente perder o pressionamento de botão ou a comunicação serial recebida, se tentar fazer demais. Em particular, você não deve tentar depurar "impressões" dentro de um ISR. O tempo necessário para fazer isso provavelmente causará mais problemas do que eles resolvem.

Uma coisa razoável a se fazer é definir um sinalizador de byte único e testá-lo na função de loop principal. Ou, armazene um byte recebido de uma porta serial em um buffer. As interrupções embutidas do cronômetro acompanham o tempo decorrido disparando toda vez que o cronômetro interno transborda e, portanto, você pode calcular o tempo decorrido sabendo quantas vezes o cronômetro transbordou.

Lembre-se, dentro de um ISR as interrupções estão desabilitadas. Portanto, esperar que o tempo retornado pelas chamadas de função millis () mude, levará à decepção. É válido obter a hora dessa maneira, apenas lembre-se de que o cronômetro não está aumentando. E se você passar muito tempo no ISR, o timer pode perder um evento de estouro, levando o tempo retornado por millis () a ficar incorreto.

Um teste mostra que, em um processador Atmega328 de 16 MHz, uma chamada para micros () leva 3,5625 µs. Uma chamada para millis () leva 1,9375 µs. Gravar (salvar) o valor atual do timer é uma coisa razoável a se fazer em um ISR. Encontrar os milissegundos decorridos é mais rápido que os microssegundos decorridos (a contagem de milissegundos é recuperada apenas de uma variável). No entanto, a contagem de microssegundos é obtida adicionando o valor atual do timer do timer 0 (que continuará aumentando) a uma "contagem de estouro do timer 0" salva.

Aviso: Como as interrupções são desativadas dentro de um ISR, e a versão mais recente do IDE do Arduino usa interrupções para leitura e gravação serial, e também para incrementar o contador usado por "millis" e "delay", você não deve tentar usar essas funções dentro de um ISR. Em outras palavras:

  • Não tente atrasar, por exemplo: delay (100);
  • Você pode obter o tempo de uma chamada para milis, no entanto, ela não aumenta, portanto, não tente atrasar esperando que ela aumente.
  • Não faça impressões seriais (por exemplo Serial.println ("ISR entered");) .
  • Não tente fazer leitura serial.

Interrupções de troca de pinos

Existem duas maneiras de detectar eventos externos nos pinos. O primeiro são os pinos especiais de "interrupção externa", D2 e ​​D3. Esses eventos discretos gerais de interrupção, um por pino. Você pode acessá-los usando attachInterrupt para cada pino. Você pode especificar uma condição de aumento, queda, alteração ou nível baixo para a interrupção.

No entanto, também existem interrupções de "troca de pinos" para todos os pinos (no Atmega328, nem sempre todos os pinos em outros processadores). Eles atuam em grupos de pinos (D0 a D7, D8 a D13 e A0 a A5). Eles também têm prioridade mais baixa do que o evento externo interrompe. No entanto, eles são um pouco mais difíceis de usar do que as interrupções externas, porque são agrupados em lotes. Portanto, se a interrupção for acionada, você precisará elaborar em seu próprio código exatamente qual pino causou a interrupção.

Código de exemplo:

ISR (PCINT0_vect)
 {
 // handle pin change interrupt for D8 to D13 here
 }  // end of PCINT0_vect

ISR (PCINT1_vect)
 {
 // handle pin change interrupt for A0 to A5 here
 }  // end of PCINT1_vect

ISR (PCINT2_vect)
 {
 // handle pin change interrupt for D0 to D7 here
 }  // end of PCINT2_vect


void setup ()
  {
  // pin change interrupt (example for D9)
  PCMSK0 |= bit (PCINT1);  // want pin 9
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to D13
  }

Para lidar com uma interrupção de troca de pinos, é necessário:

  • Especifique qual pino no grupo. Essa é a variável PCMSKn (em que n é 0, 1 ou 2 da tabela abaixo). Você pode ter interrupções em mais de um pino.
  • Habilite o grupo apropriado de interrupções (0, 1 ou 2)
  • Forneça um manipulador de interrupção, como mostrado acima

Tabela de pinos -> nomes / máscaras de alteração de pinos

D0    PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1    PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2    PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3    PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4    PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5    PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6    PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7    PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8    PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9    PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10   PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11   PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12   PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13   PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0    PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1    PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2    PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3    PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4    PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5    PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Interromper o processamento do manipulador

O manipulador de interrupção precisaria descobrir qual pino causou a interrupção se a máscara especificar mais de um (por exemplo, se você quisesse interrupções no D8 / D9 / D10). Para fazer isso, você precisará armazenar o estado anterior desse pino e trabalhar (executando um DigitalRead ou similar) se esse pino em particular tiver sido alterado.


Você provavelmente está usando interrupções de qualquer maneira ...

Um ambiente Arduino "normal" já está usando interrupções, mesmo que você não tente pessoalmente. As chamadas de função millis () e micros () fazem uso do recurso "estouro de timer". Um dos cronômetros internos (cronômetro 0) é configurado para interromper aproximadamente 1000 vezes por segundo e incrementar um contador interno que efetivamente se torna o contador millis (). Há um pouco mais do que isso, pois o ajuste é feito para a velocidade exata do relógio.

Além disso, a biblioteca serial de hardware usa interrupções para manipular dados seriais recebidos e enviados. Isso é muito útil, pois seu programa pode fazer outras coisas enquanto as interrupções estão disparando e preenchendo um buffer interno. Então, quando você verifica Serial.available (), pode descobrir o que foi colocado nesse buffer, se houver alguma coisa.


Executando a próxima instrução após ativar interrupções

Após algumas discussões e pesquisas no fórum do Arduino, esclarecemos exatamente o que acontece depois que você ativa as interrupções. Existem três maneiras principais pelas quais você pode ativar as interrupções, que não foram ativadas anteriormente:

  sei ();  // set interrupt enable flag
  SREG |= 0x80;  // set the high-order bit in the status register
  reti  ;   // assembler instruction "return from interrupt"

Em todos os casos, o processador garante que a próxima instrução após as interrupções seja ativada (se elas foram desativadas anteriormente) sempre será executada, mesmo se um evento de interrupção estiver pendente. (Por "próximo", quero dizer o próximo na sequência do programa, não necessariamente o que está fisicamente a seguir. Por exemplo, uma instrução RETI retorna ao local onde ocorreu a interrupção e depois executa mais uma instrução).

Isso permite que você escreva códigos como este:

sei ();
sleep_cpu ();

Se não for essa garantia, a interrupção pode ocorrer antes que o processador durma e nunca poderá ser despertada.


Interrupções vazias

Se você deseja apenas uma interrupção para ativar o processador, mas não faz nada em particular, pode usar a definição EMPTY_INTERRUPT, por exemplo.

EMPTY_INTERRUPT (PCINT1_vect);

Isso simplesmente gera uma instrução "reti" (retorno de interrupção). Como ele não tenta salvar ou restaurar registros, essa seria a maneira mais rápida de obter uma interrupção para ativá-lo.


Seções críticas (acesso a variáveis ​​atômicas)

Existem alguns problemas sutis sobre variáveis ​​compartilhadas entre as rotinas de serviço de interrupção (ISRs) e o código principal (ou seja, o código que não está em um ISR).

Como um ISR pode ser acionado a qualquer momento quando as interrupções são ativadas, você precisa ter cuidado ao acessar essas variáveis ​​compartilhadas, pois elas podem estar sendo atualizadas no momento em que você as acessa.

Primeiro ... quando você usa variáveis ​​"voláteis"?

Uma variável só deve ser marcada como volátil se for usada dentro de um ISR e fora de um.

  • Variáveis usadas apenas fora de um ISR não devem ser voláteis.
  • Variáveis usadas apenas dentro de um ISR não devem ser voláteis.
  • As variáveis ​​usadas dentro e fora de um ISR devem ser voláteis.

por exemplo.

volatile int counter;

Marcar uma variável como volátil indica ao compilador para não "armazenar em cache" o conteúdo das variáveis ​​em um registro do processador, mas sempre lê-lo da memória, quando necessário. Isso pode retardar o processamento, e é por isso que você não apenas torna todas as variáveis ​​voláteis, quando não são necessárias.

Desativar interrupções ao acessar uma variável volátil

Por exemplo, para comparar countcom algum número, desative as interrupções durante a comparação, caso um byte de counttenha sido atualizado pelo ISR e não o outro byte.

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop ()
  {
  noInterrupts ();    // <------ critical section
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ end critical section
  } // end of loop

Leia a folha de dados!

Mais informações sobre interrupções, temporizadores etc. podem ser obtidas na folha de dados do processador.

http://www.atmel.com/images/Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.pdf


Outros exemplos

Considerações de espaço (limite de tamanho da postagem) impedem minha listagem de mais exemplos de código. Para mais exemplos de código, veja minha página sobre interrupções .

Nick Gammon
fonte
Uma referência muito útil - que foi uma resposta impressionantemente rápida.
Dat Han Bag
Foi uma pergunta de referência. Eu tinha a resposta preparada e teria sido ainda mais rápida se a resposta não fosse longa demais, então tive que podá-la de volta. Veja o site vinculado para mais detalhes.
Nick Gammon
Sobre o "modo de suspensão", é eficiente fazer o Arduino dormir por, digamos, 500ms?
Dat Ha
@ Nick Gammon Acho que ligar ou desligar a energia (com automação ou não) para a CPU pode ser definido como uma interrupção não convencional - se você quiser fazer isso. "Eu tinha a resposta preparada" - você acabou de tirar toda a magia daquele momento que pensei que tinha.
Dat Han Bag
11
Receio que isso não seja verdade. Eu tenho um exemplo que usa interrupções de troca de pinos para sair do modo de desligamento. Além disso, como mencionei na minha página sobre interrupções, o Atmel confirmou que qualquer interrupção externa irá ativar o processador (ou seja, subindo / descendo / alterando e baixo).
Nick Gammon