Condición de carrera de sueño del microcontrolador

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 dormir, 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 despierte si no se reciben interrupciones posteriores.
  • 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 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 PIC18s, no tienen este bit, y el problema sigue ocurriendo. Sin embargo, estos micros están diseñados de tal manera que las interrupciones aún pueden activar el núcleo, independientemente de si el bit de activació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 irse a dormir. Si se activa una interrupción justo antes de ejecutar la instrucción de suspensión, el controlador de interrupciones no se ejecutará, el núcleo se reactivará y, una vez que se vuelvan a habilitar las interrupciones globales, se ejecutará el controlador de interrupciones. 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
    }
}
¿Es esta una pregunta práctica o teórica?
en teoría, usa un temporizador que lo despierta una vez cada (ingrese un valor aceptable) ms y luego vuelve a dormir si no es necesario hacer nada.
Podría borrar_interrupt_flag() justo antes de dormir, pero si puede manejar la latencia, la respuesta de Grady probablemente sea la mejor.
Lo haría interrupt_flagcomo un int, y lo incrementaría cada vez que haya una interrupción. Luego cambie el if(has_flag)a while (interrupts_count)y luego duerma. No obstante, la interrupción podría ocurrir después de haber salido del ciclo while. Si esto es un problema, ¿realizar el procesamiento en la interrupción misma?
Bueno, depende del micro que esté ejecutando. la interrupción con una pequeña latencia. Pero también sería una gran solución usar un temporizador para despertarse a un intervalo igual o menor a su latencia máxima.
@angelatlarge: He considerado usar un conteo, pero nuevamente ocurre el mismo problema cuando el conteo llega a cero. El procesamiento dentro de la interrupción es una opción siempre que el proceso () sea relativamente pequeño o no haya requisitos de latencia de interrupción. Si la interrupción necesita ejecutarse lo más rápido posible, esta tampoco es una opción.
@kenny: No estoy seguro de cómo funcionaría esto. El micro todavía iría a dormir, que es el problema.
@AndrejaKo: Esta es una pregunta práctica que he encontrado en el campo pero nunca encontré una solución limpia.
@Kvegaoro: ¡Ajá! La solución que proporciona SÍ funciona en un ATmega328. Sin embargo, no todos los micros proporcionan este bit de activación de suspensión (p. ej., pic18). Me pregunto si habría una manera de emular el comportamiento en un PIC18 (o si tendría algún soporte de hardware externo para que esto suceda).
@TRISAbits: en el PIC 18x, el enfoque que describí en mi respuesta funciona bien (es mi diseño normal cuando uso esa parte).

Respuestas (3)

Suele haber algún tipo de soporte de hardware para este caso. Por ejemplo, la instrucción de los AVR seipara habilitar las interrupciones difiere la habilitación 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

En este caso, la interrupción que se habría perdido en el ejemplo se retrasaría hasta que el procesador complete su secuencia de suspensión.

¡Gran respuesta! El algoritmo que proporciona realmente funciona muy bien en un AVR. Gracias por la sugerencia.

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 las solicitudes de interrupción. Muchos microcontroladores saldrán del modo de suspensión si una solicitud de interrupción llega al núcleo, ya sea que el núcleo esté dispuesto a aceptarla o no.

En un diseño de este tipo, un enfoque simple para lograr un comportamiento de suspensión confiable es hacer que la verificación del bucle principal borre una bandera y luego verifique si conoce alguna razón por la cual el procesador debería estar activo. Cualquier interrupción que ocurra durante ese tiempo que pueda afectar cualquiera de esas razones debe establecer la bandera. Si el bucle principal no encontró ninguna causa para permanecer despierto, y si la bandera no está configurada, el bucle principal debería deshabilitar las interrupciones y verificar la bandera nuevamente [quizás después de un par de instrucciones NOP si es posible que una interrupción quede pendiente durante una instrucción de inhabilitación-interrupción podría procesarse después de que ya se haya realizado la obtención del operando asociado con la siguiente instrucción]. Si la bandera aún no está configurada, vete a dormir.

En este escenario, una interrupción que ocurra antes de que el bucle principal deshabilite las interrupciones establecerá la bandera antes de la prueba final. Una interrupción que pasa a estar pendiente demasiado tarde para ser atendida antes de la instrucción de suspensión evitará que el procesador entre en suspensión. Ambas situaciones están bien.

Sleep-on-Exit a veces es un buen modelo para usar, pero no todas las aplicaciones realmente "encajan" en él. Por ejemplo, un dispositivo con una pantalla LCD de bajo consumo podría programarse más fácilmente con un código que se parece a:

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 presiona ningún botón y no sucede nada más, no hay razón para que el sistema no deba ir a 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 la máquina de estado, el código como el anterior suele ser la forma más lógica de manejar los flujos de interfaz de usuario altamente modal típicos de los microcontroladores pequeños.

Gracias supercat por la gran respuesta. Deshabilitar las interrupciones y luego pasar 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á configurado/borrado. Eché un vistazo al esquema de hardware de interrupción PIC18, y esta solución funcionaría.

Programe el micro para que se despierte en caso de 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
    }
}
La arquitectura Wake-on-interrupt se asume en la pregunta. No creo que tu respuesta resuelva la pregunta/problema.
@angelatlarge Punto aceptado. He agregado un ejemplo de código que creo que ayuda.
@jwygralak67: Gracias por la sugerencia, pero el código que proporciona simplemente traslada el problema a la rutina del proceso(), que ahora tiene que comprobar si se produjo la interrupción antes de ejecutar el cuerpo del proceso().
Si no se hubiera producido la interrupción, ¿por qué estamos despiertos?
@JRobert: Podríamos estar despiertos por una interrupción anterior, completar la rutina del proceso (), y cuando terminemos la prueba if(has_flag) y justo antes de dormir, obtendremos otra interrupción, lo que causa el problema que he descrito en el pregunta.
Creo que el punto es que cuando ingresa al proceso (), ha salido del modo de suspensión (), por lo que debe haber ocurrido una interrupción muy recientemente. (Y no puede ocurrir nada más hasta que vuelva a habilitar ints con eint o reti). Para muchos casos, eso es lo suficientemente bueno. Con múltiples fuentes int y sin relación temporal entre ellas, es más complejo, por supuesto.
@BrianDrummond: Ya veo. Sí, si el sistema tiene una sola fuente de interrupción, entonces esta solución funciona bien. Si tiene múltiples fuentes de interrupción, esta solución se vuelve un poco más compleja.
... tiene una sola fuente de interrupción y siempre puede procesarla antes de que se repita.