STM32 TCP/IP footprint: how much flash and RAM does Mongoose take?
"How big is it?" is one of the first questions people ask about any embedded TCP/IP stack. So I measured it. On an STM32 Nucleo-H723ZG, bare metal, the Mongoose TCP/IP stack with an HTTP server takes about 36 KB of flash and 10 KB of RAM, and each HTTP connection costs about 1.6 KB of RAM. Turn on the built-in TLS 1.3 and it's about 82 KB of flash, the same 10 KB of RAM, and about 14 KB of RAM per HTTPS connection.
Below is how I got those numbers, so you can repeat the test on your own board and not take my word for it.
Results at a glance
| Build | firmware.bin | RAM used at boot | RAM used with 1 connection |
|---|---|---|---|
| Blinky, no Mongoose | 2,824 | - | - |
| Mongoose logging only, no networking | 7,072 | 0 | - |
| TCP/IP + HTTP server, no TLS | 42,872 | 10,020 | 11,636 |
| TCP/IP + HTTP/HTTPS server, built-in TLS | 89,220 | 10,292 | 24,460 |
All values are in bytes. From these, the footprint of the networking part:
| Flash | RAM | RAM per connection | |
|---|---|---|---|
| TCP/IP stack, no TLS | ~36 KB (35,800) | ~10 KB | ~1.6 KB (1,616) |
| TCP/IP stack with TLS | ~82 KB (82,148) | ~10 KB | ~14 KB (14,168) |
Board: STM32 Nucleo-H723ZG (Cortex-M7), Ethernet.
Compiler: arm-none-eabi-gcc with -Os, newlib-nano.
Software: Mongoose with its built-in TCP/IP stack, Ethernet driver and TLS.
No RTOS, no CubeMX, no lwIP, no mbedTLS.
Two things to keep in mind. The "RAM used" figure is heap usage, I explain below exactly how it's measured. And the 12 KB of Ethernet DMA buffers is not included, because those live in a separate RAM region and you need them with any stack.
The setup
- Nucleo-H723ZG, connected to my Mac with a USB cable. The on-board ST-LINK does flashing, and also gives a serial console wired to USART3 on the MCU
- An Ethernet cable from the board to a USB Ethernet dongle on the Mac
- Internet Sharing turned on, from Wi-Fi to that dongle. The Mac runs a DHCP server on the dongle and routes traffic, so the board gets an IP address on boot and can reach the internet
The firmware is the nucleo-h723zg/minimal tutorial from the Mongoose repo. Build it like this:
git clone https://github.com/cesanta/mongoose
cd mongoose/tutorials/stm32/nucleo-h723zg/minimal
make flash
make flash fetches the CMSIS headers from the ARM and ST repositories,
builds firmware.bin and flashes it. Start a serial monitor and you'll see
the board grab an IP address. Open that address in a browser and you get a
plain text page:
Hi from Mongoose!
tick: ...
ram used: ...
ram free: ...
tick is milliseconds since boot, and the two RAM numbers come from the
firmware itself. That page is our measuring tool.
Why this firmware is good for measuring
It's about as small as firmware gets. main.c initialises the hardware, starts a Mongoose event manager, prints some debug info and runs a bare-metal super loop with two tasks: the network task and a blinky. The network task is a tiny web server that serves the page above.
The hardware layer is hal.c and hal.h, about 200 lines written by hand. It sets up the clock, the UART for debug output, the random number generator, the Ethernet pins and the LED pins. That's it. No Cube, no framework, nothing else that could eat flash and blur the numbers. Whatever grows between builds is Mongoose.
The method: four builds in a row
I built the same project four times and wrote down three numbers each time:
- Size of
firmware.bin - RAM used, printed to the serial console at startup
- RAM used, shown in the browser. At that point one HTTP connection is open, the one serving the page
Then the deltas give the answer: flash of the stack is the firmware.bin
size minus the build without networking, and one connection is RAM used in
the browser minus RAM used at boot.
The builds were:
- No Mongoose at all: I commented out
mongoose.cin the Makefile and every Mongoose call inmain.c, so only the blinky runs.firmware.binis 2,824 bytes. No logs, so no RAM numbers. That's close to the smallest blinky you can have on this board. - Mongoose, logging only:
mongoose.cback in, but only the logging macros are used, plus the log redirect to the UART.firmware.binis 7,072 bytes, RAM used is 0. - TCP/IP stack and HTTP server, no TLS: in
mongoose_config.hI setMG_TLStoMG_TLS_NONE.firmware.binis 42,872 bytes, RAM used is 10,020 at boot and 11,636 with a connection. - Same, with built-in TLS: details below.
When I refresh the page several times, tick changes but the RAM numbers
don't move. So nothing leaks per request.
How "RAM used" and "RAM free" are calculated
This is worth understanding, because otherwise those numbers are just magic.
Open link.ld, the linker script. It describes the memory layout:
MEMORY {
flash(rx) : ORIGIN = 0x08000000, LENGTH = 1024k
dtcmram (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
ram_d1 (rwx) : ORIGIN = 0x24000000, LENGTH = 320K
ram_d2 (rwx) : ORIGIN = 0x30000000, LENGTH = 32K
ram_d3 (rwx) : ORIGIN = 0x38000000, LENGTH = 16K
...
}
_estack = ORIGIN(ram_d1) + LENGTH(ram_d1);
The vector table, code (.text) and read-only data go to flash. Variables
(.data) and uninitialised variables (.bss) go to RAM_D1, the main 320 KB
block. After .bss there's an _end symbol. _estack points to the very end
of RAM_D1, and the stack grows down from there.
The space between _end and the stack is the heap. The firmware uses
newlib-nano, and its malloc() asks for memory through _sbrk(), which
hal.c overrides:
extern unsigned char _end[]; // End of data section, start of heap
static unsigned char *s_current_heap_end = _end;
size_t hal_ram_used(void) {
return (size_t) (s_current_heap_end - _end);
}
size_t hal_ram_free(void) {
unsigned char endofstack;
return (size_t) (&endofstack - s_current_heap_end);
}
s_current_heap_end is the "break" address, in Unix terms. It starts at
_end, moves up when you allocate and down when you free. So "RAM used" is
everything between _end and the break, which is what the stack and the
connections have allocated. "RAM free" is the gap between the break and the
current stack pointer. Since the stack pointer moves as functions are called,
"RAM free" wobbles a bit depending on where you call it.
Why unused code costs nothing
You might ask: in build 2, mongoose.c is compiled in completely. Why
isn't the TCP/IP stack in the binary? Because of these flags in the Makefile:
CFLAGS += -g3 -Os -ffunction-sections -fdata-sections
LDFLAGS ?= ... -Wl,--gc-sections
-ffunction-sections -fdata-sections put every function and every variable
into its own ELF section. --gc-sections tells the linker to garbage collect
the sections nobody references. If your code never calls the networking
functions, they're simply not in the firmware. Same goes for any Mongoose
feature you don't use: if you never call the MQTT client, it's not there.
The Makefile has a helper target to check this:
make show_symbols_sorted_by_size
It lists every symbol in the firmware with its size. In the logging-only build there are no networking symbols at all. The one exception is the Ethernet DMA buffers.
The Ethernet DMA buffers
The Ethernet controller on the H7 moves frames with DMA, and that DMA can't reach every RAM region. As far as I know it can't access DTCM, for example. So the linker script puts the buffers in RAM_D2:
.eth_ram (NOLOAD) : { *(.eth_ram .eth_ram*) } > ram_d2
and mongoose_config.h tags them with that section:
#define MG_ETH_RAM __attribute__((section(".eth_ram")))
By default Mongoose uses 4 receive and 4 transmit descriptors, each holding about one Ethernet frame, around 1.5 KB. That's 6 KB for RX and 6 KB for TX, 12 KB of RAM_D2 in total. I left it out of the numbers above: it's not in RAM_D1, and any network stack on this chip needs DMA buffers of some kind.
Adding TLS
Now the TLS build. In mongoose_config.h, set TLS back to built-in:
#define MG_TLS MG_TLS_BUILTIN
In main.c, add an HTTPS listener with the same event handler, and set up
TLS on every accepted HTTPS connection:
} else if (ev == MG_EV_ACCEPT && c->is_tls) {
struct mg_tls_opts opts = {
.cert = mg_str(TLS_CRT),
.key = mg_str(TLS_KEY),
};
mg_tls_init(c, &opts);
}
mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL);
mg_http_listen(&mgr, "https://0.0.0.0", http_ev_handler, NULL);
For the certificate and key I used the Mongoose TLS helper:
generate a self-signed CA, then a server key and certificate, and tick the
option to show them as C constants. You can do the same with the OpenSSL
commands listed on that page. Paste them in as TLS_CRT and TLS_KEY,
then make flash.
Results: firmware.bin is 89,220 bytes. RAM used at boot is 10,292, a
little more than before. Go to https:// and the board's IP, click through
the browser warning about the self-signed certificate, and the page says
24,460.
So with TLS:
- Flash: 89,220 - 7,072 = 82,148 bytes, about 82 KB. This includes the embedded certificate and key, so the TLS code itself is a bit smaller
- RAM: about 10 KB, same as without TLS
- One HTTPS connection: 24,460 - 10,292 = 14,168 bytes, about 14 KB
The per-connection jump from 1.6 KB to 14 KB is what TLS costs you. If you're tight on RAM, that's the number to watch: count how many HTTPS clients you expect at the same time, and multiply.
Bonus: how many requests per second?
While I had the board on the desk, I also checked throughput with siege, a simple HTTP benchmark tool: benchmark mode, 5 concurrent clients, 3 seconds, hitting the root URL.
First run: 89 transactions per second. Not great, but the console was
flooded with debug logs, and UART logging is synchronous and slow. After
changing the log level to MG_LL_INFO:
mg_log_set(MG_LL_INFO);
the same test gave about 1,600 transactions per second. Lesson: don't benchmark anything with debug logging on.
Summary
On a bare-metal STM32H723 with Mongoose:
- TCP/IP stack + HTTP server: ~36 KB flash, ~10 KB RAM, ~1.6 KB RAM per connection
- With built-in TLS: ~82 KB flash, ~10 KB RAM, ~14 KB RAM per connection
- Plus 12 KB of Ethernet DMA buffers in RAM_D2
- ~1,600 HTTP requests per second with logging at INFO level
And that's a whole stack in one library: driver, TCP/IP, HTTP and TLS. No lwIP + mbedTLS + httpd to glue together and size separately.
FAQ
How much flash does the Mongoose TCP/IP stack take on STM32?
About 36 KB on an STM32H723 built with -Os, including the Ethernet driver,
TCP/IP stack and an HTTP server. With the built-in TLS 1.3 it's about 82 KB,
including an embedded certificate and key.
How much RAM does the Mongoose TCP/IP stack need?
About 10 KB of heap with or without TLS, plus 12 KB of Ethernet DMA buffers with the default 4 RX and 4 TX descriptors. Each plain HTTP connection adds about 1.6 KB, each HTTPS connection about 14 KB.
Does unused Mongoose code end up in the firmware?
No, as long as you build with -ffunction-sections -fdata-sections and link
with --gc-sections. The linker drops every function you don't call.
Do I need an RTOS, lwIP or mbedTLS for this?
No. The test runs in a bare-metal super loop, and Mongoose provides the Ethernet driver, TCP/IP stack and TLS itself. Mongoose can also run under FreeRTOS, Zephyr and others, or on top of lwIP if you already have it.
How can I measure RAM usage on my own board?
Override _sbrk() and track the heap break, as in hal.c of the
minimal tutorial.
Used RAM is the break minus _end, free RAM is the stack pointer minus the
break.