on 2026-01-29 12:39 AM
This article guides developers through integrating a LwIP application into a TouchGFX project running on an RTOS. It covers project setup, configuration, memory protection unit (MPU) and descriptor management, and best practices to avoid common pitfalls that affect performance and stability.
In embedded systems, combining a rich graphical user interface (GUI) with network connectivity is increasingly common. TouchGFX provides a powerful framework for creating high-performance GUIs on STM32 microcontrollers, while LwIP (Lightweight IP) offers a compact TCP/IP stack suitable for embedded devices. Integrating these two under a real-time operating system (RTOS) enables responsive user interfaces alongside robust network services such as web servers.
This article uses the STM32H750B-DK development kit, which is based on the STM32H750XB microcontroller, part of the STM32H7 series. The kit is designed for high-performance applications requiring advanced graphics and connectivity.
Since this article focuses on integration, a preexisting TouchGFX project available for the STM32H750B-DK board is selected to save time. Specifically, the Gauge application is used, which features animations requiring constant display refreshing and consumes more resources.
(Optional): To generate the application, download the latest version of TouchGFXDesigner, open it, select the Gauge example for the STM32H750B-DK, choose a name for your application, and click "Create." After creation, click "Generate" and wait for the application to be generated in the specified directory.
Developers can follow these steps to test the integration or use their own TouchGFX projects.
For the LwIP application, a UDP and TCP echo server is created that runs on a separate thread under the same RTOS. This example demonstrates a generic configuration that can be adapted to other LwIP applications.
The memory protection unit (MPU) enforces memory access permissions, preventing accidental corruption and improving system stability. For RTOS-based systems running TouchGFX and LwIP, MPU configuration ensures safe concurrent access to shared resources.
A key requirement for a functional LwIP application is well-configured memory. This includes a dedicated section to store transmit and receive DMA descriptors, sufficient space for Rx buffers, and a dedicated heap memory. LwIP memory sections must not overlap with other memory areas to avoid unexpected behavior.
Since a preexisting TouchGFX application is being used, opening the Cortex®-M7 configuration for the MPU reveals that TouchGFXDesigner has already loaded the necessary configurations for the graphics application, including proper permissions and memory sections.
The LwIP heap is allocated in SRAM2, leveraging its 128 KB size to provide sufficient memory for dynamic allocation of new pbufs, TCP/UDP control blocks, and other protocol data structures. The DMA descriptors are placed at the beginning of SRAM3, occupying 256 bytes each, with attributes set to noncacheable to prevent data corruption, shareable between the CPU and DMA, and bufferable.
This configuration is specific to the STM32H750B-DK and must be adjusted when using another microcontroller, as some may have smaller SRAM sizes. When working with graphics applications or designing a custom PCB, consider these factors to select the appropriate product. This step can be skipped when using a Cortex®-M4 core.
In the ETH tab under the Connectivity category, enable the Ethernet peripheral and set it to the mode supported by your board. For this example, set it to MII. Adjust the parameters as follows:
Since this application uses RTOS, we need to enable the Ethernet global interrupt to ensure that complete reception and transmission events are detected, allowing the appropriate callback functions to be executed.
Verify that the assigned pins are correctly mapped and set the maximum output speed to Very High.
For LwIP to function properly, a thread is required for initialization. TouchGFX already has a preconfigured thread, so the focus is on the default thread. Increase its stack size to 512 words, which is sufficient to handle LwIP’s requirements, as a smaller stack size can lead to memory corruption. Keep the priority at osPriorityNormal. Monitor FreeRTOS™ heap usage to understand how much memory your application requires and how much heap is still available, allowing you to optimize memory allocation for other needs.
Enable LwIP under the Middleware tab. In the general settings, choose whether to use a static IP address or DHCP, depending on whether a working DHCP server is available on your network.
In Key Options, set the base address and size of the heap memory section defined in the MPU configuration.
For better TCP/IP performance, adjust the following parameters based on Adam Berlinger's knowledge base article:
Additionally, double the size of DEFAULT_THREAD_STACK_SIZE to prevent application failure and hard faults.
Before proceeding, under Platform Settings, select the PHY driver used (in this case, the LAN8742) so the appropriate drivers are imported into the project folder.
Leave the clock tree as generated by TouchGFX, running at a maximum of 480 MHz, with the timebase set to TIM6 for enhanced RTOS scheduling performance, and keep the rest of the peripherals unchanged. This setup is based on the pre-configured TouchGFX project. Developers should customize the project according to their specific application requirements.
When configuring Ethernet descriptors, specific memory addresses are set for each descriptor. Configuring this in CubeMX alone is insufficient if the GNU compiler embedded in STM32CubeIDE is used to build the project. A memory section dedicated to LwIP must be created to tell the compiler where to allocate each variable in memory.
This step can be skipped if the IAR Compiler or MDK ARM Compiler is used, as they support allocating variables at specific addresses directly from the source code.
Add the following code to the linker file:
.lwip_sec (NOLOAD) :
{
. = ABSOLUTE(0x30040000);
*(.RxDecripSection)
. = ABSOLUTE(0x30040100);
*(.TxDecripSection)
. = ABSOLUTE(0x30040200);
*(.Rx_PoolSection)
} >RAM_D2
This linker script snippet places important network data structures specifically Ethernet DMA descriptors and buffers at fixed memory addresses in a special RAM area (D2 SRAM) on STM32 microcontrollers. These structures are not initialized at the program start. Instead, they are set up by the software at runtime. This approach is necessary because the Ethernet hardware requires these data structures to be located at specific memory addresses for reliable and efficient operation.
The name of each section is defined by LwIP developers and associated with the variables: DMARxDscrTab, DMATxDscrTab, and memp_memory_RX_POOL_base.
The core objective of the LwIP application is to create initialization and thread functions for both UDP and TCP protocols. Init functions must be declared in the lwip.h header file to ensure proper linkage and visibility throughout the project.
/* USER CODE BEGIN 0 */
void tcpecho_init(void);
void udpecho_init(void);
/* USER CODE END 0 */
To efficiently handle UDP packet reception, create a dedicated FreeRTOS™ thread named udpecho_thread. This thread initializes a new UDP connection bound to port 7 (modifiable), allowing it to listen for incoming packets on any IP address. Within an infinite loop, the thread waits for packets using netconn_recv. Upon receiving data, it copies the payload into a local buffer and prints the message for debugging. It then prepares a new buffer to echo the received data back to the sender using netconn_sendto.
Proper error handling ensures reliable transmission, and all allocated buffers are deleted after use to prevent memory leaks. The thread is created with sufficient stack size and priority using sys_thread_new in the udpecho_init function, ensuring smooth multitasking alongside other RTOS threads.
/* USER CODE BEGIN 2 */
...
static void udpecho_thread(void *arg)
{
struct netconn *conn;
struct netbuf *buf, *tx_buf;
err_t err;
LWIP_UNUSED_ARG(arg);
char data[100] = {'\0'};
conn = netconn_new(NETCONN_UDP);
netconn_bind(conn, IP_ADDR_ANY, 7);
LWIP_ERROR("udpecho: invalid conn", (conn != NULL), return;);
while (1) {
err = netconn_recv(conn, &buf);
if (err == ERR_OK) {
/* Print received data */
strncpy(data, buf->p->payload, buf->p->len);
printf("%s \n", data);
tx_buf = netbuf_new();
netbuf_alloc(tx_buf, buf->p->tot_len);
pbuf_take(tx_buf->p, (const void *)buf->p->payload, buf->p->tot_len);
err = netconn_sendto(conn, tx_buf, (const ip_addr_t *)&(buf->addr), buf->port);
if(err != ERR_OK) {
LWIP_DEBUGF(LWIP_DBG_ON, ("netconn_send failed: %d\n", (int)err));
} else {
LWIP_DEBUGF(LWIP_DBG_ON, ("got %s\n", buffer));
}
netbuf_delete(tx_buf);
}
netbuf_delete(buf);
}
}
void udpecho_init(void)
{
sys_thread_new("udpecho_thread", udpecho_thread, NULL, (configMINIMAL_STACK_SIZE*5), osPriorityAboveNormal);
}
...
/* USER CODE END 2 */
For TCP communication, create a dedicated FreeRTOS™ thread named tcpecho_thread. It sets up a TCP connection listening on port 7 (modifiable), ready to accept incoming client connections. When a client connects, the thread continuously receives data packets, prints the received content for debugging, and echoes the data back to the client. It handles multiple packets per connection and ensures proper cleanup by closing and deleting the connection once communication ends.
The thread is launched with appropriate stack size and priority using sys_thread_new in the tcpecho_init function, maintaining efficient operation alongside other RTOS tasks.
/* USER CODE BEGIN 2 */
static void tcpecho_thread(void *arg)
{
struct netconn *conn, *newconn;
err_t err;
LWIP_UNUSED_ARG(arg);
char buffer[100] = {'\0'};
/* Create a new connection identifier. */
/* Bind connection to well known port number 7. */
conn = netconn_new(NETCONN_TCP);
netconn_bind(conn, IP_ADDR_ANY, 7);
LWIP_ERROR("tcpecho: invalid conn", (conn != NULL), return;);
/* Tell connection to go into listening mode. */
netconn_listen(conn);
while (1) {
/* Grab new connection. */
err = netconn_accept(conn, &newconn);
/* Process the new connection. */
if (err == ERR_OK) {
struct netbuf *buf;
void *data;
u16_t len;
while ((err = netconn_recv(newconn, &buf)) == ERR_OK) {
do {
/* Print received data */
strncpy(buffer, buf->p->payload, buf->p->len);
printf("%s \n", buffer);
netbuf_data(buf, &data, &len);
err = netconn_write(newconn, data, len, NETCONN_COPY);
} while (netbuf_next(buf) >= 0);
netbuf_delete(buf);
}
/* Close connection and discard connection identifier. */
netconn_close(newconn);
netconn_delete(newconn);
}
}
}
void tcpecho_init(void)
{
sys_thread_new("tcpecho_thread", tcpecho_thread, NULL, (configMINIMAL_STACK_SIZE*5), osPriorityAboveNormal);
}
/* USER CODE END 2 */
Thread functions use the netconn API functions. api.h must be included inside the lwip.c source file, as it provides the necessary declarations for the netconn interface.
/* USER CODE BEGIN 0 */
#include "api.h"
/* USER CODE END 0 */
The final step is to call the initialization functions within the LwIP task in the main source file. The function StartLwipTask handles this process. It begins by initializing the LwIP stack with MX_LWIP_Init(). Next, it starts the TCP and UDP echo server threads by calling tcpecho_init() and udpecho_init(), respectively.
After these initializations, the function enters an infinite loop where it immediately terminates the LwIP initialization thread using osThreadTerminate(lwipTaskHandle) to free resources, followed by a brief delay. This setup ensures that the echo servers run concurr
void StartLwipTask(void *argument)
{
/* init code for LWIP */
MX_LWIP_Init();
/* USER CODE BEGIN 5 */
/* Initialize the TCP echo server thread */
tcpecho_init();
/* Initialize the UDP echo server thread */
udpecho_init();
/* Infinite loop */
for(;;)
{
/* Delete the Init Thread */
osThreadTerminate(lwipTaskHandle);
osDelay(1);
}
/* USER CODE END 5 */
}
ently under the RTOS while the initialization thread exits cleanly.
void StartLwipTask(void *argument)
{
/* init code for LWIP */
MX_LWIP_Init();
/* USER CODE BEGIN 5 */
/* Initialize the TCP echo server thread */
tcpecho_init();
/* Initialize the UDP echo server thread */
udpecho_init();
/* Infinite loop */
for(;;)
{
/* Delete the Init Thread */
osThreadTerminate(lwipTaskHandle);
osDelay(1);
}
/* USER CODE END 5 */
}
Ethernet DMA descriptors are critical data structures that inform the Ethernet peripheral where to find the data buffers for incoming and outgoing packets. Proper placement of these descriptors is essential to ensure reliable data transfer and to avoid data corruption or system crashes.
Calculate the size needed for your framebuffer using the formula:
Width × Height × Color Depth (bits) / 8 bytes.
|
Resolution (pixels) |
Colors (bpp) |
Calculation |
Memory consumed (bytes) |
|
800x480 |
16 |
800 * 480 * 16 / 8 |
768,000 |
|
480x272 |
24 |
480 * 272 * 24 / 8 |
391,680 |
|
100x100 |
8 |
100 * 100 * 8 / 8 |
10,000 |
When using a double buffering scheme, two framebuffers consume twice the amount of memory.
You can let the linker allocate this space automatically by selecting the "By Allocation" option for the Buffer Location parameter under the TouchGFX tab. For more deterministic memory usage, select "By Address" and manually assign memory addresses for each framebuffer. In the latter case, addresses must be carefully chosen to prevent overlapping.
Integrating an LwIP web server with a TouchGFX application on the STM32H750B-DK under an RTOS requires careful planning and configuration: