{{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}}.
*/