Temporizador 1 (16 bits): ¿Por qué a veces se pierde la interrupción por desbordamiento?

4

Inicialización del temporizador 1, el temporizador de 16 bits en el ATmega328:

TCCR1A = 0; // normal operation
TCCR1B = bit(CS10); // no prescaling
OCR1A = 0;
OCR1B = 0;
TIMSK1 |= bit(TOIE1); // Timer/Counter 1, Overflow Interrupt Enable

Los desbordamientos de 16 bits incrementan un contador de desbordamiento:

ISR(TIMER1_OVF_vect) {
    timer1OverflowCount ++;
}

En un bucle se comprueba si los dos contadores de 16 bits se incrementan correctamente:

void loop() {
  static uint16_t lastOc = 0, lastC = 0;
  uint16_t oc, c;

  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    oc = timer1OverflowCount;
    c = TCNT1;
  }

  if (c < lastC && oc == lastOc) {
    print(oc, c, lastOc, lastC);
  }

  lastOc = oc;
  lastC = c;
}

Salida de muestra a la consola serie:

Bad overflow: oc = 31, c = 49, lastOc = 31, lastC = 65440
Bad overflow: oc = 58, c = 49, lastOc = 58, lastC = 65440
Bad overflow: oc = 66, c = 49, lastOc = 66, lastC = 65440
Bad overflow: oc = 118, c = 49, lastOc = 118, lastC = 65440
Bad overflow: oc = 127, c = 49, lastOc = 127, lastC = 65440

¿Por qué a veces el contador de desbordamiento no se incrementa?

Entiendo que el bucle accede mucho al contador de desbordamiento. Durante un acceso, las interrupciones se activan con ATOMIC_BLOCK(ATOMIC_RESTORESTATE) . Sin embargo, el acceso es rápido y espero que la interrupción por desbordamiento se ponga en cola para que nunca se pierda.

Código completo para Arduino Pro Mini ATmega328P (5V, 16MHz), compatible con Arduino IDE 1.8.1:

#include <util/atomic.h>

volatile uint16_t timer1OverflowCount = 0;

ISR(TIMER1_OVF_vect) {
  timer1OverflowCount ++;
}

void setup() {
  TCCR1A = 0; // normal operation
  TCCR1B = bit(CS10); // no prescaling
  OCR1A = 0;
  OCR1B = 0;
  TIMSK1 |= bit(TOIE1); // Timer/Counter 1, Overflow Interrupt Enable

  Serial.begin(9600);
}

void print(uint16_t oc, uint16_t c, uint16_t lastOc, uint16_t lastC) {
  Serial.print("Bad overflow: ");
  Serial.print("oc = ");
  Serial.print(oc);
  Serial.print(", c = ");
  Serial.print(c);
  Serial.print(", lastOc = ");
  Serial.print(lastOc);
  Serial.print(", lastC = ");
  Serial.println(lastC);
}

void loop() {
  static uint16_t lastOc = 0, lastC = 0;
  uint16_t oc, c;

  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    oc = timer1OverflowCount;
    c = TCNT1;
  }

  if (c < lastC && oc == lastOc) {
    print(oc, c, lastOc, lastC);
  }

  lastOc = oc;
  lastC = c;
}
    
pregunta feklee

6 respuestas

1

A menos que las interrupciones estén deshabilitadas desde el momento en que se lee un valor hasta el momento en que se usa, generalmente se debe diseñar en torno a la idea de que un intento de leer un valor de algo que está cambiando puede generar cualquier valor que se mantenga. en cualquier momento durante el intento, sin preocuparse precisamente de cuándo se muestrea.

Una forma simple y general de manejar una lectura de 32 bits es comenzar leyendo un valor dos veces en n1 y n2 (lea los bytes en cualquier orden, siempre que la última lectura de n1 sea anterior a la primera lectura de n2) y no se preocupe por desactivar las interrupciones), y luego diga:

if ((uint8_t)((n1 >> 24) ^ (n2 >> 24)) // MSB has changed
  n2 &= 0xFF000000;
else if ((uint8_t)((n1 >> 16) ^ (n2 >> 16))
  n2 &= 0xFFFF0000;
else if ((uint8_t)((n1 >> 8) ^ (n2 >> 8))
  n2 &= 0xFFFFFF00;

Si el byte superior del temporizador cambia de xx a yy, eso implica que cuando el byte superior era xx, el valor del temporizador era como máximo xxFFFFFF, y cuando el el valor era yy, el valor del temporizador era al menos yy000000. Así, en algún momento. entre cuando se leyó xx y se leyó yy, su valor debe haber sido yy000000. Una lógica similar se aplica a los otros bytes del temporizador.

Tenga en cuenta que la cantidad de tiempo requerido para ejecutar este código estará limitada, incluso en presencia de una carga pesada de interrupción, siempre que la carga sea inferior al 100%. Tenga en cuenta que el código no intenta distinguir, por ejemplo, el caso donde el temporizador era 0x01FF0000 antes del código y 0x02000000 después de [haber gastado más de 65,000 ciclos en interrupciones] o 0x01FFFFFF antes y 0x02010000 después. Informará de 0x02000000 en cualquier caso, pero eso representaría un valor que el temporizador mantuvo en algún momento durante la ejecución de ese código.

    
respondido por el supercat
8

El problema es que no está eliminando el problema real con la declaración ATOMIC. El contador está aumentando en hardware y habrá momentos en que una interrupción está pendiente pero no se ha completado. Es peor porque las operaciones de C int toman muchos ciclos en un procesador de 8 bits, pero se mostraría incluso en código ASM estricto.

Si no tiene ninguna otra interrupción de larga duración, ni siquiera tiene que cancelar las interrupciones, simplemente puede corregir el conteo.

El enfoque general es el siguiente:

  1. lea timer1OverflowCount, primer timer1OverflowCount de muestra
  2. lea el contador de hardware TCNT1 < - aquí mismo es cuando realiza muestreos con alta resolución
  3. vuelva a leer timer1OverflowCount, segundo timer1OverflowCount, muestra

Si timer1OverflowCount ha cambiado (incrementado), mire la muestra de TCNT1. Si tiene MSB = 0, use el timer1OverflowCount posterior. Si tiene MSB = 1, use el timer1OverflowCount anterior.

No hay necesidad de desactivar las interrupciones a menos que haya otras interrupciones con ISR lentos en curso que podrían hacer que el tiempo total para que lo anterior se acerque al tiempo para que el contador de hardware alcance la mitad de la cuenta. Casi siempre es indeseable desactivar las interrupciones a menos que sea absolutamente necesario.

Editar: En su caso, tiene un int para el desbordamiento, por lo que necesita hacer la lectura de la variable cambiada dentro del atómico ISR, así como lidiar con el problema anterior.

    
respondido por el Spehro Pefhany
5

El ATOMIC_BLOCK se ejecuta sin interrupción, pero las instrucciones internas no se ejecutan al mismo tiempo.

De "oc = timer1OverflowCount;" a "c = TCNT1;" TCNT1 se incrementa y no es lo mismo que cuando se lee oc.

El error se muestra cuando se produce el desbordamiento dentro del bloque ATOMIC y timer1OverflowCount no se puede actualizar e incluso las interrupciones fueron deshabilitadas. TCNT1 mostrará un valor posterior al timer1OverflowCount.

Cambie las dos instrucciones, coloque cada una en su propio bloque ATÓMICO y vea el resultado. Es un "falso positivo". Supuse que solo desea comprobar que la interrupción de desbordamiento de su TCNT1 está funcionando bien. Funciona, pero el código utilizado muestra errores donde no están.

A solicitud del OP, agrego la explicación para leer TCNT1 en su propio bloque atómico.

La lectura del registro TCNT1 de 16 bits es segura en cuanto a su incremento entre dos lecturas de registro LSB y MSB de 8 bits. Cuando se lee LSB, el registro MSB está en búfer y todas las demás lecturas mostrarán el mismo valor en el momento en que se leyó LSB.

Pero tener una interrupción entre las dos lecturas que también lee TCNT1 actualizará el valor del MSB con búfer con el nuevo valor que podría ser diferente.

No sé si se aplica aquí, pero otro registro de 16 bits podría compartir el mismo búfer que TCNT1.

Puede ver el recuento de desbordamiento durante un tiempo fijo para ver si tiene algún retraso.

El código es rápido pero también se ejecuta sin interrupciones entre los bucles.

En el bucle tienes más o menos cinco instrucciones, dos transferencias en el bloque atómico y tres en el exterior. Se puede agregar un salto y habilitar deshabilitar interrupciones. La posibilidad de que se produzca este desbordamiento al ejecutar el bloque atómico y generar el error es bastante alta.

Entonces, digamos que el procesador gasta 2us para el bloque ATOMIC y 4us para el resto del bucle, luego nuevamente 2us en el bloque ATOMIC y nuevamente 4 fuera.

Si el desbordamiento se produce mientras está en el bloque ATOMIC (que es 1/3 de posibilidad) mientras que las interrupciones están deshabilitadas y el temporizador1OverflowCount no puede incrementarse, tiene una buena posibilidad de provocar un error falso.

La posibilidad es mucho menor porque el compilador agrega algo de código para el bucle y también dentro del bloque ATOMIC el desbordamiento debe ocurrir antes de leer el TCNT1 LSB.

Este es el código de trabajo publicado por OP en el comentario:

ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { c = TCNT1; } 
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { oc = timer1OverflowCount; }

Me gustaría agregar que este código funciona usando las condiciones de la sección IF

if (c < lastC && oc == lastOc)

Pero si marca

if (c >= lastC && oc != lastOc)

significa que cambiar el recuento de desbordamiento sin un desbordamiento también tendrá errores falsos.

En este caso, si no es solo una verificación de código, entonces debe usar algo como la respuesta de Spehro leyendo el TCNT1 nuevamente (también en su bloque ATÓMICO) y usar el valor más bajo de TCNT1 en caso de un cambio en el conteo del temporizador.

Pero si necesita hacer algo cuando el contador de temporizadores se está actualizando, verifique solo el contador de temporizadores (seguro para subprocesos). Está funcionando bien.

    
respondido por el Dorian
3

Al mirar las impresiones "malas", vemos lastC = 65440 y c = 49 , lo que indica que se produjeron 96 contadores de contador entre cada temporizador leído en el ciclo principal. Debería haber ocurrido una interrupción por desbordamiento en 65536, que es aproximadamente la mitad de ese tiempo.

Pero también está leyendo timer1OverflowCount aproximadamente a la mitad de ese tiempo, por lo que, dependiendo del momento exacto de la interrupción, puede haber ocurrido o no al leer el recuento de desbordamiento. Cuando lees el temporizador, la interrupción se habrá producido y timer1OverflowCount se habrá incrementado, pero estás usando el recuento de desbordamiento histórico que puede ser antes de el temporizador se desbordó.

Para tener una mejor idea de lo que está sucediendo, podría hacer otra impresión inmediatamente después de cada "mala", ¡entonces debería ver que timer1OverflowCount ha aumentado "misteriosamente" antes de la próxima interrupción!

Para solucionar el problema, no lea timer1OverflowCount hasta que c < %código%. Entonces sabrá que debería haber ocurrido una interrupción por desbordamiento (y la siguiente es ~ 65000 marcas de distancia) para que pueda probar de manera confiable el conteo de desbordamiento.

    
respondido por el Bruce Abbott
2

Combinando bits y piezas de respuestas con información de ATmega328 versión de la hoja de datos DS40001984A Encontré la siguiente solución, que creo que es robusta:

ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
  c = TCNT1; // TCNT1 increases all the time during the following instructions
  bool timerDidOverflow = TIFR1 & 1; // TOV1 Timer/Counter 1, Overflow Flag
  byte msb = c >> 15;
  if (msb == 0 && timerDidOverflow) {
    timer1OverflowCount ++;
    TIFR1 &= 1; // Write to TOV1 to clear it and prevent triggering TIMER1_OVF
  }
  oc = timer1OverflowCount;
}

Las líneas anteriores reemplazan:

ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
  oc = timer1OverflowCount;
  c = TCNT1;
}

Como ha habido preocupación sobre la microoptimización en los comentarios, aquí está el código de ensamblaje por salida por avr-objdump :

   c = TCNT1; // TCNT1 increases all the time during the following instructions
606:    c0 91 84 00     lds    r28, 0x0084    ; 0x800084 <__stack+0x7ff785>
60a:    d0 91 85 00     lds    r29, 0x0085    ; 0x800085 <__stack+0x7ff786>
   bool timerDidOverflow = TIFR1 & 1; // TOV1 Timer/Counter 1, Overflow Flag
60e:    86 b3           in    r24, 0x16    ; 22
610:    81 70           andi    r24, 0x01    ; 1
   byte msb = c >> 15;
   if (msb == 0 && timerDidOverflow) {
612:    d7 fd           sbrc    r29, 7
614:    0e c0           rjmp    .+28         ; 0x632 <main+0x144>
616:    88 23           and    r24, r24
618:    61 f0           breq    .+24         ; 0x632 <main+0x144>
     timer1OverflowCount ++;
61a:    80 91 4c 01     lds    r24, 0x014C    ; 0x80014c <timer1OverflowCount>
61e:    90 91 4d 01     lds    r25, 0x014D    ; 0x80014d <timer1OverflowCount+0x1>
622:    01 96           adiw    r24, 0x01    ; 1
624:    90 93 4d 01     sts    0x014D, r25    ; 0x80014d <timer1OverflowCount+0x1>
628:    80 93 4c 01     sts    0x014C, r24    ; 0x80014c <timer1OverflowCount>
     TIFR1 &= 1; // Write to TOV1 to clear it and prevent triggering TIMER1_OVF
62c:    86 b3           in    r24, 0x16    ; 22
62e:    81 70           andi    r24, 0x01    ; 1
630:    86 bb           out    0x16, r24    ; 22
   }
   oc = timer1OverflowCount;
632:    e0 90 4c 01     lds    r14, 0x014C    ; 0x80014c <timer1OverflowCount>
636:    f0 90 4d 01     lds    r15, 0x014D    ; 0x80014d <timer1OverflowCount+0x1>

Nota al margen: El acceso de lectura de 16 bits a TCNT1 ocurre con la ayuda del registro TEMP . El acceso está garantizado para dar un valor consistente. Consulte las páginas 154 y 155 en datasheet versión DS40001984A . Sin la amenaza de interrupciones al acceder a TCNT1 , no es necesario colocar el acceso de lectura / escritura a TCNT1 en un bloque atómico.

A modo de comparación, aquí hay una implementación de la solución de la respuesta por @supercat , que no requiere atomicidad durante la lectura:

uint16_t c0 = TCNT1, oc0 = timer1OverflowCount;
uint16_t c = TCNT1, oc = timer1OverflowCount;

if ((oc0 >> 8) ^ (oc >> 8)) {
  oc &= 0xff00;
  c = 0;
} else if ((uint8_t)(oc0 ^ oc)) {
  c = 0;
} else if ((c0 >> 8) ^ (c >> 8)) {
  c &= 0xff00;
}

Terse es la solución de @Bruce Abbot , que probé con éxito con la siguiente implementación:

static uint16_t oc = 0;
uint16_t c;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
  c = TCNT1;
}

bool timerDidOverflow = c < lastC;
if (timerDidOverflow) {
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    oc = timer1OverflowCount;
  }
}
    
respondido por el feklee
1

Esto es solo mi aunque. Lo estás comprobando ocasionalmente, no a un intervalo fijo.

¡Lleva tiempo procesar el bucle y enviar caracteres a través de UART! Hagamos un supuesto:

Estás utilizando la velocidad en baudios de 9600, tu Arduino envía aproximadamente 57 caracteres en cada bucle, por lo que el tiempo mínimo para finalizar cada bucle es:

               57 * 8 / 9600  =  47.5 ms  ( Rough approximation)

¿Cuánto tiempo se desborda Timer1? (Basado en su configuración)

               65440 / 16000000 ≈ 4 ms

Verás, no puede capturar cada desbordamiento único.

Debes modificar tu código y usar otro temporizador para crear un intervalo de activación fijo para contar cuánto timer1OverflowCount en un tiempo fijo.

No he analizado tu código a fondo porque estoy agotado ahora (y perezoso también). Probablemente sea un error de software en lugar de una falla de hardware.

    
respondido por el Long Pham

Lea otras preguntas en las etiquetas