Forum: Mikrocontroller und Digitale Elektronik struct array in Flash setzen


von Sam .. (sam1994)


Lesenswert?

Hi

Ich habe derzeit eine struct die bisher 4 Byte groß ist, sich aber 
später verdoppeln bis vervierfachen wird. Außerdem wird diese so 30 mal 
benötigt.
16 * 30 = 480 Byte -> für den Mega8 verkraftbar, aber da diese alle 
Konstant sind, wäre es doch viel besser sie in den Flash zu verlagern. 
Da habe ich aber ein Problem: Ich habe eine Methode die die Struct 
initialisiert und die Werte setzt, aber wie mache ich das direkt, ich 
kann ja nicht die werte erst auf dem µC schreiben.

Das ist die Struct:
1
typedef struct 
2
{
3
    uint16_t time;
4
    uint16_t bonus;
5
} Game;

Muss ich das auslesen über Pointer machen, oder gibts da ein Trick wie 
man  direkt Structs aus dem Flash lesen kann?

von Bernhard M. (boregard)


Lesenswert?

Hi,

initialisieren (z.B. global oder static oder beides...):
1
static Game myGames[2] PROGMEM = { {1, 50}, {2, 100}};

auslesen geht nicht so direct, z.B.:
1
uint16_t time = pgm_read_word (&(myGames[1].time));
2
uint16_t bonus = pgm_read_word (&(myGames[1].bonus));

Gruß,
Boregard

von Sam .. (sam1994)


Lesenswert?

Danke, das funktioniert. Kannst du mir auch sagen wie ich da noch ein 
Array reinpacke und initialisiere? Denn wenn ich einfach
1
uint16_t* MeinArray;
will ich später ja die einzelnen Einträge wieder herauslesen:
1
Game GetGame(uint8_t modi)
2
{
3
    Game temp;
4
    temp.time = pgm_read_word (&(Games[modi].time))
5
    temp.bonus = pgm_read_word (&(Games[modi].bonus));
6
    temp.options = pgm_read_byte(&(Games[modi].options));
7
    for(int i = 0; i < ((temp.options << 5) & 0x1F); i++)
8
    {
9
        temp.MeinArray[i] = pgm_read_word(&(Games[modi].MeinArray[i]));
10
    } 
11
    return temp;
12
}

Die letzten 3 Byte in options bestimmen dabei die größe des Arrays.
Allerdings: Muss ich das Array nicht vorher initialisieren? So geht es 
aber nicht:
1
temp.MeinArray = new uint16_t[((temp.options << 5) & 0x1F)];

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:
> Danke, das funktioniert. Kannst du mir auch sagen wie ich da noch ein
> Array reinpacke und initialisiere? Denn wenn ich einfach
>
1
> uint16_t* MeinArray;
2
>

Das ist aber kein Array.
Das ist ein Pointer!

Du kannst natürlich diesen Pointer auch dazu benutzen, ihn auf ein 
Array, welches seinerseits im Flash liegt, zeigen zu lassen.

Dann musst du mittels pgm_readword zuerst den Pointer aus dem Flash 
holen und den dann mit den notwendigen Offsets dazu benutzen um die 
eigentlichen Daten zu lesen. Ist ein 2-stufiger Prozess, aber machbar.
1
typedef struct 
2
{
3
    uint16_t  time;
4
    uint16_t  bonus;
5
    uint16_t *data;
6
} Game;
7
8
uint16_t data1[] PROGMEM = { 5, 6, 7, 8 };
9
uint16_t data2[] PROGMEM = { 10, 11, 12 };
10
11
Game myGames[2] PROGMEM =
12
   { {1, 50, data1},
13
     {2, 100, data2},
14
   };

von Sam .. (sam1994)


Lesenswert?

Gut ich hab das jetzt so gemacht.
1
Game GetGame(uint8_t modi)
2
{
3
    Game temp;
4
    temp.time = pgm_read_word (&(Games[modi].time));
5
    temp.bonus = pgm_read_word (&(Games[modi].bonus));
6
    temp.options = pgm_read_byte(&(Games[modi].options));
7
    if((temp.options << 5) & 0x1F > 0)                      //Zeile 222
8
    {
9
        uint16_t *ptr = pgm_read_word(&(Games[modi].data)); //Zeile 224
10
        temp.data = *ptr;                                   //Zeile 225
11
    }
12
    return temp;
13
}

Da kommen aber 3 Warnungen,  außerdem funktioniert mein Programm nur 
halber. Ich wollte jetzt nur wissen, ob das obrige stimmt, und ich den 
Fehler im rest des Progs suchen muss.

Das sind die Warnungen:

222 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] suggest 
parentheses around comparison in operand of &

224 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] initialization 
makes pointer from integer without a cast

225 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] assignment makes 
pointer from integer without a cast

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:

>         uint16_t *ptr = pgm_read_word(&(Games[modi].data)); //Zeile 224
>         temp.data = *ptr;                                   //Zeile 225

Wieso * ?
ptr ist bereits die Adresse im Flash an der die Daten stehen.
Wenn du an die eigentlichen Array-Daten heranwillst, dann musst du 
diesen Pointer seinerseits wieder in eine pgm_read_ ... Funktion 
stecken.


> Da kommen aber 3 Warnungen,

dann geh sie der Reihe nach durch, lies die Warnung, denk darüber nach 
was dir der COmpiler sagen will und korrigiere entsprechend.

> halber. Ich wollte jetzt nur wissen, ob das obrige stimmt,

das kommt drauf an, wie du temp.data weiter benutzt.


> 222 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] suggest
> parentheses around comparison in operand of &

Da gehts um die Abfrage im if
  * bist du sicher, dass du ein &  haben willst
    oder doch eher ein &&

> 224 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] initialization
> makes pointer from integer without a cast

Rechts vom = steht ein INteger (nämlich das Ergebnis von pgm_read_word), 
rechts steht eine Pointer Variable. Ohne casting ist so etwas illegal. 
Man kann nicht einfach einem Pointer eine Zahl zuweisen

> 225 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] assignment makes
> pointer from integer without a cast

Dasselbe in Grün, wobei ich mir ziemlich sicher bin, das du das so 
eigentlich gar nicht willst.

von Sam .. (sam1994)


Lesenswert?

Danke, ich hab nicht drangedacht das data ja ein pointer ist. Deswegen 
wollte ich den Wert zuweisen.

Ich kann das doch einfach korriegieren indem ich *temp.data schreibe.

Die IF Abfrage ist so beabsichtigt, die soll nämlich die letzten 3 Bytes 
rauspicken. Die sagen nämlich wie groß das Array ist. die IF prüft nur 
ob im Array Elemente sind. Wenn die letzten 3 Bytes 0 sind, ist data = 
0. Das hab ich von http://visual-c.itags.org/visual-c-c++/280042/. Das 
hab ich dann (hoffentlich richtig) für das splitten in 5 und 3 Bytes 
umgeändert.

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:

> Ich kann das doch einfach korriegieren indem ich *temp.data schreibe.

Nein.
data ist ein Pointer ins Flash!
Um an die Werte zu kommen, musst du pgm_read_xxxx benutzen!

> Die IF Abfrage ist so beabsichtigt, die soll nämlich die letzten 3 Bytes
> rauspicken.

Bytes?

> Die sagen nämlich wie groß das Array ist. die IF prüft nur
> ob im Array Elemente sind. Wenn die letzten 3 Bytes 0 sind, ist data =
> 0. Das hab ich von http://visual-c.itags.org/visual-c-c++/280042/. Das
> hab ich dann (hoffentlich richtig) für das splitten in 5 und 3 Bytes
> umgeändert.

Mir ist absolut nicht klar, was du da eigentlich machen willst. Ich 
denke aber nicht, das dein Code das macht, was du eigentlich machzen 
möchtest. Es ist immer gefährlich Code, den man nicht versteht einfach 
so zu übernehmen.
Aber auf jeden Fall

    if((temp.options << 5) & 0x1F > 0)

Dir ist klar, dass > eine höhere 'Wertigkeit' hat als & ?
Der COmpiler sieht das daher als

    if((temp.options << 5) & (0x1F > 0))

und ich denke nicht, das du das haben willst.

von Sam .. (sam1994)


Lesenswert?

Ach was schreib ich. Es muss doch
1
*temp.guillotine = pgm_read_word(ptr);
heißen, oder stimmt das auch nicht.

EDIT: Sorry ich war z langsam um das zu kapieren.

Karl heinz Buchegger schrieb:
> if((temp.options << 5) & (0x1F > 0))
>
> und ich denke nicht, das du das haben willst.

Nein, möchte ich nicht. Danke für dne Hinweis.

Das Option Byte gibt in den ersten 5 Bit versch. Optionen an. Der 
restlichen 3 Bit geben die Größe des Arrays an. Übrigens hab ich den 
Code ohne das > 0 in einem Interpreter getestet.

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:
> Ach was schreib ich. Es muss doch
>
1
> *temp.guillotine = pgm_read_word(ptr);
2
>
> heißen, oder stimmt das auch nicht.

Der * da vorne sieht unlogisch aus.

> Nein, möchte ich nicht. Danke für dne Hinweis.
>
> Das Option Byte gibt in den ersten 5 Bit versch. Optionen an. Der
> restlichen 3 Bit geben die Größe des Arrays an. Übrigens hab ich den
> Code ohne das > 0 in einem Interpreter getestet.

Ohne den tatsächlichen Vergleich > 0 ändert sich auch die Interpretation

von Sam .. (sam1994)


Lesenswert?

Wenn ich aber das jetzt so schreibe kommen wieder diese Warnungen. 
Irgendwie versteh ich das nicht.

224 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] initialization
makes pointer from integer without a cast

225 D:\Projekte\Atmega\Chessclock\Dev\chess.c [Warning] assignment makes
pointer from integer without a cast
1
uint16_t *ptr = pgm_read_word(&(Games[modi].data));
2
temp.data= pgm_read_word(ptr);

Ich habe vorher nicht wirklich gesagt was ich mit dem Code vorhabe: Ich 
möchte mit dem Code den Teil vom Flash in den Ram kopieren, so benötige 
ich nur den Speicherplatz der Struct Game.

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:
> Wenn ich aber das jetzt so schreibe kommen wieder diese Warnungen.
> Irgendwie versteh ich das nicht.
>

Was gibt es daran nicht zu verstehen?
Welchen Datentyp hast du links vom =
Welchen Datentyp hast du rechts vom =


Rechts vom = steht ein Integer (nämlich das Ergebnis von pgm_read_word),
rechts steht eine Pointer Variable. Ohne casting ist so etwas illegal.
Man kann nicht einfach einer Pointer Variablen eine Zahl zuweisen.


Das ist ein Datentypproblem! Du versuchst einer Pointervariablen einen 
Nichtpointerzuzuweisen. Und das geht nun mal nicht einfach so.

  temp.data = (uint16_t*)pgm_read_word(&(Games[modi].data));

von Sam .. (sam1994)


Lesenswert?

Um das Zeug dann in den Ram zu kopieren, muss ich dann doch jedes elemnt 
lesen und den Zeiger dem Temp.data zuweisen, oder ?

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:
> Um das Zeug dann in den Ram zu kopieren, muss ich dann doch jedes elemnt
> lesen und den Zeiger dem Temp.data zuweisen, oder ?

Welches Zeugs?

Die Array Daten?

Ja, das musst du. Aber erst mal brauchst du den Pointer darauf.
Und du brauchst Speicher, an dem du die Array Daten ablegst. Woher weißt 
du wie groß das Array werden wird?

Daher: Es ist eventuell nicht so schlau, das komplette Array in den SRAM 
zu kopieren. Hol dir in der Verarbeitung des Arrays das jeweils 
benötigte Array Element aus dem Flash. Damit umgehst du eine Menge 
Probleme.

von Sam .. (sam1994)


Lesenswert?

Karl heinz Buchegger schrieb:
> Hol dir in der Verarbeitung des Arrays das jeweils
> benötigte Array Element aus dem Flash.

Das ganze wird ja eine Schachuhr. Problem: Es gibt voreingespielte 
Zeit-Modi und Manuelle. Deswegen wird die Zeit erst in den Ram kopiert, 
damit sie da beim manuellen Modus noch verändert werden kann. Die 
Methode GetGame  (in C eher funktion) soll einfach ein Game im Ram 
anlegen und den gewünschten Zeitmodi vom Flash darauf kopieren. Das 
Array wird höchtens 7 * 16bit groß, da man nur 3bits zur Größenangabe 
hat.

Also ich muss:
1. Zeiger aus dem Flash lesen, der auf das Flash-Array zeigt und das 
ganze zu einem Zeiger casten:
1
uint16_t *ptr = (uint16_t*)pgm_read_word(&(Games[modi].data));
2. Die einzelnen Einträge im Array auslesen damit sie im Ram sind und 
die Adresse vom ersten Eintrag speichern
1
uint16_t value[7];
2
for(int i = 0; i<((temp.options << 5) & 0x1F);i++)
3
{
4
    value[i] = pgm_read_word(ptr + 2*i);
5
    if(i == 0)
6
        temp.data= (uint16_t*)&value;
7
}
8
return temp;
3. Über temp.data müsste man dann auf das Array zugreifen können.

MAn könnte das auch besser mit malloc machen, aber damit hab ich noch 
nie was gemacht.

Stimmt das so?

von shift (Gast)


Lesenswert?

Mal ganz am Rande, (temp.options << 5) & 0x1F ist immer 0.

von Sam .. (sam1994)


Lesenswert?

@shift
Danke, habs jetzt auch gemerkt.
Ich denke es muss (x << 5) & 224 heißen. bzw. 0xE0 in Hexa

Außerdem muss irgendwas gewaltig falsch sein: Wenn ich game_mode = 1 
nehme, ist die Zeit vom 1. Spieler witklich game_mode = 1 und die vom 
zweiten game_mode = 2.
1
void MainMenu(void)
2
{
3
    Game temp;
4
    SetAll(0);
5
    uint8_t key = 255;
6
    while(1)
7
    {
8
        Show2Digits(game_mode,0);
9
        key = GetKey();      
10
        switch(key)
11
        {
12
            case 6: 
13
                if(game_mode > 1)
14
                    game_mode--; 
15
                 break;
16
            case 5:
17
                temp = GetGame(game_mode);
18
                PlayManual(temp,temp);
19
                break;
20
            case 4:
21
                if(game_mode < MODI_COUNT)
22
                    game_mode++;
23
                break;
24
        };
25
    }
26
}
27
28
Game GetGame(uint8_t modi)
29
{
30
    Game temp;
31
    temp.time = pgm_read_word (&(Games[modi].time));
32
    temp.bonus = pgm_read_word (&(Games[modi].bonus));
33
    temp.options = pgm_read_byte(&(Games[modi].options));
34
    uint16_t *ptr = (uint16_t*)pgm_read_word(&(Games[modi].guillotine));
35
    uint16_t value[7];
36
    for(int i = 0; i<((temp.options << 5) & 224);i++)
37
    {
38
        value[i] = pgm_read_word(ptr + 2*i);
39
        if(i == 0)
40
             temp.guillotine = (uint16_t*)&value;
41
    }
42
    return temp;
43
}

irgendwo muss da ein Zeiger erhöht werden, so dass er dann auf den 
nächsten Modi zeigt. Oder ich übersehe wiedermal einen Fehler den jeder 
auf Anhieb sieht ;)

von Sam .. (sam1994)


Angehängte Dateien:

Lesenswert?

Falls ihr den ganzen Code sehen wollt...

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:

> irgendwo muss da ein Zeiger erhöht werden, so dass er dann auf den
> nächsten Modi zeigt. Oder ich übersehe wiedermal einen Fehler den jeder
> auf Anhieb sieht ;)


Tu dir selbst einen Gefallen und verwende konstant dimensionierte 
Arrays.
1
struct game {
2
  uint16_t  member1;
3
  uint16_t  member2;
4
  uint16_t  used_entries;
5
  uint8_t   array[30];
6
};
7
8
struct game Games[] PROGMEM = {
9
10
   { 5, 8,   3, { 10, 20, 30 } },
11
   { 0, 1,   5, { 1, 2, 3, 4, 5 } },
12
};

Da Dynamik reinzubringen lohnt nicht (Flash hast du genug) und im SRAM 
mit dynamischer Allokierung rumzumurksen noch viel weniger.

Du bist noch nicht soweit um da mit Verpointerungen komplexere 
Datenstrukturen aufzubauen. Ganz abgesehen lohnt das nicht wegen 7 
Integer. Da kostet dich die Verwaltung mehr, als du mit dynamisch 
verpointerten Arrays sparen kannst.

PS: Zum Einlesen aus dem Flash baust du dir eine pgm_read_block Funktion 
nach dem Vorbild der sinngemäß gleichnamigen eeprom Funktion. Du willst 
das so benutzen
1
....
2
3
  struct game  currentGame;
4
5
  pgm_read_block( &currentGame, Games[0], sizeof( Games[0] ) );

pgm_read_block kopiert einfach die angegebene Anzahl Bytes aus dem Flash 
(adresse davon im 2.ten Argument) ins SRAM (Adresse davon im 1.ten 
Argument)
1
void pgm_read_block( void * dest, void * source, size_t byteCnt )
2
{
3
  uint8_t destPtr = (uint8_t *)dest;
4
  uint8_t srcPtr  = (uint8_t *)source;
5
  size_t i;
6
7
  for( i = 0; i < byteCnt; ++i )
8
    *destPtr++ = pgm_read_byte( srcPtr++ );
9
}

von Karl H. (kbuchegg)


Lesenswert?

1
    for(int i = 0; i<((temp.options << 5) & 224);i++)
Hör auf zu künsteln, wenn du nicht verstehst was das da macht.
Schreibs konventionell. Die paar Bytes für einen anständigen uint8_t 
Zähler hast du allemal. Du brauchst da nicht in einem options Feld 3 
Bits zu belegen.

von Sam .. (sam1994)


Lesenswert?

OK, ich gebe mich für den Anfang geschlagen, aber sobald ich das Projekt 
fertig hab würde ich es lieber so umändern wie ich es angefangen habe. 
Das ich nicht soweit bin kann schon sein, ist schließlich mein 1. 
größeres Projekt. Ich hab davor immer auf dem PC programmiert und musst 
bis auf einmal nie Speicherplatz sparen.

Ja es sind 7 Words, wo von aber von den 30 Einträge durschnittlich ein 
halber genutzt wird. 6.5  30  2 = 390 Byte einsparung. Nicht viel, 
aber dazu kommt später noch ein Array gleicher Größe aber mit Bytes. -> 
390 + 6.5 * 30 = 585 Byte. Immerhin 1/16 des Mega8.

Also ich würde mich freuen wenn es mir einmal anhand eines Beispiels 
zeigen würde, wie es geht. Es muss nicht mal auf mein Projekt 
abgerichtet sein.

Außerdem wundere ich mich immernoch, warum auf der Player2 Anzeige, wenn 
ich game_mode 1 einstelle, gamemode 2 angezeigt wird.

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:

> Ja es sind 7 Words, wo von aber von den 30 Einträge durschnittlich ein
> halber genutzt wird. 6.5  30  2 = 390 Byte einsparung. Nicht viel,
> aber dazu kommt später noch ein Array gleicher Größe aber mit Bytes. ->
> 390 + 6.5 * 30 = 585 Byte. Immerhin 1/16 des Mega8.

Du kriegst von Atmel kein Geld für nicht benutzten Speicher zurück.

Wenn du die Sache mit der Bitextraktion aus dem options Feld richtig 
gemacht hättest, hätt ich mich breit schlagen lassen. Aber so ist das 
momentan sinnlos. Da eröffnen wir einen Mehrfrontenkrieg, den du noch 
nicht gewinnen kannst.
Der Schlüssel: mal dir die Situation auf Papier auf.

von Sam .. (sam1994)


Lesenswert?

Ja, ich habe den verwechselt das die bits von rechts beginnen es muss 
natürlich & 7 heißen.

Was heißt eigentlich das:

250 D:\Projekte\Atmega\Chessclock\Dev\chess.c invalid type argument of 
'unary *' (have 'int')

Das kommt bei deinem Code.

von Karl H. (kbuchegg)


Lesenswert?

Samuel K. schrieb:

> 250 D:\Projekte\Atmega\Chessclock\Dev\chess.c invalid type argument of
> 'unary *' (have 'int')

Ich hab jetzt die Codestelle nicht rausgesucht.
Aber schaun wir mal, ob wir das auch nicht so klären können.

unary *
Was ist ein 'unary *'
Da geht es offenbar um den Operator *

Normalerweise bezeichnet der eine Multiplikation.

   a * b

und weil da links und rechts vom Operator jeweils ein Operand steht, 
also 2 Operanden vorhanden sind, bezeichnet man ihn als 'binary *'. Ein 
unary * ist das Gegenteil davon: Da hat der * nur 1 Operanden.

Das ganze ist wie bei -

  j = a - b;       das ist ein binary -
  j = -a;          hier handelt es sich um ein unary -

Wo kann ein unary * überhaupt vorkommen?
Na, bei der Dereferenzierung eines Pointers.

   i = *Ptr;

i erhält den Wert, auf den Ptr zeigt.

da ist ein unary * im Spiel. Und klarerweise muss Ptr auch ein Pointer 
Datentyp sein.

    i = *5;

ergibt keinen Sinn. 5 ist ein int und kein Pointer. Einen int kann man 
nicht dereferenzieren.

Nochmal zurück zur Fehlermeldung

invalid type argument of 'unary *' (have 'int')

Aha. unary * wird falsch verwendet. Wir wissen das er nur auf einen 
Pointer angewendet werden kann. Der Compiler gibt noch einen Hinweis: 
have int.

Anstelle eines Pointer Datentyps wurde also ein integer Datentyp 
angegeben.

Nach dem Vorgeplänkel sehen wir uns den Code an:
1
void pgm_read_block( void * dest, void * source, size_t byteCnt )
2
{
3
  uint8_t destPtr = (uint8_t *)dest;
4
  uint8_t srcPtr  = (uint8_t *)source;
5
  size_t i;
6
7
  for( i = 0; i < byteCnt; ++i )
8
    *destPtr++ = pgm_read_byte( srcPtr++ );
9
}

Es gibt nur einen unary *. Nämlich bei *destPtr
Und wir wissen auch schon, das destPtr ein Pointer sein müsste. Müsste?
1
  uint8_t destPtr = (uint8_t *)dest;
Hoppla! Da hab ich dann wohl einen Fehler beim Tippen gemacht und den 
Pointer * in der Definition vergessen. Beim anderen Pointer ditto.
1
void pgm_read_block( void * dest, void * source, size_t byteCnt )
2
{
3
  uint8_t* destPtr = (uint8_t *)dest;
4
  uint8_t* srcPtr  = (uint8_t *)source;
5
  size_t i;
6
7
  for( i = 0; i < byteCnt; ++i )
8
    *destPtr++ = pgm_read_byte( srcPtr++ );
9
}

Man beachte auch, wie jetzt plötzlich auch die Datentypen bei der 
Initialisierung von destPtr und SrcPtr übereinstimmen. Rechts vom = 
steht ein Ausdruck der einen Pointer ergibt, links vom = steht eine 
Variable, die ein Pointer ist.

von Sam .. (sam1994)


Lesenswert?

Danke, es funktioniert jetzt.

Karl heinz Buchegger schrieb:
> Du kriegst von Atmel kein Geld für nicht benutzten Speicher zurück.

Das schon, aber ehrlich gesagt hab ich auch keine Lust am Ende evl. 
jedes Byte rauszukürzen um das Programm fertigzustellen.

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.