{{indexmenu_n>10}}
====== 10 - Komunikace STM32F446RE s PC programem ======
* pro vyučující: [[courses:b3b36prg:internal:tutorialinstruction:10|]]
==== Procvičovaná témata ====
* Seriová vícebajtová (paketová) komunikace v prostředí ''mbed.org''
* Vyvolání přerušení a obslužné rutiny po stisku tlačítka v ''mbed.org''
* Definice komunikačních zpráv PC-Nucleo (//marshalling// and //unmarshalling//)
* Program pro Nucleo v ''mbed.org'' (Nucleo část HW 10)
* Načítání a posílání vícebajtových zpráv v PC aplikaci
* Možná architektura ovládací aplikace pro PC
Ve verzi mbed 149 došlo k úpravě chování přerušení Tx při odeslání znaku. Popis práce s přerušením v mbed r148 a nižší je popsán na [[courses:b3b36prg:labs:lab10old|původní verzi informací ke cvičení.]].
{{:courses:b3b36prg:labs:lab10.zip|Podpůrné soubory}} včetně binárního obrazu aplikace pro STM32F446RE.
===== Úkoly na cvičení =====
==== Sériová komunikace s přerušením v mbed.org ====
=== Přerušení a obslužné rutiny ===
V případě vícebajtové (paketové) komunikace mezi Nucleo deskou a řidícím počítačem může být výhodné v Nucleo aplikaci vyčítat a zapisovat data bajt po bajtu asynchronně ("na pozadí") a v hlavním programu používát pouze operace pro odeslání nebo načtení celé zprávy, což značně zjednodušuje implementaci. Toho můžeme docílít vytvořením kruhových front (bufferů) pro zápis a čtení ze sériového portu. V případě zápisu můžeme využít generování přerušení v okamžiku přenesení bytu po sériové lince, **kdy dojde k vyvolání přerušení v okamžiku prázdného odesílacího jednobajtového bufferu**. V programu si tak můžeme připravit zprávu jako posloupnost bajtů, kterou pošleme na sériový port tak, že překopírujeme data do kruhového bufferu, ze kterého budou jednotlivé bajty postupně odesílány na sériový port v obsluze přerušení, např.
void Tx_interrupt()
{
// send a single byte as the interrupt is triggered on empty out buffer
if (tx_in != tx_out) {
serial.putc(tx_buffer[tx_out]);
tx_out = (tx_out + 1) % BUF_SIZE;
} else { // buffer sent out, disable Tx interrupt
USART2->CR1 &= ~USART_CR1_TXEIE; // disable Tx interrupt
}
return;
}
**V obsluze přerušení je nutné zamaskovat Tx přerušení v okamžiku, kdy jsou všechna data z bufferu odeslána. Přerušení je totiž vyvoláno při prázdném buffer a program by tak nikdy přerušení neopustil, pokud by se zrovna žádná data neodesílala.
**
Podobně pro čtení můžeme využít generování přerušení v okamžiku příjmu bajtu na sériovém portu, který v obsluze přerušení pouze vložíme do bufferu. V hlavní aplikaci pak můžeme vykonávat jinou činnost a v nějakém kontrolním bodě otestujeme, zdali jsou v bufferu nová data, která z bufferu načteme do vlastní struktury reprezentující jendotlivé zprávy (pakety). Např.
void Rx_interrupt()
{
// receive bytes and stop if rx_buffer is full
while ((serial.readable()) && (((rx_in + 1) % BUF_SIZE) != rx_out)) {
rx_buffer[rx_in] = serial.getc();
rx_in = (rx_in + 1) % BUF_SIZE;
}
return;
}
V prostřed ''mbed.org'' můžeme přiradit obsluhu přerušení prostřednictvím funkce ''serial.attach()''. Nejdříve však deklarujeme funkce pro obsluhu přerušení při vysílání a příjmu dat, tj. ''Tx_interrupt()'' a ''Rx_interrupt()''. Dále deklarujeme globání proměnné pro kruhové buffery pro vysílání a příjem dat spolu s příslušnými ukazateli do kruhového bufferu.
Serial serial(SERIAL_TX, SERIAL_RX);
void Tx_interrupt();
void Rx_interrupt();
#define BUF_SIZE 255
char tx_buffer[BUF_SIZE];
char rx_buffer[BUF_SIZE];
// pointers to the circular buffers
volatile int tx_in = 0;
volatile int tx_out = 0;
volatile int rx_in = 0;
volatile int rx_out = 0;
int main(void) {
...
serial.attach(&Rx_interrupt, Serial::RxIrq); // attach interrupt handler to receive data
serial.attach(&Tx_interrupt, Serial::TxIrq); // attach interrupt handler to transmit data
...
=== Funkce pro zápis (poslání) zprávy po sériovém portu ===
V našem případě definujeme jednotlivé komunikační zprávy jako posloupnost bajtů v definovaném rámci (paketu), tj. každá zpráva (paket) má přesně definovanou délku, přičemž první bajt zprávy určuje typ zprávy a poslední bajt zprávy obsahuje tzv. kontrolní součet (''cksum''). Bajty můžeme zapisovat do ''tx_buffer'' přímo. Alternativně definujeme složený typ ''struct message'', který v kombinaci s ''union'' umožní pohodlný přístup na jednotlivé položky zpráv (paketů). Následně takovou zprávu převedeme na posloupnost bajtů, kterou uložíme do kruhového bufferu. Přestože budeme provádět kopírování jednotlivých bajtů vícekrát, výhodou tohoto přístupu je přehlednost a flexibilita ve snadné rozšiřitelnosti. Nicméně při kopírování dat do ''tx_buffer'' může být aktuální buffer plný, a celá zpráva se tam nemusí vejít. Proto kopírování dat pozastavíme do doby, než dojde k uvolnění ''tx_buffer''. Během kopírování však musíme zajistit exkluzivní přístup k ukazatelům ''tx_in'' a ''tx_out'' v rámci kritické sekce, což provedeme zakázáním přerušení, které naopak pro uvolnění ''tx_buffer'' musíme opět povolit. Funkce pro odeslání paketu definované délky provedeme například funkcí ''send_buffer()''.
bool send_buffer(const uint8_t* msg, unsigned int size)
{
if (!msg && size == 0) {
return true; // size must be > 0
}
bool ret = false;
int i = 0;
NVIC_DisableIRQ(USART2_IRQn); // start critical section for accessing global data
USART2->CR1 |= USART_CR1_TXEIE; // enable Tx interrupt on empty out buffer
bool empty = (tx_in == tx_out);
while ( (i == 0) || i < size ) { //end reading when message has been read
if ( ((tx_in + 1) % BUF_SIZE) == tx_out) { // needs buffer space
NVIC_EnableIRQ(USART2_IRQn); // enable interrupts for sending buffer
while (((tx_in + 1) % BUF_SIZE) == tx_out) {
/// let interrupt routine empty the buffer
}
NVIC_DisableIRQ(USART2_IRQn); // disable interrupts for accessing global buffer
}
tx_buffer[tx_in] = msg[i];
i += 1;
tx_in = (tx_in + 1) % BUF_SIZE;
} // send buffer has been put to tx buffer, enable Tx interrupt for sending it out
USART2->CR1 |= USART_CR1_TXEIE; // enable Tx interrupt
NVIC_EnableIRQ(USART2_IRQn); // end critical section
return ret;
}
=== Funkce čtení (příjem zprávy ze sériovém portu ===
Podobně můžeme postupovat při čtení zprávy ze sériového portu. Jelikož chceme v programu vždy pracovat s celými zprávami, je nutné po načtení prvního bajtu identifikovat zprávu (paket) a určit její délku. Délku určíme funkcí
''bool get_message_size(uint8_t msg_type, int *len)'', která vrací hodnotu ''true'' pokud se podařilo zprávu identifikovat dle předané hodnoty ''msg_type''. Jakmile načteme délku zprávy můžeme pokračovat v načítání příslušného počtu bajtů z ''rx_buffer'', ale podobně jako při odesílání, nemusí být zpráva ještě kompletně přijata. V takovém případě vyčkáme dokud se ''rx_buffer'' nenaplní v obsluze přerušení ''Rx_interrupt()''. Také v tomto případě musíme zajistit přístup k ''rx_buffer'' v rámci kritické sekce. Načtení zprávy můžeme například implementovat ve funkci ''receive_message()''.
bool receive_message(uint8_t *msg_buf, int size, int *len)
{
bool ret = false;
int i = 0;
*len = 0; // message size
NVIC_DisableIRQ(USART2_IRQn); // start critical section for accessing global data
while ( (i == 0) || (i != *len) ) {
if (rx_in == rx_out) { // wait if buffer is empty
NVIC_EnableIRQ(USART2_IRQn); // enable interrupts for receing buffer
while (rx_in == rx_out) { // wait of next character
}
NVIC_DisableIRQ(USART2_IRQn); // disable interrupts for accessing global buffer
}
uint8_t c = rx_buffer[rx_out];
if (i == 0) { // message type
if (get_message_size(c, len)) { // message type recognized
msg_buf[i++] = c;
ret = *len <= size; // msg_buffer must be large enough
} else {
ret = false;
break; // unknown message
}
} else {
msg_buf[i++] = c;
}
rx_out = (rx_out + 1) % BUF_SIZE;
}
NVIC_EnableIRQ(USART2_IRQn); // end critical section
return ret;
}
Podrobnější informace o seriovém portu a přerušení v ''mbed.org'' lze najít na [[https://developer.mbed.org/cookbook/Serial-Interrupts]].
==== Obsluha stisknutého tlačítka v přerušení v mbed.org ====
V naší aplikaci bude Nucleo deska dostávát výpočetní úlohy od ovládacího počítače a dílčí výsledky posílát přes sériový port řidící aplikaci. V průběhu výpočtu může dojít k jeho přerušení buď příslušnou zprávou od řidícího programu nebo po stisknutí uživatelského tlačítka na Nucleo desce. Jelikož v hlavním programu budeme chtít prioritně vyčítat zprávy (na bufferované v ''rx_buffer'') a především počítat jednolivé dávky výpočtů, nemusí být příma obsluha tlačítka efektivní, neboť vyčtení hodnoty může být realizováno až s velkým zpožděním a změnu tak vůbec nemusíme zaregistrovat. Z tohoto důvodu zavedeme globální proměnnou ''abort_request'', kterou nastavíme obslužné rutině ''button()'' přerušení vyvolané změnou náběžnou hranou signálu od tlačítka, tj. po uvolnění stisknutého tlačítka. Změnu proměnné pak provedeme v hlavním smyčce programu po zpracování požadavku na přeručení výpočtu. Zdrojový kód může vypadat například následovně.
InterruptIn button_event(USER_BUTTON);
volatile bool abort_request = false;
void button()
{
abort_request = true;
}
...
int main()
{
...
button_event.rise(&button);
...
while (1) {
if (abort_request) {
if (computing) { //abort computing
msg.type = MSG_ABORT;
send_message(&msg, msg_buf, MESSAGE_SIZE);
computing = false;
abort_request = false;
ticker.detach();
myled = 0;
}
}
... /// handle incoming messages
}
}
==== Definice komunikačních zpráv ====
Komunikace mezi Nucleo programem a řidícím program probíhá prostřednictvím paketových zpráv, ve které je PC program nadřazený Nucleo programu, tj. PC program zasílá zprávy s požadavky a Nucleo program odpovídá jednou nebo více zprávámi. Typy zpráv jsou následující:
* ''MSG_OK'' - kladná odpověď na přijatou zprávu ''MSG_ABORT'', ''MSG_COMPUTE''
* ''MSG_ERROR'' - odpověď na přijatou zprávu ''MSG_ABORT'', ''MSG_COMPUTE'' došlo k chybě
* ''MSG_ABORT'' - Z PC požadavek na předčasné ukončení výpočtu. Z Nucleo desky se jedná asynchronní zprávu po detekci stisknutí tlačítka, pokud běží výpočet, je tento výpočet ukončen a Nucleo zasílá //abort// zprávu.
* ''MSG_DONE'' - Poslední zpráva definující úspěšné ukončení kompletního výpočtu ''MSG_COMPUTE''.
* ''MSG_GET_VERSION'' - Žádost o zaslání verze firmware Nucleo programu. Nucleo odpovídá zprávou ''MSG_VERSION''.
* ''MSG_VERSION'' - Informace o verzi firmware Nucleo programu.
* ''MSG_STARTUP'' - Asynchronní zpráva zasílána Nucleo programem po startu jedná se o posloupnost 9 znaků ''PRG-LAB10''.
* ''MSG_COMPUTE'' - Požadavek na výpočet balíku úloh identifikovaného ''chunk_id'' s počtem úloh ''nbr_tasks''.
* ''MSG_COMPUTE_DATA'' - Dílčí výsledek výpočtu identifikovaný ''chunk_id'', ''task_id''.
=== Datové struktury komunikačních zpráv ===
Dílčí typy zpráv definujeme výčtovým typem ''message_type''.
// Definition of the communication messages
enum {
MSG_OK, // ack of the received message
MSG_ERROR, // report error on the previously received command
MSG_ABORT, // abort - from user button or from serial port
MSG_DONE, // report the requested work has been done
MSG_GET_VERSION, // request version of the firmware
MSG_VERSION, // send version of the firmware as major,minor, patch level, e.g., 1.0p1
MSG_STARTUP, // init of the message (id, up to 8 bytes long string, cksum
MSG_COMPUTE, // request computation of a batch of tasks (chunk_id, nbr_tasks)
MSG_COMPUTE_DATA, // computed result (chunk_id, task_id, result)
MSG_NBRb
} message_type;
Každá zpráva začíná 1 bajtem indetifikující typ zprávy a končí posledním bajtem kontrolního součtu. Zprávy, které mají další data definujeme jako složené typy (''struct'')
typedef struct {
uint8_t major;
uint8_t minor;
uint8_t patch;
} msg_version;
#define STARTUP_MSG_LEN 9
typedef struct {
uint8_t message[STARTUP_MSG_LEN];
} msg_startup;
typedef struct {
uint16_t chunk_id;
uint16_t nbr_tasks;
} msg_compute;
typedef struct {
uint16_t chunk_id;
uint16_t task_id;
uint8_t result;
} msg_compute_data;
Všechny možné zprávy definujeme ve složeném typu ''message'' s využítím ''union'' pro datové části jednotlivých dílčích typů zpráv.
typedef struct {
uint8_t type; // message type
union {
msg_version version;
msg_startup startup;
msg_compute compute;
msg_compute_data compute_data;
} data;
uint8_t cksum; // message command
} message;
V programu dále využijeme deklarovaný obsah zprávy ''msg_version'' s informací o verzi firmware z textovaých konstant preprocesoru, např.
#define VERSION_MAJOR 0
#define VERSION_MINOR 9
#define VERSION_PATCH 0
msg_version VERSION = { .major = VERSION_MAJOR, .minor = VERSION_MINOR, .patch = VERSION_PATCH };
=== Načtení zprávy z pole bajtů (unmarshalling) a vytvoření posloupnosti bajtů ze zprávy (marshalling) ===
Pro načtení zprávy potřebujeme informaci o velikosti (počtu bajtů) zprávy, což můžeme implementovat přes inicializované pole nebo například případně funkcí ''get_message_size()'':
bool get_message_size(uint8_t msg_type, int *len)
{
bool ret = true;
switch(msg_type) {
case MSG_OK:
case MSG_ERROR:
case MSG_ABORT:
case MSG_DONE:
case MSG_GET_VERSION:
*len = 2; // 2 bytes message - id + cksum
break;
case MSG_STARTUP:
*len = 2 + STARTUP_MSG_LEN;
break;
case MSG_VERSION:
*len = 2 + 3 * sizeof(uint8_t); // 2 + major, minor, patch
break;
case MSG_COMPUTE:
*len = 2 + 2 * sizeof(uint16_t); // 2 + chunk_id (16bit) + nbr_tasks (16bit)
break;
case MSG_COMPUTE_DATA:
*len = 2 + 2 * sizeof(uint16_t) + 1; // 2 + chunk_id (16bit) + task_id (16bit) results (8bit)
break;
default:
ret = false;
break;
}
return ret;
}
Jakmile známe délku zprávy a příslušný počet bajtů byl načten, můžeme vyplnit datovou strukturu zprávy např. funkcí ''parse_message()''
bool parse_message(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_OK:
case MSG_ERROR:
case MSG_ABORT:
case MSG_DONE:
case MSG_GET_VERSION:
break;
case MSG_STARTUP:
for (int i = 0; i < STARTUP_MSG_LEN; ++i) {
msg->data.startup.message[i] = buf[i+1];
}
break;
case MSG_VERSION:
msg->data.version.major = buf[1];
msg->data.version.minor = buf[2];
msg->data.version.patch = buf[3];
case MSG_COMPUTE: // type + chunk_id + nbr_tasks
msg->data.compute.chunk_id = (buf[1] << 8) | buf[2];
msg->data.compute.nbr_tasks = (buf[3] << 8) | buf[4];
break;
case MSG_COMPUTE_DATA: // type + chunk_id + task_id + results
msg->data.compute_data.chunk_id = (buf[1] << 8) | buf[2];
msg->data.compute_data.task_id = (buf[3] << 8) | buf[4];
msg->data.compute_data.result = buf[5];
default: // unknown message type
ret = false;
break;
} // end switch
}
return ret;
}
ve které je patrné, že datové části ''chunk_id'', ''nbr_tasks'' a ''task_id'' jsou 16 bitové hodnoty (''uint16_t'') a ve zprávě se nejdříve ukládá více významný bajt (hi-lo).
Podobně můžeme implementovat vyplnění pole bajtů ze struktury ''message'' podle dílčího typu zprávy. V našem případě voláme jak ''parse_message()'', tak odeslání zprávy z hlavní smyčky programy. Navíc odeslání znamená překopírování zprávy do ''tx_buffer'', proto vytvoříme funkce ''send_message()'', která nejdříve provede tzv. //marshalling// a následně zavolá funkci ''send_buffer()''.
bool send_message(const message *msg, uint8_t *buf, int size)
{
if (!msg || size < sizeof(message) || !buf) {
return false;
}
// 1st - serialize the message into a buffer
bool ret = true;
int len = 0;
switch(msg->type) {
case MSG_OK:
case MSG_ERROR:
case MSG_ABORT:
case MSG_DONE:
case MSG_GET_VERSION:
len = 1;
break;
case MSG_STARTUP:
for (int i = 0; i < STARTUP_MSG_LEN; ++i) {
buf[i+1] = msg->data.startup.message[i];
}
len = 1 + STARTUP_MSG_LEN;
break;
case MSG_VERSION:
buf[1] = msg->data.version.major;
buf[2] = msg->data.version.minor;
buf[3] = msg->data.version.patch;
len = 4;
break;
case MSG_COMPUTE:
buf[1] = (uint8_t)(msg->data.compute.chunk_id >> 8); // hi - chunk_id
buf[2] = (uint8_t)msg->data.compute.chunk_id; // lo - chunk_id
buf[3] = (uint8_t)(msg->data.compute.nbr_tasks >> 8);// hi - nbr_tasks
buf[4] = (uint8_t)msg->data.compute.nbr_tasks; // lo - nbr_tasks
len = 5;
break;
case MSG_COMPUTE_DATA:
buf[1] = (uint8_t)(msg->data.compute_data.chunk_id >> 8);// hi - chunk_id
buf[2] = (uint8_t)msg->data.compute_data.chunk_id; // lo - chunk_id
buf[3] = (uint8_t)(msg->data.compute_data.task_id >> 8); // hi - task_id
buf[4] = (uint8_t)msg->data.compute_data.task_id; // lo - task_id
buf[5] = msg->data.compute_data.result; // results
len = 6;
break;
default: // unknown message type
ret = false;
break;
}
// 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
ret = send_buffer(buf, len);
}
return ret;
}
=== Aplikace pro Nucleo (mbed.org) ===
Výše uvedené funkce pro práci se zprávami můžeme s výhodou využít pro implementanci hlavní smyčky programu pro Nucleo desku. Program má reagovat na zasílané zprávy z řidícího počítače a v případě zahájení výpočtu je výpočet možné přerušit buď stiskem tlačítka nebo zaslání zprávy ''MSG_ABORT''. V našem případě budeme náročný výpočet simulovat 200 ms čekáním, což představuje kompromis mezi odezvou systému a využitím výpočetních zdrojů. Výsledkem tohoto "výpočtu" je náhodné číslo od 0 do 255 (funkce ''rand()''). Nakonec největší brzdou aplikace je právě komunikace po sériovém portu rychlostí 115200 bps spolu s relativně neúsporným komunikačním protokolem (na jeden vypočtený bajt potřebujeme přenést 7 bajtů), který však na druhou stranu umožňuje relativně jednoduše dávkovat výpočet na dílčí části. Vlastnosti aplikace jsou
* Po startu program 5x krátce zabliká LED a pošle zprávu ''MSG_STARTUP''.
* Dále čeká na příkazy ze sériového portu.
* Na zprávu ''MSG_GET_VERSION'' zasílá ''MSG_VERSION''.
* Na zprávu ''MSG_COMPUTE'' odpovídá ''MSG_ERROR'' při chybě nebo ''MSG_OK'' a zahajuje výpočet, při kterém bliká LED. Po dokončení dílčího výpočtu zasílá zprávu ''MSG_COMPUTE_DATA'' a po dokončení celé dávky výpočtu zasílá ''MSG_DONE'' a ukončuje blikání LED.
* Běžící výpočet je možné přerušit zprávou ''MSG_ABORT'', na kterou Nucleo odpovídá ''MSG_OK'' nebo ''MSG_ERROR''
* Výpočet je také možné přerušit stisknutím tlačítka, které nejen ukončí výpočet a blikání LED, ale také zašle zprávu ''MSG_ABORT'', tj. výpočet končí buď zaslání ''MSG_ABORT'' nebo ''MSG_DONE''.
Hlavní smyčka může například vypadat
while (1) {
if (abort_request) {
if (computing) { //abort computing
msg.type = MSG_ABORT;
send_message(&msg, msg_buf, MESSAGE_SIZE);
computing = false;
abort_request = false;
ticker.detach();
myled = 0;
}
}
if (rx_in != rx_out) { // something is in the receive buffer
if (receive_message(msg_buf, MESSAGE_SIZE, &msg_len)) {
if (parse_message(msg_buf, msg_len, &msg)) {
switch(msg.type) {
case MSG_GET_VERSION:
msg.type = MSG_VERSION;
msg.data.version = VERSION;
send_message(&msg, msg_buf, MESSAGE_SIZE);
break;
case MSG_ABORT:
msg.type = MSG_OK;
send_message(&msg, msg_buf, MESSAGE_SIZE);
computing = false;
abort_request = false;
ticker.detach();
myled = 0;
break;
case MSG_COMPUTE:
if (msg.data.compute.nbr_tasks > 0) {
ticker.attach(tick, period);
compute_data.chunk_id = msg.data.compute.chunk_id;
compute_data.nbr_tasks = msg.data.compute.nbr_tasks;
compute_data.task_id = 0; // reset the task counter
computing = true;
}
msg.type = MSG_OK;
send_message(&msg, msg_buf, MESSAGE_SIZE);
break;
} // end switch
} else { // message has not been parsed send error
msg.type = MSG_ERROR;
send_message(&msg, msg_buf, MESSAGE_SIZE);
}
} // end message received
} else if (computing) {
if (compute_data.task_id < compute_data.nbr_tasks) {
wait(compute_time); // do some computation
msg.type = MSG_COMPUTE_DATA;
msg.data.compute_data.chunk_id = compute_data.chunk_id;
msg.data.compute_data.task_id = compute_data.task_id;
msg.data.compute_data.result = rand() % 255;
compute_data.task_id += 1;
send_message(&msg, msg_buf, MESSAGE_SIZE);
} else { //computation done
ticker.detach();
myled = 0;
msg.type = MSG_DONE;
send_message(&msg, msg_buf, MESSAGE_SIZE);
computing = false;
}
} else {
sleep(); // put the cpu to sleep mode, it will be wakeup on interrupt
}
} // end while (1)
}
===== Úkoly =====
* Implementujete program pro Nucleo.
* Otestujte základní chování programu terminálem např. ''cutecom'' nebo ''GTKTerm'' případně prostřednictvím přiložené aplikace ''lab10-main''
Program představuje řešení úlohy [[courses:b3b36prg:hw:hw10|HW 10]] části pro Nucleo.
* Ve zbývajícím čase cvičení se seznamte s úlohou [[courses:b3b36prg:hw:hw10|HW 10]] a možným řešením ovládací aplikace
* Implementujte ovládací aplikaci ''hw10-main''.