Manejo de interrupciones en microcontroladores y ejemplo de FSM

Pregunta inicial

Tengo una pregunta general sobre el manejo de interrupciones en microcontroladores. Estoy usando el MSP430, pero creo que la pregunta puede extenderse a otros uC. Me gustaría saber si es o no una buena práctica habilitar/deshabilitar interrupciones frecuentes a lo largo del código. Quiero decir, si tengo una parte del código que no va a ser sensible a las interrupciones (o peor aún, no debo escuchar las interrupciones, por cualquier motivo), ¿es mejor:

  • Deshabilite las interrupciones antes y luego vuelva a habilitarlas después de la sección crítica.
  • Coloque una bandera dentro de la ISR respectiva y (en lugar de deshabilitar la interrupción), configure la bandera en falso antes de la sección crítica y reiníciela en verdadero, justo después. Para evitar que se ejecute el código de la ISR.
  • Ninguno de los dos, así que se aceptan sugerencias.

Actualización: interrupciones y gráficos de estado

Proporcionaré una situación específica. Supongamos que queremos implementar un cuadro de estado, que se compone de 4 bloques:

  1. Transiciones/Efecto.
  2. Condiciones de salida.
  3. Actividad de entrada.
  4. Haz actividad.

Esto es lo que nos enseñó un profesor en la universidad. Probablemente, no sea la mejor manera de hacerlo siguiendo este esquema:

while(true) {

  /* Transitions/Effects */
  //----------------------------------------------------------------------
  next_state = current_state;

  switch (current_state)
  {
    case STATE_A:
      if(EVENT1) {next_state = STATE_C}
      if(d == THRESHOLD) {next_state = STATE_D; a++}
      break;
    case STATE_B:
      // transitions and effects
      break;
    (...)
  }

  /* Exit activity -> only performed if I leave the state for a new one */
  //----------------------------------------------------------------------
  if (next_state != current_state)
  {
    switch(current_state)
    {
      case STATE_A:
        // Exit activity of STATE_A
        break;
      case STATE_B:
        // Exit activity of STATE_B
        break;
        (...)
    }
  }

  /* Entry activity -> only performed the 1st time I enter a state */
  //----------------------------------------------------------------------
  if (next_state != current_state)
  {
    switch(next_state)
    {
      case STATE_A:
        // Entry activity of STATE_A
        break;
      case STATE_B:
        // Entry activity of STATE_B
        break;
      (...)
    }
  }

  current_state = next_state;

  /* Do activity */
  //----------------------------------------------------------------------
  switch (current_state)
  {
    case STATE_A:
      // Do activity of STATE_A
      break;
    case STATE_B:
      // Do activity of STATE_B
      break;
    (...)
  }
}

Supongamos también que desde, digamos STATE_A, quiero ser sensible a una interrupción procedente de un conjunto de botones (con sistema antirrebote, etc., etc.). Cuando alguien presiona uno de estos botones, se genera una interrupción y la bandera relacionada con el puerto de entrada se copia en una variable buttonPressed. Si el antirrebote está configurado de alguna manera a 200 ms (temporizador de vigilancia, temporizador, contador, ...) estamos seguros de que buttonPressedno se puede actualizar con un nuevo valor antes de los 200 ms. Esto es lo que te estoy preguntando (y a mí mismo :) por supuesto)

¿Necesito habilitar la interrupción en la actividad DO STATE_Ay deshabilitar antes de salir?

/* Do activity */
//-------------------------------------
switch (current_state)
{
  case STATE_A:
    // Do activity of STATE_A
    Enable_ButtonsInterrupt(); // and clear flags before it
    // Do fancy stuff and ...
    // ... wait until a button is pressed (e.g. LPM3 of the MSP430)
    // Here I have my buttonPressed flag ready!
    Disable_ButtonsInterrupt();
    break;
  case STATE_B:
    // Do activity of STATE_B
    break;
  (...)
}

De una manera que estoy seguro de que la próxima vez que ejecute el bloque 1 (transición/efectos) en la próxima iteración, estoy seguro de que las condiciones verificadas a lo largo de las transiciones no provienen de una interrupción posterior que ha sobrescrito el valor anterior de buttonPressedeso. necesidad (aunque es imposible que esto suceda porque deben transcurrir 250 ms).

Es difícil hacer una recomendación sin saber más sobre su situación. Pero a veces es necesario deshabilitar las interrupciones en los sistemas integrados. Es preferible mantener las interrupciones deshabilitadas solo por períodos cortos de tiempo para que no se pierdan las interrupciones. Puede ser posible deshabilitar solo interrupciones particulares en lugar de todas las interrupciones. No recuerdo haber usado nunca la técnica flag-inside-ISR como usted describió, así que soy escéptico sobre si esa es su mejor solución.

Respuestas (5)

La primera táctica es diseñar el firmware general para que esté bien que se produzcan interrupciones en cualquier momento. Tener que desactivar las interrupciones para que el código de primer plano pueda ejecutar una secuencia atómica debe hacerse con moderación. A menudo hay una forma arquitectónica de evitarlo.

Sin embargo, la máquina está para servirle, no al revés. Las reglas generales son solo para evitar que los malos programadores escriban código realmente malo. Es mucho mejor entender exactamente cómo funciona la máquina y luego diseñar una buena manera de aprovechar esas capacidades para realizar la tarea deseada.

Tenga en cuenta que, a menos que realmente esté limitado a los ciclos o las ubicaciones de memoria (ciertamente puede suceder), de lo contrario, desea optimizar la claridad y la capacidad de mantenimiento del código. Por ejemplo, si tiene una máquina de 16 bits que actualiza un contador de 32 bits en una interrupción de tic de reloj, debe asegurarse de que cuando el código de primer plano lea el contador, las dos mitades sean consistentes. Una forma es apagar las interrupciones, leer las dos palabras y luego volver a encenderlas. Si la latencia de interrupción no es crítica, entonces eso es perfectamente aceptable.

En el caso de que deba tener una latencia de interrupción baja, puede, por ejemplo, leer la palabra alta, leer la palabra baja, leer la palabra alta nuevamente y repetir si cambió. Esto ralentiza un poco el código de primer plano, pero no agrega ninguna latencia de interrupción. Hay varios pequeños trucos. Otra podría ser establecer un indicador en la rutina de interrupción que indique que el contador debe incrementarse y luego hacerlo en el bucle de eventos principal en el código de primer plano. Eso funciona bien si la tasa de interrupción del contador es lo suficientemente lenta como para que el ciclo de eventos haga el incremento antes de que la bandera se establezca nuevamente.

O, en lugar de una bandera, use un contador de una palabra. El código de primer plano mantiene una variable separada que contiene el último contador al que actualizó el sistema. Hace una resta sin signo del contador en vivo menos el valor guardado para determinar cuántos ticks a la vez tiene que manejar. Esto permite que el código de primer plano pierda hasta 2 N -1 eventos a la vez, donde N es el número de bits en una palabra nativa que la ALU puede manejar atómicamente.

Cada método tiene su propio conjunto de ventajas y desventajas. No hay una sola respuesta correcta. Nuevamente, comprenda cómo funciona la máquina, entonces no necesitará reglas generales.

Si necesita una sección crítica, debe asegurarse de que la operación que protege su sección crítica sea atómica y no pueda ser interrumpida.

Por lo tanto, deshabilitar las interrupciones, que generalmente se maneja con una sola instrucción de procesador (y se llama usando una función intrínseca del compilador), es una de las apuestas más seguras que puede tomar.

Dependiendo de su sistema, puede haber algunos problemas con eso, como que se pierda una interrupción. Algunos microcontroladores configuran las banderas independientemente del estado de la activación de interrupción global y, después de abandonar la sección crítica, las interrupciones se ejecutan y simplemente se retrasan. Pero si tiene una interrupción que ocurre a una velocidad alta, puede perder la segunda vez que ocurrió la interrupción si bloquea las interrupciones durante demasiado tiempo.

Si su sección crítica requiere que solo no se ejecute una interrupción, pero se debe ejecutar otra, el otro enfoque parece viable.

Me encuentro programando las rutinas de servicio de interrupción lo más cortas posible. Así que simplemente establecen una bandera que luego se verifica durante las rutinas normales del programa. Pero si haces eso, ten cuidado con las condiciones de la carrera mientras esperas a que se establezca esa bandera.

Hay muchas opciones y seguramente ninguna respuesta correcta a esto, este es un tema que requiere un diseño cuidadoso y merece un poco más de reflexión que otras cosas.

Si ha determinado que una sección de código debe ejecutarse sin interrupciones, entonces, excepto en circunstancias inusuales, debe deshabilitar las interrupciones durante la duración mínima posible para completar la tarea y volver a habilitarlas después.

Coloque una bandera dentro de la ISR respectiva y (en lugar de deshabilitar la interrupción), configure la bandera en falso antes de la sección crítica y reiníciela en verdadero, justo después. Para evitar que se ejecute el código de la ISR.

Esto aún permitiría que ocurra una interrupción, un salto de código, una verificación y luego un retorno. Si su código puede manejar tanta interrupción, entonces probablemente debería simplemente diseñar el ISR para establecer una bandera en lugar de realizar la verificación (sería más corta) y manejar la bandera en su rutina de código normal. Parece que alguien puso demasiado código en la interrupción y está usando la interrupción para realizar acciones más largas que deberían tener lugar en el código normal.

Si está tratando con un código donde las interrupciones son largas, una bandera como sugiere podría resolver el problema, pero aún así será mejor simplemente deshabilitar las interrupciones si no puede refactorizar el código para eliminar el código excesivo en la interrupción. .

El principal problema de hacerlo de la manera de la bandera es que no ejecuta la interrupción en absoluto, lo que puede tener repercusiones más adelante. La mayoría de los microcontroladores rastrearán los indicadores de interrupción incluso cuando las interrupciones estén deshabilitadas globalmente, y luego ejecutarán la interrupción cuando vuelva a habilitar las interrupciones:

  • Si no ocurrieron interrupciones durante la sección crítica, ninguna se ejecuta después.
  • Si ocurre una interrupción durante la sección crítica, se ejecuta una después.
  • Si ocurren múltiples interrupciones durante la sección crítica, solo se ejecuta una después.

Si su sistema es complejo y tiene que rastrear las interrupciones de manera más completa, tendrá que diseñar un sistema más complicado para rastrear las interrupciones y operar en consecuencia.

Sin embargo, si siempre diseña sus interrupciones para realizar el trabajo mínimo necesario para lograr su función y retrasa todo lo demás hasta el procesamiento regular, entonces las interrupciones rara vez afectarán negativamente a su otro código. Haga que la interrupción capture o libere datos si es necesario, o establezca/restablezca las salidas, etc., según sea necesario, luego haga que la ruta del código principal preste atención a las banderas, los búferes y las variables a las que afecta la interrupción para que el procesamiento prolongado se pueda realizar en el bucle principal. en lugar de la interrupción.

Esto debería eliminar todas las situaciones, excepto muy, muy pocas, en las que podría necesitar una sección de código ininterrumpida.

He actualizado la publicación para explicar mejor la situación en la que estoy trabajando :)
En su código de ejemplo, consideraría deshabilitar la interrupción del botón específico cuando no sea necesario y habilitarla cuando sea necesario. Hacer esto con frecuencia no es un problema por su diseño. Deje las interrupciones globales activadas para que pueda agregar otras interrupciones al código más tarde si es necesario. Alternativamente, simplemente restablezca la bandera cuando vaya al estado A y, de lo contrario, ignórela. Si se presiona el botón y se establece la bandera, ¿a quién le importa? Ignóralo hasta que vuelvas al estado A.
¡Sí! Esa puede ser una solución porque en el diseño real, con frecuencia entro en LPM3 (MSP430) con interrupciones globales habilitadas y salgo de LPM3, reanudando la ejecución, tan pronto como se detecta una interrupción. Entonces, una solución es la que presentó, que creo que se informa en la segunda parte del código: habilite las interrupciones tan pronto como comience a realizar la actividad de un estado que lo necesita y desactívelas antes de ir al bloque de transiciones. ¿Puede otra posible solución deshabilitar la interrupción justo antes de salir del "Bloque de actividad" y volver a habilitarla en algún momento (¿cuándo?) después?

Poner una bandera dentro de la ISR como usted describe probablemente no funcionará ya que básicamente está ignorando el evento que desencadenó la interrupción. Deshabilitar las interrupciones globalmente suele ser una mejor opción. Como han dicho otros, no debería tener que hacer esto muy a menudo. Tenga en cuenta que cualquier lectura o escritura que se realice a través de una sola instrucción no debería necesitar protección, ya que la instrucción se ejecuta o no.

Mucho depende del tipo de recursos que intente compartir. Si está alimentando datos del ISR al programa principal (o viceversa), podría implementar algo como un búfer FIFO. La única operación atómica sería actualizar los punteros de lectura y escritura, lo que minimiza la cantidad de tiempo que pasa con las interrupciones deshabilitadas.

Hay una sutil diferencia que debes tener en cuenta. Puede optar por "retrasar" el manejo de una interrupción o "ignorar" e interrumpir.

A menudo decimos en el código que deshabilitamos la interrupción. Lo que probablemente sucederá, debido a las funciones del hardware, es que una vez que habilitamos las interrupciones, se activará. Esto en cierto modo está retrasando la interrupción. Para que el sistema funcione de manera confiable, necesitamos saber el tiempo máximo que podemos demorar el manejo de estas interrupciones. Y luego asegúrese de que todos los casos en los que las interrupciones estén deshabilitadas se terminarán en un tiempo más corto.

A veces queremos ignorar las interrupciones. La mejor manera podría ser bloquear la interrupción a nivel de hardware. A menudo hay un controlador de interrupciones o similar donde podríamos decir qué entradas deberían generar interrupciones.