ATMega SPI Performance Tuning

 Startseite | Blog | Sitemap | Impressum | Login

Für die Anbindung des ENC28J60 über SPI suchte ich eine Möglichkeit, einen Datenblock möglichst effizient über SPI zu transferieren.

Die von einfachste Variante sieht etwa so aus:

void enc28j60_write_memory(uint8_t *buff, uint8_t count) {
     CS_LOW;
     SPI_SEND(CMD_WRITEMEMORY);
     do {
         SPDR = *buff;
         buff++;
         // wait until SPI send has finished
         while (!(SPSR & (1<<SPIF)));
     } while (--count);
    
     CS_HIGH;    
}

Mit GCC compiliert ergibt das folgenden Assemblercode:

  c2:    fc 01           movw    r30, r24
  c4:    c4 98           cbi    0x18, 4    ; 24
  c6:    8a e7           ldi    r24, 0x7A    ; 122
  c8:    8f b9           out    0x0f, r24    ; 15
  ca:    77 9b           sbis    0x0e, 7    ; 14
  cc:    fe cf           rjmp    .-4          ; 0xca <enc28j60_write_memory+0x8>
  ce:    81 91           ld    r24, Z+
  d0:    8f b9           out    0x0f, r24    ; 15
  d2:    77 9b           sbis    0x0e, 7    ; 14
  d4:    fe cf           rjmp    .-4          ; 0xd2 <enc28j60_write_memory+0x10>
  d6:    61 50           subi    r22, 0x01    ; 1
  d8:    d1 f7           brne    .-12         ; 0xce <enc28j60_write_memory+0xc>
  da:    c4 9a           sbi    0x18, 4    ; 24
  dc:    08 95           ret

Dieser Code sieht eigentlich recht gut aus. Dennoch entstehen beim Transfer recht lange Pausen zwischen 2 Bytes, wie die Auswertung mit dem Logikanalysator zeigt.

Umgruppierung der Befehle

Es ist zu sehen, dass nach dem Setzen des Statusregisters durch den ATMega noch einige Befehle abgearbeitet werden müssen, bis das nächste Byte ausgegeben wird. Die Idee ist jetzt, diese Befehle möglichst zu überspringen:

void enc28j60_write_memory(uint8_t *buff, uint8_t count) {
     CS_LOW;
     SPI_SEND(CMD_WRITEMEMORY);
     do {
        while (!(SPSR & (1<<SPIF)));
        SPDR = *buff;
         // do this while the controller sends data using SPI
         buff++;
     } while (--count);
     
     CS_HIGH;    
}

Hieraus ergibt sich folgender Assembler-Code:

000000c2 <enc28j60_write_memory>:
c2: fc 01 movw r30, r24
c4: c4 98 cbi 0x18, 4 ; 24
c6: 8a e7 ldi r24, 0x7A ; 122
c8: 8f b9 out 0x0f, r24 ; 15
ca: 77 9b sbis 0x0e, 7 ; 14
cc: fe cf rjmp .-4 ; 0xca <enc28j60_write_memory+0x8>
ce: 77 9b sbis 0x0e, 7 ; 14
d0: fe cf rjmp .-4 ; 0xce <enc28j60_write_memory+0xc>
d2: 81 91 ld r24, Z+
d4: 8f b9 out 0x0f, r24 ; 15
d6: 61 50 subi r22, 0x01 ; 1
d8: d1 f7 brne .-12 ; 0xce <enc28j60_write_memory+0xc>
da: c4 9a sbi 0x18, 4 ; 24
dc: 08 95 ret

Wenn ich das Datenblatt richtig interpretiere, sollte SPIF immer gesetzt sein, wenn keine Interrupts benutzt werden und SPI gerade keine Daten überträgt, der Code sollte also eigentlich funktionieren.  Zur Sicherheit müsste man nach der While-Schleife eigentlich auch noch eien Abfrage auf das SPIF-Bit einbauen.

Vorladen des Registers

Ein Blick auf den Assembler-Code im letzten Abschnitt zeigt, dass zwischen abgeschlossenem Senden des letzten Bytes und Senden den nächsten Byte noch ein Register geladen werden muss (r24), welches dann an den SPI-Port geschickt wird. Diese Befehle lassen sich jedoch auch vor der Abfrage des SPIF-Bits erledigen.

Also wird der C-Code nochmals wie folgt modifiziert (man braucht nicht selbst Assembler zu programmieren!)

void enc28j60_write_memory(uint8_t *buff, uint8_t count) {
CS_LOW;
SPI_SEND(CMD_WRITEMEMORY);
do {
uint8_t data=*buff;
buff++;
while (!(SPSR & (1<<SPIF)));
SPDR = data;
} while (--count);

CS_HIGH;
}

Dies wird zu folgendem Assembler-Code compiliert:

000000c2 <enc28j60_write_memory>:
c2: fc 01 movw r30, r24
c4: c4 98 cbi 0x18, 4 ; 24
c6: 8a e7 ldi r24, 0x7A ; 122
c8: 8f b9 out 0x0f, r24 ; 15
ca: 77 9b sbis 0x0e, 7 ; 14
cc: fe cf rjmp .-4 ; 0xca <enc28j60_write_memory+0x8>
ce: 81 91 ld r24, Z+
d0: 77 9b sbis 0x0e, 7 ; 14
d2: fe cf rjmp .-4 ; 0xd0 <enc28j60_write_memory+0xe>
d4: 8f b9 out 0x0f, r24 ; 15
d6: 61 50 subi r22, 0x01 ; 1
d8: d1 f7 brne .-12 ; 0xce <enc28j60_write_memory+0xc>
da: c4 9a sbi 0x18, 4 ; 24
dc: 08 95 ret

Besser dürfte es nicht mehr werden, denn nun erfolgt die Ausgabe des nächsten Bytes direkt nachdem das gelöschte SPIF-Bit erkannt wurde (Zeile d2). Die SPI-Kommunikation ist nochmals etwas schneller geworden, wie das Bild zeigt.

 

Warteschleife

Die "brutale" Variante wäre es, nicht das SPIF-Bit abzufragen, sondern einfach ein paar "NOP"-Befehle einzufügen. Ob dies jedoch unter allen Umständen korrekt funktioniert, ist mir nicht bekannt, daher halte ich persönlich von dieser Lösung nichts.

Fazit

Bei performance-kritischen Programmteilen lohnt sich ein Blick auf den erzeugten Assembler-Code unbedingt, selbst wenn man selbst nicht in Assembler programmiert, erkennt man teilweise doch Optimierungspotential.

Betrachtet man den Assembler-Code der Beispiele, so sieht man, dass immer die gleichen Befehle genutzt werden und lediglich die Reihenfolge unterschiedlich ist. Solche Optimierungen lassen sich durchführen, ohne selbst Assembler zu programmieren!

Die Optimierung funktioniert hier, weil man es praktisch mit einem Multithreading-Programm zu tun hat. Nur wird der 2.Programmthread - nämlich die Ausgabe der einzelnen Bits auf dem SPI-Port - nicht selbst implementiert, sondern ist schon in der Hardware des ATMega implementiert. Hier gilt es, die Zeit, die der Controller für das Senden benötigt, möglichst effizient im eigenen Programm auszunutzen - also so viele "nützliche" Befehle wie möglich während dieser Zeit abzuarbeiten.

 

Archivierte Seite

Diese Seite wurde archiviert, d.h. sie wird nicht mehr aktiv gepflegt und die Informationen entsprechen unter Umständen nicht mehr dem aktuellen Stand.

Werbung
Look-Out
Talking about everything
Crazy audio
DIY audio projects and more
Anmesty International SchweizMenschenrechte für alle

Menschen für MenschenKarlheinz Böhms Äthiopienhilfe