Derzeit bin ich dabei, das von den AVRs bekannte FastIO auf ARM zu
portieren. Genauer, erst mal auf einen LPC1114, also ein Cortex-M0 mit
48 MHz. FastIO ist eine Reihe Makros, die einzelne I/O Pins schalten und
die dafür benötigten Werte zur Kompilierzeit berechnen, so dass man zur
Laufzeit mit nur noch einem Load und einem Store auskommt. Der ATmega
schafft es entsprechend, Pulse von nur 2 CPU Clocks Dauer zu erzeugen.
Code für AVR und ARM sieht dann so aus:
... wenn man Glück hat. Denn der Optimierer bring recht unterschiedliche
Ergebnisse, je nachdem, wie der Code genau aussieht. Oft wird das laden
von r3 (Zieladresse) nicht aus der Schleife heraus gezogen, wodurch die
natürlich langsamer wird.
So wie angegeben läuft diese Schleife mit 9 CPU-Zyklen ( = 5,3 MHz), der
negative Puls hat 3 Zyklen ( = 63 ns). Auf dem 'Skop gemessen.
Jetzt kommt's: spreche ich vor der Schleife noch das zum Pin gehörige
IOCON-Register an, wird diese Schleife lansamer, sie braucht dann 13
Clocks. Die Pulsbreite bleibt gleich. Ansprechen des Registers genügt,
z.B. indem man den vom booten schon vorhandenen Wert zurück schreibt:
Der generierte Assembler der Schleife ist bis auf's Byte genau gleich
und die 9 Takte passen auch gut zu den Laufzeitangaben der einzelnen
Befehle im User Manual, daher kann ich mir diese Verzögerung um 4 Takte
nicht erklären. Hat jemand eine Idee, wo diese Extratakte her kommen
könnten?
P.S.: wer diese Frage zu akademisch findet: bitte cool bleiben.
Markus H. schrieb:> daher kann ich mir diese Verzögerung um 4 Takte> nicht erklären. Hat jemand eine Idee, wo diese Extratakte her kommen> könnten?
Der Zustand der Pipeline des Prozessors beeinflusst die Dauern von
Instruktionen. STR, LDR, Branch dauern nicht immer gleich lange. Hat der
Prozessor außerdem einen Cache für den Flash, kann die Anordnung des
Codes die Laufzeit beeinflussen. Wie immer der Hinweis: Zur taktgenauen
Ansteuerung von Pins verwendet man Timer.
Programmierer schrieb:> Der Zustand der Pipeline des Prozessors beeinflusst die Dauern von> Instruktionen. STR, LDR, Branch dauern nicht immer gleich lange. Hat der> Prozessor außerdem einen Cache für den Flash, kann die Anordnung des> Codes die Laufzeit beeinflussen.
Von einem Cache weiss das User Manual nichts und die Pipeline müsste bei
den gleichen Befehlen eigentlich immer gleich im Kreis laufen. Da sind
ja keine Verzweigungen oder sowas.
> Wie immer der Hinweis: Zur taktgenauen> Ansteuerung von Pins verwendet man Timer.
Das ist im Moment gar nicht das Ziel. Ich versuche erst mal, überhaupt
nachvollziehbare Ergebnisse zu bekommen. Bislang gelingt das nur
bedingt, nicht nur aus Assembler-Ebene.
Markus H. schrieb:> Derzeit bin ich dabei, das von den AVRs bekannte FastIO auf ARM zu> portieren.
Ja. Ich finde, genau das ist ein echter Fehler, denn die
zugrundeliegende Hardware ist zu unterschiedlich.
Natürlich kannst du sowas portieren und es wird auch irgendwie
funktionieren, aber mit solchen Vereinheitlichungen und deren
vermeintlichen Verbesserungen deines künftigen Codes machst du genau
eines: du trittst dem Compiler frontal ins Gesicht.
Zunächst erst mal eines: Die Port-Hardware der Controller der ARM-Riege
ist deutlich anders als die der AVR's. Zusätzlich gibt es noch
erhebliche Unterschiede zwischen den verschiedenen ARM-Genrationen,
Familien und Herstellern. Sehr häufig findet man einen Satz aus
WriteOnly-Registern zum gezielten Setzen und Rücksetzen von Portpins.
Diese hast du mit deinem Ansatz quasi ausgekickt und nun wunderst du
dich darüber, daß der Compiler dein Begehren auf andere Weise zu
realisieren versucht.
Und nun noch etwas anderes: Sehr häufig findet man im vom Compiler
erzeugten Code etwas, das man dort überhaupt nicht vermuten würde und
sucht vergeblich nach dem, was man eigentlich erwartet. Das ist bei den
heutigen ARM-Compilern ganz normal, denn die Methoden zum Optimieren des
Codes sind inzwischen so heftig geworden, daß man als
Normalprogrammierer erstmal wie das berühmte Schwein ins Uhrwerk schaut.
Wichtig zu wissen ist, daß die ARM's einen vereinheitlichten Adreßraum
für alles haben - und der ist deutlich größer als der Adresßraum, in
welchem sich beim AVR die Ports befinden. Deshalb ist es bei allen ARM's
nötig, die betreffenden Adressen für das Erreichen diverser
Port-Register irgendwann mal in CPU-Registern zu haben - in den OpCode
passen sie regelmäßig nicht hinein. Das erzeugt nen Overhead, der sich
aber vom Compiler gut wegoptimieren läßt, sobald selbiger die
tatsächlichen Adressen der beteiligten Peripherie im "Scope" hat UND
sie so nahe beieinander liegen, daß sie mit einem CPU-Register plus im
OpCode darstellbaren Offset erreichbar sind. Aber das alles geht flöten,
wenn du mit deinen "WRITE(pin, value);" Funktionen daherkommst. Deshalb
brauchst du dich nicht zu wundern, wenn am Ende uneffizienter Code
herauskommt.
Mein Rat: verzichte auf solche unnützen Funktionen und formuliere deine
Dinge anders. Versuche NIEMALS, zuerst irgendwas in einer ganz
allgemeinen Form zu formulieren und dann von dort aus wieder speziell zu
werden, sondern tu es ganz dediziert.
Also nicht WRITE(FIO3.7, 1);
Sondern FIO3SET = (1<<7);
oder noch besser:
_inline void LampeEin (void)
{ FIO3SET = (1<<7); }
_inline void LampeAus (void)
{ FIO3CLR = (1<<7); }
Das ist wesentlich lesbarer als dein WRITE(....
Ansonsten lies dich ein in das Thema Bitbanding (Alternativzugriff auf
Bits in Peripherieregistern), was bei neueren ARM's durchaus Mode ist.
W.S.
Markus H. schrieb:> Von einem Cache weiss das User Manual nichts und die Pipeline müsste bei> den gleichen Befehlen eigentlich immer gleich im Kreis laufen.
Die Cortex M Kerne haben eine Prefetch Einheit, die IIRC immer mit auf 4
Byte ausgerichteten Addressen arbeitet. Deine Schleife mit 9 Takten ist
zufällig korrekt ausgerichtet (siehe Addressangaben im Disassembler).
Die Schleife mit 13 Takten ist eventuell nicht korrekt ausgerichtet und
braucht dadurch mehr Takte zum Einlesen der Instruktionen. Schau Dir mal
die entsprechenden Addressangaben an.
Immerhin hat der wie ein Schwein drein schauende und dem Compiler ins
Gesicht tretende Normalprobrammierer es geschafft, die 72 Takte
brauchende Routine aus MBED auf 3 Takte zu verkürzen.
W.S. schrieb:> Sondern FIO3SET = (1<<7);
Wenn Du mal einen Blick auf das Vorkompilat geworfen hättest, würdest Du
sehen, dass genau das passiert. Anfänger!
W.S. schrieb:> Zusätzlich gibt es noch> erhebliche Unterschiede zwischen den verschiedenen ARM-Genrationen,> Familien und Herstellern.
das stimmt:
> Ansonsten lies dich ein in das Thema Bitbanding (Alternativzugriff auf> Bits in Peripherieregistern), was bei neueren ARM's durchaus Mode ist.
Bitbanding gibts IMO bei den LPC1114 nicht, da die einen Cortex-M0
haben. Der kann das nicht.
Dafür gibts bei Cortex M0+ (die sind etwas neuer als der M0) wiederum
die IOPORT-Anbindung, mit der kriegt man zumindest lt. Datenblatt
Single-Cycle-Zugriffe auf die GPIO-Ports hin. Die LPC11U6 von NXP sind
M0+, auch die STM32L0 und einige Kinetis.
Markus H. schrieb:> Wenn Du mal einen Blick auf das Vorkompilat geworfen hättest, würdest Du> sehen, dass genau das passiert. Anfänger!
Na, wenn einem derartige Freundlichkeiten widerfahren, dann wird man
doch gleich viel hilfsbereiter. Gelle?
W.S.
Jim M. schrieb:> Die Cortex M Kerne haben eine Prefetch Einheit, die IIRC immer mit auf 4> Byte ausgerichteten Addressen arbeitet. Deine Schleife mit 9 Takten ist> zufällig korrekt ausgerichtet (siehe Addressangaben im Disassembler).>> Die Schleife mit 13 Takten ist eventuell nicht korrekt ausgerichtet und> braucht dadurch mehr Takte zum Einlesen der Instruktionen. Schau Dir mal> die entsprechenden Addressangaben an.
Das ist mal ein guter Hinweis. Vielen Dank.
Gerd E. schrieb:> Bitbanding gibts IMO bei den LPC1114 nicht, da die einen Cortex-M0> haben.
Jein. Ich weiss nicht genau, ob das unter dem Begriff "Bitbanding"
läuft, doch jeder Port hat 12 Bits und diesen 12 Bits sind 2^12 = 4096
Adressen zugeordnet. Die erste Adresse hat die Bitmaske 0x0000, die
zweite 0x0001, die dritte 0x0002, die vierte 0x0003 und so weiter.
Schreibt man also an die richtige Adresse, bekommt man die zum Pin (oder
zu mehreren Pins) passende Bitmaske gleich mitgeliefert und kommt mit
der Schreiboperation alleine, ohne vorheriges lesen, aus.
Das ist im User Manual nur vage erwähnt, doch mit viel googeln bin ich
dann dahinter gekommen, was das bedeutet, es ist auch hier beschrieben:
https://www.mikrocontroller.net/articles/ARM_Bitbanding
Schade, dass man Ports mit 12 Bits gewählt hat, die oberen 4 Bit
brauchen einen etwas teureren 16-Bit-Schreibbefehl. Ändere ich das obige
Beispiel von PIO0_1 zu PIO0_10 wird das Binary 4 Byte grösser.
W.S. schrieb:> Na, wenn einem derartige Freundlichkeiten widerfahren, dann wird man> doch gleich viel hilfsbereiter. Gelle?
In der Tat. Wer mir ins Gesicht faucht, dem fauche ich zurück :-)
Jim M. schrieb:> Die Cortex M Kerne haben eine Prefetch Einheit, die IIRC immer mit auf 4> Byte ausgerichteten Addressen arbeitet. Deine Schleife mit 9 Takten ist> zufällig korrekt ausgerichtet (siehe Addressangaben im Disassembler).
Bingo!
Es scheinen nicht vier Bytes, sondern 4 Wörter, also 16 Bytes zu sein.
Habe mit NOPs zwischen dem Register schreiben und der Schleife
aufgefüllt und immer wieder probiert, bei 5 NOPs war die Schleife dann
wieder schnell, die Anfangsadresse der Schleife 0x460.
Jetzt bin ich erleichtert, besten Dank. Es liegt also nicht an einem
zer-konfigurierten GPIO-Port oder an einem Mysterium, sondern an der
Code-Ausrichtung.
So auf die Schnelle gegoogelt gibt es eine ganze Reihe Massnahmen, damit
umzugehen:
-falign-labels=16 hat zwar das ganze Binary um 10% aufgeblasen, die
Schleife ist allerdings schnell.
-falign-loops=16 hat keine Veränderung gebracht.
Ein
1
__ASM(".balign 16");
direkt vor der Schleife hat's auch gebracht und nur 16 Bytes gekostet.