Forum: Mikrocontroller und Digitale Elektronik "Geister"-Takte am LPC1114?


von Markus H. (traumflug)


Lesenswert?

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:
1
  SET_OUTPUT(PIO0_1);
2
  while (1) {
3
    WRITE(PIO0_1, 0);
4
    WRITE(PIO0_1, 1);
5
  }

Nach dem Precompiler für ARM:
1
  do { LPC_IOCON_TypeDef *ioreg = (LPC_IOCON_TypeDef *)((0x40000000UL) + 0x44000); LPC_GPIO_TypeDef *port = (LPC_GPIO_TypeDef *)((0x50000000UL) + 0x00000); port->DIR |= (1 << 1); } while (0);
2
  while (1) {
3
    do { LPC_GPIO_TypeDef *port = (LPC_GPIO_TypeDef *)((0x50000000UL) + 0x00000); if (0) { port->MASKED_ACCESS[1 + 1] = (1 << 1); } else { port->MASKED_ACCESS[1 + 1] = 0; } } while (0);
4
    do { LPC_GPIO_TypeDef *port = (LPC_GPIO_TypeDef *)((0x50000000UL) + 0x00000); if (1) { port->MASKED_ACCESS[1 + 1] = (1 << 1); } else { port->MASKED_ACCESS[1 + 1] = 0; } } while (0);
5
  }

Und im Assembler dann so:
1
 440:  23a0        movs  r3, #160  ; 0xa0
2
 442:  2180        movs  r1, #128  ; 0x80
3
 444:  2002        movs  r0, #2
4
 446:  05db        lsls  r3, r3, #23
5
 448:  0209        lsls  r1, r1, #8
6
 44a:  585a        ldr  r2, [r3, r1]
7
 44c:  4302        orrs  r2, r0
8
 44e:  505a        str  r2, [r3, r1]
9
10
 450:  2200        movs  r2, #0
11
 452:  609a        str  r2, [r3, #8]
12
 454:  3202        adds  r2, #2
13
 456:  609a        str  r2, [r3, #8]
14
 458:  e7fa        b.n  450 <main+0x178>

... 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:
1
  SET_OUTPUT(PIO0_1);
2
  *(volatile uint32_t *)(LPC_IOCON_BASE + 0x10) = *(volatile uint32_t *)(LPC_IOCON_BASE + 0x10);
3
  while (1) {
4
    WRITE(PIO0_1, 0);
5
    WRITE(PIO0_1, 1);
6
  }

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.

: Bearbeitet durch User
von Programmierer (Gast)


Lesenswert?

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.

von Markus H. (traumflug)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

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.

von Jim M. (turboj)


Lesenswert?

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.

von Markus H. (traumflug)


Lesenswert?

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!

von Gerd E. (robberknight)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

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.

von Markus H. (traumflug)


Lesenswert?

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.

von Markus H. (traumflug)


Lesenswert?

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 :-)

von Markus H. (traumflug)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.