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.
gcc -Os -mcpu=cortex-m3
?ldr
/cbz reg, end_of_loop
para los internos, y aún uncmp
/bnz
en 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.Respuestas:
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:
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):
fuente
r2
que var1
, pero 6 ciclos desde el que va alr1
que va ar2
(debido alb loop
intermedio), cuando quiero (a lo sumo) 5 ciclos entre muestreos. Un problema fácil de solucionar es que alcanzamosout
después de un retraso mayor después de la muestra si la salida es porquer1
es cero que cuando es porquer2
es cero. En otras noticias,ldrb
desde una dirección de banda de bits causó problemas, cambió aldr
.nop
antesloop:
y 4nop
despuésout_fast:
hacen que las muestrasldr
posterioresout_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.out_fast
tal vez podría ser más compacto que 5nop
s, tal vez simplemente haciendo otroldr
, o tal vez haciendo unab
a 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).Puedes probar esto, pero sospecho que dará los mismos 6 ciclos.
fuente