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.h
ale 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?
źródło
Odpowiedzi:
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.
źródło
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.Wątek jest zapewniany przez system operacyjny. W świecie osadzonym zwykle nie mamy systemu operacyjnego („bare metal”). Pozostawia to następujące opcje:
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.
źródło
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.
źródło
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.
źródło
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:
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.
źródło
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:
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ę:
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ę.)
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.
źródło