Hallo,
ich benötige für mein aktuelles Projekt relativ viel Speicher. Ich habe
erstmal großzügig geplant, sodass ich nun ein 512kB ansynchrones RAM mit
8Bit Speicherzellen im Einsatz habe. Ich benutze den ATmega2561 und
dessen XMEM-Interface zur Speicheraufrüstung. Ich führe 15
Addressleitungen an den µC und 4 Bankswitchleitungen, sodass 16
Speicherbänke à 32kB vorhanden sind.
Die Hardware im Detail:
µC: Atmel ATmega2561 @ 16MHz
Latch: 74AC573
RAM: CY7C1049CV
Am Ende möchte ich gern zwei Funktionen habe, in die ich eine Addresse
und einen 8Bit-Wert packe, die dann automatisch die Bank wählen, und an
die gewählte Addresse schreiben bzw. lesen. Das ganze habe ich auch
schon erfolgreich in C implementiert:
1 | void xmem_init(void){
| 2 | XMCRA |= (1<<SRE);
| 3 | XMCRB |= (1<<XMM0);
| 4 |
| 5 | DDRF |= 0xF0;
| 6 | PORTF &= ~0xF0;
| 7 | }
| 8 |
| 9 | void xmem_set(uint32_t address, uint8_t byte){
| 10 | PORTF = (((address/0x8000)<<4) + (PINF & 0x0F));
| 11 | uint8_t *zeiger = (uint8_t *) (XMEM_OFFSET + (address%0x8000));
| 12 | *zeiger = byte;
| 13 | }
| 14 |
| 15 | uint8_t xmem_get(uint32_t address){
| 16 | PORTF = (((address/0x8000)<<4) + (PINF & 0x0F));
| 17 | uint8_t *zeiger = (uint8_t *) (XMEM_OFFSET + (address%0x8000));
| 18 | return *zeiger;
| 19 | }
|
Mein Problem ist, dass ich das Gefühl habe, das Ganze ist sehr sehr
langsam (~10s für einmal vollschreiben und vollständig lesen). Ich habe
nicht so viel Erfahrung im performanten Programmieren, habe jedoch
aufgeschnappt, dass die AVRs mit 32Bit-Werten sehr langsam sind. Ist dem
so? Wie würdet ihr das Ganze lösen? Später möchte ich übrigens den
gesamten Speicherbereich als FIFO verwenden.
Vielen Dank. Gruß Sven.
Sven S. wrote:
> Mein Problem ist, dass ich das Gefühl habe, das Ganze ist sehr sehr
> langsam (~10s für einmal vollschreiben und vollständig lesen).
10s sind sehr lange. (16MHz/512k/2=150 Takte pro Lese/Schreibvorgang).
Poste mal die erzeugte .lst Datei. Ich vermute fast, er verwendet die
Divisionsroutine.
> Ich habe
> nicht so viel Erfahrung im performanten Programmieren, habe jedoch
> aufgeschnappt, dass die AVRs mit 32Bit-Werten sehr langsam sind.
32bit Operationen müssen halt über mehrere 8bit Operationen nachgebildet
werden. Das dauert mindestens mal 4x länger als eine 8bit Rechnung.
> Wie würdet ihr das Ganze lösen?
Gute Frage. Wenn du mehrmals in die selbe Bank schreibst/liest, dann
muss du nicht jedesmal umschalten. Dies müsste man nur intelligent
regeln. Eventuell könnte es sinnvoll sein, einen Block Daten auf einmal
in einen internen Puffer zu lesen.
Sven S. wrote:
> Mein Problem ist, dass ich das Gefühl habe, das Ganze ist sehr sehr
> langsam (~10s für einmal vollschreiben und vollständig lesen).
Lass mal die unnoetige Division bzw. den Modulo weg und miss noch mal.
Aber subjektiv gesehen ist das ganze schon "langsam", ich hab einen
aehnlichen Test bei insg. 128k RAM gemacht und das hat durchaus ein paar
hundert Millisekunden gedauert (allerdings mit Gegentest, also
Vergleichen des Speicherinhaltes mit einem Muster). Du kannst also davon
ausgehen dass es schon ca. 2-3s dauern wird.
Kleiner Hinweis noch: Wenn Du jedes einzelne Byte ueber die Routine
liest ist das natuerlich auch recht langsam. Wenn Du groessere
Datenbereiche auslesen willst waere es effizienter, einen Pointer
entsprechend zu initialisieren und die Bank fuer den gesamten
Lese-/Schreibvorgang auszuwaehlen. Das spart Dir nicht nur den
Funktionsaufruf sondern auch die staendige (und in Deinem Fall
umstaendliche) Berechnung der Adresse.
Du musst nur aufpassen, dass Du nicht wichtige Dinge dabei ausmaskierst,
d.h. ich wuerde die wichtigen sections im internen SRAM lassen, der
Stack muss sowieso dort sein. Dann kannst Du den kompletten externen
Speicher manuell oder mit malloc verwalten, wenn Du den Heap auf den
Beginn des externen Speichers legst.
Hmm also hatte ich mit meinem Eindruck der zu langen Dauer recht. Wenn
keiner einen konkreten Ansatz hat, muss ich wohl nochmal in mich gehen
und gründlich grübeln. Eure Schilderungen sind aber sehr hilfreich!
Benedikt, ich habe mal die *.lst-Datei angehängt.
Eine Division macht er schonmal nicht, aber das Problem liegt hier: 1 | 64 002c EFE0 ldi r30,15
| 2 | 65 002e 3695 1: lsr r19
| 3 | 66 0030 2795 ror r18
| 4 | 67 0032 1795 ror r17
| 5 | 68 0034 0795 ror r16
| 6 | 69 0036 EA95 dec r30
| 7 | 70 0038 01F4 brne 1b
|
Er schiebt 15x den 32bit Wert nach rechts. Dafür benötigt er 15x 7
Takte= 105 Takte.
Wie man das in C hinbekommt, ist eine gute Frage. Ich würde das ganze in
Assembler schreiben, dann sollte das deutlich schneller sein.
PS: Anstelle von PINF solltest du besser PORTF einlesen.
Mal so probieren:
uint8_t page = (uint8_t)(address >> 16) << 5;
if (address & (1<<15)) page |= 1<<4;
PORTF = page; // oder so ähnlich
Mit etwas Glück kriegt er den >>16 optimiert zustande. Wenn die übrigen
4 Pins von PORTF ausschliesslich Eingänge sind, dann wird es
effizienter.
Und:
uint8_t *zeiger = ... ((uint16_t)address % 0x8000));
Leider kann ich gar kein Assembler, ein klares Ausschlusskriterium also
;) Ich habe mir schon ein paar Gedanken zu dem angehenden FIFO gemacht:
1. Die Schreib- und Lesefunktionen brauchen kein Addressparameter, weil
das den aufrufenden Algorithmus gar nicht interessiert (bzw. der sie gar
nicht weiß).
2. Aktuelle Lese- und Schreibaddresse werden global als 16Bit-Wert
gespeichert, zusätzlich in je einer 8Bit-Variable die aktuelle aktive
Bank.
3. Vor jedem Lesen und Schreiben wird überprüft ob die aktuelle Addresse
gleich 2^15 ist, wenn ja, wird die Bankvariable mit mod 4 inkrementiert
und die Addresse genullt. Nach dem Schreiben wird die Addressvariable
inkrementiert.
4. Wenn die Lesevariable und die Banklesevariable gleich denen fürs
Schreiben sind, ist der FIFO leer.
Sind die obigen Überlegungen soweit richtig oder habe ich etwas
übersehen? Grundsätzlich müsste das Ganze doch deutlich schneller sein,
oder?
PS: ups (PORTF statt PINF)
Sven S. wrote:
> wird die Bankvariable mit mod 4 inkrementiert
Du meinst vermutlich MOD 16. Und wenn die Bankvariable wie die Bits im
Port linksbündig sitzt, dann erledigt sich das sowieso von selbst. Nicht
schön aber effizient.
> Sind die obigen Überlegungen soweit richtig oder habe ich etwas
> übersehen? Grundsätzlich müsste das Ganze doch deutlich schneller sein,
> oder?
Nö, wenn der Speicher ohnehin nur sequentiell verwendet wird, dann ist
das sinnvoller. Wär aber übersichtlicher, wenn du für die Adresse eine
struct RAMaddress { uint16_t offset; uint8_t page; };
verwendest.
Der Code von A. K. scheint die Lösung zu sein, zumindest optimiert der
Compiler das bei mir sehr viel besser: 1 | uint8_t page = (uint8_t)(address >> 16) << 5;
| 2 | if (address & 0x8000)
| 3 | page |= 0x10;
| 4 | PORTF = (PORTF & 0x0F) | page;
| 5 | uint8_t *zeiger = (uint8_t *) (XMEM_OFFSET | (address&0x7FFF));
| 6 | *zeiger = byte;
|
Der Code dürfte etwa Faktor 3-4x schneller sein als der alte Code.
@ A. K.
Wie oft hast du den Beitrag eigentlich editiert? Jedesmal wenn ich die
Seite neu geladen habe, sah der Code wieder anders aus ;-)
PS:
Die eigentliche Schwierigkeit ist die etwas ungünstige Pagegröße von nur
32kByte. 64kByte könnte man alleine durch das Verschieben von Bytes
umrechnen.
Ich verwende daher meist nur 256Byte des Adressraums und gebe die
restlichen Adressen per Hand aus. Das geht sehr viel schneller:
1 | typedef union conver_ {
| 2 | unsigned long dw;
| 3 | unsigned int w[2];
| 4 | unsigned char b[4];
| 5 | } CONVERTDW;
| 6 |
| 7 | void xmem_set(uint32_t address, uint8_t byte){
| 8 | CONVERTDW adr;
| 9 | adr.dw=address;
| 10 | PORTF=(PORTF&0x0F)|(adr.b[3]<<4);
| 11 | PORTC=adr.b[2]; //(oder wo auch immer A8-15 liegen)
| 12 | uint8_t *zeiger = (uint8_t *) adr.b[0];
| 13 | *zeiger = byte;
| 14 | }
|
Das dürfte nochmal etwa Faktor 2 schneller sein, als obige Lösung.
Allerdings ist es nicht wirklich portierbar. So wie fast alle auf
Geschwindigkeit optimierten Lösungen.
Ein paarmal schon, erst kam die Idee, dann die Anpassung an den Kontext.
Du hast deshalb die Optimierung der Offsetrechnung übersehen ;-). Es
gibt keinen Grund (address&0x7FFF) 32bittig zu rechnen.
Ein xmega128A1 kann viel und schnell SDRAM ansteuern und lässt sich
ähnlich zu den klassischen AVRs programmieren. (Zumindest ist es nicht
so ein Geschoss wie so manch ein 32 Bit Prozessor).
@ A. K.: Ich meinte natürlich mod 16 ;) Ich musste deinen Beitrag echt
dreimal lesen, um am Ende festzustellen, dass sich das "Nö" auf die
erste Frage bezieht.
Vielen Dank an euch beide. Das hat einmal gezeigt, dass man auch in
diesem Forum schnell zu kompetenten Antworten kommt. Wenn weitere Fragen
auftauchen, melde ich mich nochmal.
Gruß
Benedikt K. wrote:
> PORTF=(PORTF&0x0F)|(adr.b[3]<<4);
> PORTC=adr.b[2]; //(oder wo auch immer A8-15 liegen)
Die Indizes würde ich nochmal überdenken.
Ich bin übrigens nicht sicher, ob die Variante über Speicheradressen via
union schneller ist, als die geschickte Ausnutzung der Optimierung von
Schiebebefehlen. Denn wenn die union im Speicher liegt handelt sie dir
einen Stackframe ein. Bei AVRs nicht zu empfehlen.
Es hilft allerdings, wenn man vom Sourcecode her weiss, dass avr-gcc
Shifts bei 16bit weitaus besser optimiert als bei 32bit. Bei 16bit hat
er für alle Varianten eine eigene Lösung, bei 32bit nur für ein paar
Klassiker wie >>16.
A. K. wrote:
> Die Indizes würde ich nochmal überdenken.
Mist, hatte einen falsch und hab die anderen in die falsche Richtung
korrigiert. So sollte es besser sein: 1 | void xmem_set(uint32_t address, uint8_t byte){
| 2 | CONVERTDW adr;
| 3 | adr.dw=address;
| 4 | PORTF=(PORTF&0x0F)|(adr.b[2]<<4);
| 5 | PORTC=adr.b[1]; //(oder wo auch immer A8-15 liegen)
| 6 | uint8_t *zeiger = (uint8_t *) adr.b[0];
| 7 | *zeiger = byte;
| 8 | }
|
> Ich bin übrigens nicht sicher, ob die Variante über Speicheradressen via
> union schneller ist, als die geschickte Ausnutzung der Optimierung von
> Schiebebefehlen.
Ist sie, definitiv. Daher verwende ich diese Variante vor allem auch zum
Zerlegen von großen Variablen in Bytehäppchen für den UART o.ä..
> Denn wenn die union im Speicher liegt handelt sie dir
> einen Stackframe ein.
Liegt sie nicht. Der Compiler optimiert die union weg und greift direkt
auf die einzelnen Bytes zu, daher ist das Ergebnis meist sogar besser
als bei low+high<<8 oder ähnlichen 2x8 bit <-> 16bit Umwandlungen. Beim
AVR + gcc funktioniert das wunderbar, bei anderen Kombinationen muss man
vorsichtig sein, daher der Hinweis wegen der Portierung.
Noch besser als oben:
1 | uint8_t page = (uint8_t)(address >> 16) << 5;
| 2 | if ((uint16_t)address & 0x8000)
| 3 | page |= 0x10;
| 4 | PORTF = (PORTF & 0x0F) | page;
|
@ Benedikt, ich seh das doch richtig, dass man deine Lösung nur mit 16
Addressleitungen + Bankleitungen realisieren kann, oder?
@ A. K., ich verstehe bei deiner Lösung den Sinn der if-Abfrage nicht.
Könntest du diese näher erleutern, auch stimmen sie Shiftwerte nicht.
Meine an deine Gedanken angepasste Version sieht z.Z. so aus und
erbringt gut 1s Zeitersparnis:
1 | void xmem_set(uint32_t address, uint8_t byte){
| 2 | PORTF = (((uint8_t) (address >> 15) << 4) | (PORTF & 0x0F));
| 3 | uint8_t *zeiger = (uint8_t *) (XMEM_OFFSET + ((uint16_t) address%0x8000));
| 4 | *zeiger = byte;
| 5 | }
|
Sven S. wrote:
> @ Benedikt, ich seh das doch richtig, dass man deine Lösung nur mit 16
> Addressleitungen + Bankleitungen realisieren kann, oder?
Nein.
Das XMEM Interface wird auf 8bit eingestellt. Die restlichen 24bit aus
der long Adresse kann man auf 3 Ports ausgeben. Somit könnte man (mit
gleichem, bzw. sogar weniger Rechenaufwand) 4GByte Adressieren.
> @ A. K., ich verstehe bei deiner Lösung den Sinn der if-Abfrage nicht.
Die ist dazu notwendig, um das 15. Bit wieder herzustellen. Er teilt
erst durch 65536, er lässt also die 2 niederwertigsten Bytes wegfallen,
da dies besser opimiert wird. Allerdings braucht man das 15. Bit, da das
XMEM Interface nur 32768Bytes ansteuert.
Sven S. wrote:
> @ A. K., ich verstehe bei deiner Lösung den Sinn der if-Abfrage nicht.
> Könntest du diese näher erleutern, auch stimmen sie Shiftwerte nicht.
Doch, das passt schon.
> uint8_t page = (uint8_t)(address >> 16) << 5;
A16:A18 landen in page Bits 5-7. Der Rest ist 0.
> if ((uint16_t)address & 0x8000)
> page |= 0x10;
Wenn A15 gesetzt, dann wird page Bit 4 gesetzt. Also sind nun A15:A18 in
page Bits 4-7. Wie gewünscht.
> PORTF = (((uint8_t) (address >> 15) << 4) | (PORTF & 0x0F));
Der Witz meiner Lösung liegt darin, den höchst ineffizienten 32bit shift
um 15 zu vermeiden. Um 16 macht er nämlich durch selektiven Zugriff auf
die beiden oberen Bytes (4 Takte). Um 15 per Schleife (105 Takte).
Benedikt K. wrote:
> Das XMEM Interface wird auf 8bit eingestellt. Die restlichen 24bit aus
> der long Adresse kann man auf 3 Ports ausgeben. Somit könnte man (mit
> gleichem, bzw. sogar weniger Rechenaufwand) 4GByte Adressieren.
Tatsächlich? Hätte ich nicht gedacht. (Stand da nicht eben noch was mit
16MB statt 4GB? ;) )
@ A. K. wrote:
> Der Witz meiner Lösung liegt darin, den höchst ineffizienten 32bit shift
> um 15 zu vermeiden. Um 16 macht er nämlich durch selektiven Zugriff auf
> die beiden oberen Bytes (4 Takte). Um 15 per Schleife (105 Takte).
Jetzt hab ichs verstanden und sehe die Resultate: Wahnsinn, jetzt dauert
der Spaß gerade noch 2,578s. Das ist ja völlig irre!
Sven S. wrote:
> Tatsächlich? Hätte ich nicht gedacht. (Stand da nicht eben noch was mit
> 16MB statt 4GB? ;) )
Ja und ja. Ich hatte von den 24 per Hand adressierten Pins auf 2^24=16M
geschlossen, aber dazu kommen ja noch die 8 per XMEM adressierten, was
4GB ergibt.
Einen LCD Controller mit 2MByte Speicher habe ich so an einen AVR
angeschlossen, das ermöglicht etwa 500-1000kByte/s reine Datenrate.
@ Benedikt, ich wollte grad deine Lösung zum Vergleich ausprobieren,
leider bekomme ich ein Warning, das offenbar zum Komplettabsturz des
Mega führt. Folgende Zeile ist betroffen: 1 | uint8_t *zeiger = (uint8_t *) adr.b[0];
|
Mit folgender Meldung:
> warning: cast to pointer from integer of different size
Ich seh da jetzt aber auf Anhieb keinen Fehler.
Da fehlt noch das Offset, das hatte ich da vergessen hinzuzuaddieren.
Und vergiss nicht beim XMEM Interface die Größe des Adressraums auf 256
Adressen zu verkleinern.
Was der Compiler bemängelt ist folgendes: adr.b[0] ist 8bit groß,
Pointer sind aber int (16bit) groß. Durch die Addition mit dem Offset
sollte der Fehler verschwinden.
Argh, wieso seh ich solche Fehler nicht! :(
Jedenfalls erbrachte mein Test, dass A. K.'s Methode etwas schneller
ist. Der Unterschied ist jedoch mit 0,1s marginal. Danke euch vielmals
;)
PS: Die Addressleitungen hatte ich bereits auf 8 reduziert.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
|