SLIP and lwIP - IP over a serial link

SLIP and lwIP: How a Microcontroller Runs a Real IP Stack Over Serial

Getting an IP packet onto a device the size of a coin is two problems. The packet has to cross whatever physical link is available, which is often a plain serial line or a USB serial port that moves raw bytes and nothing else. And the device needs a TCP/IP stack small enough to run in tens of kilobytes of RAM. SLIP solves the first problem and lwIP solves the second. Together they let a microcontroller present itself to a host computer as a real network interface.

This article covers the SLIP framing format at the byte level, why it was deliberately kept minimal and what CSLIP and PPP added on top, the structure of the lwIP stack that runs on most microcontrollers, and how the two combine so that a Linux host gets a working network interface tunneled over a serial link.

SLIP: Framing IP Over Serial

A serial link delivers a stream of bytes with no notion of where one message ends and the next begins. SLIP, the Serial Line Internet Protocol defined in RFC 1055 in 1988, is the smallest possible answer to that: a framing convention that marks the boundaries of an IP datagram and escapes any byte inside the datagram that would be mistaken for a boundary.

SLIP reserves four byte values:

Name Value Purpose
END 0xC0 (192) Marks the end of a datagram.
ESC 0xDB (219) Escape prefix (not the ASCII ESC character).
ESC_END 0xDC (220) Sent after ESC to mean a literal 0xC0 in the data.
ESC_ESC 0xDD (221) Sent after ESC to mean a literal 0xDB in the data.

The rule is byte stuffing. To send a datagram, transmit each byte as-is, except that a data byte equal to END is sent as the two bytes ESC then ESC_END, and a data byte equal to ESC is sent as ESC then ESC_ESC. A single END byte terminates the frame. Most implementations also send an END before the datagram so that any line noise picked up while the link was idle is flushed as its own empty, harmless frame.

#define END     0xC0
#define ESC     0xDB
#define ESC_END 0xDC
#define ESC_ESC 0xDD

void slip_send(const uint8_t *p, size_t len) {
    putc(END);                     // flush any line noise as an empty frame
    for (size_t i = 0; i < len; i++) {
        switch (p[i]) {
        case END: putc(ESC); putc(ESC_END); break;   // escape a literal 0xC0
        case ESC: putc(ESC); putc(ESC_ESC); break;   // escape a literal 0xDB
        default:  putc(p[i]);
        }
    }
    putc(END);                     // end of datagram
}

The receiver is the mirror image: read bytes into a buffer, treat ESC as a one-byte lookahead that maps ESC_END back to 0xC0 and ESC_ESC back to 0xDB, and hand the buffer to the IP layer when END arrives. There is no length field. The frame is exactly the bytes between two END markers, unescaped.

SLIP defines no maximum datagram size itself. Both ends agree on an MTU out of band. The historical BSD default was 1006 bytes; 1500 is common on modern links to match Ethernet.

What SLIP Leaves Out: CSLIP and PPP

SLIP carries an IP datagram and nothing else. That minimalism is the point, and also the reason it was replaced for most uses. Four things are missing:

  • No type field. SLIP assumes the payload is always IP. It cannot multiplex a second protocol on the same link.
  • No addressing or negotiation. Neither end learns the other's IP address from the link. Both addresses must be configured by hand before the link comes up.
  • No error detection. A corrupted byte produces a corrupted datagram, and SLIP will not notice. Detection is left entirely to the IP and transport checksums above it.
  • No compression. Every datagram carries a full 40-byte TCP/IP header, which is expensive on a slow serial line where the payload might be a single keystroke.

CSLIP (Compressed SLIP), from RFC 1144 by Van Jacobson, addresses the last point. It compresses the 40-byte TCP/IP header down to around 3 to 5 bytes for a typical packet by sending only the fields that changed since the previous packet on the same connection. The framing is identical to SLIP; only the header handling differs.

PPP, the Point-to-Point Protocol in RFC 1661, addresses the rest and is what most systems use today when they need IP over a serial or dial-up link. The tradeoff is complexity.

Feature SLIP CSLIP PPP
Header compression No Yes (VJ) Optional
Address negotiation No (manual) No (manual) Yes (IPCP)
Link control / auth No No LCP + PAP/CHAP
Error detection No No Yes (FCS)
Multiple protocols IP only IP only Yes
Defined in RFC 1055 RFC 1144 RFC 1661

SLIP survives precisely because it is trivial. On a microcontroller where every kilobyte counts and the link is a known, fixed point-to-point serial connection to one host, PPP's negotiation and framing overhead buy nothing. The addresses are already known, there is only one protocol, and the upper-layer checksums already catch corruption. SLIP is a few dozen lines of code, and that is often the right size.

lwIP: A TCP/IP Stack in Tens of Kilobytes

Framing gets bytes across the wire. Something still has to build and parse the IP, TCP, and UDP headers, track connection state, answer DHCP and DNS, and hold packets in memory. On a desktop that is the kernel. On a microcontroller it is usually lwIP, a lightweight open-source TCP/IP stack written by Adam Dunkels at the Swedish Institute of Computer Science and now maintained as a community project.

lwIP was designed from the start to run in the resource budget of an embedded device: tens of kilobytes of RAM and around 40 kilobytes of code, versus the megabytes a full desktop stack assumes. It implements IPv4 and IPv6, TCP, UDP, ICMP, a DHCP client, DNS, and more, while letting the integrator compile out anything unused.

Two design choices make that footprint possible. The first is the pbuf, lwIP's packet buffer. A pbuf can chain multiple memory fragments into one logical packet and can point directly at data already sitting in a driver buffer, so a packet moves up and down the stack without being copied at each layer. The second is the netif abstraction, a small structure that represents one network interface. Ethernet, Wi-Fi, and a SLIP serial link are all just netifs with different send and receive functions, so the IP layer routes between them without knowing what the physical medium is.

lwIP exposes three APIs at increasing levels of convenience and cost:

API Style Needs an RTOS
raw / callback Event callbacks, zero-copy, runs in the stack's own context No
netconn Sequential message-passing, one thread per connection Yes
socket BSD sockets, familiar connect/send/recv calls Yes

The raw API is the smallest and fastest because it never blocks and never copies, but the application has to be written as a state machine driven by callbacks. The socket API is the easiest to port existing code to, at the cost of needing threads and a small amount of copying. Most embedded platforms ship all three and let the developer choose per application.

graph TD
    RAW["raw / callback API"] --> CORE
    NETCONN["netconn API"] --> CORE
    SOCK["BSD socket API"] --> NETCONN
    subgraph CORE["lwIP core"]
        TCP["TCP"] --> IP["IPv4 / IPv6"]
        UDP["UDP"] --> IP
        ICMP["ICMP"] --> IP
        IP --> NETIF["netif abstraction"]
    end
    NETIF --> SLIPNETIF["slipif over serial"]
    NETIF --> WIFINETIF["Wi-Fi driver"]

lwIP structure. Applications pick one of three APIs, and the IP core routes packets to whichever netif fits, including a serial SLIP interface and a Wi-Fi driver.

lwIP is the stack inside a large share of shipping embedded hardware. Espressif's ESP-IDF, the SDK for the ESP32 family, uses a fork of lwIP as its TCP/IP stack, which is why an ESP32 can hold TCP connections, run a DHCP client, and resolve DNS with no external help.

Putting Them Together: the SLIP Tunnel

lwIP ships a SLIP netif, usually called slipif. It is a netif whose send function SLIP-encodes an outgoing pbuf onto a serial port and whose receive function decodes incoming SLIP frames back into pbufs. To the rest of lwIP it looks like any other interface. That single driver is what makes a serial link into a routable network interface.

On the host side, the operating system has the matching half. On Linux, attaching a SLIP line discipline to a serial device (historically with slattach) creates a network interface named sl0. You assign an IP address to each end of the link and add a route, and from then on the kernel treats sl0 like any other interface: packets routed to it are SLIP-framed and written to the serial device.

graph TD
    subgraph HOST["Linux Host"]
        APP["Application"] --> KSTACK["Kernel TCP/IP stack"]
        KSTACK --> SL0["sl0 SLIP interface"]
        SL0 --> UART_H["USB serial port"]
    end
    subgraph DEV["Microcontroller"]
        UART_N["USB serial port"] --> SLIPIF["lwIP slipif netif"]
        SLIPIF --> ROUTE["lwIP IP routing"]
        ROUTE --> WIFI["Wi-Fi netif"]
    end
    UART_H --> UART_N
    WIFI --> AP["Access Point and LAN"]

The full path. An application packet is routed to sl0, SLIP-framed over USB serial, decoded by lwIP on the device, and forwarded out the Wi-Fi interface onto the real network.

The round trip is symmetric. An outbound packet leaves the host application, is routed to sl0, framed, and clocked out over USB. The device decodes it, lwIP routes it to the Wi-Fi netif, and it goes out over the air to the access point. The reply arrives on Wi-Fi, lwIP routes it back to the SLIP netif, it is framed onto the serial port, and the host kernel delivers it to the waiting application.

sequenceDiagram
    participant H as Host app
    participant K as Host sl0
    participant N as Device lwIP
    participant W as Wi-Fi network
    H->>K: IP datagram
    K->>N: SLIP-framed bytes over USB
    N->>N: slipif decodes, lwIP routes
    N->>W: forward out the Wi-Fi netif
    W->>N: reply datagram
    N->>K: SLIP-framed reply over USB
    K->>H: datagram delivered

A packet round trip across the tunnel. From the host application's point of view, sl0 is an ordinary network interface.

The BLEShark Nano as a Network Interface

The BLEShark Nano is an ESP32-C3 device, so its TCP/IP stack is lwIP by way of ESP-IDF. The InfiShark SDK uses exactly the mechanism above to expose the Nano as a real Wi-Fi adapter on your computer: the Nano joins a Wi-Fi network, a SLIP tunnel runs over the USB-C serial link, and your host gets an actual network interface whose traffic egresses through the Nano's radio.

Because it is a standard SLIP interface on the host and standard lwIP on the device, nothing above the interface has to know it is talking to a coin-sized radio. Host tools route over it like any other adapter. The Nano stays the edge radio and the host keeps the compute. That division is why the SDK drives the device from the host instead of running a full stack on the chip.

Understanding the two layers separately is what makes the behavior predictable. SLIP is the framing, with a fixed MTU and no error detection of its own, so a flaky USB cable shows up as dropped datagrams that TCP has to recover. lwIP is the stack, so connection state, DHCP, and routing all live on the device. When a tunnel misbehaves, the fault is almost always in one of those two well-defined places.

Get the BLEShark Nano - $49.99

Back to blog

1 comment

full circle. back to slip sans dialing

max

Leave a comment