Evitar variables globales al usar interrupciones en sistemas embebidos

13

¿Existe una buena manera de implementar la comunicación entre un ISR y el resto del programa para un sistema integrado que evite las variables globales?

Parece que el patrón general es tener una variable global que se comparte entre el ISR y el resto del programa y se usa como indicador, pero este uso de variables globales va en contra de mí. He incluido un ejemplo simple con ISR de estilo avr-libc:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

No puedo ver lo que es esencialmente un problema de alcance; ¿alguna variable accesible tanto por el ISR como por el resto del programa debe ser inherentemente global, seguramente? A pesar de esto, a menudo he visto a personas decir cosas como "las variables globales son una forma de implementar la comunicación entre los ISR y el resto del programa" (énfasis mío), lo que parece implicar que existen otros métodos; Si hay otros métodos, ¿cuáles son?


fuente
1
No es necesariamente cierto que TODO el resto del programa tenga acceso; si declaraste la variable como estática, solo el archivo en el que se declaró la variable la vería. No es nada difícil tener variables visibles en todo el archivo, pero no en el resto del programa y eso puede ayudar.
DiBosco
1
además, el indicador debe declararse volátil, porque lo está usando / cambiando fuera del flujo normal del programa. Esto obliga al compilador a no optimizar ninguna lectura / escritura para marcar, y realizar la operación de lectura / escritura real.
Next-hack el
@ next-hack Sí, eso es absolutamente correcto, lo siento, solo estaba tratando de encontrar un ejemplo rápidamente.

Respuestas:

18

Hay una forma estándar de facto de hacer esto (suponiendo que la programación C):

  • Las interrupciones / ISR son de bajo nivel y, por lo tanto, solo deben implementarse dentro del controlador relacionado con el hardware que genera la interrupción. No deben ubicarse en ningún otro lugar sino dentro de ese controlador.
  • Toda la comunicación con el ISR la realiza el conductor y solo el conductor. Si otras partes del programa necesitan acceso a esa información, tiene que solicitarla al controlador a través de funciones setter / getter o similares.
  • No debe declarar variables "globales". Variables de alcance del archivo de significado global con enlace externo. Es decir: variables que podrían recurrirse con externpalabras clave o simplemente por error.
  • En cambio, para forzar la encapsulación privada dentro del controlador, se declararán todas esas variables compartidas entre el controlador y el ISR static. Dicha variable no es global, sino que está restringida al archivo donde se declara.
  • Para evitar problemas de optimización del compilador, tales variables también deben declararse como volatile. Nota: ¡esto no da acceso atómico ni resuelve el encanto!
  • A menudo se necesita algún tipo de mecanismo de reincidencia en el controlador, en caso de que el ISR escriba en la variable. Ejemplos: interrupción desactivada, máscara de interrupción global, semáforo / mutex o lecturas atómicas garantizadas.
Lundin
fuente
Nota: puede que tenga que exponer el prototipo de la función ISR a través de un encabezado, para colocarlo en una tabla de vectores ubicada en otro archivo. Pero eso no es un problema siempre que documente que es una interrupción y que el programa no debería llamarlo.
Lundin
¿Qué diría usted si el contraargumento fuera el aumento de la sobrecarga (y el código adicional) del uso de las funciones de establecimiento / obtención? He estado revisando esto yo mismo, pensando en los estándares de código para nuestros dispositivos integrados de 8 bits.
Leroy105
2
@ Leroy105 El lenguaje C ha soportado funciones en línea por una eternidad por ahora. Aunque incluso el uso de se inlineestá volviendo obsoleto, ya que los compiladores se vuelven cada vez más inteligentes al optimizar el código. Diría que preocuparse por la sobrecarga es la "optimización pre-madura"; en la mayoría de los casos, la sobrecarga no importa, si es que incluso está presente en el código de la máquina.
Lundin
2
Dicho esto, en el caso de escribir controladores ISR, alrededor del 80-90% de todos los programadores (sin exagerar aquí) siempre tienen algo mal en ellos. El resultado son errores sutiles: indicadores borrados incorrectamente, optimización incorrecta del compilador debido a la falta de volátiles, condiciones de carrera, pésimo rendimiento en tiempo real, desbordamientos de pila, etc. aumentado aún más. Concéntrese en escribir un controlador libre de errores antes de preocuparse por cosas de interés periférico, como si el setter / getters introduce un poco de sobrecarga.
Lundin
10
este uso de variables globales va en contra de mí

Este es el verdadero problema. Superalo.

Ahora, antes de que los que se arrodillan inmediatamente griten sobre cómo esto es inmundo, déjenme calificarlo un poco. Ciertamente existe el peligro de utilizar variables globales en exceso. Pero también pueden aumentar la eficiencia, lo que a veces es importante en sistemas pequeños con recursos limitados.

La clave es pensar cuándo puede usarlos razonablemente y es poco probable que se meta en problemas, frente a un error que está esperando a suceder. Siempre hay compensaciones. Mientras que en general evitar las variables globales para la comunicación entre el código de interrupción y el primer plano es una pauta comprensible, llevarlo, como la mayoría de las pautas, al extremo de las religiones es contraproducente.

Algunos ejemplos en los que a veces uso variables globales para pasar información entre la interrupción y el código de primer plano son:

  1. Los contadores de reloj manejados por la interrupción del reloj del sistema. Por lo general, tengo una interrupción de reloj periódica que se ejecuta cada 1 ms. Eso a menudo es útil para varios tiempos en el sistema. Una forma de obtener esta información de la rutina de interrupción para que el resto del sistema pueda usarla es mantener un contador global de reloj. La rutina de interrupción incrementa el contador cada tictac del reloj. El código de primer plano puede leer el contador en cualquier momento. A menudo hago esto durante 10 ms, 100 ms e incluso tics de 1 segundo.

    Me aseguro de que los ticks de 1 ms, 10 ms y 100 ms tengan un tamaño de palabra que se pueda leer en una sola operación atómica. Si usa un lenguaje de alto nivel, asegúrese de decirle al compilador que estas variables pueden cambiar de forma asincrónica. En C, los declaras volátiles externos , por ejemplo. Por supuesto, esto es algo que se incluye en un archivo de inclusión enlatado, por lo que no necesita recordarlo para cada proyecto.

    A veces hago que el contador de tics de 1 s sea el contador de tiempo transcurrido total, así que haga que 32 bits de ancho. Eso no se puede leer en una sola operación atómica en muchos de los pequeños micro que uso, por lo que no se hace global. En cambio, se proporciona una rutina que lee el valor de varias palabras, se ocupa de posibles actualizaciones entre lecturas y devuelve el resultado.

    Por supuesto que podría haber habido rutinas para obtener los contadores de ticks más pequeños de 1 ms, 10 ms, etc. Sin embargo, eso realmente hace muy poco por usted, agrega muchas instrucciones en lugar de leer una sola palabra y usa otra ubicación de la pila de llamadas.

    ¿Cuál es el inconveniente? Supongo que alguien podría hacer un error tipográfico que accidentalmente escribe en uno de los contadores, lo que podría estropear otros tiempos en el sistema. Escribir en un mostrador deliberadamente no tendría sentido, por lo que este tipo de error tendría que ser algo involuntario como un error tipográfico. Parece muy improbable. No recuerdo que eso haya sucedido en más de 100 pequeños proyectos de microcontroladores.

  2. Filtrado final y valores A / D ajustados. Una cosa común es hacer que una rutina de interrupción maneje las lecturas de un A / D. Generalmente leo valores analógicos más rápido de lo necesario, luego aplico un poco de filtrado de paso bajo. A menudo también hay escala y desplazamiento que se aplican.

    Por ejemplo, el A / D puede estar leyendo la salida de 0 a 3 V de un divisor de voltaje para medir el suministro de 24 V. Las muchas lecturas se ejecutan a través de algún filtrado, luego se escalan para que el valor final esté en milivoltios. Si el suministro está a 24.015 V, entonces el valor final es 24015.

    El resto del sistema solo ve un valor actualizado en vivo que indica el voltaje de suministro. No sabe ni debe preocuparse cuándo se actualiza exactamente eso, especialmente porque se actualiza con mucha más frecuencia que el tiempo de establecimiento del filtro de paso bajo.

    Nuevamente, se podría usar una rutina de interfaz , pero obtienes muy poco beneficio de eso. Simplemente usar la variable global siempre que necesite el voltaje de la fuente de alimentación es mucho más simple. Recuerde que la simplicidad no es solo para la máquina, sino que eso también significa menos posibilidades de error humano.

Olin Lathrop
fuente
He estado yendo a terapia, en una semana lenta, realmente tratando de descifrar mi código. Veo el punto de Lundin sobre restringir el acceso variable, pero miro mis sistemas reales y creo que es una posibilidad tan remota que CUALQUIER PERSONA en realidad podría alterar una variable global crítica del sistema. Las funciones Getter / Setter terminan costándole gastos generales en lugar de usar un global y aceptar que estos son programas bastante simples ...
Leroy105
3
@ Leroy105 El problema no es que los "terroristas" abusen intencionalmente de la variable global. La contaminación del espacio de nombres podría ser un problema en proyectos más grandes, pero eso se puede resolver con una buena denominación. No, el verdadero problema es que el programador intenta utilizar la variable global según lo previsto, pero no lo hace correctamente. Ya sea porque no se dan cuenta del problema de condición de carrera que existe con todos los ISR, o porque arruinan la implementación del mecanismo de protección obligatorio, o simplemente porque vomitan el uso de la variable global en todo el código, creando un acoplamiento estrecho y Código ilegible.
Lundin
Sus puntos son válidos Olin, pero incluso en estos ejemplos, la sustitución extern int ticks10mscon inline int getTicks10ms()harán absolutamente ninguna diferencia en el ensamblado compilado, mientras que por otro lado se le hará difícil cambiar accidentalmente su valor en otras partes del programa, y también le permitirá a una forma de "enganchar" a esta llamada (por ejemplo, burlarse del tiempo durante la prueba de la unidad, registrar el acceso a esta variable o lo que sea). Incluso si argumenta que la posibilidad de que un programador de san cambie esta variable a cero, no hay costo de un captador en línea.
Groo
@Groo: Eso solo es cierto si está utilizando un lenguaje que admite funciones de alineación, y significa que la definición de la función getter debe ser visible para todos. En realidad, cuando uso un lenguaje de alto nivel, utilizo más funciones getter y menos variables globales. En el ensamblaje, es mucho más fácil tomar el valor de una variable global que molestarse con una función getter.
Olin Lathrop
Por supuesto, si no puede en línea, entonces la elección no es tan simple. Quería decir que con las funciones en línea (y muchos compiladores anteriores a C99 ya admitían extensiones en línea), el rendimiento no puede ser un argumento en contra de los captadores. Con un compilador de optimización razonable, debe terminar con el mismo ensamblaje producido.
Groo
2

Cualquier interrupción particular será un recurso global. A veces, sin embargo, puede ser útil que varias interrupciones compartan el mismo código. Por ejemplo, un sistema podría tener varios UART, todos los cuales deberían usar una lógica de envío / recepción similar.

Un buen enfoque para manejar eso es colocar las cosas utilizadas por el controlador de interrupción, o los punteros a ellas, en un objeto de estructura, y luego hacer que los controladores de interrupción de hardware real sean algo como:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Los objetos uart1_info, uart2_infoetc. serían variables globales, pero serían las únicas variables globales utilizadas por los manejadores de interrupciones. Todo lo demás que los manipuladores van a tocar se manejaría dentro de ellos.

Tenga en cuenta que cualquier cosa a la que acceda tanto el controlador de interrupciones como el código de la línea principal debe estar calificado volatile. Puede ser más simple declarar como volatiletodo lo que será utilizado por el controlador de interrupciones, pero si el rendimiento es importante, es posible que desee escribir código que copie información a valores temporales, los opere y luego los escriba de nuevo. Por ejemplo, en lugar de escribir:

if (foo->timer)
  foo->timer--;

escribir:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

El primer enfoque puede ser más fácil de leer y comprender, pero será menos eficiente que el último. Si eso es una preocupación dependerá de la aplicación.

Super gato
fuente
0

Aquí hay tres ideas:

Declare la variable de marca como estática para limitar el alcance a un solo archivo.

Haga que la variable de marca sea privada y use las funciones getter y setter para acceder al valor de marca.

Use un objeto de señalización como un semáforo en lugar de una variable de bandera. El ISR establecería / publicaría el semáforo.

kkrambo
fuente
0

Una interrupción (es decir, el vector que apunta a su controlador) es un recurso global. Entonces, incluso si usa alguna variable en la pila o en el montón:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

o código orientado a objetos con una función 'virtual':

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

... el primer paso debe involucrar una variable global real (o al menos estática) para alcanzar esos otros datos.

Todos estos mecanismos agregan una indirección, por lo que esto generalmente no se hace si desea exprimir el último ciclo del controlador de interrupciones.

CL.
fuente
debe declarar flag como volátil int *.
Next-hack el
0

Estoy codificando Cortex M0 / M4 en este momento y el enfoque que estamos utilizando en C ++ (no hay una etiqueta C ++, por lo que esta respuesta puede estar fuera del tema) es la siguiente:

Usamos una clase CInterruptVectorTableque contiene todas las rutinas de servicio de interrupción que se almacenan en el vector de interrupción real del controlador:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

La clase CInterruptVectorTableimplementa una abstracción de los vectores de interrupción, por lo que puede vincular diferentes funciones a los vectores de interrupción durante el tiempo de ejecución.

La interfaz de esa clase se ve así:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Debe realizar las funciones que están almacenadas en la tabla de vectores staticporque el controlador no puede proporcionar un thispuntero ya que la tabla de vectores no es un objeto. Entonces, para solucionar ese problema, tenemos el pThispuntero estático dentro del CInterruptVectorTable. Al ingresar a una de las funciones de interrupción estática, puede acceder al pThispuntero para obtener acceso a los miembros del único objeto de CInterruptVectorTable.


Ahora en el programa, puede usar el SetIsrCallbackfunctionpara proporcionar un puntero de función a una staticfunción que se llamará cuando ocurra una interrupción. Los punteros se almacenan en el InterruptVectorTable_t virtualVectorTable.

Y la implementación de una función de interrupción se ve así:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

Entonces eso llamará a un staticmétodo de otra clase (que puede ser private), que luego puede contener otro static thispuntero para obtener acceso a las variables miembro de ese objeto (solo una).

Supongo que podría construir e interactuar IInterruptHandlery almacenar punteros en los objetos, por lo que no necesita el static thispuntero en todas esas clases. (tal vez lo intentemos en la próxima iteración de nuestra arquitectura)

El otro enfoque funciona bien para nosotros, ya que los únicos objetos permitidos para implementar un controlador de interrupciones son aquellos dentro de la capa de abstracción de hardware, y generalmente solo tenemos un objeto para cada bloque de hardware, por lo que está bien trabajar con static thispunteros. Y la capa de abstracción de hardware proporciona otra abstracción a las interrupciones, llamada ICallbackque luego se implementa en la capa de dispositivo sobre el hardware.


¿Accede a datos globales? Claro que sí, pero puede hacer que la mayoría de los datos globales necesarios sean privados, como los thispunteros y las funciones de interrupción.

No es a prueba de balas, y agrega gastos generales. Tendrá dificultades para implementar una pila IO-Link con este enfoque. Pero si no está extremadamente apretado con los tiempos, esto funciona bastante bien para obtener una abstracción flexible de las interrupciones y la comunicación en los módulos sin usar variables globales que sean accesibles desde cualquier lugar.

Arsenal
fuente
1
"para que pueda vincular diferentes funciones a los vectores de interrupción durante el tiempo de ejecución" Esto suena como una mala idea. La "complejidad ciclomática" del programa simplemente iría por las nubes. Todas las combinaciones de casos de uso tendrían que probarse para que no haya conflictos de tiempo ni de uso de la pila. Mucho dolor de cabeza por una característica con muy poca utilidad IMO. (A menos que tenga un caso de cargador de arranque, esa es otra historia) En general, esto huele a meta programación.
Lundin
@Lundin Realmente no entiendo tu punto. Lo usamos para vincular, por ejemplo, la interrupción DMA al controlador de interrupciones SPI si el DMA está en uso para el SPI y al controlador de interrupciones UART si está en uso para el UART. Ambos manejadores deben ser probados, seguro, pero no es un problema. Y seguramente no tiene nada que ver con la meta programación.
Arsenal
DMA es una cosa, la asignación en tiempo de ejecución de vectores de interrupción es algo completamente diferente. Tiene sentido dejar que la configuración de un controlador DMA sea variable, en tiempo de ejecución. Una tabla de vectores, no tanto.
Lundin
@Lundin Supongo que tenemos diferentes puntos de vista sobre eso, podríamos comenzar una conversación al respecto, porque todavía no veo su problema con él, por lo que podría ser que mi respuesta esté tan mal escrita que no se entienda todo el concepto.
Arsenal