Forum: Mikrocontroller und Digitale Elektronik AVR mit C zu langsam - Inline Assembler?


von Hazel (Gast)


Lesenswert?

Hallo!

Ich arbeite an einem Projekt mit einem AT90CAN128 AVR Controller. Da der 
Controller mit den bisherigen Aufgaben (SPI-Datenausgabe und 
CAN-Datentransfer) schon an seine Leistungsgrenze gerät, habe ich mich 
mal gefragt, ob man das Ganze mit Assembler effektiver Programmieren 
kann. Leider habe ich keine Ahnung von Assembler. Ich möchte auf keinen 
Fall das ganze Projekt von vorne in Assembler neu programmieren, sondern 
vielleicht einige Teile, die ständig abgearbeitet werden, durch 
Inline-Assembler-Code ersetzen. Mir stellt sich aber die Frage, ob das 
überhaupt was bringt? Übersetzt der AVR-GCC schlecht?
Hier mal ein Beispiel:
Ich gebe per SPI Daten aus. Es gibt in der spi_drv.h das Macro 
Spi_send_byte(ch)

#define Spi_send_byte(ch) { (SPDR=ch); Spi_wait_spif(); }
mit
#define Spi_wait_spif() { while ((SPSR & (1<<SPIF)) == 0); }

wenn ich nun den Disassembler von AVR-Studio nutze, so wird mit für 
einen Aufruf Spi_send_byte(u_data.b[1]); der folgende Code angezeigt:

LDI   R30, 0x4E
LDI   R31, 0x00
LDD   R24, Y+3
STD   Z+0, R24
LDI   R30, 0x4D
LDI   R31, 0x00
LDD   R24, Z+0
TST   R24
BRGE  PC-0x04

Ist der erzeugte Code sinnvoll? Gehts kürzer, schneller? Warum wird hier 
etwas in die Register R30 und R31 geladen und dann nicht verwendet?
Bringt es was, mich in das Thema Inline-Assembler einzuarbeiten, oder 
hol ich damit nicht viel raus, weil der AVR-GCC gut genug ist?
Vielen Dank für eure Hilfe!

von Oliver (Gast)


Lesenswert?

>Da der Controller mit den bisherigen Aufgaben (SPI-Datenausgabe und
>CAN-Datentransfer) schon an seine Leistungsgrenze gerät,

Woher weisst du das?

Warteschleifen wie
1
#define Spi_wait_spif() { while ((SPSR & (1<<SPIF)) == 0); }
deuten darauf hin, daß der Prozessor doch den einen oder anderen Takt 
damit verbringt, mit voller Leistung nichts zu tun.

Oliver

von Karl H. (kbuchegg)


Lesenswert?

Hazel wrote:
> CAN-Datentransfer) schon an seine Leistungsgrenze gerät, habe ich mich
> mal gefragt, ob man das Ganze mit Assembler effektiver Programmieren
> kann.

Selten. Generell: Um einen Compiler zu schlagen muss man schon
ziemlich gut in Assembler sein. Beim AVR hat der gcc zwar noch
Potential um zuzulegen. Das bewegt sich aber im kleinen einstelligen
Prozentbereich.

> Leider habe ich keine Ahnung von Assembler. Ich möchte auf keinen
> Fall das ganze Projekt von vorne in Assembler neu programmieren, sondern
> vielleicht einige Teile, die ständig abgearbeitet werden, durch
> Inline-Assembler-Code ersetzen.

Das bringt selten was.
Wenn schon, dann solltest du versuchen auf algorithmischer
Ebene Zeit einzusparen. Da ergibt sich meist das größte
Potential.

> #define Spi_send_byte(ch) { (SPDR=ch); Spi_wait_spif(); }
> mit
> #define Spi_wait_spif() { while ((SPSR & (1<<SPIF)) == 0); }
>
> wenn ich nun den Disassembler von AVR-Studio nutze, so wird mit für
> einen Aufruf Spi_send_byte(u_data.b[1]); der folgende Code angezeigt:
>
> LDI   R30, 0x4E
> LDI   R31, 0x00
> LDD   R24, Y+3
> STD   Z+0, R24
> LDI   R30, 0x4D
> LDI   R31, 0x00
> LDD   R24, Z+0
> TST   R24
> BRGE  PC-0x04
>
> Ist der erzeugte Code sinnvoll? Gehts kürzer, schneller? Warum wird hier
> etwas in die Register R30 und R31 geladen und dann nicht verwendet?

Allein mit dieser Aussage solltest du die Idee von Assembler
gleich wieder aufgeben. R30 + R31 werden verwendet. Sie bilden
das Z-Register und das wird hier
   STD   Z+0, R24
benutzt.

> Bringt es was, mich in das Thema Inline-Assembler einzuarbeiten, oder
> hol ich damit nicht viel raus, weil der AVR-GCC gut genug ist?

Den einen oder anderen Takt wirst du damit sicherlich einsparen können.
Alleine der Aufwand steht bei einem größeren Projekt selten dafür.

Was man aber machen kann: Man kann sich ansehen, was der Compiler
mit bestimmten Konstrukten macht und dann nach Wegen suchen, den
Compiler dazu zu bringen, schnellere Konstrukte zu verwenden.
Gerade wenn es um die Verwendung von unterschiedlichen Datentypen
(int - uint8_t - long) in ein und demselben Ausdruck geht, hat der
gcc noch Potential. Aber auch hier wieder: Ein gewisses Verständnis
für Assembler ist unabdingbar. Genauso wie man analysieren muss,
warum der Compiler welche Codesequenz genauso übersetzt hat, wie
er das hat.

von Hazel (Gast)


Lesenswert?

Ja, da hast du schon Recht. Hier muss er wohl warten.
Hab aber auch keine Idee, wie ich das sonst machen soll. Es folgt ja 
danach sofort ein weiteres Byte, welches per SPI übertragen wird, da 
muss er vorher das eine fertiggestellt haben. Aber CAN hab ich z.B. mit 
Interrupts programmiert.

von Karl H. (kbuchegg)


Lesenswert?

Hazel wrote:
> Ja, da hast du schon Recht. Hier muss er wohl warten.
> Hab aber auch keine Idee, wie ich das sonst machen soll. Es folgt ja
> danach sofort ein weiteres Byte, welches per SPI übertragen wird, da
> muss er vorher das eine fertiggestellt haben.

Richtig.
Aber du musst ja nicht nach dem Start der Übertragung warten bis
die Übertragung fertig ist. Das Übertragen macht doch die
Hardware ganz von alleine. Der Proz kann in der Zwischenzeit
ja ganz was anderes machen (zb. das nächste Byte besorgen).
Was du doch in Wirklichkeit machen musst ist: Vor der eigentlichen
Übertragung sicherstellen, dass die SPI Einheit auch dazu bereit
ist.

void Spi_send_byte(ch)
{
  while ((SPSR & (1<<SPIF)) == 0)
    ;
  SPDR = ch;
}

und schon wartet der Proz nur noch dann, wenn es unbedingt
notwendig ist. Wird Spi_send_byte betreten und die vorhergehende
Übertragung ist schon eredigt, gibt es auch nichts zu warten.

von Hazel (Gast)


Lesenswert?

Das ist gut. Und ich habe einfach dem vorgefertigten Macro aus der 
spi_drv.h vertraut. werde das gleich mal ändern. Danke.
Also wenn das so ist, dann werde ich die Idee mit Inline-Assembler doch 
wieder verwerfen.

von Karl H. (kbuchegg)


Lesenswert?

Hazel wrote:
> Das ist gut. Und ich habe einfach dem vorgefertigten Macro aus der
> spi_drv.h vertraut.

Vorsicht mit dem Makro.
Wenn du dort die Reihenfolge umdrehst ergibt sich nicht
unbedingt das was du haben willst!

Eine Funktion ist besser.

#define Spi_send_byte(ch) { Spi_wait_spif(); (SPDR=ch); }
#define Spi_wait_spif() { while ((SPSR & (1<<SPIF)) == 0); }

 Spi_send_byte(u_data.b[1]);

Werden die Makros aufgelöst, ergibt sich:

{ Spi_wait_spif(); (SPDR=u_data.b[1]); }

Und damit erfolgt die Array-Adress Berechnung erst recht wieder
nach der Warte-Abfrage. Ich möchte die Adressberechnung auch
noch vor die Abfrage haben. Eine Funktion gewährleistet mir
das (selbst wenn sie inline aufgelöst wird): Argumente müssen
vor Betreten der Funktion bestimmt werden. Inline ändert daran
nichts.

Wenns unbedingt ein Makro sein muss (wovon ich eigentlich
abrate, dann so:

#define Spi_send_byte(ch) { char c = (ch); Spi_wait_spif(); SPDR = c; }

Das zwingt wieder den Compiler den Ausdruck u_data.b[1] zu bestimmen,
bevor die Warterei begonnen wird.

von Peter D. (peda)


Lesenswert?

Hazel wrote:
> Ich arbeite an einem Projekt mit einem AT90CAN128 AVR Controller. Da der
> Controller mit den bisherigen Aufgaben (SPI-Datenausgabe und
> CAN-Datentransfer) schon an seine Leistungsgrenze gerät

Pauschal kann man dazu garnichts sagen.

Du mußt erstmal feststellen, wo der Flaschenhals ist.

Hast Du ne hohe Peaklast oder Dauerlast.
Ne Peaklast kann man oft durch Pufferung entschärfen.

Hast Du Routinen, wo die CPU >10% verbräht?
Muß man die so oft aufrufen oder kann man die seltener aufrufen?

Kann man Ergebnisse zwischenspeichern, die oft neu berechnet werden?

Wenn Du nicht genau weißt, ob eine Routine ein Flaschenhals ist, setze 
eine Portpin am Anfang und lösche ihn am Ende.
Dann häng ein Multimeter dran und dann entspricht VCC = 100% CPU-Last.


Peter

von Hazel (Gast)


Lesenswert?

Danke nochmals an Karl Heinz. Ich wär genau wieder in die Falle getappt. 
:-)

Es ist so: Der Controller kommuniziert mit einem FPGA über ein 
Dual-Port-RAM. Das FPGA liefert mit einer Taktrate von 12,5kHz neue 
Daten und legt diese in das Dual-Port-RAM. Der AVR soll nun diese Daten 
vom Dual-Port-RAM holen und dann über seine CAN-Schnittstelle ausgeben 
(oder aber einen Datensatz über die SPI-Schnittstelle). Es handelt sich 
jeweils um drei Datensätze á 16 Bit. Also 6 Byte, die sogar über eine 
CAN-Botschaft übertragen werden können. Den Zugriff auf das 
Dual-Port-RAM, welches übrigens auch im FPGA realisiert ist, erledige 
ich mit einfachem Setzen und Rücksetzen von Pins (Steuerleitungen) sowie 
dem Setzen eines Adressbusses und dem Lesen von einem 8 bit Datenbus. 
Dann werden die Daten per CAN übertragen. Einfach alle 6 Byte in ein 
Datenpaket und mit einem Identifier versehen. Der CAN ist mit 1MBit/s 
konfiguriert. Leider schafft der AVR diese Abarbeitung nicht in den 
80µs, die er dafür Zeit hat und ich kann nur jeden zweiten Datensatz per 
CAN übertragen.
Sind meine Erwartungen zu hoch, oder sollte er das locker schaffen?

von Falk B. (falk)


Lesenswert?

@ Hazel (Gast)

>Dann werden die Daten per CAN übertragen. Einfach alle 6 Byte in ein
>Datenpaket und mit einem Identifier versehen. Der CAN ist mit 1MBit/s
>konfiguriert. Leider schafft der AVR diese Abarbeitung nicht in den
>80µs, die er dafür Zeit hat und ich kann nur jeden zweiten Datensatz per
>CAN übertragen.
>Sind meine Erwartungen zu hoch, oder sollte er das locker schaffen?

80us sind bei 16 MHz 1280 Takte. Das sollte reichen.

MFG
Falk

von Peter D. (peda)


Lesenswert?

Hazel wrote:

> Dann werden die Daten per CAN übertragen. Einfach alle 6 Byte in ein
> Datenpaket und mit einem Identifier versehen. Der CAN ist mit 1MBit/s
> konfiguriert. Leider schafft der AVR diese Abarbeitung nicht in den
> 80µs, die er dafür Zeit hat und ich kann nur jeden zweiten Datensatz per
> CAN übertragen.
> Sind meine Erwartungen zu hoch, oder sollte er das locker schaffen?


Der CAN-Bus hat nicht 1MBit Nutzdatentrate, da geht noch was für 
Identifier und Protokoll drauf.
6 Byte sollten etwa 120µs brauchen (ohne andere CAN-Sender und ohne 
Retry!).

Und selbst eine 10GHz CPU kann 120µs nie in 80µs schaffen!

Umgekehrt ist aber auch schon heftig knack auf knirsch.


Peter

von Castlerock (Gast)


Lesenswert?

Kurz gefast: Nein es geht nicht !!

Ich erkläre - den AVR schaft es vielleicht den Daten zu holen und 
bereitzustellen, dein CAN-bus aber nicht!  Selbst bei 1Mb/s ist jeder 
bit 1us lang.  Also 6 data bytes = CAN frame länge 88 bits (44+8n, 
n=6bytes).
das heist mit 100% buslast (was sowieso nicht auf dauer funktioniert) 
ist es nür möglich jeder 91us (88 + 3bit IFS, Bit-stuffing habe ich noch 
ignoriert) ein neue DataFrame zu senden.

Um dieser datenmenge zu senden muss du eine andere kommunikationsmedium 
benutzen, welche überlass ich dir.



***********************************************************
"Make it idiot-proof, and someone will make a better idiot"

von Hazel (Gast)


Lesenswert?

Das ist eine klare Antwort. Danke. Warum hab ich das eigentlich noch 
nicht selbst berechnet?! Jetzt weiß ichs ja. Also mit Optimieren des 
Codes geht also nichts mehr.

von Kai G. (runtimeterror)


Lesenswert?

>Wenn Du nicht genau weißt, ob eine Routine ein Flaschenhals ist, setze
>eine Portpin am Anfang und lösche ihn am Ende.
>Dann häng ein Multimeter dran und dann entspricht VCC = 100% CPU-Last.

Geniale Idee, danke! Hmm analoges VU-Meter dran gefiele mir auch ;)
Hat dann was von 'nem Drehzahlmesser.

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.