Leyendo una variable volátil multibyte que se actualiza en un ISR

5

Considere el código Arduino al final de este post para obtener una referencia completa que hace que LED13 parpadee en un ritmo de 0.5Hz impulsado por los temporizadores. Es solo una prueba de concepto, sé que se puede mejorar en varios aspectos, pero nos centramos en el siguiente problema. Aunque en el PoC a continuación solo uso bit0, el objetivo de mi PoC es encontrar una forma sólida para acceder al entero entero de 32 bits fuera del ISR. El sniplet de fuente discutido es este:

void loop() {
  digitalWrite( led , secondCounter & 1 );
}

El ISR aumenta el secondsCounter cada segundo y el led sigue bit0 de ese contador, haciéndolo parpadear.

El listado de desmontaje resultante para el bucle se ve así:

000002ae <loop>:
 2ae:   60 91 00 02     lds     r22, 0x0200     ; Read secondCounter from memory
 2b2:   70 91 01 02     lds     r23, 0x0201     ; 4 bytes, 32-vit integer
 2b6:   80 91 02 02     lds     r24, 0x0202
 2ba:   90 91 03 02     lds     r25, 0x0203
 2be:   61 70           andi    r22, 0x01       ; 1
 2c0:   77 27           eor     r23, r23
 2c2:   88 27           eor     r24, r24
 2c4:   99 27           eor     r25, r25
 2c6:   8d e0           ldi     r24, 0x0D       ; 13
 2c8:   0c 94 0a 02     jmp     0x414           ; 0x414 <digitalWrite>

Observe que, en segundo plano, las interrupciones de eventos del temporizador se activan todo el tiempo y existe la posibilidad de que una interrupción se repare mientras se lee de la memoria r22-r25. Esto puede dar como resultado que un valor entero dañado se lea de la memoria, pero se puede arreglar fácilmente deshabilitando las interrupciones mientras se lee la variable de la memoria:

void loop() {
  noInterrupts();                               // Prevent secondCounter from being updated in ISR while being read from memory in main loop.
  digitalWrite( led , secondCounter & 1 );
  interrupts();                                 // Enable interrupts 
}

Lo que resulta en:

000002ae <loop>:
 2ae:   f8 94           cli                     ; Disable interrupts
 2b0:   60 91 00 02     lds     r22, 0x0200
 2b4:   70 91 01 02     lds     r23, 0x0201
 2b8:   80 91 02 02     lds     r24, 0x0202
 2bc:   90 91 03 02     lds     r25, 0x0203
 2c0:   61 70           andi    r22, 0x01       ; 1
 2c2:   77 27           eor     r23, r23
 2c4:   88 27           eor     r24, r24
 2c6:   99 27           eor     r25, r25
 2c8:   8d e0           ldi     r24, 0x0D       ; 13
 2ca:   0e 94 0d 02     call    0x41a           ; 0x41a <digitalWrite>
 2ce:   78 94           sei                     ; Enable interrupts
 2d0:   08 95           ret

Pero ahora toda la rutina digitalWrite (que lleva un tiempo relativamente largo) se ejecuta mientras las interrupciones están deshabilitadas y las interrupciones pendientes tienen que esperar mucho tiempo antes de ser reparadas.

Una solución parece ser el uso de una variable ficticia:

void loop() {
  noInterrupts();                                     // Prevent secondCounter from being updated in ISR while being read from memory in main loop.
  uint32_t seconds = secondCounter;
  interrupts();                                       // Enable interrupts 
  digitalWrite( led , seconds & 1 );
}

Lo que resulta en un ensamblaje agradable y limpio:

000002ae <loop>:
 2ae:   f8 94           cli                     ; Disable interrupts
 2b0:   60 91 00 02     lds     r22, 0x0200     ; Read variable from memory
 2b4:   70 91 01 02     lds     r23, 0x0201
 2b8:   80 91 02 02     lds     r24, 0x0202
 2bc:   90 91 03 02     lds     r25, 0x0203
 2c0:   78 94           sei                     ; Enable interrupts
 2c2:   61 70           andi    r22, 0x01       ; Do other stuff
 2c4:   77 27           eor     r23, r23
 2c6:   88 27           eor     r24, r24
 2c8:   99 27           eor     r25, r25
 2ca:   8d e0           ldi     r24, 0x0D       ; 13
 2cc:   0c 94 0c 02     jmp     0x418   ; 0x418 <digitalWrite>

Pregunta : el uso de una variable temporal adicional parece engorroso, ¿existe una solución más inteligente y ordenada para esto?

Intento fallido : intenté crear una función como:

uint32_t atomic_int32( uint32 var ) {
  noInterrupts();
  uint32_t tmp = var;
  interrupts();
  return tmp;
}

Combinado con:

  digitalWrite( led , atomic_int32( secondCounter ) & 1 );

Pero el compilador 'optimiza' la lectura de la variable de la memoria fuera de las instrucciones cli y sei . En realidad, no hay nada entre esas instrucciones:

000002ae <loop>:
 2ae:   80 91 00 02     lds     r24, 0x0200     ; Read variable from memory
 2b2:   90 91 01 02     lds     r25, 0x0201
 2b6:   a0 91 02 02     lds     r26, 0x0202
 2ba:   b0 91 03 02     lds     r27, 0x0203
 2be:   f8 94           cli                     ; Disable interrupts
 2c0:   78 94           sei                     ; Enable interrupts
 2c2:   60 e0           ldi     r22, 0x00       ; 0
 2c4:   8d e0           ldi     r24, 0x0D       ; 13
 2c6:   0c 94 09 02     jmp     0x412           ; 0x412 <digitalWrite>

Código completo

/*
 * (c) J.P. Hendrix
 *
 * http://blog.linformatronics.nl/213/electronics/timed-1-millisecond-interrupt-routine-for-arduino
 * 
 * Timed interrupt using Timer2
 * ISR is called every 1ms
 *
 * The LED on pin13 will blink in a 0.5Hz rhythm
 */

#define _BC(bit) ( 0 << ( bit ) )
#define _BS(bit) ( 1 << ( bit ) )

// Pin 13 has an LED connected on most Arduino boards.
const uint8_t led = 13;
volatile uint16_t millisecondCounter = 0;
volatile uint32_t secondCounter = 0;

void setup() {                
  // initialize the digital pin as an output.
  pinMode( led , OUTPUT);

  // Set up Timer2 for 1 ms interrupts
  // - Arduino is clocked at 16MHz
  // - The timer is clocked through a /128 prescaler, thus 125kHz (TCCR2)
  // - The interrupt is generated when the counter hits 125 (OCR2A)
  //    at which moment the Timer is reset to 0, resulting in 1kHz intervals
  TCCR2A = _BC( COM2A1 ) | _BC( COM2A0 ) |              // Normal port operation, OC2A disconnected
           _BC( COM2B1 ) | _BC( COM2B0 ) |              // Normal port operation, OC2B disconnected 
           _BS( WGM21 )  | _BC( WGM20 );                // Clear timer on compare match

  TCCR2B = _BC( FOC2A )  | _BC( FOC2B )  |
           _BC( WGM22 )  |                              // Clear timer on compare match
           _BS( CS22 )   | _BC( CS21  )  | _BS( CS20 ); // prescaler f = clk2 / 128

  OCR2A  = 125 - 1;                                         // 16MHz / 128 = 125kHz => 125kHz/125 = 1kHz
  TCNT2  = 0; 
  TIMSK2 = _BC( OCIE2B ) | _BS( OCIE2A ) | _BC( TOIE2 );// Enable compare match A interrupts
  sei();                                                // Enable global interrupts
}

// Attach interrupt routine to the Timer Compare Interrupt
ISR( TIMER2_COMPA_vect ) {
  millisecondCounter += 1;
  if ( millisecondCounter == 1000 ) {                   // 1000 milliseconds equals 1 second
    secondCounter += 1;
    millisecondCounter = 0;
  }
};

void loop() {
    digitalWrite( led , secondCounter & 1 );
}
    
pregunta jippie

3 respuestas

2

En primer lugar, te preocupas por un problema. Dado que la operación solo depende del valor del LSB del primer byte de la variable de 32 bits, el hecho de que los otros bytes cambien o no mientras se ejecuta ese tramo de código en particular no tiene ningún efecto.

Pero en el caso general, su función atomic_int32() falla porque es el autor de la llamada quien lee la variable asociada con el argumento formal var antes de que se llame a la función , mientras construye lista de argumentos en la pila. Para solucionar esto, y realmente leer la variable dentro de la función mientras las interrupciones están desactivadas, debe pasar la dirección de la variable a la función, como esto:

uint32_t atomic_int32 (volatile uint32_t *var)
{
  noInterrupts();
  uint32_t tmp = *var;
  interrupts();
  return tmp;
}

Llámalo así:

digitalWrite (led, atomic_int32 (&secondCounter) & 1);
    
respondido por el Dave Tweed
2

Si no quiere apagar las interrupciones, considere:

uint32_t atomic_int32 (uint32_t *var)
{
  uint32_t ret;

  do {
     ret = *(volatile uint32_t *)var;
  } while (ret != *(volatile uint32_t *)var);

  return ret;
}

El listado de ensamblajes resultante es bastante sencillo:

  5e:   80 91 60 00     lds     r24, 0x0060     ; ret = *(volatile uint32_t *)var;
  62:   90 91 61 00     lds     r25, 0x0061
  66:   a0 91 62 00     lds     r26, 0x0062
  6a:   b0 91 63 00     lds     r27, 0x0063
  6e:   40 91 60 00     lds     r20, 0x0060     ; *(volatile uint32_t *)var
  72:   50 91 61 00     lds     r21, 0x0061
  76:   60 91 62 00     lds     r22, 0x0062
  7a:   70 91 63 00     lds     r23, 0x0063
  7e:   84 17           cp      r24, r20        ; ret != *(..)
  80:   95 07           cpc     r25, r21
  82:   a6 07           cpc     r26, r22
  84:   b7 07           cpc     r27, r23
  86:   59 f7           brne    .-42            ; while do ... 0x5e <main+0x1e>
    
respondido por el markrages
0

Usando un puntero de byte para la sobrecarga no incurrida por mucho tiempo (el compilador sabía lo suficiente como para no crear la variable de puntero) De hecho, incluso ahorró algo al cargar solo el byte de interés y no se requiere la suspensión de interrupciones:

#include <WProgram.h>
volatile uint32_t secondCounter = 0;
const uint8_t led = 13;

void setup(){
   ;
}

void loop() {
   byte *p = (byte*)&secondCounter;
   digitalWrite( led , *p & 1 );
}

Compilado para:

0000042a <loop>:

void loop() {
   byte *p = (byte*)&secondCounter;
   digitalWrite( led , *p & 1 );
 42a:   60 91 09 01     lds r22, 0x0109
 42e:   61 70           andi    r22, 0x01   ; 1
 430:   8d e0           ldi r24, 0x0D   ; 13
 432:   0e 94 af 01     call    0x35e   ; 0x35e <digitalWrite>
}
 436:   08 95           ret


--- Actualización: eliminando el cálculo de la volatilidad de la llamada 'digitalWrite ()':

void loop() {
   uint8_t tmp;
   noInterrupts();                                     // Prevent secondCounter from being updated in ISR while being read from memory in main loop.
   tmp = ((uint8_t)(secondCounter & 0x00400000L) != 0);
   interrupts();                                       // Enable interrupts

   digitalWrite( led, tmp );
}

da:

void loop() {
   uint8_t tmp;
   noInterrupts();                                     // Prevent secondCounter from being updated in ISR while being read from memory in main loop.
 42a:   f8 94           cli
   tmp = ((uint8_t)(secondCounter & 0x00400000L) != 0);
 42c:   80 91 09 01     lds r24, 0x0109
 430:   90 91 0a 01     lds r25, 0x010A
 434:   a0 91 0b 01     lds r26, 0x010B
 438:   b0 91 0c 01     lds r27, 0x010C
   interrupts();                                       // Enable interrupts
 43c:   78 94           sei

   digitalWrite( led, tmp );
 43e:   8d e0           ldi r24, 0x0D   ; 13
 440:   60 e0           ldi r22, 0x00   ; 0
 442:   0e 94 af 01     call    0x35e   ; 0x35e <digitalWrite>
}
    
respondido por el JRobert

Lea otras preguntas en las etiquetas