Uso de campos de bits en aplicaciones controladas por interrupciones

2

Al implementar aplicaciones controladas por interrupciones, normalmente creo un campo de bits para realizar un seguimiento de las diferentes interrupciones. Por ejemplo:

volatile struct {
    unsigned char ISR0: 1;
    unsigned char ISR1: 1;
    ...
    unsigned char ISR7: 1;
} ISRstatus;

El resto de la aplicación podría tener la siguiente estructura:

ISR(ISR0) {
    // set status flag
    ISRstatus.ISR0 = 1;
}

void main() {
    while(1) {
        if (ISRstatus.ISR0){
            // serve interrupt
            /* ... */
            // clear status flag    
            ISRstatus.ISR0 = 0;
        } /* ... */
    }
}

Recientemente encontré algunos artículos que sugerían evitar los campos de bits debido a su comportamiento inesperado en diferentes compiladores y arquitecturas.

Suponiendo que mi compilador es GCC, ¿es una mala idea utilizar este enfoque?

    
pregunta Ashton H.

7 respuestas

1

CORRECCIÓN: Como han señalado Ben (y otros comentaristas), borrar el indicador de estado en el código principal es un problema. Las escrituras en los campos de bits se implementan normalmente como una lectura-modificación-escritura, donde (en su caso) se lee el byte completo, luego se establece o se borra un bit, luego se escribe la escritura del byte modificado. En el pseudocódigo, ISRstatus.ISR0 = 0 se convertiría en:

char temp = ISRstatus;
temp &= ~0x01;
ISRstatus = temp;

El problema aquí es que una interrupción puede venir en medio de esta secuencia. Por ejemplo, digamos que el indicador ISR0 está establecido e ingresa la interrupción 5. Lo que sucede es:

<interrupt 0>
    ISRstatus |= 0x01;  //Not really atomic, but it doesn't matter here
<exit interrupt 0>

if (ISRstatus.ISR0)
{
    temp = ISRstatus;
    temp &= 0x01;
    <interrupt 5>
        ISRstatus |= 0x20;  //Not really atomic, but it doesn't matter here
    <exit interrupt 5>
    ISRstatus = temp;
}

En este ejemplo, ISRstatus debe ser igual a 0x20 después de la instrucción if, pero en cambio es igual a 0x00. La bandera ISR5 se perdió.

La forma de solucionar este problema es deshabilitar las interrupciones al escribir en la variable global en su código principal. (Las lecturas son seguras siempre y cuando toda la estructura se cargue a la vez, lo que debería ser para una estructura de 8 bits).

El estándar C no garantiza ningún pedido o empaquetamiento particular de los campos de bits. Esto significa que el uso de campos de bits para acceder a los datos almacenados en un formato específico (como los campos de registro o encabezado de paquete) no es portátil. Si solo hay un compilador para su CPU, la portabilidad no será un problema, por lo que puede salirse con la suya.

Mi lectura del estándar es que los campos de bits están diseñados para ser utilizados exactamente de la forma en que los estás utilizando. Solo ten en cuenta las limitaciones.

EDIT v2: El compilador probablemente no permitirá que un solo campo de bits cruce un límite de unidad de almacenamiento. El manual de su compilador debería tener más información, pero puede tomar un poco de prueba y error para resolver los casos de borde. Ya que todo lo que te importa es la información en los campos individuales y no su disposición dentro de la unidad de almacenamiento, esto no debería importar.

Dicho todo esto, la portabilidad generalmente no es una gran preocupación para el código de interrupción, y es poco probable que un compilador cambie la forma en que maneja los campos de bits en una versión más reciente.

    
respondido por el Adam Haun
1

De hecho, los campos de bits están muy mal especificados y el único propósito para el que puede usarlos de manera segura es para "trozos de banderas booleanas" ( vea esto para obtener ejemplos de por qué no debería usar campos de bits).

Aún así, tampoco tiene sentido usar campos de bits para ese propósito, porque hay mejores formas. En su caso:

typedef uint8_t ISRstatus;

volatile ISRstatus status;

if(status & (1 << isr_n))
{
  // flag is set
}
else
{
  // flag is not set
}

Con las optimizaciones activadas, esto debería reducirse al mismo código de la máquina (un bit check / bit set). Las ventajas de lo anterior son:

  • 100% portátil entre compiladores, microcontroladores y sistemas. El orden de los bits está garantizado, no hay relleno ni tonterías, el código se vuelve independiente de la endianess.
  • 1 << n es el estándar de facto integrado de la industria para acceder un poco en C.
  • Permite cosas más complejas como if(status & ISR_MASK_1_4) donde ISR_MASK_1_4 sería 0x0F .
respondido por el Lundin
1

Ya sea que configure y borre bits manualmente o con un campo de bits, esto podría fallar si se usa en un microcontrolador que no tiene instrucciones atómicas para configurar y borrar bits individuales.

En estas circunstancias, la configuración y el borrado de bits requerirían una lectura-modificación-escritura, que en el código principal brindaría la oportunidad de que se produjera una interrupción entre la lectura y la escritura, eliminando un bit que se acaba de establecer antes de se puede comprobar.

Entonces, usando el código anterior como base, podría ocurrir la siguiente secuencia:

  1. Interrumpe 0 disparos y establece ISR0 .
  2. El código principal comprueba ISR0 y comienza a ejecutar el código relevante.
  3. El código principal llega a la línea que borra ISR0 y lee ISRstatus en el registro.
  4. Interrumpir 1 dispara y establece ISR1 .
  5. El código principal continúa, borrando ISR0 en el registro. ISR1 ya es cero desde la lectura anterior.
  6. El valor en el registro se vuelve a escribir en ISRstatus , lo que hace que se borre ISR1 (y ISR0 ).

Si las interrupciones pueden interrumpirse entre sí, esto también puede ocurrir cuando se establece un ISR bit.

Por lo tanto, a menos que existan instrucciones para establecer y borrar atómicamente los bits (y el compilador los use), sería más seguro usar almacenamiento separado para estos valores.

Como se mencionó en un comentario a la pregunta original, sería más seguro borrar el bit al comienzo del código, no al final.

Incluso más seguro es no tener la misma variable escrita tanto por una interrupción como por el código principal. Por ejemplo, una forma común de tratar con la transmisión de datos es tener un búfer circular, con un puntero de escritura incrementado en el código principal y un puntero de lectura incrementado en la interrupción (y viceversa para la recepción de datos).

    
respondido por el Steve Melnikoff
0

una mejor implementación sería un contador activo para cada fuente de interrupción.

El controlador de interrupción incrementaría el contador activo apropiado.

El main () guardaría un valor de contador inicial. luego, durante cada ciclo, si el contador guardado no coincide con el contador activo, realice la operación y luego incremente el contador guardado ..

De esa forma no se perdería ningún evento de interrupción

Nota:

Si la interrupción se produce más rápido de lo que el controlador de interrupciones puede ejecutar y devolver, nada podría evitar que la interrupción se pierda.

    
respondido por el user3629249
0

Con avr-gcc (es decir, GCC para microcontroladores AVR de 8 bits) Espero que el campo de bits se almacene en un int . Un int es de 16 bits por defecto en avr-gcc , mientras que el tamaño de palabra que el procesador puede leer y almacenar de forma atómica es de 8 bits.

Teniendo en cuenta este caso, el uso de un valor uint8_t con las constantes #define d y las manipulaciones de bits habituales parecen ser más portátiles.

    
respondido por el ndim
0
  

Suponiendo que mi compilador es GCC, ¿es una mala idea utilizar este enfoque?

Cualquier acercamiento hecho por el hombre tendrá sus ventajas y desventajas. En este caso particular, debe pensar en los casos en que el valor de isr0 puede cambiar de manera consistente. Yo afirmaría que usted no tiene un caso aquí.

    
respondido por el dannyf
0

Muchas plataformas tienen formas de actualizar atómicamente los bits individuales dentro de un byte, pero lo logran de diferentes maneras. Aunque el Estándar permite que los campos de bits se usen dentro de volatile de almacenamiento calificado, el único contexto en el que sería útil sería cuando todas las escrituras en la estructura que contienen el campo de bits se realicen dentro del mismo "hilo" (o contexto de interrupción). Fuera de ese patrón de uso limitado, la única forma de obtener semántica conocida en la mayoría de las plataformas es utilizar la manipulación manual de bits junto con las formas específicas de las plataformas de lograr la semántica requerida.

Por ejemplo, el Cortex-M3 admite instrucciones LDREX / STREX, que se asignan a los intrínsecos del compilador. Si el código usa LDREX para leer una palabra, calcular un nuevo valor, y luego STREX para intentar escribir el nuevo valor en la misma palabra, el STREX tendrá éxito (e informará el éxito) si no se produjo ninguna interrupción después del LDREX y nada podría haber ocurrido. interrumpió la palabra en cuestión, o fallará (e informará de un error) sin escribir nada. Si el código desea establecer el bit inferior en un uint32_t volatile foo , podría hacer algo como:

void set_bit_in_foo(void)
{
  uint32_t old_value;
  do
    old_value = __LDREXW(&foo);
  while(!__STREXW(old_value | 1, &foo));
}

Si nada accede a foo entre __LDREXW y __STREXW , y no se producen interrupciones entre ellos, el nuevo valor se escribirá en foo y __STREXW devolverá 1. De lo contrario, __STREXW devolverá 1 y el código intentará nuevamente usando un valor actualizado de foo .

Otras plataformas tienen otras formas de permitir actualizaciones atómicas. C11 intenta agregar funciones para actualizaciones atómicas para plataformas que las admiten, pero está mal especificado en algunas formas clave (una implementación que pretende ofrecer soporte para operaciones atómicas no puede rechazar programas que requieren funciones que la implementación no puede admitir de manera útil , pero en su lugar debe proporcionar implementaciones rotas para esas funciones).

    
respondido por el supercat

Lea otras preguntas en las etiquetas