Eine Sache, die ich auf einer Reihe von Maschinen als nützlich empfunden habe, ist ein einfacher Stapelumschalter. Ich habe noch keinen für den PIC geschrieben, aber ich würde erwarten, dass der Ansatz auf dem PIC18 gut funktioniert, wenn beide / alle Threads insgesamt 31 oder weniger Stapelebenen verwenden. Auf dem 8051 ist die Hauptroutine:
_taskswitch:
xch a, SP
xch a, _altSP
xch a, SP
ret
Auf dem PIC vergesse ich den Namen des Stapelzeigers, aber die Routine würde ungefähr so aussehen:
_taskswitch:
movlb _altSP >> 8
movf _altSP, w, b
movff _STKPTR, altSP
movwf _STKPTR, c
Rückkehr
Rufen Sie zu Beginn Ihres Programms eine task2 () - Routine auf, die altSP mit der Adresse des alternativen Stacks lädt (16 würde wahrscheinlich für einen PIC18Fxx gut funktionieren) und die task2-Schleife ausführt. Diese Routine darf niemals zurückkehren, sonst sterben die Dinge einen schmerzhaften Tod. Stattdessen sollte _taskswitch aufgerufen werden, wann immer die Kontrolle über die primäre Task erlangt werden soll. Die primäre Task sollte dann _taskswitch aufrufen, wann immer sie der sekundären Task nachgeben möchte. Oft hat man niedliche kleine Routinen wie:
void delay_t1 (vorzeichenloser kurzer Wert)
{
machen
Taskschalter ();
while ((vorzeichenloser Kurzschluss) (millisecond_clock - val)> 0xFF00);
}
Beachten Sie, dass der Task-Switcher keine Möglichkeit hat, auf eine Bedingung zu warten. Alles, was es unterstützt, ist ein Spinwait. Auf der anderen Seite ist der Taskwechsel so schnell, dass ein Versuch, einen Taskswitch () auszuführen, während der andere Task auf das Ablaufen eines Timers wartet, zum anderen Task wechselt, den Timer überprüft und schneller zurückschaltet als ein typischer Task-Switcher würde feststellen, dass es keinen Taskswitch braucht.
Beachten Sie, dass kooperatives Multitasking einige Einschränkungen hat, aber die Notwendigkeit für viele Sperren und anderen mutexbezogenen Code in Fällen, in denen vorübergehend gestörte Invarianten schnell wiederhergestellt werden können, entfällt.
(Bearbeiten): Ein paar Vorsichtsmaßnahmen in Bezug auf automatische Variablen und solche:
- Wenn eine Routine, die Task-Switching verwendet, von beiden Threads aufgerufen wird, müssen im Allgemeinen zwei Kopien der Routine kompiliert werden (möglicherweise durch zweimaliges Einschließen derselben Quelldatei mit unterschiedlichen #define-Anweisungen). Jede Quelldatei enthält entweder Code für nur einen Thread oder Code, der zweimal kompiliert wird - einmal für jeden Thread -, sodass ich Makros wie "#define delay (x) delay_t1 (x)" oder "#define delay" verwenden kann #define delay (x) delay_tx (x) "abhängig davon, welchen Thread ich verwende.
- Ich glaube, dass PIC-Compiler, die eine aufgerufene Funktion nicht "sehen" können, davon ausgehen, dass eine solche Funktion alle CPU-Register verwerfen kann, so dass keine Register in der Task-Switch-Routine gespeichert werden müssen [ein netter Vorteil gegenüber präventives Multitasking]. Jeder, der einen ähnlichen Task-Switcher für eine andere CPU in Betracht zieht, muss die verwendeten Registerkonventionen kennen. Das Schieben von Registern vor einem Taskwechsel und das Anzeigen von Registern nach einem Taskwechsel ist eine einfache Möglichkeit, die Dinge zu erledigen, vorausgesetzt, es ist ausreichend Stapelspeicher vorhanden.
Mit kooperativem Multitasking kann man Sperrproblemen und dergleichen nicht vollständig entkommen, aber es vereinfacht die Dinge wirklich erheblich. In einem vorbeugenden RTOS mit einem komprimierenden Garbage Collector ist es beispielsweise erforderlich, das Anheften von Objekten zu ermöglichen. Wenn Sie einen kooperativen Switcher verwenden, ist dies nicht erforderlich, vorausgesetzt, der Code geht davon aus, dass sich GC-Objekte bei jedem Aufruf von taskswitch () bewegen können. Ein Verdichtungssammler, der sich nicht um festgesteckte Objekte kümmern muss, kann viel einfacher sein als einer, der dies tut.