As atribuições de ponteiros de função são atômicas no Arduino?

11

Os seguintes trechos são do código-fonte da biblioteca TimerOne :

// TimerOne.h:
void (*isrCallback)();

// TimerOne.cpp:
ISR(TIMER1_OVF_vect) // interrupt service routine that wraps a user defined function supplied by attachInterrupt
{
  Timer1.isrCallback();
}

// TimerOne.cpp:
void TimerOne::attachInterrupt(void (*isr)(), long microseconds)
{
  if(microseconds > 0) setPeriod(microseconds);
  isrCallback = isr; // register the user's callback with the real ISR
  TIMSK1 = _BV(TOIE1); // sets the timer overflow interrupt enable bit
  resume();                                                                                            
}

A pergunta: se o cronômetro já estiver em execução, e o programa principal chamar attachInterrupt(), o temporizador poderá ocorrer durante a atribuição do ponteiro de função isrCallback = isr;? Então, com um tempo de sorte, o Timer1.isrCallback();ponteiro de função consistiria em parte no antigo e em parte no novo endereço, fazendo com que o ISR pulasse para um local falso?

Suponho que possa ser esse o caso, pois os ponteiros de função certamente são maiores que 1 byte e o acesso a dados de> 1 byte não é atômico. As soluções possíveis podem ser:

  • Sempre ligue detachInterrupt()para garantir que o cronômetro não esteja funcionando antes de chamar attachInterrupt(), ou seja, esclareça os documentos do Timer1.
  • Ou modifique o Timer1, desativando temporariamente o estouro de temporizador interrompe temporariamente antes de isrCallback = isr;

Isso faz sentido ou há algo nas Timer1fontes ou atribuições de ponteiros de função que eu perdi?

Joonas Pulakka
fonte

Respostas:

7

Dê uma olhada no código para attachInterrupt () e detachInterrupt () em /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/WInterrupts.c(bem, é aí que eles estão em um Mac, de qualquer maneira. A estrutura de arquivos do Arduino em outros sistemas operacionais provavelmente é semelhante nos níveis mais baixos do caminho).

Parece que attachInterrupt () supõe que a interrupção em questão ainda não esteja ativada porque grava o ponteiro da função sem tomar nenhuma precaução. Observe que detachInterrupts () desabilita a interrupção do destino antes de gravar um ponteiro NULL em seu vetor. Então, eu, pelo menos, o uso de um detachInterrupt()/ attachInterrupt()par

Eu gostaria de executar esse código em uma seção crítica, por conta própria. Parece que sua primeira maneira (desanexar e depois anexar) funcionaria, embora eu não tenha certeza de que não poderia perder uma interrupção infelizmente programada. A folha de dados do seu MCU pode ter mais a dizer sobre isso. Mas também não tenho certeza, neste momento, que um global cli()/ sei()também não perca. A folha de dados do ATMega2560, seção 6.8, diz "Ao usar a instrução SEI para habilitar interrupções, a instrução a seguir SEI será executada antes de qualquer interrupção pendente, como mostrado neste exemplo", parecendo sugerir que ele pode armazenar em buffer uma interrupção enquanto as interrupções estão desligados.

JRobert
fonte
É realmente útil mergulhar nas fontes :) O mecanismo de interrupção de anexar / desanexar do TimerOne parece ser feito da mesma forma que o padrão (do WInterrupt) e, portanto, possui os mesmos "recursos".
Joonas Pulakka
0

Parece que você tem razão. A coisa lógica a ser seria desativar as interrupções de forma que você não as reative se elas foram desativadas em primeiro lugar. Por exemplo:

  uint8_t oldSREG = SREG;  // remember if interrupts are on
  cli();                   // make the next line interruptible
  isrCallback = isr;       // register the user's callback with the real ISR
  SREG = oldSREG;          // turn interrupts back on, if they were on before

"Ao usar a instrução SEI para ativar interrupções, a instrução a seguir SEI será executada antes de qualquer interrupção pendente, conforme mostrado neste exemplo"

A intenção disso é permitir que você escreva um código como este:

  sei ();         // enable interrupts
  sleep_cpu ();   // sleep

Sem essa disposição, você poderá interromper essas duas linhas e, assim, dormir indefinidamente (porque a interrupção que iria acordar você ocorreu antes de você dormir). A disposição no processador de que a próxima instrução, após as interrupções, seja ativada se elas não foram ativadas antes, é sempre executada, protege contra isso.

Nick Gammon
fonte
Não seria mais eficiente TIMSK1=0; TIFR1=_BV(TOV1); isrCallback=isr; TIMSK1=_BV(TOIE1);? Poupa um registro da CPU e não contribui com latência de interrupção.
Edgar Bonet
E os outros bits, como ICIE1, OCIE1B, OCIE1A? Eu entendo sobre a latência, mas as interrupções devem ser capazes de lidar com alguns ciclos de clock de não estarem disponíveis. Pode haver uma condição de corrida. A interrupção pode já ter sido acionada (ou seja, o sinalizador na CPU está definido) e desativar o TIMSK1 pode não impedir que ela seja manipulada. Você também pode precisar redefinir o TOV1 (escrevendo 1 no TIFR1) para garantir que isso não aconteça. A incerteza ali me leva a pensar que desligar interrupções globalmente é o caminho mais seguro.
Nick Gammon