Mongoose Integration Guide: RP2040 / RP2350

This guide shows how to add Mongoose to an existing Raspberry Pi Pico SDK firmware project for RP2040 and RP2350 boards. 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 RP2040 or RP2350 board will:

  • use Mongoose's built-in TCP/IP stack
  • get an IP address using DHCP
  • serve a minimal HTTP endpoint

For a quick start, use the following examples from the Mongoose repo. Note that the RP2040 example can be built for RP2350 and vise versa - just change the PICO_BOARD in the CMakeLists.txt.

Add Mongoose

Create a mongoose/ directory in your Pico SDK project and copy two files there from GitHub:

Create the mongoose/mongoose_config.h file. Copy one of the following:

Add Mongoose sources to your firmware target, add the mongoose/ directory to the include paths, and link the Pico SDK libraries required by your network driver.

file(GLOB MONGOOSE_SOURCES mongoose/*.c)
add_executable(firmware main.c ${MONGOOSE_SOURCES})
target_include_directories(firmware PRIVATE mongoose)

If your project enables OTA updates, make sure your mongoose_config.h has:

#define MG_OTA MG_OTA_PICOSDK

and prevent aggressive loop optimization in the firmware target:

target_compile_options(firmware PRIVATE -fno-tree-loop-distribute-patterns)

Choose a network driver

Mongoose's built-in TCP/IP stack needs one low-level network driver. On Pico SDK projects, the most common choices are:

  • MG_ENABLE_DRIVER_W5500 for W5500-based Ethernet boards and modules
  • MG_ENABLE_DRIVER_PICO_W for Pico W / Pico 2 W Wi-Fi using the CYW43 chip
  • a project-local RMII driver for an external PHY such as LAN8720
  • a project-local USB network driver that bridges Mongoose to TinyUSB RNDIS or CDC-ECM callbacks

Use the W5500 path for wired Ethernet, including the W5500-EVB-Pico2 example. Use the Pico W path for wireless networking on Pico W and Pico 2 W boards. Use the RMII path when you want Ethernet through an external PHY and are prepared to allocate PIO state machines and DMA channels. Use the USB path when the board should appear to the host computer as a USB network adapter.

Network hardware options

W5500 Ethernet
W5500 is the simplest wired option. It is available on boards such as W5500-EVB-Pico and W5500-EVB-Pico2, or as a small add-on module connected over SPI. The default example wiring uses spi0:

W5500 Raspberry Pi Pico / Pico 2
MISO GPIO16 / SPI0_RX
SCS / SCNn GPIO17 / SPI0_CSn
SCLK GPIO18 / SPI0_SCK
MOSI GPIO19 / SPI0_TX
GND GND
3.3V 3V3

If you use a board with the W5500 already fitted, keep the example pin mapping. If you use another W5500 module, change the SPI pin constants in main.c to match your wiring. The W5500 driver also needs a locally administered MAC address; deriving one from the Pico unique board ID is a practical default for development.

Pico W / Pico 2 W Wi-Fi
Pico W and Pico 2 W use the onboard CYW43 wireless chip. There is no external network wiring, but the Pico SDK board type must be set to pico_w or pico2-w so the SDK enables the wireless support libraries. In standalone Mongoose TCP/IP mode, configure the SSID, password, optional access-point SSID, and access-point IP settings through MG_SET_WIFI_CONFIG.

The Wi-Fi driver can join an existing 2.4 GHz network as a station, or create a small access point by enabling AP mode in the Wi-Fi configuration.

LAN8720 RMII Ethernet
The LAN8720 option uses an external RMII PHY connected directly to RP2040 GPIOs. RP2040 does not have a hardware Ethernet MAC, so the example uses PIO state machines and DMA channels to implement the MAC-side RMII logic. This is more hardware-sensitive than W5500, but it avoids an SPI Ethernet controller.

The LAN8720 reference clock, often labelled RETCLK on add-on boards, must feed the RP2040. The example connects it to GPIO20 and configures the RP2040 system clock from that 50 MHz reference:

#define CLKREFPIN 20
clock_configure_gpin(clk_sys, CLKREFPIN, 50 * MHZ, 50 * MHZ);

The RMII and SMI signals are grouped on consecutive GPIOs because the driver uses PIO:

LAN8720 signal Raspberry Pi Pico
RX0, RX1, CRS_DV GPIO6, GPIO7, GPIO8
TX0, TX1, TX-EN GPIO10, GPIO11, GPIO12
MDIO, MDC GPIO14, GPIO15
RETCLK GPIO20
VCC 3V3
GND GND

Set the PHY address to match your LAN8720 module, commonly 0 or 1.

USB RNDIS / CDC-ECM
USB networking does not require Ethernet or Wi-Fi hardware. The firmware uses TinyUSB to enumerate as a USB network adapter. Linux and Windows commonly use RNDIS; macOS uses CDC-ECM, so the example can expose a dual configuration when needed.

This mode is normally a point-to-point link between the Pico and the host computer. The example assigns the board a static IP address, 192.168.3.1, and enables Mongoose's DHCP server so the host receives 192.168.3.2. The USB driver passes frames between TinyUSB callbacks and Mongoose's TCP/IP queue using mg_tcpip_qwrite().

Because the project owns TinyUSB directly, USB stdio is disabled in the RNDIS example and UART stdio is used for logs.

Configure Pico SDK

Set the Pico SDK board type before including pico_sdk_import.cmake. For example, for W5500-EVB-Pico2:

set(PICO_BOARD pico2)
include(pico_sdk_import.cmake)

For Pico 2 W:

set(PICO_BOARD pico2-w)
include(pico_sdk_import.cmake)

Use pico or pico_w instead when building for RP2040-based Pico boards.

Configure W5500 Ethernet

For W5500 Ethernet, initialize SPI and provide Mongoose with three callbacks: select, deselect, and transfer. The W5500-EVB-Pico2 example uses these pins:

enum { SPI_CS = 17, SPI_CLK = 18, SPI_TX = 19, SPI_RX = 16 };

Add the SPI support code to main.c:

#include "hardware/spi.h"
#include "pico/stdlib.h"
#include "pico/unique_id.h"
#include "mongoose.h"

enum { SPI_CS = 17, SPI_CLK = 18, SPI_TX = 19, SPI_RX = 16 };

void spi_begin(void *spi) {
  gpio_put(SPI_CS, 0);
}

void spi_end(void *spi) {
  gpio_put(SPI_CS, 1);
}

void spi_txn(void *spi, uint8_t *tx, uint8_t *rx, size_t len) {
  if (tx != NULL && rx != NULL) {
    spi_write_read_blocking(spi0, tx, rx, len);
  } else if (tx != NULL) {
    spi_write_blocking(spi0, tx, len);
  } else {
    spi_read_blocking(spi0, 0xff, rx, len);
  }
}

static void ethernet_init(void) {
  spi_init(spi0, 500 * 1000);
  gpio_set_function(SPI_RX, GPIO_FUNC_SPI);
  gpio_set_function(SPI_TX, GPIO_FUNC_SPI);
  gpio_set_function(SPI_CLK, GPIO_FUNC_SPI);
  gpio_init(SPI_CS);
  gpio_set_dir(SPI_CS, GPIO_OUT);
  gpio_put(SPI_CS, 1);
}

Set a stable locally administered MAC address. For example, derive it from the unique board ID:

void genmac(unsigned char *mac) {
  pico_unique_board_id_t board_id;
  pico_get_unique_board_id(&board_id);
  mac[0] = 2;
  memcpy(&mac[1], &board_id.id[3], 5);
}

Then extend mongoose_config.h:

extern void genmac(unsigned char *mac);
#define MG_SET_MAC_ADDRESS(mac) genmac(mac)

#include <stddef.h>
#include <stdint.h>

extern void spi_begin(void *);
extern void spi_end(void *);
extern void spi_txn(void *spi, uint8_t *tx, uint8_t *rx, size_t len);

#define MG_TCPIP_DRIVER_INIT(mgr)                                          \
  do {                                                                     \
    static struct mg_tcpip_spi spi_ = {NULL, spi_begin, spi_end, spi_txn}; \
    static struct mg_tcpip_if mif_;                                        \
    mif_.ip = MG_TCPIP_IP;                                                 \
    mif_.mask = MG_TCPIP_MASK;                                             \
    mif_.gw = MG_TCPIP_GW;                                                 \
    mif_.driver = &mg_tcpip_driver_w5500;                                  \
    mif_.driver_data = &spi_;                                              \
    MG_SET_MAC_ADDRESS(mif_.mac);                                          \
    mg_tcpip_init(mgr, &mif_);                                             \
    MG_INFO(("Driver: W5500, MAC: %M", mg_print_mac, mif_.mac));           \
  } while (0)

Configure Pico W / Pico 2 W Wi-Fi

For Pico W and Pico 2 W, set your Wi-Fi credentials in main.c:

#define WIFI_NAME "mywifi"
#define WIFI_PASS "mypassword"
#define WIFI_AP_NAME "mywifi2"
#define WIFI_AP_PASS "mypassword2"
#define ENABLE_AP 0

#include "mongoose.h"

void wifi_setconfig(void *data) {
  struct mg_tcpip_driver_pico_w_data *d =
      (struct mg_tcpip_driver_pico_w_data *) data;
  struct mg_wifi_data *wifi = &d->wifi;
  wifi->ssid = WIFI_NAME;
  wifi->pass = WIFI_PASS;
  wifi->apssid = WIFI_AP_NAME;
  wifi->appass = WIFI_AP_PASS;
  wifi->apip = MG_IPV4(192, 168, 111, 1);
  wifi->apmask = MG_IPV4(255, 255, 255, 0);
  wifi->security = 0;
  wifi->apsecurity = 0;
  wifi->apchannel = 10;
  wifi->apmode = ENABLE_AP;
}

Then extend mongoose_config.h:

extern void wifi_setconfig(void *data);
#define MG_SET_WIFI_CONFIG(data) wifi_setconfig(data)

Set ENABLE_AP to 1 if you want the board to create an access point instead of joining an existing Wi-Fi network.

Add an HTTP server

Initialize the Pico SDK, initialize the network hardware if required, then start the Mongoose event manager and poll it forever:

static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    uint64_t tick = to_ms_since_boot(get_absolute_time());
    if (mg_match(hm->uri, mg_str("/api/tick"), NULL)) {
      mg_http_reply(c, 200, "", "{%m:%llu}\n", MG_ESC("tick"), tick);
    } else {
      mg_http_reply(c, 200, "", "Hi from Mongoose, tick %llu\n", tick);
    }
  }
}

static void run_mongoose(void) {
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);
  mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL);

  for (;;) {
    mg_mgr_poll(&mgr, 1);
  }
}

int main(void) {
  stdio_init_all();

  // W5500 Ethernet projects only:
  ethernet_init();

  MG_INFO(("Hardware initialised, starting firmware..."));
  run_mongoose();
  return 0;
}

For Pico W / Pico 2 W projects, do not call ethernet_init().

Build and run

If you are starting from one of the Mongoose examples, run:

cd ~/src/mongoose/tutorials/rp/w5500-evb-pico2
make build flash

or:

cd ~/src/mongoose/tutorials/rp/pico2-w
make build flash

The example Makefile downloads Pico SDK, copies mongoose.c and mongoose.h, configures CMake, builds build/firmware.uf2, and flashes it with picotool.

For your own project, build it the same way you build any Pico SDK firmware. The important output is the .uf2 file produced by pico_add_extra_outputs().

Start a serial console. In the serial logs, you should see messages like this:

mongoose.c:...:mg_mgr_init       Mongoose version ...
mongoose.c:...:MG_TCPIP_DRIVER   Driver: W5500, MAC: 02:...
mongoose.c:...:tx_dhcp_discover DHCP discover sent
mongoose.c:...:rx_dhcp_client   Lease: 86400 sec
mongoose.c:...:onstatechange    READY, IP: 192.168.1.42

For Pico W / Pico 2 W, the driver line reports the Pico W Wi-Fi driver instead of W5500.

Now you can open the printed IP address in a browser, call /api/tick, or integrate a Web UI device dashboard.

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

Troubleshooting

No IP address is printed
Check the serial console, USB stdio configuration, DHCP server, cable or Wi-Fi credentials, and the selected Mongoose driver.

W5500 does not get a DHCP lease
Check the SPI pin mapping, chip select pin, W5500 power, Ethernet cable, and switch or router. If you changed pins, update both the GPIO setup and the SPI_CS, SPI_CLK, SPI_TX, and SPI_RX constants.

Pico W / Pico 2 W cannot join Wi-Fi
Check WIFI_NAME and WIFI_PASS in main.c, signal strength, 2.4 GHz network availability, and whether the access point requires a security mode not configured by your project.

No DHCP server

If DHCP discover messages repeat without a lease, either packets are not leaving the board or no DHCP server is present on that network.

For W5500 Ethernet, run Wireshark on the Ethernet network and filter for UDP ports 67 and 68. If you do not see messages from your device, check SPI and W5500 wiring. If packets are present but no response arrives, check the DHCP server.

When connecting a board directly to a workstation, the easiest option is often to use a USB-to-Ethernet dongle and enable Internet sharing on that Ethernet interface.

FAQ

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

Does this work on RP2040 and RP2350?
Yes. Use the Pico SDK board definition that matches your board, for example pico, pico_w, pico2, or pico2_w. The examples above target RP2350, but the same integration pattern applies to RP2040 boards with a supported network interface.

Can I use a static IP address?
Yes. Define MG_TCPIP_IP, MG_TCPIP_GW, and MG_TCPIP_MASK in mongoose_config.h. If they are left unset and in this case Mongoose uses DHCP.

How do I know the network is working?
Check the serial log for READY, IP: ..., then open that IP address in a browser.