Latence d'interruption sur un microcontrôleur STM32F303

8

Je travaille sur un projet qui implique un microcontrôleur STM32 (sur la carte STM32303C-EVAL pour être exact) qui doit répondre à une interruption externe. Je veux que la réaction à l'interruption externe soit aussi rapide que possible. J'ai modifié un exemple de bibliothèque périphérique standard à partir de la page Web ST et le programme actuel bascule simplement une LED à chaque front montant successif sur 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); 
}

Le gestionnaire d'interruption ressemble à ceci:

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

Dans ce cas particulier, les interruptions sont créées par un générateur de fonction programmable externe fonctionnant à 100 Hz. Après avoir examiné la réponse du MCU sur un oscilloscope, j'ai été plutôt surpris qu'il nous faut près de 1,32 pour que le MCU commence à traiter l'interruption: entrez la description de l'image ici

Avec le MCU fonctionnant à 72 MHz (j'ai vérifié la sortie SYSCLK sur la broche MCO au préalable), cela représente près de 89 cycles d'horloge. La réponse du MCU à l'interruption ne devrait-elle pas être beaucoup plus rapide?

PS Le code a été compilé avec IAR Embedded Workbench et optimisé pour la vitesse la plus élevée.

KR
la source
Êtes-vous sûr que c'est le délai pour commencer le traitement de l'interruption? Que se passe-t-il lorsque vous supprimez la condition if et basculez simplement?
BeB00
@ BeB00 l' if{}instruction est nécessaire car la routine d'interruption ne sait pas quelle est la source de l'interruption.
Rohat Kılıç
Si je me souviens bien, la latence devrait être d'environ 10-15 cycles
BeB00
1
D'accord, mais que se passe-t-il lorsque vous le supprimez dans votre test? Je suppose que vous n'avez pas une charge d'autres interruptions déclenchant cela en permanence, vous devriez donc être en mesure de mieux comprendre le retard réel
BeB00
1
Cela ne devrait vraiment pas être un mystère. Regardez le code assembleur compilé pour votre fonction d'interruption et reportez-vous au manuel de référence ARM approprié pour additionner le nombre de cycles d'horloge pour chaque instruction ...
brhans

Réponses:

8

Problème

Eh bien, vous devez regarder les fonctions que vous utilisez, vous ne pouvez pas simplement faire des hypothèses sur la vitesse du code que vous n'avez pas regardé:

Il s'agit de la fonction 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;

}

Comme vous pouvez le voir, ce n'est pas une chose simple nécessitant juste un cycle ou deux.

Vient ensuite votre fonction de bascule LED:

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

Vous avez donc ici une indexation de tableau et une lecture-écriture-modification pour basculer la LED.

Les HAL finissent souvent par créer une bonne quantité de frais généraux car ils doivent prendre soin de mauvais réglages et d'une mauvaise utilisation des fonctions. La vérification des paramètres nécessaires ainsi que la traduction d'un simple paramètre en un bit du registre peuvent prendre une quantité importante de calcul (enfin pour une interruption critique en temps).

Donc, dans votre cas, vous devez implémenter votre interruption bare metal directement sur les registres et ne compter sur aucun HAL.


Exemple de solution

Par exemple quelque chose comme:

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

Remarque: cela ne fera pas basculer la LED mais la réglera simplement. Il n'y a pas de bascule atomique disponible sur les GPIO STM. Je n'aime pas non plus la ifconstruction que j'ai utilisée, mais elle génère un assemblage plus rapide que mon préféréif (EXTI_PR_PR6 == (EXTI->PR & EXTI_PR_PR6)) .

Une variante à bascule pourrait être quelque chose dans ce sens:

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

L'utilisation d'une variable résidant dans la RAM au lieu d'utiliser le ODRregistre devrait être plus rapide, surtout lorsque vous utilisez 72 MHz, car l'accès aux périphériques peut être plus lent en raison de la synchronisation entre les différents domaines d'horloge et les horloges périphériques fonctionnant simplement à une fréquence inférieure. Bien sûr, vous ne pouvez pas modifier l'état de la LED en dehors de l'interruption pour que la bascule fonctionne correctement. Ou la variable doit être globale (alors vous devez utiliser le volatilemot - clé pour la déclarer) et vous devez la changer partout en conséquence.

Notez également que j'utilise C ++, d'où le type boolet pas un uint8_ttype ou similaire pour implémenter un indicateur. Bien que si la vitesse est votre principale préoccupation, vous devriez probablement opter pour un uint32_tindicateur car il sera toujours correctement aligné et ne générera pas de code supplémentaire lors de l'accès.

La simplification est possible parce que nous espérons que vous savez ce que vous faites et que vous le gardez toujours ainsi. Si vous n'avez vraiment qu'une seule interruption activée pour le gestionnaire EXTI9_5, vous pouvez vous débarrasser complètement de la vérification du registre en attente, ce qui réduit encore plus le nombre de cycles.

Cela conduit à un autre potentiel d'optimisation: utilisez une ligne EXTI qui a une seule interruption comme celle de EXTI1 à EXTI4. Là, vous n'avez pas à vérifier si la ligne correcte a déclenché votre interruption.

Arsenal
la source
1
Il est difficile de dire à partir du code C combien d'instructions cela prendrait. J'ai vu de plus grandes fonctions optimisées pour quelques instructions qui n'impliquaient même pas un appel réel.
Dmitry Grigoryev
1
@DmitryGrigoryev as register sont déclarés car volatilele compilateur n'est pas autorisé à optimiser beaucoup dans les fonctions ci-dessus et si les fonctions ne sont pas implémentées en ligne dans l'en-tête, l'appel n'est généralement pas optimisé non plus.
Arsenal
5

Suite à la suggestion de PeterJ, j'ai omis l'utilisation de SPL. L'intégralité de mon code ressemble à ceci:

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

    }
}

et les instructions d'assemblage ressemblent à ceci:

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

Cela améliore un peu les choses, car j'ai réussi à obtenir une réponse en ~ 440 ns à 64 MHz (soit 28 cycles d'horloge).

KR
la source
2
Changez votre BRR |= et BSRR |= en juste BRR = et BSRR = , ces registres sont en écriture seule, votre code les lit, ORRing la valeur puis écrit. qui pourrait être optimisé en une seule STRinstruction.
Colin
Déplacez votre gestionnaire EXTI et vos vecteurs vers CCMRAM
P__J__
3

La réponse est extrêmement simple: une excellente bibliothèque HAL (ou SPL). Si vous faites quelque chose de sensible au temps, utilisez plutôt des registres périphériques nus. Ensuite, vous obtiendrez la latence correcte. Je ne peux pas comprendre quel est l'intérêt d'utiliser cette bibliothèque ridicule pour basculer la broche !! ou pour vérifier le registre des statues.

P J__
la source
3

Il y a quelques erreurs dans votre code = le registre BSRR est en écriture uniquement. N'utilisez pas l'opérateur | =, juste un simple "=". Il définira / réinitialisera les broches appropriées. Les zéros sont ignorés.

Cela vous fera économiser quelques horloges. Autre conseil: déplacez votre table vectorielle et interrompez les routines vers CCMRAM. Vous économiserez quelques autres ticks (flash waitstates etc)

PS Je ne peux pas commenter car je n'ai pas assez de réputation :)

P J__
la source