Schnelle Leistung von einer STM32-MCU

Ich arbeite mit dem STM32F303VC Discovery Kit und bin etwas verwirrt über seine Leistung. Um mich mit dem System vertraut zu machen, habe ich ein sehr einfaches Programm geschrieben, um einfach die Bit-Banging-Geschwindigkeit dieser MCU zu testen. Der Code lässt sich wie folgt aufschlüsseln:

  1. HSI-Takt (8 MHz) ist eingeschaltet;
  2. PLL wird mit dem Prescaler von 16 initiiert, um HSI / 2 * 16 = 64 MHz zu erreichen;
  3. PLL wird als SYSCLK bezeichnet;
  4. SYSCLK wird am MCO-Pin (PA8) überwacht, und einer der Pins (PE10) wird in der Endlosschleife ständig umgeschaltet.

Der Quellcode für dieses Programm ist unten dargestellt:

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

      }
}

Der Code wurde mit CoIDE V2 mit der GNU ARM Embedded Toolchain unter Verwendung von -O1-Optimierung kompiliert. Die mit einem Oszilloskop untersuchten Signale an den Pins PA8 (MCO) und PE10 sehen so aus:Geben Sie hier die Bildbeschreibung ein

Der SYSCLK scheint korrekt konfiguriert zu sein, da der MCO (orange Kurve) eine Schwingung von knapp 64 MHz aufweist (unter Berücksichtigung der Fehlergrenze der internen Uhr). Das Seltsame für mich ist das Verhalten auf PE10 (blaue Kurve). In der unendlichen While(1)-Schleife dauert es 4 + 4 + 5 = 13 Taktzyklen, um eine elementare 3-Schritt-Operation durchzuführen (dh Bit-Set/Bit-Reset/Return). Auf anderen Optimierungsstufen (z. B. -O2, -O3, ar -Os) wird es noch schlimmer: Mehrere zusätzliche Taktzyklen werden zum LOW-Teil des Signals hinzugefügt, dh zwischen der fallenden und steigenden Flanke von PE10 (das LSI scheint irgendwie aktiviert zu werden um diesen Zustand zu beheben).

Wird dieses Verhalten von dieser MCU erwartet? Ich würde mir vorstellen, dass eine so einfache Aufgabe wie das Setzen und Zurücksetzen eines Bits 2-4 mal schneller sein sollte. Gibt es eine Möglichkeit, die Dinge zu beschleunigen?

Hast du mal versucht mit einer anderen MCU zu vergleichen?
Was versuchst du zu erreichen? Wenn Sie eine schnell oszillierende Ausgabe wünschen, sollten Sie Timer verwenden. Wenn Sie mit schnellen seriellen Protokollen kommunizieren möchten, sollten Sie die entsprechende Hardware-Peripherie verwenden.
Toller Start mit dem Bausatz!!
Sie dürfen nicht |= BSRR- oder BRR-Register verwenden, da sie nur zum Schreiben sind.

Antworten (4)

Die Frage hier ist wirklich: Was ist der Maschinencode, den Sie aus dem C-Programm generieren, und wie unterscheidet er sich von dem, was Sie erwarten würden?

Wenn Sie keinen Zugriff auf den Originalcode hätten, wäre dies eine Übung in Reverse Engineering gewesen (im Grunde etwas, das mit beginnt: radare2 -A arm image.bin; aaa; VV), aber Sie haben den Code, also macht es alles einfacher.

Kompilieren Sie es zuerst mit dem -ghinzugefügten Flag CFLAGS(an der gleichen Stelle, an der Sie auch angeben -O1). Sehen Sie sich dann die generierte Assembly an:

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

Beachten Sie, dass sowohl der Name der objdumpBinärdatei als auch Ihre zwischengeschaltete ELF-Datei unterschiedlich sein können.

Normalerweise können Sie auch einfach den Teil überspringen, in dem GCC den Assembler aufruft, und sich nur die Assembly-Datei ansehen. Fügen Sie einfach -Szur GCC-Befehlszeile hinzu – aber das wird normalerweise Ihren Build beschädigen, also würden Sie es höchstwahrscheinlich außerhalb Ihrer IDE machen.

Ich habe die Assemblierung einer leicht gepatchten Version Ihres Codes durchgeführt :

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

und bekam folgendes (Auszug, vollständiger Code unter Link oben):

.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

Das ist eine Schleife (beachten Sie den unbedingten Sprung zu .L5 am Ende und das Label .L5 am Anfang).

Was wir hier sehen, ist, dass wir

  • zuerst ldr(Load Register) das Register r2mit dem Wert am Speicherplatz gespeichert in r3+ 24 Bytes. Wenn Sie zu faul sind, das nachzuschlagen: Sehr wahrscheinlich ist der Standort von BSRR.
  • Dann ORdas r2Register mit der Konstante 1024 == (1<<10), was dem Setzen des 10. Bits in diesem Register entsprechen würde, und das Ergebnis in r2sich selbst schreiben.
  • Dann str(speichern) Sie das Ergebnis in dem Speicherplatz, aus dem wir im ersten Schritt gelesen haben
  • und dann aus Faulheit dasselbe für einen anderen Speicherplatz wiederholen: höchstwahrscheinlich BRRdie Adresse von .
  • Abschließend b(Verzweigung) zurück zum ersten Schritt.

Wir haben also 7 Anweisungen, nicht drei, um damit zu beginnen. Nur das bpassiert einmal, und daher ist es sehr wahrscheinlich, dass eine ungerade Anzahl von Zyklen benötigt wird (wir haben insgesamt 13, also muss irgendwo eine ungerade Anzahl von Zyklen herkommen). Da alle ungeraden Zahlen unter 13 1, 3, 5, 7, 9, 11 sind und wir alle Zahlen größer als 13-6 ausschließen können (vorausgesetzt, die CPU kann eine Anweisung nicht in weniger als einem Zyklus ausführen), wissen wir Bescheid dass das b1, 3, 5 oder 7 CPU-Zyklen dauert.

So wie wir sind, habe ich mir die Anleitungsdokumentation von ARM angesehen und wie viele Zyklen sie für den M3 benötigen:

  • ldrdauert 2 Zyklen (in den meisten Fällen)
  • orrdauert 1 Zyklus
  • strdauert 2 Zyklen
  • bdauert 2 bis 4 Zyklen. Wir wissen, dass es eine ungerade Zahl sein muss, also muss es hier 3 sein.

Das stimmt alles mit deiner Beobachtung überein:

13 = 2 ( c l d r + c Ö r r + c s t r ) + c b = 2 ( 2 + 1 + 2 ) + 3 = 2 5 + 3


Wie die obige Berechnung zeigt, wird es kaum eine Möglichkeit geben, Ihre Schleife schneller zu machen – die Ausgangspins auf ARM-Prozessoren sind normalerweise speicherabgebildet , nicht CPU-Kernregister, also müssen Sie die übliche Routine zum Laden – Ändern – Speichern durchlaufen , wenn Sie wollen alles mit denen machen.

Was Sie natürlich tun könnten, ist, den Wert des Pins nicht bei jeder Schleifeniteration zu lesen ( implizit|= zu lesen), sondern einfach den Wert einer lokalen Variablen darauf zu schreiben, die Sie bei jeder Schleifeniteration umschalten.

Beachten Sie, dass Sie meiner Meinung nach mit 8-Bit-Mikros vertraut sind und versuchen würden, nur 8-Bit-Werte zu lesen, sie in lokalen 8-Bit-Variablen zu speichern und sie in 8-Bit-Blöcken zu schreiben. Nicht. ARM ist eine 32-Bit-Architektur, und das Extrahieren von 8 Bit eines 32-Bit-Wortes kann zusätzliche Anweisungen erfordern. Wenn Sie können, lesen Sie einfach das gesamte 32-Bit-Wort, ändern Sie, was Sie brauchen, und schreiben Sie es als Ganzes zurück. Ob das möglich ist, hängt natürlich davon ab, worauf Sie schreiben, dh das Layout und die Funktionalität Ihres speicherabgebildeten GPIO. Konsultieren Sie das STM32F3-Datenblatt/Benutzerhandbuch für Informationen darüber, was in der 32-Bit-Datei gespeichert ist, die das Bit enthält, das Sie umschalten möchten.


Jetzt habe ich versucht, Ihr Problem mit dem länger werdenden "niedrigen" Zeitraum zu reproduzieren, aber ich konnte es einfach nicht - die Schleife sieht mit genau so aus -O3wie -O1mit meiner Compiler-Version. Das musst du selbst machen! Vielleicht verwenden Sie eine alte Version von GCC mit suboptimaler ARM-Unterstützung.

Wäre das Speichern ( =anstelle von |=), wie Sie sagen, nicht genau die Beschleunigung, nach der das OP sucht? Der Grund, warum ARMs die BRR- und BSRR-Register getrennt haben, besteht darin, dass kein Lesen, Modifizieren und Schreiben erforderlich ist. In diesem Fall könnten die Konstanten in Registern außerhalb der Schleife gespeichert werden, sodass die innere Schleife nur aus 2 Saiten und einer Verzweigung bestehen würde, also 2 + 2 + 3 = 7 Zyklen für die gesamte Runde?
Vielen Dank. Das hat wirklich einiges aufgeklärt. Es war ein bisschen voreilig, darauf zu bestehen, dass nur 3 Taktzyklen benötigt würden - 6 bis 7 Zyklen waren etwas, worauf ich eigentlich gehofft hatte. Der -O3Fehler scheint nach dem Reinigen und Neuaufbau der Lösung verschwunden zu sein. Trotzdem scheint mein Assembler-Code eine zusätzliche UTXH-Anweisung zu enthalten:.L5: ldrh r3, [r2, #24] uxth r3, r3 orr r3, r3, #1024 strh r3, [r2, #24] @ movhi ldr r3, [r2, #40] orr r3, r3, #1024 str r3, [r2, #40] b .L5
uxthist da, weil GPIO->BSRRLes (fälschlicherweise) als 16-Bit-Register in Ihren Headern definiert ist. Verwenden Sie eine neuere Version der Header aus den STM32CubeF3- Bibliotheken, in denen es kein BSRRL und BSRRH gibt, sondern ein einzelnes 32-Bit- BSRRRegister. @Marcus hat anscheinend die richtigen Header, daher führt sein Code vollständige 32-Bit-Zugriffe durch, anstatt ein Halbwort zu laden und es zu erweitern.
Warum würde das Laden eines einzelnen Bytes zusätzliche Anweisungen erfordern? Die ARM-Architektur hat LDRBund STRBdie Byte-Lese-/Schreibvorgänge in einer einzigen Anweisung ausführen, oder?
@psmears: weil LDRBund LDRHdie unteren Bits des Zielregisters laden und die höherwertigen Bits in Ruhe lassen. Der Wert muss für den bitweisen ODER-Operator auf 32 Bit ohne Vorzeichen hochgestuft werden, da GPIO_BSRR_BS_10dies so definiert ist. UXTBoder UXTHsetzt die nicht geladenen Bits auf 0.
@berendi Genau. Verwenden des richtigen Headers und BSRRanstelle von BSRRList die Korrektur, die ich vornehmen musste, um sie auf meinem Computer zu erstellen (daher habe ich den vollständigen Code gepostet).
@berendi: Vielleicht fehlt mir etwas, aber laut Speicher (und den Dokumenten ) LDRBerstreckt sich Null auf 32 Bit. Reden wir über verschiedene Varianten der ARM-Architektur oder so etwas?
@psmears die Tatsache, dass die LDRBAnweisung existiert, bedeutet nicht, dass der Header so geschrieben ist, dass er verwendet werden kann, ist meine Vermutung hier.
@psmears: du hast recht uxtboder uxthist unnötig, es ist ein bekannter gcc-fehler
@MarcusMüller: Sicher - ich habe nur die Behauptung in Frage gestellt, dass das Ausführen von Byte-Operationen anstelle von Wort-Operationen mehr Anweisungen erfordern würde, da dies von meiner Erinnerung an die ARM ISA abweicht :)
@berendi: Ah, danke, schön zu wissen, dass ich nicht verrückt werde :)
Der M3-Kern kann Bit-Banding unterstützen (nicht sicher, ob diese spezielle Implementierung dies tut), wobei eine 1-MB-Region des peripheren Speichers in eine 32-MB-Region aliasiert wird. Jedes Bit hat eine diskrete Wortadresse (nur Bit 0 wird verwendet). Vermutlich immer noch langsamer als nur ein Laden/Speichern.
Das Hauptproblem ist: BSRR und BRR und nur schreiben. Es gibt keinen Grund, sie zu ODERn – Sie haben diesen sehr wichtigen Punkt verpasst

Die Register BSRRund BRRdienen zum Setzen und Zurücksetzen einzelner Portbits:

GPIO-Port-Bit-Set/Reset-Register (GPIOx_BSRR)

...

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

BSy: Port x setzt Bit y (y= 0..15)

Diese Bits sind schreibgeschützt. Ein Lesen dieser Bits gibt den Wert 0x0000 zurück.

0: Keine Aktion auf dem entsprechenden ODRx-Bit

1: Setzt das entsprechende ODRx-Bit

Wie Sie sehen können, ergibt das Lesen dieser Register immer 0, also was Ihr Code ist

GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;

effektiv ist GPIOE->BRR = 0 | GPIO_BRR_BR_10, aber der Optimierer weiß das nicht, also generiert er eine Folge von LDR, ORR, STRAnweisungen anstelle eines einzelnen Speichers.

Sie können den teuren Read-Modify-Write-Vorgang vermeiden, indem Sie einfach schreiben

GPIOE->BSRRL = GPIO_BSRR_BS_10;
GPIOE->BRR = GPIO_BRR_BR_10;

Sie könnten eine weitere Verbesserung erzielen, indem Sie die Schleife auf eine Adresse ausrichten, die gleichmäßig durch 8 teilbar ist. Versuchen Sie, eine oder mode- asm("nop");Anweisung vor die while(1)Schleife zu setzen.

Um das hier Gesagte zu ergänzen: Sicherlich beim Cortex-M, aber bei so ziemlich jedem Prozessor (mit Pipeline, Cache, Verzweigungsvorhersage oder anderen Funktionen) ist es trivial, selbst die einfachste Schleife zu nehmen:

top:
   subs r0,#1
   bne top

Führen Sie es so viele Millionen Mal aus, wie Sie möchten, aber Sie können die Leistung dieser Schleife stark variieren lassen, nur diese beiden Anweisungen, fügen Sie einige Nops in der Mitte hinzu, wenn Sie möchten; es spielt keine Rolle.

Das Ändern der Ausrichtung der Schleife kann die Leistung dramatisch variieren, insbesondere bei einer kleinen Schleife wie dieser, wenn zwei Abrufleitungen anstelle von einer erforderlich sind, verbrauchen Sie diese zusätzlichen Kosten auf einem Mikrocontroller wie diesem, bei dem der Flash um 2 langsamer als die CPU ist oder 3 und dann durch Erhöhen der Uhr wird das Verhältnis sogar noch schlechter 3 oder 4 oder 5 als das Hinzufügen von zusätzlichem Abrufen.

Sie haben wahrscheinlich keinen Cache, aber wenn Sie einen hatten, hilft es in einigen Fällen, aber in anderen tut es weh und / oder macht keinen Unterschied. Die Verzweigungsvorhersage, die Sie hier haben oder nicht haben (wahrscheinlich nicht), kann nur so weit wie in der Pipe vorgesehen sehen, also selbst wenn Sie die Schleife so geändert haben, dass sie sich verzweigt und am Ende eine unbedingte Verzweigung hatte (einfacher für einen Verzweigungsprädiktor). verwenden) spart Ihnen nur so viele Takte (Größe der Pipe, von wo aus es normalerweise abrufen würde, bis wie tief der Prädiktor sehen kann) beim nächsten Abruf und/oder es führt keinen Vorabruf durch, nur für den Fall.

Indem Sie die Ausrichtung in Bezug auf Abruf- und Cache-Zeilen ändern, können Sie beeinflussen, ob der Verzweigungsprädiktor Ihnen hilft oder nicht, und das kann in der Gesamtleistung gesehen werden, selbst wenn Sie nur zwei Anweisungen oder diese beiden mit einigen Nops testen .

Es ist ziemlich trivial, dies zu tun, und sobald Sie das verstanden haben, können Sie bei kompiliertem Code oder sogar handgeschriebener Assemblierung sehen, dass seine Leistung aufgrund dieser Faktoren stark variieren kann, indem Sie einige bis zu ein paar hundert Prozent hinzufügen oder einsparen. eine Zeile C-Code, ein schlecht platzierter Nop.

Nachdem Sie gelernt haben, das BSRR-Register zu verwenden, versuchen Sie, Ihren Code aus dem RAM (Kopieren und Springen) anstelle von Flash auszuführen, was Ihnen eine sofortige 2- bis 3-fache Leistungssteigerung bei der Ausführung geben sollte, ohne etwas anderes zu tun.

Wird dieses Verhalten von dieser MCU erwartet?

Es ist ein Verhalten Ihres Codes.

  1. Sie sollten in BRR/BSRR-Register schreiben, nicht lesen-modifizieren-schreiben, wie Sie es jetzt tun.

  2. Sie verursachen auch Schleifen-Overhead. Replizieren Sie für maximale Leistung die BRR/BSRR-Operationen immer und immer wieder → kopieren Sie sie und fügen Sie sie mehrmals in die Schleife ein, damit Sie viele Set/Reset-Zyklen durchlaufen, bevor ein Schleifen-Overhead entsteht.

Bearbeiten: Einige Schnelltests unter IAR.

ein Flip-Through-Schreiben in BRR/BSRR erfordert 6 Befehle bei moderater Optimierung und 3 Befehle bei höchster Optimierungsstufe; Ein Durchblättern von RMW'ng dauert 10 Anweisungen / 6 Anweisungen.

Loop-Overhead extra.

Durch den Wechsel |=zu =einem einzelnen Bit verbraucht die Set/Reset-Phase 9 Taktzyklen ( Link ). Der Assemblercode ist 3 Anweisungen lang:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
Rollen Sie Schleifen nicht manuell ab. Das ist praktisch nie eine gute Idee. In diesem speziellen Fall ist es besonders verheerend: Es macht die Wellenform nicht periodisch. Außerdem ist es nicht unbedingt schneller, den gleichen Code viele Male im Flash zu haben. Dies trifft hier vielleicht nicht zu (es könnte sein!), aber viele Leute denken, dass das Aufrollen von Schleifen hilfreich ist, dass Compiler ( gcc -funroll-loops) sehr gut können und dass, wenn es (wie hier) missbraucht wird, den gegenteiligen Effekt von dem hat, was Sie wollen.
Eine Endlosschleife kann niemals effektiv entrollt werden, um ein konsistentes Zeitverhalten aufrechtzuerhalten.
@MarcusMüller: Endlosschleifen können manchmal sinnvoll entrollt werden, während ein konsistentes Timing beibehalten wird, wenn es in einigen Wiederholungen der Schleife Punkte gibt, an denen eine Anweisung keine sichtbare Wirkung hätte. Wenn beispielsweise somePortLatchein Port gesteuert wird, dessen untere 4 Bits für die Ausgabe gesetzt sind, kann es möglich sein, sich while(1) { SomePortLatch ^= (ctr++); }in Code zu entrollen, der 15 Werte ausgibt und dann in einer Schleife zurückkehrt, um zu dem Zeitpunkt zu beginnen, an dem er andernfalls zweimal hintereinander denselben Wert ausgeben würde.
Superkatze, stimmt. Auch Effekte wie das Timing der Speicherschnittstelle usw. können es sinnvoll machen, "teilweise" abzurollen. Meine Aussage war zu allgemein, aber ich finde, Dannys Rat ist noch allgemeiner und sogar gefährlich