Forum: Compiler & IDEs Multithreading auf dem AVR erster Ansatz


von Thomas Maierhofer (Gast)


Lesenswert?

Nach meinem ersten Tag AVR Programmierung hab ich mal ein Experiment 
gewagt, um das "Umbiegen des Stack Pointers" zu testen. Ich kann die 
Einsprungadresse bereits umsetzen. Damit ist der erste Schritt in 
Richtung Multithreading gemacht.

Achtung für die Gurus:
Das ist nur ein Experiment! Es stellt noch keine Kontextumschaltung dar, 
wie sie ein echter Scheduler machen muss. Für mich als AVR Anfänger ist 
das nur ein Test ob ich den Rücksprung umgeschaltet kriege. Das die 
Register und der Stackpointer etc. nicht gesichert werden ist mir klar.

So, jetzt meine Frage: Wie würdet ihr das weitere Sichern und Setzen des 
Kontextes gestalten? Auf was ist da zu achten. Insbesondere beim AVR, 
denn prinzipiell bin ich in der Systemprogrammierung schon einigermaßen 
fit (auf dem PC, nicht auf dem AVR).

/*************************** Quellcode ****************/
#include <io.h>
#include <interrupt.h>
#include <sig-avr.h>


void Thread1( void )
{
  for(;;)
    PORTB = 0x0F;
}


void Thread2( void )
{
  for(;;)
    PORTB = 0xF0;
}


_attribute_ ((naked))
_attribute_ ((noreturn))
void main( void )
{
  TCCR0 = 0x05;  /* Timer Initialisierung */
  TIMSK = 0x02;
  DDRB =0xFF;   /* Port B auf Ausgabe */
     sei();
     for (;;) {}  /* so meine kleine Schleife, du läufst nur bis zum 
ersten Interrupt */
}


_attribute_ ((naked))
_attribute_ ((noreturn))
INTERRUPT(SIG_OVERFLOW0)
{
  static int flag = 0;  /* Flag zum Wechsel der Threads */
  PORTB ^= 0xFF;
  int theSP = SP;
  int * ReturnAdress = (int *) theSP;
  ++ReturnAdress;
  if( flag )
    *ReturnAdress = Thread2;
  else
    *ReturnAdress = Thread1;
  flag = ~flag;
  asm("reti");
}

von Sebastian Fahrner (Gast)


Lesenswert?

Interessant.
Da Du ja Softwareentwickler bist, brauch ich Dich wohl nicht auf die 
Gefahren hinzuweisen, die diese Form von Taskwechseln mit sich bringt.

Ich hätte es mit den Statusmaschinen gemacht, da man bei diesen immer 
ganz genau weiß, wo umgeschaltet wird und nicht z.B. mitten in einem 
Befehl.

Viel Erfolg,

Sebastian

von Oliver K. (Gast)


Lesenswert?

Hallo Thomas,

ich habe mir schon einmal für den C167 ein komplettes 
Multitasking-System mit Semaphoren, Mailboxen und Round-Robin-Scheduler 
gebastelt. Allerdings komplett in Assembler. Die Threads liefen dann 
aber in C (Keil Compiler). Ich kann nur eines sagen: So würde ich es nie 
wieder machen!

Machen würde ich es jetzt so: Die ganze Taskverwaltung (Listen, 
Kontextspeicher) in Hochsprache. Nur den Kontextwechsel in Assembler, 
also Register sicher, neue Register setzen, Stackbereich umblenden etc.

Mit diesem Hochsprachenansatz kann der Scheduler dann beliebig angepasst 
und verändert werden.

Grüße
Oliver

von Peter D. (peda)


Lesenswert?

Ich rate davon ab, den Stack zu vergewaltigen und zuviel in Interrupts 
zu machen.

Du bist Dir bewußt, daß der AVR keine verschiedenen Interruptprioritäten 
kennt ?

D.h. wenn Du alles im Timerinterrupt machst, hast Du quasi keine echten 
Interrupts mehr zur Verfügung.


Ich hab mir mal einen Sheduler geschrieben, der mit in der Mainloop 
aufgerufen wird. Er dient dazu, alle periodischen oder zeitverzögerten 
Prozesse auszuführen (LED blinken, Timeouthandler aufsetzen usw.)

Da er immer an einer bestimmten Stelle in der Mainloop aufgerufen wird, 
kann es nicht zu Konflikten mit anderen Routinen kommen bzw. es werden 
nicht viele Ressourcen gleichzeitig benötigt.

Für jeden aktiven Prozeß in diesem Scheduler braucht man nur 4 Byte 
(Adresse, Zeitinterval, Nummer des nächsten in der Liste).

Bei Interesse kann ich den Kode posten mit Ampelsteuerung als Beispiel, 
geschrieben für den AT89C2051 (128Byte RAM, 2kB Flash), getestet unter 
Borland-C.


Peter

P.S.:
Ich glaube nicht, daß es einen universellen Programmierstil für alles 
gibt.
Optimale Ergebnisse erziehlt man nur dann, wenn man die speziellen 
Eigenheiten der Applikation und der verwendeten CPU berücksichtigt.

von Joerg Wunsch (Gast)


Lesenswert?

Btw., attribute naked und noreturn bei main() erübrigen
sich von selbst.  Der Compiler weiß das (außer bei
-ffreestanding) von allein.

von Thomas Maierhofer (Gast)


Lesenswert?

Ich will mal ein paar Fragen zu den bisherigen Artikeln aufwerfen:

Vergewaltigung des Stacks:
Wo liegt das Risiko? Ich kann doch auf einem AVR das verfügbare RAM 
einteilen wie es mir passt. Wenn ich z.B. der Meinung bin 3 Threads mit 
jeweils 32 Byte Stack Frame laufen lassen zu müssen, dann kann ich das 
doch genau bestimmen. So ein AVR Programm ist doch absolut 
deterministisch? Übrigens macht das jeder Scheduler so.

Verwendung der Interrupts:
Auch wenn ich den Timer Overflow Interrupt benutze, können doch andere 
Interrupts weiterhin auftreten? Was verliere ich also dadurch? Ich kann 
durch die fehlende Priorisierung noch nicht garantieren, dass ich nicht 
gerade in einer Kontextumschaltung bin. Na und? Das muss ich auch gar 
nicht! Ich kann doch die Software so designen, dass die 
Kontextumschaltungen auf keinen Fall mit den Interruptroutinen 
kollidieren.
Falls die Verwendung von Timer Interrupts andere Interrupts blockieren 
würden, dann dürfte man gar nichts mit Timer Interrupts machen! Das kann 
also meiner Meinung nach nicht sein!

Interrupts und Umschaltung mitten im Befehl?
Das stimmt doch nicht oder? Also bei anderen Prozessoren - und ich bin 
mir sicher das geht überhaupt nicht anders - wird eine Befehlsatomare 
Kontextumschaltung garantiert. So eine CPU ist im Prinzip hier doch 
völlig Doof. Im Prinzip: Interrup liegt an - erst mal aktuellen Befehl 
abarbeiten. Dann IP auf den Stack legen und den Interrupt Vector in den 
IP laden. ISR abarbeiten. Mit IRET die alte IP vom Stack nehmen und da 
den nächsten Befahl ausführen.
Halb ausgeführte Befehle gibt es nicht. Die CPU hat keine Möglichkeit 
sich den Status eines halb ausgeführten Befehls zu merken. (Im gäbe es 
hierfür nur den Stack, denn während der Abarbeitung eines Interrupts 
kann ein weiterer auftreten, und da liegt definitiv nichts!)

Zur Umsetzung:
Ich werde das Kernel in C schreiben.

Zur Zielsetzung:
Threads haben nur dann Sinn, wenn mehrere völlig unterschiedliche 
Aufgaben gleichzeitig ablaufen sollen. Eine Status Maschine benötigt 
hierfür zu viele Zustände, und kann schlußendlich zu wesentlich größerem 
Code führen. Prinzipiell sollten Threads aber nur dann eingesetzt 
werden, wenn die Problemstellung es fordert.
ICh denke aber, dass es insbesondere bei komplexeren Programmen auf den 
großen AVRs mehr Vorteile als Nachteile hat.

von Peter D. (peda)


Lesenswert?

@Thomas,

im Prinzip könnte Dein Ansatz funktionieren, aber die Funktionen, die Du 
"einkellerst" müssen immer bis zum nächsten Timerinterrupt beendet sein.

Ansonsten läuft Dein Stack über, da Du immerfort "++ReturnAdress;" 
machst, aber nie ein dazugehörendes "RET" kommt.

D.h. in Deinem Beispiel mit Thread1 und Thread2 als Endlosfunktionen 
kracht es ganz gewaltig.


Daher sehe ich keine Vorteil gegenüber einem Scheduler in der Mainloop.

Im Gegenteil, in der Mainloop können Threads beschleunigt oder verzögert 
ausgeführt werden, je nach restlicher Auslastung der CPU.
Z.B. erfolgt das Auslesen eines ADC und Auffrischen des Displays 
langsamer oder schneller, was aber die gesamte Funktion in keinster 
Weise stört.
Es sieht eben nur schöner aus, wenn das Display schneller reagiert.

Wenn dagegen mal ein ganzer Timerinterrupt unter den Tisch fällt, kann 
das schon ernsthafte Konsequenzen haben, mindestens geht aber Deine 
Systemuhr nach.



Peter

von Sebastian Fahrner (Gast)


Lesenswert?

>Halb ausgeführte Befehle gibt es nicht.

Doch, die gibt es zumindest in C:

int meins;
meins++;

"meins++" bedeutet für den AVR zwei Assembler-Befehle, weil er nur ein 
8-Bit-µC ist. Dein Taskwechsel-Interrupt kann nun zwischen dem ersten 
und dem zweiten kommen. In diesem Fall ist der Inhalt von "meins" 
zumindest solange falsch, bis es in seinem Thread weitergeht. Das mußt 
Du halt berücksichtigen.

Grüße,

Sebastian

von Schmittchen (Gast)


Lesenswert?

@Thomas Maierhofer:
> Threads haben nur dann Sinn, wenn mehrere völlig unterschiedliche Aufgaben 
gleichzeitig ablaufen sollen. Eine Status Maschine benötigt hierfür zu viele 
Zustände, und kann schlußendlich zu wesentlich größerem Code führen.

Was spricht dabei gegen den Einsatz mehrerer Statemaschinen, die 
voneinander unabhängig sein können?
Ist so eine Konstruktion dann ein "kooperatives Betriebssystem"?

Wo kann man sich in diese Theorien einlesen bzw. wie heisst die 
Statemaschinenlehre/-wissenschaft?

Schmittchen.

von Sebastian Fahrner (Gast)


Lesenswert?

Noch ein Nachtrag zu den unteilbaren Befehlen:

Wenn Du ein 16-Bit-IO-Register ausliest, z.B. einen Timer, brauchst Du 
zwei Befehle dafür. Wenn es schlecht läuft, kann folgendes passieren:

1. Timer = 0x00ff
2. Du liest das Low-byte -> 0xff
3. In dieser Zeit zählt der Timer 1 hoch -> 0x0100
4. Du liest das High-Byte -> 0x01
5. Du erhälst den falschen 16-Bit-Wert 0x01ff

Aus diesem Grund hat der AVR das "TEMP"-Register. Wenn Du den Timer 
ausliest, wird der 16-Bit-Wert "gecaptured", d.h. ein Abbild davon 
gemacht (in einem Schritt). Dieses Abbild liest Du dann. Das ist aber 
Hardware!!
Ich glaube, alle µCs haben solche Mechanismen (z.B. die 8051er haben 
dafür die sog. Capture-Einheit).

Dein Programm hat solche Mechanismen nicht. D.h., Du müßtest vor jeder 
16- oder 32-Bit-Aktion den Umschaltinterrupt ausschalten und danach 
wieder ein.

Der C166 hat dafür übrigens eine spezielle Funktion, um eine Gruppe von 
Befehlen "atomar" zu machen, d.h. ununterbrechbar für Interrupts.

Grüße,

Sebastian

von Oliver K. (Gast)


Lesenswert?

Hallo Thomas,
sich selbst eine Multitasking-Umgebung zu basteln, ist ein steiniger 
Weg. Das haben schon andere mal gemacht. Schau doch mal in diese Seite 
rein.

http://www.barello.net/avrx/


Grüße
Oliver

von Thomas Maierhofer (Gast)


Lesenswert?

Noch eine Frage zu den unteilbaren Befehlen.
ICh nehme an, das folgende Voraussetzungen erfüllt sind:

Wenn eine Interrupt Service Routine (ISR) installiert ist, also z.B. für 
den Timer Interrupt, dann müssen doch zumindest folgende Voraussetzungen 
erfüllt sein:

1. Die Unterbrechung des aktuellen Programms darf nicht zu irgendwelchen 
Seiteneffekten auf dem aktuellen Programm führen. Insbesondere muss der 
AVR den Wiedereintritt in den aktuellen Kontext vollständig ermöglichen. 
Das bedeutet, dass Operationen die Register involvieren, auf die kein 
Zugriff beteht atomar abgewickelt werden. Wäre das nicht so, würde die 
ISR diese Operation zerschießen, und keine Interrupts wären möglich.

2. Die Rettung und Wiederherstellung des aktuellen Status ist der ISR 
vollständig möglich. Wäre das nicht so, dann könnte die ISR den 
Wiedereintritt in den kritischen Bereich nicht wiederherstellen.

3. Sind die Annahmen 1+2 für einen bestimmten Kontext falsch, so muss 
dieser Kontext zumindest für das kurze, problematische Fragment die 
Interrupts deaktivieren können. Der anstehende Interrupt darf dabei 
nicht verlohren gehen. (In bezug auf einen Scheduler darf der Interrupt 
verlohren gehen, da dann eben die Zeitscheibe etwas länger wird).

Diese Annahmen müssen richtig sein, ansonsten wäre kein Betriebssystem 
mit Multithreading auf dem AVR möglich. Es gibt diese aber bereits.

Wo ist eurer Menung nach der Denkfehler

von Joerg Wunsch (Gast)


Lesenswert?

3. ist so.  Du mußt dann eben nur manuell sicherstellen,
daß Dein Programm bei allen Zugriffen auf kritische 16-bit
IO register explizit ein cli()/sei() darum macht.  In einem
,,normalen'' Programm ist das nicht so kritisch, solange
man garantieren kann, daß innerhalb der ISRs selbst nicht
weitere Zugriffe auf derartige 16-bit Register stattfinden.

von Peter D. (peda)


Lesenswert?

Generell besteht fast jede C-Instruktion aus mehreren Assemblerbefehlen, 
ist also nicht atomar.

Das ist ja auch nichts schlimmes, solange nicht auf Variablen oder Ports 
zugegriffen wird, die auch von Interrupts verwendet werden.
Anderenfalls ist bei der Mainloop das Problem einfach zu lösen, indem 
solche Zugriffe mit SEI und CLI geklammert werden.
Der Interrupt kann immer direkt zugreifen, da ihn ja niemend 
unterbrechen kann.


Bei Multitasking kann im Prinzip jeder jeden unterbrechen und das macht 
die Sache höllisch kompliziert.

In der "Elektronik" war da mal ein langer Artikel unter anderem betreffs 
der Parameterübergabe zwischen verschiedenen Threads, aber jede Variante 
hatte ihre Fallgruben (Prioritätsinversion, Deadlock, falsche Daten 
usw.) und bis zum Schluß des Artikels wurden die Methoden zwar immer 
komplizierter und Zeit- und Speicheraufwendiger. Aber eine 100%-ige 
Lösung wurde trotzdem nicht gefunden.


Interessant ist beim AVR aber das Auslesen von 16-Bit-Registern. Dazu 
wird ein temporäres Register verwendet, aber das Datenblatt schweigt 
sich darüber aus, ob dieses für jedes 16-Bit-Register separat existiert 
oder nur eins für alle.

Im ersteren Fall kann z.B. das Main den Timer auslesen und ein Interrupt 
kann genau dazwischen hauen und das Capture auslesen, ohne Konflikte.

Ist das Temp aber für alle gemeinsam, krachts dann.

Hat das schon mal jemand rausgekriegt ?

Ich klammere sicherheitshalber immer, sobald auch nur ein einziger 
Interrupt Zugriff auf ein 16-Bit-Register hat.


Peter


P.S.:
Hast Du den Denkfehler in Bezug auf den Stacküberlauf bei Deinem obigen 
Programm durch endlose Einkellerung von Mehrfachinstanzen Deiner beiden 
Endlosfunktionen nachvollziehen können ?

Du müßtest also noch irgendeinen Mechanismus einbauen, der irgendwie 
feststellt ob eine Funktion schon beendet ist und davon abhängig diese 
Funktion eben nicht immer wieder neu aufruft, sondern irgendwie aus den 
Tiefen des Stacks die Adresse rausfischt, wo sie irgendwann früher mal 
unterbrochen wurde und dann dort fortsetzt.

Wenn man aber was tief aus dem Stack holt, muß alles dahinter nach unten 
verschoben werden, damit der Stack wieder ausbalanziert ist.
Wenn aber eine Funktion einen temporären Puffer im Stack angelegt hat 
und darauf einen Zeiger gesetzt hat, darf dieser nicht verschoben werden 
!!?

Klingt ja ganz schön kompliziert, wirds daher auch wohl sein.

von Joerg Wunsch (Gast)


Lesenswert?

>  Interessant ist beim AVR aber das Auslesen von
> 16-Bit-Registern. Dazu wird ein temporäres Register
> verwendet, aber das Datenblatt schweigt sich darüber aus,
> ob dieses für jedes 16-Bit-Register separat existiert oder
> nur eins für alle.

Die Atmel-Datenblätter sind nicht sonderlich gut, aber so
schlecht sind sie nun auch nicht, wie Du meinst.

Es sind erstens nur einige 16-Bit-Register davon betroffen,
nämlich die zeitkritischen (im Timer und wenn ich mich recht
entsinne eins im ADC), zweitens ist klar und deutlich
beschrieben, daß das TEMP-Register allen gemeinsam ist.

von Sebastian Fahrner (Gast)


Lesenswert?

>1. Die Unterbrechung des aktuellen Programms darf nicht uu irgendwelchen 
Seiteneffekten auf dem aktuellen Programm führen. Insbesondere muss der AVR den 
Wiedereintritt in den aktuellen Kontext vollständig ermöglichen.

Das heißt im Klartext: DEIN PROGRAMM muß dies ermöglichen.
Du mußt sicherstellen, daß in einem "kritischen Abschnitt" nicht 
unterbrochen wird.
Du mußt sicherstellen, daß niemals zwei Prozesse gleichzeitig auf 
gemeinsame Daten zugreifen.
Wenn Du das sicherstellst, hast Du keine Probleme.


Noch ein Beispiel: Impulse zählen im Interrupt:

long Impulse;

INTERRUPT(SIG_INT0)
  {
  Impulse++;
  }

long LeseImpulse_FALSCH(void)
  {
  return(Impulse);
  }

Der Befehl "return(Impulse);" muß die 4 Bytes von "long Impulse" 
kopieren. Das sind vier Assemblerbefehle, während denen ein Interrupt 
kommen könnte, und den Wert von "Impulse" verfälschen könnte.

long LeseImpulse_RICHTIG(void)
  {
  long temp;
  cli();
  temp = Impulse;
  sei();
  return(temp);
  }

Die richtige Funktion LeseImpulse() bildet eine Kopie von "Impulse". 
Währenddessen muß der Interrupt verboten sein. Diese Kopie wird dann 
zurückgegeben.
Kommt in dieser Zeit ein Interrupt, wird das Requestflag gesetzt -> der 
Interrupt geht nicht verloren.

Grüße,

Sebastian

von Peter D. (peda)


Lesenswert?

@Jörg,

hast recht, jetzt hab ichs auch gefunden:

"This temporary register is also used when accessing OCR1A and ICR1."


Peter

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.