Latencia de interrupción en una MCU STM32F303

8

Estoy trabajando en un proyecto que involucra una MCU STM32 (en la placa STM32303C-EVAL para ser exactos) que tiene que responder a una interrupción externa. Quiero que la reacción a la interrupción externa sea lo más rápida posible. He modificado un ejemplo de biblioteca periférica estándar de la página web ST y el programa actual simplemente alterna un LED en cada borde ascendente sucesivo en PE6:

#include "stm32f30x.h"
#include "stm32303c_eval.h"

EXTI_InitTypeDef   EXTI_InitStructure;
GPIO_InitTypeDef   GPIO_InitStructure;
NVIC_InitTypeDef   NVIC_InitStructure;

static void EXTI9_5_Config(void);

int main(void)
{

  /* Initialize LEDs mounted on STM32303C-EVAL board */
  STM_EVAL_LEDInit(LED1);

  /* Configure PE6 in interrupt mode */
  EXTI9_5_Config();

  /* Infinite loop */
  while (1)
  {
  }
}

// Configure PE6 and PD5 in interrupt mode
static void EXTI9_5_Config(void)
{
  /* Enable clocks */
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOD | RCC_AHBPeriph_GPIOE, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

  /* Configure input */
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
  GPIO_Init(GPIOD, &GPIO_InitStructure);

  /* Connect EXTI6 Line to PE6 pin */
  SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource6);

  /* Configure Button EXTI line */
  EXTI_InitStructure.EXTI_Line = EXTI_Line6;
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;  
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  EXTI_Init(&EXTI_InitStructure);

  /* Enable and set interrupt to the highest priority */
  NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure); 
}

El manejador de interrupciones se ve así:

void EXTI9_5_IRQHandler(void)
{ 
  if((EXTI_GetITStatus(EXTI_Line6) != RESET))
  {
    /* Toggle LD1 */
    STM_EVAL_LEDToggle(LED1);

    /* Clear the EXTI line 6 pending bit */
    EXTI_ClearITPendingBit(EXTI_Line6);
  }
}

En este caso particular, las interrupciones son creadas por un generador externo de funciones programables que funciona a 100 Hz. Después de examinar la respuesta de la MCU en un osciloscopio, me sorprendió bastante que nos tome casi 1.32 para que la MCU comience a procesar la interrupción: ingrese la descripción de la imagen aquí

Con la MCU funcionando a 72 MHz (he comprobado la salida SYSCLK en el pin MCO de antemano) esto equivale a casi 89 ciclos de reloj. ¿No debería ser la respuesta de MCU a la interrupción mucho más rápida?

PD El código fue compilado con IAR Embedded Workbench y optimizado para la velocidad más alta.

KR
fuente
¿Estás seguro de que es la demora para comenzar a procesar la interrupción? ¿Qué sucede cuando eliminas la condición if y solo cambias?
BeB00
@ BeB00 la if{}declaración es necesaria porque la rutina de interrupción no sabe cuál es la fuente de la interrupción.
Rohat Kılıç
Si no recuerdo mal, la latencia debería ser de alrededor de 10-15 ciclos
BeB00
1
Bien, pero ¿qué sucede cuando lo eliminas en tu experimento? Supongo que no tiene una carga de otras interrupciones que desencadenan esto constantemente, por lo que debería poder tener una mejor idea de la demora real
BeB00
1
Realmente no debería ser un misterio. Mire el código del ensamblador compilado para su función de interrupción y consulte el manual de referencia ARM correspondiente para sumar el número de ciclos de reloj para cada instrucción ...
brhans

Respuestas:

8

Problema

Bueno, tienes que mirar las funciones que estás usando, no puedes simplemente hacer suposiciones sobre la velocidad del código que no has visto:

Esta es la función EXTI_GetITStatus:

ITStatus EXTI_GetITStatus   (   uint32_t    EXTI_Line    )  
{
  ITStatus bitstatus = RESET;
  uint32_t enablestatus = 0;

  /* Check the parameters */
  assert_param(IS_GET_EXTI_LINE(EXTI_Line));

  enablestatus =  *(__IO uint32_t *) (((uint32_t) &(EXTI->IMR)) + ((EXTI_Line) >> 5 ) * 0x20) & (uint32_t)(1 << (EXTI_Line & 0x1F));

  if ( (((*(__IO uint32_t *) (((uint32_t) &(EXTI->PR)) + (((EXTI_Line) >> 5 ) * 0x20) )) & (uint32_t)(1 << (EXTI_Line & 0x1F))) != (uint32_t)RESET) && (enablestatus != (uint32_t)RESET))
  {
    bitstatus = SET;
  }
  else
  {
    bitstatus = RESET;
  }
  return bitstatus;

}

Como puede ver, esto no es algo simple que requiere solo un ciclo o dos.

Lo siguiente es su función de alternar LED:

void STM_EVAL_LEDToggle (   Led_TypeDef     Led  )  
{
  GPIO_PORT[Led]->ODR ^= GPIO_PIN[Led];
}

Entonces, aquí tiene una indexación de matriz y una lectura, modificación, escritura para alternar el LED.

Los HAL a menudo terminan creando una buena cantidad de sobrecarga porque deben ocuparse de la configuración incorrecta y el uso incorrecto de las funciones. La verificación de parámetros necesarios y también la traducción de un parámetro simple a un bit en el registro pueden requerir una gran cantidad de cómputo (bueno, al menos por una interrupción crítica de tiempo).

Entonces, en su caso, debe implementar su interrupción de metal desnudo directamente en los registros y no confiar en ningún HAL.


Solución de ejemplo

Por ejemplo algo como:

if (EXTI->PR & EXTI_PR_PR6)
{
    GPIOE->BSRR = GPIO_BSRR_BS_8;
    EXTI->PR = EXTI_PR_PR6;
}

Nota: esto no cambiará el LED sino que simplemente lo configurará. No hay una palanca atómica disponible en los GPIO STM. Tampoco me gusta la ifconstrucción que utilicé, pero genera un ensamblaje más rápido que mi preferido if (EXTI_PR_PR6 == (EXTI->PR & EXTI_PR_PR6)).

Una variante de alternancia podría ser algo así:

static bool LEDstate = false;
if (EXTI->PR & EXTI_PR_PR6)
{
    if (!LEDstate)
    {
        GPIOE->BSRR = GPIO_BSRR_BS_8;
        LEDstate = true;
    }
    else
    {
        GPIOE->BSRR = GPIO_BSRR_BR_8;
        LEDstate = false;
    }
    EXTI->PR = EXTI_PR_PR6;
}

El uso de una variable que reside en la RAM en lugar de usar el ODRregistro debería ser más rápido, especialmente cuando usa 72 MHz, porque el acceso a los periféricos puede ser más lento debido a la sincronización entre diferentes dominios de reloj y relojes periféricos que simplemente se ejecutan a una frecuencia más baja. Por supuesto, no puede cambiar el estado del LED fuera de la interrupción para que la palanca funcione correctamente. O la variable debe ser global (entonces debe usar la volatilepalabra clave al declararla) y debe cambiarla en todas partes en consecuencia.

También tenga en cuenta que estoy usando C ++, por lo tanto, booly no algún uint8_ttipo o similar para implementar una bandera. Aunque si la velocidad es su principal preocupación, probablemente debería optar por un uint32_tindicador, ya que siempre estará alineado correctamente y no generará código adicional al acceder.

La simplificación es posible porque esperemos que sepa lo que está haciendo y siempre lo mantenga así. Si realmente solo tiene una interrupción habilitada para el controlador EXTI9_5, puede deshacerse de la verificación de registro pendiente por completo, reduciendo aún más el número de ciclos.

Esto conduce a otro potencial de optimización: use una línea EXTI que tenga una sola interrupción como una de EXTI1 a EXTI4. Allí no tiene que verificar si la línea correcta ha activado su interrupción.

Arsenal
fuente
1
Es difícil saber del código C cuántas instrucciones tomaría. He visto funciones más grandes optimizadas para un par de instrucciones que ni siquiera implicaban una llamada real.
Dmitry Grigoryev
1
@DmitryGrigoryev como registro se declara ya que volatileel compilador no puede optimizar mucho en las funciones anteriores y si las funciones no se implementan en línea en el encabezado, la llamada generalmente tampoco se optimiza.
Arsenal
5

Siguiendo la sugerencia de PeterJ, he omitido el uso de SPL. La totalidad de mi código se ve así:

#include "stm32f30x.h"

void EXTI0_IRQHandler(void)
{
    // I am simply toggling the pin within the interrupt, as I only want to check the response speed.
     GPIOE->BSRR |= GPIO_BSRR_BS_10;
     GPIOE->BRR |= GPIO_BRR_BR_10;
     EXTI->PR |= EXTI_PR_PR0;
}

int main()
{
    // Initialize the HSI:
    RCC->CR |= RCC_CR_HSION;
    while(!(RCC->CR&RCC_CR_HSIRDY));

    // PLL configuration:
    RCC->CFGR &= ~RCC_CFGR_PLLSRC;     // HSI / 2 selected as the PLL input clock.
    RCC->CFGR |= RCC_CFGR_PLLMULL16;   // 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_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

    // LED output:
    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;

    // Interrupt on PA0:
    RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
    GPIOA->MODER &= ~(GPIO_MODER_MODER0);
    GPIOA->OSPEEDR |= (GPIO_OSPEEDER_OSPEEDR0);
    GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPDR0);
    SYSCFG->EXTICR[0] &= SYSCFG_EXTICR1_EXTI0_PA;
    EXTI->RTSR = EXTI_RTSR_TR0;
    EXTI->IMR = EXTI_IMR_MR0; 
    NVIC_SetPriority(EXTI0_IRQn, 1);
    NVIC_EnableIRQ(EXTI0_IRQn);

    while(1)
    {

    }
}

y la instrucción de montaje se ve así:

EXTI0_IRQHandler:
        LDR.N    R0,??DataTable1  ;; 0x48001018
        LDR      R1,[R0, #+0]
        ORR      R1,R1,#0x400
        STR      R1,[R0, #+0]
        LDRH     R2,[R0, #+16]
        ORR      R2,R2,#0x400
        STRH     R2,[R0, #+16]
        LDR.N    R0,??DataTable1_1  ;; 0x40010414
        LDR      R1,[R0, #+0]
        ORR      R1,R1,#0x1
        STR      R1,[R0, #+0]
        BX       LR               ;; return

Esto mejora bastante las cosas, ya que he logrado obtener una respuesta en ~ 440 ns @ 64 MHz (es decir, 28 ciclos de reloj).

KR
fuente
2
Cambie your BRR |= y BSRR |= a just BRR = y BSRR = , esos registros son solo de escritura, su código los lee, registra ORRel valor y luego escribe. eso podría optimizarse para una sola STRinstrucción.
Colin
Mueva su controlador y vectores EXTI a CCMRAM
P__J__
3

La respuesta es extremadamente fácil: gran biblioteca HAL (o SPL). Si hace algo sensible al tiempo, utilice registros periféricos desnudos. Entonces obtendrás la latencia correcta. No puedo entender cuál es el punto de usar esta biblioteca ridícula para alternar el pin. o para verificar el registro de la estatua.

P__J__
fuente
3

Hay algunos errores en su código = el registro BSRR es solo de escritura. No utilice el operador | =, simplemente "=". Establecerá / restablecerá los pines adecuados. Los ceros son ignorados.

Le ahorrará un par de relojes. Otra pista: mueva su tabla de vectores e interrumpa las rutinas a CCMRAM. Guardará algunas otras marcas (estados de espera flash, etc.)

PD: No puedo comentar porque no tengo suficiente reputación :)

P__J__
fuente