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;
}
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.
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:
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.
timer1OverflowCount
? Supongo que es posible que timer1OverflowCount
se incremente justo en medio de la lectura, es decir, justo entre varias LDS
instrucciones.timer1OverflowCount
es 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 00000000
→ 00000000 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.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.
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)
.Mirando las impresiones 'malas' vemos lastC = 65440
y 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 timer1OverflowCount
aproximadamente 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 timer1OverflowCount
ha incrementado 'misteriosamente' antes de la siguiente interrupción.
Para solucionar el problema, no lea timer1OverflowCount
hasta 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.
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 TCNT1
ocurre con la ayuda del TEMP
registro. 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;
}
}
sbrc r29, 7
no da ni cerca de 32 pasos. Con respecto a dividir el bloque atómico: entre bloques, TIMER1_OVF
puede dispararse o cualquier otra interrupción de duración desconocida. Entre cada dos pasos, esto tiene consecuencias potencialmente desastrosas.oc0 >> 8 ^ oc >> 8
es lo mismo que (oc0 >> 8) ^ (oc >> 8)
seguir la precedencia del operador C.TIFR1
es 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
.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 / 16000000 ≈ 4 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 timer1OverflowCount
en 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.
47.5 ms
no 6 ms
_
Spehro Pefhany
mitu raj
feklee
oc = 31, c = 49, lastOc = 31, lastC = 65440
significa queTCNT1
se desbordó (65440→49) pero el contador de desbordamiento permaneció igual (31).Pham largo
dorio
alex.forencich
dorio
feklee
dorio
dorio
dorio