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.
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.
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.
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 NOP
después del vector Timer1comp. Esto NOP
en realidad está aterrizando en el siguiente vector. Si bien no hace daño aquí, es al menos superfluo.
-El CS01
bit en la línea LDI WR1, (1<<WGM12)|(1<<CS10) ; Timer in CTC mode, TOP = OCR1A
en 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 :Loop
porque, 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!
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.
espirina
espirina
gran josh
espirina
LDI WR[1|2|3], BYTE[1|2|3](10)
), la frecuencia ahora está alrededor de 45,44 Hz, en lugar de 50 Hz...gran josh
espirina
gran josh
espirina
gran josh