Obteniendo un rendimiento rápido de una MCU STM32

11

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:

  1. El reloj HSI (8 MHz) está encendido;
  2. PLL se inicia con el con el preescalador de 16 para lograr HSI / 2 * 16 = 64 MHz;
  3. PLL se designa como SYSCLK;
  4. 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í: ingrese la descripción de la imagen aquí

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?

KR
fuente
¿Has probado con alguna otra MCU para comparar?
Marko Buršič
3
¿Qué estás tratando de lograr? Si desea una salida oscilante rápida, debería usar temporizadores. Si desea interactuar con protocolos seriales rápidos, debe usar el hardware periférico correspondiente.
Jonas Schäfer
2
Gran comienzo con el kit !!
Scott Seidman
No debe | = registros BSRR o BRR ya que son solo de escritura.
P__J__

Respuestas:

25

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 -gbandera agregada al CFLAGS(mismo lugar donde también especifica -O1). Luego, mire el ensamblaje generado:

arm-none-eabi-objdump -S yourprog.elf

Tenga en cuenta que, por supuesto, tanto el nombre del objdumpbinario 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 -Sa 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 :

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

  • primero ldr(registro de carga) el registro r2con el valor en la ubicación de memoria almacenada en r3+ 24 Bytes. Ser demasiado vago para buscar eso: muy probablemente la ubicación de BSRR.
  • Luego, ORel r2registro con la constante 1024 == (1<<10), que correspondería a establecer el décimo bit en ese registro, y escribir el resultado en r2sí mismo.
  • Luego str(almacene) el resultado en la ubicación de la memoria que leímos en el primer paso
  • y luego repita lo mismo para una ubicación de memoria diferente, por flojera: la dirección más probable BRR.
  • Finalmente b(bifurcación) de vuelta al primer paso.

Entonces tenemos 7 instrucciones, no tres, para comenzar. Solo bsucede 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 que btoma 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 ciclo
  • str toma 2 ciclos
  • btoma 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:

13=2(cldr+corr+cstr)+cb=2(2+1+2)+3=25+3

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 -O3que -O1con 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.

Marcus Müller
fuente
44
¿No sería simplemente almacenar (en =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.
Timo
Gracias. Eso realmente aclaró un poco las cosas. Fue un poco apresurado pensar insistir en que solo se necesitarían 3 ciclos de reloj; de 6 a 7 ciclos eran algo que realmente esperaba. El -O3error 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
KR
1
uxthestá ahí porque GPIO->BSRRLestá (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 único BSRRregistro 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.
berendi - protestando el
¿Por qué cargar un solo byte requiere instrucciones adicionales? La arquitectura ARM tiene LDRBy STRBque realiza lectura / escritura de bytes en una sola instrucción, ¿no?
psmears
1
El núcleo M3 puede admitir bandas de bits (no estoy seguro si esta implementación particular lo hace), donde una región de 1 MB de espacio de memoria periférica tiene un alias a una región de 32 MB. Cada bit tiene una dirección de palabra discreta (solo se usa el bit 0). Presumiblemente aún más lento que solo una carga / tienda.
Sean Houlihane
8

Los registros BSRRy BRRson para configurar y restablecer bits de puerto individuales:

Conjunto de bits de puerto GPIO / registro de reinicio (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, leer estos registros siempre da 0, por lo tanto, cuál es su código

GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;

efectivamente hace es GPIOE->BRR = 0 | GPIO_BRR_BR_10, pero el optimizador no sabe que, por lo que genera una secuencia de LDR, ORR, STRlas instrucciones en lugar de una sola tienda.

Puede evitar la costosa operación de lectura-modificación-escritura simplemente escribiendo

GPIOE->BSRRL = GPIO_BSRR_BS_10;
GPIOE->BRR = GPIO_BRR_BR_10;

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 del while(1)bucle.

berendi - protestando
fuente
1

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:

top:
   subs r0,#1
   bne top

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.

viejo contador de tiempo
fuente
0

¿Se espera este comportamiento de esta MCU?

Es un comportamiento de su código.

  1. Debe escribir en registros BRR / BSRR, no leer-modificar-escribir como lo hace ahora.

  2. 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.

dannyf
fuente
Al cambiar |=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
KR
1
No desenrolle los bucles manualmente. Prácticamente nunca es una buena idea. En este caso particular, es especialmente desastroso: hace que la forma de onda no sea periódica. Además, tener el mismo código muchas veces en flash no es necesariamente más rápido. Esto podría no aplicarse aquí (¡podría!), Pero el desenrollado de bucles es algo que mucha gente piensa que ayuda, que los compiladores ( gcc -funroll-loops) pueden hacer muy bien y que cuando se abusa (como aquí) tiene el efecto inverso de lo que desea.
Marcus Müller
Un bucle infinito nunca se puede desenrollar de manera efectiva para mantener un comportamiento de sincronización constante.
Marcus Müller
1
@ MarcusMüller: a veces se pueden desenrollar bucles infinitos mientras se mantiene un tiempo constante si hay puntos en algunas repeticiones del bucle donde una instrucción no tendría un efecto visible. Por ejemplo, si somePortLatchcontrola un puerto cuyos 4 bits inferiores están configurados para la salida, es posible desenrollar while(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.
supercat
Supercat, cierto. Además, los efectos como el tiempo de la interfaz de memoria, etc., pueden hacer que sea razonable desenrollar "parcialmente". Mi declaración fue demasiado general, pero siento que el consejo de Danny es aún más general, e incluso peligroso
Marcus Müller