HomeMongoose NetworkingFixed Memory Pool

STM32 Fixed Memory Pool for Networking using Mongoose

In this tutorial, we will limit the network memory usage of the Mongoose networking library to a fixed, preallocated memory pool. This ensures that networking never eats into the RAM reserved for other parts of your application.

We will use O1heap — a fast, deterministic memory allocator — to manage a fixed 40 KB RAM block. Mongoose will use this block exclusively for all network operations. We will also use the real-time graphs from the previous tutorial to monitor live memory usage and the number of active connections on a web dashboard.

This is Part 6 of the STM32 TCP/IP with Mongoose series. The earlier parts are listed below:

How Fixed Memory Pool Works with Mongoose

When Mongoose runs on an embedded system, it allocates and frees memory dynamically as connections are opened and closed. By default, it pulls from the general heap — which is shared with the rest of your application. If a heavy network load or a memory leak happens, it can starve other parts of your firmware.

The fix is to give Mongoose its own dedicated block of memory. Mongoose supports this through a custom memory allocator hook. Once you plug in your own mg_calloc and mg_free, Mongoose will only allocate from wherever you tell it to — in our case, a fixed array in RAM.

Why Limit Network Memory on Embedded Systems

Embedded systems have limited RAM, and that RAM is shared between your application logic, stack, heap, and peripherals. If networking is allowed to grow without limits, a burst of incoming connections or large payloads can crash the system or cause undefined behavior.

By capping the network memory to a fixed pool, you get:

  • Predictable memory behavior — the network can never take more than you allow
  • System stability — even under heavy load, your non-network code always has its RAM
  • Easier debugging — out-of-memory conditions in the network are isolated and visible

In this tutorial, we allocate 40 KB for networking. When that pool is full, new connections are rejected gracefully. Existing connections continue to work.

What Is O1heap and Why We Use It

O1heap is a memory allocator designed specifically for hard real-time and safety-critical embedded systems. Its main properties are:

  • O(1) allocation and free — the time it takes is constant, regardless of heap state
  • Minimal overhead — small metadata, no fragmentation over time with correct usage
  • No external dependencies — it is a pair of single .c and .h files

We need just two files from the O1heap GitHub repository: o1heap.c and o1heap.h. There are no FreeRTOS dependencies, no extra headers, and no configuration file. This makes it much easier to integrate than alternatives like the FreeRTOS heap implementations.

How Mongoose Uses Custom Memory Allocators

Mongoose allocates memory through two internal functions: mg_calloc() and mg_free(). These are defined inside mongoose.c by default. Mongoose lets us replace these with our own versions by setting a build option in the configuration file.

Once we define MG_ENABLE_CUSTOM_CALLOC 1 in mongoose_config.h, the default implementations are removed. The compiler will then expect us to provide our own mg_calloc and mg_free. Inside those functions, we route all allocations to O1heap.

The flow looks like this:

Mongoose allocates memory
     → calls mg_calloc()
         → our custom mg_calloc() calls o1heapAllocate()
             → memory comes from our fixed 40 KB buffer

Mongoose Dashboard – Designing the UI

We start with the LED Toggle dashboard generated in a previous tutorial. This already has the LED control panel and basic structure in place. We will add two memory graphs and a connection gauge on top of it.

Create a new project in Mongoose Wizard — select the STM32H745 Discovery board, CubeIDE as the build environment, and Bare Metal for RTOS. Start from the LED Toggle dashboard template.

Creating the Memory Graphs and Connection Gauge

Add a new container to the dashboard. Inside this container, add two graph elements — one for memory used and one for free memory. Both graphs show the last 30 seconds of data and update every 25 milliseconds, which gives a near real-time view of the memory state.

Mongoose wizard dashboard showing two real-time memory graphs inside a container for STM32H745

Since we are allocating 40 KB for networking, set the Y-axis scale to 40000 for both graphs. This keeps both graphs on the same scale, making it easy to see used + free = 40,000 at a glance.

{"xrange":30,"updateMs":25,"uplot":{"scales":{"y":{"auto":false,"min":0,"max":40000}}}}
  • “xrange”: 30
    We set the X-axis range to 30 sec. The graph will display data for the last 30 seconds, basically real time updates only.
  • “updateMs”: 25
    The graph updates every 25 ms. It fetches new data from the API at this interval.
  • “auto”: false
    We disable automatic scaling of the Y-axis.
  • “min”: 0, “max”: 40000
    We manually set the Y-axis range from 0 to 40000. This is useful to display the memory upto 40KB.

This setup ensures the graph shows a fixed range with controlled updates, which makes the visualization stable and easy to read.

Along with the graphs, add a gauge element to the LED control panel area. This gauge will show the number of active connections in real time. Set the gauge range from 0 to 24, with ticks at every 4 connections.

Mongoose wizard dashboard showing connection gauge added to LED control panel, range 0 to 24

Creating the Stats REST API Endpoint

The LED endpoint is already present. We need to add a new endpoint called stats that carries three values:

  • ram_occupied — memory currently used from the pool (integer)
  • ram_free — memory remaining in the pool (integer)
  • num_connections — number of active Mongoose connections (integer)
Mongoose wizard REST API endpoint configuration for stats with ram_occupied, ram_free and num_connections attributes

Set this endpoint to read-only, since the web UI only reads from the MCU. It does not write anything back.

The Mongoose wizard will auto-generate a struct stats and a getter function signature in mongoose_glue.c:

struct stats {
  int ram_occupied;
  int ram_free;
  int num_connections;
};

Configuring WebSocket Updates

Go to the Settings tab and enable WebSocket. Also set the auto-update interval to 1 second. This activates the heartbeat mechanism — the page will refresh its data every second automatically.

Mongoose wizard Settings tab showing WebSocket enabled and auto-update interval set to 1 second

Now go to the Page Content tab and assign the API variables to their elements:

Mongoose wizard Page Content tab showing stats API variables assigned to memory graphs and connection gauge
  • stats.ram_occupied → memory usage graph
  • stats.ram_free → free memory graph
  • stats.num_connections → connection gauge

Generate the project and import it into STM32 CubeIDE.

O1heap Integration, Setup, Code & Results

With the project imported, we now need to add the O1heap allocator, hook it into Mongoose, write the getter functions, and configure the GPIO pins for the LEDs. Let us go through each step.

Adding O1heap to the Project

Go to the O1heap GitHub repository. Open o1heap.c and copy the raw content. Create a new source file inside the Src folder of your CubeIDE project and paste the content. Do the same for o1heap.h — create a header file inside Inc and paste the header content.

The image below shows the project structure with o1heap library added.

STM32 CubeIDE project structure showing o1heap.c and o1heap.h added to Src and Inc folders

Defining Custom mg_calloc and mg_free Functions

Open mongoose_config.h and add the following line:

#define MG_ENABLE_CUSTOM_CALLOC 1

If you build the project now, the compiler will report an error about mg_calloc being undefined. That is expected — we just told Mongoose we are providing our own version, and now we need to write it.

Open main.c. Include the O1heap header at the top:

#include "o1heap.h"

Now define the memory pool. We use a 40 KB buffer, aligned to O1HEAP_ALIGNMENT:

#define O1HEAP_SIZE		40*1024

static uint8_t o1heap_buffer[O1HEAP_SIZE] __attribute__((aligned(O1HEAP_ALIGNMENT)));
static O1HeapInstance *heap_instance = NULL;

Add an initialization function that sets up the heap before Mongoose starts:

void o1heap_Init (void)
{
	o1heap = o1heapInit(o1heap_buffer, sizeof(o1heap_buffer));
	if (o1heap == NULL)
	{
		// failed to initialize, check alignement
	}
}

Call o1heap_Init() inside main() before any peripheral or Mongoose initialization:

o1heap_Init();

Now write the custom mg_calloc and mg_free functions. You can find the default function signatures inside mongoose.c — copy those signatures and implement them in main.c:

void *mg_calloc(size_t count, size_t size) {
    size_t total = count * size;
    void *ptr = o1heapAllocate(o1heap, total);

    if (ptr) {
        memset(ptr, 0, total);
    }
    return ptr;
}

void mg_free(void *ptr) {
	o1heapFree(o1heap, ptr);
}

mg_calloc allocates the memory and zeroes it out, just like calloc is supposed to. mg_free simply releases it back to the pool. Mongoose calls both of these internally — we never call them directly.


Writing the Stats Getter and LED Functions

Open mongoose_glue.c and find the auto-generated getter for stats. We should not edit this file directly — copy the function name and write our own version in main.c.

The O1heap library provides a o1heapGetDiagnostics() function that returns a structure with details about the heap state. We use this to fill in the stats:

void my_get_stats(struct stats *data)
{
	O1HeapDiagnostics diag = o1heapGetDiagnostics(o1heap);
	data->ram_occupied = diag.allocated;
	data->ram_free = diag.capacity - diag.allocated;

    data->num_connections = 0;
    for (struct mg_connection *c = g_mgr.conns; c != NULL; c = c->next)
    {
        data->num_connections++;
    }
}

The mgr here is the Mongoose event manager instance. We count connections by walking the linked list of active connections.

For the LED controls, we first need to configure the GPIO pins in CubeMX. Open STM32CubeMX, search for pins PI8 and PE6, and configure both as GPIO output. Since this is a dual-core board, open the GPIO settings and assign both pins to the Cortex-M7 context. Regenerate the project.

STM32CubeMX GPIO configuration showing PI8 and PE6 set as output pins assigned to Cortex-M7 context

Now write the LED getter function:

void my_get_leds(struct leds *data){
	data->led1 = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_6);
	data->led2 = HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_8);
}

And the LED setter function:

void my_set_leds(struct leds *data) {
	HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, data->led1);
	HAL_GPIO_WritePin(GPIOI, GPIO_PIN_8, data->led2);
}

Registering HTTP Handlers and WebSocket Reporter

After mongoose_init(), register the HTTP handlers and start the WebSocket reporters:

  mongoose_set_http_handlers("stats", my_get_stats, NULL);
  mongoose_set_http_handlers("leds", my_get_leds, my_set_leds);

  mongoose_add_ws_reporter(100, "stats");

The stats reporter pushes data every 100 milliseconds, which keeps the graphs smooth and near real-time. The LED endpoint uses both a getter and a setter since the UI can both read and write LED state.

Make sure the endpoint names — "stats" and "leds" — exactly match what you defined in the Mongoose wizard.


Full Code

Here is the complete set of additions made to main.c:

#include "o1heap.h"
#include <string.h>

/* ---- Memory Pool ---- */
#define O1HEAP_SIZE		40*1024
static uint8_t o1heap_buffer[O1HEAP_SIZE] __attribute__((aligned(O1HEAP_ALIGNMENT)));
static O1HeapInstance *heap_instance = NULL;

void o1heap_Init (void)
{
	o1heap = o1heapInit(o1heap_buffer, sizeof(o1heap_buffer));
	if (o1heap == NULL)
	{
		// failed to initialize, check alignement
	}
}


/* ---- Custom Mongoose Allocator ---- */
void *mg_calloc(size_t count, size_t size) {
    size_t total = count * size;
    void *ptr = o1heapAllocate(o1heap, total);

    if (ptr) {
        memset(ptr, 0, total);
    }
    return ptr;
}

void mg_free(void *ptr) {
	o1heapFree(o1heap, ptr);
}

/* ---- Stats Getter ---- */
void my_get_stats(struct stats *data)
{
	O1HeapDiagnostics diag = o1heapGetDiagnostics(o1heap);
	data->ram_occupied = diag.allocated;
	data->ram_free = diag.capacity - diag.allocated;

    data->num_connections = 0;
    for (struct mg_connection *c = g_mgr.conns; c != NULL; c = c->next)
    {
        data->num_connections++;
    }
}

/* ---- LED Getter ---- */
void my_get_leds(struct leds *data){
	data->led1 = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_6);
	data->led2 = HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_8);
}

/* ---- LED Setter ---- */
void my_set_leds(struct leds *data) {
	HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, data->led1);
	HAL_GPIO_WritePin(GPIOI, GPIO_PIN_8, data->led2);
}

/* ---- Inside main() ---- */
// Call before any peripheral init:
  /* USER CODE BEGIN SysInit */
  o1heap_Init();
  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_ETH_Init();
  MX_USART3_UART_Init();
  MX_RNG_Init();
  /* USER CODE BEGIN 2 */
  mongoose_init();
  mongoose_set_http_handlers("stats", my_get_stats, NULL);
  mongoose_set_http_handlers("leds", my_get_leds, my_set_leds);
  mongoose_add_ws_reporter(100, "stats");
  for (;;) {
    mongoose_poll();
}

Output

Serial Console

Mongoose will initialize and print the DHCP-assigned / Static IP address. Initially, the logs will show normal heartbeat messages.

The image below shows the serial console output after flashing — Mongoose initialized and IP address assigned.

STM32H745 serial console showing Mongoose initialization log and DHCP IP address assignment

Once the memory pool fills up, you will see out-of-memory messages in the logs. This is expected and correct behavior — Mongoose is respecting the memory limit we set.

The image below shows the serial console with out-of-memory log messages after the pool is exhausted.

STM32 Mongoose serial log showing out-of-memory warning messages when pool is full

Web UI Dashboard

Open the assigned IP in a browser. The dashboard loads with the LED controls, two memory graphs, and the connection gauge.

The image below shows the web dashboard with real-time memory usage and free memory graphs updating live.

Mongoose web dashboard on STM32 showing real-time memory usage graph, free memory graph, and active connection gauge

At baseline, memory usage is around 28 KB and free memory is around 12 KB. The number of active connections starts at 6 — this includes the HTTP and HTTPS listeners, the WebSocket connection, the heartbeat API call, and a few internal connections Mongoose maintains.

As we open more browser tabs, each new connection consumes more memory. The memory usage climbs toward 35 KB, then 37 KB, while free memory drops accordingly.

The image below shows the graphs as more browser tabs are opened — memory usage rises and free memory decreases.

STM32 Mongoose memory usage graph rising to 37KB as more browser connections are added

Once the pool is fully exhausted, Mongoose stops accepting new connections. Closing existing tabs frees memory — you can see the usage graph drop and the free memory graph rise again in real time.

This is exactly what we want: a hard cap on network memory that keeps the rest of the system safe.

STM32 Fixed Memory Pool for Networking using Mongoose — Video Tutorial

This video walks through the complete implementation of a fixed memory pool for Mongoose networking on STM32. We integrate the O1heap allocator, hook it into Mongoose’s custom allocator interface, and monitor real-time RAM usage and active connections on a live web dashboard using WebSocket graphs.

Download STM32 Fixed Memory Pool (Mongoose) Project Files

Complete CubeIDE project with O1heap integration, custom Mongoose allocator, real-time memory graphs, and LED control. Free to download — support the work if it helped you.

CubeIDE Project O1heap + Mongoose STM32H745

STM32 fixed memory pool — Frequently Asked Questions

Conclusion

In this tutorial, we limited Mongoose's network memory usage to a fixed 40 KB RAM pool using the O1heap allocator. The key idea is simple — instead of letting Mongoose pull from the shared heap freely, we gave it its own dedicated block of memory and plugged in a custom mg_calloc and mg_free to route all allocations through O1heap.

We could see this working live on the dashboard. As more browser tabs opened, memory usage climbed. Once the pool was exhausted, Mongoose stopped accepting new connections — but the system kept running. No crash, no undefined behavior. And as tabs were closed, the memory came back. The graphs made all of this visible in real time, which is a nice way to validate that the memory constraint is actually working as expected.

This approach works on any STM32 with enough RAM. You just change the NETWORK_HEAP_SIZE define to whatever fits your system. On tighter devices, even a 10–15 KB pool is enough for a few simultaneous connections.

Browse More STM32 Mongoose TCP/IP Tutorials

About the Author
Arun Rawat
Arun Rawat
Embedded Systems Engineer · Founder, ControllersTech

Arun is an embedded systems engineer with 10+ years of experience in STM32, ESP32, and AVR microcontrollers. He created ControllersTech to share practical tutorials on embedded software, HAL drivers, RTOS, and hardware design — grounded in real industrial automation experience.

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
×

Don’t Miss Future STM32 Tutorials

Join thousands of developers getting free guides, code examples, and updates.