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.
Btw., attribute naked und noreturn bei main() erübrigen sich von selbst. Der Compiler weiß das (außer bei -ffreestanding) von allein.
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
@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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.