PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Multithreading mit AVRco



Johannes
20.04.2004, 18:48
Moin,
da ich von den AVRs noch nicht so die Ahnung habe, aber demnächst damit anfangen möchte, hier eine Interessefrage:

Wie ich gelesen habe, unterstützt der AVRco Multithreading. Soweit ich das kenne ist dafür aber normalerweise ein Betriebsystem auf dem Chip nötig. Wieso ist das bei den AVRs nicht so? Wird da der Code direkt so "zusammengewurschtelt", dass eine Art Multithreading dabei herauskommt?

Gruß
Johannes

LinuxMan
20.04.2004, 18:54
Wo steht das er Multithreading unterstützt? Controller unterstützen das doch eigentlich nie, höchstens ein Betriebsystem kann das unterstützen.

Johannes
20.04.2004, 18:58
Das steht in der Anleitung zum AVRco:

Was kann der AVRco? Ein Überblick
[...]
Multitasking: Der Compiler ist in der Lage bis zu 15 Tasks und Prozesse zu verwalten. Zur Kommunikation zwischen den Tasks oder Prozessen
stehen Semaphoren und Pipes zur Verfügung.

LinuxMan
20.04.2004, 19:04
Was ist denn "AVRco" überhaupt?

Johannes
20.04.2004, 19:11
Das ist ein Pascal-Compiler für AVRs.

LinuxMan
20.04.2004, 19:27
Ach so! Dachte du redest von reinem AVR Controller. Ein Compiler kann natürlich richtiges Multithreading unterstützen. Er produziert halt zusätzlichen Code eine Art MiniBetriebsystem was er in das generierte Programm einbaut. Dadurch ist normales Multithreading denkbar. Wie sinnvoll das ist, hängt von der Qualität des Compilers ab.
Ein "Nicht Multithreading Programm" kann durchaus effizienter sein

Johannes
20.04.2004, 19:31
>Wie ich gelesen habe, unterstützt der AVRco Multithreading.

Ok, danke. Hatte mich doch gewundert, wie du ja auch eben ;-)

Gruß
Johannes

hrrh
20.04.2004, 19:51
hier wird multithreading mit multiasking verwechselt.
multithreading: alle prozesse werden GLEICHZEITIG verarbeitet. normalerweise nimmt man dafuer mehrere prozessoren (fuer jeden prozess einen)
multiasking: ein prozessor verarbeitet mehrere prozesse scheinbar gleichzeitig, dies wird daduerch erreicht, dass zwischen den prozessen schnell abgewechselt wird ohne dass der anwender das merkt (kann trozdem stoeren).

pbürgi
20.04.2004, 21:58
Seit ihr euch sicher, dass mit AVRs nicht auch Multithreating möglich ist?
Leider finde ich die entsprechende ct grade nicht aber ich hab dieses Jahr erst(, da ich es nicht auf den Archiv-CDs finde) was zu diesem Thema gelesen, wie man selbst ein multithreating realisieren kann. Das war zwar mit einem anderen Mikrocontroller, aber das müsste auch mit nem avr klappen.
Ich glaub man unterbricht mit nem Timer per Interrupts immer den aktuellen Thread und ruft den neuen in der Liste auf, auf diese weise kann man sogar Thread-Prioritäten realisieren.

Johannes
20.04.2004, 22:39
Tja ich weiß es nicht.

Aber das mit Multithreading und Multitasking stimmt so nicht, jedenfalls wäre es mir sehr neu.
Auf Desktop PCs (mit normalerweise einer CPU) wird bei verschiedenen "Programmen" von Tasks gesprochen. Einzelne Threads sind denen untergeordnet. Das hat doch gar nichts damit zu tun, ob sie wirklich gleichzeitig oder nur scheinbar gleichzeitig ausgeführt werden.

Gruß
Johannes

Frank
20.04.2004, 23:38
So ist es! Echte Parallelverarbeitung gibt es doch ganz selten, in der Praxis arbeiten doch die meißten Controller und Microprozessoren immer Befehl für Befehl ab. Das Multitasking wird doch von der Software verwaltet (Zeitscheibenverfahren etc.)
Wenn du richtige Parallelverarbeitung haben möchtest, dann nimm einfach zwei Controller. Aber da wirst du sehen das dann auch andere Problemchen auftreten, nämlich der Datenaustausch und die Form der Zusammenarbeit.
Also meines wissens wird bei Atmel Multitasking nicht speziell duch irgendwelche Befehle oder Hardwarevorkehrungen unterstützt. Man muß es halt per Software regeln, wenn man es denn wirklich braucht. Ich nehme mal an das das bei der C-Control II auch per Software (Betriebsystem) gemanagt wird, bin mir aber nicht sicher.

21.04.2004, 08:37
Jup, bei der CC2 ist ein Betriebssystem drauf. Deshalb ist das Teil auch so langsam...

Johannes
21.04.2004, 08:38
Also, war von mir der Beitrag :-)

pbürgi
21.04.2004, 18:51
Wird im PC das Multithreading nicht auch nur vom Betriebssystem bereitgestellt? Der Artikel in der CT beschreibt sozusagen, wie man sich sein einfaches Betriebssystem für nen Mikrocontroller programmiert. Das ist doch das selbe.

Frank
21.04.2004, 19:01
Ja so ist es. Wobei aber NT / Windows 2000 und vermutlich XP auch Boards mit mehreren Microprozessoren unterstützt.

Kjion
21.04.2004, 20:07
Der Artikel in der CT beschreibt sozusagen, wie man sich sein einfaches Betriebssystem für nen Mikrocontroller programmiert. Das ist doch das selbe.
Die Frage ist nur ob das auf 8-Bit Mikroprozessoren wirklich sinnvoll ist. Wenn die Hälfte des Speichers und der Rechenzeit benötig um eine Art Multithreading zu haben ist das eher fraglich. Meistens kann man viele Sachen quasi parallel durch Interrupts ablaufen lassen. Das ist meistens die bessere/schnellere Lösung.
Meine Meinung dazu...

MfG Kjion

pbürgi
21.04.2004, 20:14
Das ist doch genau das was in dem Artikel beschrieben ist, hab ich doch in diesem Thread oben geschrieben.

hrrh
21.04.2004, 21:18
... fuer weniger geuebte programmierer faellt die moeglichkeit ein betriebssystem fuer einenen microcontroller zu schreibern sowiso flach. ob ein betriebssystem sinnvoll ist, ist schon in anbetracht der (sicher vorhandenen) fehler fraglich :( sowas bringt nur eine fehlerquelle mehr (und diese fehlerquelle ist schwerer zu beheben) meiner meinung nach also nicht sehr sinnvoll.

Johannes
21.04.2004, 21:36
OK, aber Multithreading hat natürlich auch erhebliche Vorteile, z.B. die Sicherheit. Wenn irgendwo etwas nicht stimmt, läuft trotzdem die Sensorüberwachung weiter, etc.

Aber wenn der AVRco das so kann, dann reicht das für mich.

Womit programmiert ihr eigentlich eure AVRs?

Gruß
Johannes

recycle
21.04.2004, 21:47
fuer weniger geuebte programmierer faellt die moeglichkeit ein betriebssystem fuer einenen microcontroller zu schreibern sowiso flach. ob ein betriebssystem sinnvoll ist, ist schon in anbetracht der (sicher vorhandenen) fehler fraglich

Vielleicht sollte man bei "Betriebssystem" nicht unbedingt immer gleich an Monster wie Windows, Linux, Unix oder ähnliches denken.

Das was PBürgi da beschreibt ist doch eigentlich mehr ein Scheduler, der per Timer und Interrupt automatisch die verschiedenen Subroutinen aufruft die man sowieso braucht.
Allzuviel Programmcode steckt da vielleicht gar nicht hinter.
Wenn man den einmal richtig geschrieben hat, könnte ich mir schon vorstellen, dass das weniger fehleranfällig ist, als alles einfach in eine Schleife zu packen oder von jeder Routine die nächste manuell aufzurufen.

Der Vorteil von gut programmiertem Multitasking und Multithreading ist ja nicht nur, dass mehrere Programme oder Threeads scheinbar parallel ablaufen, sondern auch, dass sich nicht gleich alles aufhängt, wenn eine Routine hängt.

Wäre doch schön, wenn der Robby auch noch nach Hause kommt, obwohl irgendeine fehlerhafte Routine in einer Endloschleife hängt.

Wenn pbürgi seine CT irgendwann mal wiederfindet und ein paar Details mehr posten könnte, fände ich das schon ganz interessant.

morty
22.04.2004, 15:15
schaut mal bei www.avrfreaks.net vorbei. Da ist ein sehr guter Artikel zu dem Thema. Letzendlich installiert man ein Betriebssystem, das die verschieden Threads(Prozess)/Tasks(Aufgabe) oder wie auch immer ihr die nennen wollt verwaltet. (Will mich hier nicht festlegen, aber ich glaube als Task bezeichnet man ein Programm das mehrere Prozesse haben kann) Grundsätzlich werden die Befehle nacheinander verarbeitet. (Es gibt zwar auch IC's mit mehreren Prozessoren aber ich glaub nicht in dieser Preisklasse)

pbürgi
23.04.2004, 11:31
OK ich hab nochmal nach der ct gesucht: es ist die 6/2004(8.3.2004) ab Seite 248.
Hier könnt ihr euch den kompletten Quellcode für den MSP430 downloaden, kann man aber sicher auch mim AVR realisieren:
ftp://ftp.heise.de/pub/ct/listings/0406-248.zip

Hier ist die komplette Linkseite von heise.de:
http://www.heise.de/ct/04/06/links/248.shtml

Wer die C't daheim hat hat Glück, denn da wird der Code erklärt.

Es wird hier natürlich kein echtes Betriebssystem programmiert, ich muss zugeben "Scheduler"(recycle) passt deutlich besser. Es ist wie recycle sagt ein Programm, das per Timer verschiedenen Pseudo-Threads verschieden viel Rechenzeit zuschaufelt.

malef
27.04.2004, 11:18
Also
schaut mal unter http://instruct1.cit.cornell.edu/courses/ee476/

und dann unter : AT90 MCU realtime operating systems

da gibts ne Menge MultiThREADING KERNELS

Thread bedeutet alle Funktionen (die Threads eben) laufen in einem gemeinsamen Speicherbereich (da der AVR oder sonstiger MC keine Memory Managment Unit besitzt um die Prozesse voneinaner zu schützen)
diese Threads werden aber Preemtiv (also vom Kernel selbst(zBsp:durch TimerINT) umgeschaltet und alles was zum Thread gehört (Stack,Register) wind gesichert und wenn der Thread wieder aktiv wird , wieder zürückgeholt !(Context Switching)
-Jeder Thread hat seinen eigenen Stack und seine eigenen Register

usw....

27.04.2004, 11:29
Dort findet man zum Beispiel das:


;***** Simple time-based scheduler ********
;
;The goal is to allow several tasks to execute at a regular rate
;without making a HUGE timer ISR. The timer 0 interrupt just
;decrements counters which are tested in a loop to dispatch
;subroutines. Timer 0 ticks at a mSec rate in this code.

;Task descriptions:
; task 1: blink LED 0 2/sec
; task 2: blink LED 1 1/sec
; task 3: Detect button 0 and change task 1 blink rate if the button is down
;
;********************************
;You will need to CHANGE this path
,nolist
.include "c:\avrtools\appnotes\8515def.inc"
.list

;********************************
;define registers
.def save =r1
.def temp =r16 ;temporary register
.def LED =r17 ;the actual LED value to display
.def reload =r18 ;holds timer reload value

;Specify the interval for each task to execute:
;since we are toggling LED state on each task entry,
;the task times are 1/2 the desired blink times
.equ t1 =250 ;250 mSec
.equ t2 =125 ;125 mSec - Task2 will need to have another counter
.equ t3 =30 ;30 mSec interval for the keyboard scanner

;********************************
;RAM locations:

;Task execution time intervals
.dseg
time1: .byte 1
time2: .byte 1
time3: .byte 1

;Task2 state variable needed to
;count 4 times the 1/4 second secheduled time
tsk2c: .byte 1

;message from task3 to task1 to change rate
tsk3m: .byte 1

;*******************************
; Initialization
.cseg
.org $0000
rjmp RESET ;reset entry vector
reti
reti
reti
reti
reti
reti
rjmp TIMER
reti
reti
reti
reti
reti

RESET:
ldi Temp, LOW(RAMEND) ;setup stack pointer
out SPL, Temp
ldi Temp, HIGH(RAMEND)
out SPH, Temp

;set up the PORTs
ser Temp ;set PORTB to be
out DDRB,Temp ;all outputs
ldi Temp, 0xff ;turn off LEDs
out PortB, Temp

clr Temp ;set PORTD to be
out DDRD,Temp ;all inputs

;set up timer 0 for 1 mSec ticks
ldi Temp,exp2(TOIE0);enable timer interrupt
out TIMSK, Temp
ldi Temp, 3 ;prescale timer by 64
out TCCR0, Temp
ldi Reload,256-62 ;preload timer since
out TCNT0, Reload ;62.5 x (64x.25) microSec = 1.0 mSec.

;initialize task timers
ldi temp, t1 ;mSec
sts time1, temp
ldi temp, t2
sts time2, temp
ldi temp, t3
sts time3, temp

;initialize LED state
ldi LED, 0xff ;all LEDs off

;initialize task2 state variable (4 times thru equals 1/2 sec)
ldi temp, 4
sts tsk2c, temp

;initalize task3 message to zero (don't modify task1 rate)
ldi temp, 0
sts tsk3m, temp

;Start the clock ticking
sei ;enable all interrupts

************************************************** ****************
;Now start scheduling events.
;This is the main program loop.
;All tasks are subroutines called from here when
;their respective timers reach zero.

Sched:
tsk1: lds temp, time1 ;test for first task ready
tst temp
brne tsk2 ;if not then skip it
rcall Task1

tsk2: lds temp, time2 ;test for second task ready
tst temp
brne tsk3 ;if not then skip it
rcall Task2

tsk3: lds temp, time3 ;test for third task ready
tst temp
brne Sched ;if not then skip it
rcall Task3

rjmp Sched

;************************************************* ****************
;The three actual tasks:
;******************************
;LED 0 2/sec (but modified by Task 3)
Task1: lds temp, tsk3m ;get the message from task 3
tst temp ;and test it for "fast/normal"
brne t1fast

;if we get here the message from task 3 ="normal rate"
ldi temp, t1 ;reinit time counter
sts time1, temp
rjmp t1blk

;if we get here the message from task 3 ="fast rate"
t1fast: ldi temp, t1 ;reinit time counter
lsr temp ;divide time by four
lsr temp
sts time1, temp

;now toggle the LED
t1blk: mov temp, LED ;isolate the zero bit
andi temp, 0b00000001; and invert it
andi LED, 0b11111110
com temp
andi temp, 0b00000001
or LED, temp
out PORTB, LED

ret ;go back to scheduler

;******************************
;LED 1 1/sec
Task2: ldi temp, t2 ;reinit time counter
sts time2, temp

lds temp, tsk2c ;find out if 4 counts have occured
dec temp ;so that we can count to 1/2 second
sts tsk2c, temp
tst temp
brne t2exit ;if not then leave

;if we get here, then 1/2 second has passed and we
;should toggle the LED
mov temp, LED ;isolate the zero bit
andi temp, 0b00000010; invert it
andi LED, 0b11111101; then combine it with LED again
com temp
andi temp, 0b00000010
or LED, temp
out PORTB, LED

ldi temp, 4 ;and reset the state variable
sts tsk2c, temp

t2exit: ret ;go back to scheduler

;******************************
;Button detect and modify Task 1 blink rate
Task3: ldi temp, t3 ;reinit time counter
sts time3, temp

in temp, PIND ;get all the buttons
com temp ;convert a button-down to a one
andi temp, 0b00000001;isolate button 0
brne t3modt

;if we get here then set message to "normal" task1 rate
ldi temp, 0 ;set message value
sts tsk3m, temp ;and store it
rjmp t3exit

;if we get here then set message to "fast" task1 rate
t3modt: ldi temp, 1 ;set message value
sts tsk3m, temp ;and store it

t3exit: ret ;go back to scheduler


;************************************************* *************
;timer 0 ISR (timer-zero overflow)
;Enters every 1.0 mSec

TIMER: in save, SREG
out TCNT0, Reload ; keeps clock ticking at 1 mSec
push temp ; use temp in ISR

;update each of the three timers
;for the three tasks
;BUT if the count is zero don't do anything until the process
;resets it

lds temp, time1
tst temp
breq t0t2
dec temp
sts time1, temp

t0t2: lds temp, time2
tst temp
breq t0t3
dec temp
sts time2, temp

t0t3: lds temp, time3
tst temp
breq t0end
dec temp
sts time3, temp

t0end: pop temp
out SREG, save
reti ;back to backgound tasks
;************************************************* *************

Ist glaub für Mega32

27.04.2004, 11:31
Und in C sieht es so aus:



//Dup the function of sched1.asm
//used as an example in the program organization doc.

#include <90s8515.h>

//timeout values for each task
#define t1 250
#define t2 125
#define t3 60

//the three task subroutines
void task1(void); //blink at 2 or 8 Hz
void task2(void); //blink at 1 Hz
void task3(void); //detect button and modify task 1 rate

void initialize(void); //all the usual mcu stuff

unsigned char reload; //timer 0 reload to set 1 mSec
unsigned char time1, time2, time3; //timeout counters
unsigned char tsk2c; //task 2 counter to get to 1/2 second
unsigned char tsk3m; //task 3 message to task 1
unsigned char led; //light states

//************************************************** ********
//timer 0 overflow ISR
interrupt [TIM0_OVF] void timer0_overflow(void)
{
//reload to force 1 mSec overflow
TCNT0=reload;

//Decrement the three times if they are not already zero
if (time1>0) --time1;
if (time2>0) --time2;
if (time3>0) --time3;
}

//************************************************** ********
//Entry point and task scheduler loop
void main(void)
{
initialize();

//main task scheduler loop
while(1)
{
if (time1==0) task1();
if (time2==0) task2();
if (time3==0) task3();
}
}

//************************************************** ********
//Task subroutines
//Task 1
void task1(void)
{
time1=t1; //reset the task timer
if (tsk3m != 0) time1 >>= 2; //check for task 3 message

//toggle the zeros bit
if ((led & 0x01) == 0)
led = led | 0x01;
else
led = led & 0xfe;

PORTB = led;
}

//*******************************
//Task 2
void task2(void)
{
time2=t2; //reset the task timer
if (--tsk2c == 0) //have we waited 1/2 second?
{
tsk2c = 4; //reload the 1/2 sec counter

//toggle the ones bit
if ((led & 0x02) == 0)
led = led | 0x02;
else
led = led & 0xfd;

PORTB = led;
}
}

//*******************************
//Task 3
void task3(void)
{
time3=t3; //reset the task timer
tsk3m = ~PIND & 0x01; //generate the message for task 1
}

//************************************************** ********
//Set it all up
void initialize(void)
{
//set up the ports
DDRD=0x00; // PORT D is an input
DDRB=0xff; // PORT B is an ouput
PORTB=0;

//set up timer 0
reload=256-62; //value for 1 Msec
TCNT0=reload;
TIMSK=2; //turn on timer 0 overflow ISR
TCCR0=3; //prescalar to 64

//init the LED status (all off)
led=0xff;

//init the task timers
time1=t1;
time2=t2;
time3=t3;

//init the task 2 state variable
//for four ticks
tsk2c=4;

//init the task 3 message
//for no message
tsk3m=0;

//crank up the ISRs
#asm
sei
#endasm
}

23.05.2004, 18:31
Nach einigem Halbwissen werde ich erst mal ein bisschen Aufklärung betreiben ;)

In der Betriebssystemtechnik unterscheidet man grundsätzlich bei Multitasking-BS Prozesse und Threads.
Der wesentliche Unterschied ist, dass Prozesse in einem eigenem Addressraum ablaufen, Threads hingegen nicht. Threads laufen im Kontext von Prozessen. Aufgeweicht wird das Ganze durch shared Memory, den sich Prozesse teilen können.

Was ein Multitasking-Betriebssystem für Atemls AVR angeht, so muss man sich das in der Tat nicht als ein grosses BS ala Linux vorstellen.
Im einfachsten Fall ist das ein simpler Round-Robin-Scheduler, der in festen Zeitscheiben die Threads aufruft. Basta. Ein paar Programmzeilen, das war es.
Das Ganze kann man dann beliebig um die Üblichen Zutaten erweitern: variable Zeitscheiben, feste Prioritäten, variable Prioritäten, Interprozesskommunikation (Semaphore, shared Memory, etc).

Auch wenn man es etwas komplexer macht, so kostet das auch nur Rechenzeit in der Gegend von maximal einigen Prozent.

Ob das Ganze Sinn macht, muss jeder selbst wissen. Die weitaus meisten Controller-Anwendungen in der Industrie funktionieren nach einer Studie aus diesem Jahr immer noch nach dem guten alten Hauptschleife-Flags-Interrupt-Prinzip, wo in der Hauptschleife im Wesentlichen die Flags ausgewertet werden und der Controller dann die entsprechenden Routinen ausführt. In den entsprechenden Interrups werden die Flags dann je nach Ereignis gesetzt.
Vorteile macht ein Multitasking-System sicher, wenn man mit mehreren Leuten relativ unabhängig voneinander an einem Projekt arbeitet.
Und es macht sicherlich auch unabhängiger von der Hardware, wenn es ein komplexeres System ist, welches stärker abstrahiert.

hrrh
24.05.2004, 19:26
wenn die microprozessoren noch schneller sind und mehr speicher haben machts mehr sinn. wenn 3 prozesse gleichzeitig laufen, braucht man 3 mal so viel ram

Hellmut
07.07.2004, 00:38
Hallo Freunde

hab mal genau gewusst was der Unterschied zwischen Multitasking und Multithreading ist. Und zwar über Apples altes Mc68k basierendes Betriebssystem. Dort war es so, daß die Programmierrichtilinien verlangten Code so zu schreiben, dass wenn ein Unterprogramm ausgeführt war man keine Bits gesetzt haben durfte die dem eigenen Programm die Kontrolle über den Prozessor auschließßlich gewährten, also Interrupts maskieren. Das Betriebssystem speicherte die requests der einzelnen tasks selber auf seinen eigenen Stack, und so hat ein Interrupt dann eine Routine des Betriebssystems ausgelöst die der nächsten Task in Stack die Kontroller über den Computer gibt. Hat jetzt ein Programmierer diese maskable interrupts maskiert konnte er den Rechner ohne zeitlliche Begrenzung für sein Programm benutzen. Als dann bei Apple echtes Multitasking kam konnte das Betriebssystem über einen Timer und einem nicht maskable Interuupt die Kontrolle wieder übernehmen, hat die Inhalte aller Register auf das Stack gerettet und die nächste Task begann immer damit ihre Registerinhalte vom Stack zu holen und einzulesen.

Diese Beschreibung zeigt wie man in jedem Prozessor Multitasking einführen kann. Ich kenne den AVR noch nicht, aber ich vermute in Analogie es gibt dort eine Möglichkeit über einen Hardware Timer einen Soft Interrupt auszulösen der einen Zeiger auf eine Interrupt-Service Rotine verweist. Diese Rotine müßte dann wie oben beschrieben die "Laufzeitumgebung der alten Task auf den Stack zu pushen, für die nächste Routine die "Laufzeit" Umgebung wieder einrichten und den Programm Counter auf den letzten Wert, oder letzten Wert plus eins setzen. Natürlich nicht vergessen den Timer zu reseten und neu zu starten.

Wichtig erscheint mir aber der Hinweis, dass wenn eine Task eine "Echtzeit" Aufgabe bearbeitet die Möglichkeit haben muß das Interrupt des Taskmanagers zu maskieren und erst dann wieder entmaskieren wenn das zeitkritische erledigt ist. Gerade im embedded Bereich, und das sind wir hier ja.


Beim Mu

hrrh
07.07.2004, 14:24
vielleicht waere ein microprozessor mit linux besser geeignet (gibts fertig zu kaufen). die sind viel schneller als ein avr, haben viel mehr ram und mehr flash. sind nur etwas teurer.

Hellmut
07.07.2004, 14:48
Hallo hrrh

Du weisst schon das wir über Routinen sprechen die nur wenige Zeilen lang sind? Übrigens werden in der Industrie viele sogenannte RTOS eingesetzt die damit werben extrem effizient und extrem kleines footprint zu haben?! Und das in sehr kostensensitiven Anwendungen, z.B 68hc05!?

leon
07.07.2004, 22:05
Hallo,

schaut Euch mal die Demo bei http://www.e-lab.de/ an.
Meine mega8 laufen wunderbar mit dem Multitasking,
obwohl es nicht immer einfach ist, die Fehler zu finden,
die man sich selbst eingebaut hat. ;-)

Gruß leon

Hellmut
07.07.2004, 23:26
Hallo Leon

eine tolle Quelle für Information

Danke