Condición de carrera de sueño del microcontrolador

11

Dado un microcontrolador que ejecuta el siguiente código:

volatile bool has_flag = false;

void interrupt(void) //called when an interrupt is received
{
    clear_interrupt_flag(); //clear interrupt flag
    has_flag = true; //signal that we have an interrupt to process
}

int main()
{
    while(1)
    {
        if(has_flag) //if we had an interrupt
        {
            has_flag = false; //clear the interrupt flag
            process(); //process the interrupt
        }
        else
            sleep(); //place the micro to sleep
    }
}

Supongamos que la if(has_flag)condición se evalúa como falsa y estamos a punto de ejecutar la instrucción de suspensión. Justo antes de ejecutar la instrucción de suspensión, recibimos una interrupción. Después de dejar la interrupción, ejecutamos la instrucción de suspensión.

Esta secuencia de ejecución no es deseable porque:

  • El microcontrolador se fue a dormir en lugar de despertarse y llamar process().
  • Es posible que el microcontrolador nunca se active si no se recibe ninguna interrupción.
  • La llamada a process()se pospone hasta la próxima interrupción.

¿Cómo se puede escribir el código para evitar que ocurra esta condición de carrera?

Editar

Algunos microcontroladores, como el ATMega, tienen un bit de habilitación de suspensión que evita que ocurra esta condición (gracias Kvegaoro por señalar esto). JRoberts ofrece una implementación de ejemplo que ejemplifica este comportamiento.

Otros micros, como los PIC18, no tienen este bit y el problema aún ocurre. Sin embargo, estos micros están diseñados de tal manera que las interrupciones aún pueden despertar el núcleo independientemente de si el bit de habilitación de interrupción global está configurado (gracias supercat por señalar esto). Para tales arquitecturas, la solución es deshabilitar las interrupciones globales justo antes de ir a dormir. Si se dispara una interrupción justo antes de ejecutar la instrucción de suspensión, el controlador de interrupción no se ejecutará, el núcleo se activará y, una vez que se vuelvan a habilitar las interrupciones globales, se ejecutará el controlador de interrupción. En pseudocódigo, la implementación se vería así:

int main()
{
    while(1)
    {
        //clear global interrupt enable bit.
        //if the flag tested below is not set, then we enter
        //sleep with the global interrupt bit cleared, which is
        //the intended behavior.
        disable_global_interrupts();

        if(has_flag) //if we had an interrupt
        {
            has_flag = false; //clear the interrupt flag
            enable_global_interrupts();  //set global interrupt enable bit.

            process(); //process the interrupt
        }
        else
            sleep(); //place the micro to sleep
    }
}
TRISAbits
fuente
¿Es esta pregunta práctica o teórica?
AndrejaKo
en teoría, usa un temporizador que lo despierta una vez cada (ingrese un valor aceptable) ms y luego vuelve a dormir si no necesita hacer nada.
Grady Player
1
Lo haría interrupt_flagcomo un int, y lo incrementaría cada vez que haya una interrupción. Luego cambie if(has_flag)a while (interrupts_count)y luego duerma. No obstante, la interrupción podría ocurrir después de que haya salido del ciclo while. Si esto es un problema, ¿el procesamiento en la interrupción misma?
angelatlarge
1
bueno, depende de qué micro esté ejecutando. Si se tratara de un ATmega328, posiblemente podría deshabilitar el modo de suspensión en la interrupción, por lo que si se produce la condición de carrera que describe, la función de suspensión se anularía, vuelva de nuevo y procesará la interrupción con una pequeña latencia. Pero también usar un temporizador para despertarse a un intervalo igual o menor a su latencia máxima también sería una gran solución
Kvegaoro
1
@TRISAbits: en el PIC 18x, el enfoque que describí en mi respuesta funciona bien (es mi diseño normal cuando uso esa parte).
supercat

Respuestas:

9

Por lo general, hay algún tipo de soporte de hardware para este caso. Por ejemplo, la seiinstrucción de los AVR para habilitar las interrupciones difiere hasta que se complete la siguiente instrucción. Con ella se puede hacer:

forever,
   interrupts off;
   if has_flag,
      interrupts on;
      process interrupt;
   else,
      interrupts-on-and-sleep;    # won't be interrupted
   end
end

La interrupción que se hubiera perdido en el ejemplo se retrasaría en este caso hasta que el procesador complete su secuencia de suspensión.

JRobert
fuente
¡Gran respuesta! El algoritmo que proporciona realmente funciona realmente bien en un AVR. Gracias por la sugerencia.
TRISAbits
3

En muchos microcontroladores, además de poder habilitar o deshabilitar causas de interrupción particulares (generalmente dentro de un módulo de controlador de interrupción), hay un indicador maestro dentro del núcleo de la CPU que determina si se aceptarán solicitudes de interrupción. Muchos microcontroladores saldrán del modo de suspensión si una solicitud de interrupción llega al núcleo, tanto si el núcleo está dispuesto a aceptarlo como si no.

En tal diseño, un enfoque simple para lograr un comportamiento de sueño confiable es hacer que la verificación del bucle principal borre una bandera y luego verifique si conoce alguna razón por la que el procesador debe estar despierto. Cualquier interrupción que ocurra durante ese tiempo que pueda afectar a cualquiera de esos motivos debe establecer la bandera. Si el bucle principal no encontró ninguna causa para permanecer despierto, y si el indicador no está configurado, el bucle principal debe deshabilitar las interrupciones y verificar el indicador nuevamente [tal vez después de un par de instrucciones NOP si es posible que haya una interrupción pendiente durante una instrucción deshabilitar-interrumpir podría procesarse después de que la búsqueda de operando asociada con la siguiente instrucción ya se haya realizado]. Si la bandera aún no está configurada, entonces ve a dormir.

Bajo este escenario, una interrupción que ocurre antes de que el bucle principal desactive las interrupciones establecerá la bandera antes de la prueba final. Una interrupción que queda pendiente demasiado tarde para recibir servicio antes de la instrucción de suspensión evitará que el procesador se suspenda. Ambas situaciones están bien.

Dormir al salir es a veces un buen modelo para usar, pero no todas las aplicaciones realmente lo "encajan". Por ejemplo, un dispositivo con una pantalla LCD de bajo consumo de energía podría programarse más fácilmente con un código similar al siguiente:

void select_view_user(int default_user)
{
  int current_user;
  int ch;
  current_user = default_user;
  do
  {
    lcd_printf(0, "User %d");
    lcd_printf(1, ...whatever... );
    get_key();
    if (key_hit(KEY_UP)) {current_user = (current_user + 1) % MAX_USERS};
    if (key_hit(KEY_DOWN)) {current_user = (current_user + MAX_USERS-1) % MAX_USERS};
    if (key_hit(KEY_ENTER)) view_user(current_user);
  } while(!key_hit(KEY_EXIT | KEY_TIMEOUT));
}

Si no se presionan botones y no sucede nada más, no hay ninguna razón por la cual el sistema no deba dormir durante la ejecución del get_keymétodo. Si bien es posible que las teclas activen una interrupción y administren toda la interacción de la interfaz de usuario a través de una máquina de estado, un código como el anterior es a menudo la forma más lógica de manejar flujos de interfaz de usuario altamente modal típicos de microcontroladores pequeños.

Super gato
fuente
Gracias supercat por la excelente respuesta. Deshabilitar las interrupciones y luego ir a dormir es una solución fantástica, siempre que el núcleo se despierte de cualquier fuente de interrupción, independientemente de si el bit de interrupción global está activado / desactivado. Eché un vistazo al esquema de hardware de interrupción PIC18, y esta solución funcionaría.
TRISAbits
1

Programe el micro para despertarse en la interrupción.

Los detalles específicos variarán según el micro que esté utilizando.

Luego modifique la rutina main ():

int main()
{
    while(1)
    {
        sleep();
        process(); //process the interrupt
    }
}
jwygralak67
fuente
1
La arquitectura Wake-on-interrupt se supone en la pregunta. No creo que su respuesta resuelva la pregunta / problema.
angelatlarge
@angelatlarge Punto aceptado. He agregado un ejemplo de código que creo que ayuda.
jwygralak67
@ jwygralak67: Gracias por la sugerencia, pero el código que proporciona simplemente mueve el problema a la rutina process (), que ahora tiene que comprobar si se produjo la interrupción antes de ejecutar el cuerpo del proceso ().
TRISAbits
2
Si la interrupción no hubiera ocurrido, ¿por qué estamos despiertos?
JRobert
1
@JRobert: Podríamos estar despiertos de una interrupción previa, completar la rutina process (), y cuando terminemos la prueba if (has_flag) y justo antes del sueño, recibiremos otra interrupción, lo que causa el problema que describí en el pregunta.
TRISAbits