Search
* pro vyučující: 10
lab10.zip
mbed.org
Podpůrné soubory včetně binárního obrazu aplikace pro STM32F446RE.
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; }
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.attach()
Tx_interrupt()
Rx_interrupt()
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 ...
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().
cksum
tx_buffer
struct message
union
tx_in
tx_out
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; }
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 get_message_size(uint8_t msg_type, int *len)
true
msg_type
rx_buffer
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; }
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ě.
abort_request
button()
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 } }
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
MSG_ABORT
MSG_COMPUTE
MSG_ERROR
MSG_DONE
MSG_GET_VERSION
MSG_VERSION
MSG_STARTUP
PRG-LAB10
chunk_id
nbr_tasks
MSG_COMPUTE_DATA
task_id
Dílčí typy zpráv definujeme výčtovým typem message_type.
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)
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.
message
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ř.
msg_version
#define VERSION_MAJOR 0 #define VERSION_MINOR 9 #define VERSION_PATCH 0 msg_version VERSION = { .major = VERSION_MAJOR, .minor = VERSION_MINOR, .patch = VERSION_PATCH };
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():
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()
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).
uint16_t
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().
send_message()
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; }
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
rand()
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) }
cutecom
GTKTerm
lab10-main
hw10-main