STM32 Ethernet – UDP Client

W ostatnim czasie miałem okazję pracować nad pewnym projektem wykorzystującym mikrokontroler STM32 oraz interfejs Ethernet. Zapewne podobnie jak inne osoby podejmujące się tego tematu natknąłem się na pewien problem – brak „prostych” poradników. We większości dostępnych źródeł łączenie interfejsu Ethernet i układów STM32 opisane jest dość zagmatwanie i według mnie brakuje instrukcji opisujących jak skonfigurować tę funkcjonalność w najprostszy możliwy sposób, bez zbędnego zgłębiania aspektów sieciowych. Dlatego postanowiłem przygotować ten oraz powiązane z nim materiały poruszające temat Ethernetu na STM32. W tym artykule pokaże wam podstawową funkcjonalność klienta UDP. Czyli kodu, którego działanie jest niejako odwrotnością serwera UDP. W poprzednim poradniku to STM32 pełnił rolę serwera, z którym łączył się komputer, tym razem to na komputerze utworzymy serwer, z którym łączyć będzie się mikrokontroler.

W projektach korzystam z płytki Nucleo F767ZI, kody dostępne są do pobrania na moim profilu GitHub.

Ostrzeżenie o nowej wersji firmware

Przygotowując kody do tego poradnika, korzystałem z CubeMX V6.12.1. Jeśli ktoś będzie chciał skorzystać z gotowego projektu, który udostępniam na moim profilu GitHub, prawdopodobnie spotka się z ostrzeżeniem o nowej wersji firmwaru. W takim przypadku zalecam kontynuowanie i korzystanie z wersji 6.12.1.

Konfiguracja projektu

Przygotowując projekt klienta UDP należy skorzystać z konfiguracji opisanej w pierwszym artykule. Jest ona identyczna i polega na wybraniu opcji RMII dla interfejsu Ethernet oraz odpowiedniego drivera i adresu IP w sekcji LWIP.

Przygotowanie biblioteki UDP Client

udp_client.h

#ifndef INC_UDP_CLIENT_H_
#define INC_UDP_CLIENT_H_

void udpClient_connect(void);

#endif /* INC_UDPCLIENTRAW_H_ */

Konstrukcja pliku nagłówkowego udp_client.h jest niezwykle prosta i zawiera tylko prototyp funkcji, której zadnaiem jest połączenie się z serwerem.

udp_client.c

#include "lwip/pbuf.h"
#include "lwip/udp.h"
#include "lwip/tcp.h"
#include "stdio.h"
#include "string.h"
#include "udp_client.h"

Plik C zaczynamy od deklaracji bibliotek lwip, wspierających działanie interfejsu Ethernet. Dodatkowo znalazły się tutaj standardowe paczki stdio.h, string.h, jak i nagłówkowy plik udp_client.h.

void udp_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port);
static void udpClient_send(const char *message);

struct udp_pcb *upcb;
char buffer[100];

W kolejnym kroku deklarujemy prototypy funkcji wywoływanej po odebraniu danych z serwera – udp_receive_callback oraz wysyłania danych przez STM32 – udpClient_send.

Następnie tworzona jest struktura udp_pcb dla klienta UDP oraz zmienna buforowa, w której przechowywane będą odebrane dane.

void udpClient_connect(void)
{
    err_t err;

    /* 1. Create a new UDP control block  */
    upcb = udp_new();

    /* Bind the block to module's IP and port */
    ip_addr_t myIPaddr;
    IP_ADDR4(&myIPaddr, 192, 168, 8, 200);
    udp_bind(upcb, &myIPaddr, 1100);

    /* Configure destination IP address and port */
    ip_addr_t DestIPaddr;
    IP_ADDR4(&DestIPaddr, 192, 168, 8, 108);
    err = udp_connect(upcb, &DestIPaddr, 12);

    if (err == ERR_OK)
    {
        /* 2. Send "Hello World" message to server */
        udpClient_send("Hello World");

        /* 3. Set a receive callback for the upcb */
        udp_recv(upcb, udp_receive_callback, NULL);
    }
}

Funkcja udpClient_connect łączy się z serwerem. W jej pierwszej części tworzymy nowy blok UDP, na którym będziemy pracować, a następnie konfigurujemy adres IP i port naszego mikrokontrolera. W moim przypadku jest to 192.168.8.200 i 1100. Następnie musimy określić adres i port serwera, z którym chcemy się połączyć. Będzie to IP 192.168.8.108, port 12, czyli komputer w mojej sieci.

Jeśli połączenie zostanie nawiązane poprawnie wykonana zostanie funkcja warunkowa, wysyłająca do serwera tekst „Hello World” wraz z zarejestrowanie wiadomości zwrotnej.

static void udpClient_send(const char *message)
{
    struct pbuf *txBuf;
    int len = strlen(message);

    /* Allocate pbuf from pool */
    txBuf = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);

    if (txBuf != NULL)
    {
        /* Copy data to pbuf */
        pbuf_take(txBuf, message, len);

        /* Send UDP data */
        udp_send(upcb, txBuf);

        /* Free pbuf */
        pbuf_free(txBuf);
    }
}

Struktura funkcji wysyłającej dane jest dość prosta. Na początku alokujemy bufor pbuf, które zawierać będą dane do wysłania. Następnie, jeśli poprzednia operacja wykonała się poprawnie, kopiujemy dane, wysyłamy je i zwalniamy bufor.

void udp_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
    /* Copy the data from the pbuf */
    strncpy(buffer, (char *)p->payload, p->len);
    buffer[p->len] = '\0';  // Ensure null termination

    /* Free receive pbuf */
    pbuf_free(p);
}

Ostatnią funkcją jest callback wywoływany automatycznie po odebraniu danych z serwera. Jego zadaniem jest przypisanie ich do bufora, jednocześnie dodając na końcu terminator NULL i jego zwolnienie po wykonaniu operacji.

Główny kod programu

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
extern struct netif gnetif;
/* USER CODE END 0 */ 

W pliku main.c, który zawiera główny, wykonywany przez STM32 program umieszczamy deklarację struktury netif o nazwie gnetif. Jest to podstawowa struktura zdefiniowana we wbudowanej bibliotece LwIP (Lightweight IP) obsługującej stos sieciowy.

/* USER CODE BEGIN 2 */
  udpClient_connect();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  ethernetif_input(&gnetif);
	  sys_check_timeouts();
  }
  /* USER CODE END 3 */
}

W głównej pętli while umieszczamy instrukcje ethernetif_input(&gnetif); oraz sys_check_timeouts();. Ich zadaniem jest kolejno odebrać wszystkie dane z portu Ethernet i przypisać je do stosu sieciowego, innymi słowy przekazać je do struktury gnetif, do której wskaźnik umieszczony jest w argumencie. Drugie polecenie sprawdza i obsługuje wszystkie timeouty związane ze stosem LwIP. Dodatkowo powyżej pętli while znalazła się funkcja inicjalizująca klienta UDP.

Tak przygotowany program należy zapisać w pamięci mikrokontrolera, pamiętając, aby wcześniej podłączyć do płytki przewód Ethernet. Choć robiąc to później, nie stanie się nic złego.

Uruchomienie

Do sprawdzenia działania kodu wykorzystałem program Hercules, dostępny za darmo w internecie. Z jego poziomu, a dokładnie zakładki UDP możemy uruchomić serwer na naszym komputerze. Zakładki Module IP oraz Port są nieznaczące, najważniejszy jest Local port, czyli port na jakim utworzony zostanie serwer. Jego adres IP, będzie zgodny z IP komputera, wartość tą najłatwiej jest sprawdzić poleceniem arp -a w konsoli CMD. Po uruchomieniu serwera oraz podłączeniu zasilania do STM32 po chwili w odebranych danych powinno pojawić się Hello World. Oznacza to, ze mikrokontroler poprawnie połączył się z komputerem i wysłał przygotowane dane.

Jak widać implementacja klienta UDP w STM32 jest dość prosta. W kolejnych poradnikach przyjrzymy się bliżej nieco bardziej wyrafinowanej komunikacji, czyli TCP.

Chcesz być na bieżąco?
Dołącz do newslettera

Otrzymywać będziesz powiadomienia o nowych artykułach oraz informacje o projektach, nad którymi pracuję.

Przewiń do góry