Forum: Mikrocontroller und Digitale Elektronik Schnellste Software-SPI Implementierung auf AVR


von Tim  . (cpldcpu)


Lesenswert?

Hier mal ein Puzzle für die AVR-Assembler-Hacker: Wie viele Taktzyklen 
benötigt man pro Bit für eine Software-SPI Implementierung als Master?

Randbedingungen:
-Es werden nur Daten gesendet. Dazu sind zwei Leitungen notwendig: SCK 
und MOSI.
-Beide Leitungen befinden sich auf dem gleichen Port und können demnach 
mit einem Befehl geschrieben werden.
-Die Daten werden auf der steigenden Flanke von SCK gesampled. Demnach 
können Daten auf der fallenden Flanke geändert werden.
-SCK muss nicht symmetrisch sein.

Ich komme auf ein Minimum von 5 Taktzyklen:
1
   OUT PORT, Rx
2
   SBRC Ry,bit
3
   OUT PORT, Ry
4
   SBI PORT, clkbit

von Detlef K. (adenin)


Lesenswert?

Wann Du das mal für 8 Bit schreiben würdest, dann können wir 
AVR-Assembler-Hacker mehr dazu sagen. :)

von Ralph (Gast)


Lesenswert?

Wenn du schnell haben willst nimm die HW SPI, alles andere ist Murks.

von Tim  . (cpldcpu)


Lesenswert?

Hier als Ausgangsbasis meine aktuelle C-Implementierung. Mit dieser 
kommt man auf fck/10. (1.6 MHz SCK bei 16Mhz Taktfrequenz.
1
inline void spiwrite(uint8_t c) {
2
  uint8_t i;
3
  uint8_t mask1=SPIPORT & ~( (1<<SCK) | (1<<MOSI) );
4
  uint8_t mask2=mask1 | (1<<MOSI);
5
  
6
  /* Assumed state before call: SCK- Low, MOSI- High */  
7
  for (i=0; i<8 ;i++)
8
  {
9
    if (!(c&0x80)) SPIPORT = mask1;  // set data low
10
    
11
    SPIPORT |=  (1<< SCK); // SCK hi , data sampled here
12
    SPIPORT = mask2;       // SCK low, MOSI hi
13
    c<<=1;            
14
  }
15
  /* State after call: SCK Low, MOSI high */
16
}

Dies ist das Ergebnis mit AVRGCC und -O1:

1
inline void spiwrite(uint8_t c) {
2
  uint8_t i;
3
  uint8_t mask1=SPIPORT & ~( (1<<SCK) | (1<<MOSI) );
4
 31a:  35 b1         in  r19, 0x05  ; 5
5
 31c:  37 7d         andi  r19, 0xD7  ; 215
6
  uint8_t mask2=mask1 | (1<<MOSI);
7
 31e:  23 2f         mov  r18, r19
8
 320:  28 60         ori  r18, 0x08  ; 8
9
 322:  98 e0         ldi  r25, 0x08  ; 8
10
  
11
  /* Assumed state before call: SCK- Low, MOSI- High */
12
  
13
  for (i=0; i<8 ;i++)
14
  {
15
    if (!(c&0x80)) SPIPORT = mask1;  // set data low
16
 324:  88 23         and  r24, r24
17
 326:  0c f0         brlt  .+2        ; 0x32a <writecommand+0x12>
18
 328:  35 b9         out  0x05, r19  ; 5
19
    
20
    SPIPORT |=  (1<< SCK); // SCK hi , data sampled here
21
 32a:  2d 9a         sbi  0x05, 5  ; 5
22
    SPIPORT = mask2;       // SCK low, MOSI hi
23
 32c:  25 b9         out  0x05, r18  ; 5
24
    c<<=1;            
25
 32e:  88 0f         add  r24, r24
26
 330:  91 50         subi  r25, 0x01  ; 1
27
  uint8_t mask1=SPIPORT & ~( (1<<SCK) | (1<<MOSI) );
28
  uint8_t mask2=mask1 | (1<<MOSI);
29
  
30
  /* Assumed state before call: SCK- Low, MOSI- High */
31
  
32
  for (i=0; i<8 ;i++)
33
 332:  c1 f7         brne  .-16       ; 0x324 <writecommand+0xc>
34
/* SPI general support functions */
35
36
void writecommand(uint8_t c) {
37
  RSPORT &= ~(1 << RS);
38
  spiwrite(c);
39
}
40
 334:  08 95         ret

: Bearbeitet durch User
von Tim  . (cpldcpu)


Lesenswert?

Wie man sieht, hat der Compiler schon einige ziemlich clevere 
Optimierungen eingebaut. Auf den ersten Blick kann man mit Assembler nur 
einen Taktzyklus herausoptimieren, mit Änderung der Funktion 2.

Die aktuelle C-Version ist schon schneller als der übliche verbreitete 
Code:
1
  // Fast SPI bitbang swiped from LPD8806 library
2
    for(uint8_t bit = 0x80; bit; bit >>= 1) {
3
      if(c & bit) *dataport |=  datapinmask;
4
      else        *dataport &= ~datapinmask;
5
      *clkport |=  clkpinmask;
6
      *clkport &= ~clkpinmask;
7
    }

von Tim  . (cpldcpu)


Lesenswert?

Nach loop-unrolling, und einigen Assembleroptimierungen komme ich auf 5 
Taktzyklen pro Bit, also fck/5. Geht es noch schneller?


1
inline void spiwrite(uint8_t c) {
2
  uint8_t i;
3
  uint8_t mask1=SPIPORT & ~( (1<<SCK) | (1<<MOSI) );
4
  uint8_t mask2=mask1 | (1<<MOSI);
5
  
6
  // Assumed state before call: SCK- Low, MOSI- High
7
  
8
    asm volatile(
9
    
10
    "    sbrs  %0,7  \n\t"    // 1 bit7
11
    "    out    %1,%2  \n\t"    // 2
12
    "    sbi    %1,%4  \n\t"    // 4
13
    "    out    %1,%3  \n\t"    // 5
14
    
15
    "    sbrs  %0,6  \n\t"    // bit6
16
    "    out    %1,%2  \n\t"
17
    "    sbi    %1,%4  \n\t"
18
    "    out    %1,%3  \n\t"
19
    "    sbrs  %0,5  \n\t"    // bit5
20
    "    out    %1,%2  \n\t"
21
    "    sbi    %1,%4  \n\t"
22
    "    out    %1,%3  \n\t"
23
    "    sbrs  %0,4  \n\t"    // bit4
24
    "    out    %1,%2  \n\t"
25
    "    sbi    %1,%4  \n\t"
26
    "    out    %1,%3  \n\t"
27
    "    sbrs  %0,3  \n\t"    // bit3
28
    "    out    %1,%2  \n\t"
29
    "    sbi    %1,%4  \n\t"
30
    "    out    %1,%3  \n\t"
31
    "    sbrs  %0,2  \n\t"    // bit2
32
    "    out    %1,%2  \n\t"
33
    "    sbi    %1,%4  \n\t"
34
    "    out    %1,%3  \n\t"
35
    "    sbrs  %0,1  \n\t"    // bit1
36
    "    out    %1,%2  \n\t"
37
    "    sbi    %1,%4  \n\t"
38
    "    out    %1,%3  \n\t"
39
    "    sbrs  %0,0  \n\t"    // bit0
40
    "    out    %1,%2  \n\t"
41
    "    sbi    %1,%4  \n\t"
42
    "    out    %1,%3  \n\t"
43
    :  
44
    :  "r" (c), "I" (_SFR_IO_ADDR(SPIPORT)), "r" (mask1), "r" (mask2), "I" (SCK)
45
    );
46
47
  // State after call: SCK Low, MOSI high
48
}

: Bearbeitet durch User
von Martin S. (msperl)


Lesenswert?

Theoretisch (nicht getestet) sollte ein MASTER-SPI-TX wie folgt auch in 
4 Zyklen gehen:
1
;; DATAR   ... Register mit den zu transferierenden Daten
2
;; PORTX   ... Das PORT Register für den Transfer
3
;; PINX    ... Das PIN  Register für den Transfer
4
;; OUTR    ... Register mit  dem "Standard" Wert für PORTX
5
;; PINMOSI ... Das bit im PORT/PIN Register für MOSI
6
;; PINSCK  ... Das bit im PORT/PIN Register für SCK
7
;; SCKPINR ... Register mit dem PINSCK bit gesetzt (1<<PINSCK)
8
;; BITNUM  ... das wievielte bit soll verschickt werden?
9
  bst DATAR  ,BITNUM
10
  bld OUTR   ,PINMOSI
11
  out PORTX  ,OUTR
12
  out PINX   ,SCKPINR

wenn man das bei den weiteren Bits etwas umsortiert, so kann SCK auch 
symmetrisch sein - geht halt nur beim ersten Bit nicht.

Sieht dann halt so aus:
1
;; DATAR   ... Register mit den zu transferierenden Daten
2
;; PORTX   ... Das PORT Register für den Transfer
3
;; PINX    ... Das PIN  Register für den Transfer
4
;; OUTR    ... Register mit  dem "Standard" Wert für PORTX
5
;; PINMOSI ... Das bit im PORT/PIN Register für MOSI
6
;; PINSCK  ... Das bit im PORT/PIN Register für SCK
7
;; SCKPINR ... Register mit dem PINSCK bit gesetzt (1<<PINSCK)
8
  bst DATAR  ,7
9
  bld OUTR   ,PINMOSI
10
  out PORTX  ,OUTR
11
  bst DATAR  ,6
12
  out PINX   ,SCKPINR
13
  bld OUTR   ,PINMOSI
14
  out PORTX  ,OUTR
15
  bst DATAR  ,5
16
  out PINX   ,SCKPINR
17
  bld OUTR   ,PINMOSI
18
  out PORTX  ,OUTR
19
  bst DATAR  ,4
20
  out PINX   ,SCKPINR
21
  bld OUTR   ,PINMOSI
22
  out PORTX  ,OUTR
23
  bst DATAR  ,3
24
  out PINX   ,SCKPINR
25
  bld OUTR   ,PINMOSI
26
  out PORTX  ,OUTR
27
  bst DATAR  ,2
28
  out PINX   ,SCKPINR
29
  bld OUTR   ,PINMOSI
30
  out PORTX  ,OUTR
31
  bst DATAR  ,1
32
  out PINX   ,SCKPINR
33
  bld OUTR   ,PINMOSI
34
  out PORTX  ,OUTR
35
  bst DATAR  ,0
36
  out PINX   ,SCKPINR
37
  bld OUTR   ,PINMOSI
38
  out PORTX  ,OUTR

SPI-MASTER-RX ist auch nicht viel aufwändiger:
1
;; DATAR   ... Register mit den empfangenen Daten
2
;; PORTX   ... Das PORT Register für den Transfer
3
;; PINX    ... Das PIN  Register für den Transfer
4
;; OUTR    ... Register mit  dem "Standard" Wert für PORTX
5
;; PINSCK  ... Das bit im PORT/PIN Register für SCK
6
;; SCKPINR ... Register mit dem PINSCK bit gesetzt (1<<PINSCK)
7
;; PINMISO ... Das bit im PORT/PIN Register für MISO
8
;; BITNUM  ... das wievielte bit soll verschickt werden?
9
  out PORTX ,OUTR
10
  in  TMP   ,PINX
11
  bst TMP   ,PINMISO
12
  out PINX  ,SCKPINR
13
  bld DATAR ,BITNUM
und ist mit 5 Zyklen fast symmetrisch - fuer volle SCK-Symmetrie ein NOP 
am Schluss anhaengen.

und zuletzt beides zusammengesetzt SPI-MASTER-TX/RX in 7 Zyklen:
1
;; DATAR   ... Register mit den zu transferierenden und empfangenden Daten
2
;; OUTR    ... Register mit  dem "Standard" Wert für PORTX
3
;; PORTX   ... Das PORT Register für den Transfer
4
;; PINX    ... Das PIN  Register für den Transfer
5
;; PINMOSI ... Das bit im PORT/PIN Register für MOSI
6
;; PINSCK  ... Das bit im PORT/PIN Register für SCK
7
;; SCKPINR ... Register mit dem PINSCK bit gesetzt (1<<PINSCK)
8
;; PINMISO ... Das bit im PORT/PIN Register für MISO
9
;; BITNUM  ... das wievielte bit soll verschickt werden?
10
  bst DATAR ,BITNUM
11
  bld OUTR  ,PINMOSI
12
  out PORTX ,OUTR
13
  in  TMP   ,PINX
14
  bst TMP   ,PINMISO
15
  bld DATAR ,BITNUM
16
  out PINXX ,SCKPINR
und ist mit 7 Zyklen fast symmetrisch - fuer volle SCK-Symmetrie wieder 
ein NOP am Schluss anhaengen.

Martin

P.s: Eines der Male wo das T-Flag wirklich nuetzlich ist...

P.p.s: ich bin nicht sicher, aber vielleicht liesse sich im TX/RX Fall 
bei geschickten HW-Randbedingungen (z.b. "pin-wahl" MISO/MOSI auf 
PORT0/7) auch die BST/BLD bloecke durch LSR/LSL und ROL/ROR ersetzen und 
so eine Zyklus sparen.

Dürfte aber erfordern dass die PORT Werte der "restlichen" Pins egal 
sind und sich ändern dürfen - sprich:
Alle anderen Pins sind auf INPUT mit externem Pullup - damit spielt 
interner Pullup Wechsel keine Rolle.
Allerdings ist der Fall TX+RX gleichzeitig doch eher selten, sodass ich 
mir das Austuefteln diese Variante spare...

von Tim  . (cpldcpu)


Lesenswert?

Hallo Martin,

Super Trick! Das T-Flag existierte bei mir gedanklich gar nicht, so 
wenig Nutzen hatte es bisher. Für diese Anwendung lässt es sich aber 
ideal einsetzen.

Die Einsparung des zusätzlichen Taktzyklus kommt daher, dass Du die 
Toggle-Funktion nutzt, statt SCK mit SBI zu setzen. Den gleichen Trick 
könnte man auch mit der Bit-Afrage mit SBRS kombinieren, um auf 4 
Taktzyklen pro Bit zu kommen. Allerdings ist die Variante mit dem T-Flag 
wegen des symmetrischen Clocksignals natürlich eleganter.

Noch einen Taktzyklus einzusparen stelle ich mir schwierig vor. Selbst 
mit den Shift-Befehlen muss man irgendwie gleichzeitig das Clock-Signal 
setzten.

: Bearbeitet durch User
von MCUA (Gast)


Lesenswert?

>Wie man sieht, hat der Compiler schon einige ziemlich clevere ...

Was er nicht eingebaut hat,
mit ein bisschen zus. Logic (mit Enab- u AVR-Clk -Anschluss) kann man 
den SCLK autom schalten.
Dann kann man mit
1
BST Ra,b1
2
BLD Rb,b2
3
OUT Px,Rb
jedes einzelne Bit in 3 Takten rausschicken.

von Tim  . (cpldcpu)


Lesenswert?

MCUA schrieb:
> Was er nicht eingebaut hat,
> mit ein bisschen zus. Logic (mit Enab- u AVR-Clk -Anschluss) kann man
> den SCLK autom schalten.

Naja, aber die Idee war doch eine reine Softwareimplementierung :)

Ich nutze den Code übrigens für Echtzeitdebugging, indem ich über zwei 
unbenutzte Pins Debugginginformationen ausgebe und mit einem LA 
analysiere. Das ist z.B. für V-USB Projekte auf dem AVR sehr nützlich.

: Bearbeitet durch User
von Markus W. (Firma: guloshop.de) (m-w)


Lesenswert?

MCUA schrieb:
> mit ein bisschen zus. Logic (mit Enab- u AVR-Clk -Anschluss) kann man
> den SCLK autom schalten.

Meintest du damit das Schalten eines Timer-Ausgangs (z.B. OC0A), der 
dann als CLK verwendet wird? Könnte auch gehen.

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.