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:
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:
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?
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 -g
hinzugefü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 objdump
Binä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 -S
zur 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
ldr
(Load Register) das Register r2
mit dem Wert am Speicherplatz gespeichert in r3
+ 24 Bytes. Wenn Sie zu faul sind, das nachzuschlagen: Sehr wahrscheinlich ist der Standort von BSRR
.OR
das r2
Register mit der Konstante 1024 == (1<<10)
, was dem Setzen des 10. Bits in diesem Register entsprechen würde, und das Ergebnis in r2
sich selbst schreiben.str
(speichern) Sie das Ergebnis in dem Speicherplatz, aus dem wir im ersten Schritt gelesen habenBRR
die Adresse von .b
(Verzweigung) zurück zum ersten Schritt.Wir haben also 7 Anweisungen, nicht drei, um damit zu beginnen. Nur das b
passiert 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 b
1, 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:
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 stimmt alles mit deiner Beobachtung überein:
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 -O3
wie -O1
mit meiner Compiler-Version. Das musst du selbst machen! Vielleicht verwenden Sie eine alte Version von GCC mit suboptimaler ARM-Unterstützung.
=
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?-O3
Fehler 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
uxth
ist da, weil GPIO->BSRRL
es (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- BSRR
Register. @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.LDRB
und STRB
die Byte-Lese-/Schreibvorgänge in einer einzigen Anweisung ausführen, oder?LDRB
und LDRH
die 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_10
dies so definiert ist. UXTB
oder UXTH
setzt die nicht geladenen Bits auf 0.BSRR
anstelle von BSRRL
ist die Korrektur, die ich vornehmen musste, um sie auf meinem Computer zu erstellen (daher habe ich den vollständigen Code gepostet).LDRB
erstreckt sich Null auf 32 Bit. Reden wir über verschiedene Varianten der ARM-Architektur oder so etwas?LDRB
Anweisung existiert, bedeutet nicht, dass der Header so geschrieben ist, dass er verwendet werden kann, ist meine Vermutung hier.Die Register BSRR
und BRR
dienen 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
, STR
Anweisungen 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.
Sie sollten in BRR/BSRR-Register schreiben, nicht lesen-modifizieren-schreiben, wie Sie es jetzt tun.
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.
|=
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
gcc -funroll-loops
) sehr gut können und dass, wenn es (wie hier) missbraucht wird, den gegenteiligen Effekt von dem hat, was Sie wollen.somePortLatch
ein 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.
Marko Buršič
Jonas Schäfer
Scott Seidman
0___________