Mongoose Integration Guide: STM32

This guide shows how to add Mongoose to an existing STM32 firmware project. The example uses CubeMX, the most common STM32 workflow. Mongoose can run on top of existing TCP/IP stacks such as lwIP or Zephyr; however, this guide explains how to integrate Mongoose in standalone mode using Mongoose's built-in TCP/IP stack.

By the end of this guide, your STM32 board will:

  • use Mongoose's built-in TCP/IP stack
  • get an IP address over Ethernet using DHCP

For a quick start, you can use the following examples from the Mongoose repo:

Configure peripherals

Open CubeMX and configure the following peripherals:

  • In the "Project Manager" tab, set heap size at least to 64Kb (0x10000), and stack size at least to 8Kb (0x2000)
  • In Pinout & Configuration → Security, enable RNG
  • In Pinout & Configuration → Connectivity, enable USART in Asynchronous mode and configure the TX and RX pins. See board pinouts for the correct pins
  • In Pinout & Configuration → Connectivity, enable ETH in RMII mode and configure the Ethernet pins. Again, see board pinouts for the correct pins
  • Click "Generate Code"

NOTE: Do not enable the Ethernet IRQ handler in CubeMX, because it enables Cube's Ethernet driver. This guide uses Mongoose's Ethernet driver.

Add Mongoose

Create a mongoose/ directory and copy the following files there from GitHub:

  • mongoose.c - source file with implementation
  • mongoose.h - header file with API
  • sign.js - for firmware digital signing. only required if you use firmware OTA

Create the Mongoose/mongoose_config.h file:

// Mongoose configuration file. For a full list of build options, see
// https://mongoose.ws/docs/getting-started/build-options/

#pragma once
#define MG_ARCH MG_ARCH_CUBE

Add the Mongoose/ directory to the include paths, and add Mongoose/mongoose.c to the build. The exact steps depend on your chosen IDE. For VS Code CMake, edit the CMakeLists.txt file in the project root:

# Add sources to executable
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
    # Add user sources here
    ${CMAKE_SOURCE_DIR}/mongoose/mongoose.c
)

# Add include paths
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
    # Add user-defined include paths
    ${CMAKE_SOURCE_DIR}/mongoose/
)

As an alternative to the steps above, instead of copying files from GitHub, you can enable the I-CUBE-Mongoose software pack in CubeMX. That will copy those three files to Middlewares/Third_Party/Cesanta_Mongoose/.

Configure .bin file generation

If you're using CubeIDE, select Project / Properties / C/C++ build / Settings / MCU/MPU Post build outputs, enable "Convert to binary file (-O binary)"

If you're using VSCode, add this section to the end of the top-level CMakeLists.txt:

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
  COMMAND ${CMAKE_OBJCOPY} -O binary
          ${PROJECT_NAME}.elf
          ${PROJECT_NAME}.bin
)

Update linker script

Open the .ld linker script in the project root, and verify the configured memory region for the .data section. If that memory region is not accessible by the Ethernet DMA controller, add the following line after the .data rules. You may need to change RAM_D2 to the DMA-accessible region:

/* The Mongoose driver puts DMA buffers into the .eth_ram ELF section. It should be accessible by the ETH DMA */
.eth_ram : { *(.eth_ram .eth_ram*) } > RAM_D2 AT > FLASH

Modify main.c

In Core/Src/main.c, add the Mongoose include. Make sure to add the following snippets inside the BEGIN/END markers to prevent CubeMX from deleting them during code regeneration:

/* USER CODE BEGIN Includes */
#include "mongoose.h"
/* USER CODE END Includes */

In Core/Src/main.c, add a log_fn callback to route Mongoose log output to UART:

/* USER CODE BEGIN 0 */
static void log_fn(char ch, void *param) {
  HAL_UART_Transmit(param, (unsigned char *) &ch, 1, HAL_MAX_DELAY);
}
/* USER CODE END 0 */

In Core/Src/main.c, initialize the event manager, set the log function, and run the event loop:

  /* USER CODE BEGIN WHILE */
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);
  mg_log_set_fn(log_fn, &huart3);

  while (1)
  {
    mg_mgr_poll(&mgr, 1);
    /* USER CODE END WHILE */

Build and run

Start a serial console, rebuild, and flash the device. In the serial logs, you should see something like this:

0      2 mongoose.c:26153:mg_phy_init   PHY ID: 0x07 0xc131 (LAN87x)
6      2 mongoose.c:5834:mg_mgr_init    Driver: stm32h, MAC: 2a:37:94:03:d5:74
d      3 mongoose.c:5841:mg_mgr_init    MG_IO_SIZE: 512, TLS: builtin
13     3 mongoose.c:5758:mg_listen      1 0 http://0.0.0.0:80
19     1 mongoose.c:6260:onstatechange  Link down
7d6    3 mongoose.c:28424:mg_tcpip_driv Link is 100M full-duplex
7db    3 mongoose.c:6404:tx_dhcp_discov DHCP discover sent. Our MAC: 2a:37:94:03:d5:74
831    3 mongoose.c:6382:tx_dhcp_reques DHCP req sent
836    2 mongoose.c:6559:rx_dhcp_client Lease: 3600 sec (3602)
83c    2 mongoose.c:6249:onstatechange  READY, IP: 192.168.2.31
842    2 mongoose.c:6250:onstatechange         GW: 192.168.2.1
847    2 mongoose.c:6253:onstatechange        MAC: 2a:37:94:03:d5:74

Now you can ping the device using the IP address printed in the logs, and integrate a Web UI device dashboard.

Check the Troubleshooting section if it does not work as expected.

Secure Firmware Updates

If you use Mongoose OTA Manager, enable digital signing for your firmware. In the terminal, generate public and private keys for your digital signature. In the project root, run:

node mongoose/sign.js keygen

Wrote private.pem  (keep secret, never ship in firmware)

Add to your mongoose_config.h:
#define MG_OTA_PUBLIC_KEY { \
 ....
}

The "#define MG_OTA_PUBLIC_KEY ..." is your public key. Copy/paste it into your mongoose_config.h. Then, rebuild your firmware. From now on, your firmware will accept only firmwares which are signed by the private.pem. Keep it private!

To sign your firmware, run:

node mongoose/sign.js build/Debug/PROJECT_NAME.bin

This will create build/Debug/PROJECT_NAME.signed.bin. The sign.js script adds a 64-byte P-256 signature block followed by 4-bytes MGSG marker, so we can detect if the given .bin file was signed or not. Mongoose OTA code uses MG_OTA_PUBLIC_KEY from mongoose_config.h to verify the signature.

To sign your builds automatically, add this snippet to your CMakeLists.txt:

If you use Mongoose OTA Manager and wish to use secure updates, add this snippet for automatic firmware digital signing:

if (EXISTS ${CMAKE_SOURCE_DIR}/private.pem)
  add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
      COMMAND node ${CMAKE_SOURCE_DIR}/mongoose/sign.js sign ${PROJECT_NAME}.bin ${CMAKE_SOURCE_DIR}/private.pem
      VERBATIM
  )
endif()

Login to Mongoose OTA Manager. Click on the "info" toolbar button to trigger the instructions. Copy-paste configuration snippet to your mongoose_config.h that define MG_OTA_URL and MG_OTA_FIRMWARE_VERSION.

Rebuild and reflash your firmware. Now it should appear in the device list. Make changes to your firmware, upload the signed binary, assign it to your devices - and they will pull periodically and uppdate automatically.

Troubleshooting

No IP address is printed
Check Ethernet cable, PHY pins, RMII configuration, DHCP server, and board pinout.

No free descriptors

If you see the "No free descriptors:" message, the likely cause is the missing/incorrect .eth_ram snippet in the linker script, or the wrong Ethernet pins; double-check the list against your device documentation.

No DHCP server

If you see log messages like this:

130b0  3 mongoose.c:4776:tx_dhcp_discov DHCP discover sent. Our MAC: 02:03:04:05:06:07
13498  3 mongoose.c:4776:tx_dhcp_discov DHCP discover sent. Our MAC: 02:03:04:05:06:07

One possible reason is that the transmission side of the driver is not working. Run Wireshark on the Ethernet network, filter for UDP ports 67 and 68. If you do not see messages from your device, then transmission is not working; check the pins and the linker script.

Another possible reason is that no DHCP server is running on the network your device is connected to. That usually happens when a device is connected back-to-back with the workstation.

The easiest option is to use a USB-to-Ethernet dongle and enable Internet sharing on the Ethernet interface.

FAQ

Do I need LwIP for this?
No. This guide uses Mongoose's built-in TCP/IP stack instead of LwIP.

Should I enable the Ethernet IRQ handler in CubeMX?

No. Do not enable CubeMX's Ethernet IRQ handler, because that enables Cube's Ethernet driver. This guide uses Mongoose's Ethernet driver.

Why is .eth_ram needed on STM32H7?

STM32H7 Ethernet DMA needs buffers in DMA-accessible memory. The .eth_ram section ensures Ethernet buffers are placed correctly.

How do I know the network is working?

Check the serial log for READY, IP: ..., then open that IP address in a browser.