Forum: Compiler & IDEs XMega-Assembler: Welche Register darf ich verwenden?


von Christoph M. (chrito)


Lesenswert?

Hallo Forum,

habe einen C-Schnipsel in ASM umgewandelt, um die Ausführungszeit zu 
frisieren. Der Schnipsel prüft, ob neue Daten im SPI-Puffer sind 
(SPIC.IF gesetzt). Falls ja oder falls ein Timeout (R19:R18 von 0xFFFF 
nach 0x0000 durchzählen) geschieht, dann gehts weiter. In C sieht das so 
aus:
1
uint16_t Zaehler16Bit=0;
2
while ((!(SPI2_PORT.STATUS& 0b10000000))&&(--Zaehler16Bit));

Wenn ich jetzt Register R19 und R18 für den Timeout-Zähler verwende, 
überschreibe ich an anderer Stelle im Programm ungewollt Register :/
Wenn ich R21 und R20 nehme, dann geht alles.

Darf ich diese Register wirklich nutzen?
1
                  
2
asm("LDI R20, 0x00");   /* R18 auf 0xFF setzen, R18 als Zähler nutzen */  \
3
asm("LDI R21, 0x00");   /* R19 auf 0xFF setzen, R19 als Zähler nutzen */  \
4
asm("Anfang:");                              \
5
asm("LDS R24,0x08C2");  /* SPIC.Status nach R24 laden */          \
6
asm("TST R24");         /* Status.IF testen */                \
7
asm("BRLT Ende");       /* Zum Ende springen, falls Bit 7 gesetzt */    \
8
asm("INC R20");         /* R18 inkrementieren */              \
9
asm("BRNE Anfang");     /* falls nicht 0, dann Sprung */          \
10
asm("INC R21");         /* R18 dekrementieren */              \
11
asm("BRNE Anfang");     /* falls nicht 0, dann Sprung */          \
12
asm("Ende:");


Hintergrund: Nachdem neue Daten im SPI-Puffer sind, soll der Puffer so 
schnell wie möglich ausgelesen werden und für neue Daten frei werden, um 
den Datendurchsatz des SPI zu maximieren.

Danke für eure Tipps! :)
VG
Christoph

: Bearbeitet durch User
von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Ich vermute, dass du den GCC nimmst:

http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_reg_usage

Aber: ein derartiges Inline-Assembler-Gehacke (verzeih' mir bitten den
Ausdruck) ist überhaupt nicht sinnvoll.  Beim GCC benutzt man
sinnvollerweise dessen Constraint-System, um ihm eine maximale
Integration in die Optimierung zu ermöglichen.  Dann legst du die
Register auch gar nicht mehr selbst fest, die du benutzt, sondern lässt
sie vom Compiler zuordnen.

Weiterhin passen deine Kommentare nicht zum Text (Kommentar behauptet,
irgendein Register wird mit 0xFF geladen, dabei wird ein ganz anderes
mit 0 geladen), und du benutzt absolute Zahlen, wo man besser Namen
übergibt.

: Bearbeitet durch Moderator
von Falk B. (falk)


Lesenswert?

@ Christoph M. (chrito)

>habe einen C-Schnipsel in ASM umgewandelt, um die Ausführungszeit zu
>frisieren.

Und du glaubst, daß du damit großartig schneller bist?

>nach 0x0000 durchzählen) geschieht, dann gehts weiter. In C sieht das so
>aus:
>Zaehler16Bit=0;
>while ((!(SPI2_PORT.STATUS& 0b10000000))&&(--Zaehler16Bit));

Hast du dir den entstandenen ASM-Code angeschaut? War der so schlecht 
und langsam?

>Wenn ich R21 und R20 nehme, dann geht alles.

>Darf ich diese Register wirklich nutzen?

Nein. Inlineassembler hat seine besonderen Regeln und 
Kommunikationsmittel zum Compiler. Wenn du denn WIRKLICH eine Routine in 
ASM beschleunigen willst, mach eine reine ASM-Funktion draus. Das ist 
deutlich einfacher zu schreiben und man muss nicht so viele Randbedingen 
beachten. Es gibt eine gute Application Note von Atmel dazu.

Atmel AT1886: Mixing Assembly and C with AVRGCC - Microchip

http://ww1.microchip.com/downloads/en/appnotes/doc42055.pdf

>Hintergrund: Nachdem neue Daten im SPI-Puffer sind, soll der Puffer so
>schnell wie möglich ausgelesen werden und für neue Daten frei werden, um
>den Datendurchsatz des SPI zu maximieren.

Dazu braucht man kein ASM, dazu reicht C.

Der Trick liegt darin, daß man während der SPI-Übertragung schon die 
nächsten Daten holt und in einer Variable zwischenspeichert und dann 
erst prüft, ob SPI wieder neue Daten aufnehmen kann. Beim Lesen per SPI 
muss man so oder so erstmal die Daten aus dem Register auslesen und den 
neuen Zyklus starten. Aber auch hier kann man durch eine 
Zwischenvariable Zeit gewinnen.

von Oliver S. (oliverso)


Lesenswert?

Prinzipiell darfst du dem Compiler überhaupt niemals dazwischenpfuschen, 
ohne ihm das explizit mitzuteilen.

Wie sieht den der vom Compiler generierte Code (mit Optimierung) aus?

Und ansonsten sind die Kommnetare in deinem Code das klassische Beispiel 
dafür, wie es nicht sein sollte...

> asm("INC R20");      /* R18 inkrementieren */              \
> asm("INC R21");      /* R18 dekrementieren */              \

Oliver

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Falk B. schrieb:
> Hast du dir den entstandenen ASM-Code angeschaut? War der so schlecht
> und langsam?
1
.global foo
2
        .type   foo, @function
3
foo:
4
/* prologue: function */
5
/* frame size = 0 */
6
/* stack size = 0 */
7
.L__stack_usage = 0
8
        ldi r24,0
9
        ldi r25,0
10
.L3:
11
        lds r18,2242
12
        sbrc r18,7
13
        rjmp .L1
14
        sbiw r24,1
15
        brne .L3
16
.L1:
17
        ret
Dürfte kaum langsamer sein als seine Inline-Assembler-Variante. ;-)

von Christoph M. (chrito)


Lesenswert?

Danke für eure Antworten!

Der Compiler-Code von
1
uint16_t Zaehler16Bit=0;
2
while ((!(SPI2_PORT.STATUS& 0b10000000))&&(--Zaehler16Bit));
sieht so aus:
1
0000059A  STS 0x2176,R1    Store direct to data space 
2
0000059C  STS 0x2177,R1    Store direct to data space 
3
0000059E  LDI R30,0xC0    Load immediate 
4
0000059F  LDI R31,0x08    Load immediate 
5
000005A0  LDD R24,Z+2    Load indirect with displacement 
6
000005A1  TST R24    Test for Zero or Minus 
7
000005A2  BRLT PC+0x0C    Branch if less than, signed 
8
000005A3  LDS R24,0x2176    Load direct from data space 
9
000005A5  LDS R25,0x2177    Load direct from data space 
10
000005A7  SBIW R24,0x01    Subtract immediate from word 
11
000005A8  STS 0x2176,R24    Store direct to data space 
12
000005AA  STS 0x2177,R25    Store direct to data space 
13
000005AC  OR R24,R25    Logical OR 
14
000005AD  BRNE PC-0x0D    Branch if not equal

Und mit den richtigen Kommentaren hier noch einmal:
1
asm("LDI R20, 0x00");   /* R20 auf 0x00 setzen, R20 als Zähler nutzen */  \
2
asm("LDI R21, 0x00");   /* R21 auf 0x00 setzen, R21 als Zähler nutzen */  \
3
asm("Anfang:");                              \
4
asm("LDS R24,0x08C2");  /* SPIC.Status nach R24 laden */          \
5
asm("TST R24");         /* Status.IF testen */                \
6
asm("BRLT Ende");       /* Zum Ende springen, falls Bit 7 gesetzt */    \
7
asm("INC R20");         /* R20 inkrementieren */              \
8
asm("BRNE Anfang");     /* falls nicht 0, dann Sprung */          \
9
asm("INC R21");         /* R21 dekrementieren */              \
10
asm("BRNE Anfang");     /* falls nicht 0, dann Sprung */          \
11
asm("Ende:");

Würde schon behaupten, dass mein Code einige Takte schneller ist, da er 
den Zähler mit zwei Registern realisiert und nichts in data space 
wegspeichern muss.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Wie wäre es mit dem Lösen der Handbremse?  Schalt' doch einfach mal
die Optimierung ein.  Was dann rauskommt, habe ich oben gepostet, da
kann sich dein Assemblercode noch ein paar Takte dahinter verstecken.

von Christoph M. (chrito)


Lesenswert?

Falk B. schrieb:
> Der Trick liegt darin, daß man während der SPI-Übertragung schon die
> nächsten Daten holt und in einer Variable zwischenspeichert und dann
> erst prüft, ob SPI wieder neue Daten aufnehmen kann.

Die zu versendenden Daten liegen schon in einem Puffer bereit. Wenn ich 
SPIC.Data während der Übertragung schreibe, dann erzeuge ich doch 
Datenmüll!?
Am schnellsten muss es doch dann sein, direkt nach dem IF-Flag 
auszulesen und das neue Byte zu schreiben.

Mit Interrupt hatte ich es probiert, da ist die Reaktion auf IF durch 
den Overhead des IR langsamer. Ständig auf IF=1 prüfen und dann 
übertragen geht deutlich schneller.

von Falk B. (falk)


Lesenswert?

@Christoph M. (chrito)

>> Der Trick liegt darin, daß man während der SPI-Übertragung schon die
>> nächsten Daten holt und in einer Variable zwischenspeichert und dann
>> erst prüft, ob SPI wieder neue Daten aufnehmen kann.

>Die zu versendenden Daten liegen schon in einem Puffer bereit.

In einem Array. Das muss adressiert werden und die Daten aus dem RAM 
gelesen werden.

> Wenn ich
>SPIC.Data während der Übertragung schreibe, dann erzeuge ich doch
>Datenmüll!?

Davon war keine Sekunde die Rede!

>Am schnellsten muss es doch dann sein, direkt nach dem IF-Flag
>auszulesen und das neue Byte zu schreiben.

Fast.

>Mit Interrupt hatte ich es probiert, da ist die Reaktion auf IF durch
>den Overhead des IR langsamer.

Logisch, das ist auch Unsinn.

> Ständig auf IF=1 prüfen und dann
>übertragen geht deutlich schneller.

Die Erkenntnis ist nicht sonderlich neu ;-)

Zeigt mal vollständigen Code für deine Übertragung.

Beitrag "Re: Frage zu IR-Remote+LED-Strips an AVR"

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Christoph M. schrieb:
> Wenn ich SPIC.Data während der Übertragung schreibe, dann erzeuge ich
> doch Datenmüll!?

Ja, leider.  Das SPI der Xmegas hat keinen zusätzlichen Puffer
implementiert.

Du könntest allerdings eine USART im SPI-Modus benutzen, die hat sowas. 
:)

von Christoph M. (chrito)


Lesenswert?

Hallo Jörg,

mit O3 bekomme ich:
1
000007AC  STS 0x2176,R1    Store direct to data space 
2
000007AE  STS 0x2177,R1    Store direct to data space 
3
000007B0  RJMP PC+0x000C    Relative jump 
4
000007B1  LDS R24,0x2176    Load direct from data space 
5
000007B3  LDS R25,0x2177    Load direct from data space 
6
000007B5  SBIW R24,0x01    Subtract immediate from word 
7
000007B6  STS 0x2176,R24    Store direct to data space 
8
000007B8  STS 0x2177,R25    Store direct to data space 
9
000007BA  OR R24,R25    Logical OR 
10
000007BB  BREQ PC+0x05    Branch if equal 
11
000007BC  LDS R24,0x08C2    Load direct from data space 
12
000007BE  SBRS R24,7    Skip if bit in register set 
13
000007BF  RJMP PC-0x000E    Relative jump

Das ist ein Befehl kürzer als mit O1... und er verwendet weiterhin zum 
zählen data space.

von Falk B. (falk)


Lesenswert?

Vor allem, wozu meint man, bei SPI einen Timeout Zähler zu brauchen?
Ein SPI-Transfer als Master hat IMMER ein definiertes Ende!

von Falk B. (falk)


Lesenswert?

@ Christoph M. (chrito)

>mit O3 bekomme ich:

Glaub ich nicht. Denn bei Optimierung schreibt der Compiler die Daten 
ganz sicher nicht in den RAM zurück sondern rechnet mit einem Register!

>000007B1  LDS R24,0x2176    Load direct from data space
>000007B3  LDS R25,0x2177    Load direct from data space
>000007B5  SBIW R24,0x01    Subtract immediate from word
>000007B6  STS 0x2176,R24    Store direct to data space
>000007B8  STS 0x2177,R25    Store direct to data space

Das Rückschreiben hier ist verdächtig.

Ich glaub du hast nicht wirklich mit Optimierung übersetzt.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Christoph M. schrieb:
> und er verwendet weiterhin zum zählen data space.

Unsinniges "volatile" irgendwo?  Es gibt keinen Grund, warum der
Compiler das in den SRAM legen sollte.

Der Code, den ich oben gepostet habe, stammt vom Compiler, allerdings
mit -Os.  -O3 nimmt man nur, wenn man zu viel Flash hat, denn es
sagt dem Compiler: „Platz spielt keine Rolle, Hauptsache, es wird
schnell.“  Da werden dann bspw. massiv Schleifen entrollt.

Ansonsten: poste einfach compilierfähigen Code statt des Zweizeilers
oben.  Der Code, den ich für den Test compiliert hatte, war:
1
#include <avr/io.h>
2
3
#define SPI2_PORT SPIC
4
5
void foo(void){
6
uint16_t Zaehler16Bit=0;
7
while ((!(SPI2_PORT.STATUS& 0b10000000))&&(--Zaehler16Bit));
8
}

von Christoph M. (chrito)


Lesenswert?

Jörg W. schrieb:
> Christoph M. schrieb:
>> Wenn ich SPIC.Data während der Übertragung schreibe, dann erzeuge ich
>> doch Datenmüll!?
>
> Ja, leider.  Das SPI der Xmegas hat keinen zusätzlichen Puffer
> implementiert.
>
> Du könntest allerdings eine USART im SPI-Modus benutzen, die hat sowas.
> :)
Ok, dann bin ich doch noch nicht ganz verkalkt :)

Also dachte ich mir, ist die schnellste Lösung, direkt auf das kommende 
IF-Flag die Daten abzuholen und neue in SPIC.DATA hineinzuwerfen. Also 
ungefähr so:
1
while ((!(SPIC.STATUS& 0b10000000))&&(--Zaehler16Bit)); //warte
2
SPI_Empfangspuffer[Pufferindex] = SPIC.DATA;  // hole Byte  
3
SPIC.DATA = SPI_Sendepuffer[Pufferindex];     // sende Byte

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Ich denke, dass du das neue Byte sogar schon zum Senden anschieben
kannst, bevor du das vorige einliest.  Die Empfangsrichtung ist ja
wenigstens gepuffert, anders als die Senderichtung.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Aber Falks Frage bleibt natürlich: wofür soll der Timeout gut sein?
Der schlägt nur dann zu, wenn du keinen SPI-Takt im Master gestartet
hast, aber dann hast du einfach einen Bug im Code.  Sofern garantiert
ist, dass der Takt des Masters läuft, dann ist nach 8 SPI-Takten
ein Schiebevorgang zu Ende.

von Christoph M. (chrito)


Lesenswert?

Jörg W. schrieb:
> Aber Falks Frage bleibt natürlich: wofür soll der Timeout gut sein?
> Der schlägt nur dann zu, wenn du keinen SPI-Takt im Master gestartet
> hast, aber dann hast du einfach einen Bug im Code.  Sofern garantiert
> ist, dass der Takt des Masters läuft, dann ist nach 8 SPI-Takten
> ein Schiebevorgang zu Ende.
Der Timeout verhindert, dass der Controller in der while-Schleife hängen 
bleibt, falls das SPI-Modul nie das SPIC.IF setzt. Kann z.B. passieren, 
falls das SPI nicht angschlossen ist (dann soll das Gerät trotzdem noch 
funktionieren) oder ein Übertragungsfehler geschieht.

von Falk B. (falk)


Lesenswert?

@Christoph M. (chrito)

>Der Timeout verhindert, dass der Controller in der while-Schleife hängen
>bleibt, falls das SPI-Modul nie das SPIC.IF setzt.

Kann nie passieren, es sein denn, dein uC ist massiv defekt.

> Kann z.B. passieren,
>falls das SPI nicht angschlossen ist

Kaum, denn das SPI ist fester Siliziumbestandteil.

> (dann soll das Gerät trotzdem noch
>funktionieren) oder ein Übertragungsfehler geschieht.

Auch Unsinn. Du kannst mit SPI beliebig Daten hin- und herschaufeln, 
ganz egal ob da eine IC dranhängt oder nicht, denn es gibt keine aktive 
Rückmeldung wie bei I2C.

>ungefähr so:

>while ((!(SPIC.STATUS& 0b10000000))&&(--Zaehler16Bit)); //warte
>SPI_Empfangspuffer[Pufferindex] = SPIC.DATA;  // hole Byte
>SPIC.DATA = SPI_Sendepuffer[Pufferindex];     // sende Byte

Nö, so nicht. Lies mal das hier.

http://www.matuschek.net/atmega-spi/

Gilt auch Für die Xmegas, da gibt es keinen nennenswerten Unterschied.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Christoph M. schrieb:
> Der Timeout verhindert, dass der Controller in der while-Schleife hängen
> bleibt, falls das SPI-Modul nie das SPIC.IF setzt.

Falls das passieren kann, wäre folgende Lösung möglich wenn's wirklich 
schnell sein soll:

Der Timeout wird asynchron per Timer-ISR ausgelöst:
1
#include <setjmp.h>
2
#include <stdbool.h>
3
#include <avr/io.h>
4
#include <avr/interrupt.h>
5
6
static jmp_buf buf_timeout;
7
8
bool send (void)
9
{
10
    if (setjmp (buf_timeout))
11
    {
12
        // Timeout!
13
        // Disable Timer and respective IRQ
14
        __asm ("; Timeout");
15
        return 0;
16
    }
17
18
    // Set Timer and enable respective IRQ.
19
    // Code that might time-out
20
    __asm ("; Code");
21
    return 1;
22
}
23
24
ISR (INT0_vect)
25
{
26
    longjmp (buf_timeout, 1);
27
}

avr-gcc v8 -Os erzeugt für ATmega8 und send():
1
send:
2
  push r28
3
  push r29
4
  in r28,__SP_L__
5
  in r29,__SP_H__
6
  ldi r24,lo8(buf_timeout)
7
  ldi r25,hi8(buf_timeout)
8
  rcall setjmp
9
  or r24,r25
10
  breq .L2
11
  ; Timeout
12
  ldi r24,0
13
.L1:
14
  pop r29
15
  pop r28
16
  ret
17
.L2:
18
  ; Code
19
  ldi r24,lo8(1)
20
  rjmp .L1
Damit ist der heiße Zweig frei von Warterei und Testen auf Timeout.  Der 
Ansatz entspricht einer setjmp/longjmp Exception, und der Overhead 
wandert in den kalten Zweig, nämlich Handling von longjmp und 
Stack-Unwind in der ISR:
1
__vector_1:
2
  push r1
3
  push r0
4
  in r0,__SREG__
5
  push r0
6
  clr __zero_reg__
7
  ldi r22,lo8(1)
8
  ldi r23,0
9
  ldi r24,lo8(buf_timeout)
10
  ldi r25,hi8(buf_timeout)
11
  rcall longjmp

: Bearbeitet durch User
von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Falk B. schrieb:
> Kann nie passieren, es sein denn, dein uC ist massiv defekt.

Er muss nicht defekt sein, aber zumindest massiv fehlkonfiguriert:
wenn man dem SPI-Modul den Takt klaut, dann passiert natürlich
nichts mehr, es gibt also auch kein Interruptflag.

Bei ARMs ist dieses Phänomen häufiger anzutreffen, da viele
Peripheriemodule standardmäßig nicht vom Takt versorgt werden, damit
sie keine sinnlose Energie schlucken.  Beim Xmega kann man die Takte
zwar auch ausschalten, aber sie sind standardmäßig alle an.

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.