Evitar las variables globales al usar interrupciones en sistemas integrados

9

¿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 una bandera, pero este uso de las variables globales va en contra del grano. He incluido un ejemplo simple utilizando los 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; cualquier variable accesible tanto por el ISR como por el resto del programa debe ser inherentemente global, ¿no? 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 hay otros metodos Si hay otros métodos, ¿cuáles son?

    
pregunta p0llard

6 respuestas

12

Hay una manera estándar de facto para hacer esto (asumiendo la programación en 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 que no esté dentro de ese controlador.
  • Toda la comunicación con el ISR se realiza solo por el conductor y el conductor. Si otras partes del programa necesitan acceso a esa información, tiene que solicitarla al conductor a través de las funciones de establecimiento / obtención o similares.
  • No debe declarar variables "globales". Significado global de variables de alcance de archivo con enlace externo. Es decir: variables a las que se podría recurrir con la palabra clave extern o simplemente por error.
  • En cambio, para forzar la encapsulación privada dentro del controlador, todas las variables compartidas entre el controlador y el ISR se declararán static . Dicha variable es no global, pero está restringida al archivo donde se declara.
  • Para evitar problemas de optimización del compilador, dichas variables también deben declararse como volatile . Nota: ¡esto no da acceso atómico ni resuelve la re-entrada!
  • A menudo se necesita algún tipo de mecanismo de reingreso en el controlador, en caso de que el ISR escriba en la variable. Ejemplos: desactivación de interrupción, máscara de interrupción global, semáforo / mutex o lecturas atómicas garantizadas.
respondido por el Lundin
5
este uso de variables globales va en contra del grano para mí

Este es el verdadero problema. Supéralo.

Ahora, antes de que los tirones de las rodillas se quejen de inmediato acerca de cómo esto es impuro, permítanme calificar eso un poco. Ciertamente hay peligro en el uso de 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 puedes usarlos razonablemente y es poco probable que te metas en problemas, en lugar de un error que está esperando que suceda. Siempre hay compensaciones. Si bien generalmente evitar las variables globales para la comunicación entre código de interrupción y primer plano es una guía comprensible, llevarla, como la mayoría de las otras pautas, a una religión extrema es contraproducente.

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

  1. Contadores de tick del reloj administrados por la interrupción del reloj del sistema. Normalmente tengo una interrupción de reloj periódica que se ejecuta cada 1 ms. Eso es a menudo útil para varios tiempos en el sistema. Una forma de sacar esta información de la rutina de interrupción al lugar donde el resto del sistema puede usarla es mantener un contador de tics del reloj global. La rutina de interrupción incrementa el contador cada tic 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 1 segundo tics.

    Me aseguro de que los tramos de 1 ms, 10 ms y 100 ms tengan un tamaño de palabra que pueda leerse en una sola operación atómica. Si utiliza un lenguaje de alto nivel, asegúrese de decirle al compilador que estas variables pueden cambiar de forma asíncrona. En C, los declara extern volatile , por ejemplo. Por supuesto, esto es algo que se incluye en un archivo de inclusión enlatado, por lo que no necesita recordar eso para cada proyecto.

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

    Por supuesto, 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 ti, agrega muchas instrucciones en lugar de leer una sola palabra y usa otra ubicación de pila de llamadas.

    ¿Cuál es la desventaja? Supongo que alguien podría hacer un error tipográfico que accidentalmente escribe en uno de los contadores, lo que luego podría desordenar otros tiempos en el sistema. Escribir en un contador deliberadamente no tendría sentido, por lo que este tipo de error tendría que ser algo no intencional como un error tipográfico. Parece muy improbable. No recuerdo que alguna vez haya ocurrido en más de 100 pequeños proyectos de microcontroladores.

  2. Valores A / D finales filtrados y ajustados. Una cosa común a hacer es tener una rutina de interrupción que maneja las lecturas de un A / D. Normalmente 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 compensación 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 algunos filtros, y luego se escalan para que el valor final esté en milivoltios. Si el suministro es a 24.015 V, entonces el valor final es 24015.

    El resto del sistema solo ve un valor actualizado en vivo que indica la tensión de alimentación. No sabe ni tiene que preocuparse de cuándo se actualiza exactamente eso, especialmente porque se actualiza con mucha más frecuencia que el tiempo de ajuste del filtro de paso bajo.

    Una vez más, se podría usar una rutina de interfaz , pero se obtiene muy poco beneficio de eso. El uso de 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 más simple también significa menos posibilidades de error humano.

respondido por el Olin Lathrop
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 puede tener varios UART, todos los cuales deberían usar una lógica de envío / recepción similar.

Un buen enfoque para manejar es colocar las cosas utilizadas por el controlador de interrupción, o los punteros a ellos, en un objeto de estructura, y luego hacer que los controladores de interrupción de hardware sean 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_info , etc. serían variables globales, pero serían las variables globales únicas utilizadas por los manejadores de interrupciones. Todo lo demás que los manejadores tocarán se manejará dentro de esos.

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

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

escribe:

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 segundo. Si eso es una preocupación dependería de la aplicación.

    
respondido por el supercat
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 la bandera sea privada y use las funciones de obtención y configuración para acceder al valor de la bandera.

Utilice 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.

    
respondido por el kkrambo
0

Una interrupción (es decir, el vector que apunta a su controlador) es un recurso global. Entonces, incluso si usas 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 incluir una variable global real (o al menos estática) para alcanzar esos otros datos.

Todos estos mecanismos agregan un direccionamiento indirecto, por lo que generalmente esto no se hace si desea eliminar el último ciclo del controlador de interrupciones.

    
respondido por el CL.
0

Estoy codificando para Cortex M0 / M4 en este momento y el enfoque que estamos usando en C ++ (no hay una etiqueta de C ++, por lo que esta respuesta podría estar fuera de tema) es la siguiente:

Utilizamos una clase CInterruptVectorTable que 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 CInterruptVectorTable implementa 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 static porque el controlador no puede proporcionar un puntero this ya que la tabla de vectores no es un objeto. Entonces, para solucionar ese problema, tenemos el punto estático pThis dentro del CInterruptVectorTable . Al ingresar a una de las funciones de interrupción estática, puede acceder al puntero pThis para obtener acceso a los miembros del único objeto de CInterruptVectorTable .

Ahora en el programa, puede usar SetIsrCallbackfunction para proporcionar un puntero a una función static 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, se llamará al método static de otra clase (que puede ser private ), que luego puede contener otro static this -pointer para obtener acceso a las variables miembro de ese objeto (solo una ).

Supongo que podría crear e interactuar como IInterruptHandler y almacenar punteros a objetos, por lo que no necesita el puntero static this 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 que pueden implementar un controlador de interrupciones son aquellos que se encuentran 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 this -puntos. Y la capa de abstracción del hardware proporciona otra abstracción a las interrupciones, denominada ICallback , que luego se implementa en la capa del dispositivo sobre el hardware.

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

No es a prueba de balas, y agrega gastos generales. Tendrá dificultades para implementar una pila IO-Link utilizando este enfoque. Pero si no estás muy ajustado a los tiempos, esto funciona bastante bien para obtener una abstracción flexible de interrupciones y comunicación en módulos sin utilizar variables globales a las que se pueda acceder desde cualquier lugar.

    
respondido por el Arsenal

Lea otras preguntas en las etiquetas