Forum: Mikrocontroller und Digitale Elektronik RAM-Bankswitching - wie am geschicktesten mit GCC auf AVR?


von Sven S. (schwerminator)


Lesenswert?

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.

von Benedikt K. (benedikt)


Lesenswert?

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.

von Michael G. (linuxgeek) Benutzerseite


Lesenswert?

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.

von Sven S. (schwerminator)


Angehängte Dateien:

Lesenswert?

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.

von Benedikt K. (benedikt)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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));

von Sven S. (schwerminator)


Lesenswert?

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)

von (prx) A. K. (prx)


Lesenswert?

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.

von Benedikt K. (benedikt)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

von Simon K. (simon) Benutzerseite


Lesenswert?

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).

von Sven S. (schwerminator)


Lesenswert?

@ 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ß

von (prx) A. K. (prx)


Lesenswert?

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.

von Benedikt K. (benedikt)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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;

von Sven S. (schwerminator)


Lesenswert?

@ 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
}

von Benedikt K. (benedikt)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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).

von Sven S. (schwerminator)


Lesenswert?

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!

von Benedikt K. (benedikt)


Lesenswert?

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.

von Sven S. (schwerminator)


Lesenswert?

@ 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.

von Benedikt K. (benedikt)


Lesenswert?

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.

von Sven S. (schwerminator)


Lesenswert?

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.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.