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:
- HSI-Takt (8 MHz) ist eingeschaltet;
- PLL wird mit dem mit dem Vorteiler von 16 initiiert, um HSI / 2 * 16 = 64 MHz zu erreichen;
- PLL wird als SYSCLK bezeichnet.
- 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:
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?
Antworten:
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
-g
Flag, das an derCFLAGS
gleichen Stelle hinzugefügt wurde (dieselbe Stelle, an der Sie auch angeben-O1
). Schauen Sie sich dann die generierte Baugruppe an:Beachten Sie, dass natürlich sowohl der Name der
objdump
Binä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
-S
die 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 :
und bekam folgendes (Auszug, vollständiger Code unter Link oben):
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
ldr
(Laderegister) das Registerr2
mit dem Wert am Speicherort inr3
+ 24 Bytes. Zu faul sein, um das nachzuschlagen: sehr wahrscheinlich der Ort vonBSRR
.OR
dasr2
Register mit der Konstante1024 == (1<<10)
, die dem Setzen des 10. Bits in diesem Register entsprechen würde, und das Ergebnis in sichr2
selbst schreiben .str
(Speicher) das Ergebnis in der Speicherstelle wir im ersten Schritt gelesen habenBRR
die Adresse.b
(Verzweigen) zurück zum ersten Schritt.Wir haben also zunächst 7 Anweisungen, nicht drei. Das
b
passiert 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 dasb
1, 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 Zyklusstr
dauert 2 Zyklenb
dauert 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:
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
-O3
wie-O1
bei meiner Compiler-Version. Das musst du selbst machen! Möglicherweise verwenden Sie eine alte Version von GCC mit suboptimaler ARM-Unterstützung.quelle
=
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?-O3
Fehler 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
uxth
ist da, weilGPIO->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-BSRR
Register. @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.LDRB
undSTRB
die Byte-Lese- / Schreibvorgänge in einem einzigen Befehl ausführen, nicht wahr?Die Register
BSRR
undBRR
dienen zum Setzen und Zurücksetzen einzelner Portbits:Wie Sie sehen können, ergibt das Lesen dieser Register immer 0, also Ihren Code
tut effektiv ist
GPIOE->BRR = 0 | GPIO_BRR_BR_10
, aber der Optimierer nicht weiß , dass, so dass er eine Folge von erzeugtLDR
,ORR
,STR
Befehle anstelle eines einzigen Speicher.Sie können den teuren Lese-, Änderungs- und Schreibvorgang vermeiden, indem Sie einfach schreiben
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 diewhile(1)
Schleife zu stellen.quelle
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:
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.
quelle
Es ist ein Verhalten Ihres Codes.
Sie sollten in BRR / BSRR-Register schreiben und nicht wie jetzt lesen, ändern, schreiben.
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.
quelle
|=
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
gcc -funroll-loops
) sehr gut können und das bei Missbrauch (wie hier) den umgekehrten Effekt dessen hat, was Sie wollen.somePortLatch
ein Port gesteuert wird, dessen untere 4 Bits für die Ausgabe festgelegt sind, kann es möglich sein, sichwhile(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.