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 firmwarestatic 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 firmwarerx()
- 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 };
- If a driver has to be polled for data, then it will also implement an
up()
points to a function returning the state of the interface; to know whether the interface is up or not, Mongoose calls this functionstatic 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 stackMG_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
Pull CMSIS core, this also includes the basic support for CMSIS Driver
Pull CMSIS Driver, this repository has driver code for several widely used PHYs and some Ethernet chips (stand-alone controllers, not MCUs)
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
Write code to interact with CMSIS Driver, See below
In your development environment, add necessary include paths for these above. From our Makefile:
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 firmwareSee 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 firmwarerx()
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 stackMG_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
, andgw
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 Librarynet.c
,net.h
- part of the device dashboard example, contains the Web functionalitypacked_fs.c
- part of device dashboard example, embeds the Web UI used by the dashboardstartup_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
, whereIP_ADDRESS
is the board's IP address printed on the serial console. You should see a login screen as in the image aboveFrom 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: