Přehled revizí | |
---|---|
Revize 1.1 | 26. 5. 2005 |
Přidána poznámka o nedostatcích malých motorků. | |
Revize 1.2 | 12. 4. 2006 |
Upraveno LS 2005/2006 | |
Revize 1.3 | 9. 5. 2006 |
Přidány kapitoly 4.1 a 4.2 | |
Revize 1.3 | 30. 4. 2007 |
Upraven text o zpracování přerušení | |
14. 4. 2009 | |
Periodické spoustění vláken je snažší s clock_nanosleep(). |
Obsah
Poslední změna: úterý 14. dubna 2009
Naprogramuje aplikaci pro řízení otáček stejnosměrného motoru. Motor se připojuje na paralelní port. Pomocí IRC snímače měřte polohu a rychlost motoru. Rychlost otáček ovlivňujte PWM signálem, který bude generován vaší aplikací. Pro regulaci otáček použijte PID (či lepší) regulátor.
Poznámka
Ve strojovně jsou v tuto chvíli 3 typy motorků:
nový model velký (s tlustou hřídelkou), 24 V, 400 přerušení/ot.
nový model malý (s tenkou hřídelkou), 24 V, 400 přerušení/ot.
staré modely s viditelným IRC senzorem, 12 V, 16 přerušení/ot.
U nových malých motorků je problém, že je v nich hardwarová chyba, která se projevuje tak, že když se motorek točí vysokou rychlostí, přestane generovat přerušení. Pokud váš program nefunguje při vyšších rychlostech, je to pravděpodobně tento problém a ne chyba vašeho programu. U velkých motorků se tato chyba nevyskytuje.
Než sem napíšeme podrobnější popis v češtině, můžete se podívat na anglický popis motorku a jeho řízení v RT Linuxu.
Když chce člověk z Linuxu přímo přistupovat k hardwaru (v našem případě k paralelnímu portu), je potřeba napsat pro daný hardware ovladač. Ovladač je kus kódu, který má stejná „práva“ jako jádro operačního systému a to mu umožňuje k hardwaru přistupovat. Na druhou stranu, pokud jsou v ovladači chyby, mohou tato práva způsobit, že ovladač způsobí nestabilitu/zatuhnutí celého systému. Uživatelské programy k hardwaru mohou přistupovat tak, že využívají služeb ovladačů. Pokud je chyba pouze v aplikaci, operační systém to zjistí a vrátí chybu, případně aplikaci ukončí. Rozhodně aplikace nemůže způsobit nestabilitu celého systému.
Protože po vás nemůžeme chtít abyste psali ovladače (k tomu je třeba
kromě znalosti hardwaru také znalost operačního systému), budete k řízení
motorku využívat některé vlastnosti Linuxu, které umožňují přístup k HW
přímo z uživatelských aplikací. Standardně má k těmto vlastnostem přístup
pouze uživatel root
, ale ve strojovně je speciálně
upravené jádro (2.6.19.7-rt15), které
dává tyto možnosti i obyčejným uživatelům.
Kromě drobných změn v jádře popsaných výše je jádro rozšířeno o realtime-preempt patch od Ingo Molnara, který přidává podporu pro časovače s vysokým rozlišením a umožňuje rychlejší odezvy systému na přerušení. Stránky zabývající se těmito úpravami lze nalézt na adrese http://rt.wiki.kernel.org/.
Přístup k hardwarovým registrům se provádí pomocí zápisů na resp.
čtení z tzv. I/O portů. Pro přístup k portům je potřeba zažádat operační
systém o povolení. Pokud danou oblast portů nepoužívá žádný driver ani
aplikace bude vaší aplikaci povolen přístup. Pro povolení přístupu k
portům je potřeba zavolat funkci ioperm
(viz
man ioperm), která očekává tři parametry. První udává
adresu prvního portu ke kterému chceme přistupovat, druhý obsahuje počet
portů a třetí říká jestli žádáme o přístup nebo se naopak práva přístupu
vzdáváme.
Alternativně lze jádro požádat o vypnutí kontroly přístupu k
portům pro daný proces a jeho potomky voláním funkce
iopl
s parametrem nastaveným na hodnotu 3. Od
tohoto okamžiku nejsou již nadále přístupy k portům ani použití
privilegovaných instrukcí pro povolování a zakazování přerušení pro daný
proces kontrolovány.
Pokud se při použití postupu s ioperm
vyskytnou problémy s náhodným hlášením signálu SIGSEGV na instrukcích
inb
a outb
, použijte metodu s
voláním iopl
.
Aby mohl v Linuxu běhat emulátor MS DOSu, obsahuje Linux mechanismus, který umožňují aplikacím požádat jádro o to, aby při příchodu daného přerušení poslalo procesu signál. Přímé použití tohoto mechanismu není úplně triviální a tak jsme vám připravili jednoduchou knihovnu, která tuto složitost zastiňuje a nabízí uživatelům jednoduché rozhraní. Ve vaší aplikaci můžete použít tyto funkce:
request_emu_irq
Slouží k registraci obslužné funkce přerušení. Jako parametry se předávají číslo IRQ, adresa obslužné rutiny, příznaky (jen pro kompatibilitu s ovladači), popis zařízení a parametr, který se předává obslužné rutině.
release_emu_irq
Odregistruje obslužnou rutinu.
emu_irq_thread_cli
Zakáže příjem přerušení (příjem signálu) v aktuálním vlákně.
emu_irq_thread_sti
Opět povolí příjem přerušení v aktuálním vlákně.
emu_irq_thread_prio_set
Funkce umožňuje předpřipravit nebo modifikovat prioritu vlákna, které obsluhuje žádosti o přerušení. Vlákno přerušení má nastavenu plánovací politiku SCHED_FIFO a pokud není priorita zadána, je nastavena na polovinu rozsahu.
Po zavolání funkce request_emu_irq()
, je
spuštěno vlákno, které bude přijímat a zpracovávat jádrem systému na
signály převedené požadavky od zdroje přerušení. Vlákno na příchod
požadavku čeká v systémovém volání sigwait()
. Tím
je zajištěno, že obsluha přerušení běží paralelně s ostatními vlákny a
nekoliduje s jinými systémovými voláními
(nanosleep()
, atd).
Stáhněte si knihovnu spolu s testovacím programem. Knihovnu rozbalte příkazem tar xvzf emu-irq.tar.gz. Testovacím programem otestujte jestli funguje přístup na porty a přicházejí vám přerušení. Aplikace si zaregistruje obsluhu přerušení od motorku, roztočí motorek plnými otáčkami na jednu stranu a počítá kolik přijala přerušení. Po určitém počtu impulzů přepne směr otáčení a nakonec motorek zastaví. Pokud pak budete otáčet motorkem ručně, mělo by se měnit počitadlo přerušení a informace o úrovni jednotlivých fázových signálů.
Knihovna popsaná v předchozím odstavci je poněkud složitější, protože se snaží emulovat způsob registrování přerušení, který se používá v jádře operačního systému Linux pro psaní driverů. Pokud je požadavek na vytvářený kód omezen pouze na běh v uživatelském programu, lze obsluhu přerušení implementovat přímo v kódu aplikace.
Použití služeb jádra, které umožňují přijímání požadavků na
přerušení v uživatelském prostoru je poněkud složitější. Tyto služby
byly primárně vytvořené proto, aby bylo možné pro architekturu x86
napsat program emulující prostředí operačního systému DOS (DOSEMU),
které umožní pouštět aplikace a i drivery původně určené pro toto
prostředí. Tyto služby jsou natolik specifické, že nejsou zpřístupněné
přes standardní hlavičkové soubory. Proto je potřeba službu
vm86_plus
zpřístupnit následujícím kódem:
#include <asm/vm86.h> #include <sys/syscall.h> #define NEW_SYS_vm86 166 static inline int vm86_plus(int function, int param) { int __res; __asm__ __volatile__("int $0x80\n" :"=a" (__res):"a" ((int)NEW_SYS_vm86), "b" (function), "c" (param)); return __res; }
Dále je potřeba zvolit signál, který bude použitý k doručení události. Například
#define EMU_IRQ_SIG SIGIO
Poté je již možné implementovat vlastní vlákno pro obsluhu přerušení:
void *my_irq_thread(void *) { sigset_t my_set; int ret; int intno = LPT_IRQ; int signo; sigemptyset(&my_set); sigaddset(&my_set,EMU_IRQ_SIG); pthread_sigmask(SIG_BLOCK, &my_set,NULL); ret = vm86_plus(VM86_REQUEST_IRQ, (EMU_IRQ_SIG<<8)|intno); if(ret < 0) /* registrace byla neúspěšná */ while(!terminate){ ret = sigwait(&my_set, &signo); if(ret) { /* došlo k chybě či přerušení jiným signálem */ } ret = vm86_plus(VM86_GET_AND_RESET_IRQ, intno); if(ret > 0) { /* kód zpracování události */ } } vm86_plus(VM86_FREE_IRQ,intno); }
Tělo nového vlákna nejdříve zablokuje asynchronní přijímání signálu, který je dále využit při registraci požadavku na doručování informace o příchodu hardwarového přerušení. Vlákno pak zpracovává požadavky ve smyčce, kde synchronně čeká na příchod signálu.
V řídicím programu je potřeba některé akce provádět periodicky (např.
generování PWM, výpočet aktuální rychlosti otáčení motorku). Periodické
spouštění programové smyčky můžeme vyřešit několika způsoby. Jako
nejjednodušší se nabízí použití funkcí sleep()
a spol.
Programová smyčka pak vypadá zhruba takto:
while(!Finished) { do_work(); sleep(1); }
Pokud ale vykonání funkce do_work()
trvá nějakou nenulovou dobu, perioda vykonávání této smyčky už není 1 s jak
jsme si přáli, a navíc může kolísat.
Tento efekt můžeme odstranit postupem, kdy si předtím než bude vlákno
uspáno určíme absolutní čas, do kdy má spánek trvat a použijeme funkci
clock_nanosleep()
pro absolutní čekání.
#include <time.h> const struct timespec period = {/*seconds*/, /*nanoseconds*/}; struct timespec time_to_wait; clock_gettime(CLOCK_REALTIME,&time_to_wait); while(!Finished) { do_work(); timespec_add(&time_to_wait,&time_to_wait,&period); clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &time_to_wait,NULL); }
Při překladu je kvůli funkci clock_nanosleep()
potřeba připojit knihovnu librt (tj. použít přepínač
-lrt). Dále je nutné upozornit na to, že čas je uložen ve
struktuře timespec
struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds. only number in range 0 - 999 999 999 is valid */ }
a při počítání s tímto typem nemůžeme zacházet jako s
jednoduchými typy, např. int. Můžete si o tom přečíst v manuálu
ke knihovně libc. Zdrojový kód s výše použitými funkcemi
timespec_add()
a timespec_sub()
je
ke stažení zde .
Jako další řešení periodického spouštění smyčky se nabízí možnost použít intervalový časovač. V tomto případě systémovým voláním vytvoříme časovač, který pak s nastavenou periodou zasílá procesu signál. Pak synchronizujeme vykonávání programové smyčky s tímto signálem.
create_and_set_timer(); while(!Finished) { do_work(); wait_for_signal(); }
Vzhledem k tomu že používání časovačů nebylo na cvičeních probíráno a není úplně triviální, tak je zde tato možnost zmíněna jen pro informaci.
Funkce nanosleep()
může skončit předčasně, když
proces, který je touto funkcí uspán, obdrží neblokovaný signál (návratová
hodnota funkce nanosleep()
je potom -1, a
errno
nastaveno na EINTR, viz manuálové stránky). Z
toho důvodu je vhodné ve vláknech, kde budeme nanosleep používat,
preventivně zablokovat všechny signály kromě SIGKILL a SIGSTOP, které
zablokovat nejdou, případně potom povolit některé specifické signály,
jmenovitě SIGINT, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTERM, SIGINT,
SIGQUIT.
#include <pthread.h> #include <signal.h> /* Blokování všech signálů (kromě SIGKILL a SIGSTOP)*/ sigset_t my_set; sigfillset(&my_set); pthread_sigmask(SIG_BLOCK,&my_set,NULL); /* Případné povolení některého signálu, který má být přijímán */ sigemptyset(&my_set); sigaddset(&my_set,SIGINT); pthread_sigmask(SIG_UNBLOCK,&my_set,NULL);
Přestože standardní Linux není real-time operačním systémem,
existuje zde možnost, jak nějakému vláknu přiřadit tzv. real-time
prioritu. V tom případě má pak dané vlákno přednost před obyčejnými (ne
real-time) vlákny, a stane běžícím vždy v nejkratším možném čase poté co
je připraven k běhu (pokud neběží jiné vlákno s vyšší nebo stejnou
real-time prioritou). Získáme tak lepší odezvu od daného vlákna, ale je
třeba dát si pozor, aby takové vlákno neprovádělo nějakou výpočetně či
jinak časově náročnou činnost, protože v tom případě by na danou dobu
počítač „zatuhl“. Real-time priorita celého procesu se
nastavuje voláním funkce sched_setscheduler()
, pro
nastavení priority jednoho určitého vlákna slouží funkce
pthread_setschedparam()
. Linux nabízí tři strategie
rozvrhování, v této úloze si vystačíte se strategií SCHED_FIFO. Pro
zjištění rozsahu priorit pro danou strategii existují funkce
sched_get_priority_min()
a
sched_get_priority_max()
, pro strategii SCHED_FIFO
dovoluje Linux použít prioritu v rozsahu 1 až 99. Následuje příklad na
nastavení real-time priority vlákna (překlad zase s knihovnou
librt):
#include <pthread.h> #include <sched.h> struct sched_param scheduling_parameters; /* Požadovaná priorita je maximální priorita - 4 */ scheduling_parameters.sched_priority = sched_get_priority_max(SCHED_FIFO) - 4; /* Nastavení rozvrhovací strategie SCHED_FIFO a výše uvedené priority pro aktuální vlákno */ if (0 != pthread_setschedparam(pthread_self(), SCHED_FIFO, &scheduling_parameters)) { perror("pthread_setschedparam error"); }
Při nastavování real-time priority vláken ve vašem programu se řiďte jednoduchým pravidlem "čím kratší perioda vykonávání, tím vyšší priorita".
Výpis vláken spuštěných uživatelem, který je setříděný vzestupně
podle priorit a obsahuje sloupec s prioritami a politikou jejich plánování
rtprio
, lze získat například následujícím
příkazem.
ps H --sort rtprio -o pid,policy,rtprio,state,tname,time,command
Zahrnutí i systémových threadů se docílí přidáním příznaků
ax
.
Alternativně lze přímo spustit thread s požadovaným nastavením politiky plánovače a priority.
struct sched_param my_schedul_param; pthread_attr_t my_attrib; pthread_t my_thread; pthread_attr_init(&my_attrib); my_schedul_param.sched_priority = sched_get_priority_max(SCHED_FIFO) - 4; pthread_attr_setschedparam(&my_attrib,&my_schedul_param); pthread_attr_setschedpolicy(&my_attrib, SCHED_FIFO); pthread_attr_setinheritsched(&my_attrib, PTHREAD_EXPLICIT_SCHED); pthread_create( &my_thread, &my_attrib, my_thread_function, NULL);
O prioritách a plánovači procesů (scheduler) se dočtete více třeba zde.
Máte-li hotové základní funkce pro ovládaní motoru (vlákno
generující PWM, vlákno měřící aktuální rychlost), můžete se pustit do
implementace regulátoru. V dalším textu budu předpokládat, že naměřenou
rychlost ukládáte do proměnné rychlost
, žádaná hodnota
rychlosti je v proměnné reference
a vlákno generující
PWM generuje pulsy, jejichž šířka je úměrná hodnotě proměnné
akce
.
P-regulátor je téměř nejjednodušší regulátor jaký si můžete vymyslet. Vzorec pro výpočet akčního zásahu je:
akce = P * (reference - rychlost)
kde P
je konstanta regulátoru. Velikost této
konstanty závisí na jak na dynamických vlastnostech motorku tak na tom v
jakých jednotkách jsou hodnoty proměnných.
P-regulátor má nevýhodu v tom, že skutečná rychlost nikde nebude rovna žádané rychlosti (s výjimkou nulové rychlosti). Vždy bude existovat nějaké odchylka. Odchylku lze potlačit použitím PI-regulátoru.
PI-regulátor obsahuje kromě proporcionální složky také složku integrační. Tato složka integruje odchylku a na základě tohoto integrálu upravuje akční zásah. V jazyce C se integrační složka nejjednodušeji naimplementuje jako suma odchylek. Výpočet bude zhruba následující:
odchylka = reference - rychlost; akce = P*odchylka + I*suma; suma = suma + odchylka;
Pokud budou správně nastaveny
konstanty P a I (například zkusmo, metodou Zieglera-Nicholse), skutečná
rychlost pak bude přesně odpovídat žádané rychlosti. V opravdu dobré
implementaci je potřeba se vyrovnat s problémem zvaným windup. Točí-li
se motorek a my ho například rukou zastavíme, bude odchylka skutečné
rychlosti od žádané poměrně velká a hodnota proměnné
suma
bude rychle narůstat. V okamžiku kdy motorek
pustíme, akční zásah regulátoru bude ovlivněn jen integrační složkou,
protože ta bude strašně veliká. Motorek se tedy bude dlouho točit
maximální rychlostí, než se suma
zase
„odintegruje“ zpátky na původní hodnotu.
Způsobů jak řešit windup je mnoho a většinou spočívají v omezování maximální velikosti integrační složky. Stačí nalistovat patřičnou kapitolu v nějaké učebnici řízení.
Výše naznačený způsob implementace regulátoru sice funguje, ale
není optimální. Problém většinou bývá, že číslicové regulátory neběží na
výkonných procesorech a tyto procesory často nemají podporu pro práci s
desetinnými čísly. Je proto potřeba aby všechny proměnné použité při
výpočtu nabývaly hodnot omezeného rozsahu. To se o předchozí
implementaci říct nedá, protože proměnná suma
může
mít hodnotu téměř libovolnou. Pokud bychom takovýto regulátor
implementovali na 16 bitovém procesoru, určitě bychom na tento problém
brzy narazili.
Popis metod efektivní implementace regulátorů je nad rámec tohoto textu a zájemce odkazuji na předměty o řízení pro vyšší ročníky.
Všechny připomínky k předmětu, obsahu stránek, objevené chyby v ukázkových programech apod. adresujte na autory: