¿Son atómicas las asignaciones de puntero de función en Arduino?

11

Los siguientes fragmentos son del código fuente de la 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();                                                                                            
}

La pregunta: si el temporizador ya se está ejecutando y el programa principal llama attachInterrupt(), ¿podría ocurrir la interrupción del temporizador allí durante la asignación del puntero de función isrCallback = isr;? Entonces, con suerte, el Timer1.isrCallback();puntero de la función consistiría en parte en la dirección anterior y en la nueva, lo que provocaría que el ISR salte a una ubicación falsa.

Supongo que este podría ser el caso, ya que los punteros de función son ciertamente más anchos que 1 byte, y acceder a datos de> 1 byte no es atómico. Las posibles soluciones podrían ser:

  • Siempre llame detachInterrupt()para asegurarse de que el temporizador no se esté ejecutando attachInterrupt(), es decir, aclare los documentos de Timer1.
  • O bien, modifique Timer1, deshabilitando las interrupciones de desbordamiento del temporizador temporalmente justo antes isrCallback = isr;

¿Tiene sentido o hay algo en las Timer1fuentes o en las asignaciones de puntero de función que me he perdido?

Joonas Pulakka
fuente

Respuestas:

7

Eche un vistazo al código para attachInterrupt () y detachInterrupt () en /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/WInterrupts.c(bueno, ahí es donde están en una Mac, de todos modos. La estructura de archivos Arduino en otros sistemas operativos probablemente se ve similar en los niveles inferiores de la ruta).

Parece que attachInterrupt () supone que la interrupción en cuestión aún no está habilitada porque escribe el puntero de función sin tomar ninguna precaución. Tenga en cuenta que detachInterrupts () deshabilita la interrupción de destino antes de escribir un puntero NULL en su vector. Así que al menos usaría un detachInterrupt()/ attachInterrupt()par

Me gustaría ejecutar cualquier código de este tipo en una sección crítica, yo mismo. Parece que su primera forma (separar, luego adjuntar) funcionaría, aunque no puedo estar seguro de que no pueda faltar una interrupción desafortunadamente cronometrada. La hoja de datos para su MCU podría tener más que decir al respecto. Pero tampoco estoy seguro en este momento, que un global cli()/ sei()tampoco lo extrañaría. La hoja de datos ATMega2560, sección 6.8, dice "Cuando se usa la instrucción SEI para habilitar interrupciones, la instrucción que sigue a SEI se ejecutará antes de cualquier interrupción pendiente, como se muestra en este ejemplo", lo que parece implicar que puede amortiguar una interrupción mientras las interrupciones están fuera.

JRobert
fuente
De hecho, es útil sumergirse en las fuentes :) El mecanismo de interrupción de conexión / desconexión de TimerOne parece estar hecho de manera similar al estándar (Winterrupt's) y tiene las mismas "características".
Joonas Pulakka
0

Parece que tienes un punto. Lo lógico sería deshabilitar las interrupciones de tal manera que no las vuelva a habilitar si se deshabilitaron en primer lugar. Por ejemplo:

  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

"Cuando se usa la instrucción SEI para habilitar las interrupciones, la instrucción que sigue a SEI se ejecutará antes de cualquier interrupción pendiente, como se muestra en este ejemplo"

La intención de esto es permitirle escribir código como este:

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

Sin esa disposición, podría obtener una interrupción entre esas dos líneas y, por lo tanto, dormir indefinidamente (porque la interrupción que iba a despertar se produjo antes de dormir). La provisión en el procesador de que la siguiente instrucción, después de que las interrupciones se habiliten si no se habilitaron antes, siempre se ejecuta, protege contra esto.

Nick Gammon
fuente
¿No sería más eficiente TIMSK1=0; TIFR1=_BV(TOV1); isrCallback=isr; TIMSK1=_BV(TOIE1);? Ahorra un registro de CPU y no contribuye a interrumpir la latencia.
Edgar Bonet
¿Qué pasa con los otros bits, como ICIE1, OCIE1B, OCIE1A? Entiendo sobre la latencia, pero las interrupciones deberían poder hacer frente a un par de ciclos de reloj de no estar disponible. Puede haber una condición de carrera. Es posible que la interrupción ya se haya activado (es decir, que el indicador en la CPU esté configurado) y que el apagado de TIMSK1 no impida que se maneje. También es posible que deba restablecer TOV1 (escribiendo 1 en TIFR1) para asegurarse de que eso no suceda. La incertidumbre allí me lleva a pensar que apagar las interrupciones a nivel mundial es el curso más seguro.
Nick Gammon