Latência de interrupção em um MCU STM32F303

8

Estou trabalhando em um projeto que envolve um STM32 MCU (na placa STM32303C-EVAL para ser exato) que precisa responder a uma interrupção externa. Quero que a reação à interrupção externa seja o mais rápida possível. Modifiquei um exemplo de biblioteca periférica padrão na página da Web ST e o programa atual simplesmente alterna um LED em cada borda ascendente sucessiva do 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); 
}

O manipulador de interrupção fica assim:

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);
  }
}

Nesse caso em particular, as interrupções são criadas por um gerador de função programável externo funcionando a 100 Hz. Depois de examinar a resposta do MCU em um osciloscópio, fiquei surpreso que levamos quase 1,32 para o MCU começar a processar a interrupção: insira a descrição da imagem aqui

Com o MCU rodando a 72 MHz (verifiquei a saída SYSCLK no pino MCO de antemão), isso equivale a quase 89 ciclos de clock. A resposta do MCU à interrupção não deveria ser muito mais rápida?

PS O código foi compilado com o IAR Embedded Workbench e otimizado para maior velocidade.

KR
fonte
Tem certeza de que é o atraso para iniciar o processamento da interrupção? O que acontece quando você remove a condição if e apenas alterna?
BB00
@ BeB00 a if{}declaração é necessária porque a rotina de interrupção não sabe qual é a fonte da interrupção.
Rohat Kılıç
Se bem me lembro, a latência deve ser em torno de 10-15 ciclos
BeB00
1
Certo, mas o que acontece quando você o remove em seu experimento? Eu suponho que você não tem uma carga de outras interrupções provocando isso constantemente, então você deve ser capaz de obter uma sensação melhor para o atraso real
BeB00
1
Realmente não deveria ser um mistério. Olhada no código assembler compilado para sua função de interrupção e referem-se a ARM apropriado ref manual para somar o número de ciclos de clock para cada instrução ...
brhans

Respostas:

8

Problema

Bem, você tem que olhar para as funções que está usando, não pode simplesmente fazer suposições sobre a velocidade do código que não olhou:

Esta é a função 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 você pode ver, isso não é algo simples que requer apenas um ciclo ou dois.

A seguir, sua função de alternância de LED:

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

Então, aqui você tem alguma indexação de matriz e uma gravação de modificação de leitura para alternar o LED.

Os HALs geralmente acabam criando uma boa quantidade de sobrecarga, porque precisam cuidar de configurações incorretas e do uso incorreto das funções. A verificação de parâmetros necessária e também a conversão de um parâmetro simples para um bit no registro podem levar uma quantidade séria de computação (pelo menos por uma interrupção crítica no tempo).

Portanto, no seu caso, você deve implementar sua interrupção bare metal diretamente nos registros e não confiar em nenhum HAL.


Solução de exemplo

Por exemplo, algo como:

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

Nota: isso não alterna o LED, mas simplesmente o define. Não há alternância atômica disponível nos GPIOs do STM. Também não gosto da ifconstrução que usei, mas gera uma montagem mais rápida que a minha preferida if (EXTI_PR_PR6 == (EXTI->PR & EXTI_PR_PR6)).

Uma variante de alternância pode ser algo assim:

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;
}

O uso de uma variável que reside na RAM em vez de o ODRregistro deve ser mais rápido, especialmente quando você usa 72 MHz, porque o acesso aos periféricos pode ser mais lento devido à sincronização entre diferentes domínios do relógio e relógios periféricos, executando simplesmente em uma frequência mais baixa. Obviamente, você não pode alterar o estado do LED fora da interrupção para que a alternância funcione corretamente. Ou a variável deve ser global (então você precisa usar a volatilepalavra - chave ao declará-la) e precisa alterá-la em qualquer lugar de acordo.

Observe também que estou usando C ++, portanto o boole não algum uint8_ttipo ou similar para implementar um sinalizador. Embora a velocidade seja sua principal preocupação, provavelmente você deve optar por um uint32_tpara o sinalizador, pois ele sempre estará alinhado corretamente e não gerará código adicional ao acessar.

A simplificação é possível porque, com sorte, você sabe o que está fazendo e sempre o mantém. Se você realmente tiver apenas uma única interrupção ativada para o manipulador EXTI9_5, poderá se livrar completamente da verificação de registro pendente, reduzindo ainda mais o número de ciclos.

Isso leva a outro potencial de otimização: use uma linha EXTI que tenha uma única interrupção, como uma das EXTI1 a EXTI4. Lá, você não precisa verificar se a linha correta provocou sua interrupção.

Arsenal
fonte
1
É difícil dizer pelo código C quantas instruções seriam necessárias. Vi funções maiores otimizadas para algumas instruções que nem sequer envolviam uma chamada real.
Dmitry Grigoryev
1
@DmitryGrigoryev como register é declarado, pois volatileo compilador não pode otimizar muito nas funções acima e se as funções não forem implementadas em linha no cabeçalho, a chamada também não será otimizada.
Arsenal
5

Seguindo a sugestão de PeterJ, omiti o uso de SPL. A totalidade do meu código fica assim:

#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)
    {

    }
}

e as instruções de montagem são assim:

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

Isso melhora bastante as coisas, pois eu consegui obter uma resposta em ~ 440 ns a 64 MHz (ou seja, 28 ciclos de clock).

KR
fonte
2
Altere your BRR |= e BSRR |= para just BRR = e BSRR = , esses registradores são apenas para gravação, seu código os está lendo, ORRdigitando o valor e depois escrevendo. que poderia ser otimizado para uma única STRinstrução.
Colin
Mova seu manipulador e vetores EXTI para CCMRAM
P__J__
3

A resposta é extremamente fácil: ótima biblioteca HAL (ou SPL). Se você fizer algo sensível ao tempo, use registros periféricos simples. Então você obterá a latência correta. Eu não consigo entender qual é o objetivo de usar essa biblioteca ridícula para alternar o pino !! ou para verificar o registro da estátua.

P__J__
fonte
3

Existem alguns erros no seu código = o registro BSRR é somente gravação. Não use o operador | =, apenas "=". Ele irá definir / redefinir os pinos adequados. Os zeros são ignorados.

Economizará alguns relógios. Outra dica: mova sua tabela de vetores e interrompa as rotinas para o CCMRAM. Você salvará alguns outros carrapatos (flash waitstates etc)

PS Não posso comentar, pois não tenho reputação suficiente :)

P__J__
fonte