Frecuencia incorrecta con Timer1 en Atmega328p en modo CTC

Quiero hacer que un LED parpadee a una frecuencia de 0,5 Hz. Entonces, uso un Atmega328p, con una frecuencia de 16 MHz, y el Timer1 en modo CTC, que activa una interrupción cada milisegundo. Sin embargo, mi programa, en Atmel Assembly, se ve así:

.include "m328pdef.inc"

.def     WR1 = R16    ; the working registers
.def     WR2 = R17
.def     WR3 = R18

.def     T1 = r13     ; A 24 bits value used to hold the
.def     T2 = r14     ; number of milliseconds
.def     T3 = r15     ; In T1 is the LSB, and in T3 the MSB

.cseg
.org  $0000
    RJMP    Start

.org OC1Aaddr         ; The address of the Timer1A compare match interrupt
    JMP     Timer1comp
    NOP

.org INT_VECTORS_SIZE

;---------------;
;   Settings    ;
;---------------;
Start:
    ; Initialize the Stack Pointer
    LDI     WR1, HIGH(RAMEND)
    OUT     SPH, WR1
    LDI     WR1, LOW(RAMEND)
    OUT     SPH, WR1

    ; Timer1A setup
    CLR     T1        ; Reset the ms counter
    CLR     T2
    CLR     T3

    LDI     WR1, (1<<WGM12)|(1<<CS10)  ; Timer in CTC mode, TOP = OCR1A
    STS     TCCR1B, WR1

    LDI     WR1, HIGH(15999)  ; Must wait 16000 clock cycles in order to
    LDI     WR2, LOW(15999)   ; have a frequency of interrupt of 1 kHz.
    STS     OCR1AH, WR1       ; ISR doesn't interrupt timer, so have
    STS     OCR1AL, WR2       ; to set OCR1A to 16000 - 1

    LDI     WR1, 1 << OCIE1A  ; Enable Output Compare A match interrupt
    STS     TIMSK1, WR1

    ; I/O settings
    SER     WR1               ; The LED is plugged on any pin of port D
    OUT     DDRD, WR1
    OUT     PORTD, WR1

    ; Enable interrupts
    SEI

;---------------;
;  Main Loop    ;
;---------------;
Loop:
    CLI                    ; Reset the ms counter
    CLR    T1
    CLR    T2
    CLR    T3
    SEI

    IN     WR1, PORTD      ; Toggle port D
    SER    WR2
    EOR    WR1, WR2
    OUT    PORTD, WR1

    ; HERE WAS MY ERROR
    ;LDI    WR1, BYTE1(988)  ; 12 clock cycles are passed
    ;LDI    WR2, BYTE2(988)  ; We want to wait 1000 ms
    ;LDI    WR3, BYTE3(988)

    ; CORRECT WAY
    LDI    WR1, BYTE1(1000)  ; We have already spent 12 clock cycles (750 ns)
    LDI    WR2, BYTE2(1000)  ; which is totally negligible against
    LDI    WR3, BYTE3(1000)  ; our delay of 1000 ms

Wait:
    CLI
    CP     T1, WR1    ; Compare the time (ms) since start of loop
    CPC    T2, WR2    ; with 1000
    CPC    T3, WR3
    SEI

    BRLO   Wait       ; Go to Wait if time < 1000 ms

    RJMP   Loop

;-----------------;
;   Timer1A ISR   ;
;-----------------;
Timer1comp:
    ; Save the current state on the stack
    PUSH    WR1
    IN      WR1, SREG
    PUSH    WR1

    INC     T1          ; Increment the ms counter
     BRNE    Timer1compEnd
    INC     T2
     BRNE    Timer1compEnd
    INC     T3

Timer1compEnd:
    ; Reset the state
    POP     WR1
    OUT     SREG, WR1
    POP     WR1

    RETI

El problema es que cuando mido la frecuencia de parpadeo del LED, en lugar de encontrar 0,500 Hz, veo 0,506 Hz. Me pregunto de dónde viene este error.

Además, soy realmente nuevo en la programación ensambladora, así que estaré encantado de recibir cualquier tipo de consejo.

EDITAR 1

Finalmente, encontré el origen de mi problema: en el Loop principal, cargo WR1:WR2:WR3 con 988 ms , porque el Loop principal toma 12 ciclos de reloj . Sin embargo, incluso si el lazo principal toma 12 ciclos de reloj, solo toma 12 / 16000000 = 0.75 µs, que es totalmente insignificante contra el período de 1 s. Podemos suponer que este Loop toma 0s: luego, espero 12 ms menos de lo que esperaría, lo que representa un error de 1.2 % como dijo Michael Karas. Para corregir esto, debo configurar WR1: WR2: WR3 en (1000 - 0.00075), entonces 1000.

EDITAR 2

Hubo un problema con mi código anterior: en el ciclo Wait, probé si el tiempo deseado era mayor o igual al tiempo transcurrido. Por lo tanto, si el tiempo transcurrido es 1000, entonces el microcontrolador esperaría hasta el milisegundo 1001 para bifurcarse al bucle principal. Entonces, para corregir esto, debo probar si el tiempo transcurrido es menor o no que el tiempo deseado.

Respuestas (2)

Intente usar un valor de 15999 para TOP en OCR1.

No es necesario tener en cuenta el tiempo que se tarda en ejecutar la ISR. El temporizador funciona libremente. En el modo CTC, comienza en 0 y cuenta hasta el valor SUPERIOR, luego vuelve a cero y comienza de nuevo. Opcionalmente, puede generar una interrupción cada vez que se borra, pero esto es un efecto secundario: contará así por sí solo con o sin interrupción.

El temporizador continúa contando mientras se ejecuta su ISR. Se volverá a llamar a su ISR la próxima vez que se borre, independientemente del tiempo que tarde en ejecutarse, siempre que termine de ejecutarse antes del próximo borrado y la próxima interrupción.

Por lo tanto, también puedes eliminar las líneas...

LDI    WR1, BYTE1(988)  ; 12 clock cycles are passed
LDI    WR2, BYTE2(988)  ; We want to wait 1000 ms
LDI    WR3, BYTE3(988)

...ya que no tienen ningún efecto.

Tenga en cuenta que con el código anterior, su LED no alternará exactamente cada 2 segundos debido a la fluctuación debido al hecho de que el ISR puede interrumpir la tarea en primer plano en momentos aleatorios. Si se interrumpe después de que ocurra la comparación, el LED no cambiará hasta el siguiente paso. Todavía tendrá una frecuencia de salida muy sólida de 0,5 Hz, solo habrá unas pocas docenas de ciclos de fluctuación en cualquier transición. Si desea exactamente que la salida esté libre de fluctuaciones +/- 1 ciclo, puede habilitar uno de los pines de comparación de salida en modo alternar y dejar que el hardware parpadee el LED por usted.

¿Tener sentido?

El código ASM final es muy bueno, pero aquí hay algunas sugerencias...

-No creo que necesites el NOPdespués del vector Timer1comp. Esto NOPen realidad está aterrizando en el siguiente vector. Si bien no hace daño aquí, es al menos superfluo.

-El CS01bit en la línea LDI WR1, (1<<WGM12)|(1<<CS10) ; Timer in CTC mode, TOP = OCR1Aen realidad habilita el temporizador y comienza a contar. Dado que aún no ha asignado los registros OCR, se establecen en sus valores iniciales de 0, lo que significa que se produce una coincidencia inmediatamente. Nuevamente, esto realmente no causa ningún problema en el programa actual, pero una vez que comience a agregar funcionalidad, puede hacer que algunos errores sean difíciles de encontrar. Es mejor configurar todos los registros del temporizador y luego habilitarlo como paso final cuando todo esté listo y en un estado conocido. Esto implicaría cambiar la línea anterior LDI WR1, (1<<WGM12)y luego agregar una línea adicional LDI WR1, (1<<WGM12)|(1<<CS10)al final de la configuración.

-No configuraría manualmente los pines de salida altos al inicio. En su lugar, déjelos configurarse en el primer tiempo de cambio de ms. Esto retrasará el inicio de la generación de la señal, pero se asegurará de que el primer pulso tenga el ancho correcto. Como está escrito, el primer pulso será corto en el tiempo entre el momento en que inicia el temporizador y el momento en que establece los pines altos. Si realmente debe hacer que la generación de la señal comience lo antes posible después del reinicio, entonces puede configurar los pines en alto al comienzo del programa y luego inicializar el TCNT para tener en cuenta el tiempo perdido en el primer paso.

-Puedes reemplazar las líneas...

IN     WR1, PORTD      ; Toggle port D
SER    WR2
EOR    WR1, WR2
OUT    PORTD, WR1

...con el ligeramente más eficiente...

SER    WR2 
OUT    PIND, WR1

De la hoja de datos:

14.2.2 Cambiar el Pin

Escribir uno lógico en PINxn alterna el valor de PORTxn, independientemente del valor de DDRxn.

-Borrar los registros Tx en el subproceso principal podría causar una condición de carrera potencial y tics perdidos. No sabe el valor de Tx cuando pasa :Loopporque, en teoría, el ISR podría haberlos actualizado en segundo plano desde la última vez que los miró. En la práctica, esto es imposible con el código actual, pero si comienza a agregar cosas, podría generar errores difíciles de encontrar. Imagine, por ejemplo, que el Tx aumenta de 1000 a 1001 entre el momento en que lo compara y el momento en que lo borra. Ahora ha perdido este milisegundo para siempre y luego el próximo pulso será de 1 ms corto. En general, se considera una mala práctica actualizar un valor de 2 subprocesos diferentes por este motivo.

Una forma de evitar esto sería restar 1,000 de Tx en la parte superior del ciclo en lugar de borrarlo a 0. Otra forma de evitar esto sería verificar la coincidencia con 1,000 dentro del ISR y en su lugar simplemente establecer una bandera que el ciclo principal luego comprueba y borra (así es como lo hace el código del temporizador Ardunio).

Pero creo que para este código, la solución más fácil y eficiente sería mover el incremento, la prueba, la acción y el resto al ISR. Si bien generalmente desea hacer lo menos posible en el ISR, al mover las cosas podemos hacer que el ISR aún sea muy corto. Veo que aprecias hacer malabares con los bits, así que en lugar de contar de 0 a 1000, cuento de -1000 a 0. Creo que esto hace que la verificación de límites sea un poco más eficiente dentro de la ISR. También moví los registros Tx para poderlos LDI. Aquí está este código modificado final...

.def     WR1 = R16    ; the working registers
.def     WR2 = R17
.def     WR3 = R18

.def     T1 = r19     ; A 24 bits value used to hold the
.def     T2 = r20     ; number of milliseconds
.def     T3 = r21     ; In T1 is the LSB, and in T3 the MSB

.cseg
.org  $0000
    RJMP    Start

.org OC1Aaddr         ; The address of the Timer1A compare match interrupt
    JMP     Timer1comp

.org INT_VECTORS_SIZE

;---------------;
;   Settings    ;
;---------------;
Start:
    ; Initialize the Stack Pointer
    LDI     WR1, HIGH(RAMEND)
    OUT     SPH, WR1
    LDI     WR1, LOW(RAMEND)
    OUT     SPH, WR1

    LDI     T1, BYTE1(-1000)   ; Init the ms counter to -1000
    LDI     T2, BYTE2(-1000)   ; we start a a negative number and count up becuase it
    LDI     T3, BYTE3(-1000)   ; is more efficient to test if we got to 0

    ; Timer1A setup
    LDI     WR1, (1<<WGM12)   ; Timer in CTC mode, TOP = OCR1A
    STS     TCCR1B, WR1

    LDI     WR1, HIGH(15999)  ; Must wait 16000 clock cycles in order to
    LDI     WR2, LOW(15999)   ; have a frequency of interrupt of 1 kHz.
    STS     OCR1AH, WR1       ; ISR doesn't interrupt timer, so have
    STS     OCR1AL, WR2       ; to set OCR1A to 16000 - 1

    LDI     WR1, 1 << OCIE1A  ; Enable Output Compare A match interrupt
    STS     TIMSK1, WR1

    ; I/O settings
    SER     WR1               ; The LED is plugged on any pin of port D
    OUT     DDRD, WR1

    ; Enable Timer  
    LDS     WR1, TCCR1B
    ORI     WR1, (1<<CS10)  ; clock prescaler=1
    STS     TCCR1B, WR1

    ; Enable interrupts
    SEI

;---------------;
;  Main Loop    ;
;---------------;
Loop:
    RJMP   Loop

;-----------------;
;   Timer1A ISR   ;
;-----------------;
Timer1comp:
    ; Save the current state on the stack
    PUSH    WR1
    IN      WR1, SREG
    PUSH    WR1

    LDI     WR1,1       ; Increment the ms counter LSB by 1

    ADD     T1,WR1      

    LDI     WR1,0       ; Increment the rest by zero so only the carry will propigate

    ADC     T2,WR1
    ADC     T3,WR1

    BRCC    Timer1compEnd   ; If carry is set, then we rolled the ms counter

    ;Reset the ms counter back to -1000

    LDI     T1, BYTE1(-1000)   ; Init the ms counter
    LDI     T2, BYTE2(-1000)        
    LDI     T3, BYTE3(-1000)        

    SER    WR1
    OUT    PIND, WR1         ;; Toggle all PORTD output pins


Timer1compEnd:
    ; Reset the state
    POP     WR1
    OUT     SREG, WR1
    POP     WR1

    RETI

Este código es funcionalmente equivalente pero aproximadamente un 15 % más pequeño y elimina algunas condiciones de carrera que podrían haberse convertido en errores en futuras versiones. Avísame si me he perdido algo.

Nuevamente, permítanme reiterar que su código original era muy bueno y mucho más limpio que muchos códigos de producción comercial que he visto. Teniendo en cuenta que eres nuevo en la programación de asm, ¡creo que con un poco de experiencia te convertirás rápidamente en un maestro de asm!

Gracias por la aclaración, siempre pensé que la interrupción no solo detenía la ejecución del programa, sino que también bloqueaba la funcionalidad del hardware, como los temporizadores. Pero esto tiene mucho sentido, después de todo. Sin embargo, no entiendo por qué dices que puedo eliminar las 3 líneas: ok, log<sub>2</sub>(1000) < 16, así que solo podría usar dos registros, pero estos están aquí para detener el ciclo de retardo pseudo-infinito ! Finalmente, ¿por qué el hecho de que OCR1A tenga doble búfer cambia algo?
Además, he intentado cambiar OCR1A a 15999: ahora, la frecuencia de parpadeo es de 565 mHz, mientras que con OCR1A = 15976 es exactamente de 500 mHz...
¡Déjame encender mi sistema y echar un vistazo!
Otro hecho sorprendente apareció: si trato de retrasar solo 10 ms ( LDI WR[1|2|3], BYTE[1|2|3](10)), la frecuencia ahora está alrededor de 45,44 Hz, en lugar de 50 Hz...
Hay algunas cosas que están pasando aquí. Dame algo de tiempo para instalar un 328 en mi banco para que pueda hacer algunas tomas de alcance.
Finalmente encontré el problema: ahora, el error es de aproximadamente 0.1 µs por cada microsegundo retrasado.
Ok, lo tengo todo configurado. Con solo mirar el código, parece que la comparación anterior ahora está completamente rota. ¿Puede actualizar la pregunta anterior con el mejor código más reciente que tiene junto con lo que no funciona correctamente y lo investigaremos?
Lo siento, me expresé mal: resolví el problema, provino del bucle Wait y no de nada relacionado con timer1. En este bucle de espera, hice un bucle hasta T > 1000, en lugar de hacerlo hasta T >= 1000. Cambiando eso, todo funciona correctamente y solo tengo un error de 0,01 %, en promedio. Entonces, la configuración correcta es OCR1A = 15999. Muchas gracias por su ayuda.

El valor de 0,506 Hz cuando esperaba 0,500 Hz está errado por un factor de 1,2 %.

Debe verificar la precisión de su interrupción de 1 ms para ver si también está desviada en un 1.2%.

Si eso está apagado, podría ser su intento de hacer que la interrupción exactamente 1 mseg necesite ser ajustado por un conteo o dos.