Cómo implementar una función de retardo precisa en Keil c51 (para c8051) que espera la cantidad exacta de relojes de CPU, el número de relojes varía de 1 a 255

0

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 funcionara en un rango más estrecho, el rango mínimo absoluto es de 20 a 127 relojes de CPU. Así que estos están por debajo o justo por encima de los retrasos de microsegundos (50MHz CPU clock), relativamente cortos de varios relojes a varias decenas de relojes. El problema con el sondeo de un temporizador es que la precisión resulta en un paso de 7 relojes como máximo, según la implementación, por ejemplo:

  1. while (! TF0) {} Mientras que, no, y operador de bits, todos juntos toman 7 relojes. Entonces, si llamo a cualquier cosa entre los relojes 15-21, se producirá un retraso de 21 relojes ...
  2. Uso de la interrupción del temporizador y el modo de parada de la CPU : proporciona buenos resultados para más de 50 relojes, probablemente depende de la condición actual de la CPU, por lo que a veces va más allá de 50 relojes, en el rango de 100 relojes debido a la interrupción y activación hasta la latencia, pero cualquier cosa debajo vuelve a ser plana 50 (o 100) relojes de CPU.
  3. Utilizando switch-case , con por ejemplo 30 entradas para 30 retrasos con 1 incremento de reloj, con un número diferente de NOPes como retardo, da como resultado una optimización del compilador que lo hace impredecible en términos de tiempo y, sobre todo, demasiado largo De nuevo, más de 100 relojes. Esto hace que el enfoque sea inutilizable.
  4. Estoy planeando probar la tabla de puntero a funciones con un número diferente de NOPes. Pero antes de intentarlo ya veo dos problemas en este enfoque: a. Requerirá mucha memoria y me queda 1k; segundo. la latencia de una función de vacío (vacío) dentro y fuera es de alrededor de 18 relojes, por lo que es muy ajustada para cumplir con el mínimo absoluto de 20 relojes que necesito ...

¿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 que el bucle en C, y la instrucción "djnz" toma 5-6 ciclos de CPU, en lugar de la hoja de datos que se indica 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

EDIT

Muchas gracias a todos por un gran aporte, no puedo imaginar que el flujo de ideas sea tan positivo y productivo. Así que mi profundo agradecimiento a todos los que contribuyeron, y contribuirá en el futuro. Entonces, después de sus valiosos aportes, y grandes ideas, se me ocurrió algo que me funciona, 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

Por lo tanto, PORT_ACTIVE (x) es una función #define que activa el puerto de pulsos. Como tengo todo el tiempo que necesito antes de comenzar el pulso, pude aprovechar la mayor parte de los gastos generales relacionados con las decisiones antes de la activación real del puerto. Entonces, la instrucción de retorno es prácticamente la misma cantidad de tiempo, por lo que ahora puedo generar un pulso con un mínimo de 8 ciclos de ciclos de ancho y hasta 20 ciclos. Ahora lo estoy extendiendo hasta 100 relojes, a expensas de la memoria de almacenamiento disponible, por supuesto. Y, de hecho, esta solución es, de hecho, gracias a la idea de JimmyB de poner la activación del pulso en la función y no antes, y, por supuesto, gracias a las grandes ideas de TCROSLEY, de cómo manejar los impares y hasta los retrasos, es simplemente que pasar al ensamblaje no es realmente amigable para la experiencia de depuración, y el código hace mucho más que simples retrasos, por lo que prefiero quedarme en C.

Una nota más, es que tan pronto como terminé de celebrar una solución de trabajo, encontré el siguiente problema.

SEGUNDO PROBLEMA

Necesito ejecutar un segundo pulso de regreso a la primera con ancho independiente. Así que no hay sobrecarga para el segundo pulso, de lo contrario terminará con un ancho variable. Casi me vuelve a colocar en el lugar en el que estaba antes, ya que el segundo pulso se limita nuevamente al cuello de botella de 6 ciclos del bucle while, a menos que haya una manera de colocar la sobrecarga de ramificación del segundo pulso antes del primer pulso. . ¿Alguna idea sobre eso?

    
pregunta Cezar

3 respuestas

2

Como han mencionado otros, esto se hace mejor en ensamblaje. Aquí está mi intento original de codificar esto, cuando pensé que las instrucciones de salto tomaron 2 o 4 ciclos (vea Editar a continuación para obtener 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 una llamada se realiza como ACALL (nn), donde nn es una constante o una variable en una variable de byte, por lo que el parámetro se puede pasar utilizando una instrucción MOV A, # n de un ciclo, por ejemplo. El tiempo mínimo que puede hacer es de 20 relojes, como pidió.

mov  A,#n        ; 1   
acall delay_sub  ; 4

No se comprueba si el parámetro es mayor o igual a 20, cualquier valor menor a 20 dará una sincronización incorrecta.

La instrucción mov y una llamada tomarán 5 ciclos. En primer lugar, la cuenta (i) se divide por dos para tener en cuenta 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, de modo que el valor en el registro que se va a disminuir (R7) se encuentra en el rango 1, 2, 3 ... R7 se reduce luego en un circuito cerrado (dos ciclos por cuenta). Hay un recuento 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 de ACALL o los de 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 un registro de cero; el manual del compilador le dirá qué registros se pueden usar dentro de una función de ensamblador sin tener que guardarlos (si los hay).

Esto tampoco tiene en cuenta la inhabilitación y la reactivación de las interrupciones, si es necesario, para garantizar que la rutina de sincronización no se interrumpirá.

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 la memoria caché de instrucciones está dañada o no). Esto puede ser diferente para otras versiones del 8051.

Finalmente, no he mostrado la sintaxis para saltar al ensamblaje en línea y volver a salir. La subrutina también se puede colocar en un archivo separado y ensamblar.

Editar

Desde que escribí el código original, el OP me informó que el número de ciclos de reloj para un salto en su 8051 es 5 o 6, no el 2 o 4 indicado en la hoja de datos que leí. Así que he reescrito la rutina para tener esto en cuenta. Desafortunadamente, esto supera el conteo mínimo de ciclos que puede cronometrarse a 32 en lugar de 20. Por lo tanto, si es absolutamente necesario manejar los conteos 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 supongo que la instrucción DJNZ toma 6 ciclos. Por lo tanto, tenemos que hacer un ciclo de 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. Estoy dejando el código original, en caso de que alguien tenga un 8051 con una instrucción DJNZ de dos ciclos.

Para conteos de 20-31, puede crear una subrutina con solo un nop, que lleva 12 ciclos, incluida la llamada y el retorno:

void delay12(void)
{
    nop
}

Para 20-23 conteos, lo llamaría una vez más agregando 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 sería cuesta solo cuatro instrucciones más la subrutina que se supone que se usará más de una vez.). Para 24 a 31 conteos, llamaría delay12 dos veces y agregaría 0 a 5 nops y / o una instrucción de salto según sea necesario.

Para retrasar 20 ciclos:

    acall delayl12
    jump next
next:
    nop
    nop
    
respondido por el tcrosley
1

Podrías (creo, no recuerdo la arquitectura 8051 que bien) hacer un salto calculado en un 'mar' de nops. Tal vez combínelo con un bucle para reducir el número requerido de nops (o puede haber una forma más sofisticada de hacerlo ...)

Hice eso una vez en la arquitectura MCS-48 anterior, pero relacionada, para lidiar con la latencia variable o algo similar.

Debería trabajar en ensamblaje para precisión de un solo ciclo. Keil admite un par de métodos de uso de ensamblaje en combinación con C, y probablemente el ensamblaje en línea más simple funcione para usted.

Habrá una sobrecarga en lo que sea que hagas, por lo que lo mejor que puedes hacer será algo como n + 1 a n + 255 ciclos de retraso. n < = 20 ciclos deben ser factibles.

    
respondido por el Spehro Pefhany
0

Sí, como señaló Spehro, debe trabajar en el ensamblaje y puede obtener una precisión de hasta un ciclo de la máquina utilizando el ensamblaje pero no un ciclo de reloj.

Dos maneras de usar lenguaje ensamblador

  1. Utilice los comandos y bucles nop de tal manera que los comandos y el tiempo de ejecución nops sean su demora de tiempo requerida. De esta manera, necesita saber cuántos ciclos de máquina toma cada comando en su código.

  2. Usa temporizadores.

Los compiladores modificarán su código C para optimizar el código, lo que seguramente cambiará su retraso, por lo que tenemos que ir hacia el ensamblaje para obtener precisión.

Como he estudiado lenguaje ensamblador de 8051 hace un año. No tiene una gran cantidad de instrucciones y si estudias seguramente aprenderás mucho sobre el microcontrolador y su arquitectura básica. Aunque no tiene que aprender a ensamblar un microcontrolador, pero saber cómo funciona el lenguaje ensamblador para al menos un microcontrolador, seguramente será útil.

    
respondido por el Jasser

Lea otras preguntas en las etiquetas