El circuito de sondeo más rápido: ¿cómo puedo recortar 1 ciclo de CPU?


8

En una aplicación en tiempo real¹ en un ARM Cortex M3 (similar a STM32F101), necesito sondear un poco del registro de un periférico interno hasta que sea cero, en un bucle lo más ajustado posible. Utilizo bandas de bits para acceder al bit apropiado. El código C (de trabajo) es

while (*(volatile uint32_t*)kMyBit != 0);

Ese código se copia en la RAM ejecutable en el chip. Después de una optimización manual², el ciclo de sondeo se reduce a lo siguiente, que cronometré³ a 6 ciclos:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 D1FC      BNE      0x00600200

¿Cómo se puede reducir la incertidumbre del sondeo? Un ciclo de 5 ciclos encajaría en mi objetivo: muestrear el mismo bit lo más cerca posible a 15.5 ciclos después de que se puso a cero.

Mi especificación requiere la detección confiable de un pulso bajo al menos 6.5 ciclos de reloj de la CPU; clasificándolo confiablemente como corto si dura menos de 12.5 ciclos; y clasificarlo de manera confiable siempre que dure más de 18.5 ciclos. Los pulsos no tienen una relación de fase definida con el reloj de la CPU, que es mi única referencia de sincronización precisa. Eso requiere un bucle de sondeo de 5 relojes como máximo. En realidad, estoy emulando código que se ejecutó en una CPU de 8 bits de hace décadas que podría sondear con un ciclo de 5 relojes, y lo que hizo se ha convertido en la especificación.


Traté de compensar la alineación del código insertando NOP antes del ciclo, en las muchas variantes que probé, pero nunca observé un cambio.

Traté de invertir el CMP y LDR, pero aún obtengo 6 ciclos:

0x00600200 681A      LDR      r2,[r3,#0x00]
; we loop here
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FC      BNE      0x00600202

Este es de 8 ciclos.

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 681A      LDR      r2,[r3,#0x00]
0x00600204 2A00      CMP      r2,#0x00
0x00600206 D1FB      BNE      0x00600200

Pero este es de 9 ciclos:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FB      BNE      0x00600200

¹ Medir cuánto tiempo el bit es bajo, en un contexto donde no se produce ninguna interrupción.

² El código inicial generado por el compilador utilizó r12 como el registro de destino, y eso agregó 4 bytes de código al bucle, lo que costó 1 ciclo.

³ Los números proporcionados se obtienen con un emulador STIce en tiempo real supuestamente preciso en el ciclo y su función de activación del emulador en la lectura en la dirección del registro. Anteriormente probé el contador "Estados" con un punto de interrupción en el bucle, pero el resultado depende de la ubicación del punto de interrupción. Un solo paso es aún peor: siempre da 4 ciclos para LDR, cuando eso es al menos en algún momento hasta 3.


la alineación puede importar, el dominio del reloj gpio podría dominar el rendimiento, así como los estados de espera flash, serán 3+ relojes pero podrían ser 6+ o incluso más dependiendo. Esperaría que las bandas de bits no sean un éxito en el rendimiento de las lecturas, pero podría probar el bit en lugar de compararlo con cero y ver. En
resumen,

1
¿El modo Thumb no tiene una instrucción cbnz para comparar y ramificar en otro registro que es cero? ¿Compilaste con gcc -Os -mcpu=cortex-m3?
Peter Cordes

1
@ Peter Cordes: no estoy usando gcc, sino ArmCC 5 (compilador de la generación anterior de ARM antes de usar LLVM). La optimización es por tiempo y al máximo, y se supone que las opciones para la CPU se configurarán automáticamente por el IDE, pero lo comprobaré. Sí, hay CBZ / CBNZ, pero a medida que leo el documento no puede ramificarse hacia atrás.
fgrieu

1
Ok, entonces (o el compilador) podría desenrollarse con ldr/ cbz reg, end_of_looppara los internos, y aún un cmp/ bnzen la parte inferior. Pero eso le daría un intervalo de sondeo no uniforme, por ejemplo, 1 de cada 8 sondeos, en caso de que sea importante.
Peter Cordes

2
¿Estás seguro de que no has malinterpretado la especificación? Tal vez la especificación si se refiere a "ciclos específicos del dispositivo que no son ciclos de CPU" (por ejemplo, un temporizador o UART con su propia fuente de reloj que tiene ciclos mucho más lentos), y tal vez "tan corto como 13 ciclos específicos del dispositivo" podría ser "tan corto como 13000 ciclos específicos de CPU ".
Brendan

Respuestas:


8

Si entiendo la pregunta correctamente, no son necesariamente los ciclos de bucle los que deben reducirse, sino el número de ciclos entre muestras consecuentes (es decir, instrucciones LDR). Pero puede haber más de un LDR por iteración. Puedes probar algo como esto:

    ldrb    r1, [r0]

loop:
    cbz     r1, out
    ldrb    r2, [r0]
    cbz     r2, out
    ldrb    r1, [r0]
    b       loop

out:

El espacio entre las dos instrucciones LDRB varía para que las muestras no estén espaciadas uniformemente.

Esto puede retrasar ligeramente la salida del bucle, pero de la descripción del problema no puedo decir si es importante o no.

Tengo acceso al modelo M7 con precisión de ciclo, y cuando el proceso se estabiliza, su ciclo original se ejecuta en M7 en 3 ciclos por iteración (es decir, LDR cada 3 ciclos), mientras que el ciclo propuesto anterior se ejecuta en 4 ciclos, pero ahora hay dos LDR allí (entonces LDR cada 2 ciclos). La tasa de muestreo definitivamente mejora.

Para dar crédito, @Peter Cordes propuso un desenrollamiento con CBZ como un descanso .

Es cierto que M3 será más lento, pero aún así vale la pena intentarlo, si es la frecuencia de muestreo que buscas.

También puede verificar si LDRB en lugar de LDR (como en el código anterior) cambia algo, aunque no espero que lo haga.

UPD: Tengo otra versión de bucle 2-LDR que en M7 completa en 3 ciclos que puede probar por interés (también las interrupciones CBZ permiten un fácil equilibrio de las rutas después del bucle):

    ldr     r1, [r0]

loop:
    ldr     r2, [r0]
    cbz     r1, out_slow
    cbz     r2, out_fast
    ldr     r1, [r0]
    b       loop

out_fast:
    /* NOPs as required */

out_slow:

Buenas noticias: funciona a 10 ciclos / bucle con 2 muestreos, por lo que la frecuencia de muestreo promedio está bien. Problema grave: el retraso entre muestras es de 4 ciclos desde el que va al r2que va r1, pero 6 ciclos desde el que va al r1que va a r2(debido al b loopintermedio), cuando quiero (a lo sumo) 5 ciclos entre muestreos. Un problema fácil de solucionar es que alcanzamos outdespués de un retraso mayor después de la muestra si la salida es porque r1es cero que cuando es porque r2es cero. En otras noticias, ldrbdesde una dirección de banda de bits causó problemas, cambió a ldr.
fgrieu

2
Me alegro de que funcionó :) No quería entrar en ecualizar las muestras antes de confirmar que la idea básica funcionó para usted. También es difícil predecir cómo se ejecuta mi M7 se traduce en M3. Le di esa versión en bucle porque produjo una frecuencia de muestreo uniforme en M7 (LDR cada 2 ciclos). Pero también tuve otra versión que en realidad fue más rápida en M7 (3 ciclos por ciclo y aún 2 LDR), puedes probarlo por interés. Actualizaré mi respuesta.
alex_mv

3
Confirmo que su segundo ciclo de sondeo se ejecuta en 10 ciclos con dos muestras equidistantes en mi emú Cortex-M3. Aplasta mi respuesta (ahora eliminada) en simplicidad, y permite probar pulsos más cortos. 2 nopantes loop:y 4 nopdespués out_fast:hacen que las muestras ldrposteriores out_slow:10 ciclos después de la muestra que se vio por primera vez en cero, cualquiera de los tres que fue. Mi especificación (como está redactada en la pregunta) requiere 13, y eso es trivial de ajustar. ¡Problema 100% resuelto! Muchas gracias, así como a Peter Cordes por su comentario, y B Degan por la primera recompensa.
fgrieu

@fgrieu: oh sí, ese es un truco inteligente en la última actualización. out_fasttal vez podría ser más compacto que 5 nops, tal vez simplemente haciendo otro ldr, o tal vez haciendo una ba la siguiente instrucción, si eso lleva más ciclos que un NOP en su CPU sin contaminar la predicción de rama (si la hay).
Peter Cordes

1

Puedes probar esto, pero sospecho que dará los mismos 6 ciclos.

0x00600200 581a      LDR      r2,[r3,r0]; initialize r0 to 0x0
0x00600202 4282      CMP      r2,r0
0x00600204 D1FC      BNE      0x00600200

44
Lo intenté: 6 ciclos :-(
fgrieu
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.