W przypadku MCU z obnażonym metalem, w porównaniu do domowej roboty kodu z pętlą w tle i architekturą przerwania timera, jakie są korzyści z nie-zapobiegawczego systemu operacyjnego? Jakie spośród tych korzyści są na tyle atrakcyjne, że projekt może przyjąć nieprzeszkadzający system operacyjny, a nie używać domowego kodu z architekturą pętli tła?
.
Wyjaśnienie do pytania:
Naprawdę doceniam wszystkich, którzy odpowiedzieli na moje pytanie. Wydaje mi się, że odpowiedź już prawie nadeszła. Dodaję to wyjaśnienie do mojego pytania tutaj, które pokazuje moje własne rozważania i może pomóc zawęzić pytanie lub uczynić je bardziej precyzyjnym.
Staram się zrozumieć, jak wybrać najbardziej odpowiedni RTOS dla danego projektu.
Aby to osiągnąć, pomocne będzie lepsze zrozumienie podstawowych pojęć i najbardziej atrakcyjnych korzyści z różnych rodzajów RTOS i odpowiedniej ceny, ponieważ nie ma najlepszej RTOS dla wszystkich aplikacji.
Kilka lat temu czytałem książki o systemie operacyjnym, ale nie mam ich już przy sobie. Szukałem w Internecie, zanim opublikowałem tutaj pytanie, i okazało się, że ta informacja była najbardziej pomocna: http://www.ustudy.in/node/5456 .
Istnieje wiele innych pomocnych informacji, takich jak wprowadzenie na stronie internetowej różnych RTOS, artykuły porównujące planowanie wyprzedzające i planowanie nieprzewidziane itp.
Ale nie znalazłem żadnego tematu wspomnianego, kiedy wybrać nieprzekazujący RTOS, a kiedy lepiej jest po prostu napisać własny kod, używając przerwania timera i pętli tła.
Mam pewne własne odpowiedzi, ale nie jestem z nich wystarczająco zadowolony.
Naprawdę chciałbym poznać odpowiedź lub opinię bardziej doświadczonych ludzi, szczególnie w praktyce przemysłowej.
Jak dotąd rozumiem:
bez względu na to, czy korzystasz z systemu operacyjnego, zawsze potrzebujesz pewnych kodów planowania, nawet w postaci kodu takiego jak:
in the timer interrupt which occurs every 10ms
if(it's 10ms)
{
call function A / execute task A;
}
if(it's 50ms)
{
call function B / execute task B;
}
Korzyść 1:
nieprzekazujący system operacyjny określa sposób / styl programowania dla kodu planowania, aby inżynierowie mogli dzielić ten sam widok, nawet jeśli wcześniej nie byli w tym samym projekcie. Następnie, mając ten sam pogląd na temat zadania koncepcyjnego, inżynierowie mogą pracować nad różnymi zadaniami i testować je, profilować je w jak największym stopniu niezależnie.
Ale ile naprawdę możemy na tym zyskać? Jeśli inżynierowie pracują nad tym samym projektem, mogą znaleźć sposób, aby dobrze dzielić ten sam widok bez korzystania z nieprzeszkadzającego systemu operacyjnego.
Jeśli jeden inżynier pochodzi z innego projektu lub firmy, odniesie korzyść, jeśli wcześniej znał system operacyjny. Ale jeśli tego nie zrobił, to nie wydaje się, żeby uczynił nowy system operacyjny lub nowy fragment kodu.
Korzyści 2:
Jeśli kod systemu operacyjnego został dobrze przetestowany, oszczędza to czas od debugowania. To naprawdę dobra zaleta.
Ale jeśli aplikacja ma tylko około 5 zadań, myślę, że pisanie własnego kodu za pomocą przerwania timera i pętli tła nie jest zbyt trudne.
Nie-zapobiegawczy system operacyjny jest tutaj określany jako komercyjny / wolny / starszy system operacyjny z nieprzewidzianym harmonogramem.
Kiedy opublikowałem to pytanie, myślę głównie o niektórych systemach operacyjnych, takich jak:
(1) KISS Kernel (Mały niepostępny system RTOS - zgłoszony przez jego stronę internetową)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (lekki RTOS - deklarowany przez swoją stronę internetową)
(3) FreeRTOS (To jest prewencyjny RTOS, ale jak rozumiem, może być również skonfigurowany jako nieprzekazowy RTOS)
(4) uC / OS (podobny do FreeRTOS)
(5) ) starszy kod systemu operacyjnego / harmonogramu w niektórych firmach (zazwyczaj tworzony i obsługiwany przez firmę wewnętrznie)
(Nie można dodać więcej linków, ponieważ ograniczenia z nowego konta StackOverflow)
Jak rozumiem, nieprzenikliwy system operacyjny to zbiór następujących kodów:
(1) program planujący korzystający ze strategii nieprewencyjnej.
(2) udogodnienia do komunikacji między zadaniami, muteksu, synchronizacji i kontroli czasu.
(3) zarządzanie pamięcią.
(4) inne przydatne narzędzia / biblioteki, takie jak system plików, stos sieciowy, GUI itp. (FreeRTOS i uC / OS to zapewniają, ale nie jestem pewien, czy nadal działają, gdy harmonogram jest skonfigurowany jako zapobiegawczy)
Niektóre z nie zawsze tam są. Ale harmonogram jest koniecznością.
źródło
Odpowiedzi:
Pachnie to trochę nie na temat, ale postaram się skierować to z powrotem na właściwe tory.
Zapobiegawcza wielozadaniowość oznacza, że system operacyjny lub jądro może zawiesić aktualnie działający wątek i przejść do innego w oparciu o dowolną heurystykę planowania. Najczęściej działające wątki nie mają pojęcia, że w systemie dzieją się inne rzeczy, a to oznacza dla twojego kodu to, że musisz uważnie go zaprojektować, aby jeśli jądro zdecydowało się zawiesić wątek w środku wieloetapowa operacja (np. zmiana wyjścia PWM, wybór nowego kanału ADC, odczyt stanu z urządzenia peryferyjnego I2C itp.) i pozostawienie na jakiś czas innego wątku, aby te dwa wątki nie kolidowały ze sobą.
Dowolny przykład: załóżmy, że jesteś nowy w wielowątkowych systemach osadzonych i masz mały system z I2C ADC, SPI LCD i I2C EEPROM. Zdecydowałeś, że dobrym pomysłem będzie posiadanie dwóch wątków: jednego, który odczytuje z ADC i zapisuje próbki do EEPROM, a drugi, który odczytuje 10 ostatnich próbek, uśrednia je i wyświetla na wyświetlaczu SPI LCD. Niedoświadczony projekt wyglądałby mniej więcej tak (rażąco uproszczony):
To bardzo prymitywny i szybki przykład. Nie koduj w ten sposób!
Teraz pamiętaj, że wyprzedzający wielozadaniowy system operacyjny może zawiesić jeden z tych wątków w dowolnym wierszu kodu (właściwie w dowolnej instrukcji asemblacji) i dać czas drugiemu wątkowi na uruchomienie.
Pomyśl o tym. Wyobraź sobie, co by się stało, gdyby system operacyjny zdecydował się zawiesić
adc_thread()
między ustawieniem adresu EE, aby zapisywać i zapisywać rzeczywiste dane.lcd_thread()
działał, kręcił się wokół urządzenia peryferyjnego I2C, aby odczytać potrzebne dane, a gdyadc_thread()
znów nadejdzie jego kolej, pamięć EEPROM nie będzie w tym samym stanie, w jakim została. Wszystko nie działałoby zbyt dobrze. Co gorsza, może nawet działać przez większość czasu, ale nie przez cały czas, i oszalałbyś, próbując dowiedzieć się, dlaczego twój kod nie działa, gdy WYGLĄDA tak, jak powinien!To najlepszy przykład; system operacyjny może zdecydować o uprzedzeniu
i2c_write()
zadc_thread()
kontekstu i ponownym uruchomieniu go zlcd_thread()
kontekstu! Sprawy mogą się naprawdę szybko popsuć.Kiedy piszesz kod do pracy w środowisku wyprzedzającym wielozadaniowość, musisz użyć mechanizmów blokujących, aby upewnić się, że jeśli Twój kod zostanie zawieszony w nieodpowiednim czasie, całe piekło się nie rozpadnie.
Z kolei wielozadaniowość kooperacyjna oznacza, że każdy wątek kontroluje, kiedy rezygnuje z czasu wykonywania. Kodowanie jest prostsze, ale kod musi być starannie zaprojektowany, aby upewnić się, że wszystkie wątki mają wystarczająco dużo czasu na uruchomienie. Kolejny wymyślony przykład:
Ten kod nie będzie działał tak, jak myślisz, a nawet jeśli wydaje się, że działa, nie będzie działał wraz ze wzrostem szybkości przesyłania danych wątku echa. Jeszcze raz, spójrzmy na to.
echo_thread()
czeka na pojawienie się bajtu w UART, a następnie pobiera go i czeka, aż będzie miejsce na napisanie go, a następnie zapisuje. Po wykonaniu tej czynności można uruchomić inne wątki.seconds_counter()
zwiększy licznik, odczekaj 1000 ms, a następnie da innym wątkom szansę na uruchomienie. Jeśli dwa bajty wejdą do UART podczas tak długiego czasusleep()
, możesz przegapić ich zobaczenie, ponieważ nasz hipotetyczny UART nie ma FIFO do przechowywania znaków, gdy procesor jest zajęty robieniem innych rzeczy.Prawidłowym sposobem zaimplementowania tego bardzo kiepskiego przykładu byłoby umieszczenie
yield_cpu()
gdziekolwiek masz zajętą pętlę. Pomoże to w rozwoju, ale może powodować inne problemy. np. jeśli czas ma kluczowe znaczenie, a procesor przechodzi na inny wątek, który trwa dłużej niż się spodziewasz, możesz zostać odrzucony. System operacyjny z wyprzedzeniem wielozadaniowości nie miałby tego problemu, ponieważ siłą zawiesza wątki, aby upewnić się, że wszystkie wątki są poprawnie zaplanowane.Co to ma wspólnego z zegarem i pętlą tła? Czasomierz i pętla tła są bardzo podobne do powyższego przykładu wielozadaniowości kooperacyjnej:
Wygląda to bardzo podobnie do kooperacyjnego przykładu wątków; masz licznik czasu, który konfiguruje zdarzenia, i główną pętlę, która ich szuka i działa na nie w sposób atomowy. Nie musisz się martwić, że „wątki” ADC i LCD zakłócają się wzajemnie, ponieważ jeden nigdy nie będzie przeszkadzał drugiemu. Nadal musisz się martwić, że „wątek” będzie trwał zbyt długo; np. co się stanie, jeśli
get_adc_data()
zajmie 30ms? stracisz trzy możliwości sprawdzenia postaci i powtórzenia jej.Implementacja pętli + timera jest często o wiele prostsza do wdrożenia niż mikrojądro współpracujące z wieloma zadaniami, ponieważ kod można zaprojektować bardziej specyficznie dla danego zadania. Nie jest tak naprawdę wielozadaniowość, jak projektowanie stałego systemu, w którym każdy podsystem ma trochę czasu na wykonanie swoich zadań w bardzo konkretny i przewidywalny sposób. Nawet system wielozadaniowy współpracujący musi mieć ogólną strukturę zadań dla każdego wątku, a następny wątek do uruchomienia jest określany przez funkcję planowania, która może stać się dość złożona.
Mechanizmy blokujące dla wszystkich trzech systemów są takie same, ale narzut wymagany dla każdego z nich jest zupełnie inny.
Osobiście prawie zawsze koduję do tego ostatniego standardu, implementacji pętli + timera. Uważam, że gwintowanie powinno być stosowane bardzo oszczędnie. Pisanie i debugowanie jest nie tylko bardziej skomplikowane, ale także wymaga większego nakładu pracy (zapobiegawcze wielozadaniowe mikrojądro zawsze będzie większe niż głupio prosty licznik zdarzeń i obserwator zdarzeń w głównej pętli).
Jest też powiedzenie, że każdy, kto pracuje nad wątkami, doceni:
:-)
źródło
poll()
od razu przychodzi mi na myśl).Wielozadaniowość może być przydatną abstrakcją w wielu projektach mikrokontrolerów, chociaż prawdziwy harmonogram wyprzedzający byłby zbyt ciężki i niepotrzebny w większości przypadków. Zrobiłem dobrze ponad 100 projektów mikrokontrolerów. Wielokrotnie korzystałem z funkcji współpracy, ale uprzedzające przełączanie zadań z powiązanym bagażem nie było jak dotąd właściwe.
Problemy z wyprzedzającym zadawaniem w stosunku do wspólnego zadawania są następujące:
Zasadniczo poświęcenie zadania konkretnemu zadaniu ma sens, gdy procesor może to obsłużyć, a zadanie jest wystarczająco skomplikowane z wystarczającą operacją zależną od historii, więc rozbicie go na kilka osobnych zdarzeń byłoby kłopotliwe. Zasadniczo dzieje się tak w przypadku obsługi wejściowego strumienia komunikacyjnego. Takie rzeczy są zwykle silnie zależne od stanu w zależności od niektórych wcześniejszych danych wejściowych. Na przykład mogą istnieć bajty kodu operacji, po których następują bajty danych unikalne dla każdego kodu operacji. Następnie pojawia się problem, że bajty przychodzą do ciebie, gdy coś innego ma ochotę je wysłać. Dzięki osobnemu zadaniu obsługującemu strumień wejściowy, możesz sprawić, aby pojawił się w kodzie zadania tak, jakbyś wychodził i pobierał następny bajt.
Ogólnie rzecz biorąc, zadania są przydatne, gdy istnieje wiele kontekstów państwowych. Zadania są w zasadzie maszynami stanu, przy czym PC jest zmienną stanu.
Wiele rzeczy, które musi zrobić mikro, można wyrazić jako reagowanie na zestaw zdarzeń. W rezultacie zazwyczaj mam główną pętlę zdarzeń. To sprawdza kolejno każde możliwe zdarzenie, a następnie przeskakuje z powrotem na górę i robi to wszystko ponownie. Gdy obsługa zdarzenia zajmuje więcej niż kilka cykli, po zakończeniu obsługi zwykle przeskakuję z powrotem na początek pętli zdarzenia. Oznacza to, że zdarzenia mają domyślny priorytet na podstawie tego, gdzie są sprawdzane na liście. W wielu prostych systemach jest to wystarczająco dobre.
Czasami dostajesz trochę bardziej skomplikowane zadania. Często można je podzielić na sekwencję niewielkiej liczby osobnych rzeczy do zrobienia. W takich przypadkach można użyć flag wewnętrznych jako zdarzeń. Robiłem takie rzeczy wiele razy na niskiej jakości PIC.
Jeśli masz podstawową strukturę zdarzeń jak wyżej, ale musisz także na przykład odpowiedzieć na strumień poleceń przez UART, wtedy przydatne jest, aby osobne zadanie obsługiwało odebrany strumień UART. Niektóre mikrokontrolery mają ograniczone zasoby sprzętowe do wielozadaniowości, na przykład PIC 16, który nie może odczytać ani zapisać własnego stosu wywołań. W takich przypadkach używam tak zwanego pseudo-zadania dla procesora poleceń UART. Główna pętla zdarzeń nadal obsługuje wszystko inne, ale jednym z jej zdarzeń jest to, że UART odebrał nowe bajty. W takim przypadku przeskakuje do procedury, która uruchamia to pseudo-zadanie. Moduł poleceń UART zawiera kod zadania, a adres wykonania i kilka wartości rejestru zadania są zapisywane w pamięci RAM w tym module. Kod przeskakiwany przez pętlę zdarzeń zapisuje bieżące rejestry, ładuje zapisane rejestry zadań, i przeskakuje na adres restartu zadania. Kod zadania wywołuje makro YIELD, które wykonuje operację odwrotną, która następnie ostatecznie przeskakuje z powrotem na początek głównej pętli zdarzeń. W niektórych przypadkach główna pętla zdarzeń uruchamia pseudo-zadanie raz na przebieg, zwykle u dołu, aby było to zdarzenie o niskim priorytecie.
Na PIC 18 i nowszych używam prawdziwego systemu zadań współpracy, ponieważ stos wywołań jest możliwy do odczytu i zapisu przez oprogramowanie układowe. W tych systemach adres restartu, kilka innych elementów stanu i wskaźnik stosu danych są przechowywane w buforze pamięci dla każdego zadania. Aby wszystkie pozostałe zadania mogły zostać uruchomione raz, zadanie wywołuje TASK_YIELD. Zapisuje bieżący stan zadania, przegląda listę następnego dostępnego zadania, ładuje jego stan, a następnie uruchamia.
W tej architekturze główna pętla zdarzeń jest po prostu kolejnym zadaniem, z wywołaniem TASK_YIELD na górze pętli.
Cały mój wielozadaniowy kod dla PIC jest dostępny za darmo. Aby to zobaczyć, zainstaluj wersję PIC Development Tools na stronie http://www.embedinc.com/pic/dload.htm . Poszukaj plików z „zadaniem” w ich nazwach w katalogu SOURCE> PIC dla 8-bitowych PIC oraz w katalogu SOURCE> DSPIC dla 16-bitowych PIC.
źródło
Edycja: (Opuszczę mój wcześniejszy post poniżej; może kiedyś komuś pomoże.)
Systemy operacyjne wielozadaniowe dowolnego rodzaju i procedury obsługi przerwań nie są - lub nie powinny - być konkurencyjną architekturą systemu. Są przeznaczone do różnych zadań na różnych poziomach systemu. Przerwania są tak naprawdę przeznaczone dla krótkich sekwencji kodu do obsługi natychmiastowych obowiązków, takich jak ponowne uruchomienie urządzenia, być może odpytywanie urządzeń nieprzeszkadzających, mierzenie czasu w oprogramowaniu itp. Zazwyczaj zakłada się, że tło wykona dalsze przetwarzanie, które nie jest już krytyczne po czasie natychmiastowe potrzeby zostały zaspokojone. Jeśli wszystko, co musisz zrobić, to zrestartować stoper i przełączyć diodę LED lub impulsować inne urządzenie, ISR zwykle może to wszystko bezpiecznie zrobić na pierwszym planie. W przeciwnym razie musi poinformować tło (ustawiając flagę lub ustawiając w kolejce wiadomość), że coś musi zrobić, i zwolnić procesor.
Widziałem bardzo proste struktury programu, którego pętla tło jest tylko bezczynne pętla:
for(;;){ ; }
. Cała praca została wykonana w ISR timera. Może to działać, gdy program musi powtórzyć pewną stałą operację, która gwarantuje zakończenie w czasie krótszym niż czas; przychodzą na myśl pewne ograniczone rodzaje przetwarzania sygnału.Osobiście piszę ISR, które oczyszczają wyjście, i pozwalam, aby tło przejęło wszystko, co wymaga zrobienia, nawet jeśli jest to tak proste jak pomnożenie i dodać, że można to zrobić w ułamku czasu. Dlaczego? Któregoś dnia wpadnę na świetny pomysł, aby dodać kolejną „prostą” funkcję do mojego programu i „cholera, wystarczy krótki ISR, aby to zrobić” i nagle moja wcześniej prosta architektura wywołuje interakcje, których nie planowałem i dzieje się niekonsekwentnie. Debugowanie nie jest zbyt zabawne.
(Wcześniej opublikowane porównanie dwóch rodzajów wielozadaniowości)
Przełączanie zadań: Funkcja wyprzedzająca MT zajmuje się przełączaniem zadań, w tym upewniając się, że żaden wątek nie jest pozbawiony procesora, a wątki o wysokim priorytecie uruchomią się, gdy tylko będą gotowe. Cooperative MT wymaga od programisty upewnienia się, że żaden wątek nie utrzymuje procesora zbyt długo. Musisz także zdecydować, jak długo jest za długi. Oznacza to również, że za każdym razem, gdy modyfikujesz kod, musisz wiedzieć, czy jakikolwiek segment kodu przekracza teraz kwant czasowy.
Ochrona operacji nieatomowych: dzięki PMT musisz upewnić się, że zamiana wątków nie występuje w trakcie operacji, których nie można dzielić. Odczytywanie / zapisywanie określonych par rejestrów urządzeń, które muszą być obsługiwane na przykład w określonej kolejności lub w maksymalnym czasie. Z CMT jest to dość łatwe - po prostu nie poddawaj procesora w trakcie takiej operacji.
Debugowanie: Generalnie łatwiejsze dzięki CMT, ponieważ planujesz, kiedy / gdzie nastąpi przełączenie wątku. Warunki wyścigu między wątkami i błędami związane z niechronionymi wątkami operacjami z PMT są szczególnie trudne do debugowania, ponieważ zmiany wątków są probabilistyczne, więc nie powtarzalne.
Zrozumienie kodu: Wątki napisane dla PMT są napisane tak, jakby mogły być samodzielne. Wątki napisane dla CMT są zapisywane jako segmenty i zależnie od wybranej struktury programu, czytelnikowi może być trudniej.
Używanie kodu biblioteki, który nie jest bezpieczny dla wątków: Musisz sprawdzić, czy każda funkcja biblioteki, którą wywołujesz, jest bezpieczna dla wątków PMT. printf () i scanf () i ich warianty prawie zawsze nie są bezpieczne dla wątków. Dzięki CMT będziesz wiedział, że nie nastąpi zmiana nici, z wyjątkiem sytuacji, gdy specjalnie podasz procesor.
System napędzany maszyną o skończonym stanie do sterowania urządzeniem mechanicznym i / lub śledzenia zdarzeń zewnętrznych jest często dobrym kandydatem do CMT, ponieważ na każdym wydarzeniu nie ma wiele do zrobienia - uruchomić lub zatrzymać silnik, ustawić flagę, wybrać następny stan itd. Zatem funkcje zmiany stanu są z natury krótkie.
Podejście hybrydowe może działać naprawdę dobrze w tego rodzaju systemach: CMT do zarządzania maszyną stanową (a zatem i większością sprzętu) działającą jako jeden wątek oraz jeden lub dwa kolejne wątki do wykonywania dłuższych obliczeń uruchomionych przez stan zmiana.
źródło