6 votos

¿Es una mala idea desactivar las interrupciones sensibles al tiempo en C?

A veces veo código que desactiva las interrupciones, por ejemplo para realizar una lectura/escritura no atómica en una variable global utilizada en un ISR. En AVR con gcc, esto puede parecer:

ExpensiveOperation();
cli();
// Perform a non-atomic read/write
sei(); // Assume that it is acceptable to enable global interrupts here.

Las macros cli y sei se expanden a declaraciones volátiles asm con barreras de memoria. La palabra clave volátil asegura que las instrucciones cli/sei no se optimicen, mientras que las barreras de memoria aseguran que si la lectura/escritura no atómica es en una variable volátil, se producirá entre las instrucciones cli y sei. Sin embargo, esta página sugiere que nada impide al compilador poner ExpensiveOperation después de la instrucción cli.

Las interrupciones suelen requerir una sincronización precisa. Si al deshabilitar las interrupciones en C se corre el riesgo de que se ejecuten operaciones costosas con las interrupciones deshabilitadas, ¿deberían deshabilitarse las interrupciones críticas para la temporización sólo en ensamblador en línea (o debería reescribirse el programa para utilizar sólo lecturas/escrituras atómicas)?

0 votos

¿Por qué cree que no se pueden realizar operaciones costosas en el montaje en línea?

4 votos

@IgnacioVazquez-Abrams: El objetivo es asegurar que dado algo como cli(); counter++; sei(); El código generado sólo desactivará las interrupciones durante unos pocos ciclos de instrucción. Si el compilador reordena el código para cli(); ExpensiveOperation(); counter++; sei(); y costosa operación tardara muchos milisegundos en completarse, eso podría ser desastroso.

0 votos

@IgnacioVazquez-Abrams: Desgraciadamente, el diseño de C nunca ha incluido ninguna característica que impida a un optimizador hacer esas cosas porque en el momento en que se diseñó C no se esperaba que los compiladores hicieran esas cosas, independientemente de que una directiva lo prohibiera.

3voto

Neil Foley Puntos 1313

El problema principal aquí es que este compilador aparentemente decidió reordenar las instrucciones de ensamblador en línea como si fueran cualquier código C, sin tener ni idea de lo que hacen esas instrucciones. Así que, aunque esas instrucciones no accedan a ninguna variable de nivel C, ¡acceden al registro de código de condición fundamental!

El compilador no puede reordenar explícitamente las instrucciones si esto afecta al comportamiento del programa, y punto. Y es bastante difícil encontrar algo que afecte al comportamiento del programa más que escribir en el registro más fundamental del núcleo de la CPU.

Así que este es un comportamiento sin sentido y sólo puede ser considerado como un error del compilador, probablemente en el puerto AVR.


Una posible solución a este error del compilador podría ser introducir un efecto secundario, tal y como especifica el estándar C, antes de la llamada a cli(). Una forma de hacerlo es implementar ExpensiveOperation() como una función C real. Tenga en cuenta que en el ejemplo enlazado, la operación cara no era una función. Otra forma sería escribir un código ficticio como

ExpensiveOperation();
volatile uint8_t invoke_side_effect = 0;
cli();

Por lo demás, el procedimiento estándar cuando se encuentra un optimizador con errores es desactivar ese optimizador. Si puedes encontrar la opción concreta que permite al compilador reordenar las instrucciones y desactivarla, es lo mejor que puedes hacer.

2voto

Alex Andronov Puntos 178

Dependiendo del tipo de efectos secundarios ExpensiveOperation() tiene, puede ser ilegítimo para un compilador moverlo más allá de una llamada a una función que el compilador no puede "ver" en si, desde el punto de vista del compilador, habría una posibilidad de que dicha función haga uso de tales efectos secundarios. Sería útil que hubiera una función estándar, típicamente implementada por un intrínseco, que en realidad no hiciera nada, pero que un compilador tuviera que tratar como si pudiera hacer algo. Los compiladores que tratasen dicha función como intrínseca podrían evitar tener que generar una instrucción de "llamada" inútil [o cualquier otro código] para ella, más allá del hecho necesario para garantizar que cualquier variable global que se almacene en caché en los registros se vacíe antes de la "llamada".

Desafortunadamente, mientras que llamar a una función externa probablemente obligaría a un compilador a procesar ExpensiveOperation() en su totalidad primero, no conozco nada que elimine al 100% la posibilidad de que un compilador identifique partes de esa función cuya ejecución pueda ser diferida. Aun así, añadir un efecto secundario secuenciado a ExpensiveOperation() y llamar a una función externa que requiera que se haya completado cualquier efecto secundario de este tipo puede ser lo mejor que se puede esperar.

1voto

Al pacino Puntos 415

Puedes intentar poner la secuencia cli()/write/sei() en su propia función. He encontrado que los optimizadores tienden a ser reacios a mover el código a través de las funciones, especialmente cuando se optimiza para el tamaño. Sin embargo, no estoy seguro sobre AVR-GCC específicamente.

0 votos

Si la optimización en tiempo de enlace está activada (y es posible que algún día algunos compiladores la activen por defecto), es menos probable que esta estrategia funcione. Al menos en gcc, parece que la única manera de garantizar que no se produzca el intercambio es hacerlo todo en ensamblaje en línea. El compilador mencionado en este puesto parece ser capaz de hacerlo en C, así que no todos los compiladores tienen este problema.

0 votos

Sí, sospecho que la verdadera solución es utilizar un compilador diseñado para la programación embebida y dejar las optimizaciones agresivas desactivadas.

1voto

Kiran Puntos 320

Hay varias cuestiones aquí:

  1. AFAIK, sólo hay un bit para controlar las interrupciones en un AVR. Así que la discusión sobre un control más preciso de las interrupciones no se aplica a esta CPU.

  2. El ejemplo ilustra un fuerte caso para buscar en el ensamblador el código generado por el compilador.
    Debería ser práctico detectar todos los usos de cli a sei con un pequeño script de edición aplicado a la salida de objdump (para que sea fácil de controlar). Un script basado en el programa podría resaltar todos los ejemplos de "llamada" entre ellos.
    Esto podría mostrar que no hay ningún problema que resolver en su código.

  3. El control de las interrupciones mediante cli()/sei() es la forma más fácil para un desarrollador de asegurar el acceso atómico, y por lo tanto mantener la consistencia de la memoria, para los valores multibyte en un AVR.
    Tener que escribir en ensamblador puede ser tan propenso a los errores, o tener un impacto negativo en la optimización, que es un enfoque pobre. Yo no lo haría hasta que el resultado del paso 1 demuestre que hay un problema que resolver.

  4. El ejemplo muestra que el compilador ha generado código para la división de enteros insertando una llamada a una subrutina para val = 65535U / val y esa llamada es el código que se ha "optimizado" entre cli() y sei().
    Sin embargo, lo hace no probar que las subrutinas generadas por el usuario se mueven en el código entre cli() y sei(). Así que esto puede no ser un problema para el caso, potencialmente, mucho más significativo.

  5. Este ejemplo se soluciona introduciendo un nuevo volátil variable:

    #define cli() __asm volatile( "cli" ::: "memory" )
    #define sei() __asm volatile( "sei" ::: "memory" )
    unsigned int ivar;  
    void test2( unsigned int val ) {  
       volatile unsigned int val1 = 65535U / val;
       cli();
       ivar = val1;
       sei();
    }

El código generado se convirtió en:

  92:   cf 93           push    r28
  94:   df 93           push    r29
  96:   00 d0           rcall   .+0         ; 0x98 <_Z5test2j+0x6>
  98:   cd b7           in  r28, 0x3d   ; 61
  9a:   de b7           in  r29, 0x3e   ; 62
  9c:   bc 01           movw    r22, r24
  9e:   8f ef           ldi r24, 0xFF   ; 255
  a0:   9f ef           ldi r25, 0xFF   ; 255
  a2:   0e 94 fb 00     call    0x1f6   ; 0x1f6 <__udivmodhi4>
  a6:   7a 83           std Y+2, r23    ; 0x02
  a8:   69 83           std Y+1, r22    ; 0x01
  aa:   f8 94           cli  <----------------------- switch off interrupts
  ac:   89 81           ldd r24, Y+1    ; 0x01
  ae:   9a 81           ldd r25, Y+2    ; 0x02
  b0:   90 93 01 01     sts 0x0101, r25
  b4:   80 93 00 01     sts 0x0100, r24
  b8:   78 94           sei  <----------------------- switch on interrupts
  ba:   0f 90           pop r0
  bc:   0f 90           pop r0
  be:   df 91           pop r29
  c0:   cf 91           pop r28
  c2:   08 95           ret

Esto no es bonito, pero es relativamente fácil, y puede ser suficiente. Por supuesto, evita las optimizaciones prematuras; no hagas cambios sutiles para controlar el compilador hasta que sea importante, y preferiblemente después de que el código esté funcionando y sea estable.

Yo revisaría el código generado, y esperaría a ver un problema, para luego resolver ese caso específico.

Creo que levantaría un informe de error si mi código a nivel de usuario se mueve entre el cli()/sei(). Eso daría a los desarrolladores del compilador la oportunidad de identificar soluciones o desarrollar correcciones. Los desarrolladores de compiladores son libres de inventar soluciones, a menudo utilizando pragmas, y podrían responder a un informe de error ofreciendo una solución sólida.

Mientras tanto, sería más fácil continuar por el camino fácil, en lugar de dificultar el desarrollo, hasta que haya pruebas de un problema significativo.

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X