Estoy trabajando con el kit de descubrimiento STM32F303VC y estoy un poco desconcertado por su rendimiento. Para familiarizarme con el sistema, he escrito un programa muy simple simplemente para probar la velocidad de esta MCU. El código se puede desglosar de la siguiente manera:
- El reloj HSI (8 MHz) está encendido;
- PLL se inicia con el con el preescalador de 16 para lograr HSI / 2 * 16 = 64 MHz;
- PLL se designa como SYSCLK;
- SYSCLK se controla en el pin MCO (PA8), y uno de los pines (PE10) se alterna constantemente en el bucle infinito.
El código fuente de este programa se presenta a continuación:
#include "stm32f3xx.h"
int main(void)
{
// Initialize the HSI:
RCC->CR |= RCC_CR_HSION;
while(!(RCC->CR&RCC_CR_HSIRDY));
// Initialize the LSI:
// RCC->CSR |= RCC_CSR_LSION;
// while(!(RCC->CSR & RCC_CSR_LSIRDY));
// PLL configuration:
RCC->CFGR &= ~RCC_CFGR_PLLSRC; // HSI / 2 selected as the PLL input clock.
RCC->CFGR |= RCC_CFGR_PLLMUL16; // HSI / 2 * 16 = 64 MHz
RCC->CR |= RCC_CR_PLLON; // Enable PLL
while(!(RCC->CR&RCC_CR_PLLRDY)); // Wait until PLL is ready
// Flash configuration:
FLASH->ACR |= FLASH_ACR_PRFTBE;
FLASH->ACR |= FLASH_ACR_LATENCY_1;
// Main clock output (MCO):
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
GPIOA->MODER |= GPIO_MODER_MODER8_1;
GPIOA->OTYPER &= ~GPIO_OTYPER_OT_8;
GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR8;
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8;
GPIOA->AFR[0] &= ~GPIO_AFRL_AFRL0;
// Output on the MCO pin:
//RCC->CFGR |= RCC_CFGR_MCO_HSI;
//RCC->CFGR |= RCC_CFGR_MCO_LSI;
//RCC->CFGR |= RCC_CFGR_MCO_PLL;
RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;
// PLL as the system clock
RCC->CFGR &= ~RCC_CFGR_SW; // Clear the SW bits
RCC->CFGR |= RCC_CFGR_SW_PLL; //Select PLL as the system clock
while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL); //Wait until PLL is used
// Bit-bang monitoring:
RCC->AHBENR |= RCC_AHBENR_GPIOEEN;
GPIOE->MODER |= GPIO_MODER_MODER10_0;
GPIOE->OTYPER &= ~GPIO_OTYPER_OT_10;
GPIOE->PUPDR &= ~GPIO_PUPDR_PUPDR10;
GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR10;
while(1)
{
GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;
}
}
El código se compiló con CoIDE V2 con GNU ARM Embedded Toolchain usando la optimización -O1. Las señales en los pines PA8 (MCO) y PE10, examinadas con un osciloscopio, se ven así:
El SYSCLK parece estar configurado correctamente, ya que la MCO (curva naranja) exhibe una oscilación de casi 64 MHz (considerando el margen de error del reloj interno). La parte extraña para mí es el comportamiento en PE10 (curva azul). En el ciclo while (1) infinito, se requieren 4 + 4 + 5 = 13 ciclos de reloj para realizar una operación elemental de 3 pasos (es decir, ajuste de bits / restablecimiento de bits / retorno). Se empeora aún más en otros niveles de optimización (por ejemplo, -O2, -O3, ar -Os): se agregan varios ciclos de reloj adicionales a la parte BAJA de la señal, es decir, entre los bordes ascendente y descendente de PE10 (lo que permite que el LSI de alguna manera parezca para remediar esta situación).
¿Se espera este comportamiento de esta MCU? Me imagino que una tarea tan simple como configurar y restablecer un poco debería ser 2-4 veces más rápido. ¿Hay alguna manera de acelerar las cosas?
Respuestas:
La pregunta aquí realmente es: ¿cuál es el código de máquina que está generando desde el programa C y cómo difiere de lo que esperaría?
Si no tuviera acceso al código original, esto habría sido un ejercicio de ingeniería inversa (básicamente algo que comienza con:)
radare2 -A arm image.bin; aaa; VV
, pero tiene el código, así que todo lo hace más fácil.Primero, compílelo con la
-g
bandera agregada alCFLAGS
(mismo lugar donde también especifica-O1
). Luego, mire el ensamblaje generado:Tenga en cuenta que, por supuesto, tanto el nombre del
objdump
binario como el archivo ELF intermedio pueden ser diferentes.Por lo general, también puede omitir la parte donde GCC invoca el ensamblador y solo mirar el archivo de ensamblaje. Simplemente agregue
-S
a la línea de comando GCC, pero eso normalmente interrumpirá su compilación, por lo que probablemente lo haga fuera de su IDE.Hice el ensamblaje de una versión ligeramente parcheada de su código :
y obtuve lo siguiente (extracto, código completo en el enlace de arriba):
Que es un bucle (observe el salto incondicional a .L5 al final y la etiqueta .L5 al principio).
Lo que vemos aquí es que nosotros
ldr
(registro de carga) el registror2
con el valor en la ubicación de memoria almacenada enr3
+ 24 Bytes. Ser demasiado vago para buscar eso: muy probablemente la ubicación deBSRR
.OR
elr2
registro con la constante1024 == (1<<10)
, que correspondería a establecer el décimo bit en ese registro, y escribir el resultado enr2
sí mismo.str
(almacene) el resultado en la ubicación de la memoria que leímos en el primer pasoBRR
.b
(bifurcación) de vuelta al primer paso.Entonces tenemos 7 instrucciones, no tres, para comenzar. Solo
b
sucede una vez y, por lo tanto, es muy probable que esté tomando un número impar de ciclos (tenemos 13 en total, por lo tanto, en algún lugar debe proceder un recuento de ciclos impares). Como todos los números impares por debajo de 13 son 1, 3, 5, 7, 9, 11, y podemos descartar cualquier número mayor que 13-6 (suponiendo que la CPU no pueda ejecutar una instrucción en menos de un ciclo), sabemos queb
toma 1, 3, 5 o 7 ciclos de CPU.Siendo quienes somos, miré la documentación de instrucciones de ARM y cuántos ciclos toman para el M3:
ldr
toma 2 ciclos (en la mayoría de los casos)orr
toma 1 ciclostr
toma 2 ciclosb
toma de 2 a 4 ciclos. Sabemos que debe ser un número impar, por lo que debe tomar 3, aquí.Todo eso se alinea con tu observación:
Como muestra el cálculo anterior, difícilmente habrá una forma de hacer que su ciclo sea más rápido: los pines de salida en los procesadores ARM generalmente están mapeados en memoria , no en los registros del núcleo de la CPU, por lo que debe pasar por la rutina habitual de carga, modificación y almacenamiento si quieres hacer algo con eso.
Lo que, por supuesto, podría hacer no es leer (
|=
implícitamente tiene que leer) el valor del pin en cada iteración de bucle, sino simplemente escribir el valor de una variable local en él, que simplemente alterna cada iteración de bucle.Tenga en cuenta que creo que podría estar familiarizado con los micros de 8 bits, e intentaría leer solo valores de 8 bits, almacenarlos en variables locales de 8 bits y escribirlos en fragmentos de 8 bits. No lo hagas ARM es una arquitectura de 32 bits, y extraer 8 bits de una palabra de 32 bits puede requerir instrucciones adicionales. Si puede, simplemente lea la palabra completa de 32 bits, modifique lo que necesita y escríbala como un todo. Si eso es posible, por supuesto, depende de lo que esté escribiendo, es decir, el diseño y la funcionalidad de su GPIO mapeado en memoria. Consulte la hoja de datos STM32F3 / guía del usuario para obtener información sobre lo que está almacenado en el bit de 32 bits que contiene el bit que desea alternar.
Ahora, intenté reproducir su problema con el período "bajo" cada vez más largo, pero simplemente no pude: el bucle se ve exactamente igual
-O3
que-O1
con mi versión del compilador. ¡Tendrás que hacerlo tú mismo! Tal vez esté utilizando alguna versión antigua de GCC con soporte ARM subóptimo.fuente
=
lugar de|=
), como usted dice, exactamente la velocidad que busca el OP? La razón por la cual los ARM tienen los registros BRR y BSRR por separado es que no requieren lectura-modificación-escritura. En este caso, las constantes podrían almacenarse en registros fuera del bucle, por lo que el bucle interno sería de solo 2 str y una rama, por lo que 2 + 2 +3 = 7 ciclos para toda la ronda.-O3
error parece haber desaparecido después de limpiar y reconstruir la solución. Sin embargo, mi código de ensamblaje parece tener una instrucción UTXH adicional dentro de él:.L5:
ldrh r3, [r2, #24]
uxth r3, r3
orr r3, r3, #1024
strh r3, [r2, #24] @ movhi
ldr r3, [r2, #40]
orr r3, r3, #1024
str r3, [r2, #40]
b .L5
uxth
está ahí porqueGPIO->BSRRL
está (incorrectamente) definido como un registro de 16 bits en sus encabezados. Utilice una versión reciente de los encabezados, de las bibliotecas STM32CubeF3 , donde no hay BSRRL y BSRRH, sino un únicoBSRR
registro de 32 bits . Aparentemente, @Marcus tiene los encabezados correctos, por lo que su código realiza accesos completos de 32 bits en lugar de cargar una media palabra y extenderla.LDRB
ySTRB
que realiza lectura / escritura de bytes en una sola instrucción, ¿no?Los registros
BSRR
yBRR
son para configurar y restablecer bits de puerto individuales:Como puede ver, leer estos registros siempre da 0, por lo tanto, cuál es su código
efectivamente hace es
GPIOE->BRR = 0 | GPIO_BRR_BR_10
, pero el optimizador no sabe que, por lo que genera una secuencia deLDR
,ORR
,STR
las instrucciones en lugar de una sola tienda.Puede evitar la costosa operación de lectura-modificación-escritura simplemente escribiendo
Puede obtener una mejora adicional alineando el bucle con una dirección que sea divisible por 8. Trate de poner una o
asm("nop");
instrucciones de modo antes delwhile(1)
bucle.fuente
Para agregar a lo que se ha dicho aquí: Ciertamente con el Cortex-M, pero casi cualquier procesador (con una tubería, caché, predicción de ramificación u otras características), es trivial tomar incluso el bucle más simple:
Ejecútelo tantos millones de veces como desee, pero sea capaz de hacer que el rendimiento de ese ciclo varíe ampliamente, solo esas dos instrucciones, agregue algunos nops en el medio si lo desea; No importa.
Cambiar la alineación del bucle puede variar drásticamente el rendimiento, especialmente con un bucle pequeño como ese si se necesitan dos líneas de búsqueda en lugar de una, se come ese costo adicional, en un microcontrolador como este donde el flash es más lento que la CPU en 2 o 3 y luego subiendo el reloj la proporción empeora aún más 3 o 4 o 5 que agregar una recuperación adicional.
Es probable que no tenga un caché, pero si lo tenía, ayuda en algunos casos, pero duele en otros y / o no hace la diferencia. La predicción de ramificación que puede o no tener aquí (probablemente no) solo puede ver hasta donde se diseñó en la tubería, por lo que incluso si cambió el bucle para ramificarse y tuvo una ramificación incondicional al final (más fácil para un predictor de ramificación) use) todo lo que hace es ahorrarle tantos relojes (tamaño de la tubería desde donde normalmente buscaría hasta qué tan profundo puede ver el predictor) en la siguiente búsqueda y / o no hace una captación previa por si acaso.
Al cambiar la alineación con respecto a las líneas de búsqueda y caché, puede afectar si el predictor de ramificación lo está ayudando o no, y eso se puede ver en el rendimiento general, incluso si solo está probando dos instrucciones o esas dos con algunos nops .
Es algo trivial hacer esto, y una vez que comprende eso, luego de tomar el código compilado, o incluso el ensamblaje escrito a mano, puede ver que su rendimiento puede variar ampliamente debido a estos factores, agregando o ahorrando unos pocos a un par de cientos por ciento, una línea de código C, un nop mal colocado.
Después de aprender a usar el registro BSRR, intente ejecutar su código desde RAM (copiar y saltar) en lugar de flash que debería darle un aumento instantáneo del rendimiento de 2 a 3 veces en la ejecución sin hacer nada más.
fuente
Es un comportamiento de su código.
Debe escribir en registros BRR / BSRR, no leer-modificar-escribir como lo hace ahora.
También incurre en gastos generales de bucle. Para obtener el máximo rendimiento, reproduzca las operaciones BRR / BSRR una y otra vez → cópielas y péguelas en el bucle varias veces para que pase por muchos ciclos de configuración / reinicio antes de la sobrecarga de un bucle.
editar: algunas pruebas rápidas bajo IAR.
un cambio de escritura a BRR / BSRR toma 6 instrucciones bajo optimización moderada y 3 instrucciones bajo el nivel más alto de optimización; un vistazo a RMW'ng requiere 10 instrucciones / 6 instrucciones.
bucle sobrecarga adicional.
fuente
|=
a=
una fase de ajuste / restablecimiento de un solo bit, se consumen 9 ciclos de reloj ( enlace ). El código de ensamblaje tiene 3 instrucciones de largo:.L5
strh r1, [r3, #24] @ movhi
str r2, [r3, #40]
b .L5
gcc -funroll-loops
) pueden hacer muy bien y que cuando se abusa (como aquí) tiene el efecto inverso de lo que desea.somePortLatch
controla un puerto cuyos 4 bits inferiores están configurados para la salida, es posible desenrollarwhile(1) { SomePortLatch ^= (ctr++); }
en un código que genera 15 valores y luego vuelve a comenzar en el momento en que, de lo contrario, generaría el mismo valor dos veces seguidas.