RAM-Bankswitching - wie am geschicktesten mit GCC auf AVR?

#1203320
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.
#1203341
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.
Persönliche Seite #1203366
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.
#1203425
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.
#1203452
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)
#1203470
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.
#1203473
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.
#1203489
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ß
#1203494
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.
#1203514
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.
#1203588
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
}
#1203600
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.
#1203604
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).
#1203615
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!
#1203620
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.
#1203651
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.
#1203659
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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren