Wielozadaniowość na mikrokontrolerach PIC


17

Wielozadaniowość jest obecnie ważna. Zastanawiam się, jak możemy to osiągnąć w mikrokontrolerach i programowaniu wbudowanym. Projektuję system oparty na mikrokontrolerze PIC. Zaprojektowałem jego oprogramowanie w MplabX IDE za pomocą C, a następnie zaprojektowałem dla niego aplikację w Visual Studio przy użyciu C #.

Skoro przyzwyczaiłem się do używania wątków w programowaniu w języku C # na pulpicie do realizacji zadań równoległych, czy istnieje sposób, aby zrobić to samo w kodzie mikrokontrolera? MplabX IDE zapewnia, pthreads.hale jest to tylko skrót bez implementacji. Wiem, że istnieje obsługa FreeRTOS, ale korzystanie z niej sprawia, że ​​kod jest bardziej złożony. Niektóre forum mówi, że przerwania mogą być również używane jako wielozadaniowość, ale nie sądzę, że przerwania są równoważne wątkom.

Projektuję system, który wysyła niektóre dane do UART, a jednocześnie musi wysyłać dane do strony internetowej za pośrednictwem (przewodowego) Ethernetu. Użytkownik może kontrolować wyjście za pośrednictwem strony internetowej, ale wyjście włącza się / wyłącza z opóźnieniem 2-3 sekund. To jest problem, przed którym stoję. Czy jest jakieś rozwiązanie do wielozadaniowości w mikrokontrolerach?


Wątków można używać tylko na procesorach z systemem operacyjnym, ponieważ wątki są częścią procesu, a procesy są używane tylko w systemach operacyjnych.
Kolko_i_Krzyzyk

@Zola tak masz rację. Ale co w przypadku kontrolerów?
Samoloty


1
Czy możesz wyjaśnić, dlaczego potrzebujesz prawdziwej wielozadaniowości i nie możesz racjonalnie wdrożyć swojego oprogramowania w oparciu o podejście do zadań typu round-robin lub pętli select () lub podobnej?
whatsisname

2
Cóż, jak już powiedziałem, wysyłam i odbieram dane do UART, a jednocześnie wysyłam i odbieram dane do sieci Ethernet. Oprócz tego muszę również zapisywać dane na karcie SD wraz z czasem, więc tak, dotyczy DS1307 RTC i EEPROM. Do tej pory mam tylko 1 UART, ale może za kilka dni będę wysyłać i odbierać dane z 3 modułów UART. Strona otrzyma również dane z 5 różnych systemów zainstalowanych w zdalnym miejscu. Wszystko to musi być równoległe, ale nie nie równoległe, ale z opóźnieniem kilku sekund. !
Samoloty

Odpowiedzi:


20

Istnieją dwa główne typy wielozadaniowych systemów operacyjnych: zapobiegawcze i kooperacyjne. Oba pozwalają na zdefiniowanie wielu zadań w systemie, różnica polega na tym, jak działa przełączanie zadań. Oczywiście w przypadku jednego procesora lokalnego w danym momencie działa tylko jedno zadanie.

Oba typy systemów wielozadaniowych wymagają osobnego stosu dla każdego zadania. Oznacza to dwie rzeczy: po pierwsze, procesor pozwala na umieszczanie stosów w dowolnym miejscu w pamięci RAM, a zatem ma instrukcje do przesuwania wskaźnika stosu (SP) - tzn. Nie ma stosu specjalnego przeznaczenia, takiego jak na niskim poziomie PIC. Pomija to serię PIC10, 12 i 16.

Możesz napisać system operacyjny prawie całkowicie w C, ale przełącznik zadań, w którym SP się porusza, musi być w zestawie. W różnych momentach pisałem przełączniki zadań dla PIC24, PIC32, 8051 i 80x86. Odwagi są całkiem różne w zależności od architektury procesora.

Drugim wymaganiem jest wystarczająca ilość pamięci RAM, aby zapewnić wiele stosów. Zwykle ktoś chciałby mieć kilkaset bajtów na stos; ale nawet przy zaledwie 128 bajtach na zadanie osiem stosów będzie wymagało 1 KB bajtów pamięci RAM - nie trzeba jednak przydzielać stosu tego samego rozmiaru dla każdego zadania. Pamiętaj, że potrzebujesz stosu wystarczającego do obsłużenia bieżącego zadania i wszystkich wywołań do jego zagnieżdżonych podprogramów, ale także stosu miejsca na wywołanie przerwania, ponieważ nigdy nie wiadomo, kiedy nastąpi.

Istnieją dość proste metody określania, ile stosu używasz do każdego zadania; na przykład możesz zainicjować wszystkie stosy do określonej wartości, powiedzmy 0x55, i uruchomić system na chwilę, a następnie zatrzymać i zbadać pamięć.

Nie mówisz, jakiego rodzaju PIC chcesz użyć. Większość PIC24 i PIC32 będzie miało dużo miejsca na system operacyjny wielozadaniowy; PIC18 (jedyny 8-bitowy PIC posiadający stosy w pamięci RAM) ma maksymalny rozmiar pamięci RAM wynoszący 4K. Więc to dość niepewne.

W przypadku wielozadaniowości kooperacyjnej (prostszej z dwóch) przełączanie zadań odbywa się tylko wtedy, gdy zadanie „rezygnuje” z kontroli nad systemem operacyjnym. Dzieje się tak za każdym razem, gdy zadanie musi wywołać procedurę systemu operacyjnego, aby wykonać funkcję, na którą będzie czekał, na przykład żądanie We / Wy lub wywołanie timera. Ułatwia to systemowi przełączanie stosów, ponieważ nie jest konieczne zapisywanie wszystkich rejestrów i informacji o stanie, SP można po prostu przełączyć na inne zadanie (jeśli nie ma innych zadań gotowych do uruchomienia, stos bezczynności jest podana kontrola). Jeśli bieżące zadanie nie musi nawiązywać połączenia z systemem operacyjnym, ale działa przez jakiś czas, musi dobrowolnie zrezygnować z kontroli, aby system mógł reagować.

Problem ze współpracującą wielozadaniowością polega na tym, że jeśli zadanie nigdy nie poddaje się kontroli, może zapchać system. Tylko on i wszelkie procedury przerwań, które przypadkowo otrzymają kontrolę, mogą działać, więc system operacyjny wydaje się blokować. Jest to aspekt „kooperacyjny” tych systemów. Jeśli zaimplementowany jest zegar nadzoru, który jest resetowany tylko po wykonaniu przełączenia zadania, możliwe jest przechwycenie tych błędnych zadań.

Windows 3.1 i wcześniejsze były współpracującymi systemami operacyjnymi, dlatego częściowo ich wydajność nie była tak świetna.

Zapobiegawcza wielozadaniowość jest trudniejsza do wdrożenia. Tutaj zadania nie są wymagane do ręcznego zrezygnowania z kontroli, ale zamiast tego każdemu zadaniu można poświęcić maksymalny czas na uruchomienie (powiedzmy 10 ms), a następnie przełącza się zadanie na następne zadanie, jeśli takie istnieje. Wymaga to arbitralnego zatrzymania zadania, zapisania wszystkich informacji o stanie, a następnie przełączenia SP do innego zadania i uruchomienia go. To sprawia, że ​​przełącznik zadań jest bardziej skomplikowany, wymaga większego stosu i nieco spowalnia system.

Zarówno w przypadku wielozadaniowości kooperacyjnej, jak i zapobiegawczej, przerwania mogą wystąpić w dowolnym momencie, co tymczasowo wstrzyma uruchomione zadanie.

Jak zauważa supercat w komentarzu, jedną z zalet wielozadaniowości kooperacyjnej jest łatwiejsze współdzielenie zasobów (np. Sprzęt taki jak wielokanałowy ADC lub oprogramowanie takie jak modyfikacja połączonej listy). Czasami dwa zadania chcą mieć dostęp do tego samego zasobu w tym samym czasie. Dzięki planowaniu wyprzedzającemu system operacyjny mógłby przełączać zadania w trakcie jednego zadania za pomocą zasobu. Dlatego konieczne są blokady, aby uniemożliwić wejście do innego zadania i dostęp do tego samego zasobu. W przypadku wielozadaniowości kooperacyjnej nie jest to konieczne, ponieważ zadanie kontroluje, kiedy zwolni je z powrotem do systemu operacyjnego.


3
Zaletą wielozadaniowości kooperacyjnej jest to, że w większości przypadków nie jest konieczne stosowanie zamków do koordynowania dostępu do zasobów. Wystarczy upewnić się, że zadania zawsze pozostawiają zasoby w stanie do udostępniania, ilekroć zrzekną się kontroli. Zapobiegawcza wielozadaniowość jest znacznie bardziej skomplikowana, jeśli zadanie może zostać wyłączone, gdy blokuje zasoby potrzebne do wykonania innego zadania. W niektórych przypadkach drugie zadanie może zostać zablokowane na dłużej niż byłoby w systemie kooperacyjnym, ponieważ zadanie polegające na utrzymaniu zamka poświęciłoby system ...
supercat

1
... pełne zasoby do ukończenia akcji, która (w systemie wyprzedzającym) wymagałaby blokady, udostępniając strzeżony obiekt do drugiego zadania.
supercat,

1
Chociaż wielozadaniowość kooperacyjna wymaga dyscypliny, zapewnienie spełnienia wymagań czasowych może czasem być łatwiejsze w przypadku wielozadaniowości kooperacyjnej niż w przypadku czynności zapobiegawczych. Ponieważ bardzo niewiele blokad będzie musiało być utrzymywanych na przełączniku zadań, pięciozadaniowy, okrągły, robinowy system przełączania zadań, w którym zadania nie muszą przekraczać 10 ms bez ustępowania, w połączeniu z małą logiką, która mówi „Jeśli zadanie X pilnie musi uruchomić, uruchom go dalej ”, zapewni, że zadanie X nigdy nie będzie musiało czekać dłużej niż 10 ms, gdy zasygnalizuje, zanim zacznie działać. Natomiast jeśli zadanie wymaga blokady, które zadanie X ...
supercat

1
... będzie potrzebował, ale zostanie wyłączony przez przełącznik wyprzedzający przed jego zwolnieniem, X może nie zrobić nic użytecznego, dopóki planista CPU nie uruchomi pierwszego zadania. O ile program planujący nie zawiera logiki do rozpoznawania i obsługi inwersji priorytetów, może minąć trochę czasu, zanim zacznie pozwalać pierwszemu zadaniu zakończyć działalność i zwolnić blokadę. Takie problemy nie są nierozwiązywalne, ale ich rozwiązanie wymaga dużej złożoności, której można by uniknąć w systemie współpracy. Systemy kooperacyjne działają świetnie oprócz jednego gotcha: ...
supercat

3
nie potrzebujesz wielu stosów w kooperacji, jeśli kodujesz w kontynuacjach. Zasadniczo twój kod jest podzielony na funkcje, void foo(void* context)logika kontrolera (jądro) pobiera jedną parę wskaźnika i parę wskaźników funkcji kolejki i wywołuje je pojedynczo. Ta funkcja używa kontekstu do przechowywania swoich zmiennych i tym podobnych, a następnie może dodać przesłanie kontynuacji do kolejki. Funkcje te muszą szybko powrócić, aby umożliwić innym zadaniom ich moment w CPU. Jest to metoda oparta na zdarzeniach, wymagająca tylko jednego stosu.
maniak zapadkowy

16

Wątek jest zapewniany przez system operacyjny. W świecie osadzonym zwykle nie mamy systemu operacyjnego („bare metal”). Pozostawia to następujące opcje:

  • Klasyczna główna pętla wyborcza. Twoja główna funkcja ma chwilę (1), która wykonuje zadanie 1, a następnie wykonuje zadanie 2 ...
  • Pętla główna + flagi ISR: Masz ISR, który wykonuje funkcję krytyczną czasowo, a następnie powiadamia główną pętlę za pomocą zmiennej flagi, że zadanie wymaga obsługi. Być może ISR umieszcza nowy znak w okrągłym buforze, a następnie mówi głównej pętli, aby obsłużyła dane, gdy jest na to gotowa.
  • Cały ISR: duża część logiki jest wykonywana z ISR. Na nowoczesnym kontrolerze takim jak ARM, który ma wiele poziomów priorytetu. Może to zapewnić potężny schemat „podobny do wątku”, ale może być także mylące podczas debugowania, więc powinno być zarezerwowane tylko dla krytycznych ograniczeń czasowych.
  • RTOS: Jądro RTOS (obsługiwane przez ISR timera) może umożliwiać przełączanie między wieloma wątkami wykonania. Wspomniałeś o FreeRTOS.

Radzę skorzystać z najprostszego z powyższych schematów, który będzie działał dla Twojej aplikacji. Z tego, co opisujesz, chciałbym, aby główna pętla generowała pakiety i umieszczała je w okrągłych buforach. Następnie należy mieć sterownik oparty na UART ISR, który uruchamia się za każdym razem, gdy poprzedni bajt jest wysyłany do momentu wysłania bufora, a następnie czeka na więcej zawartości bufora. Podobne podejście do sieci Ethernet.


3
Jest to bardzo przydatna odpowiedź, ponieważ dotyczy źródła problemu (jak wielozadaniowość w małym systemie osadzonym, a nie wątki jako rozwiązanie). Znakomity byłby akapit o tym, jak można go zastosować do pierwotnego pytania, być może włączając wady i zalety każdego z nich w tym scenariuszu.
David,

8

Tak jak w przypadku każdego jednordzeniowego procesora wykonywanie wielopoziomowego oprogramowania nie jest możliwe. Musisz więc zadbać o przełączanie między wieloma zadaniami w jedną stronę. Zajmują się tym różne RTOS. Mają harmonogram i na podstawie tiku systemowego będą przełączać się między różnymi zadaniami, aby zapewnić możliwość wielozadaniowości.

Pojęcia związane z tym (zapisywanie i przywracanie kontekstu) są dość skomplikowane, więc wykonanie tego ręcznie prawdopodobnie będzie trudne i sprawi, że kod będzie bardziej złożony, a ponieważ nigdy wcześniej tego nie robiłeś, wystąpią błędy. Radzę tu użyć przetestowanego RTOS, takiego jak FreeRTOS.

Wspomniałeś, że przerwania zapewniają poziom wielozadaniowości. To jest trochę prawda. Przerwanie spowoduje przerwanie bieżącego programu w dowolnym momencie i wykonanie kodu w tym miejscu, jest to porównywalne z systemem dwóch zadań, w którym masz 1 zadanie o niskim priorytecie i inne o wysokim priorytecie, które kończą się w jednym segmencie harmonogramu.

Możesz więc napisać moduł obsługi przerwania dla cyklicznego timera, który wyśle ​​kilka pakietów przez UART, a następnie pozwólmy, aby reszta twojego programu działała przez kilka milisekund i wysyłała kolejne kilka bajtów. W ten sposób masz ograniczoną możliwość wykonywania wielu zadań jednocześnie. Ale będziesz miał dość długie przerwanie, co może być złą rzeczą.

Jedynym prawdziwym sposobem na wykonanie wielu zadań jednocześnie na jednym rdzeniu MCU jest użycie DMA i urządzeń peryferyjnych, ponieważ działają one niezależnie od rdzenia (DMA i MCU współużytkują tę samą magistralę, więc działają nieco wolniej, gdy oba są aktywne). Tak więc, podczas gdy DMA przetasowuje bajty do UART, twój rdzeń może swobodnie wysyłać rzeczy do sieci Ethernet.


2
dzięki, DMA brzmi interesująco. Na pewno będę tego szukał!
Samolot

Nie wszystkie serie PIC mają DMA.
Matt Young,

1
Używam PIC32;)
Aircraft

6

Inne odpowiedzi już opisywały najczęściej używane opcje (główna pętla, ISR, RTOS). Oto kolejna opcja jako kompromis: Protothreads . Jest to w zasadzie bardzo lekka biblioteka dla wątków, która używa pętli głównej i niektórych makr C do „emulacji” RTOS. Oczywiście nie jest to pełny system operacyjny, ale w przypadku „prostych” wątków może być przydatny.


skąd mogę pobrać kod źródłowy dla systemu Windows? Myślę, że jest dostępny tylko dla systemu Linux.!
Samoloty

@CZAbhinav Powinno to być niezależne od systemu operacyjnego. Możesz pobrać najnowsze pobieranie tutaj .
erebos,

Jestem teraz w systemie Windows i korzystam z MplabX, nie sądzę, żeby był on użyteczny tutaj. W każdym razie dzięki.!
Samoloty

Nie słyszałem o protothreads, brzmi jak interesująca technika.
Arsenał

@CZAbhinav O czym ty mówisz? Jest to kod C i nie ma nic wspólnego z systemem operacyjnym.
Matt Young,

3

Mój podstawowy projekt dla minimalnego przedziału czasu RTOS niewiele się zmienił w kilku mikro rodzinach. Zasadniczo jest to przerwa czasowa prowadząca maszynę stanu. Procedura obsługi przerwania jest jądrem systemu operacyjnego, a instrukcja switch w głównej pętli to zadania użytkownika. Sterowniki urządzeń to procedury obsługi przerwań dla przerwań we / wy.

Podstawowa struktura jest następująca:

unsigned char tick;

void interrupt HANDLER(void) {
    device_driver_A();
    device_driver_B();
    if(T0IF)
    {
        TMR0 = TICK_1MS;
        T0IF = 0;   // reset timer interrupt
        tick ++;
    }
}

void main(void)
{
    init();

    while (1) {
        // periodic tasks:
        if (tick % 10 == 0) { // roughly every 10 ms
            task_A();
            task_B();    
        }
        if (tick % 55 == 0) { // roughly every 55 ms
            task_C();
            task_D();    
        }

        // tasks that need to run every loop:
        task_E();
        task_F();
    }
}

Jest to w zasadzie współpracujący system wielozadaniowy. Zadania są zapisywane, aby nigdy nie wchodzić w nieskończoną pętlę, ale nie obchodzi nas to, ponieważ zadania działają w pętli zdarzeń, więc nieskończona pętla jest niejawna. Jest to podobny styl programowania do języków zorientowanych na zdarzenia / nieblokujących się, takich jak javascript lub go.

Możesz zobaczyć przykład tego stylu architektury w moim oprogramowaniu nadajnika RC (tak, faktycznie używam go do latania samolotami RC, więc jest to nieco krytyczne z punktu widzenia bezpieczeństwa, aby zapobiec awariom moich samolotów i potencjalnie zabijaniu ludzi): https://github.com / slebetman / pic-txmod . Ma w zasadzie 3 zadania - 2 zadania w czasie rzeczywistym zaimplementowane jako stanowe sterowniki urządzeń (patrz rzeczy ppmio) i 1 zadanie w tle implementujące logikę miksowania. Zasadniczo jest podobny do serwera WWW, ponieważ ma 2 wątki we / wy.


1
Tak naprawdę nie nazwałbym tego „współpracującym wielozadaniowością”, ponieważ tak naprawdę nie różni się zasadniczo od żadnego innego programu mikrokontrolera, który musi wykonywać wiele rzeczy.
whatsisname

2

Chociaż doceniam, że pytanie dotyczy konkretnie użycia wbudowanego systemu RTOS, wydaje mi się, że szerszym pytaniem jest „jak osiągnąć wielozadaniowość na wbudowanej platformie”.

Zdecydowanie odradzam przynajmniej korzystanie z wbudowanego RTOS. Radzę to, ponieważ uważam, że najważniejsze jest, aby najpierw dowiedzieć się, jak osiągnąć „współbieżność” zadania za pomocą bardzo prostych technik programowania składających się z prostych harmonogramów zadań i maszyn stanów.

Aby wyjątkowo krótko wyjaśnić pojęcie, każdy moduł pracy, który należy wykonać (tj. Każde „zadanie”), ma określoną funkcję, którą należy okresowo wywoływać („zaznaczać”), aby moduł ten mógł wykonać pewne czynności. Moduł zachowuje swój bieżący stan. Następnie masz główną nieskończoną pętlę (harmonogram), która wywołuje funkcje modułu.

Surowa ilustracja:

for(;;)
{
    main_lcd_ui_tick();
    networking_tick();
}


...

// In your LCD UI module:
void main_lcd_ui_tick(void)
{
    check_for_key_presses();
    update_lcd();
}

...

// In your networking module:
void networking_tick(void)
{
    //'Tick' the TCP/IP library. In this example, I'm periodically
    //calling the main function for Keil's TCP/IP library.
    main_TcpNet();
}

Jednowątkowa struktura programowania, taka jak ta, w której okresowo wywoływane są funkcje głównej maszyny stanu z głównej pętli harmonogramu, jest wszechobecna w programowaniu osadzonym i dlatego zdecydowanie zachęcam OP do zapoznania się z nim i wygodnego korzystania z niego, zanim przejdziemy bezpośrednio do korzystania z niego Zadania / wątki RTOS.

Pracuję na urządzeniu wbudowanym, które ma sprzętowy interfejs LCD, wewnętrzny serwer WWW, klient poczty e-mail, klient DDNS, VOIP i wiele innych funkcji. Chociaż używamy RTOS (Keil RTX), liczba używanych pojedynczych wątków (zadań) jest bardzo mała i większość „wielozadaniowości” osiąga się jak opisano powyżej.

Aby podać kilka przykładów bibliotek demonstrujących tę koncepcję:

  1. Biblioteka sieciowa Keil. Cały stos TCP / IP może być uruchamiany jednowątkowo; okresowo wywołujesz main_TcpNet (), który iteruje stos TCP / IP i każdą inną opcję sieciową, którą skompilowałeś z biblioteki (np. serwer WWW). Zobacz http://www.keil.com/support/man/docs/rlarm/rlarm_main_tcpnet.htm . Trzeba przyznać, że w niektórych sytuacjach (być może poza zakresem tej odpowiedzi) osiągasz punkt, w którym zaczyna się przydać lub konieczne jest używanie wątków (szczególnie jeśli korzystasz z blokujących gniazd BSD). (Kolejna uwaga: nowy V5 MDK-ARM faktycznie tworzy dedykowany wątek Ethernet - ale ja tylko próbuję przedstawić ilustrację.)

  2. Biblioteka VOIP Linphone. Sama biblioteka linphone jest jednowątkowa. Wywołujesz iterate()funkcję w wystarczającym odstępie czasu. Zobacz http://www.linphone.org/docs/liblinphone-javadoc/org/linphone/core/LinphoneCore.html#iterate () . (Trochę kiepskiego przykładu, ponieważ użyłem tego na wbudowanej platformie Linux i bibliotekach zależności Linphone niewątpliwie spawnują wątki, ale znowu to ilustruje pewien punkt).

Wracając do konkretnego problemu nakreślonego przez OP, problemem wydaje się być fakt, że komunikacja UART musi odbywać się w tym samym czasie co niektóre sieci (przesyłanie pakietów przez TCP / IP). Nie wiem, jakiej biblioteki sieciowej faktycznie używasz, ale zakładam, że ma ona główną funkcję, którą należy często wywoływać. Będziesz musiał napisać swój kod, który zajmuje się transmisją / odbiorem danych UART, aby był zorganizowany w podobny sposób, jako maszyna stanu, która może być iterowana przez okresowe wywołania funkcji głównej.


2
Dzięki za to miłe wytłumaczenie, używam biblioteki TCP / IP dostarczonej przez Microchip i jest to bardzo ogromny złożony kod. W jakiś sposób udało mi się rozbić go na części i sprawić, że będzie użyteczny zgodnie z moimi wymaganiami. Na pewno spróbuję jednego z twoich podejść.!
Samoloty

Baw się dobrze :) Korzystanie z RTOS zdecydowanie ułatwia życie w wielu sytuacjach. Moim zdaniem użycie wątku (zadania) znacznie ułatwia programowanie w jednym sensie, ponieważ można uniknąć konieczności dzielenia zadania na maszynę stanu. Zamiast tego po prostu piszesz kod zadania, tak jak w programach C #, z kodem zadania utworzonym tak, jakby to była jedyna rzecz, która istnieje. Niezbędne jest zbadanie obu podejść, a gdy robisz więcej programów wbudowanych, zaczynasz wyczuwać, które podejście jest najlepsze w każdej sytuacji.
Trevor Page

Wolę też używać opcji wątków. :)
Samolot
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.