Exploring SLIP Networking Over UART with Zephyr and Linux: A Quick Guide
In this article we will explore network connectivity over UART connection using SLIP framing on Zephyr RTOS.
Even if your IoT device board does not have a hardware ethernet interface, you can still use standard networking methods to make your software design easier.
This is because networking is not just limited to devices that have an ethernet interface.
Networking is about efficiently transferring data between multiple nodes.
The moment you have "more than one sensor connected to a gateway" scenario, you NEED networking.
Networking is for any device that uses:
-
BTLE radios
-
WiFi interfaces
-
LoRaWAN radios
-
LPWAN radios
-
Conventional networking interfaces
When adding networking capabilities to an embedded system it is good to make sure that we can reuse the existing network stack and ensure that we don’t need to implement data transfer logic from scratch.
Overview
In this article, I will demonstrate how networking functions within the context of a UART interface. Specifically, we will utilize a UART as our physical layer and transfer network packets transparently over it, allowing us to use the networking stack on our embedded device and communicate with any computer on the internet.
The goal is to set up full packet routing across our embedded infrastructure using the Swedish Embedded SDK/Zephyr RTOS and Linux. We will be using Zephyr RTOS networking because Zephyr already offers a comprehensive networking stack, and we will use Linux on the opposite end of the UART connection.
This setup is particularly useful when you have a BTLE or LoRA enabled microcontroller and a UART connection to a Linux CPU, which is common in robot control applications, mesh gateways, and distributed sensing solutions.
In such scenarios, the Linux module on your board acts as a gateway to the outside world, while the microcontroller handles low-level communication with machinery.
If we can successfully set up networking and our Linux device is connected to the internet via a conventional LAN, then we can simply set up routing on the Linux board and our microcontroller will be able to communicate with any internet IP address, allowing us to send MQTT or CoAP packets to a message broker service without any extra software besides existing Zephyr modules and libraries.
This involves several steps that we need to cover:
-
UART Setup: we will use this uart for packet transfers.
-
TAP Setup: this will be our Linux virtual ethernet interface that will be accepting packets destined to our embedded device connected over UART.
-
Routing setup: we will configure our linux board to route packets between the interfaces so that we can reach the internet (provided our linux board already has a connection to the outside world)
After this we will be able to run our MQTT publishing example and be able to route packets to our MQTT broker through our linux machine. We will do all this in simulation so no hardware is needed for following the content of this article.
To make the process simple I have provided a script in Swedish Embedded SDK that
installs all necessary tools locally. Although you can use
swedishembedded/build:latest
docker image as well.
Here is how to install all tools locally:
mkdir swedishembedded && cd swedishembedded
git clone https://github.com/swedishembedded/sdk sdk && cd sdk
./scripts/install-sdk
./scripts/init
Serial networking in Zephyr
The networking stack is a piece of software that accepts raw data frames as input and allows applications to register callbacks to receive data addressed to them. On a conventional system, these callbacks are implemented using sockets, which are a convenient way for userspace applications to interface with the low-level kernel networking stack. The application can then simply wait for data on the socket and execute code once data is available.
Sockets are simply data streams with an address (IP+port), and the networking stack ensures that incoming data flow is directed to the correct recipients.
When you need to route messages in your IoT firmware, be cautious about trying to create your own protocols for message routing. It is likely that your newly invented protocol will not be sufficiently robust or will simply be a re-implementation of an existing solution (often much worse).
There are several ways to get data into the networking stack:
-
Ethernet PHY: We can have an Ethernet physical interface driver that will copy data from the ethernet chip, which has received the data over the wire, and pass it to the networking stack for routing to the correct recipient.
-
Direct injection: We can also directly send a software-generated packet to a networking interface. This will inject the packet into the networking stack and the networking code will then pass the packet to the correct destination based on the routing rules we have set up. If the packet is destined for an address that is acceptable by another networking interface on the local machine, the networking stack will pass the packet to the code that handles that interface. That code will then determine which application listening on that interface should receive the packet.
-
Any other data source: We can also pass the packet over a serial port or SPI and inject it into the networking stack. This allows us to have full scale networking without requiring a physical ethernet interface.
Of course, having a physical ethernet interface has many benefits, such as robust and fast transmission on the wire and hardware caching of incoming packets. Some physical interfaces on specialized SoCs (like Broadcom) even support direct DMA transfers between networking adapters, resulting in a significant speed increase when passing packets between ethernet and WiFi, for example.
However, we are trying to cover a particular case here with a custom gateway solution where an ethernet interface is not available on the microcontroller.
We must therefore pass the ethernet packets over UART.
UART and framing
We cannot simply pass packets to the UART because a UART is designed to handle bytes of data.
UARTs are known for losing data at any time and it is also possible to miss an incoming data stream if the board is performing a very intensive task and is unable to respond to the UART interrupt in time.
To address this issue, several framing protocols have been developed, including:
-
Asynchronous Start-Stop (ASS)
-
HDLC (High-Level Data Link Control)
-
PPP (Point-to-Point Protocol)
-
SLIP (Serial Line Internet Protocol)
-
XON/XOFF flow control
-
RTS/CTS flow control
In this article, we will be using SLIP.
Building the MQTT sample
Zephyr has a sample called samples/net/mqtt_publisher (opens in a new tab) but it does not support SLIP out of the box (at the time of this writing).
Therefore, we need to enable SLIP:
prj.conf
CONFIG_NET_SLIP_TAP=y
CONFIG_SLIP_MAC_ADDR="00:00:00:00:00:01"
CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1"
CONFIG_NET_CONFIG_MY_IPV4_NETMASK="255.255.255.0"
CONFIG_NET_CONFIG_MY_IPV4_GW="192.168.10.10"
CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2"
What we are trying to do here is configure our Zephyr board to have an IP
address of 192.0.2.1
, which will be the address that the networking stack
considers its own. On the other end of the UART, we are connected to a virtual
TAP interface which will have the address 192.0.2.2
.
The router for us from the Zephyr side is the Linux machine which has the
address 192.168.10.10
which is the local gateway where all packets coming out
of the Zephyr interface should be routed if their destination is "outside
world".
When we pass a packet to the networking stack on the Zephyr board, if the packet is destined for any address other than our own, the networking stack will try to resolve the gateway MAC address using ARP and then send the packet to the network with the gateway’s MAC address embedded in the Ethernet frame. This will effectively make the packet arrive at the Linux board and then the Linux board will decide what to do with it (for example, taking the MAC of its gateway, overwriting the Ethernet frame destination with it, and then passing that frame further out until it reaches our MQTT server).
Routing on the Ethernet level is simply about two things: determining which wire (essentially networking interface) to send the packet to (which is determined by the routing table) and then writing the packet to that wire with the correct receiver ID (MAC address).
As the packet passes through Ethernet switches, it will be copied into many other wires, and the Ethernet adapter that has the given MAC address will then accept the packet and the networking stack managing that adapter will then process the packet in the same way.
So the concept is really quite simple. We just need to have a way of passing these frames over a UART wire instead of an Ethernet wire.
QEMU simulation
We are going to use QEMU simulation so we can build our application from Zephyr directory like this:
west build -b qemu_x86 samples/net/mqtt_publisher/ -t run
Make sure that you have updated the prj.conf
to include SLIP support!
If you just run that build command, you will get an error:
qemu-system-i386: -serial unix:/tmp/slip.sock: Failed to connect to '/tmp/slip.sock': No such file or directory
qemu-system-i386: -serial unix:/tmp/slip.sock: could not connect serial device to character backend 'unix:/tmp/slip.sock'
If you don’t see this error then you have not configured SLIP in the project build options so make sure you can see it.
The QEMU board defines uart1
for use with SLIP, but the method that Zephyr
uses in the main repository is limited. This is because it uses a uart-pipe
device, which is an artificial driver that can only be connected to a single
uart. If you want to have multiple SLIP interfaces, you will need to modify the
main slip driver to work with an explicitly specified uart.
The slip driver is implemented here (opens in a new tab).
The SLIP protocol is defined in RFC1055 (opens in a new tab).
Serial to UART
The command in the sample above that uses QEMU attempts to connect UART1 to a
socket file called /tmp/slip.sock
, which is supposed to be a Unix socket.
However, in order to set up SLIP on a Linux machine, we need a terminal device
(similar to what you would have with a hardware UART).
Therefore, we utilize a utility called socat
to create a virtual pseudo
terminal (PTY) and create the Unix socket file to which QEMU can then connect:
socat PTY,link=/tmp/slip.dev UNIX-LISTEN:/tmp/slip.sock,fork
We use the "fork" option to ensure that socat does not exit when QEMU hangs up, which is typically what you want. However, in test scenarios, you should omit this option to ensure that the socket is deleted after QEMU exits.
If you now run the same "run" command, you will see that the example is attempting to connect to the MQTT server:
west build -b qemu_x86 samples/net/mqtt_publisher/ -t run
*** Booting Zephyr OS build zephyr-v3.2.0-1-ga0b3ba0db414 ***
[00:00:00.000,000] <inf> net_config: Initializing network
[00:00:00.000,000] <inf> net_config: IPv4 address: 192.0.2.1
[00:00:00.000,000] <inf> net_mqtt_publisher_sample: attempting to connect:
[00:00:03.030,000] <inf> net_mqtt_publisher_sample: mqtt_connect: -116 <ERROR>
The packets are currently being sent from the MQTT stack in Zephyr via the SLIP
interface (uart0
) and arriving at the /tmp/slip.dev
interface, which is the
UART counterpart on the Linux side.
In order to connect this UART to a virtual ethernet interface, we need to use a "TAP" interface, which is a virtual L2 ethernet interface.
Serial to Ethernet
The terminal can usually be configured to run in SLIP mode by using
ioctl call SIOCSIFENCAP
and passing parameter set to cslip
. This is
demonstrated by the utility slattach
: link (opens in a new tab)
sudo slattach -s 115200 -p cslip -L /tmp/slip.dev
However, this method doesn’t work with a pseudoterminal. So we need to use a custom utility for this in this case.
Zephyr net-tools (../tools/net-tools
if you are using a zephyr workspace) has
a tunslip6 utility which accomplishes this binding for a pseudoterminal:
tunslip6.c (opens in a new tab)
This utility also includes a SLIP protocol parser that completely bypasses the
Linux kernel SLIP driver (which would otherwise be used if you use slattach
on
a real UART).
It creates a tap network interface (tap0) and then executes the following commands on that interface:
ifconfig tap0 up
ip -6 route add 2001:db8::/64 dev tap0
ip -6 addr add 2001:db8::2/64 dev tap0
ip route add 192.0.2.0/24 dev tap0
ip addr add 192.0.2.2/24 dev tap0
Unfortunately, the IP addresses in the default net-tools package are hardcoded, so you will need to modify the utility for your own system if you want to use it with more flexible settings. For example, if you change your Zephyr IP address to something other than 192.0.2.2, the utility will stop working.
In this case, we are doing the following:
-
Creating a new TAP interface (a virtual ethernet interface)
-
Configuring this interface to have an IP of 192.0.2.2
-
Setting up routing for this interface such that all IP packets addressed to 192.0.2.XX are passed to our tap0 interface.
NAT and masquerading
While this is sufficient for communicating with any software that is listening on 192.168.2.2, it is not enough to send packets to other networks.
To be able to send packets to our MQTT server on a different network, we will need to enable routing as well. On the Linux side, we need to do this:
sudo sysctl sysctl net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -j MASQUERADE
This will apply routing rules to all packets coming out of network interface
tap0
.
Communicating with the MQTT server
We can now modify the MQTT example to connect to our MQTT broker outside of the local network. This way, we should be able to see the messages.
samples/net/mqtt_publisher/src/config.h
#define SERVER_ADDR "your-mqtt-server-ip"
Rerunning the example now successfully shows that messages are delivered:
[00:00:11.250,000] <inf> net_mqtt: Connect completed
[00:00:11.250,000] <inf> net_mqtt_publisher_sample: MQTT client connected!
[00:00:11.250,000] <inf> net_mqtt_publisher_sample: try_to_connect: 0 <OK>
[00:00:11.260,000] <inf> net_mqtt_publisher_sample: mqtt_ping: 0 <OK>
[00:00:11.260,000] <inf> net_mqtt_publisher_sample: PINGRESP packet
If you subscribe to the sensors
topic at the MQTT broker you will also see the
message results:
Topic: sensors QoS: 0
DOORS:OPEN_QoS2
Recap
This concludes this tutorial on using serial UART for transparent embedded networking.
The main points I would like you to take away from this article are:
-
Do not implement any custom routing protocols for passing messages between sensors; instead, use a networking stack.
-
Modify and adapt existing Zephyr utilities for your firmware development tasks instead of creating your own tools from scratch.
-
Whenever possible, use a networking stack to reduce the number of weak links in the communication of your application.