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

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;
}

Ejemplo de salida 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 no se incrementa el contador de desbordamiento?

Entiendo que el ciclo accede mucho al contador de desbordamiento. Durante un acceso, las interrupciones se desactivan con ATOMIC_BLOCK(ATOMIC_RESTORESTATE). Sin embargo, el acceso es rápido y espero que la interrupción de 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;
}
¿Qué código ensamblador emite la instrucción ATOMIC_BLOCK(..){..}?
oc está incrementando... entonces, ¿cuál es el problema aquí?
@MITURAJ Mire el resultado del ejemplo. oc = 31, c = 49, lastOc = 31, lastC = 65440significa que TCNT1se desbordó (65440→49) pero el contador de desbordamiento permaneció igual (31).
@Spehro Pefhany Puede encontrar más información aquí .
Sería interesante ver una lista de asm después de reemplazar la llamada de impresión con algunos NOP-s para ver el tiempo.
Un par de cosas para intentar: alternar un pin en el ISR, alternar un pin cuando lee TCNT1, mostrar la señal de desbordamiento de hardware en un pin y conectar todo eso a un osciloscopio o analizador lógico. Eso debería darle una imagen decente de lo que está sucediendo internamente. Además, tome el indicador de interrupción al final de su bloque atómico e imprímalo para ver si se solicitó una interrupción dentro del bloque atómico, ya que ese es probablemente el caso aquí.
@feklee, ¿ha intentado cambiar las instrucciones como en mi respuesta? ¿Para leer TCNT1 primero y luego timer1OverflowCount?
@Dorian Sí, pero no funciona.
@feklee Coloque cada instrucción en su propio bloque ATOMIC, vea la respuesta modificada. También es seguro para subprocesos.
Un problema de hilo cuando no se deshabilitan las interrupciones, como sugiere Sphero, también será con TMR1, el byte de orden superior almacenado en el búfer se puede cambiar por una interrupción que también lee TMR1.
¿Cuál es su propósito, hacer algo en el código principal en el desbordamiento de TMR1 o simplemente verificar si funciona correctamente?

Respuestas (6)

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 esa cosa tenga en cualquier momento. durante el intento, sin preocuparse de cuándo precisamente se muestrea.

Una forma simple de propósito general de manejar una lectura de 32 bits es comenzar leyendo un valor dos veces en n1 y n2 (leer los bytes en cualquier orden, siempre que la última lectura de n1 preceda a la primera lectura de n2) y no No te preocupes por deshabilitar las interrupciones) y luego decir:

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 valor era yy, el valor del temporizador era como mínimo yy000000. Por lo tanto, en algún momento entre la lectura de xx y la lectura de yy, su valor debe haber sido yy000000. Se aplica una lógica similar a los otros bytes del temporizador.

Tenga en cuenta que la cantidad de tiempo necesaria para ejecutar este código estará limitada, incluso en presencia de una gran carga de interrupción, siempre que la carga sea inferior al 100 %. Tenga en cuenta que el código no intenta distinguir entre, por ejemplo, el caso en el que el temporizador era 0x01FF0000 antes del código y 0x02000000 después [de haber pasado más de 65 000 ciclos en interrupciones] o 0x01FFFFFF antes y 0x02010000 después. Informará 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.

¡Buen truco e independiente de la MCU utilizada! Lo probé con éxito con la implementación que agregué a mi respuesta .

El problema es que no está eliminando el problema real con la declaración ATOMIC: el contador se incrementa en el hardware y habrá momentos en que una interrupción esté pendiente pero no completada. Es peor porque las operaciones de C int toman muchos ciclos en un procesador de 8 bits, pero aparecería incluso en un código asm ajustado.

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

El enfoque general es el siguiente:

  1. leer timer1OverflowCount, primera muestra timer1OverflowCount
  2. lea el contador de hardware TCNT1 <-- justo aquí es cuando está muestreando con alta resolución
  3. lea timer1OverflowCount nuevamente, segunda muestra de timer1OverflowCount

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

No hay necesidad de desactivar las interrupciones a menos que haya otras interrupciones con ISR lentos que podrían causar que el tiempo total para lo anterior se acerque al tiempo para que el contador de hardware alcance la mitad del conteo completo. 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 debe realizar la lectura de la variable cambiada dentro del atómico ISR, además de tratar el problema anterior.

Esto parece hacer el truco, gracias! ¿ Pero no debería deshabilitar las interrupciones al leer timer1OverflowCount? Supongo que es posible que timer1OverflowCountse incremente justo en medio de la lectura, es decir, justo entre varias LDSinstrucciones.
Porque no es necesario (en cualquier caso se corregirá la lectura). Desactivar las interrupciones globalmente puede aumentar la latencia a las interrupciones de mayor prioridad en los procesadores que las admiten; es simplemente mala forma si no es necesario.
Ejemplo: (a) timer1OverflowCountes un valor de 16 bits de: 11111111 00000000(b) El primer byte se lee ( LDS): 11111111(c) La interrupción de desbordamiento se incrementa 11111111 0000000000000000 00000001. (little endian) (d) Se lee el segundo byte ( LDS): 00000001. El valor total leído es 0xb111111111 = 760495542545, en lugar de 0xb100000000 = 760209211392. Esto es incorrecto. ¿O estoy equivocado? No veo cómo su propuesta corrige esta condición de carrera.
Ah, está bien, sí, en ese caso, si se trata de una variable de 16 bits en un procesador de 8 bits, también puede jugar un truco similar con lecturas dobles si necesita mantener las interrupciones.

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 al leer oc.

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

Cambia las dos instrucciones, coloca cada una en su propio bloque ATOMIC y observa el resultado. Es un "falso positivo". Supuse que solo desea verificar que su interrupción de desbordamiento TCNT1 funciona bien. Funciona, pero el código utilizado muestra errores donde no los hay.

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

Leer el registro TCNT1 de 16 bits es seguro con respecto a su incremento entre dos lecturas de registro LSB y MSB de 8 bits, cuando se lee LSB, el registro MSB se almacena en búfer y todas las lecturas posteriores 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 lea TCNT1 actualizará el valor del MSB almacenado en 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 bucles.

En el bucle tienes más o menos cinco instrucciones, dos transferencias en el bloque atómico y tres fuera. Se puede agregar un salto y habilitar deshabilitar interrupciones. La posibilidad de que se produzca este desbordamiento durante la ejecución del bloque atómico y que genere el error es bastante alta.

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

Si el desbordamiento ocurre mientras está en el bloque ATOMIC (que es 1/3 de probabilidad) mientras las interrupciones están deshabilitadas y timer1OverflowCount no se puede incrementar, tiene muchas posibilidades de generar un error falso.

La posibilidad es mucho menor porque el compilador agrega algo de código para el ciclo y también dentro del bloque ATOMIC, el desbordamiento debe ocurrir antes de leer 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 revisas

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

lo que significa 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, debe usar algo como la respuesta de Spehro leyendo el TCNT1 nuevamente (también en su bloque ATOMIC) y usar el valor más bajo de TCNT1 en caso de que cambie el conteo del temporizador.

Pero si necesita hacer algo cuando el conteo del temporizador se está actualizando, verifique solo el conteo del temporizador (seguro para subprocesos). Está funcionando bien.

Gracias, esto funciona: ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { c = TCNT1; } ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { oc = timer1OverflowCount; }Uno puede preguntarse: ¿ Por qué es necesario colocar c = TCNT1;en su propio bloque atómico? Después de todo, el acceso de lectura de 16 bits a TCNT1 ocurre con la ayuda del registro TEMP. Consulte la versión de hoja de datos DS40001984A, páginas 154 y 155. Curiosamente, se indica explícitamente: "Los siguientes ejemplos de código muestran cómo acceder a los registros del temporizador de 16 bits, suponiendo que ninguna interrupción actualice el registro temporal". Quizás se dispara durante la lectura de TCNT1 ISR(TIMER1_OVF_vect).
¿Podría agregar estas consideraciones a la respuesta para que sea más completa? También sería bueno que explicara a qué se refiere con "falso positivo". No entiendo la segunda mitad de tu respuesta.

Mirando las impresiones 'malas' vemos lastC = 65440y c = 49, lo que indica que ocurrieron 96 tics de contador entre cada lectura del temporizador en el bucle principal. Debería haber ocurrido una interrupción de desbordamiento en 65536, que está aproximadamente a la mitad de ese tiempo.

Pero también está leyendo timer1OverflowCountaproximadamente a la mitad de ese tiempo, por lo que, dependiendo del momento exacto de la interrupción, puede haber ocurrido o no cuando lee el conteo de desbordamiento. Cuando lea el temporizador, la interrupción habrá ocurrido y timer1OverflowCount se habrá incrementado, pero está utilizando el conteo de desbordamiento histórico que puede haber antes de que el temporizador se desborde.

Para tener una mejor idea de lo que está sucediendo, puede hacer otra impresión inmediatamente después de cada 'malo', luego debería ver que se timer1OverflowCountha incrementado 'misteriosamente' antes de la siguiente interrupción.

Para solucionar el problema, no lea timer1OverflowCounthasta c< lastC. Entonces sabrá que acaba de ocurrir una interrupción de desbordamiento (y la siguiente está a unos 65000 tics de distancia) para que pueda probar de manera confiable el conteo de desbordamiento.

Solución muy simple, gracias! Agregué una implementación a mi respuesta .

Al combinar fragmentos de respuestas con información de la hoja de datos ATmega328 versión DS40001984A, encontré la siguiente solución, que creo que es sólida:

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 por la microoptimización en los comentarios, aquí está el código ensamblador por salida de 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 TCNT1ocurre con la ayuda del TEMPregistro. El acceso está garantizado para producir un valor constante. Consulte las páginas 154 y 155 en la hoja de datos versión DS40001984A . Sin la amenaza de interrupciones en el acceso, noTCNT1 es necesario colocar el acceso de lectura/escritura en un bloque atómico.TCNT1

A modo de comparación, aquí hay una implementación de la solución de la respuesta de @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;
  }
}
Votó como una opción, pero en realidad el código es peor de esa manera. La latencia de interrupción es mucho mayor. Para TCNT1 interrumpir desde la línea "bool..." hasta el final y para otro interrumpir todo el bloque. Para tres bloques atómicos pequeños, la latencia es tan alta como la más larga.
Al menos reemplace "msb = c >> 15", que se realiza en más de 32 pasos o 7 si el compilador realiza alguna optimización ignorando el byte inferior con "msb = c & 0x1000" o simplemente omítalo y reemplace "if(msb= =0 &&..." con "if(c<0x1000 &&..." que toma menos tiempo de procesamiento. No hay una ganancia real para usar solo un bloque.
Por cierto, ¿por qué almacena en búfer el indicador de desbordamiento ya que no importa cuándo se muestrea? Puede usar "si ((c<0x1000)&&(TIFR1 & 1)){..."
@Dorian sbrc r29, 7no da ni cerca de 32 pasos. Con respecto a dividir el bloque atómico: entre bloques, TIMER1_OVFpuede dispararse o cualquier otra interrupción de duración desconocida. Entre cada dos pasos, esto tiene consecuencias potencialmente desastrosas.
El operador de desplazamiento a la derecha debe estar separado por paréntesis del operador exclusivo-or.
@supercat oc0 >> 8 ^ oc >> 8es lo mismo que (oc0 >> 8) ^ (oc >> 8)seguir la precedencia del operador C.
Entonces el compilador es más inteligente. Hace lo que sugerí a tus espaldas. :-)
Por supuesto, no me sugirieron que dividiera su código, sino que usara una solución con bloques más pequeños. ¿Puedes publicar también el código asm para mi versión con dos bloques?
@feklee: Eso es cierto, pero muchos compiladores tienen advertencias opcionales sobre el uso de ciertas combinaciones de operadores sin paréntesis, y agregar paréntesis hará que el código sea más claro y garantizará que se compilará sin quejas incluso cuando tales opciones estén habilitadas.
No es un compilador perfecto, no optimizó la lectura y la prueba de TIFR1 (en cuanto a c cambió a msb y luego a thested) porque pasó por alto su comentario de que el momento de leer TIFR1 no es importante.
@Dorian, nunca dije tal cosa. La nota TIFR1es similar a una variable volátil C. Su valor puede cambiar en cualquier momento. Se desaconsejaría al compilador cambiar la posición del acceso a TIFR1.
@supercat Acabo de probar algunos compiladores con todas las advertencias habilitadas (gcc, clang, vc). Ninguno da una advertencia precisamente en este caso. Sin embargo, agregué los paréntesis. (De hecho, normalmente agrego muchos paréntesis porque no puedo recordar todos los casos de precedencia de operadores. Aquí me tomé el tiempo para buscarlo).
Lo sé. Era solo una broma inofensiva. Es mejor escribir un buen código desde el principio. Un poco decepcionado con su elección, sigue siendo su q
Aún así, su pregunta no se editó y la respuesta correcta y completa es: "Está actualizado, su código de prueba es incorrecto" y una buena manera de verificar observando el recuento de desbordamiento a largo plazo. Pero está bien, es tu elección no la mejor.
@feklee: tal vez este caso particular no da una advertencia, pero parece que la mayoría de las combinaciones de desplazamiento a la izquierda/derecha con otros operadores dan advertencias, y un buen código tiene la propiedad de que no solo hace lo que el programador pretende, sino que deja un lector humano sin ninguna duda de que lo hará. Agregar paréntesis ayuda con esto último.

Este es solo mi pensamiento. Lo está comprobando ocasionalmente, no en un intervalo fijo.

¡Lleva tiempo procesar el bucle y enviar caracteres a través de UART! Hagamos una suposición:

Está utilizando la velocidad de transmisión de 9600, su Arduino envía alrededor de 57 caracteres en cada ciclo, por lo que el tiempo mínimo para terminar cada ciclo es:

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

¿Cuánto tiempo se desborda el Timer1? (Según su configuración)

               65440 / 160000004 ms

Verá, no puede atrapar todos los desbordamientos.

Debe modificar su código y usar otro temporizador para crear un intervalo de activación fijo para contar cuánto timer1OverflowCounten un tiempo fijo.

No analicé su código a fondo porque ahora estoy agotado (y también perezoso). Probablemente se trate de un error de software en lugar de una deserción del hardware.

Él usa uart solo después de que se encuentra la fecha perdida, por lo que no cuenta para el tiempo de bucle.
Como dije: "No puede atrapar todos los desbordamientos".
Esto significa que el problema es un poco peor. Pero incluso si la instrucción de impresión no está almacenada en el búfer, se perderá verificar solo un desbordamiento después de encontrar un error.
Lo siento, he editado. 47.5 msno 6 ms_
Acabo de notar el error baudios vs bytes. Aún así, a su respuesta le falta la respuesta en sí. Simplemente muestra que el problema está ahí, pero no explica la causa. No voté negativamente, pero debería ser más bien un comentario.