{{indexmenu_n>10}} ====== 10 - Komunikace STM32F446RE s PC programem ====== * pro vyučující: [[courses:b3b36prg:internal:tutorialinstruction:10|]] ^ Výchozí soubory | {{ :courses:b3b36prg:labs:lab10.zip |}} | ^ Podpora | [[courses:b3b36prg:resources:nucleo|Video tutoriál k mbed.org a Nucleo (97 min)]]| ==== 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 false; // size must be > 0 } 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 true; } === 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)) && i < size ) { 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_NBR } 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]; break; 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]; break; 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''.