Conteo de ciclos con CPU modernas (por ejemplo, ARM)

En muchas aplicaciones, una CPU cuya ejecución de instrucción tiene una relación de tiempo conocida con los estímulos de entrada esperados puede manejar tareas que requerirían una CPU mucho más rápida si no se conociera la relación. Por ejemplo, en un proyecto que hice usando un PSOC para generar video, usé código para generar un byte de datos de video cada 16 relojes de CPU. Dado que probar si el dispositivo SPI está listo y bifurcarse si no, IIRC tomaría 13 relojes, y cargar y almacenar datos de salida tomaría 11, no había forma de probar la preparación del dispositivo entre bytes; en cambio, simplemente arreglé que el procesador ejecutara exactamente 16 ciclos de código por cada byte después del primero (creo que usé una carga indexada real, una carga indexada ficticia y una tienda). La primera escritura SPI de cada línea ocurrió antes del inicio del video, y para cada escritura subsiguiente había una ventana de 16 ciclos en la que la escritura podía ocurrir sin saturación o subejecución del búfer. El ciclo de bifurcación generó una ventana de incertidumbre de 13 ciclos, pero la ejecución predecible de 16 ciclos significó que la incertidumbre para todos los bytes posteriores se ajustaría a esa misma ventana de 13 ciclos (que a su vez encajaría dentro de la ventana de 16 ciclos de cuando la escritura podría aceptarse). ocurrir).

Para las CPU más antiguas, la información de temporización de las instrucciones era clara, disponible e inequívoca. Para los ARM más nuevos, la información de tiempo parece mucho más vaga. Entiendo que cuando el código se ejecuta desde flash, el comportamiento del almacenamiento en caché puede hacer que las cosas sean mucho más difíciles de predecir, por lo que esperaría que cualquier código contado por ciclos se ejecute desde la RAM. Sin embargo, incluso cuando se ejecuta código desde la RAM, las especificaciones parecen un poco vagas. ¿Sigue siendo una buena idea el uso de código contado por ciclos? Si es así, ¿cuáles son las mejores técnicas para que funcione de manera confiable? ¿Hasta qué punto se puede suponer con seguridad que un proveedor de chips no introducirá silenciosamente un chip "nuevo y mejorado" que elimina un ciclo de la ejecución de ciertas instrucciones en ciertos casos?

Suponiendo que el siguiente ciclo comienza en un límite de palabra, ¿cómo se determinaría en función de las especificaciones exactamente cuánto tiempo tomaría (suponga que Cortex-M3 con memoria de estado de espera cero; nada más sobre el sistema debería importar para este ejemplo).

mi bucle:
  mov r0,r0 ; Instrucciones breves y sencillas para permitir la búsqueda previa de más instrucciones
  mov r0,r0 ; Instrucciones breves y sencillas para permitir la búsqueda previa de más instrucciones
  mov r0,r0 ; Instrucciones breves y sencillas para permitir la búsqueda previa de más instrucciones
  mov r0,r0 ; Instrucciones breves y sencillas para permitir la búsqueda previa de más instrucciones
  mov r0,r0 ; Instrucciones breves y sencillas para permitir la búsqueda previa de más instrucciones
  mov r0,r0 ; Instrucciones breves y sencillas para permitir la búsqueda previa de más instrucciones
  agrega r2,r1,#0x12000000 ; instrucción de 2 palabras
  ; Repita lo siguiente, posiblemente con diferentes operandos
  ; Seguirá agregando valores hasta que ocurra un acarreo
  itcc
  agregacc r2,r2,#0x12000000 ; Instrucción de 2 palabras, más "palabra" extra para itcc
  itcc
  agregacc r2,r2,#0x12000000 ; Instrucción de 2 palabras, más "palabra" extra para itcc
  itcc
  agregacc r2,r2,#0x12000000 ; Instrucción de 2 palabras, más "palabra" extra para itcc
  itcc
  agregacc r2,r2,#0x12000000 ; Instrucción de 2 palabras, más "palabra" extra para itcc
;...etc, con más instrucciones condicionales de dos palabras
  secundario r8,r8,#1
  mi bucle bpl

Durante la ejecución de las primeras seis instrucciones, el núcleo tendría tiempo para buscar seis palabras, de las cuales se ejecutarían tres, por lo que podría haber hasta tres precargadas. Las siguientes instrucciones son de tres palabras cada una, por lo que no sería posible que el núcleo obtenga las instrucciones tan rápido como se están ejecutando. Esperaría que algunas de las instrucciones "eso" tomaran un ciclo, pero no sé cómo predecir cuáles.

Sería bueno si ARM pudiera especificar ciertas condiciones bajo las cuales el tiempo de la instrucción "it" sería determinista (por ejemplo, si no hay estados de espera o contención de bus de código, y las dos instrucciones anteriores son instrucciones de registro de 16 bits, etc.) pero no he visto ninguna de esas especificaciones.

Ejemplo de aplicación

Supongamos que uno está tratando de diseñar una placa secundaria para un Atari 2600 para generar una salida de video por componentes a 480P. El 2600 tiene un reloj de píxeles de 3,579 MHz y un reloj de CPU de 1,19 MHz (reloj de puntos/3). Para video componente de 480P, cada línea debe emitirse dos veces, lo que implica una salida de reloj de puntos de 7,158 MHz. Debido a que el chip de video de Atari (TIA) emite uno de los 128 colores utilizando una señal de luminancia de 3 bits más una señal de fase con una resolución de aproximadamente 18 ns, sería difícil determinar con precisión el color con solo observar las salidas. Un mejor enfoque sería interceptar escrituras en los registros de color, observar los valores escritos y alimentar cada registro en los valores de luminancia TIA correspondientes al número de registro.

Todo esto se podría hacer con un FPGA, pero algunos dispositivos ARM bastante rápidos se pueden obtener mucho más baratos que un FPGA con suficiente RAM para manejar el almacenamiento en búfer necesario (sí, sé que para los volúmenes que se pueden producir, el costo es ' t un factor real). Sin embargo, requerir que el ARM mire la señal del reloj entrante aumentaría significativamente la velocidad requerida de la CPU. Los recuentos de ciclos predecibles podrían hacer las cosas más limpias.

Un enfoque de diseño relativamente simple sería hacer que un CPLD observe la CPU y el TIA y genere una señal de sincronización RGB+ de 13 bits, y luego haga que ARM DMA tome valores de 16 bits de un puerto y los escriba en otro con la sincronización adecuada. Sin embargo, sería un desafío de diseño interesante ver si un ARM barato podría hacer todo. DMA podría ser un aspecto útil de un enfoque todo en uno si se pudieran predecir sus efectos en los recuentos de ciclos de la CPU (especialmente si los ciclos de DMA pudieran ocurrir en ciclos cuando el bus de memoria estaba inactivo), pero en algún punto del proceso el ARM tendría que realizar sus funciones de búsqueda de tablas y observación de autobuses. Tenga en cuenta que, a diferencia de muchas arquitecturas de video donde los registros de color se escriben durante los intervalos de borrado, el Atari 2600 frecuentemente escribe en los registros de color durante la parte mostrada de un cuadro.

Quizás el mejor enfoque sería usar un par de chips de lógica discreta para identificar las escrituras de color y forzar los bits inferiores de los registros de color a los valores adecuados, y luego usar dos canales DMA para muestrear el bus de CPU entrante y los datos de salida TIA, y un tercer canal DMA para generar los datos de salida. Entonces, la CPU estaría libre para procesar todos los datos de ambas fuentes para cada línea de escaneo, realizar la traducción necesaria y almacenarlos en el búfer para la salida. El único aspecto de las funciones del adaptador que tendría que ocurrir en "tiempo real" sería la anulación de los datos escritos en COLUxx, y eso podría solucionarse utilizando dos chips lógicos comunes.

Respuestas (5)

Voto por DMA. Es realmente flexible en Cortex-M3 y superior, y puede hacer todo tipo de cosas locas, como obtener datos automáticamente de un lugar y enviarlos a otro con una velocidad específica o en algunos eventos sin gastar NINGÚN ciclo de CPU. DMA es mucho más fiable.

Pero puede ser bastante difícil de entender en detalles.

Otra opción son los núcleos blandos en FPGA con implementación de hardware de estas cosas ajustadas.

Me gusta la noción de DMA. Sin embargo, no creo que el núcleo Cortex M3 tenga DMA; esa es una función de los chips de los fabricantes individuales, y todos parecen implementarlo de manera diferente. Una cosa que encuentro molesta con al menos la única implementación con la que realmente he jugado (STM32L152), es que no puedo encontrar ninguna forma de tener una luz estroboscópica cuando se emiten datos DMA. Tampoco está claro qué factores pueden afectar la puntualidad de la DMA.
En cualquier caso, con respecto a una de las primeras aplicaciones en las que estaba pensando para un ciclo preciso, publiqué más información en la pregunta original. Tengo curiosidad por lo que piensas. Otra situación en la que estaba pensando en cambiar el ciclo sería enviar datos de pantalla a una pantalla LCD a color. Los datos se almacenarían en memoria RAM utilizando colores de 8 bits, pero la pantalla necesita colores de 16 bits. La forma más rápida en la que pensé para generar datos habría sido usar hardware para generar las luces estroboscópicas de escritura, por lo que la CPU solo tendría que registrar los datos. ¿Sería bueno traducir 8 -> 16 bits en un pequeño búfer ...
... y luego organizar DMA para transferir eso, o ¿cuál sería el mejor enfoque?

La información de tiempo está disponible, pero, como usted señaló, ocasionalmente puede ser vaga. Hay mucha información de tiempo en la Sección 18.2 y la Tabla 18.1 del Manual de referencia técnica para Cortex-M3, por ejemplo, ( pdf aquí ), y un extracto aquí:

extracto de 18.2

que dan una lista de condiciones para el tiempo máximo. El momento de muchas instrucciones depende de factores externos, algunos de los cuales dejan ambigüedades. He resaltado cada una de las ambigüedades que encontré en el siguiente extracto de esa sección:

[1] Las ramas toman un ciclo para la instrucción y luego la canalización se recarga para la instrucción de destino. Las sucursales no tomadas son 1 ciclo en total. Las ramas tomadas con un inmediato son normalmente 1 ciclo de recarga de tubería (2 ciclos en total). Las ramas tomadas con el operando de registro son normalmente 2 ciclos de recarga de tubería (3 ciclos en total). La recarga de la tubería es más larga [¿Cuánto tiempo más?] cuando se bifurca a instrucciones de 32 bits no alineadas además de los accesos a una memoria más lenta. Se emite una sugerencia de bifurcación al bus de código que permite que un sistema más lento [¿Cuánto más lento?] se precargue. Esto puede [¿Es esto opcional?] reducir [¿Por cuánto?] la penalización del objetivo de bifurcación por una memoria más lenta, pero nunca menos de lo que se muestra aquí.

[2] Generalmente, las instrucciones de carga y almacenamiento toman dos ciclos para el primer acceso y un ciclo para cada acceso adicional. Las tiendas con compensaciones inmediatas toman un ciclo.

[3] UMULL/SMULL/UMLAL/SMLAL usan terminación anticipada según el tamaño de los valores de origen [¿Qué tamaños?]. Estos son interrumpibles (abandonados/reiniciados), con una latencia en el peor de los casos de un ciclo. Las versiones MLAL toman de cuatro a siete ciclos y las versiones MULL toman de tres a cinco ciclos . Para MLAL, la versión firmada es un ciclo más larga que la no firmada.

[4] Las instrucciones de TI se pueden plegar . [¿Cuándo? Ver comentarios.]

[5] Los tiempos de DIV dependen del dividendo y el divisor . [El mismo problema que MUL] DIV es interrumpible (abandonado/reiniciado), con una latencia en el peor de los casos de un ciclo. Cuando el dividendo y el divisor son similares [¿Qué tan similares?] en tamaño, la división termina rápidamente. El tiempo mínimo es para casos de divisor mayor que dividendo y divisor de cero. Un divisor de cero devuelve cero (no es un error), aunque hay una trampa de depuración disponible para detectar este caso. [¿Cuáles son los rangos que se dieron para MUL?]

[6] El sueño es un ciclo para la instrucción más tantos ciclos de sueño como sea apropiado. WFE solo usa un ciclo cuando el evento ha pasado. WFI es normalmente más de un ciclo a menos que ocurra una interrupción pendiente exactamente al ingresar a WFI.

[7] ISB toma un ciclo (actúa como sucursal). DMB y DSB tardan un ciclo a menos que haya datos pendientes en el búfer de escritura o LSU. Si entra una interrupción durante una barrera, se abandona/reinicia.

Para todos los casos de uso, será más complejo que "Esta instrucción es un ciclo, esta instrucción es dos ciclos, esto es un ciclo..." conteo posible en procesadores más simples, más lentos y más antiguos. Para algunos casos de uso, no encontrará ambigüedades. Si encuentra ambigüedades, le sugiero:

  1. Póngase en contacto con su proveedor y pregúntele cuál es el tiempo de instrucción para su caso de uso.
  2. Prueba para especificar el comportamiento ambiguo
  3. Vuelva a probar cualquier revisión del procesador y especialmente cuando cambie de proveedor.

Estos requisitos probablemente respondan a su pregunta: "No, no es una buena idea, a menos que las dificultades encontradas valgan la pena", pero eso ya lo sabía.

Consideraría que lo siguiente es vago: "La recarga de la tubería es más larga cuando se bifurca a instrucciones de 32 bits no alineadas además de los accesos a una memoria más lenta" no dice si agrega exactamente un ciclo, y "Las instrucciones de TI se pueden doblar" no No se especifica en qué condiciones serán o no serán.
El tiempo "IT" parecería especialmente preocupante, ya que es una instrucción que a menudo se usaría dentro de un ciclo cerrado de ciclo contado, y estoy bastante seguro de que no siempre se puede plegar. Supongo que si uno siempre se bifurca al comienzo de un ciclo sensible al tiempo, obliga al ciclo a comenzar en un límite de palabra, evita cualquier carga condicional o almacena dentro del ciclo, y uno no coloca ninguna instrucción "IT" inmediatamente después de la carga o la actualización del registro de la tienda, los tiempos de "TI" serían consistentes, pero la especificación no lo deja claro.
Mi conjetura sería que la TI probablemente podría (verazmente) notar algo como, "En ausencia de estados de espera o contención de bus de código, el plegamiento de la TI está garantizado si (1) la instrucción anterior era una instrucción de 16 bits que no accedía memoria o el contador del programa; y (2) la siguiente instrucción es una instrucción de 16 bits, o la instrucción anterior no fue el objetivo de una rama "desalineada". El plegamiento de TI también puede ocurrir en otras circunstancias no especificadas". Tal especificación permitiría escribir programas con un tiempo de instrucción de TI predecible al garantizar que el código esté organizado como se indica.
Wow, confieso que solo había pasado por simples recuentos de ciclos en el peor de los casos, en lugar de luchar con las advertencias debajo de la mesa. Mi respuesta actualizada destaca algunas otras ambigüedades.
Hay muchas situaciones en las que uno está interesado en los recuentos del peor de los casos, y un buen número en el que uno está interesado en los recuentos del mejor de los casos (por ejemplo, si un puerto SPI puede generar un byte cada 16 ciclos, generar cada byte tomaría 14 ciclos en el mejor de los casos, y verificar la preparación tomaría 5 ciclos, verificar la preparación cada byte limitaría la velocidad a un byte cada 19 ciclos en el mejor de los casos; escribir a ciegas con dos NOP adicionales permitiría una velocidad de un byte cada 16 ciclos en el mejor de los casos ). Los casos en los que se necesita una sincronización precisa no son tan comunes, pero pueden surgir.
La necesidad de una sincronización tan precisa puede aliviarse en algunos casos con periféricos que ofrecen almacenamiento en búfer junto con una indicación de qué tan llenos están. Por ejemplo, supongamos que uno tiene un puerto SPI con un búfer de cuatro bytes y un indicador casi vacío, y las escrituras ciegas tardarían entre 14 y 16 ciclos. Si el búfer está casi vacío, uno podría escribir a ciegas 24 bytes (usando 336-384 ciclos, durante los cuales al menos 21 bytes habrían salido del puerto) antes de verificar la preparación del búfer. Una vez que el búfer está casi vacío, se pueden escribir 24 bytes más.
Tenga en cuenta que para que ese enfoque funcione bien, uno debe tener una idea bastante clara de cuánto tiempo tardará el código. Cuanto menos exactamente se sepa eso, más sobrecarga será necesaria para la sincronización.

Una forma de solucionar este problema es usar dispositivos con tiempos deterministas o predecibles, como los chips Parallax Propeller y XMOS:

http://www.parallaxsemiconductor.com/multicoreconcept

http://www.xmos.com/

El conteo de ciclos funciona muy bien con Propeller (se debe usar el lenguaje ensamblador), mientras que los dispositivos XMOS tienen una utilidad de software muy potente, el XMOS Timing Analyzer, que funciona con aplicaciones escritas en el lenguaje de programación XC:

https://www.xmos.com/download/public/XMOS-Timing-Analyzer-Whitepaper%281%29.pdf

Empiezo a pensar que Leon tiene acciones en XMOS... ;-)
Simplemente me gustan sus fichas y la gente que trabaja allí. Parallax también es una buena compañía con buenos productos.
Sí, sin ofender. Simplemente me sorprende que todas las respuestas (excepto una) donde se menciona XMOS son suyas. No hay nada de malo en estar entusiasmado con algo.
@Federico, @Leon: eso es exactamente lo que me preocupa un poco de XMOS: ¿por qué solo hay 1 usuario en el mundo (al menos eso es lo que parece)? Si es tan genial, ¿por qué no es la comidilla de la ciudad? Nunca escuché a nadie hablar de eso, menos usarlo.
Prueba los foros de XMOS: xcore.com

El conteo de ciclos se vuelve más problemático a medida que se aleja de los microcontroladores de bajo nivel y se pasa a los procesadores informáticos de propósito más general. El primero generalmente tiene un tiempo de instrucción bien especificado, en parte por las razones de su sitio. También se debe a que su arquitectura es bastante simple, por lo que los tiempos de instrucción son fijos y se pueden conocer.

Un buen ejemplo de esto son la mayoría de los PIC de Microchip. Las series 10, 12, 16 y 18 tienen un tiempo de instrucción muy bien documentado y predecible. Esta puede ser una característica útil en el tipo de pequeñas aplicaciones de control para las que están destinados estos chips.

A medida que se aleja del costo ultrabajo y, por lo tanto, el diseñador puede gastar más área de chip para obtener una mayor velocidad de una arquitectura más exótica, también se aleja de la previsibilidad. Eche un vistazo a las variantes x86 modernas como ejemplos extremos de esto. Hay varios niveles de cachés, virtualización de la memoria, búsqueda anticipada, canalización y más, que hacen que contar los ciclos de instrucciones sea casi imposible. Sin embargo, en esta aplicación no importa, ya que el cliente está interesado en la alta velocidad, no en la previsibilidad del tiempo de instrucción.

Incluso puede ver este efecto en funcionamiento en modelos superiores de Microchip. El núcleo de 24 bits (series 24, 30 y 33) tiene un tiempo de instrucción predecible en gran medida, excepto algunas excepciones cuando hay contenciones de bus de registro. Por ejemplo, en algunos casos la máquina inserta un bloqueo cuando la siguiente instrucción usa un registro con algunos modos de direccionamiento indirecto cuyo valor fue cambiado en la instrucción anterior. Este tipo de bloqueo es inusual en un dsPIC, y la mayoría de las veces puede ignorarlo, pero muestra cómo estas cosas se deslizan debido a que los diseñadores intentan brindarle un procesador más rápido y más capaz.

Entonces, la respuesta básica es que eso es parte de la compensación cuando eliges un procesador. Para aplicaciones de control pequeñas, puede elegir algo pequeño, económico, de bajo consumo y con un tiempo de instrucción predecible. A medida que exige más potencia de procesamiento, la arquitectura cambia, por lo que debe renunciar a un tiempo de instrucción predecible. Afortunadamente, eso es un problema menor a medida que se llega a aplicaciones de propósito general y de uso intensivo de cómputo, por lo que creo que las ventajas y desventajas funcionan razonablemente bien.

Estoy de acuerdo en que, en general, las aplicaciones que son más intensivas en computación se vuelven menos sensibles a la sincronización microscópica, pero hay algunos escenarios en los que uno podría necesitar un poco más de potencia de procesamiento que el PIC-18, pero también necesita previsibilidad. Me pregunto hasta qué punto debería esforzarme por aprender cosas como las arquitecturas PIC de 16 bits, o hasta qué punto debería pensar que el ARM probablemente sea adecuado.

Sí, aún puede hacerlo, incluso en un ARM. El mayor problema con eso en un ARM es que ARM vende núcleos, no chips, y se conoce la sincronización del núcleo, pero lo que el proveedor de chips envuelve varía de un proveedor a otro y, a veces, de una familia de chips a otra dentro del proveedor. Entonces, un chip en particular de un proveedor en particular puede ser bastante determinista (si no usa cachés, por ejemplo), pero se vuelve más difícil de portar. Cuando se trata de 5 relojes aquí y 11 relojes allá, el uso de temporizadores es problemático, ya que la cantidad de instrucciones que se necesitan para probar el temporizador y determinar si su tiempo de espera ha expirado. Por los sonidos de su experiencia de programación pasada, estoy dispuesto a apostar que probablemente depure con un osciloscopio como lo hago yo, por lo que puede probar un bucle cerrado en el chip a la velocidad del reloj, mirar el spi o i2c o cualquier forma de onda, agregue o quitar nops, cambiar el número de veces a través del bucle y básicamente sintonizar. Al igual que con cualquier plataforma, no usar interrupciones ayuda en gran medida a la naturaleza determinista de la ejecución de instrucciones.

No, no es tan simple como un PIC, pero aún así es bastante factible, especialmente si el retraso/tiempo se acerca a la velocidad del reloj del procesador. Varios proveedores basados ​​en ARM le permiten multiplicar la velocidad del reloj y obtener, por ejemplo, 60 MHz de una referencia de 8 MHz, por lo que si necesita una interfaz de 2 MHz en lugar de hacer algo cada 4 instrucciones, puede aumentar el reloj (si tiene la presupuesto de energía) y luego use un temporizador y tenga muchos relojes para hacer otras cosas también.