{{indexmenu_n>12}} ====== 12 - Propojení STM32F446RE sériovou linkou ====== /* ^ Výchozí soubory | {{:courses:b3b36prg:labs:prg-lab11.zip}} včetně binárních obrazů aplikací pro STM32F446RE | */ * Seriová jednobytová komunikace na straně Nucleo. * Seriová jednobytová komunikace na straně počítače s OS. * Propojení Nuclea a počítače, blikání LED a reakce na tlačítko. * Diskuze kompilace, křížové kompilace a flashování. [[courses:b3b36prg:internal:tutorialinstruction:11|(Pro vyučující.)]] ===== Jednobytová komunikace na straně Nucleo ===== * Pracujeme v [[https://studio.keil.arm.com/|Keil Studio]]. Použijeme objekt ''BufferedSerial'' nad USB, s rychlostí nastavenou na 115200 baudů. * Nastavme chování objektu na "blokující". Tzn. MCU nepostoupí na další instrukci dokud nedorazí zpráva. * Po restartu Nuclea program krátce (5x s periodou 50ms) zabliká a pošle na seriový port inicializační byte ''i'' a zhasne LED. * Následně program bude reagovat na přijetí znaků * ''s'' - rozsvícení LED. Nucleo pošle zpět na seriový port znak ''a'' (ack). * ''e'' - zhasnutí LED. Nucleo pošle zpět na seriový port znak ''a'' (ack). * ''f'' - změní stav LED. Nucleo pošle zpět na seriový port znak ''a'' (ack). * Na jiný znak program odpoví chybovou zprávou ''!'' (fail). * V případě, že není zrovna k dispozici zpráva, Nucleo pošle znak ''*''. (Toto nastane v případě neblokovaného režimu.) * Program otestujte na počítači pomocí terminálu, např. ''GTKterm'', ''cutecom'' nebo ''jerm''. Terminál připojte k portu ''\dev\ttyACM0''. * Vyzkoušejte, jak se chování změní v neblokujícím režimu. #include "mbed.h" DigitalOut myled(LED1); BufferedSerial serial (USBTX, USBRX, 115200); void blink(uint32_t i, Kernel::Clock::duration_u32 t = 50ms){ for(; i > 0; i--){ myled = true; ThisThread::sleep_for(t); myled = false; ThisThread::sleep_for(t); } } int main() { serial.set_blocking(true); const char init_msg = 'i'; serial.write(&init_msg, 1); blink(5); while(true){ char received_value; ssize_t received_in_fact = serial.read(&received_value, 1); char acknowledge_msg = 'a'; if(received_in_fact == 1){ switch(received_value){ case 's': myled = true; break; case 'e': myled = false; break; case 'f': myled = !myled; break; default: acknowledge_msg = '!'; } }else{ acknowledge_msg = '*'; } serial.write(&acknowledge_msg, 1); } } ===== Jednobytová komunikace na straně počítače s OS ===== V rámci operačního systému je zpravidla sériový port zpřístupňěn jako **soubor** (tzv. blokového zařízení) v adresáři ''/dev'', např. ''/dev/ttyUSB0'', ''/dev/cuaU0'' nebo ''/dev/ttyACM0'', v závislosti na konkrétním operačním systému a příslušném ovladači. (Po připojení Nuclea k počítači například spusťte příkaz ''dmesg'', který zobrazí jak bylo Nucleo rozpoznáno.) Možností otevření a konfigurace sériového rozhraní je několik. Pro jednoduchost lze využít funkce z modulu ''prg_serial'', který je dostupný na [[courses:b3b36prg:tutorials:serial|návodné stránce]]. * serial_open() - otevře daný soubor seriového portu a nastaví rychlost na 115200 bps * serial_close() - zavře daný file deskriptor * serial_putc() - zapíše daný znak do sériového portu * serial_getc() - přečte jeden znak ze sériového port S využítím funkcí z ''prg_serial.h'' nahraďte virtuální termínál použitý v minulé úloze (''GTKterm'' apod.) vlastní aplikaci pro komunikaci s Nucleo boardem. Aplikaci rozšiřte například popisem jednotlivých akcí výpisem na terminal. Uvědomte si, se kterými všemi soubory se pracuje (stdin, stdout, sériové rozhraní). #include #include #include #include "prg_serial.h" int main(int argc, char *argv[]){ int ret = 0; const char *serial = argc > 1 ? argv[1] : "/dev/ttyACM0"; fprintf(stderr, "INFO: open serial port at %s\n", serial); int fd = serial_open(serial); if (fd != -1) { // read from serial port _Bool quit = false; while (!quit) { int c = getchar(); _Bool write_read_response = false; switch (c) { case 'q': printf("Quit the program\n"); quit = true; break; case 's': printf("Send 's' - LED on\n"); write_read_response = true; break; case 'e': printf("Send 'e' - LED off\n"); write_read_response = true; break; case 'f': printf("Send 'f' - LED switch\n"); write_read_response = true; break; default: printf("Ignoring char '%d'\n", c); break; } if (write_read_response) { int r = serial_putc(fd, c); if (r != -1) { fprintf(stderr, "DEBUG: Received response '%d'\n", r); } else { fprintf(stderr, "ERROR: Error in received responses\n"); } } } serial_close(fd); } else { fprintf(stderr, "ERROR: Cannot open device %s\n", serial); } return ret; } ===== Nastavení režimu "raw" ===== Načítání stisku klávesy vyžaduje z důvodu bufferování standardního vstupu potvrzení koncem řádku (stisknutí klávesy enter), což není příliš šikovné. Proto nastate terminál do tzv. "raw" režimu např. prostřednictvím funkce ''cfmakeraw()'' nebo ''stty raw -echo'', viz 6. přednáška. Po skončení programu je vhodné terminál opět přepnout do běžného režimu, proto implementujte funkce pro přepínání do/z raw řežimu např. void set_raw(_Bool set) { static struct termios tio, tioOld; tcgetattr(STDIN_FILENO, &tio); if (set) { // put the terminal to raw tioOld = tio; //backup cfmakeraw(&tio); tio.c_lflag &= ~ECHO; // assure echo is disabled tio.c_oflag |= OPOST; // enable output postprocessing tcsetattr(STDIN_FILENO, TCSANOW, &tio); } else { // set the previous settingsreset tcsetattr(STDIN_FILENO, TCSANOW, &tioOld); } } Nebo využitím volání externího procesu funkcí ''system()'', jejímž argumentem je jméno programu spolu s příslušnými argumenty, např. void set_raw(_Bool set) { if (set) { system("stty raw -echo"); // enable raw, disable echo } else { system("stty -raw echo"); // disable raw, enable echo } } S využitím raw režimu terminálu zjednodušíme aplikaci pro ovládání LED na Nucleo desce ++++ Možné řešení | int ret = 0; char c; const char *serial = argc > 1 ? argv[1] : "/dev/ttyACM0"; int fd = serial_open(serial); if (fd != -1) { // read from serial port set_raw_1(1); // set the raw mode _Bool quit = 0; while (!quit) { if ((c = getchar()) == 's' || c == 'e' || c == 'f') { if (serial_putc(fd, c) == -1) { fprintf(stderr, "ERROR: Error in received responses\n"); quit = 1; } } quit = c == 'q'; } // end while() serial_close(fd); set_raw_1(0); } else { fprintf(stderr, "ERROR: Cannot open device %s\n", serial); } return ret; ++++ Program vyzkoušejte a také otestujte chování při náhlem odpojení Nucleo desky při běhu programu nebo resetu Nuclea desky. ** ** == Rozbitý terminál po ukončení programu v "raw" režimu == Pokud program nastaví terminál do režimu "raw" a skončí (například příkazem ''kill'', nebo-li signálem ''SIGINT''), terminál si může zachovat s původní nastavení. V takovém případě je možné zkusit příkaz ''reset'', nebo program vybavit handlerem signálu: void handler(int signal_code) { set_raw_1(0); exit(0); } int main(){ signal(SIGINT, intHandler); ... } ===== Aplikace pro ovládání blikání LED na Nucleo desce ===== Oba programy rozšiřte pro ovládání blikání LED zasláním znaků * ''1'' - nastaví periodu blikání LED na 50 ms * ''2'' - nastaví periodu blikání LED na 100 ms * ''3'' - nastaví periodu blikání LED na 200 ms * ''4'' - nastaví periodu blikání LED na 500 ms * ''5'' - nastaví periodu blikání LED na 1000 ms. * ''0'' - zastaví blikání, nechá LED ve stavu v jakém zrovna je. Nucleo pošle po spuštění znak ''i'' a každé přijetí příkazu pro nastavení periody potvrdí znakem ''a''. Aplikace na straně počítače bude fungovat v režimu "raw" a bude zobrazovat zprávy zaslané Nucleem. Blikání LED v programu pro Nucleo je možno realizovat třemi způsoby: * V hlavní smyčce funkce ''main'' v kombinaci s neblokující verzí ''serial.read'' za použití funkce ''ThisThread::sleep_for(...)''. * Pomocí objektu ''Ticker'' a metody ''attach'', která nastaví obsluhu přerušení podobně jako u tlačítka v minulém cvičení v kombinaci s blokující variantou sériového příjmu. * Program bude posílat s vhodnou frekvencí zprávy ''f''. Program pro Nucleo nebude nijak modifikovaný. (Tento způsob není ideální - pokud bychom měli pro Nucleo externí napájení, předchozí řešení by oproti tomuto zvládly blikat i po odpojení USB.) /* V aktuální verzi programu (viz výše) pro Nucleo je řízení LED realizováno v hlavním cyklu ve funkci ''main()'', která aktivně čeká na příjem znaku. Pro blikání LED nezávisle na komunikaci můžeme po dotazu na přítomnost znaku v bufferu sériového portu uvažovat čas po který LED svítí/nesvítí a realizovat tak blikání s požadovanou periodu. Výhodnější je v tomto případě použít časovač, který vyvolá přerušení (interupt) vždy po uběhnutí definované doby. */ Vypracováním a předvedením této úlohy cvičícímu na tomto nebo příštím cvičení lze získat až 6 bodů do hodnocení v rámci celého předmětu PRG. Kontrolovat se bude program na Nucleu a program na počítači nezávisle proti referenčnímu řešení. /* Ticker ticker; // callback function for LED blinking void tick() { myled = !myled; } // main function int main() { float periods[] = { 0.05, 0.1, 0.2, 0.5, 1.0 }; ... switch(c) { case 's': myled = 1; ticker.detach(); break; case 'e': ... default: if (c >= '1' && c <= '5') { ticker.attach(tick, periods[c-'1']); } else { ok = 0; } break; ... Dále rozšiřte ovládácí program pro posílání znaků '1' až '5' nastavující periodu blikání, např. if ( (c = getchar()) == 's' || c == 'e' || (c >= '1' && c <= '5') ) { if (serial_putc(fd, c) == -1) { fprintf(stderr, "ERROR: Error in received responses\n"); quit = true; } } */ ===== HAL, memory-mapped IO a křížová kompilace programu ===== Následující program demonstruje použití knihovny HAL (Hardware Abstraction Layer) pro jednobytovou komunikaci a ovládání blikání LED zasláním znaků '1' až '5'. Pointa spočívá v tom, že jednotlivé periferie (piny, USB, ADC, ...) jsou ve skutečnosti propojené s paměťovou sběrnicí a mají tedy v rámci adresního prostoru svou adresu. Knihovna HAL obsahuje přesné definice adres pro jednotlivé periferie, implementované do jazyka C jako ukazatele na struktury na konkrétních místech v adresním prostoru. Zápisem hodnoty do adresy se fyzicky nastaví napětí na daném obvodu. Největší část programu níže tvoří inicializace jednotlivých periferií a nastavení hardware časovače. Přímým přístupem k registrům periferií lze dosáhnout pokročilejšího chování. Samotná hlavní smyčka v metodě main se omezuje na čtení/zápis do několika řídících registrů a neliší se tak výrazně od programu sestaveného v prostředí mbed. #include "stm32f4xx_hal.h" #include "stm32f4xx_hal_dma.h" #include "stm32f4xx_hal_uart.h" #include "stm32f4xx_hal_usart.h" #include "system_stm32f4xx.h" // volatile variable to enable/disable LED blinking volatile int blink; /* method to initilaize the LED on PA5 GPIO pin*/ void InitializeLEDs(void) { RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOAEN); // enable clocks GPIOA->MODER |= (0x1 << 10); // set port A pin 5 output GPIOA->OSPEEDR |= (0x1 << 11); // set port A pin 5 output speed fast GPIOA->PUPDR |= (0x1 << 10); // set port A pin 5 pull-up } /* method to initialize the serial port interface */ void InitializeSerial(int baudrate){ /* Initialize USART pins */ RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOAEN); // enable clocks GPIOA->MODER = (GPIOA->MODER & 0xffffff0f) | 0xA0; //set PA2 and PA3 to alternate function '10' GPIOA->OTYPER = (GPIOA->OTYPER & 0xfffffff3); // set push-pull register GPIOA->PUPDR = (GPIOA->PUPDR & 0xffffff0f); // set no pull-up GPIOA->AFR[0] = (GPIOA->AFR[0] & 0xffff77ff) | 0x7700; // set alternate function to USART /* Initialize USART core */ RCC->APB1ENR |= (RCC_APB1ENR_USART2EN); // enable clocks USART2->CR1 &= ~USART_CR1_UE; // temporarily disable the core USART2->CR1 = (uint32_t)(USART_WORDLENGTH_8B | USART_PARITY_NONE | USART_MODE_TX_RX); // set parameters USART2->CR2 = (uint32_t)(USART_CLOCK_ENABLED | UART_STOPBITS_1); USART2->CR2 &= ~(USART_CR2_LINEN | USART_CR2_CLKEN); USART2->CR3 &= ~(USART_CR3_SCEN | USART_CR3_HDSEL | USART_CR3_IREN); USART2->BRR = __USART_BRR(HAL_RCC_GetPCLK1Freq(), baudrate); // set baudrate USART2->CR1 |= USART_CR1_UE; // enable the core } /* method to initilaize the timer2 */ void InitializeTimer() { RCC->APB1ENR |= (RCC_APB1ENR_TIM2EN); // enable clocks TIM2->CR1 |= 0x1; // Counter enable bit to start the timer TIM2->PSC = 40000; // Prescale register TIM2->ARR = 400; // auto-reload register - the reset value of the counter } /* method to enable interrupts for timer 2 */ void EnableTimerInterrupt() { TIM2->DIER |= (0x1); //enable interrupt NVIC_EnableIRQ(TIM2_IRQn); //enable interrupts from timer2 } /* method to disable interrupts for timer 2 */ void DisableTimerInterrupt() { TIM2->DIER &= ~(0x1); //enable interrupt NVIC_DisableIRQ(TIM2_IRQn); //enable interrupts from timer2 } /* Interrupt handler routine for timer 2 */ extern void TIM2_IRQHandler() { if (((TIM2->SR) & 0x01) == 1){ //read timer status register TIM2->SR &= ~(0x1); //clear timer status register if(blink){ GPIOA->ODR ^= (0x1 << 5); //blink the led } } } /* method to delay by busy loop */ void ms_delay(int ms) { while (ms-- > 0) { volatile int x = 5971; while (x-- > 0) __asm("nop"); // no operation } } /* main method */ int main() { /* update frequency of the STM board to 160 MHz */ SystemClock_Config(); SystemCoreClockUpdate(); /* Initialize PA5 LED GPIO pin */ InitializeLEDs(); /* Initialize serial port */ InitializeSerial(115200); /* Initialize timer for LED blinking and enable interrupts */ InitializeTimer(); EnableTimerInterrupt(); /* blink the LED 5 times */ GPIOA->ODR |= (1 << 5); //LED_on int i; for (i = 0; i < 5*2; ++i) { ms_delay(100); GPIOA->ODR ^= (1 << 5); //toggle LED } /* Send character i */ USART2->DR |= 'i'; int ok = 1; uint16_t periods[5] = { 200, 400, 800, 2000, 4000 }; blink = 0; /* loop to receive from UART and blink the LED accordingly */ while(1){ while((USART2->SR & (0x01 << 5)) == 0){} // active wait for received byte ok = 1; char c = USART2->DR; // reading received byte switch(c){ case 's': blink = 0; GPIOA->ODR |= (1 << 5); //LED - on break; case 'e': blink = 0; GPIOA->ODR &= ~(1 << 5); //LED - off break; default: if (c >= '1' && c <= '5') { blink = 1; TIM2->ARR = periods[c - '1']; //update period counter TIM2->EGR = 0x1; //reset timer } else { ok = 0; } break; } while((USART2->SR & (0x01 << 7)) == 0){} // active wait to allow sending USART2->DR =(ok ? 'a' : 'f'); // send response } } ** ** == Křížová kompilace == Křížová kompilace znamená kompilování programu pro procesor s jinou architekturou, než má procesor, který kompilaci provádí. Prakticky to znamená použít vhodný kompilátor, který umí sestavit strojový kód dané architektury. Při programování MCU je pak také nutné správným způsobem slinkovat dílčí kompilační jednotky. Při linkování je nutné správně nastavit rozsahy jednotlivých typů pamětí (''.data'', ''.text'', ...). /* Zdrojové soubory projektu určené ke cross-kompilaci jsou k dispozici jako {{:courses:b3b36prg:labs:lab11-cross_compile.zip|lab11_cross_compile}}. */