Jedną z rzeczy, które uważam za przydatne na wielu maszynach, jest prosty przełącznik stosów. Tak naprawdę nie napisałem żadnego dla PIC, ale spodziewałbym się, że to podejście zadziała dobrze na PIC18, jeśli oba / wszystkie wątki wykorzystają w sumie 31 lub mniej poziomów stosu. W 8051 główną rutyną jest:
_taskswitch:
xch a, SP
xch a, _altSP
xch a, SP
gnić
Na PIC nie pamiętam nazwy wskaźnika stosu, ale procedura mogłaby wyglądać mniej więcej tak:
_taskswitch:
movlb _altSP >> 8
movf _altSP, w, b
movff _STKPTR, altSP
movwf _STKPTR, c
powrót
Na początku programu wywołaj procedurę task2 (), która ładuje altSP z adresem alternatywnego stosu (16 prawdopodobnie działałby dobrze dla PIC18Fxx) i uruchamia pętlę task2; ta rutyna nie może nigdy powrócić, inaczej rzeczy umrą bolesną śmiercią. Zamiast tego powinien wywoływać _taskswitch, ilekroć chce uzyskać kontrolę nad głównym zadaniem; główne zadanie powinno wtedy wywołać _taskswitch, ilekroć chce ustąpić drugiemu zadaniu. Często można mieć słodkie małe rutyny, takie jak:
void delay_t1 (unsigned short val)
{
zrobić
taskwitch ();
while ((bez znaku krótkie) (millisecond_clock - val)> 0xFF00);
}
Zauważ, że przełącznik zadań nie ma możliwości „czekania na warunek”; obsługuje tylko spinwait. Z drugiej strony przełącznik zadań jest tak szybki, że próba przełączenia zadań () w czasie, gdy inne zadanie czeka na wygaśnięcie timera, przełączy się na inne zadanie, sprawdzi timer i wróci szybciej niż typowy przełącznik zadań ustalą, że nie trzeba przełączać zadań.
Należy pamiętać, że wielozadaniowość kooperacyjna ma pewne ograniczenia, ale pozwala uniknąć konieczności blokowania i innego kodu związanego z muteksami w przypadkach, w których niezmienniki tymczasowo zakłócone można szybko przywrócić.
(Edytuj): Kilka zastrzeżeń dotyczących zmiennych automatycznych i takich:
- jeśli procedura, która korzysta z przełączania zadań, jest wywoływana z obu wątków, generalnie konieczne będzie skompilowanie dwóch kopii procedury (prawdopodobnie poprzez #włączenie tego samego pliku źródłowego z różnymi instrukcjami #define). Każdy podany plik źródłowy albo będzie zawierał kod tylko dla jednego wątku, albo będzie zawierał kod, który zostanie skompilowany dwukrotnie - raz dla każdego wątku - dzięki czemu mogę używać makr typu „#define delay (x) delay_t1 (x)” lub # zdefiniować opóźnienie (x) opóźnienie_tx (x) "w zależności od używanego wątku.
- Uważam, że kompilatory PIC, które nie widzą wywoływanej funkcji, założą, że taka funkcja może zniszczyć wszystkie rejestry procesora, unikając w ten sposób potrzeby zapisywania jakichkolwiek rejestrów w procedurze przełączania zadań [fajna korzyść w porównaniu z zapobiegawcza wielozadaniowość]. Każdy, kto rozważa zastosowanie podobnego przełącznika zadań dla dowolnego innego procesora, musi znać stosowane konwencje rejestru. Przesuwanie rejestrów przed zmianą zadania i usuwanie ich po nich jest łatwym sposobem na załatwienie spraw, zakładając, że istnieje wystarczająca ilość miejsca na stosie.
Wspólna wielozadaniowość nie pozwala całkowicie uniknąć problemów związanych z blokowaniem i tym podobnych, ale naprawdę znacznie upraszcza rzeczy. Na przykład w prewencyjnym systemie RTOS z kompaktowaniem modułu wyrzucania elementów bezużytecznych należy zezwolić na przypięcie obiektów. Gdy używasz przełącznika kooperacyjnego, nie jest to konieczne, pod warunkiem, że kod zakłada, że obiekty GC mogą się przesuwać w dowolnym momencie wywołania funkcji taskwitch (). Kompaktowy kolektor, który nie musi się martwić o przypięte obiekty, może być znacznie prostszy niż ten, który ma.