Unterbrechen Sie die Latenz auf einer STM32F303-MCU

8

Ich arbeite an einem Projekt, das eine STM32-MCU (genauer gesagt auf der STM32303C-EVAL-Karte) umfasst, die auf einen externen Interrupt reagieren muss. Ich möchte, dass die Reaktion auf den externen Interrupt so schnell wie möglich erfolgt. Ich habe ein Beispiel für eine Standard-Peripheriebibliothek auf der ST-Webseite geändert und das aktuelle Programm schaltet einfach eine LED bei jeder aufeinanderfolgenden ansteigenden Flanke auf PE6 um:

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

Der Interrupt-Handler sieht folgendermaßen aus:

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

In diesem speziellen Fall werden die Interrupts von einem externen programmierbaren Funktionsgenerator erzeugt, der mit 100 Hz läuft. Nachdem ich die MCU-Antwort auf einem Oszilloskop untersucht hatte, war ich ziemlich überrascht, dass die MCU fast 1,32 Minuten benötigt, um mit der Verarbeitung des Interrupts zu beginnen: Geben Sie hier die Bildbeschreibung ein

Bei einer MCU mit 72 MHz (ich habe zuvor den SYSCLK-Ausgang am MCO-Pin überprüft) entspricht dies fast 89 Taktzyklen. Sollte die MCU-Reaktion auf den Interrupt nicht viel schneller sein?

PS Der Code wurde mit IAR Embedded Workbench kompiliert und für höchste Geschwindigkeit optimiert.

KR
quelle
Sind Sie sicher, dass dies die Verzögerung ist, um mit der Verarbeitung des Interrupts zu beginnen? Was passiert, wenn Sie die if-Bedingung entfernen und einfach umschalten?
BeB00
@ BeB00 Die if{}Anweisung wird benötigt, da die Interruptroutine die Quelle des Interrupts nicht kennt.
Rohat Kılıç
Wenn ich mich richtig erinnere, sollte die Latenz ungefähr 10-15 Zyklen
betragen
1
Richtig, aber was passiert, wenn Sie es in Ihrem Experiment entfernen? Ich gehe davon aus, dass Sie nicht viele andere Interrupts haben, die dies ständig auslösen, sodass Sie ein besseres Gefühl für die tatsächliche Verzögerung bekommen sollten
BeB00
1
Es sollte wirklich kein Rätsel sein. Schauen Sie sich den kompilierten Assembler-Code für Ihre Interrupt-Funktion an und
lesen Sie

Antworten:

8

Problem

Nun, Sie müssen sich die Funktionen ansehen, die Sie verwenden. Sie können nicht einfach Annahmen über die Geschwindigkeit des Codes treffen, den Sie nicht betrachtet haben:

Dies ist die EXTI_GetITStatus-Funktion:

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;

}

Wie Sie sehen können, ist dies keine einfache Sache, die nur ein oder zwei Zyklen erfordert.

Als nächstes kommt Ihre LED-Umschaltfunktion:

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

Hier haben Sie also eine Array-Indizierung und einen Lese- und Schreibvorgang, um die LED umzuschalten.

HALs verursachen häufig einen hohen Overhead, da sie sich um falsche Einstellungen und die falsche Verwendung der Funktionen kümmern müssen. Die erforderliche Parameterprüfung und auch die Übersetzung von einem einfachen Parameter in ein Bit im Register kann einen erheblichen Rechenaufwand erfordern (zumindest für einen zeitkritischen Interrupt).

In Ihrem Fall sollten Sie Ihr Interrupt-Bare-Metal direkt in den Registern implementieren und sich nicht auf HAL verlassen.


Beispiellösung

Zum Beispiel so etwas wie:

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

Hinweis: Dadurch wird die LED nicht umgeschaltet, sondern einfach eingestellt. Auf den STM-GPIOs ist kein atomarer Umschalter verfügbar. Ich mag das ifKonstrukt, das ich verwendet habe, auch nicht , aber es erzeugt eine schnellere Assemblierung als meine bevorzugte if (EXTI_PR_PR6 == (EXTI->PR & EXTI_PR_PR6)).

Eine Umschaltvariante könnte in diese Richtung gehen:

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

Die Verwendung einer Variablen im RAM anstelle des ODRRegisters sollte schneller sein, insbesondere wenn Sie 72 MHz verwenden, da der Zugriff auf die Peripheriegeräte aufgrund der Synchronisation zwischen verschiedenen Taktdomänen und Peripherietakten, die einfach mit einer niedrigeren Frequenz ausgeführt werden, langsamer sein kann. Natürlich dürfen Sie den Status der LED außerhalb des Interrupts nicht ändern, damit der Umschalter ordnungsgemäß funktioniert. Oder die Variable muss global sein (dann müssen Sie das volatileSchlüsselwort verwenden, wenn Sie es deklarieren) und Sie müssen es überall entsprechend ändern.

Beachten Sie auch, dass ich C ++ verwende, daher der boolund kein uint8_tTyp oder ähnliches, um ein Flag zu implementieren. Wenn Geschwindigkeit Ihr Hauptanliegen ist, sollten Sie sich wahrscheinlich für ein uint32_tFlag entscheiden, da dieses immer korrekt ausgerichtet ist und beim Zugriff keinen zusätzlichen Code generiert.

Die Vereinfachung ist möglich, weil Sie hoffentlich wissen, was Sie tun, und es immer so halten. Wenn Sie wirklich nur einen einzigen Interrupt für den EXTI9_5-Handler aktiviert haben, können Sie die ausstehende Registerprüfung vollständig entfernen und die Anzahl der Zyklen noch weiter reduzieren.

Dies führt zu einem weiteren Optimierungspotential: Verwenden Sie eine EXTI-Leitung mit einem einzelnen Interrupt wie EXTI1 bis EXTI4. Dort müssen Sie nicht prüfen, ob die richtige Leitung Ihren Interrupt ausgelöst hat.

Arsenal
quelle
1
An C-Code ist schwer zu erkennen, wie viele Anweisungen erforderlich sind. Ich habe größere Funktionen gesehen, die für einige Anweisungen optimiert wurden, die nicht einmal einen tatsächlichen Aufruf beinhalteten.
Dmitry Grigoryev
1
@DmitryGrigoryev als Register werden deklariert, da volatileder Compiler in den obigen Funktionen nicht viel optimieren darf. Wenn die Funktionen nicht inline im Header implementiert sind, wird der Aufruf normalerweise auch nicht wegoptimiert.
Arsenal
5

Auf den Vorschlag von PeterJ habe ich die Verwendung von SPL weggelassen. Der gesamte Code sieht folgendermaßen aus:

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

    }
}

und die Montageanleitung sieht folgendermaßen aus:

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

Dies verbessert die Sache erheblich, da ich es geschafft habe, eine Antwort in ~ 440 ns bei 64 MHz (dh 28 Taktzyklen) zu erhalten.

KR
quelle
2
Ändern Sie Ihr BRR |= und BSRR |= in nur BRR = und BSRR = , diese Register sind nur schreibbar, Ihr Code liest sie, gibt ORRden Wert ein und schreibt dann. das könnte auf einen einzigen STRBefehl optimiert werden .
Colin
Verschieben Sie Ihren EXTI-Handler und Ihre Vektoren zu CCMRAM
P__J__
3

Die Antwort ist sehr einfach: großartige HAL- (oder SPL-) Bibliothek. Wenn Sie etwas zeitkritisches tun, verwenden Sie stattdessen bloße Peripherieregister. Dann erhalten Sie die richtige Latenz. Ich kann nicht verstehen, was der Sinn ist, diese lächerliche Bibliothek zu benutzen, um den Stift umzuschalten !! oder um das Statuenregister zu überprüfen.

P__J__
quelle
3

Ihr Code enthält einige Fehler = Das BSRR-Register ist nur schreibbar. Verwenden Sie nicht den Operator | =, sondern nur das einfache "=". Die richtigen Pins werden gesetzt / zurückgesetzt. Nullen werden ignoriert.

Sie sparen ein paar Uhren. Ein weiterer Hinweis: Verschieben Sie Ihre Vektortabellen- und Interruptroutinen nach CCMRAM. Sie speichern einige weitere Ticks (Flash-Wartezustände usw.)

PS Ich kann nicht kommentieren, da ich nicht genug Ruf habe :)

P__J__
quelle