12 - Propojení STM32F446RE sériovou linkou

  • 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í.

(Pro vyučující.)

Jednobytová komunikace na straně Nucleo

  • Pracujeme v 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 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 <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
 
#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í

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.)
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í.

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, …).

courses/b3b36prg/labs/lab12.txt · Last modified: 2023/02/16 21:00 by zoulamar