Zadání 2 - Řízení stejnosměrného motorku

Přehled revizí
Revize 1.126. 5. 2005
Přidána poznámka o nedostatcích malých motorků.
Revize 1.212. 4. 2006
Upraveno LS 2005/2006
Revize 1.39. 5. 2006
Přidány kapitoly 4.1 a 4.2
Revize 1.330. 4. 2007
Upraven text o zpracování přerušení
14. 4. 2009
Periodické spoustění vláken je snažší s clock_nanosleep().

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.

2. Popis motorku

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.

3. Přímý přístup k hardwaru z 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.

3.1. Real-Time rozšíření Linuxu

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/.

3.2. Přístup k I/O portům

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.

3.3. Obslužná rutina přerušení

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).

3.4. Testovací program a knihovna

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ů.

3.5. Obsluha přerušení bez využití knihovny

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.

4. Periodické spouštění programové smyčky

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.

4.1. Blokování signálů

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);

4.2. Real-time priorita vláken

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.

5. Implementace regulátoru

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.

5.1. P-regulátor

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.

5.2. PI-regulátor

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í.

5.3. Efektivní implementace

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: