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");
}
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
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
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.
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.
@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
>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
@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.
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
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
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
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.
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.
> 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.
>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