Frage zu Software-SPI

#3817570
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?
Gast #3817593
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
Gast #3817595
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
#3817630
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
#3817653
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.
Gast #3817718
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.
Gast #3817964
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.
#3818052
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.
Angehängte Dateien:
Gast #3818440
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
#3818531
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.
(Firma: Brainit GmbH) #3819124
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.
#3819284
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.
(Firma: Brainit GmbH) #3819319
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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren