Obtención de un rendimiento rápido de una MCU STM32

11

Estoy trabajando con el STM32F303VC kit de descubrimiento y me sorprende un poco su rendimiento. Para familiarizarme con el sistema, he escrito un programa muy simple simplemente para probar la velocidad de bits de esta MCU. El código se puede desglosar de la siguiente manera:

  1. El reloj HSI (8 MHz) está activado;
  2. PLL se inicia con el prescaler de 16 para alcanzar HSI / 2 * 16 = 64 MHz;
  3. PLL se designa como SYSCLK;
  4. SYSCLK se monitorea 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 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) muestra 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 infinito while (1), se requieren 4 + 4 + 5 = 13 ciclos de reloj para realizar una operación elemental de 3 pasos (es decir, bit-set / bit-reset / return). Empeora aún más en otros niveles de optimización (p. Ej., -O2, -O3, ar -Os): se agregan varios ciclos de reloj adicionales a la parte BAJA de la señal, es decir, entre los flancos descendentes y ascendentes de PE10 (lo que hace que parezca que el LSI parece 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 a 4 veces más rápida. ¿Hay alguna manera de acelerar las cosas?

    
pregunta K.R.

4 respuestas

25

La pregunta aquí realmente es: ¿cuál es el código de máquina que estás generando desde el programa C y en qué se diferencia de lo que esperas?

Si no tuvieras acceso al código original, esto hubiera sido un ejercicio de ingeniería inversa (básicamente algo que comienza con: radare2 -A arm image.bin; aaa; VV ), pero tienes el código para que todo sea más fácil.

Primero, compílelo con el indicador -g agregado al CFLAGS (el mismo lugar donde también especifica -O1 ). Luego, mira el ensamblado generado:

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

Tenga en cuenta que, por supuesto, tanto el nombre del binario objdump como su archivo ELF intermedio podrían 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. Solo agregue -S a la línea de comandos de GCC, pero eso normalmente romperá su compilación, por lo que lo más probable es que lo haga fuera de su IDE.

Hice el ensamblado de una versión ligeramente parcheada de tu 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 (note 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 r2 con el valor en la ubicación de la memoria almacenada en r3 + 24 bytes. Ser demasiado perezoso para buscar eso: muy probablemente la ubicación de BSRR .
  • Luego, OR el r2 se registra con la constante 1024 == (1<<10) , lo que correspondería a establecer el décimo bit en ese registro, y escribe el resultado en el mismo r2 .
  • Luego str (almacena) el resultado en la ubicación de la memoria que hemos leído en el primer paso
  • y luego repita lo mismo para una ubicación de memoria diferente, fuera de pereza: lo más probable es que la dirección de BRR .
  • Finalmente, b (rama) regresa al primer paso.

Para empezar, tenemos 7 instrucciones, no tres. Solo el b ocurre una vez, y por lo tanto es muy probable que se trate de un número impar de ciclos (tenemos 13 en total, por lo que en algún lugar debe provenir un recuento de ciclos impares). 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 (asumiendo que la CPU no puede ejecutar una instrucción en menos de un ciclo), sabemos que el b toma 1, 3, 5 o 7 ciclos de CPU.

Siendo quienes somos, miré la documentación de ARM de instrucciones 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
  • b lleva de 2 a 4 ciclos. Sabemos que debe ser un número impar, por lo que debe tomar 3, aquí.

Todo se alinea con tu observación:

$$ \ begin {align} 13 & = 2 \ cdot (& c_ \ mathtt {ldr} & + c_ \ mathtt {orr} & + c_ \ mathtt {str}) & + c_ \ mathtt {b} \\ & = 2 \ cdot (& 2 & + 1 & +2) & +3 \\ & = 2 \ cdot & 5 & & & +3 \ end {align} $$

Como se muestra en el cálculo anterior, difícilmente habrá una manera de hacer que su bucle sea más rápido: los pines de salida en los procesadores ARM generalmente son asignados a la memoria , no a los registros del núcleo de la CPU, por lo que tiene que pasar por lo habitual. cargar - modificar - almacenar rutina si quieres hacer algo con eso.

Lo que podrías hacer, por supuesto, es no leer ( |= implícitamente tiene para leer) el valor del pin en cada iteración de bucle, sino simplemente escribirle el valor de una variable local, que simplemente alternar cada iteración de bucle.

Tenga en cuenta que creo que podría estar familiarizado con los micros de 8 bits y que estaría intentando 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 de nuevo. Si eso es posible, por supuesto, depende de lo que escriba, 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 está almacenado en los 32 bits que contienen 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 con -O3 que con -O1 con mi versión del compilador. ¡Tendrás que hacerlo tú mismo! Tal vez esté usando alguna versión antigua de GCC con soporte de ARM por debajo de lo óptimo.

    
respondido por el Marcus Müller
8

Los registros BSRR y BRR son para configurar y restablecer bits de puerto individuales:

  

Registro de restablecimiento / restablecimiento de bits de puerto GPIO (GPIOx_BSRR)

     

...

     

(x = A..H) Bits 15: 0

     

BSy: El puerto x establece el bit y (y = 0..15)

     

Estos bits son de solo escritura. Una lectura para   estos bits devuelven 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 tanto, cuál es 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 instrucciones LDR , ORR , STR 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 alguna mejora adicional al alinear el bucle con una dirección divisible uniformemente entre 8. Intente colocar una o las instrucciones de modo asm("nop"); antes del bucle while(1) .

    
respondido por el berendi
1

Para agregar a lo que se ha dicho aquí: Ciertamente con Cortex-M, pero casi cualquier procesador (con una tubería, caché, predicción de rama u otras características), es trivial tomar incluso el bucle más simple:

top:
   subs r0,#1
   bne top

Ejecútalo tantos millones de veces como quieras, pero puedes tener el rendimiento de ese bucle que varía ampliamente, solo esas dos instrucciones, agrega algunos nops en el medio si lo deseas; no importa.

Cambiar la alineación del bucle puede variar dramáticamente el rendimiento, especialmente con un bucle pequeño como que si toma dos líneas de alcance en lugar de una, consume ese costo adicional, en un microcontrolador como este, donde el flash es más lento que el flash. CPU por 2 o 3 y luego, al aumentar la velocidad del reloj, la proporción empeora 3, 4 o 5 que agregar una recuperación adicional.

Es probable que no tenga un caché, pero si lo tenía, eso ayuda en algunos casos, pero duele en otros y / o no hace una diferencia. La predicción de ramificación que puede o no tener aquí (probablemente no) solo puede verse tan lejos como 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 usar) todo lo que hace es ahorrarle muchos relojes (tamaño de la tubería desde donde normalmente alcanzaría hasta qué punto puede ver el predictor) en la próxima búsqueda y / o no hace una búsqueda previa en caso de que sea. p>

Al cambiar la alineación con respecto a las líneas de captura y caché, puede afectar si el predictor de rama 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 entiendes eso, luego de tomar un código compilado, o incluso un ensamblaje escrito a mano, puedes ver que su rendimiento puede variar ampliamente debido a estos factores, agregando o guardando algunos en un par cien por ciento, una línea de código C, una nop mal colocada.

Después de aprender a usar el registro BSRR, intente ejecutar su código desde la RAM (copiar y saltar) en lugar de un flash que debería darle un aumento instantáneo de 2 a 3 veces el rendimiento en la ejecución sin hacer nada más.

    
respondido por el old_timer
0
  

¿Se espera este comportamiento de esta MCU?

Es un comportamiento de tu código.

  1. Debes escribir en los registros BRR / BSRR, no leer-modificar-escribir como lo haces ahora.

  2. También incurres en la sobrecarga del bucle. Para obtener el máximo rendimiento, replique las operaciones BRR / BSRR una y otra vez → copie y pegue en el bucle varias veces para que pase por muchos ciclos de configuración / reinicio antes de una sobrecarga de bucle.

editar: algunas pruebas rápidas bajo IAR.

un giro en la escritura a BRR / BSRR toma 6 instrucciones bajo optimización moderada y 3 instrucciones en el nivel más alto de optimización; un giro a través de RMW'ng toma 10 instrucciones / 6 instrucciones.

sobrecarga de bucle extra.

    
respondido por el dannyf

Lea otras preguntas en las etiquetas