Buenas...
Después de varios días de investigación, ya queda claro el funcionamiento de este tipo de retardos.
(Sigue una explicación de cómo funcionan, luego una muy breve explicación de cómo calcular los parámetros, y el anuncio de un programa en Perl, más unas hojas de cálculo, que ayudan a la creación de vuestros propios retardos)
Funcionamiento
Para explicarlo, veamos un ejemplo:
Código:
; Delay = 400ms
; Clock frequency = 4mhz
; Actual delay = 0.4 seconds = 400000 cycles
; Error = 0.00 %
cblock 0x70
d1
d2
d3
endc
;399999 cycles
movlw 0x36
movwf d1
movlw 0xE0
movwf d2
movlw 0x01
movwf d3
Delay_0
decfsz d1, f
goto $+2
decfsz d2, f
goto $+2
decfsz d3, f
goto Delay_0
;1 cycles
nop
La teoría de funcionamiento es la siguiente: el retraso se produce por la ejecución de bucles anidados que, sencillamente, consumen ciclos de instrucción. La cuestión es encontrar la combinación adecuada de ciclos anidados para que se aproxime lo más posible al retardo o espera que deseamos.
Los bucles anidados consumen una serie de ciclos fijos, y otros variables. Un bucle, esencialmente, consume 3 ciclos cada vez que se ejecuta la instrucción de decrementado (1 ciclo) más el goto que le sigue (2 ciclos), y 2 ciclos si al hacer el decremento (1 ciclo) el contador pasa de tener un valor '1' a '0', por lo que entonces el procesador 'salta' la instrucción siguiente (el goto), consumiendo 1 ciclo más.
Simplificando, podemos decir que, si un bucle comienza con un determinado valor en el contador 'd1', realizará (d1-1) vueltas consumiendo 3 ciclos, más una última vuelta que consumirá 2 ciclos.
Ahora bien... 3 y 2 ciclos son cifras pequeñas, que para bucles cortos están bien, pero si queremos esperas más largas, nos obligará a hacer demasiados bucles.
La solución que adoptan los autores del Generador de Golovchenko es la de aumentar la cantidad de ciclos de instrucción consumidos por los bucles más internos: cuanto más niveles internos hay, más ciclos deben consumir.
En el ejemplo, vemos que el bucle de 'd1', consume 7 ciclos de forma normal (1 ciclo para decrementar, y 2 ciclos por cada uno de los 3 goto que se ejecutan en cascada). Y en la última vuelta de 'd1', consume los 2 ciclos de siempre para llegar a la instrucción de decrementado de 'd2'.
Y con 'd2' pasa lo mismo: consume 5 ciclos (1 del decremento y 4 ciclos por los 2 goto que le siguen). Para 'd3', tenemos el caso sencillo de 3 ciclos por vuelta (1 decremento más 2 de un goto).
A estos ciclos consumidos por los bucles hay que sumar los 2 ciclos * número de bucles anidados, correspondientes a las instrucciones mov* que hay antes de los bucles, donde se realiza la carga de los contadores.
Un detalle... Supongamos que 'd1' -como en el ejemplo-, comienza con el valor 0x36. Entonces el bucle más interno consumirá (0x36-1) vueltas, a razón de 7 ciclos por vuelta, más una última vuelta, con 2 ciclos más. En ese momento, 'd1' queda con el valor '0'. A continuación, entramos en el decremento de 'd2', que pasará de 0xE0 a 0
F. Y saltará a ejecutar de nuevo el bucle interno de 'd1'. Como 'd1' valía '0', tenemos entonces que el bucle más interno se ejecutará (256-1) veces consumiendo 7 ciclos cada vez, hasta que en la última vuelta, cuando pase de '1' a '0', consumirá 2 ciclos más.
Entonces, tenemos que los primeros valores que damos a los contadores, son un 'ajuste fino' de los bucles, sabiendo que una vez que se agoten, en la siguiente vuelta se ejecutará un 'giro completo' de 256-1 vueltas de ese mismo bucle. Esto podríamos llamarlo el 'núcleo duro' del retardo. Y ese 'núcleo duro' se ejecutará tantas veces como indique el bucle anidado más externo a él (menos en la primera vuelta, claro).
Puede parecer que es muy complicado, entonces, saber cómo calcular cuántos bucles anidados y qué valores poner al inicio, en los contadores.
Simplificación
Pero, resulta que no lo es tanto. El efecto de varios goto en cascada, permite simplificar el cálculo, ya que los ciclos consumidos por los bucles más externos, en las últimas vueltas, se compensan con los ciclos consumidos con los goto, de tal manera, que al final todo depende del número de ciclos consumido por el bucle más interno.
Ejemplo: Para 3 bucles anidados, tenemos que:
- el bucle 'd1' consume 7 ciclos por vuelta
- el bucle 'd2' consume 5 ciclos por vuelta
- el bucle 'd3' consume 5 ciclos por vuelta
- la última vuelta de cada bucle, siempre consume 2 ciclos, independientemente de su nivel de anidamiento
De forma matemática, queda así:
<ciclos consumidos por 3 niveles de anidamiento> =
<ciclos consumidos por 2 niveles de anidamiento, en su primero vuelta> +
<ciclos consumidos por cada vuelta en el tercer nivel> *
(<ciclos consumidos por el decremento en el tercer nivel> +
<ciclos consumidos por el núcleo duro del segundo nivel>)
+<ciclos de la última vuelta del tercer nivel>
Esta es una fórmula que podemos desplegar, pero es más fácil si la vemos con los valores del ejemplo (3 niveles de anidamiento):
d3 = <nivel 2>
+ (d3-1)(3 + <núcleo duro nivel 2>)+2 =
d3 = <nivel 1>
+ (d2-1)(5+<núcleo duro nivel 1>)+2
+ (d3-1)(3+<núcleo duro nivel 2>)+2 =
d3 = 0+(d1-1)(7+0)+2
+ (d2-1)(5+0+(256-1)(0+7)+2)+2
+ (d3-1)(3+<núcleo duro nivel 1>+(256-1)(<núcleo duro nivel 1>+5)+2)+2 =
d3 = 7(d1-1)+2
+ (d2-1)(5+ 7(256-1)+2)+2
+ (d3-1)(3+ 7(256-1)+2 +(256-1)(7(256-1)+2+5)+2)+2 =
d3 = 6 + 7(d1-1)
+ (d2-1)(5+ 7*256-7+2)
+ (d3-1)(3+ 7*256-7+2 +(256-1)(7*256-7+2+5)+2) =
d3 = 6 + 7(d1-1)
+ (d2-1)(7*256)
+ (d3-1)(3+ 7*256-7+2 +(256-1)(7*256)+2) =
d3 = 6 + 7(d1-1)
+ 7(d2-1)256
+ (d3-1)(7*256 +7(256-1)256) =
d3 = 6 + 7(d1-1)
+ 7(d2-1)256
+ (d3-1)(7(256 + (256-1)256)) =
d3 = 6 + 7(d1-1)
+ 7(d2-1)256
+ (d3-1)(7(256 (1 + (256-1))) =
d3 = 6 + 7(d1-1)
+ 7(d2-1)256
+ 7(d3-1)256*256 =
d3 = 6 + 7((d1-1) + (d2-1)256 + (d3-1)256*256)
Y ya vemos un patrón:
<ciclos consumidos por N niveles de anidamiento> =
2 * N
+ <ciclos consumidos por el nivel más anidado = 2*N+1>
* (
(<contador nivel más anidado>-1)
+(<contador nivel siguiente>-1) * 256
+(<contador nivel siguiente>-1) * 256 * 256
+( ... )
)
Vemos que obtenemos la simplificación comentada antes: los ciclos que consumen los bucles más externos (5 y 3 ciclos) no aparecen en la fórmula. Sólo son relevantes los del bucle más interno. Esto simplifica el cálculo de los contadores, para obtener un determinado retardo.
Sólo falta añadirle la cantidad de ciclos consumidos por la carga de los contadores y los ciclos extra que necesitamos al final (ver explicación unos párrafos más abajo).
Contadores
Ese cálculo se puede realizar de varias maneras. Una de ellas es ir probando partiendo de un sólo bucle anidado. Si con los ciclos consumidos no es suficiente, probamos con un nivel de anidamiento más.
En cada vuelta de este cálculo, hacemos lo siguiente: dividimos de forma sucesiva la cantidad de ciclos del retardo por el número de ciclos del bucle más interno, y luego por una división entera de (256^(nivel de anidamiento-1)). Esto último es para saber la cantidad de vueltas del 'núcleo duro' que necesitamos generar (ver pseudo-código en el mensaje anterior).
Para tres niveles, por ejemplo
PHP:
ciclos := número_de_ciclos_que_deseamos_generar - (2 * 2 * 3)
loop := int(ciclos / (1 + 2 * 3))
d1 := 1 + (int(loop/1) % 256) = 1 + loop % 256
d2 := 1 + (int(loop/256) % 256)
d3 := 1 + (int(loop/65536) % 256)
ciclos_restantes_al_final := ciclos - loop * (1 + 2 * 3)
Al final, como es normal, el cálculo del retardo no es perfecto. Suelen quedar (<niveles de anidamiento>*2+1)-1 ciclos fuera del cálculo, pero eso se remedia con el añadido, al final de unos cuantos goto +$0 que consumen 2 ciclos cada uno, y si acaso un nop, que consumirá 1 ciclo más.
El cálculo no es sencillo si lo hacemos de forma manual. Pero para eso están los ordenadores
Adjunto un libro de hojas de cálculo (en formato OpenDocument) donde, cada hoja, contiene los cálculos para diversos niveles de anidamiento: desde 1 a 5 niveles. Con un nivel de anidamiento -vamos, un solo bucle-, se pueden obtener retardos de casi 800 ciclos, mientras que con 5 niveles, se puede obtener retardos de más de 12 billones de ciclos. Para un sistema a 4 Mhz, eso son unos 139 días.
He creado también
un programa en Perl v5.14 que produce la misma salida que el Generador de Golovchenko. Estos son los argumentos de entrada:
Código:
./delay_pic.pl [-s <nombre subrutina>] <frecuencia([hz]|khz|mhz|ghz)> <espera([c]|d|h|m|s|ms|us|ns)>
Subrutina:
-s: generar código para subrutina
Frecuencia:
hz : hertzios (por defecto)
khz: kilohertzios
mhz: megahertzios
ghz: gigahertzios
Retardo:
c: ciclos de procesador (por defecto)
d: días
h: horas
m: minutos
s: segundos
ms: milisegundos
us: microsegundos
ns: nanosegundos
Ejemplos:
./delay_pic.pl 4Mhz 300ms
./delay_pic.pl 32768Hz 1s
./delay_pic.pl 20000000 80000000
Al programa hay que darle dos parámetros: la frecuencia de reloj del sistema, y la cantidad de retardo que queremos. Ese retardo se puede expresar en unidades de tiempo o en ciclos de procesador.
De forma opcional, se puede indicar el argumento '-s' junto con un nombre, y genera el código necesario para hacer un retardo en forma de subrutina, con el nombre indicado, y teniendo en cuenta los ciclos consumidos por el call y el return de la misma.
Ejemplo:
Código:
$ ./delay_pic.pl -s Delay 4mhz 400ms
; Delay = 400ms
; Clock frequency = 4mhz
; Actual delay = 0.4 seconds = 400000 cycles
; Error = 0.00 %
cblock 0x70
d1
d2
d3
endc
Delay
;399992 cycles
movlw 0x35
movwf d1
movlw 0xE0
movwf d2
movlw 0x01
movwf d3
Delay_0
decfsz d1, f
goto $+2
decfsz d2, f
goto $+2
decfsz d3, f
goto Delay_0
;4 cycles
goto $+1
goto $+1
;4 cycles (including call)
return
; Mon May 19 03:21:05 2014
; Generated by delay_pic.pl (Joaquin Ferrero. May 19, 2014 version)
Bueno, el resultado no es exactamente idéntico, pero son detalles menores (como es el caso de darle un valor inicial al cblock).
Naturalmente, como el código Perl está disponible, se puede modificar como se quiera (por ejemplo, el nombre de los contadores).
Y nada más. Espero que esta explicación, las hojas de cálculo y el programa os sirvan para vuestros proyectos.
Saludos.