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ść serwera UDP.
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 serwera 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 Server
udp_server.h
#ifndef INC_UDP_SERVER_H_
#define INC_UDP_SERVER_H_
void udpServer_init(void);
#endif
Konstrukcja pliku nagłówkowego udp_server.h jest niezwykle prosta i zawiera tylko prototyp funkcji inicjalizującej.
udp_server.c
#include "lwip/pbuf.h" // Provides structures and functions for packet buffers.
#include "lwip/udp.h" // Includes UDP-specific functionality.
#include "lwip/tcp.h" // Used for TCP/IP stack functionality (though not required for UDP operations).
#include "stdio.h" // Standard input/output library.
#include "udp_server.h" // Custom header file for this server (likely contains function prototypes).
// Callback function declaration.
void udp_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port);
Na początku pliku udp_server.c dołączamy wbudowane w bibliotekę lwip pliki nagłówkowe związane z funkcjonownaiem interfejsu Ethernet. Dodatkowo zadeklarowana została standardowa biblioteka stdio.h oraz plik nagłówkowy utworzony wcześniej.
W tej sekcji umieszcozny jest też prototyp funkcji udp_revieve_callback, która uruchamiana będzie, gdy nawiązane zostanie połączenie z modułem STM32.
void udpServer_init(void)
{
// UDP Control Block structure
struct udp_pcb *upcb;
err_t err;
/* 1. Create a new UDP control block */
upcb = udp_new();
/* 2. Bind the upcb to the local port */
ip_addr_t myIPADDR; // Define the IP address for the server.
IP_ADDR4(&myIPADDR, 192, 168, 8, 200); // Set IP to 192.168.8.200
// Bind the UDP control block to the IP and port 1100.
err = udp_bind(upcb, &myIPADDR, 1100);
/* 3. Set a receive callback for the upcb */
if (err == ERR_OK)
{
// Register the callback function for receiving data.
udp_recv(upcb, udp_receive_callback, NULL);
}
else
{
// If binding fails, remove the UDP control block.
udp_remove(upcb);
}
}
Funkcja inicjalizująca tworzy strukturę udp_pcb dla serwera UDP. W kodzie umieścić należy adres IP, który przypisany został do modułu STM32 oraz port, którym wymieniane będą dane. Tutaj też znajduje się odwołanie do udp_recieve_callback, czyli funkcji, której zadaniem będzie obsługa danych przychodzących.
void udp_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
struct pbuf *txBuf; // Packet buffer for transmitting data.
/* Get the IP of the Client */
char *remoteIP = ipaddr_ntoa(addr); // Convert client IP address to a readable string.
char response[100];
// Determine the response based on the received payload
if (strncmp((char *)p->payload, "UDP00", 5) == 0)
{
sprintf(response, "Hello World\n");
}
else if (strncmp((char *)p->payload, "UDP01", 5) == 0)
{
sprintf(response, "RafalBartoszak\n");
}
else
{
sprintf(response, "ERR\n");
}
int len = strlen(response);
/* Allocate a pbuf for the outgoing message from RAM */
txBuf = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
/* Copy the response message into the buffer */
pbuf_take(txBuf, response, len);
/* Connect to the remote client */
udp_connect(upcb, addr, port);
/* Send a reply to the client */
udp_send(upcb, txBuf);
/* Disconnect the UDP connection to allow new clients */
udp_disconnect(upcb);
/* Free the transmit buffer */
pbuf_free(txBuf);
/* Free the receive buffer */
pbuf_free(p);
}
udp_recieve_callback jest funkcją zwrotną (callbackiem), wywoływaną automatycznie przez stos lwIP w momencie, gdy serwer UDP odbierze pakiet danych od klienta. Oczekuje on przekazania kilku argumentów: arg – dodatkowy argument przekazywany przez udp_recv() – tutaj nieużywany, upcb – wskaźnik do kontrolki UDP – reprezentuje połączenie serwera, p – bufor zawierający dane odebrane od klienta, addr – adres IP klienta, który wysłał dane oraz port – numer portu, z którego klient wysłał dane.
W pierwszym kroku tworzone są odpowiednie zmienne i struktury wykorzystywane w ciele funkcji. Następnie dzięki funkcjom warunkowym możemy zareagować na otrzymane dane i przygotować zmienną response przechowującą tekst do odesłania. W kodzie znajdziecie prostą obsługę dwóch komend UDP00 – odpowiedź Hello World, UDP01 – odpowiedź RafalBartoszak. W przypadku każdych innych danych serwer odeśle kod ERR.
Ostatnim krokiem jest odpowiednia alokacja zmiennej response i uruchamianie tymczasowego połączenia, w którego czasie dane zostaną wysłane do klienta. Na końcu umieszczone zostało dodatkowe zwolnienie pamięci.
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 */
udpServer_init();
/* 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 serwer 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 połączyć się z dowolnym tego typu serwerem. Wpisując odpowiednie IP i Port, takie jak ustawiliśmy w kodzie dla STM32 możemy nawiązać połączenie z mikrokontrolerem. W dolnej części okna widoczne są trzy miejsca na dane do wysłania. Jak widać wysłałem kolejno UDP00, UDP01 oraz test. Odpowiedzi mikrokontrolera widoczne są wyżej i są zgodne z opisanym kodem.
Jak widać implementacja serwera UDP w STM32 jest dość prosta. W następnym poradniku odwrócimy role i to z poziomu mikrokontrolera będziemy łączyć się z innym serwerem UDP.