Obě aplikace (ovládací i Nucleo) lze implementovat rozličným způsobem a lze také začít úplně od začátku, což může být výhodné zejména pokud je cílem realizovat aplikaci s bonusovými funkcionalitami. Doporučený postup je však využít řešení předešlých domácích úkolů, zejména HW 10, které obsahují dílčí části a způsoby řešení nejdůležitější funkcionalit - vícebajtová komunikace, ošetření zdrojů více asynchroních událostí, přerušení na Nucleo desce, vícevláknovou aplikaci a vyčítání klávesnice. S ohledem na dostupnost binárního obrazu s testovacím řešením části pro Nucleo desku je vhodné začít s implementací ovládací aplikace, následně využít dostupnou aplikaci pro Nucleo desku a otestovaci komunikaci a implementovat vlastní aplikaci pro Nucleo desku, např. s využitím předchozí aplikace v rámci řešení HW 10.
Jeden z doporučených postupů může být následující:
'a
' vlákno ošetřující vstup z klávesnice zasílá zprávu EV_ABORT
hlavnímu vláknu, které následně odesílá zprávu MSG_ABORT
Nucleo desce.
INFO
, DEBUG
, WARN
a ERROR
.
xwin_init()
a uvolnění zdrojů xwin_close()
spolu s otestováním generování obrázku, např. barevné pruhy, červený obrázek, zelený obrázek atd., s využitím funkce xwin_redraw()
.
compute()
, kterou identicky můžete použít pro implementaci aplikace na Nucleo desce: uint8_t compute(double cx, double cy, double px, double py, uint8_t max_iteration);
'q
', možnost přerušení výpočtu, atd.
Pokud to bude možné konzultujte svůj postup s řešením semestrální práce se svým cvičícím na cvičení. Zdrojové kódy včas odevzdejte do odevzdávacího systému a domluvte si s cvičícím termín konzultace pro odevzdání a ohodnocení semestrální práce.
Použití vláken není vyloženě nutné, nicméně je velmi doporučené, protože umožňuje organizovat a zpřehlednit kód. V každém vláknu je možné se soustředit na konkrétní části odpovídající obsluze zdroje asynchroních zpráv, případně vizualizaci. Lze využít realizovaných programů HW 09 (Nucleo) - Jednobajtová komunikace nebo HW 10 (Nucleo) - Interaktivní aplikace s komunikací s nadřazeným počítačem, které v zásadě odpovídají ukázce na 8. přednášce.
Například lze realizovat vlákna pro každý zdroj asynchronních události, které jsou v aplikaci v zásadě dva a to
Přirozeně se nabízí vytvořit jedno hlavní vlákno, ve kterém můžeme implementovat aplikační logiku a po jednom vláknu pro každý zdroj asynchronních události (vstup z klávesnice a vyčítání dat ze sériového portu).
Jelikož chceme umožnit okamžité (nebo velmi rychlé) ukončení aplikace při stisku klávesy q
, použijeme neblokovaný režim pro načítání dat ze sériového portu, například s využitím pollingu implementovaného v dostupném modulu prg_serial_nonblock.c
.
Komunikace po sériové lince je v našem případě plně duplexní, proto můžeme bezpečně v jednom vláknu pouze číst ze sériového portu a v jiném vláknu zapisovat. Určitě je dobré číst vstup pouze v jediném vláknu (platí jak pro sériový port, tak pro klávesnici). Z hlediska zápisu na sériový port je obecně vhodné též přístupovat k zápisu z jediného vlákna a to zejména z důvodu zapsání kompletní zprávy na sériový portu. Proto je v případě přístupu k sériovému portu z více vláken využít kritické sekce (tj. exkluzivní přístup řízený mutexem). V našem případě je plně postačující zapisovat na sériový portu například pouze z hlavního vlákna, které řeší kompletní aplikační logiku programu.
Komunikaci mezi vlákny lze realizovat opět různými způsoby. Nicméně v případě hlavního vlákna a dvou vláken pro vyčítání interaktivního uživatelského vstupu z klávesnice a čtení dat ze sériového portu probíhá komunikace pouze směrem od čtecích vláken do hlavního vlákna. Proto hlavní vlákno realizujeme jako smyčku, ve které vyčítáme zprávy z fronty, do které zapisují zprávy vyčítací vlákna. Hlavní vlákno reaguje na uživatelský vstup vytvořením příslušené zprávy pro Nucleo nebo výpisem či aktualizací grafického výstupu po zpracování zpráv od Nuclea.
Motivační fronta zpráv je dostupná v modulu event_queue.c
a odpovídá kruhové frontě realizované v HW 06 - Kruhová fronta v poli. Hlavní rozdíl je v přístupu k datové struktuře z více vláken. Frontu používáme ve stylu producent/konzument, kdy hlavní vlákno je konzumentem a vyčítací vlákna produkují zprávy. Proto je nutné zajistit exkluzivní přístup do fronty prostřednictvím kritické sekce (mutexu).
V případě, že je fronta prázdná, očekáváme, že konzument pozastaví svou činnost do doby, než se objeví nová zpráv, tak abychom zbytečně nevyužívali výpočetní zdroje. Vynucené ukončení od uživatele v tomto případě pošle zprávu, proto nepředstavuje žádný problém. V opačném případě, kdy konzument nestíhá zpracovat zprávy a fronta je plná, je nutné pozastavit činnost konzumentů a v případě zpracování zprávy (uvolnění místa ve frontě) je vhodné takového konzumenta znovou aktivovat. Proto použijeme podmíněné proměnné a mutex. Obecně očekáváme, že hlavní vlákno je dostatečně rychlé a k přeplnění by nemělo docházat často, případně jen nárazově. Podle toho volíme velikost fronty.
Vlastní zprávy ve frontě lze realizovat různě, od jednoduchého ukazatele na typ void
až například po samostatné typy zpráv ve výčtovém typu a rozlišení zdroje zprávy jak je použito v dostupném souboru event_queue.h
.
event_queue.h
.
Konkrétně v event_queue.h
definujeme dva zdroje:
typedef enum { EV_NUCLEO, EV_KEYBOARD, EV_NUM } event_source;
a pro každý zdroj zprávy definujeme samostatný obsah zprávy strukturami
typedef struct { int param; } event_keyboard; typedef struct { message *msg; } event_serial;
Tedy pro vstup z klávesnice máme pouze datovou část v podobně int
hodnoty. V případě zprávy ze sériového portu používáme přímo datovou strukturu message
, kterou předáváme jako ukazatel, abychom se vyhnuli zbytečnému kopírování kompletní zprávy, která může být několik (desítek) bajtů dlouhá.
Samotnou zprávu pak definujeme jako zdroje zprávy (event_source
) a typ zpráv (event_type
), které jsou následně doplněny o vlastní zprávu a to buď struct event_keyboard
nebo struct event_serial
podle toho jestli je typ zprávy EV_KEYBOARD
nebo EV_NUCLEO
. Proto zde s výhodou použijeme typ union
.
typedef struct { event_source source; event_type type; union { int param; message *msg; } data; } event;
Položka data
struktury event
tak obsahuje param
i msg
. To zdali je v proměnné typu event
zpráva od klávesnice (param
) nebo ze sériového portu (msg
) rozlišíme při vytváření zprávy nastavením příslušné datové části a typu zprávy. Příklad použití může vypadat například
void process_event(event *ev) { if (ev->type == EV_KEYBOARD) { printf("Param value %\d\n", ev->data.param); } else if (ev->type == EV_NUCLEO) { printf("Msg ptr %p\n", ev->data.msg); } }
Komunikace po sériovém portu je zápis a čtení posloupností bajtů, které jsou organizovány do zpráv. Ve zvoleném případě zpráv se jedná o zprávy definovaných (fixních) délek. Zcela jistě lze přístupovat ke zprávám jako posloupnosti bajtů a k jednolivým částech přistupovat prostřednictvím indexů prvků. Takový přístup však není příliš čitelný, neboť vyžaduje detailní znalost organizace jednotlivých zpráv. Proto může být výhodnější definovat si pro každý typ zprávy samostatnou strukturu tak, jak je to provedeno v přiloženém souboru messages.h
. Tím získamé relativně čitelný přístup na dílčí položky jednotlivých zpráv. Komunikační zpráva pro Nucleo pak odpovídá struktuře message
, která obsahuje jednobajtový indikátor typu zprávy type
a union možných dílčích zpráv
typedef struct { uint8_t type; // message type union { msg_version version; msg_startup startup; msg_set_compute set_compute; msg_compute compute; msg_compute_data compute_data; } data; uint8_t cksum; // message command } message;
Kromě typu a dílčího obsahu zprávy v položce data
dále zpráva obsahuje položku cksum
, která představuje primitivní způsob jak kontrolovat konzistenci a správnost došlých bajtů. Hodnota cksum
je spočítána tak, aby součet jednotlivých bajtů (uint8_t
) zprávy včetně cksum
byl 0xFF. Tedy pokud se stane nějaká chyba, je možné ji detekovat. Jedná se o základní detekčních mechanismus a v praktických úloha se zpravidla navrhují cycklické redundatní kódy podle charakteru možných chyb. V našem případě nám postačuje tento jednoduchý kontrolní součet cksum
, který je součástí transformace struktury zprávy do pole bajtů, která se provádí ve funkci fill_message_buf()
v modulu messages.c
ve druhé části funkce.
// 2nd - send the message buffer if (ret) { // message recognized buf[0] = msg->type; buf[*len] = 0; // cksum for (int i = 0; i < *len; ++i) { buf[*len] += buf[i]; } buf[*len] = 255 - buf[*len]; // compute cksum *len += 1; // add cksum to buffer }neboť kontrolní součet je poslední bajt zpráv.
cksum
. Obecně však předpokládáme nějaký výchozí definovaný stav, např. běží ovládací aplikace a resetujeme Nucleo. V takovém případě čteme ze sériového portu první znak, na základě kterého identifikuje typ zprávy, který definuje délku zprávy. Proto dále čteme očekávaný počet bajtů. Následně kontrolujeme cksum
. Pokud vše sedí, pokračujeme dále. Pokud ne, hlasíme chybu. Může se však stát taková chyba, kterou nedokážeme jednoduchým cksum
detekovat, případně dojde k výpadku např. jednoho bajtu. Pak interpretujeme přijatá chybná data jako nějaký konkrétní typ zprávy, což může stále fungovat, jen to nebude podle našich původních představ. Obecně se tomu úplně nevyhneme, ale můžeme riziko snížit kontrolou během interpretace dat, použitím lepší detekce CRC nebo samoopravných kódů. V neposlední řadě také uvedením do nějakého výchozího stavu, například resetem Nuclea. Nicméně pro naši aplikaci je sériové rozhraní dostatečně spolehlivé a zpravidla jediný problém, se kterým se můžeme setkat, je při zapnutí nebo přeprogramování Nuclea, při kterém mohou být přečteny ze sérivého portu nějaké náhodné bajty. V takovém případě postačí restart Nuclea a/nebo ovládací aplikace.
Se zprávou jako strukturou se z hlediska přístupu k jednotlivým položkám zprávy pracuje mnohem intuitivněji než indexování do pole bajtů. Nicméně přenos zprávy po sériové lince je přenos definovaného počtu bajtů odpovídající jednotlivým položkám. Převodu zprávy (objektu / datové struktury) na posloupnost bajtů se říká marshalling a v dostupném modulu messages.c
je implementován ve funkci fill_message_buf()
, která je jednou ze tří funkcí vyžadující detailní znalost organizace jendotlivých zpráv jako posloupnosti bajtů. Princip můžeme demonstrovat na zprávě MSG_VERSION
, která je 5 bajtů dlouhá (typ zprávy, tři datové položky - major, minor, patch a cksum
). Vyplnění prvního batju a posledního bajtu je pro všechny zprávy identické, a proto je součástí druhé části funkce uvedené výše. Vlastní datová část pak odpovída případu kdy msg→type
je MSG_VERSION
, např.
switch(msg->type) { ... case MSG_VERSION: buf[1] = msg->data.version.major; buf[2] = msg->data.version.minor; buf[3] = msg->data.version.patch; *len = 4; break; ... buf[0] = msg->type; ... buf[*len] = 255 - buf[*len];kde je demonstrován přístup k datové položce
data
typu union
, která je v tomto případě validní jako data.version
, neboť type
je MSG_VERSION
, který používáme právě pro indikaci typu zprávy.
Při čtení zprávy ze sériového portu očekáváme, že ja vstupní buffer prázdný a první znak, který přečteme, odpovídá typu zprávy. Na základě typu zprávy můžeme určit počet bajtů zprávy, které je nutné přečíst, abychom mohli vytvořit zprávu konkrétního typu jako proměnnou typu struct
. V modulu messages.c
je definice funkce get_message_size()
, která vrací očekávanou délku zprávy podle typu prostřednictvím předáváného ukazatele len
. Například pro zprávu typu MSG_VERSION
může funkce vypadat
bool get_message_size(uint8_t msg_type, int *len) { bool ret = true; switch(msg_type) { ... case MSG_VERSION: *len = 2 + 3 * sizeof(uint8_t); // 2 + major, minor, patch break; ... default: ret = false; break; } return ret; }což je explicitní zápis pro 2 bajty společné pro všechny zprávy a 3 bajty pro major, minor a patch. Nic nám nebrání definovat například
*len = 5;Případně s využitím číslování typu zpráv jako pole. Konkrétní způsob je tak spíše věcí vkusu a představy o čitelnosti.
V okamžiku, že načteme očekávaný počet bajtů, můžeme pole bajtů interpretovat jako konkrétní zprávu. Příklad implementce je opět dostupný v modulu messages.c
ve funkci parse_message_buf()
, která může například pro MSG_VERSION
vypadat následovně.
bool parse_message_buf(const uint8_t *buf, int size, message *msg) { uint8_t cksum = 0; for (int i = 0; i < size; ++i) { cksum += buf[i]; } bool ret = false; int message_size; if ( size > 0 && cksum == 0xff && // sum of all bytes must be 255 ((msg->type = buf[0]) >= 0) && msg->type < MSG_NBR && get_message_size(msg->type, &message_size) && size == message_size) { ret = true; switch(msg->type) { ... case MSG_VERSION: msg->data.version.major = buf[1]; msg->data.version.minor = buf[2]; msg->data.version.patch = buf[3]; break; ... default: // unknown message type ret = false; break; } // end switch } return ret; }Opět vycházíme ze znalosti definice komunikačních zpráv jako posloupnosti bajtů. Testujeme
cksum
a případně reportujeme chybu návratovou hodnout false
.
messages.c
je, že pouze v tomto modulu je nutné použít znalost definice komunikačních zpráv jako posloupnosti bajtů. Máme tak pouze jedno místo, které zvyšuje přehlednost a případné ladění chyb.