wtorek, 10 października 2017

RTOS - pierwsze dotknięcie kijem.


RTOS czyli Real Time Operating System. Coś z czym predzej czy później przyjdzie nam się zetknąć programując mikrokontrolery. Choś pojęcie system operacyjny w mikrokontrolerach wydawać by się mogło , że to jakiś kosmos.
Ale kosmosem to nie jest a systemy RTOS stają się obecnie chlebem powszednim w systemach wbudowanych.
W artykule przedstawiam moje pierwsze spotkanie z RTOS i próbę poznania  tego zagadnienia przy pomocy mikrokontrolera PIC. Dokąd mnie ta droga zaprowadzi nie mam zielonego pojęcia :)


RTOS używany jest tam gdzie wymagana jest pełna kontrola czasowa i synchronizacja wielu procesów tak aby działały one bez wzajemnego zakłócania. Przy czym za proces rozumiemy np miganie diodą LED :). Problemu nie ma jak migamy jedną ale jak chcemy migać kilkoma i każda np z inną częstotliwością to zaczynamy drapać się w głowę i szukać rozwiązań typu timery programowe, operacje modulo etc. Można i tak ale mądrzy ludzie ogarneli tego typu problemy i stworzyli  RTOS-a po co zatem głowić się nad wymyśleniem powtórnie koła ? skoro mamy do dyspozycji cała furę na kołach.

Dopóki jesteśmy na etapie testowania pojedyńczych procesów czyli np. miganie jedną diodą LED , wyświetlanie napisu na LCD itp to w kierunku RTOS-a nie mamy potrzeby patrzeć. Przyjdzie jednak kiedyś taka chwila , że wyjdziemy z etapu mono wątkowości w aplikacji/programie i będziemy  chcieli aby wiele procesów połaczyć ze sobą tak aby nie blokowały się wzajemnie czy też nie zakłócały swojej pracy. Wtedy jest czas na zaznajomienie się z tym co ma do zaoferowania RTOS.

Mylne można odnieść wrażenie , że skoro system "operacyjny" to zapcha nam zasoby mikrokontrolera tak, że zostanie tylko miejsce na mruganie jedną diodą LED :) . RTOS potrzebuje od kilkuset bajtów do kilku kB pamięci programu w zależności od stopnia skomplikowania implementacji systemu. Czyli jest możliwość dobrania systemu do możliwości mikrokontrolera i potrzeb

Naukę RTOS-a zaleca się zacząć od konkretnych przykładów. Dlatego każdy szanujący się producent aplikuje system wraz z przykładami.

Jednym z bardziej popularnych RTOS-ów  na rynku jest freeRTOS.
Jest on dostępny do większości popularnych mikrokontrolerów w tym do mikrokontrolerów PIC.
Należy tu nadmienić, że nie znajdziemy za wiele materiałów w jezyku Polskim na ten temat, stąd prosty wniosek , że trzeba się uczyć języków. Dokumentacja do freeRTOS-a jest dosyć obszerna i dla zielonego jak szyczypiorek w temacie może być nie lada wyzwaniem. Możemy też szybko się do tematu zrazić z racji przerostu przytłaczającej wiedzy w dokumentacji :)

Dlatego ja swoją przygodę z RTOS postanowiłem zacząć nie od kobyły freeRTOS-a ale od  źrebaczka czyli od zwiewnej i lekkiej biblioteki RTOS dla mikrokontrolerów PIC24/dsPIC33 oferowanej za darmo przez Microchipa.
Dla porównania dokumentacja freeRTOS to ponad 400 stron a dokumentacja do picRTOS (moja nazwa) to tylko 15 stron :). freeRTOS zajmuje ok 6-12 kB Flash-a a picRTOS nie cały 1 kB.

Oczywiście coś za coś o ile freeRTOS posiada mocno rozbudowane wątki do sterowania procesami i mnóstwo opcji i podopcji o tyle picRTOS posiada jedynie podstawowe możliwości w zakresie sterowania procesami. Ale czy picRTOS jest zabawką ?? zdecydowanie nie, jak podkreśla Microchip to bardzo lekkie i profesjonalne narzędzie dla programistów systemów wbudowanych.

Biblioteka od Microchipa została napisana w asemblerze co daje gwarancję działania prawie z prędkością światła :). To co fajnego jest w tej bibliotece to m.in zaimplementowana możliwość podglądu działania procesów w skali czasu  za pomocą dowolnego wielokanałowego zewnętrznego analizatora stanów logicznych. A nic lepiej nie przemawia do człowieka jak pismo obrazkowe :) Mamy również wgląd w przyjaznej formie za pomocą jednej tablicy w dane o wykorzystaniu stosu przez każdy z procesów. Wielkość stosu rezerwowanego dla każdego procesu jest parametrem ważnym a nawet bardzo ważnym , tę dane musimy podać jawnie przy inicjalizacji każdego procesu.

Sterowanie procesami w picRTOS odbywa się w oparciu o algorytm  round robin. Co to jest i czym to się je ? . Posłużymy się tutaj informacjami z neta :

Algorytm RR
Algorytm planowania rotacyjnego (ang. round-robin - RR) jest oparty na algorytmie FCFS, jednakże różni się od FCFS faktem, że możliwe jest wywłaszczanie. Algorytm ten jest chętnie stosowany w systemach z podziałem czasu.
Zasada działania algorytmu jest następująca. Z każdym procesem powiązany jest kwant czasu, najczęściej 10 do 100 ms. Procesom znajdującym się w kolejce (traktowanej jako cykliczna) procesów gotowych do wykonania, przydzielane są odpowiednie odcinki czasu, nie dłuższe niż jeden kwant czasu.

Do implementacji algorytmu, podobnie jak w przypadku FCFS, używa się kolejki FIFO - brany jest pierwszy proces z wyjścia kolejki, ustawiany jest timer na przerwanie po upływie 1 kwantu czasu i proces jest wysyłany do procesora. Jeżeli czas trwania fazy procesu jest krótszy od 1 kwantu czasu, proces sam zwolni procesor i rozpocznie się obsługiwanie kolejnego procesu z kolejki. Jeżeli proces ma dłuższą fazę od 1 kwantu czasu, to nastąpi przerwanie zegarowe systemu operacyjnego, nastąpi przełączenie kontekstu, a proces do niedawna wykonywany, trafi na koniec kolejki procesów gotowych do wykonania.
Zobaczmy, ile wynosi średni czas oczekiwania w porównaniu z metodą FCFS dla następujących danych:

ProcesCzas trwania fazy
    P1
    P2
    P3
           21 ms
           6 ms
           3 ms
Dla kroku 7 ms diagram Gantta dla powyższych procesów wyglądał będzie następująco:
[Diagram Gantta]

Średni czas oczekiwania wynosi więc: (0+13+16)/3 = 9,67 ms.

Wydajność algorytmu rotacyjnego zależy od kwantu czasu. W przypadku, gdy kwant czasu jest bardzo długi (dąży do nieskończoności) metoda RR działa jak FCFS. Natomiast w przypadku, gdy kwant czasu jest bardzo krótki (dąży do zera) planowanie RR nazywamy dzieleniem procesora (ang. processor sharing), gdyż zachodzi złudzenie, że każdy z n procesów ma własny procesor działający z prędkością 1/n prędkości prawdziwego procesora [1].

W przypadku algorytmu RR, podczas mówienia o wydajności, należy zastanowić się nad zagadnieniem przełączania kontekstu. Im mniejszy jest kwant czasu, tym system bardziej jest narażony na częste przełączenia kontekstu. Jeżeli czas przełączania kontekstu wynosi 10 procent kwantu czasu, to niemalże 10 procent czasu procesora jest tracone w wyniku przełączania kontekstu.

Algorytm jak algorytm ma swoje wady i swoje zalety o których trzeba mieć pojęcie. Nazwa algorytmu  podoba mi się bo jej skrót kojarzy się z dobranocką o brygadzie RR.

Każdą bibliotekę RTOS trzeba przystosować do współpracy z konkretnym mikrokontrolerem i ten proces wymaga skupienia bo można tu polec. Przystosowanie polega na zainkludowaniu do biblioteki plików nagłówkowych konkretnego mikrokontrolera i ewentualnie przekonfigurowanie inicjalizacji sprzętowej. Nie chcę nawet myśleć jak by to przebiegało w przypadku ARM-ów.

Od strony sprzętowej swoje rozważania oprę na mikrokontrolerze 16-bitowym PIC24HJ128GP502 rozpędzony zegarem wewnętrznym do ok 40 MHz. Mikrokontrolery 16-bitowe od Microchipa są bardzo wdzięczne do programowania w stosunku do 8-bitowców znacznie elastyczniejsze nie wspominając już o większej mocy i zasobach a w stosunku do 32-bitowców łatwiejsze do okiełznania (patrząc na rejestr 32 bitowy dostajemy oczopląsu a przy 16-bitach ten efekt jeszcze nie występuje :) ). W przyszłości mam zamiar jak nie połamię zębów na postawieniu RTOS-a na mikrokontrolerze 32-bitowym PIC32MM lub MX


Zabawę z picRTOS zaczynamy od ściągnięcia biblioteki : Biblioteka RTOS
Po rozpakowaniu mamy dosyć sporo plików. Nas interesuje tylko samo core biblioteki i tu będziemy potrzebować następujące pliki :

multitask.h    (z katalogu Include)
PosixTypes.h (z katalogu Include)
multitask.s    (z katalogu Source)
Example_1.c  (z katalogu Source)

W katalogu bibliotecznym Source znajdują się przykłady od Example_1.c do Example_5.c . Dołączamy do projektu jeden przykład i zmieniamy jego nazwę na main.c.

W projekcie będziemy potrzebować jeszcze trzech plików nagłówkowych do konkretnego mikrokontrolera w naszym przypadku do PIC24HJ128GP502. Plików tych nie ma w bibliotece RTOS, musimy je poszukać i dołaczyć do projektu z katalogu kompilatora XC16.
W LINUX-ie ścieżki do potrzebnych plików to :
/opt/microchip/xc16/v1.30/support/PIC24H/h/p24HJ128GP502.h
/opt/microchip/xc16/v1.30/support/PIC24H/inc/p24HJ128GP502.inc 
/opt/microchip/xc16/v1.30/support/PIC24H/inc/p24Hxxxx.inc 

Te trzy pliki dołączamy do projektu jako pliki nagłówkowe. Nasze IDE to MPLABX od Microchipa dla przypomnienia.
Po stworzenia w MPLABX projektu, struktura katalogów powinna wyglądać następująco :




Przy pierwszym uruchomieniu program bedzie zgłaszał szereg błędów, głównie wynikających np z konfiguracji sprzętowej mikrokontrolera. Samo przystosowanie biblioteki i przykładów pod konkretny procek nie sprawiło mi większych problemów, kompilator bardzo klarownie sygnalizował miejsca błędów (brawo dla kompilatora XC16).

Po ogarnięciu etapu przystosowania biblioteki do konkretnego mikrokontrolera PIC24HJ128GP502, czas na przeczytanie instrukcji do biblioteki :) Spróbujemy to przetłumaczyć z chińsko-angielskiego na polski w końcu to nie 400 stron jak w freeRTOS. Instrukcję znajdziemy w katalogu Doc w ściągnietej bibliotece.

Na wstępie instrukcji znajdziemy informację , że na potrzeby obsługi jądra picRTOS zostanie zajęty jeden timer, standardowo jest to Timer1. Można użyć innego Timera np Timer2 ,  na stronie 14, jest infomacja jak to zmienić. Timer1 ustawiony jest w bibliotece na najwyższy priorytet, przy użyciu innych timerów w programie należy ich priorytety ustawiać poniżej 1.

Z instrukcji wynika, że musimy ustawić parametry czasowe systemu w pliku multitask.h. Są to :

#define FREQ_FCY 40.0 (częstotliwość w MHz zegara z jakim odpalamy nasz mikrokontroler) 
#define TIME_SLICE 5 (ta wartość ustawia częstotliwość przełączania pojedyńczego zadania/procesu w kolejce, typowe wartości w przedziale 1-25 ms, w naszym przypadku 5ms) 
Ale uwaga maksymalną częstotliwość przełączania zadania jaką możemy ustawić skorelowana jest z zegarem procka i preskalerem i tak dla 40 MHz możemy TIME_SLICE ustawić maksymalnie na ok 13 ms ((1/40MHz) * 8 * 65536)) . Jeśli chcemy więcej to trzeba zmienić preskaler na większy od 8 np na 64. Ustawiamy to w wierszu :  
#define PRESCALE  8.0


Teraz na podstawie instrukcji skupimy się na funkcjach jakie nam oferuje biblioteka ,  5 przykładów zawartych w bibliotece stara się przedstawić  po co są te funkcje i jak ich używać :



typedef void(*Taskptr)(void);

void CreateTask(Taskptr task, uint16_t stack_size);Funkcja tworzy nam nowe zadanie/proces. Argumenty jakie przekazujemy to wskaźnik na funkcję zadania/procesu i wielkość zagnieżdzenia stosu dla procesu)

void Multitask(uint16_t tmr_period); 
Funkcja uruchamia nam sterowanie zadaniami/procesami . Argumentem jest TMR_PERIOD zdefiniowany jak poniżej :
#define TMR_PERIOD (((TIME_SLICE)*(1000.0))/((PRESCALE)/(FREQ_FCY))) 

void TaskSleep(uint16_t count);
Funkcja wstrzymująca wykonanie zadania/procesu na podaną w argumencie ilość obiegów kolejki. Przy każdym wywołaniu zadania licznik odlicza w dół.

void TaskYield(void);
Wywołanie tej funkcji wewnątrz zadania/procesu , przerywa bieżący proces i przekazuje sterowanie do następnego w kolejce zadania/procesu.



void DisableInterrupts (void);  

Ta funkcja wyłacza przerwania. Używa się w krytycznych miejscach gdzie nie możemy sobie pozwolić aby wywołanie przerwania , zakłóciło nam wykonanie zadania.


void EnableInterrupts (void); 

wznawia działanie przerwań.

void WaitForEvent(uint16_t event); 

Ta funkcja ustawia flagę zdarzeń lub kombinację flag zdarzeń a następnie zawiesza działanie zadania/procesu do chwili skasowania flagi. System obsługuje 16 szt flag. Flagi mogą być kasowane w przerwaniach.



void TriggerEvent(uint16_t event);  



Ta funkcja jest wywoływana w celu wywołania zdarzenia i usunięcia flagi zdarzenia. Może być używany jako

środki komunikacji między zadaniami lub wywołane z przerwania systemu.



void SetSemaphore(uint16_t *sem);

void ClearSemaphore(uint16_t *sem); 



Te dwie funkcje współpracują ze sobą, aby kontrolować dostęp do 

zasobów lub funkcji. Semafor to taki przełącznik zadań w dostępie do jakiejś funkcji lub innego zadania. Przykładowo jeśli jakaś funkcja jest wywołana lub współużytkowana przez co najmniej dwa zadania, w takim przypadku musi  być jakaś kontrola dostępu aby dwa zadania równocześnie nie miały dostępu do jednej i tej samej funkcji lub zasobu. Semafory umożliwiają taką kontrolę. Jeśli semafor jest ustawiony to zadanie jest wstrzymywane , jeśli semafor skaskujemy to zadanie jest gotowe do realizacji.

Semafor binarny jest wykorzystywany do synchronizacji oraz wzajemnego wykluczania zadań.
W picRTOS funkcje semafora służą do sterowania zadaniami , które nie mogą być wywołany z peryferyjnego przerwania. We freeRTOS semafory są znacznie bardziej rozbudowane i mają możliwość współpracy z przerwaniami tak to zrozumiałem na obecnym etapie.

void CreateMessage(uint8_t MsgID, uint8_t message_size);
Bool MessageWrite(uint8_t MsgID, void *message);
Bool MessageWaiting(uint8_t MsgID);
Bool MessageRead(uint8_t MsgID, void *message);
Bool ByteWrite(uint8_t MsgID, uint8_t index, uint8_t byte);
uint8_t ByteRead(uint8_t MsgID, uint8_t index);


Te funkcje działają w połączeniu ze sobą w celu przekazania wiadomości pomiędzy zadaniami. Jest to metoda komunikacji między zadaniami. Wiadomość może mieć dowolny typ lub strukturę.


void TraceStack(uint16_t *StackTop);

void TraceTask(uint16_t TaskID, volatile uint16_t *port, uint16_t port_bit);


Funkcje te są używane podczas debugowania kodu jako pomoc w śledzeniu zajętości stosu przez poszczególne zadania.

hmm... jakby to powiedzieć i to już koniec jesli chodzi o funkcje.

I tak z tego nic nie rozumiemy jesli nie pomacamy na przykładach ....

Zreasumujmy na tym etapie co z grupsza wiemy o picRTOS. Możemy sobie wyobrazić , że jest to taka ciuchcia z wagonikami jadąca po okręgu. Na okręgu mamy jedną stację załadunkową. Ciuchcia to core odbowiedzialna za motorykę całego składu. Wagoniki to pojemniki do których wrzucamy węgiel a węgiel to nasze zadania. Na załadunek każdego wagonika w punkcie załadunkowym mamy skończony czas np 5 ms :) Jeśli uda nam się załadować węgiel poniżej 5 ms to wysyłamy komendę do ciuchci (TaskYeld()) aby skład podstawił kolejny wagonik do załadunku. Jeśli załadunek przekroczy czas 5 ms to nastąpią jakieś kombinacje na końcu składu aby nie załadowany węgiel zabrać :). Do zobrazowania przebiegu działania picRTOS bardzo by się przydał analizator stanów logicznych, który umożliwi podgląd jak to wszystko czasowo wygląda. Na razie nie mam takowego. Biblioteka ma w standardzie ustawione piny PB0...PB4 na których zachodzą zmiany stanów w tak uruchomienia zadania i zakończenia, czyli możemy monitorować naraz 5 zadań.

Istotna jeszcze rzecz, że każde zadanie ma przydzielony kawałek swojego stosu czyli jest jakby niezależnym innobytem.

Wiemy , że musimy projekt /program rozważać jak gdyby z innej perspektywy tzn dzielić funkcjonalnie na kawałki czyli zadania. Zadania mogą się ze sobą komunikować ale to nie zmienia faktu , że są funkcjonalnie niezależnymi innobytami.

Zerknijmy na przykłady dołączone do biblioteki czyli Example_1.c do Example_5.c. Pamiętajmy, że każdy z przykładów dodajemy oddzielnie do projektu i zmieniamy jego nazwę na main.c.

Example_1.c - ten przykład obrazuje jak tworzymy zadania, jak wygląda struktura programu etc czyli taka piaskownica. Mamy tutaj utworzonych 5 zadań, które zmienią tylko wartość zmiennych , delaye modelują czas trwania zadania. Wszystko tutaj jest proste klarowne i zrozumiałe. Przykład ten jest idealny do zobrazowania na analizatorze stanów logicznych.

Example_2.c - przykład pokazuje jak stosować Eventy. Event ustawiony w przerwaniu Timer2  steruje zadaniem czyli albo blokuje wykonanie zadania albo odblokowuje. Przykład prosty i czytelny.

Example_3.c - w tym przykładzie można zrozumieć działanie semaforów w picRTOS. Mamy tutaj trzy zadania które odpalają jedną i tą samą funkcję zewnętrzną. Semafor jest ustawiany i dezaktywowany w funkcji zewnętrznej. Jeśli np zadanie nr 0 odpali funkcję zewnętrzną , działania w funkcji się nie skończą a przyjdzie wywołanie tej funkcji  z zadania nr 1 to dopóki nie zostanie dezaktywowany semafor ustawiony na końcu funkcji dopóty żadne inne zadanie nie otrzyma dostępu do funkcji.
Dopóki nie zobaczyłem tego przykładu nie kumałem do końca o co chodzi z tymi semaforami, brawo dla przykładu.

Example_4.c - tutaj zobaczymy jak działają wiadomości. Przesyłana wiadomość może być dowolnego typu  np może to być cała struktura. Za pomocą wiadomości zadania mogą się komunikować ze sobą i wymieniać dane.

Dumałem nad tym jak wykorzystać picRTOS w praktycznym aspekcie ? Wpadłem na myśl , że niegłupim pomysłem byłoby wykorzystanie go do sterowania warstwami w wyświetlaczu znakowym LCD. Z grubsza chodzi o to , żeby stworzyć sobie kilka warstw w pamięci RAM jako oddzielne zadania i jedno zadanie , które będzie sklejać te warstwy i wysyłać do pamięci wyświetlacza LCD. Uzyskamy w ten sposób wielowątkowe sterowanie wyświetlaczem i niesamowitą elastykę. Na poszczególnych warstwach możemy aplikować np menu, czas, animację etc, warstwy mogą być widoczne lub nie nakładać się na siebie przesuwać etc. Ponieważ wszystkie operacje na warstwach następują w buforach w pamięci RAM a tylko efekt końcowy wysyłamy do LCD ,zwiększamy prędkość operacji z wyświetlaczem. Hmm... myślę , że to niegłupi pomysł.

picRTOS jest naprawdę genialnym kawałkiem zoptymalizowanego softu . Potężną zaletą jest prostota implementacji i mały rozmiar biblioteki (1kB), można z jego pomocą zrobić naprawdę niezawodnie działającą multiwątkową aplikację.
Jest idealnym sposobem na łagodne wejście w tematykę RTOS-ów.  Zaczynając przygodę od np potężnego freeRTOS-a można połamać zęby przy pierwszym podejściu i się zrazić do tematu.

Kurcze naprawdę się zakochałem w tej bibliotece :) .Gorąco polecam picRTOS.


Pozdrawiam
picmajster.blog@gmail.com

Linki :


Poniżej pliki core biblioteki niezbędne do działania picRTOS. Pliki zostały przystosowane do współpracy z mikrokontrolerem PIC24HJ128GP502. Plik example_1.c to nasz main.c. Trzon biblioteki to plik asemblerowy multitask.s :

/*--------------------------------------------
  Title       : Header file for multitask.s V3.3  
  Filename    : multitask.h 
    
  Copyright @ Bob The Bass 2012  

----------------------------------------------*/

/*--- Frequency of Instruction Cycle Clock in Megahertz (Fcy) ---*/

#define FREQ_FCY 40.0

/*--- Task time slice in mili Seconds ---*/

#define TIME_SLICE  5.0   

/*--- Macro to calculate Timer period (8:1 Prescale value) ---*/

#define PRESCALE  8.0
#define TMR_PERIOD (((TIME_SLICE)*(1000.0))/((PRESCALE)/(FREQ_FCY)))

/*--- System configuration parameters --*/

#ifndef MULTITASK_H 
#define MULTITASK_H


#include "PosixTypes.h"

/*--- Function pointer type define ---*/

typedef void(*Taskptr)(void);

/*--- Multitask function prototypes ---*/

void CreateTask(Taskptr task, uint16_t stack_size);
void Multitask(uint16_t tmr_period);

void TaskSleep(uint16_t count);
void TaskYield(void);
void DisableInterrupts(void);
void EnableInterrupts(void);

/*--- Event flag functions ---*/

void WaitForEvent(uint16_t event);
void TriggerEvent(uint16_t event);

/*--- Semaphore functions ---*/

void SetSemaphore(uint16_t *sem);
void ClearSemaphore(uint16_t *sem);

/*--- Message passing functions ---*/

void CreateMessage(uint8_t MsgID, uint8_t message_size);
Bool MessageWrite(uint8_t MsgID, void *message);
Bool ByteWrite(uint8_t MsgID, uint8_t index, uint8_t byte);
Bool MessageWaiting(uint8_t MsgID);
Bool MessageRead(uint8_t MsgID, void *message);
uint8_t ByteRead(uint8_t MsgID, uint8_t index);

/*--- Debug and Trace functions ---*/

#ifdef __DEBUG
void TraceStack(uint16_t *StackTop);
void TraceTask(uint16_t TaskID, volatile uint16_t *port, uint16_t port_bit);
#endif
#endif  /* Include gaurd */ 
        
/*--- End of File ---*/
/*--------------------------------------------------------------------
  File Name     : PosixTypes.h 
  Description   : ISO (Posix) typedef file for Pic
                : Misra Rule 6.3 (advisory)                      

  Revision      : 1.0 
  Date          : 21/08/05 
  
----------------------------------------------------------------------*/  

#ifndef POSIXTYPES_H
#define POSIXTYPES_H
            
/*--- Standard type definitions. ---*/

/* Plain char, only to be used for the storage and 
use of character values. Misra Rule 6.1 (Required) */

typedef unsigned char   uint8_t;    /* unsigned 8 bit type definition */
typedef signed char     int8_t;     /* signed 8 bit type definition */
typedef unsigned int    uint16_t;   /* unsigned 16 bit type definition */
typedef signed int      int16_t;    /* signed 16 bit type definition */
typedef unsigned long   uint32_t;   /* unsigned 32 bit type definition */
typedef signed long     int32_t;    /* signed 32 bit type definition */
typedef unsigned int    Bool;       /* Bool type definition */

/*--- Standard constant definition. ---*/

#define False ((Bool)0x0000U)
#define True  ((Bool)0x0001U)

#endif

/*--- End of file. ---*/
/*-------------------------------------------------

Title       : Multitasking kernel V3.3 
Filename    : multitask.s

Copyright @ Bob The Bass 2012 
      
---------------------------------------------------*/
/* ONLY! uncomment this line for PIC24F types with DSRPAG and DSWPAG instead of PSVPAG */

        ;.equiv NO_PSV_PAGE,1 

/*--- Select include files ---*/
        .ifdef __dsPIC30F
        .include "p30fxxxx.inc"
        .equ VALID_INC,1
        .equiv DSPIC_TYPE,1
        .endif

        .ifdef __dsPIC33F
        .include "p33Fxxxx.inc"
        .equ VALID_INC,1
        .equiv DSPIC_TYPE,1
        .endif

        .ifdef __PIC24F
        .include "p24Fxxxx.inc"
        .equ VALID_INC,1
        .endif

        .ifdef __PIC24H
        .include "p24Hxxxx.inc"
        .equ VALID_INC,1
        .endif

        .ifndef VALID_INC
        .error "Generic include file not found?"
        .endif

/*--- Macros for Timer interrupt ---*/

        .macro  StartTaskTimer      ;Initialise task timer
        mov     w0,PR1              ;TMR_PERIOD to timer period register
        clr     TMR1                ;Clear timer

        bset    IPC0,#T1IP0         ;Set timer priority to 1  
        bclr    IPC0,#T1IP1         
        bclr    IPC0,#T1IP2

        bclr    IFS0,#T1IF          ;Clear interrupt flag
        bset    IEC0,#T1IE          ;Enable interrupt

        mov     #0x8010,w0          
        mov     w0,T1CON            ;Start Timer, Prescale 8:1
        .endm

        .macro  ClearInterrupt
        bclr    IFS0,#T1IF          ;Clear interrupt flag
        .endm

        .macro  StartTasking        ;Enable Tasking interrupt
        bset    IEC0,#T1IE          ;Set interrupt enable flag 
        .endm   

        .macro  StopTasking         ;Disable Tasking interrupt
        bclr    IEC0,#T1IE          ;Clear interrupt enable flag
        .endm

/*--- Global Task Functions ---*/

        .global __T1Interrupt 
        .global _CreateTask
        .global _Multitask

        .global _TaskSleep
        .global _TaskYield
        .global _DisableInterrupts
        .global _EnableInterrupts
        
/*--- Global Debug and Trace functions ---*/

        .ifdef __DEBUG
        .global _TraceTask
        .global _TraceStack
        .endif       

/*--- Global support functions ---*/

        .global _WaitForEvent
        .global _TriggerEvent

        .global _SetSemaphore
        .global _ClearSemaphore

/*--- Global message functions ---*/

        .global _CreateMessage
        .global _MessageWrite
        .global _ByteWrite
        .global _MessageWaiting
        .global _MessageRead
        .global _ByteRead

/*------ Task state constants ------*/

        .equiv  READY,      0
        .equiv  BLOCKED,    1
        .equiv  WAITING,    2     
        .equiv  ASLEEP,     3

/*--- Offsets into task structure ---*/

        .equiv  STATUS,     4
        .equiv  PENDING,    6
        .equiv  EVENTFLAGS, 8
        .equiv  SLEEPING,   10

        .ifdef __DEBUG
        .equiv  TRACEPORT,  12  
        .equiv  TRACEBIT,   14  
        .endif   

/*--- Offsets into message structure ---*/

        .equiv  MSGID,      2
        .equiv  MSGSIZE,    4
        .equiv  MESSAGE,    6

/*--- Uninitialized ram allocation. ---*/

        .bss  
        StackTop:       .space 2 
        Critical:       .space 2
                  
/*--- Initialized ram allocation. ---*/

        .data
        TaskList:       .int 0
        MessageList:    .int 0 

        .ifdef __DEBUG
        TraceBuffer:    .int 0
        .endif   
            
/*--- Program entry ---*/

        .text 
              
/*--- Create Task structure and initialise task stack ---*/

_CreateTask:
        sl      w1,w1               ;Allign, stack depth to bytes
        mov     w15,w14             ;Save stack pointer
        sub     w14,#4,w14          ;Point to callers return address
        mov     w14,w4              ;Start of free memory
        add     w1,w15,w15          ;Add stack size to stack pointer
        mov     w15,w12             ;Save top of task stack
        repeat  #1
        mov     [w14++],[w15++]     ;Shift callers return address and SR
        mov     [w14],[w15]         ;To new stack pointer
        
        mov     w4,Critical         ;Use variable to save task pointer
        cp0     TaskList            ;First stack in list? 
        bra     nz,1f
        mov     w4,TaskList         ;Intialise TaskList
        clr     w5                  ;Clear ID counter
        
1:      mov     w12,[w4++]          ;Pointer to next task        
        mov     w4,StackTop         ;Save pointer to stack top
        mov     w4,[w4++]           

        mov     w5,w2               ;ID counter
        inc     w5,w5               ;Increment ID counter
        swap    w2                  ;ID to hi-byte
        mov.b   #READY,w2
        mov     w2,[w4++]           ;Task Status to ready
        add     #6,w4               ;Task flags space

.ifdef __DEBUG
        add     #4,w4               ;Trace bits space
.endif
        mov     w0,[w4++]           ;Pointer to task function
        clr     [w4++]              ;High byte of function pointer

        mov     #36,w1              ;Register stack space

.ifdef DSPIC_TYPE
        add     #22,w1              ;Add dsp register stack space
.endif

        add     w1,w4,w4            ;Add size of registers to task structure
        mov     _CORCON,w1          
        mov     w1,[w4++]           ;Intialise CORCON

.ifdef NO_PSV_PAGE                  ;PIC24F types with no PSVPAG
        mov     _DSRPAG,w1          
        mov     w1,[w4++]           ;Intialise DSRPAG
        mov     _DSWPAG,w1          
        mov     w1,[w4++]           ;Intialise DSWPAG
.else
        mov     _PSVPAG,w1          
        mov     w1,[w4++]           ;Intialise PSVPAG
.endif
    
        mov     StackTop,w1         ;Get pointer to stack top       
        mov     w4,[w1]             ;Intialise stack pointer    
        return

/*--- Start Timer and Multitasking ---*/

_Multitask:
        StartTaskTimer              ;Initialise task timer
        mov     Critical,w2         ;Save last task address  
        mov     TaskList,w0         ;Pointer to first Task        
        mov     w0,[w2]             ;Create linked list

.ifdef __DEBUG
        rcall   SetPortBit          ;Set Trace bit
.endif  

        mov     w0,StackTop         ;Save Stack address
        inc2    StackTop            ;Point to Task structure
        add     #16,w0              ;Offset to Task function pointer

.ifdef __DEBUG
        add     #4,w0               ;Trace Port and Trace Bit
.endif
        mov     w0,w15              ;Initialise stack with Task function address          
        retfie                      ;Start Multitasking

/*--- Task sleep ---*/

_TaskSleep:
        mov     StackTop,w2         ;Load task pointer
        dec2    w2,w2               ;Point to task structure        
        mov     w0,[w2+SLEEPING]    ;Save sleep time   
        mov     #ASLEEP,w0          ;Change status to Sleeping
        mov.b   w0,[w2+STATUS]      ;Fall through to yield  

/*--- Task yield ---*/

_TaskYield:
        bset    SR,#5               ;Set interrupt priority level to 1
        clr     TMR1                ;Clear timer for next task
               
/*--- Task scheduler Timer interrupt ---*/
        
__T1Interrupt:
        ClearInterrupt              ;Clear interrupt flag           
        push    SR                  
        push    w0                           
        mov     #0xe0,w0
        ior.b   SR                  ;Disable interrupts          
        push    w1
        push.d  w2
        push.d  w4
        push.d  w6
        push.d  w8
        push.d  w10
        push.d  w12
        push    w14
        push    RCOUNT
        push    TBLPAG

.ifdef DSPIC_TYPE
        push    ACCAL
        push    ACCAH
        push    ACCAU
        push    ACCBL
        push    ACCBH
        push    ACCBU
        push    DCOUNT
        push    DOSTARTL
        push    DOSTARTH
        push    DOENDL
        push    DOENDH  
.endif
        push    CORCON

.ifdef NO_PSV_PAGE
        push    DSRPAG
        push    DSWPAG
.else
        push    PSVPAG
.endif

        mov     StackTop,w0         ;Task top of stack
        mov     w15,[w0]            ;Move top of stack to pointer

/*--- Task scheduler ---*/

        dec2    w0,w0               ;Pointer to current task
                
.ifdef __DEBUG
        rcall   ClearPortBit        ;Clear Trace bit
        cp0     TraceBuffer         ;Test for trace buffer
        bra     z,1f                ;Trace buffer not allocated
        sub     w15,w0,w1           ;Get stack depth
        rcall   SaveStackDepth      ;Save maximum stack depth
.endif    
                 
/*--- Loop to find next ready task ---*/

1:      mov     [w0],w0             ;Pointer to next task
        mov.b   [w0+STATUS],w1      ;Task status
        cp.b    w1,#READY           ;Test if ready state
        bra     z,StartTask         ;Start task

        cp.b    w1,#ASLEEP          ;Test if sleeping
        bra     nz,1b

        mov     [w0+SLEEPING],w1    ;Get timer value
        cp0     w1
        bra     z,2f                ;Check if timed out

        dec     w1,w1               ;Decrement timer
        mov     w1,[w0+SLEEPING]    ;Update timer value
        bra     1b                  ;Next task
             
2:      mov     #READY,w1           ;Status to ready    
        mov.b   w1,[w0+STATUS]      ;Start task    
    
                               
/*--- Start next Task ---*/

StartTask:
                
.ifdef __DEBUG       
        rcall   SetPortBit          ;Set tasks trace bit
.endif  

        inc2    w0,w0               ;Pointer to stack top
        mov     w0,StackTop         ;Save pointer for next time
        mov     [w0],w15            ;Initialise stack pointer

/*--- Restore context ---*/

.ifdef NO_PSV_PAGE
        pop     DSWPAG
        pop     DSRPAG
.else
        pop     PSVPAG
.endif

        pop     CORCON

.ifdef DSPIC_TYPE
        pop     DOENDH
        pop     DOENDL
        pop     DOSTARTH
        pop     DOSTARTL
        pop     DCOUNT
        pop     ACCBU
        pop     ACCBH
        pop     ACCBL
        pop     ACCAU
        pop     ACCAH
        pop     ACCAL
.endif
        pop     TBLPAG
        pop     RCOUNT  
        pop     w14
        pop.d   w12
        pop.d   w10
        pop.d   w8
        pop.d   w6
        pop.d   w4
        pop.d   w2
        pop.d   w0
        pop     SR
        retfie

/*--- Disable interrupts ---*/

_DisableInterrupts:
        push    w0
        mov     SR,w0
        mov     w0,Critical         ;Save status register
        mov     #0xe0,w0            ;Set interrupt bits
        ior.b   SR                  ;Disable interrupts
        pop     w0
        return

/*--- Enable interrupts ---*/

_EnableInterrupts:
        push    w0
        mov     Critical,w0         ;Restore status register
        mov     w0,SR
        pop     w0
        return

/*--- Set Wait for event flag ---*/

_WaitForEvent:
        StopTasking                 ;Stop Task switching
        mov     StackTop,w2         ;Load task pointer
        dec2    w2,w2               ;Point to task structure        
        mov     w0,[w2+EVENTFLAGS]  ;Save Flags       
        mov     #WAITING,w0
        mov.b   w0,[w2+STATUS]      ;Change status to Waiting

        StartTasking                ;Resume task switching
        rcall   _TaskYield          ;Force task switch
        return
               
/*--- Trigger event ---*/

_TriggerEvent:
        StopTasking                 ;Stop Task switching
        mov     StackTop,w1         ;Load running task pointer 
        dec2    w1,w1               ;Pointer to task struture            
        mov     w1,w2               ;w2 as task list end 
        mov     [w1],w1             ;Next task in list
        com     w0,w0               ;Flags to clear mask 

1:      mov.b   [w1+STATUS],w3      ;Get task status
        cp.b    w3,#WAITING         ;Test if waiting
        bra     nz,2f               ;Not waiting
        mov     [w1+EVENTFLAGS],w3  ;Get task flags
        and     w0,w3,w3            ;Mask flags               
        mov     w3,[w1+EVENTFLAGS]  ;Save result
        bra     nz,2f               ;Test if all clear

        mov     #READY,w3           ;All flags clear
        mov.b   w3,[w1+STATUS]      ;Change status to Ready

2:      mov     [w1],w1             ;Next task in list
        cp      w1,w2               ;Task list end?
        bra     nz,1b      

        StartTasking                ;Resume task switching
        return
    
/*--- Set Semaphore ---*/

_SetSemaphore:
        StopTasking                 ;Stop Task switching
        mov     StackTop,w1         ;Load running task pointer 
        dec2    w1,w1               ;Pointer to current task
        mov     w0,[w1+PENDING]     ;Semaphore passed in w0 
        cp0     [w0]                ;Dereference pointer
        bra     z,2f                ;Semaphore is free? 
      
        mov     #BLOCKED,w2         ;Semaphore is not free
        mov.b   w2,[w1+STATUS]      ;Change status to Blocked
     
1:      StartTasking                ;Restart kernel
        rcall   _TaskYield          ;Force task switch
   
        StopTasking                 ;Stop Task switching
        cp0     [w0]                ;Check if semaphore clear
        bra     z,2f                ;Semaphore is clear

        mov     #BLOCKED,w2         ;Semaphore is not free
        mov.b   w2,[w1+STATUS]      ;Keep status Blocked
        bra     1b                  ;Task yield
             
2:      inc     [w0],[w0]           ;Set Semaphore
        StartTasking                ;Resume Task switching
        return      

/*--- Clear Semaphore ---*/

_ClearSemaphore:
        StopTasking                 ;Stop Task switching                 
        mov     StackTop,w1         ;Load running task pointer 
        dec2    w1,w1               ;Pointer to current task            
        mov     w1,w4               ;Save as task list end
        mov     [w1],w1             ;Pointer to next task       
        clr     [w0]                ;Clear Semaphore 

1:      mov.b   [w1+STATUS],w2      ;Task state 
        cp.b    w2,#BLOCKED         ;Is task blocked?
        bra     nz,2f               ;Not blocked
       
        mov     [w1+PENDING],w2     ;Task semaphore     
        cp      w0,w2               ;This Semaphore?
        bra     nz,2f               ;Not this semaphore

        mov     #READY,w2           ;Change status to Ready
        mov.b   w2,[w1+STATUS]  
  
2:      mov     [w1],w1             ;Pointer to next task
        cp      w1,w4               ;Task list end?
        bra     nz,1b

        StartTasking                ;Resume Task switching
        return

/*--- Create Message Box ---*/

_CreateMessage:
        ze      w1,w1               ;Clear high byte of message size
        btsc    w1,#0               ;Test message size for allignment
        inc     w1,w1               ;Allign, make even     
        add     w1,#6,w3            ;Add size of message structure
        mov     SPLIM,w2            ;Stack limit
        inc2    w2,w2               ;Last message address
        sub     w2,w3,w2            ;Subtract from last message address
        mov     MessageList,w4      ;Pointer to next message
        mov     w4,[w2]             ;Save pointer
        mov     w2,MessageList      ;Update start of list
        dec2    w2,w4               ;Set new stack limit
        mov     w4,SPLIM
        ze      w0,w0               ;Clear high byte of Message ID
        mov     w0,[++w2]           ;Save message ID
        mov     w1,[++w2]           ;Save message size     
        return

/*--- Write message ---*/

_MessageWrite:
        rcall   FindMessage         ;Get pointer to message
        push    SR                  ;Save status register   
        rcall   Message_SetUp       ;Set up for write        
        repeat  w3                  ;Message size counter
        mov.b   [w1++],[w0++]       ;Message in w1
        mov     #1,w0               ;Set write flag
        mov.b   w0,[w4+MSGID+1]     ;Save flag
        pop     SR                  ;Restore status register       
        return

/*--- Write a byte to the message buffer ---*/

_ByteWrite:
        rcall   FindMessage         ;Get pointer to message
        push    SR                  ;Save status register                            
        rcall   Message_SetUp       ;Set up for write  
        cp.b    w1,w3               ;Check index within bounds
        bra     gtu,1f              ;Index out of bounds
        add.b   w1,w0,w0            ;Add index to message pointer
        mov.b   w2,[w0]             ;Write byte
        mov     #1,w0               ;Return 1
        bra     2f

1:      clr     w0                  ;Byte not written
2:      pop     SR                  ;Restore status register 
        return

/*--- Read message ---*/

_MessageRead:
        rcall   FindMessage         ;Get pointer to message
        push    SR                  ;Save status register   
        rcall   Message_SetUp       ;Set up for read 
        repeat  w3                  ;Message size counter
        mov.b   [w0++],[w1++]       ;Move message to w1
        clr     w3                  ;Clear read flag
        mov.b   w3,[w4+MSGID+1]     ;Save flag  
        mov     #1,w0               ;Return 1
        pop     SR                  ;Restore status register    
        return

/*--- Read a byte from the message buffer ---*/

_ByteRead:
        rcall   FindMessage         ;Get pointer to message
        push    SR                  ;Save status register 
        rcall   Message_SetUp       ;Set up for read
        cp.b    w1,w3               ;Check index within bounds
        bra     gtu,1f              ;Index out of bounds
        add.b   w1,w0,w0            ;Add index to message pointer                           
        mov.b   [w0],w0             ;Read byte
        bra     2f

1:      clr     w0                  ;Byte not read
2:      pop     SR                  ;Restore status register 
        return

/*--- Check message box status ---*/

_MessageWaiting:
        rcall   FindMessage         ;Get pointer to message
        clr     w0
        mov.b   [w4+MSGID+1],w0     ;Message flag value
        return

/*--- Find message from ID ---*/

FindMessage:
        mov     MessageList,w4      ;Pointer to message list
1:      mov.b   [w4+MSGID],w3       ;Message ID
        cp.b    w0,w3               ;This message?
        bra     z,2f                ;Message found
        mov     [w4],w4             ;Next message in list
        cp0     w4                  ;List end?
        bra     nz,1b

        pop.d   w0                  ;skip return address
        clr     w0                  ;Message not found
2:      return

/*--- Set up message size and pointers ---*/

Message_SetUp:                         
        mov     #0xe0,w0
        ior.b   SR                  ;Disable interrupts 
        add     w4,#MESSAGE,w0      ;Pointer to message 
        mov     [w4+MSGSIZE],w3     ;Message size
        dec     w3,w3               ;Adjust size for repeat loop
        return

/*--- Debug and trace functions ---*/ 

.ifdef __DEBUG

/*--- Initialise Trace buffer address ---*/

_TraceStack:
        mov     w0,TraceBuffer      ;Pointer to Stack Trace array
        return

/*--- Save maximum used stack depth ---*/

SaveStackDepth:
        mov     [w0+STATUS],w2     
        swap    w2                  ;Get task ID
        ze      w2,w2               ;Clear Hi-byte
        sl      w2,w2               ;Allign

        mov     TraceBuffer,w3
        add     w2,w3,w3            ;Buffer address
        mov     [w3],w2             ;Last used stack depth

        sl      w2,w2               ;Change to bytes    
        cp      w1,w2               ;Current used stack depth 
        bra     leu,1f              ;Compare
        asr     w1,#1,w1            ;If greater change to depth size
        mov     w1,[w3]             ;Save largest stack depth
1:      return

/*--- Initialise Task trace port and bit ---*/

_TraceTask:
        mov     TaskList,w3         ;First task address
        cp0     w0                  ;Task ID
        bra     z,2f
1:      mov     [w3],w3             ;Next task in list
        dec     w0,w0               
        bra     nz,1b

2:      mov     w1,[w3+TRACEPORT]   ;Address of Port
        mov     #1,w4
        sl      w4,w2,w4            ;Port trace Bit        
        mov     w4,[w3+TRACEBIT]
        return

/*--- Set Trace port bit ---*/

SetPortBit:
        mov     [w0+TRACEPORT],w1   ;Address of Port
        cp0     w1
        bra     z,1f                ;Test if valid
        mov     [w0+TRACEBIT],w2    ;Port Bit
        mov     [w1],w3             ;Port value
        ior     w3,w2,w3            ;Set bit with mask
        mov     w3,[w1]             ;Update port value             
1:      return

/*--- Clear Trace port bit ---*/

ClearPortBit:
        mov     [w0+TRACEPORT],w1   ;Address of Port
        cp0     w1
        bra     z,1f                ;Test if valid
        mov     [w0+TRACEBIT],w2    ;Port Bit
        com     w2,w2               ;Clear bitmask
        mov     [w1],w3             ;Port value
        and     w3,w2,w3            ;Clear bit with mask
        mov     w3,[w1]             ;Update port value             
1:      return

.endif                              ;End of conditional debug compile
        .end                        ;End of program code

/*--- End of file. ---*/
/*--------------------------------------------------------------------

  Title       : Create Task Demo 
  Filename    : Example_1.c 
    
  Copyright @ Bob The Bass 2012 
    
--------------------------------------------------------------------*/

/*--- Using a Pic24HJ128GP502 ---*/
#include "xc.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h> /*dyrektywy uint8_t itp*/
#include <string.h>
#include "ustaw_zegar.h" /*tutaj m.in ustawione FCY*/
#include <libpic30.h> /*dostep do delay-i,musi byc po zaincludowaniu ustaw_zegar.h*/
#include "multitask.h" 
/*--- Configuration fuses ---*/

/*--- Trace maximum stack usage ---*/  

#ifdef __DEBUG
#define NUM_TASKS 5U
uint16_t UsedStack[NUM_TASKS]; 
#endif

/*--- Task function prototypes ---*/

void task_0(void);
void task_1(void);
void task_2(void);
void task_3(void);
void task_4(void);

/*--- Local function prototypes ---*/

void init_io(void);
void Delay(uint16_t delay);

/*--- Global variables ---*/

uint16_t var0 = 0;
uint16_t var1 = 0;
uint16_t var2 = 0;
uint16_t var3 = 0;
uint16_t var4 = 0;

/*--- Program Entry ---*/

int main(void)
  {
  ustaw_zegar(); /*odpalamy zegar wewnetrzny na ok 40MHz*/
//  __delay_ms(50); /*stabilizacja napiec*/
  init_io();

  CreateTask(task_0, 56);   /* All task create functions must be called from main() */
  CreateTask(task_1, 56);   /* in one unbroken sequence */
  CreateTask(task_2, 56);  
  CreateTask(task_3, 56);
  CreateTask(task_4, 56);  
  
  #ifdef __DEBUG
  TraceStack(&UsedStack[0]);
  TraceTask(0, &PORTB, 0);
  TraceTask(1, &PORTB, 1);
  TraceTask(2, &PORTB, 2);
  TraceTask(3, &PORTB, 3);
  TraceTask(4, &PORTB, 4);
  #endif

  Multitask(TMR_PERIOD);
  return 0;
  }

/*--- Task 0 ---*/

void task_0(void)
  {  
  while(1)
    {
    Delay(5);
    var0++; 
    }   
  }

/*--- Task 1 ---*/

void task_1(void)
  {  
  while(1)
    {
    Delay(5);
    TaskSleep(2);
    var1++;
    }
  }

/*--- Task 2 ---*/

void task_2(void)
  {
  while(1)
    {
    Delay(15);
    var2++;    
    }
  }

/*--- Task 3 ---*/

void task_3(void)
  {
  while(1)
    {
    Delay(5);
    var3++;
    }
  }

/*--- Task 4 ---*/

void task_4(void)
  {
  while(1)
    {
    Delay(20);
    var4--;
    }
  }

/*--- Re-Entrant delay function ---*/

void Delay(uint16_t delay)
  {
  uint16_t timer;

  while(delay--)
    {
    timer = 1000;
    while(timer--){
      Nop();
      }
    }
  }

/*--- Initialise io port ---*/

void init_io(void)
  {
  AD1PCFGL = 0x1E3F;
  PORTB = 0;
  TRISB = 0;      /* All outputs */
  }

/*--- Stack underflow/overflow trap  ---*/

#ifdef __DEBUG

void __attribute__((__interrupt__, no_auto_psv)) _StackError(void)
  {
  for(;;){
    Nop();
    }
  }

/*--- Address error trap  ---*/

void __attribute__((__interrupt__, no_auto_psv)) _AddressError(void)
  {
  for(;;){
    Nop();
    }
  }

#endif

/*--- End of File ---*/

3 komentarze:

  1. Jestem pod wrażeniem. Bardzo fajny wpis.

    OdpowiedzUsuń
  2. Dziękuję za uznanie , sam z ciekawością przeczytałem po 3 latach ten artykuł.

    OdpowiedzUsuń
  3. Inny ciekawy small RTOS dla MCU PIC - http://wiki.pic24.ru/doku.php/en/tnkernel/ref/intro z pełną dokumentacją.

    OdpowiedzUsuń