Forum: Compiler & IDEs Frage zu Software-SPI


von M. S. (elpaco)


Lesenswert?

Hallo zusammen,

vor kurzem habe ich eine SPI in Software umgesetzt. Die Frage, die ich 
habe betrifft spezifisch das setzen/löschen der Bits auf dem Data-Port 
während des Sendevorgangs.

Hier im Forum sehe ich fast immer die Lösung, mit einer Maske zu 
arbeiten, die dann in jedem Schleifendurchlauf geshiftet wird um somit 
das Bit zu maskieren, das gesendet werden soll. Beispielsweise so 
(nächstbestes Beispiel, das ich gefunden habe):
1
void display_out(unsigned char out)
2
{
3
  unsigned char mask = 0x80;
4
  
5
  while(mask)
6
  {
7
    DISPLAY_PORT &= ~(1 << CLK);
8
    if(mask & out)
9
    {
10
      DISPLAY_PORT |= (1 << SDA);
11
    }
12
    else
13
    {
14
      DISPLAY_PORT &= ~(1 << SDA);
15
    }
16
    DISPLAY_PORT |= (1 << CLK);
17
    mask = mask >> 1;
18
  }  
19
}

Ich selbst habe es gelöst durch
1
...
2
if(data&(1<<i))
3
...

Das funktioniert auch soweit. Ich frage mich daher, warum man den 
"Umweg" über die Maske macht, also extra eine Variable einführt, die 
dann ebenfalls durch das Shiften CPU-Zyklen verbrät.

Kann mir da jemand weiterhelfen?

von Christian (Gast)


Lesenswert?

viele Wege führen nach Rom...

Unterschied der beiden Methoden:
Du schiebst die Bits von rechts nach links raus, wenn du anderstrum 
schieben willst geht das natürlich auch.
Allerdings hast du schon mal die Anzahl der Schiebeoperationen gezählt, 
die für einen kompletten Schleifendurchlauf zusammenkommen?
28 versus 7 - damit ist die erste Methode, trotz einer temporären 
Variable, sicherlich schneller.

Christian

von Chris (Gast)


Lesenswert?

Hallo elpaco,

du brauchst so oder so eine Variable, mit der du die Maske erstellt. 
Deine Lösung sieht auf den ersten Blick vielleicht "billiger" (in Bezug 
auf CPU-Zyklen) aus, jedoch benutzt du ja auch eine Variable (dein "i" - 
vermutlich von einer for-Schleife).

Ich kann dir jetzt nicht genau sagen, wie dein Compiler optimiert, aber 
die Lösung mit der Masken-Variablen sieht für mich so als, als ob sie 
effizienter wäre, da die Maske gleichzeitig auch die Schleifenbedingung 
darstellt.

KURZE (!) Gegenüberstellung der beiden Varianten:
Maske:
-INIT
-Schleifenbedingung überprüfen
-Pinwackeln
-Rechtsshift
-Rücksprung zur Schleife

for-Schleife:
-INIT
-Schleifenbedingung überprüfen
-Linksshift um i Stellen
-Pinwackeln
-Zählvariable verändern
-Rücksprung zur Schleife

Viele Grüße

von M. S. (elpaco)


Lesenswert?

Okay, das heißt das (1<<i) ist im Endeffekt nichts anderes als eine 
Variable anzulegen und sie i-mal zu shiften. Und durch die Maske spart 
man sich das, jeden Durchlauf neu zu shiften. Man muss nur insgesamt N 
mal shiften und nicht
 mal mit N = Anzahl der Schleifendurchläufe

: Bearbeitet durch User
von Fabian O. (xfr)


Lesenswert?

Der teure Teil an Deiner Lösung ist das "1<<i". Auch wenn es harmlos 
aussieht, die meisten Mikrocontroller können den Wert nicht mit einem 
Maschinenbefehl berechnen. Ohne Barrel-Shifter kann der Prozessor pro 
Befehl nur um eine Stelle nach rechts/links shiften.

Der Compiler muss also eine Schleife bauen mit einer temporären Variable 
(Anfangswert 1), die er i-Mal um eine Stelle nach links shiftet. Und das 
in jedem Schleifendurchlauf neu. Das braucht deutlich mehr Zeit.

Falls der Compiler perfekt optimiert, könnte am Ende der gleiche Code 
wie in der Variante mit der expliziten Maske rauskommen. Drauf verlassen 
würde ich mich aber nicht.

von holger (Gast)


Lesenswert?

>Ich frage mich daher, warum man den
>"Umweg" über die Maske macht, also extra eine Variable einführt, die
>dann ebenfalls durch das Shiften CPU-Zyklen verbrät.

Da kann man dir eigentlich nur den Tip geben mal den erzeugten
Assemblercode anzusehen. Zusätzlich dann vor der Schleife mal
einen Pin setzen und danach löschen. Das ganze dann auf einem
Osci ansehen oder mit dem Simulator mal CPU Zyklen zählen lassen.

Dann siehst du ob es einen Unterschied gibt oder nicht.

von SAMUEL (Gast)


Lesenswert?

AVR Studio benutzen. beide Varianten Programmieren. Im Simulator laufen 
lassen und das Assemblerfenster anschauen. Am Ende bekommst Du noch die 
komplette Zeit deiner Routine angezeigt. Die Sauberste Lösung ist 
übrigens es direkt selber in Assembler zu programmieren und einbinden, 
wenn man meint es sei Zeitkritisch.

von Ast E. (vis)


Lesenswert?

Fabian O. schrieb:
> Der teure Teil an Deiner Lösung ist das "1<<i". Auch wenn es
> harmlos
> aussieht, die meisten Mikrocontroller können den Wert nicht mit einem
> Maschinenbefehl berechnen. Ohne Barrel-Shifter kann der Prozessor pro
> Befehl nur um eine Stelle nach rechts/links shiften.

so sieht es aus :-)

von Peter D. (peda)


Angehängte Dateien:

Lesenswert?

SAMUEL schrieb:
> Die Sauberste Lösung ist
> übrigens es direkt selber in Assembler zu programmieren und einbinden,
> wenn man meint es sei Zeitkritisch.

Nä, wat hammer gelacht.
1
uint8_t shift_io( uint8_t b )   // send / receive byte
2
{
3
  uint8_t i;
4
5
  SPI_CLK_DDR = 1;              // set as output
6
  SPI_MOSI_DDR = 1;
7
8
  for( i = 8; i; i-- ){         // 8 bits
9
    SPI_MOSI = 0;
10
    if( b & 0x80 )              // high bit first
11
      SPI_MOSI = 1;
12
    b <<= 1;
13
    SPI_CLK = 1;
14
    if( SPI_MISO_PIN )
15
      b++;
16
    SPI_CLK = 0;
17
  }
18
  return b;
19
}

Und hier das Listing:
1
uint8_t shift_io( uint8_t b )   // send / receive byte
2
{
3
  uint8_t i;
4
5
  SPI_CLK_DDR = 1;              // set as output
6
  2e:  b8 9a         sbi  0x17, 0  ; 23
7
  SPI_MOSI_DDR = 1;
8
  30:  b9 9a         sbi  0x17, 1  ; 23
9
  32:  98 e0         ldi  r25, 0x08  ; 8
10
11
  for( i = 8; i; i-- ){         // 8 bits
12
    SPI_MOSI = 0;
13
  34:  c1 98         cbi  0x18, 1  ; 24
14
    if( b & 0x80 )              // high bit first
15
  36:  87 fd         sbrc  r24, 7
16
      SPI_MOSI = 1;
17
  38:  c1 9a         sbi  0x18, 1  ; 24
18
    b <<= 1;
19
  3a:  88 0f         add  r24, r24
20
    SPI_CLK = 1;
21
  3c:  c0 9a         sbi  0x18, 0  ; 24
22
    if( SPI_MISO_PIN )
23
  3e:  b2 99         sbic  0x16, 2  ; 22
24
      b++;
25
  40:  8f 5f         subi  r24, 0xFF  ; 255
26
    SPI_CLK = 0;
27
  42:  c0 98         cbi  0x18, 0  ; 24
28
  uint8_t i;
29
30
  SPI_CLK_DDR = 1;              // set as output
31
  SPI_MOSI_DDR = 1;
32
33
  for( i = 8; i; i-- ){         // 8 bits
34
  44:  91 50         subi  r25, 0x01  ; 1
35
  46:  b1 f7         brne  .-20       ; 0x34 <__CCP__>
36
    if( SPI_MISO_PIN )
37
      b++;
38
    SPI_CLK = 0;
39
  }
40
  return b;
41
}
42
  48:  08 95         ret
Bin mal gespannt, wie man das in Assembler noch besser optimiert.

von SAMUEL (Gast)


Lesenswert?

Haha jenau ich lach mit.

1. War das Allgemein gemeint.
2. Eine gut geschriebene Assemblerfunktion ist <= Compiler
3. Komme ich zum rausschieben eines Bytes via SPI auf 12 instructions, 
bei deinem  Compiler Beispiel zähle ich 14.
1
             ;shift out a byte (r24) 
2
ShiftByte:   ldi  r20, 8                
3
4
SHIFT_LOOP:  cbi  PORTB, CLK            
5
             cbi  PORTB, SDI                    
6
                
7
             rol  r24                  
8
             brcc  SHIFT_LOW                  
9
             sbi  PORTB, SDI              
10
SHIFT_LOW:   sbi  PORTB, CLK                
11
        
12
             dec  r20                  
13
             brne  SHIFT_LOOP              
14
15
             sbi  PORTB, LCK              
16
             cbi  PORTB, LCK              
17
             ret
Haha

: Bearbeitet durch User
von SAMUEL (Gast)


Lesenswert?

Ich muss mich korrigieren es sind bei mir sogar nur 10 Instrucktion's 
die letzten beiden:

sbi  PORTB, LCK
cbi  PORTB, LCK

gehören schon nicht mehr zu SPI-Routine. Also noch mal mehr hahaha...

von Peter D. (peda)


Lesenswert?

SAMUEL schrieb:
> Ich muss mich korrigieren es sind bei mir sogar nur 10 Instrucktion's

Ich hätte gedacht, es wäre aus den Kommentaren ersichtlich, daß meine 
Routine etwas mehr macht.
Die zusätzlichen Befehle setzen die Pins auf Ausgang und lesen MISO ein.
Oft muß man einen SPI-Slave ja auch auslesen.

Wenn ich das weglasse, sinds auch 10.

: Bearbeitet durch User
von SAMUEL (Gast)


Lesenswert?

Warum jedes mal aufs neue als Ausgang konfigurieren. In der Regel halten 
die ihren Zustand.

von Karl H. (kbuchegg)


Lesenswert?

SAMUEL schrieb:
> Warum jedes mal aufs neue als Ausgang konfigurieren. In der Regel halten
> die ihren Zustand.

Dann nimms raus, wenn dir das nicht gefällt.
Das war doch gar nicht der springende Punkt, den PeDa angesprochen hat.

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

SAMUEL schrieb:
> Eine gut geschriebene Assemblerfunktion ist <= Compiler

premature opti... jaja, schon gut, ich bin ja schon still...

von M. S. (elpaco)


Lesenswert?

Im Endeffekt ist es dann auch ineffizient, bspw ADC-Register mit z.B. 
(1<<ADSC) zu setzen, sofern diese nicht I/O sind und somit sbi/cbi 
können? Da wäre dann ein immediate-Wert effizienter, wenn ich das 
richtig verstehe?

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

M. S. schrieb:
> Im Endeffekt ist es dann auch ineffizient, bspw ADC-Register mit z.B.
> (1<<ADSC) zu setzen, sofern diese nicht I/O sind und somit sbi/cbi
> können? Da wäre dann ein immediate-Wert effizienter, wenn ich das
> richtig verstehe?

Der Compiler macht dir daraus schon einen immediate-Wert, solange beide 
seiten des << Operators konstant sind.

von Peter D. (peda)


Lesenswert?

M. S. schrieb:
> Im Endeffekt ist es dann auch ineffizient, bspw ADC-Register mit z.B.
> (1<<ADSC) zu setzen

Konstante Ausdrücke werden schon zur Compilezeit ausgerechnet.
Du kannst daher ohne Bedenken Konstanten in float definieren, z.B. 
Faktoren für die Berechnung von Zeiten, Spannungen, Temperaturen usw..
Aber sicherheitshalber vor der Verwendung noch nach Ganzzahl (uint16_t) 
casten.

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

Peter Dannegger schrieb:
> Du kannst daher ohne Bedenken Konstanten in float definieren, z.B.
> Faktoren für die Berechnung von Zeiten, Spannungen, Temperaturen usw..
> Aber sicherheitshalber vor der Verwendung noch nach Ganzzahl (uint16_t)
> casten.

Wobei hier das Klammern noch wesentlich wichtiger als das casten ist.

z.B. wird
1
 
2
#define A 47.11
3
#define B 8.15
4
float y = x * A / B;
nicht durch eine Multiplikation mit der Konstante 5.78 (=47.11/8.15) 
ersetzt (musste ich schmerzhaft lernen). Der resultierende 
Assembler-Code enthält eine Multiplikation und eine (teure) Division

Der Code wird wesentlich schneller, wenn man nur eine Klammer setzt:
1
 
2
#define A 47.11
3
#define B 8.15
4
double y = x * (A / B);
Hier rechnet der Compiler wirklich eine Konstante und erzeugt nur eine 
Multiplikation.

Wichtig: das ist kein Fehler des Compilers, aufgrund der begrenzten 
genauigkeit von Fließkommazahlen (egal ob float oder double) muss er 
richtigerweise so vorgehen.

: Bearbeitet durch User
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.