Hallo!
Ich arbeite an einem Projekt mit einem AT90CAN128AVR 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!
>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
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.
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.
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.
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.
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.
Hazel wrote:
> Ich arbeite an einem Projekt mit einem AT90CAN128AVR 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
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?
@ 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
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
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"
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.
>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.