Entonces, como dice el título, necesito retrasos exactos, no demasiado largos, idealmente en el rango de 0 a 350 relojes de CPU, pero si algo funciona en un rango más estrecho, el rango mínimo absoluto es de 20 a 127 relojes de CPU. Por lo tanto, estos están por debajo o justo por encima de los retrasos de un solo microsegundo (reloj de CPU de 50 MHz), relativamente cortos de varios relojes a varias decenas de relojes. El problema de sondear un temporizador es que la precisión da como resultado un paso de 7 relojes como máximo, según la implementación, por ejemplo:
¿Cómo abordar este tipo de problema? ¿Alguna idea será más que bienvenida?
Por cierto, lo ejecuto en el microcontrolador C8051F38x de Silicon Labs, usando C51 y Keil para codificar y compilar si eso importa.
El código que surgió como una solución parcial, parece que sigue el mismo tiempo mientras que el bucle en C lo hace, y la instrucción "djnz" toma 5-6 ciclos de CPU, en lugar de la hoja de datos indicada 2/4.
ACC_save= ACC;
ACC = counter102;
P0b3 = 1; // Start the Pulse
#pragma ASM // Precice DELAY using assembler
clr C // ; 1 Clear Carry
rrc A // ; 1 C = 1 if odd
jnc even // ; 2 or 4 extra 2 cycles if branch taken (spoils cache)
nop // ; 1
nop // ; 1
clr C // ; 1
even:
subb A,#4 // ; 1
mov R7,A // ; 1
loop:
djnz R7, loop // ; supposed to be 2, but practically takes 5 to 6 cycles!
#pragma ENDASM`
P0b3 = 0; // Stop the Pulse
EDITAR
Muchas gracias a todos por sus excelentes aportes, no podía imaginar que el flujo de ideas pudiera ser tan positivo y, lo que es más importante, productivo. Así que mi profundo agradecimiento a todos los que contribuyeron y contribuirán en el futuro. Entonces, después de sus valiosos aportes y sus excelentes ideas, se me ocurrió algo que funciona para mí, hasta cierto punto. El código está abajo:
void delay(unsigned char delay_time) {
switch (delay_time)
{case 8: goto Q08;
case 9: goto Q09;
case 10: goto Q10;
case 11: goto Q11;
case 12: goto Q12;
case 13: goto Q13;
case 14: goto Q14;
case 15: goto Q15;
case 16: goto Q16;
case 17: goto Q17;
case 18: goto Q18;
case 19: goto Q19;
case 20: goto Q20;
default : goto Q00; }
Q19: PORT_ACTIVE(1); // 2clk
Q17: PORT_ACTIVE(1); // 2clk
Q15: PORT_ACTIVE(1); // 2clk
Q13: PORT_ACTIVE(1); // 2clk
Q11: PORT_ACTIVE(1); // 2clk
Q09: PORT_ACTIVE(1); // 2clk
_nop_(); // 1clk
goto EXIT1; // Skip the Even delay part
Q20: PORT_ACTIVE(1); // 2clk
Q18: PORT_ACTIVE(1); // 2clk
Q16: PORT_ACTIVE(1); // 2clk
Q14: PORT_ACTIVE(1); // 2clk
Q12: PORT_ACTIVE(1); // 2clk
Q10: PORT_ACTIVE(1); // 2clk
Q08: PORT_ACTIVE(1); // 2clk
Q00: // 0clk
EXIT1:
return; // Exit from the function takes 7 clocks
} // END of function delay
// Continued execution after the delay function
PORT_ACTIVE(0); // 2clk
Entonces PORT_ACTIVE(x) es una función #define que activa el puerto pulsante. Dado que tengo todo el tiempo que necesito antes de comenzar el pulso, pude exprimir la mayor parte de los gastos generales relacionados con las decisiones antes de la activación real del puerto. Luego, la instrucción de retorno toma prácticamente siempre la misma cantidad de tiempo, por lo que ahora puedo generar un pulso con un mínimo de 8 ciclos de clk de ancho y hasta 20 ciclos. Ahora lo estoy ampliando hasta 100 relojes, a expensas de la memoria de almacenamiento disponible, por supuesto. Y entonces, esta solución es, de hecho, gracias a la idea de JimmyB de colocar la activación del pulso en la función y no antes, y por supuesto, gracias a las excelentes ideas de TCROSLEY, de cómo administrar los retrasos pares e impares, es solo que cambiar a ensamblaje no es realmente amigable para la experiencia de depuración,
Una nota más, es que tan pronto como terminé de celebrar una solución funcional, llegué al siguiente problema.
SEGUNDO PROBLEMA
Necesito ejecutar un segundo pulso espalda con espalda al primero con ancho independiente. Por lo tanto, no hay sobrecarga para el segundo pulso, de lo contrario, terminará con un ancho variable. Prácticamente me devuelve al punto en el que estaba antes, ya que el segundo pulso está nuevamente limitado al cuello de botella de 6 ciclos del ciclo while, a menos que haya una manera de colocar la sobrecarga de bifurcación para el segundo pulso antes del primer pulso. ¿Alguna idea al respecto?
Como han mencionado otros, esto se hace mejor en el ensamblaje. Aquí está mi intento original de codificar esto, cuando pensé que las instrucciones de salto tomaban 2 o 4 ciclos (ver Editar a continuación para ver la versión revisada).
void delay_sub(unsigned char i)
{
// convert 20, 21, 22 etc to count in R7 of 1, 2, 3 (extra cycle added if i is odd)
; cycles
rrc A ; 1 c = 1 if odd
jnc even ; 2 or 4 extra 2 cycles if branch taken (spoils cache)
nop ; 1 delete if using lcall's instead of acall's
nop ; 1 same
clc ; 1 in either case carry is clear prior to subb
even:
subb A,#9 ; 1
mov R7,A ; 1 R7 now = (i / 2) - 9
//while (i--);
loop:
djnz R7, loop ; 2 loop address should be in cache, so no extra cycles needed
ret ; 6
}
timing calculation (assuming acall's)
if i even:
5+7+R7*2+6 = minimum of 20 22 24 ... => R7 = 1, 2, 3 ...
if i odd:
5+8+R7*2+6 = minimum of 21 23 25 ... => R7 = 1, 2, 3 ...
Supone que se realiza una llamada como ACALL(nn), donde nn es una constante o una variable en una variable de byte, de modo que el parámetro se puede pasar mediante una instrucción MOV A,#n de un ciclo, por ejemplo. El cronometraje mínimo que puedes hacer es de 20 relojes, como lo pediste.
mov A,#n ; 1
acall delay_sub ; 4
No hay verificación de que el parámetro sea mayor o igual a 20, cualquier valor menor a 20 dará una temporización incorrecta.
La instrucción mov y acall tomarán 5 ciclos. En primer lugar, la cuenta (i) se divide por dos para dar cuenta de que la instrucción DJNZ toma dos ciclos. Luego, el conteo se ajusta para agregar un ciclo si i es impar. Finalmente, se resta un valor fijo para que el valor en el registro a decrementar (R7) esté en el rango 1, 2, 3 ... R7 luego se decrementa en un ciclo cerrado (dos ciclos por conteo). Hay un número de ciclos fijo de 6 para la devolución.
Si tiene que usar un LCALL en lugar de un ACALL, el tiempo mínimo que puede hacer será de 21 relojes en lugar de 20, y deberá eliminar los dos nop después de la instrucción jnc. Tienes que usar todos los ACALL o LCALL, no puedes mezclarlos.
Evitaría usar C para llamar a la función a menos que pueda garantizar que el compilador no agregue una sobrecarga adicional. Además, estoy usando R7 como registro de scratch; su manual del compilador le dirá qué registros se pueden usar dentro de una función de ensamblador sin tener que guardarlos (si corresponde).
Esto tampoco tiene en cuenta la desactivación y reactivación de las interrupciones, si es necesario, para garantizar que la rutina de temporización no se interrumpa.
El comportamiento de las instrucciones de salto se basa en la hoja de datos del C8051F38x tal como lo entiendo (en términos de cuándo se daña o no la memoria caché de instrucciones). Esto puede ser diferente para otras versiones del 8051.
Finalmente, no he mostrado la sintaxis para saltar al ensamblado en línea y volver a salir. La subrutina también podría colocarse en un archivo separado y ensamblarse.
Desde que escribí el código original, el OP me informó que la cantidad de ciclos de reloj para un salto en su 8051 es 5 o 6, no los 2 o 4 indicados en la hoja de datos que leí. Así que he reescrito la rutina para tener esto en cuenta. Desafortunadamente, esto aumenta el recuento mínimo de ciclos que se puede programar en 32 en lugar de 20. Por lo tanto, si es absolutamente necesario manejar los recuentos entre 20 y 31, será necesario escribir un código de propósito especial específico para ese caso (ver más abajo).
void delay_sub(unsigned char i)
{
// minimum value of i is 32
; cycles
clr C ; 1
subb A,#32 ; 1 adjust for overhead of call and this routine
; a branch could be added here in case the result is negative
mov B,#6 ; 1
div AB ; 4 quotient in A, remainder in B
mov DPTR,#adjustcycles ; 1
mov R7,B ; 1
mov B,A ; 1 save quotient in B as temp
mov A,#6 ; 1
clr C ; 1
subb A,R7 ; 1 A now has 5 - B (remainder)
mov R7,#0 ; 1
jmp @A+DPTR ; 6 jump into table to add clocks based on remainder
adjustcycles: ; execute additional cycles based on remainder
inc R7 ; 1 for remainder of 5
inc R7 ; 1 for remainder of 4
inc R7 ; 1 for remainder of 3
inc R7 ; 1 for remainder of 2
inc R7 ; 1 for remainder of 1
nop ; 1 for remainder of 0
mov A,B ; 1 now has i / 6, have already adjusted for remainder
loop:
djnz loop ; 6
ret ; 6
timing in clock cycles is: 5 (call) + 21 (fixed overhead) + 6*(i/6) + (i%6) + 6 (ret)
if i = 0, 5 + 21 + 6 = 32 therefore that is the minimum count
En lugar de dividir el parámetro i por 2 como en el ejemplo anterior, ahora tengo que dividirlo por 6 porque asumo que la instrucción DJNZ toma 6 ciclos. Entonces, necesitamos hacer un ciclo i / 6 veces, y también agregar de 0 a 5 ciclos para el resto (i % 6).
El resto de mis comentarios anteriores se aplican bastante bien a este ejemplo. Dejo el código original, en caso de que alguien tenga un 8051 con una instrucción DJNZ de dos ciclos.
Para cuentas de 20 a 31, podría crear una subrutina con solo un nop, que toma 12 ciclos, incluida la llamada y la devolución:
void delay12(void)
{
nop
}
Para 20-23 conteos, lo llamaría una vez más agregaría de 8 a 11 nops después de la llamada (o un salto ficticio a la siguiente instrucción que consumiría 6 ciclos más 2 a 5 nops, por lo que retrasar 20 ciclos costaría solo cuatro instrucciones más la subrutina que se supone que se utiliza más de una vez). Para conteos de 24 a 31, llamaría a delay12 dos veces y agregaría de 0 a 5 nops y/o una instrucción de salto según sea necesario.
Así que para retrasar 20 ciclos:
acall delayl12
jump next
next:
nop
nop
djnz R7, loop
con dos comandos dec A
:; y luego JNZ loop
, pero el mismo resultado ... Estoy haciendo algo mal y no soy lo suficientemente bueno en ensamblaje para averiguar qué es ...Podrías (creo, no recuerdo bien la arquitectura 8051) hacer un salto calculado en un 'mar' de nops. Quizás combínelo con un ciclo para reducir el número requerido de nops (o puede haber una forma más sofisticada de hacerlo...)
Lo hice una vez en la arquitectura MCS-48 anterior, pero relacionada, para lidiar con la latencia variable o algo similar.
Debe trabajar en ensamblaje para precisión de ciclo único. Keil admite un par de métodos para usar ensamblado en combinación con C, y probablemente el ensamblado en línea más simple funcione para usted.
Independientemente de lo que haga, habrá algunos gastos generales, por lo que lo mejor que puede hacer será algo como n+1 a n+255 ciclos de retraso. n <= 20 ciclos deberían ser factibles.
Sí, como señaló Spehro, debe trabajar en ensamblaje y puede obtener una precisión de hasta un ciclo de máquina usando ensamblaje pero no un ciclo de reloj.
Dos formas de usar lenguaje ensamblador
Use los comandos y bucles nop de tal manera que el tiempo de ejecución de los comandos y nops sea el tiempo de demora requerido. De esta manera, necesita saber cuántos ciclos de máquina toma cada comando en su código.
Usa temporizadores.
Los compiladores modificarán su código C para optimizar el código, lo que seguramente cambiaría su retraso, por lo que debemos avanzar hacia el ensamblaje para mayor precisión.
Como he estudiado lenguaje ensamblador de 8051 hace un año. No tiene mucho conjunto de instrucciones y si estudias seguramente aprenderás mucho sobre microcontroladores y su arquitectura básica. Aunque no tiene que aprender a ensamblar ningún microcontrolador, seguramente sería útil saber cómo funciona el lenguaje ensamblador para al menos un microcontrolador.
crosley
david tweed
cézar
cézar
jaser
jimmyb
jimmyb
crosley
cézar