Schnelle Leistung von einer STM32-MCU

11

Ich arbeite mit dem STM32F303VC Discovery Kit und bin etwas verwirrt über dessen 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 kann wie folgt unterteilt werden:

  1. HSI-Takt (8 MHz) ist eingeschaltet;
  2. PLL wird mit dem mit dem Vorteiler 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 ständig in der Endlosschleife 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 der -O1-Optimierung kompiliert. Die mit einem Oszilloskop untersuchten Signale an den Pins PA8 (MCO) und PE10 sehen folgendermaßen aus: Geben Sie hier die Bildbeschreibung ein

Der SYSCLK scheint korrekt konfiguriert zu sein, da der MCO (orange Kurve) eine Schwingung von fast 64 MHz aufweist (unter Berücksichtigung der Fehlergrenze des internen Takts). Der seltsame Teil für mich ist das Verhalten auf PE10 (blaue Kurve). In der Endlosschleife while (1) dauert es 4 + 4 + 5 = 13 Taktzyklen, um eine elementare 3-Schritt-Operation durchzuführen (dh Bit-Set / Bit-Reset / Return). Bei anderen Optimierungsstufen (z. B. -O2, -O3, ar -Os) wird es noch schlimmer: Dem LOW-Teil des Signals werden mehrere zusätzliche Taktzyklen hinzugefügt, dh zwischen den fallenden und steigenden Flanken von PE10 (was den LSI irgendwie zu ermöglichen scheint um diese Situation zu beheben).

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

KR
quelle
Haben Sie versucht, mit einer anderen MCU zu vergleichen?
Marko Buršič
3
Was versuchst du zu erreichen? Wenn Sie einen schnell oszillierenden Ausgang wünschen, sollten Sie Timer verwenden. Wenn Sie mit schnellen seriellen Protokollen kommunizieren möchten, sollten Sie die entsprechende Hardware-Peripherie verwenden.
Jonas Schäfer
2
Toller Start mit dem Kit !!
Scott Seidman
Sie dürfen keine | = BSRR- oder BRR-Register verwenden, da diese nur geschrieben werden.
P__J__

Antworten:

25

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 im Reverse Engineering gewesen (im Grunde genommen etwas, das mit :) beginnt radare2 -A arm image.bin; aaa; VV, aber Sie haben den Code, damit dies alles einfacher wird.

Kompilieren Sie es zunächst mit dem -gFlag, das an der CFLAGSgleichen Stelle hinzugefügt wurde (dieselbe Stelle, an der Sie auch angeben -O1). Schauen Sie sich dann die generierte Baugruppe an:

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

Beachten Sie, dass natürlich sowohl der Name der objdumpBinärdatei als auch Ihre ELF-Zwischendatei unterschiedlich sein können.

Normalerweise können Sie auch einfach den Teil überspringen, in dem GCC den Assembler aufruft, und sich die Assembly-Datei ansehen. Fügen Sie einfach -Sdie GCC-Befehlszeile hinzu - dies führt jedoch normalerweise zu einer Unterbrechung Ihres Builds, sodass Sie dies höchstwahrscheinlich außerhalb Ihrer IDE tun würden .

Ich habe eine leicht gepatchte Version Ihres Codes zusammengestellt :

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

Welches ist eine Schleife (beachten Sie den bedingungslosen Sprung zu .L5 am Ende und das .L5-Label am Anfang).

Was wir hier sehen ist, dass wir

  • Zuerst ldr(Laderegister) das Register r2mit dem Wert am Speicherort in r3+ 24 Bytes. Zu faul sein, um das nachzuschlagen: sehr wahrscheinlich der Ort von BSRR.
  • Dann ORdas r2Register mit der Konstante 1024 == (1<<10), die dem Setzen des 10. Bits in diesem Register entsprechen würde, und das Ergebnis in sich r2selbst schreiben .
  • Dann str(Speicher) das Ergebnis in der Speicherstelle wir im ersten Schritt gelesen haben
  • und wiederholen Sie das gleiche für einen anderen Speicherort, aus Faulheit: höchstwahrscheinlich BRRdie Adresse.
  • Zum Schluss b(Verzweigen) zurück zum ersten Schritt.

Wir haben also zunächst 7 Anweisungen, nicht drei. Das bpassiert nur einmal und ist daher sehr wahrscheinlich, was eine ungerade Anzahl von Zyklen benötigt (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 einen Befehl nicht in weniger als einem Zyklus ausführen ), wissen wir dass das b1, 3, 5 oder 7 CPU-Zyklen dauert.

Als wer wir sind, habe ich mir die ARM-Dokumentation mit Anweisungen angesehen und wie viele Zyklen sie für den M3 benötigen:

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

Das alles stimmt mit Ihrer Beobachtung überein:

13=2(cldr+cÖrr+cstr)+cb=2(2+1+2)+3=25+3

Wie die obige Berechnung zeigt, gibt es kaum eine Möglichkeit, Ihre Schleife schneller zu machen - die Ausgangspins von ARM-Prozessoren sind normalerweise speicherabgebildet , nicht CPU-Kernregister. Sie müssen also die übliche Routine zum Laden, Ändern und Speichern durchlaufen, wenn du willst irgendetwas damit machen.

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

Beachten Sie, dass Sie möglicherweise 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 in 8-Bit-Blöcken zu schreiben. Tu es nicht. ARM ist eine 32-Bit-Architektur, und das Extrahieren von 8 Bit eines 32-Bit-Wortes erfordert möglicherweise zusätzliche Anweisungen. Wenn Sie können, lesen Sie einfach das gesamte 32-Bit-Wort, ändern Sie das, was Sie benötigen, und schreiben Sie es als Ganzes zurück. Ob dies möglich ist, hängt natürlich davon ab, woran Sie schreiben, dh vom Layout und der Funktionalität Ihres GPIO mit Speicherzuordnung. Informationen dazu, was im 32-Bit-Bit gespeichert ist und das Bit enthält, das Sie umschalten möchten, finden Sie im STM32F3-Datenblatt / Benutzerhandbuch.


Jetzt habe ich versucht, Ihr Problem zu reproduzieren, wobei die "niedrige" Periode länger wurde, aber ich konnte es einfach nicht - die Schleife sieht genauso aus -O3wie -O1bei meiner Compiler-Version. Das musst du selbst machen! Möglicherweise verwenden Sie eine alte Version von GCC mit suboptimaler ARM-Unterstützung.

Marcus Müller
quelle
4
Wäre das Speichern ( =statt |=) nicht, 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, Ändern, Schreiben erforderlich ist. In diesem Fall könnten die Konstanten in Registern außerhalb der Schleife gespeichert werden, so dass die innere Schleife nur 2 Strs und eine Verzweigung wäre, also 2 + 2 + 3 = 7 Zyklen für die gesamte Runde?
Timo
Vielen Dank. Das hat die Dinge wirklich ziemlich geklärt. Es war ein bisschen voreilig, darauf zu bestehen, dass nur 3 Taktzyklen benötigt würden - 6 bis 7 Zyklen waren etwas, auf das ich eigentlich gehofft hatte. Der -O3Fehler scheint nach dem Reinigen und Wiederherstellen 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
KR
1
uxthist da, weil GPIO->BSRRL(fälschlicherweise) in Ihren Headern ein 16-Bit-Register definiert ist. Verwenden Sie eine aktuelle 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, so dass sein Code vollständige 32-Bit-Zugriffe ausführt, anstatt ein Halbwort zu laden und es zu erweitern.
Berendi - Protest
Warum sollte das Laden eines einzelnen Bytes zusätzliche Anweisungen erfordern? Die ARM-Architektur hat LDRBund STRBdie Byte-Lese- / Schreibvorgänge in einem einzigen Befehl ausführen, nicht wahr?
Psmears
1
Der M3-Kern kann Bitbanding unterstützen (nicht sicher, ob diese spezielle Implementierung dies tut), wobei ein 1-MB-Bereich des peripheren Speicherplatzes auf einen 32-MB-Bereich ausgerichtet ist. Jedes Bit hat eine diskrete Wortadresse (nur Bit 0 wird verwendet). Vermutlich immer noch langsamer als nur ein Laden.
Sean Houlihane
8

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

Register zum Setzen / Zurücksetzen des GPIO-Portbits (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 das entsprechende ODRx-Bit

1: Setzt das entsprechende ODRx-Bit

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

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

tut effektiv ist GPIOE->BRR = 0 | GPIO_BRR_BR_10, aber der Optimierer nicht weiß , dass, so dass er eine Folge von erzeugt LDR, ORR, STRBefehle anstelle eines einzigen Speicher.

Sie können den teuren Lese-, Änderungs- und Schreibvorgang vermeiden, indem Sie einfach schreiben

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

Sie können eine weitere Verbesserung erzielen, indem Sie die Schleife an einer Adresse ausrichten, die gleichmäßig durch 8 teilbar ist. Versuchen Sie, eine oder einen Modus zu setzen asm("nop"); vor die while(1)Schleife zu stellen.

berendi - protestieren
quelle
1

Um das hier Gesagte zu ergänzen: Mit dem Cortex-M, aber so ziemlich jedem Prozessor (mit einer Pipeline, einem Cache, einer 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. Fügen Sie nur einige dieser Nops in der Mitte hinzu, wenn Sie möchten. es spielt keine Rolle.

Das Ändern der Ausrichtung der Schleife kann die Leistung erheblich verändern, insbesondere bei einer kleinen Schleife wie dieser, wenn zwei Abrufzeilen anstelle einer benötigt werden, entstehen 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 noch schlechter 3 oder 4 oder 5 als durch Hinzufügen von zusätzlichem Abrufen.

Sie haben wahrscheinlich keinen Cache, aber wenn Sie das hatten, hilft es in einigen Fällen, aber in anderen tut es weh und / oder macht keinen Unterschied. Eine Verzweigungsvorhersage, die Sie hier haben können oder nicht (wahrscheinlich nicht), kann nur so weit sehen, wie sie in der Pipe vorgesehen ist. Selbst wenn Sie die Schleife in Verzweigung geändert haben und am Ende eine bedingungslose Verzweigung hatten (einfacher für eine Verzweigungsvorhersage) Verwenden Sie) alles, was Sie tun müssen, ist, dass Sie beim nächsten Abruf so viele Uhren (Größe der Pipe, von der normalerweise abgerufen wird, bis zu der Tiefe, die der Prädiktor sehen kann) sparen und / oder für alle Fälle keinen Vorabruf durchführen.

Durch Ändern der Ausrichtung in Bezug auf Abruf- und Cache-Zeilen können Sie beeinflussen, ob der Verzweigungsprädiktor Ihnen hilft oder nicht. Dies zeigt sich in der Gesamtleistung, selbst wenn Sie nur zwei Anweisungen oder diese beiden mit einigen Nops testen .

Es ist etwas trivial, dies zu tun, und wenn Sie dies verstanden haben und dann kompilierten Code oder sogar handgeschriebene Assemblierungen verwenden, können Sie feststellen, dass die Leistung aufgrund dieser Faktoren stark variieren kann, indem Sie einige bis ein paar hundert Prozent hinzufügen oder einsparen. eine Zeile C-Code, eine schlecht platzierte Nr.

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

Oldtimer
quelle
0

Wird dieses Verhalten von dieser MCU erwartet?

Es ist ein Verhalten Ihres Codes.

  1. Sie sollten in BRR / BSRR-Register schreiben und nicht wie jetzt lesen, ändern, schreiben.

  2. Sie verursachen auch Schleifen-Overhead. Um maximale Leistung zu erzielen, replizieren Sie die BRR / BSRR-Vorgänge immer wieder → Kopieren Sie sie und fügen Sie sie mehrmals in die Schleife ein, damit Sie viele Einstell- / Rücksetzzyklen durchlaufen, bevor eine Schleife überlastet wird.

bearbeiten: einige schnelle Tests unter IAR.

Ein Durchblättern des Schreibens in BRR / BSRR erfordert 6 Anweisungen bei mäßiger Optimierung und 3 Anweisungen bei höchster Optimierungsstufe. Ein Durchblättern von RMW'ng erfordert 10 Anweisungen / 6 Anweisungen.

Loop Overhead extra.

dannyf
quelle
Durch den Wechsel |=zu =einer Einzelbit-Set / Reset-Phase werden 9 Taktzyklen ( Link ) verbraucht . Der Montagecode ist 3 Anweisungen lang:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR
1
Rollen nicht manuell abrollen. Das ist praktisch nie eine gute Idee. In diesem speziellen Fall ist es besonders katastrophal: Es macht die Wellenform nicht periodisch. Außerdem ist es nicht unbedingt schneller, denselben Code viele Male in Flash zu haben. Dies trifft hier möglicherweise nicht zu (es könnte sein!), Aber das Abrollen von Schleifen ist etwas, das viele Leute für hilfreich halten, das Compiler ( gcc -funroll-loops) sehr gut können und das bei Missbrauch (wie hier) den umgekehrten Effekt dessen hat, was Sie wollen.
Marcus Müller
Eine Endlosschleife kann niemals effektiv abgewickelt werden , um ein konsistentes Timing-Verhalten aufrechtzuerhalten.
Marcus Müller
1
@ MarcusMüller: Endlosschleifen können manchmal sinnvoll abgewickelt werden, während ein konsistentes Timing beibehalten wird, wenn in einigen Wiederholungen der Schleife Punkte vorhanden sind, an denen eine Anweisung keine sichtbare Wirkung hätte. Wenn beispielsweise somePortLatchein Port gesteuert wird, dessen untere 4 Bits für die Ausgabe festgelegt sind, kann es möglich sein, sich while(1) { SomePortLatch ^= (ctr++); }in Code abzuwickeln, der 15 Werte ausgibt, und dann zu dem Zeitpunkt zurückzukehren, an dem derselbe Wert ansonsten zweimal hintereinander ausgegeben würde.
Supercat
Supercat, stimmt. Außerdem können Effekte wie das Timing der Speicherschnittstelle usw. das "teilweise" Abrollen sinnvoll machen. Meine Aussage war zu allgemein, aber ich denke, Dannys Rat ist noch allgemeiner und sogar gefährlich
Marcus Müller