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 transferencia de bits de esta MCU. El código se puede dividir de la siguiente manera:
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 utilizando 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 bucle while(1) infinito, se necesitan 4 + 4 + 5 = 13 ciclos de reloj para realizar una operación elemental de 3 pasos (es decir, establecimiento de bit/reinicio de bit/retorno). 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 flancos descendente y ascendente de PE10 (habilitar el LSI de alguna manera parece para remediar esta situación).
¿Se espera este comportamiento de este MCU? Me imagino que una tarea tan simple como configurar y restablecer un bit debería ser de 2 a 4 veces más rápida. ¿Hay alguna manera de acelerar las cosas?
La pregunta aquí realmente es: ¿cuál es el código de máquina que está generando a partir del programa C y en qué se diferencia de lo que esperaría?
Si no tuvieras acceso al código original, esto habría sido un ejercicio de ingeniería inversa (básicamente algo que comenzara con: radare2 -A arm image.bin; aaa; VV
), pero tienes el código, así que esto lo hace todo más fácil.
Primero, compílelo con la -g
bandera agregada a CFLAGS
(mismo lugar donde también especifica -O1
). Luego, mire el ensamblado generado:
arm-none-eabi-objdump -S yourprog.elf
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 en la que GCC invoca al ensamblador y simplemente mirar el archivo de ensamblaje. Simplemente agregue -S
a la línea de comandos de GCC, pero eso normalmente romperá 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 :
arm-none-eabi-gcc
-O1 ## your optimization level
-S ## stop after generating assembly, i.e. don't run `as`
-I/path/to/CMSIS/ST/STM32F3xx/ -I/path/to/CMSIS/include
test.c
y obtuve lo siguiente (extracto, código completo en el enlace de arriba):
.L5:
ldr r2, [r3, #24]
orr r2, r2, #1024
str r2, [r3, #24]
ldr r2, [r3, #40]
orr r2, r2, #1024
str r2, [r3, #40]
b .L5
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 registro r2
con el valor en la ubicación de memoria almacenada en r3
+ 24 Bytes. Ser demasiado perezoso para buscar eso: muy probablemente la ubicación de BSRR
.OR
el r2
registro con la constante 1024 == (1<<10)
, que correspondería a establecer el bit 10 en ese registro, y escribir el resultado en r2
sí mismo.str
(almacene) el resultado en la ubicación de memoria que hemos leído en el primer pasoBRR
la dirección de .b
(rama) de vuelta al primer paso.Así que tenemos 7 instrucciones, no tres, para empezar. 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 que en algún lugar debe provenir un recuento impar de ciclos). Dado que 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 puede ejecutar una instrucción en menos de un ciclo), sabemos que b
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
lleva 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:
Por supuesto, lo que podría hacer es no leer ( |=
implícitamente tiene que leer) el valor del pin en cada iteración del bucle, sino simplemente escribirle el valor de una variable local, que simplemente cambia en cada iteración del bucle.
Tenga en cuenta que creo que podría estar familiarizado con los micros de 8 bits y que intentaría leer solo valores de 8 bits, almacenarlos en variables locales de 8 bits y escribirlos en fragmentos de 8 bits. No. 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 necesite y vuelva a escribirlo completo. 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/guía del usuario de STM32F3 para obtener información sobre lo que se almacena en los 32 bits que contienen el bit que desea alternar.
Ahora, traté de 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.
=
en lugar de |=
), como usted dice, exactamente la aceleración que está buscando el OP? La razón por la que los ARM tienen los registros BRR y BSRR por separado es para no requerir lectura-modificación-escritura. En este caso, las constantes podrían almacenarse en registros fuera del ciclo, por lo que el ciclo interno sería solo 2 str y una rama, ¿entonces 2 + 2 +3 = 7 ciclos para toda la ronda?-O3
error parece haber desaparecido después de limpiar y reconstruir la solución. No obstante, mi código ensamblador parece tener una instrucción UTXH adicional dentro:.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á allí porque GPIO->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 ni BSRRH, sino un solo registro de 32 bits BSRR
. @Marcus aparentemente 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
y STRB
que realizan lecturas/escrituras de bytes en una sola instrucción, ¿no?LDRB
y LDRH
cargue los bits inferiores del registro de destino, y deje los bits de orden superior solos. El valor debe promoverse a 32 bits sin signo para el operador OR bit a bit, porque GPIO_BSRR_BS_10
así está definido. UXTB
o UXTH
establece esos bits no cargados en 0.BSRR
en lugar de BSRRL
es la solución que tuve que hacer para construirlo en mi máquina (por lo tanto, publiqué el código completo)LDRB
el cero se extiende a 32 bits. ¿Estamos hablando de diferentes variantes de la arquitectura ARM o algo así?LDRB
instrucción exista no significa que el encabezado esté escrito para que pueda usarse, supongo que aquí.Los registros BSRR
y BRR
son para establecer y restablecer bits de puerto individuales:
Registro de establecimiento/reinicio de bit de puerto GPIO (GPIOx_BSRR)
...
(x = A..H) Bits 15:0
BSy: Puerto x establece bit y (y= 0..15)
Estos bits son de solo escritura. Una lectura de estos bits devuelve el valor 0x0000.
0: ninguna acción en el bit ODRx correspondiente
1: establece el bit ODRx correspondiente
Como puede ver, la lectura de estos registros siempre da 0, por lo que su código
GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;
efectivamente es GPIOE->BRR = 0 | GPIO_BRR_BR_10
, pero el optimizador no lo sabe, por lo que genera una secuencia de LDR
instrucciones en lugar ORR
de STR
una sola tienda.
Puede evitar la costosa operación de lectura, modificación y escritura simplemente escribiendo
GPIOE->BSRRL = GPIO_BSRR_BS_10;
GPIOE->BRR = GPIO_BRR_BR_10;
Puede obtener alguna mejora adicional al alinear el bucle con una dirección divisible por 8. Intente poner asm("nop");
las instrucciones de uno o modo antes del while(1)
bucle.
Para agregar a lo que se ha dicho aquí: ciertamente con Cortex-M, pero prácticamente con cualquier procesador (con canalización, caché, predicción de bifurcación u otras características), es trivial tomar incluso el ciclo más simple:
top:
subs r0,#1
bne top
Ejecútalo tantos millones de veces como quieras, pero haz que el rendimiento de ese ciclo varíe ampliamente, solo esas dos instrucciones, agrega algunos nops en el medio si lo deseas; no importa.
Cambiar la alineación del ciclo puede variar drásticamente el rendimiento, especialmente con un ciclo pequeño como ese, si toma dos líneas de búsqueda en lugar de una, se come ese costo adicional, en un microcontrolador como este, donde el flash es 2 más lento que la CPU. o 3 y luego, al aumentar el reloj, la relación se vuelve aún peor 3 o 4 o 5 que agregar búsqueda adicional.
Es probable que no tenga un caché, pero si lo tuviera, ayuda en algunos casos, pero duele en otros y/o no hace la diferencia. La predicción de bifurcación que puede o no tener aquí (probablemente no) solo puede ver hasta donde está diseñada en la tubería, por lo que incluso si cambió el ciclo para ramificarse y tuvo una bifurcación incondicional al final (más fácil para un predictor de bifurcación use) todo lo que hace es ahorrarle muchos relojes (el tamaño de la tubería desde donde normalmente buscaría hasta qué tan profundo puede ver el predictor) en la próxima búsqueda y/o no hace una búsqueda 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 bifurcación lo está ayudando o no, y eso se puede ver en el rendimiento general, incluso si solo está probando dos instrucciones o esos dos con algunos nops .
Es algo trivial hacer esto, y una vez que comprenda eso, luego tomando el código compilado, o incluso el ensamblado escrito a mano, puede ver que su rendimiento puede variar ampliamente debido a estos factores, agregando o ahorrando algunos hasta 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, lo que debería brindarle un aumento instantáneo de rendimiento de 2 a 3 veces en la ejecución sin hacer nada más.
¿Se espera este comportamiento de este MCU?
Es un comportamiento de su código.
Debe escribir en los registros BRR/BSRR, no leer-modificar-escribir como lo hace ahora.
También incurre en una sobrecarga de bucle. Para obtener el máximo rendimiento, replique las operaciones de BRR/BSRR una y otra vez → cópielas y péguelas en el ciclo varias veces para pasar por muchos ciclos de configuración/reinicio antes de que un ciclo se sobrecargue.
editar: algunas pruebas rápidas bajo IAR.
un salto a través de la escritura a BRR/BSRR requiere 6 instrucciones con una optimización moderada y 3 instrucciones con el nivel más alto de optimización; hojear RMW'ng toma 10 instrucciones / 6 instrucciones.
extra de sobrecarga de bucle.
|=
a =
una fase de establecimiento/reinicio de un solo bit, se consumen 9 ciclos de reloj ( enlace ). El código ensamblador 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 que se desarrolle while(1) { SomePortLatch ^= (ctr++); }
en un código que genera 15 valores y luego regresa para comenzar en el momento en que, de lo contrario, generaría el mismo valor dos veces seguidas.
Marko Bursic
Jonas Schäfer
scott seidman
0___________