Drivers for Mongoose built-in TCP/IP stack

Overview

Mongoose Library provides its own TCP/IP stack that can be activated by setting the build option MG_ENABLE_TCPIP to 1. This tutorial shows how to develop your own driver.

API

This stack provides a very simple API:

struct mg_tcpip_driver {
  bool (*init)(struct mg_tcpip_if *);                         // Init driver
  size_t (*tx)(const void *, size_t, struct mg_tcpip_if *);   // Transmit frame
  size_t (*rx)(void *buf, size_t len, struct mg_tcpip_if *);  // Receive frame
  bool (*up)(struct mg_tcpip_if *);                           // Up/down status
};

The struct mg_tcpip_driver provides pointers to driver functionality:

  • init() takes care of initializing the driver context, and the underlying hardware or lower level firmware

    static struct mg_tcpip_if *s_ifp;
    
    static bool init(struct mg_tcpip_if *ifp) {
      s_ifp = ifp;
      // Setup MDC clock
      // Setup PHY clock
      // Init DMA descriptors
      // Init PHY
      // Setup MAC controller
      ...
    }
    
  • tx() points to a function to be called when Mongoose needs to send a frame. It delivers the frame to the underlying hardware or lower level firmware

  • rx()

    • If a driver has to be polled for data, then it will also implement an rx() function. Otherwise, there will be a NULL pointer there.
    • On the other hand, if received data is asynchronous to Mongoose, there is an internal lock-free queue to decouple contexts
      • push to the queue by calling mg_tcpip_qwrite(). The TCP/IP stack will run on the next call to the event manager and then pop the frame from the queue to be processed.

        void my_IRQHandler(void) {
          // Ack IRQ
          // Frame received, size_t len
          mg_tcpip_qwrite(s_rxbuf[s_rxno], len > 4 ? len - 4 : len, s_ifp);
        }
        
      • you can set the queue length to your required size, otherwise a default will be used. Mongoose will allocate the necessary memory.

        struct mg_tcpip_if mif = { ...
                                  .recv_queue.size = 4096
                                 };
        
  • up() points to a function returning the state of the interface; to know whether the interface is up or not, Mongoose calls this function

    static bool up(struct mg_tcpip_if *ifp) {
      uint16_t bsr = phy_read(phy_addr, PHY_BSR); // Read PHY link state
      bool up = bsr & MG_BIT(2) ? 1 : 0;
      if ((ifp->state == MG_TCPIP_STATE_DOWN) && up) {  // link state just went up
        // Read PHY link characteristics
        // Check Full-/Half-duplex
        // Check 10/100M
        // Configure MAC controller
      }
      return up;
    }
    

Compilation options

  • MG_ENABLE_TCPIP=1 - enables the built-in TCP/IP stack
  • MG_ENABLE_DRIVER_xxx=1 - enables the xxx driver

Each driver is activated by setting its respective build option, for example, CMSIS-driver requires MG_ENABLE_DRIVER_CMSIS to be set to 1

#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_xxx) && MG_ENABLE_DRIVER_xxx
...
#endif

Examples

Mongoose Library has many built-in drivers available for STM32, NXP, and many others. Take a look at their implementation in the src/drivers/ directory. These drivers mostly don't use any vendor HAL.

There are some examples on how to work with an external USB library to provide RNDIS services, for example for STM32 and RP2040

For the RP2040, there is also an example on using its PIOs to implement an RMII MAC controller

As the basis for writing your own driver, either using CMSIS or a vendor HAL, we provide a working example for a CMSIS-Driver interface.

CMSIS-Driver

Mongoose includes a driver for CMSIS-Driver, that is, Mongoose built-in TCP/IP stack can run over any (ARM) chip that has a CMSIS Driver for its Ethernet controller, and uses a PHY that also has a CMSIS Driver.

The following example has been developed on an STM32F746 Nucleo board. Beyond the STM HAL specifics, it can be adapted to work on any chip that has a CMSIS-Driver available.

We'll only discuss those aspects that are specific to this driver, for a generic tutorial go the STM32 baremetal tutorial

Actions

  1. Pull CMSIS core, this also includes the basic support for CMSIS Driver

  2. Pull CMSIS Driver, this repository has driver code for several widely used PHYs and some Ethernet chips (stand-alone controllers, not MCUs)

  3. Pull the device family CMSIS Pack, this includes CMSIS headers and also includes CMSIS Drivers for those peripherals that have Middleware (Ethernet, CAN, UART...) available

  4. Write code to interact with CMSIS Driver, See below

  5. In your development environment, add necessary include paths for these above. From our Makefile:

  6. In your development environment, add necessary low-level files and dependencies. In this example, the CMSIS Driver for the STM32F746 uses the STM32 HAL. To develop for another chip, you need to find and solve the driver dependencies for its libraries. You can see how we did it in the Appendix

Driver code

  • init() takes care of initializing the driver context and the lower level firmware

    See that the low-level driver may or may not provide a built-in MAC address, so our driver has to check its capabilities

  • tx() points to a function to be called when Mongoose needs to send a frame. It delivers the frame to the lower level firmware

  • rx()

    • If a driver has to be polled for data, then it will also implement an rx() function. We know of this situation either by inspecting the low-level firmware or checking the CMSIS Driver capabilities returned by an initialization function.

      In the STM32F7 case, this function will not be used, but we provided a generic implementation to ease porting to other chips

    • If received data is asynchronous, CMSIS Driver will call us; here we will push to Mongoose internal lock-free queue. Due to the way CMSIS Driver is structured, we can't use the simple function shown above and need to go a bit deeper: we first reserve (book) space in our queue, in order to get a pointer to destination memory; then we pass it to CMSIS driver to copy the data and finally we add that frame to the queue.

  • up() points to a function returning the state of the interface; we call the PHY functions in CMSIS Driver

Compilation options

  • MG_ENABLE_TCPIP=1 - enables the built-in TCP/IP stack
  • MG_ENABLE_DRIVER_CMSIS=1 - enables the driver

We define these in mongoose_config.h

Ethernet controller initialization

We need to enable the MAC GPIO pins to connect to the dev board PHY using RMII, and configure the clocks. We decided not to use STM32 HAL functions to keep the extra code minimal, as we already have our own baremetal HAL developed:

TCP/IP initialization

The built-in TCP/IP stack has to be enabled to be compiled in, and so Mongoose will work in association with it. This is done by defining MG_ENABLE_TCPIP=1. We do this in mongoose_config.h. In that file, we add the proper definitions:

Then this networking stack has to be configured and initialized. This is done by calling mg_tcpip_init() and passing it a pointer to a struct mg_tcpip_if. Inside this structure:

  • have pointers to a struct mg_tcpip_driver, in this case, that of the CMSIS Driver code
  • For DHCP: set ip as zero
  • For a static configuration, specify ip, mask, and gw in network byte order

In this example we use DHCP, but you can remove the comments and set a static configuration if you want:

Note that, we also need to specify a unique MAC address; this example provides a macro to transform the chip built-in unique ID into a unicast locally administered address; for production runs you'll have to consider among several options, from adding a MAC address chip in your hardware design to registering with the IEEE Registration Authority.

Demo overview

This example is a plain GCC make-based project with the following files:

  • main.c - provides the main() entry point with hardware init, LED blinking and network init
  • hal.h - provides a simple API on top of the CMSIS API, like gpio_write(), uart_init(), etc
  • sysinit.c - provides the SystemInit() function with system clock setup, SysTick setup, etc
  • syscalls.c - provides low level functions expected by the ARM GCC C library
  • link.ld - a GNU linker script file, used for building the firmware binary
  • Makefile - a GNU Makefile for building, flashing and testing the project
  • mongoose.c, mongoose.h - Mongoose Library
  • net.c, net.h - part of the device dashboard example, contains the Web functionality
  • packed_fs.c - part of device dashboard example, embeds the Web UI used by the dashboard
  • startup_stm32f746xx.s - part of STM32 CMSIS, used for startup code and IRQ vector table

Below is a general process outline:

  • The board IP addressing will be provided by a DHCP server. If you want to set a static configuration, set IP address, network mask and gateway in main.c; see above
  • Build the example (see below) and run it on a development board
  • The firmware initializes the network
  • After initialization, the application starts Mongoose's event loop and blinks a blue LED
  • Once the blue LED starts blinking, the example is ready
  • Open your web browser and navigate to the board IP address, you should see a nice device dashboard

Build and run

  • Follow the Build Tools tutorial to setup your development environment.

  • Start a terminal in the project directory; clone the Mongoose Library repo, and run the make build command.

    git clone https://github.com/cesanta/mongoose
    cd mongoose/examples/stm32/nucleo-f746zg-make-baremetal-builtin
    make build
    
  • In order to flash this recently built firmware to your board, plug it in a USB port and execute:

    make flash
    

    As long as there is only one board plugged in, stlink will find it; though we need to know the serial port device to be able to get the log information. In Linux it is probably /dev/ttyACM0

  • When the firmware is flashed, the board should signal its state by blinking the blue LED. We now need to know the IP address of the board to connect to it. If we used DHCP, as it is the default, we can check our DHCP server logs or see the device logs. Let's do this.

  • To connect to the board, in this example we'll be using picocom; we configure it for 115200bps. Use the proper serial device.

    picocom /dev/ttyACM0 -i -b 115200
    picocom v2.2
    ...
    Terminal ready
    0      2 main.c:59:main                 Starting, CPU freq 216 MHz
    6      2 main.c:75:main                 MAC: 02:33:47:5b:3e:32. Waiting for IP...
    ...
    808    2 mongoose.c:7349:onstatechange  READY, IP: 192.168.0.137
    80d    2 mongoose.c:7350:onstatechange         GW: 192.168.0.1
    813    2 mongoose.c:7352:onstatechange         Lease: 86188 sec
    819    2 main.c:80:main                 Initialising application...
    81f    3 mongoose.c:3421:mg_listen      1 0x0 http://0.0.0.0
    824    2 main.c:84:main                 Starting event loop
    
  • Now start a browser on http://IP_ADDRESS, where IP_ADDRESS is the board's IP address printed on the serial console. You should see a login screen as in the image above

  • From here on, if you want to try the dashboard features please go to the device dashboard tutorial and follow some of the steps depicted there.

Appendix: STM32 specifics

The CMSIS Driver for the STM32F746 uses the STM32 HAL, so we have copied some files from an STM32CubeMX generated project in order to have some defines available:

  • MX_Device.h - STM32CubeMX generates this when we configure a project for this board
  • RTE_Components.h - A CMSIS-Pack environment generates this when configuring a project. As we build our own Makefile, we take care of dependencies and this is an empty file in order to satisfy dependencies, as some HAL files #include it.

Then we picked the correct sources to compile, and provided a HAL_GetTick() function to satisfy those parts of the HAL that use it, without having to include more pieces of the HAL, as we already have a time base in place.

Finally, we provided needed globals and a HAL_GetTick() function to satisfy those parts of the HAL that use it, without having to include more pieces of the HAL, as we already have a time base in place: