Embedded HandbookApps
All sections › XII. Networks and DevOps for the engineer
STM32F407 · level intermediate · 25 min read
Open in the interactive handbook

106б. A packet byte by byte: Ethernet, ARP, IPv4, ICMP, UDP and TCP as a microcontroller sees them

In section 105 our code asked the Linux kernel itself: it sent an ARP request and a ping and parsed the replies. In section 106 it performed the TCP handshake. In section 106а a frame travelled from the wire into an F407 buffer through the descriptor ring. Now it is the other way round: we are the ones being asked. Here is the smallest stack that answers ARP, ping and UDP, with no OS and no memory allocation. It works right in the buffer where DMA placed the frame and builds the reply in the transmit buffer.

References: the reference manual RM0090 (the transmit descriptor and checksum insertion), the HAL header stm32f4xx_hal_eth.h from the Keil pack STM32F4xx_DFP, opt.h from lwIP in the STM32CubeF4 1.28.0 package. The stack code is one and the same for the PC and for the F407. On the PC it is built by gcc, and it answers the real Linux kernel through a tap interface. For the F407 it is built by armclang; the listings come from the fromelf disassembler.

Terms used in the documentation

Term Meaning
EtherType the frame type field: 0x0800 is IPv4, 0x0806 is ARP
echo request, echo reply ICMP echo request and echo reply: this is what ping is
network byte order the network byte order: most significant byte first
packed structure a structure with no gaps between fields, at any address
incremental checksum update correcting the checksum after one word is replaced
checksum insertion control (CIC) transmit descriptor bits: which checksums the MAC inserts
store-and-forward (TSF) transmission starts once the whole frame is in the FIFO
tap interface a virtual Linux interface: Ethernet frames go to a program

106б.1 One call per frame

The whole stack is one function. It takes a received frame and a buffer for the reply and returns the reply length or 0:

unsigned net_input(net_t *nt, const uint8_t *f, unsigned n, uint8_t *out);

Inside are branches on the frame fields. The destination MAC: ours or broadcast. EtherType: ARP or IPv4. For IPv4: the header checksum, the destination address, whether it is a fragment, and the protocol number: 1 is ICMP, 17 is UDP, 6 is TCP. Every dropped frame has its own counter in net_t: not ours, bad checksum, not parsed. There are no queues, timers or memory allocation: a frame arrives, the reply is built, the frame goes out.

A UDP echo frame byte by byte: Ethernet 14 bytes, IPv4 20 bytes, IP addresses at offsets 26 and 30, UDP 8 bytes at offset 34, data from 42; in a buffer aligned to 4 these offsets give an address with a remainder of 2; below is the frame's path through net_input: the receive ring, the destination MAC check, EtherType, then an ARP reply or IPv4 and the protocol number, with the reply built in the transmit buffer


106б.2 Headers: structures, byte order, alignment

The headers are described as packed structures. Sizes and offsets are guarded by the compiler, not by a comment:

_Static_assert(sizeof(eth_hdr_t) == 14, "заголовок Ethernet — 14 байт");
_Static_assert(sizeof(arp_pkt_t) == 28, "ARP — 28 байт");
_Static_assert(sizeof(ip_hdr_t) == 20, "IPv4 без параметров — 20 байт");
_Static_assert(sizeof(icmp_hdr_t) == 8, "ICMP echo — 8 байт");
_Static_assert(sizeof(udp_hdr_t) == 8, "UDP — 8 байт");
_Static_assert(sizeof(tcp_hdr_t) == 20, "TCP без параметров — 20 байт");
_Static_assert(offsetof(ip_hdr_t, src) == 12 && offsetof(tcp_hdr_t, flags) == 13, "смещения полей как в стандарте");

Byte order. On the network the most significant byte comes first, while Cortex-M and x86 store numbers least significant byte first. You can swap the data on every read, or you can swap the constant, once, at compile time. The stack compares the field as it is with a byte-swapped constant: e->type == NET16(0x0806). No bytes are swapped on any frame at all.

Alignment. The Ethernet header is 14 bytes, so everything after it is shifted by 2 from a word boundary. The DMA buffer is aligned to 4, and the IP addresses start at offsets 26 and 30, UDP at 34, the data at 42: all have a remainder of 2. An ordinary structure promises the compiler an aligned address, a packed one does not. The difference shows in the code of the same function src ^ dst:

pair_xor:         LDRD  r0,r1,[r0,#0]
                  EORS  r0,r0,r1
pair_xor_packed:  LDR   r1,[r0,#0]
                  LDR   r0,[r0,#4]
                  EORS  r0,r0,r1

For the ordinary structure armclang merged the two reads into one LDRD. An unaligned LDRD on Cortex-M4 is always a UsageFault, with any settings (sections 7б and 35). For the packed one there are two LDR, which the M4 executes from any address as long as the UNALIGN_TRP bit in CCR is not set. That is why the stack's structures are packed, IP addresses are stored as bytes, and the 32-bit checksum words are read through memcpy, which armclang turns into the same single LDR (listing in 106б.3). lwIP has the ETH_PAD_SIZE parameter for the same purpose: how many bytes to add before the Ethernet header to align what comes after it. By default it is 0 (section 106в).


106б.3 The checksum: in 32-bit words

The checksum is the same as in section 105: addition with end-around carry and inversion. But you can add 32-bit words instead of 16-bit ones. The carries accumulate in the upper half of a 64-bit accumulator. Byte order does not matter to the checksum: the result, written into the packet as it is, is already in network order.

uint32_t csum_add(uint32_t acc, const void *p, size_t n)
{
    const uint8_t *b = p;
    uint64_t s = acc;
    for (; n >= 4; n -= 4, b += 4) {
        uint32_t w;
        memcpy(&w, b, 4);               /* одна LDR: невыровненный адрес Cortex-M4 допускает */
        s += w;
    }
    if (n >= 2) {
        uint16_t h;
        memcpy(&h, b, 2);
        s += h;
        b += 2;
        n -= 2;
    }
    if (n)
        s += *b;                        /* нечётный байт — старший байт последнего слова в сети */
    s = (s & 0xFFFFFFFFu) + (s >> 32);
    return (uint32_t)s + (uint32_t)(s >> 32);
}

armclang unrolled the loop by 4 words. Each word takes an LDR, an ADDS and an ADC: the addition and the carry into the upper half. 4 more instructions are the counter and the branch for all 16 bytes:

LDR   r5,[r1,#0]
LDR   r4,[r1,#4]
LDR   r3,[r1,#8]
ADDS  r0,r0,r5
LDR   r6,[r1,#0xc]
ADC   r7,r7,#0
ADDS  r0,r0,r4
ADC   r7,r7,#0
ADDS  r0,r0,r3
ADC   r3,r7,#0
ADDS  r0,r0,r6
SUB   r2,r2,#0x10
ADC   r7,r3,#0
CMP   r2,#3
ADD   r1,r1,#0x10
BHI   {pc}-0x2a

16 instructions for 16 bytes: 4 instructions per 32-bit word.

UDP and TCP also compute the checksum over the pseudo-header (section 106.1). In the IP header the source and destination addresses lie next to each other, so the pseudo-header is not assembled separately: 8 bytes are taken straight from the packet:

static uint16_t l4_sum(const ip_hdr_t *ip, const void *seg, unsigned len, uint8_t proto)
{
    uint32_t acc = csum_add(NET16(proto) + NET16(len), ip->src, 8);   /* src и dst лежат подряд */
    return csum_fin(csum_add(acc, seg, len));
}

For an ICMP echo reply the checksum is not recomputed at all. The reply is a copy of the request in which type 8 has become 0. The checksum is corrected by the difference of one 16-bit word (RFC 1624):

static uint16_t csum_update(uint16_t hc, uint16_t m, uint16_t m1)
{
    return csum_fin((uint32_t)(uint16_t)~hc + (uint16_t)~m + m1);
}

In the experiment (106б.5) the request has 0x9d7b in the checksum field, the reply 0xa57b. The word "type, code" decreased by 0x0800, and the field grew by the same amount. For a ping with 1472 bytes of data this is a fix of three words instead of a pass over 1480 bytes.


106б.4 Replies: ARP, ping, UDP and TCP reset

ARP. To the request "who has 10.106.2.50" we reply with our MAC. In the reply the operation is 2, the sender is us, the target is whoever asked. The reply is 42 bytes: 14 of header and 28 of ARP.

Ping. The echo reply is a copy of the request: the same identifier, the same sequence number, the same data, type 0. Only the IP header is new, with its own packet number and the "don't fragment" flag. The ICMP checksum is fixed with the correction from 106б.3.

UDP. Port 7 is echo: the data comes back in capital letters, so in the experiment you can see that our code answered, not the kernel. A zero in the UDP checksum field means "no checksum". Such a packet is accepted without a check, and a computed zero is sent as 0xFFFF. The length is taken from the UDP header and compared with IP, not with the frame length. A short frame on the wire is padded to 60 bytes, and the tail of zeros does not count as data.

TCP. The header is parsed (ports, numbers, header length, flags), but there are no connections. Any segment other than a reset gets an RST reply, as from a host with no listening port. A SYN gets RST+ACK acknowledging "number + 1": SYN takes up one number. Writing your own TCP for a device is not worth it. You need retransmission on timeout, a window, segment ordering, wraparound numbers and memory for each connection (section 106): that is exactly a ready-made stack. lwIP is covered in section 106в.


106б.5 Experiment: the same code against the Linux kernel

The stack code knows nothing about the hardware, so on the PC gcc builds it without changes. Instead of the descriptor ring it has a tap interface: the Linux kernel hands whole Ethernet frames to the program (IFF_TAP | IFF_NO_PI) and accepts replies from it as if from the wire. The container runs with no network (--network none), but with the right to create tap0. On one side of tap0 is our stack (10.106.2.50), on the other the kernel (10.106.2.1). ARP requests and pings are sent by BusyBox programs, UDP and TCP by kernel sockets from Python.

What Sent Replies Reply time, ms (min/avg/max)
arping 3 3 —
ping, 56 bytes of data 20 20 0.067 / 0.101 / 0.295
ping, 1472 bytes of data 3 3 0.088 / 0.190 / 0.394
UDP echo to port 7 20 20 0.076 / 0.111 / 0.137

A ping with 1472 bytes of data is a 1500-byte IP packet and a 1514-byte frame: the longest one without fragmentation. The kernel accepted it too. A TCP connection to port 80 was refused after 0.251 ms: the kernel believed our RST. The reply time is the kernel, tap and a program on the PC, not the microcontroller. It only shows that the reply does not wait for a timeout.

Then the "loop": Python builds UDP frames itself, with struct and its own checksum, and sends them into tap0 through a raw socket. A correct frame got a reply. A frame with zero in the UDP checksum field did too. With a corrupted UDP checksum, with a corrupted IP checksum and with someone else's destination MAC there were no replies. All 50 frames from our stack were caught by a separate socket and recomputed from scratch: 4 ARP, 23 ICMP, 22 UDP and 1 TCP, not a single bad checksum.

The stack counters after the experiment: 60 frames received. Replies: ARP 4 (3 from arping and 1 from the kernel itself before the first ping), ICMP 23, UDP 22, RST 1. Dropped for a bad checksum: 2, not ours: 8, namely 7 IPv6 frames that the kernel sends to multicast addresses and 1 loop frame with someone else's MAC. Not parsed: 0.

The ARP exchange byte by byte, the kernel's request and our reply:

Ethernet    ff ff ff ff ff ff 02 00 00 00 00 01 08 06
ARP         00 01 08 00 06 04 00 01
отправитель 02 00 00 00 00 01 0a 6a 02 01
искомый     ff ff ff ff ff ff 0a 6a 02 32

Ethernet    02 00 00 00 00 01 02 00 00 00 01 06 08 06
ARP         00 01 08 00 06 04 00 02
отправитель 02 00 00 00 01 06 0a 6a 02 32
искомый     02 00 00 00 00 01 0a 6a 02 01

And the UDP exchange: the datagram "stm32 udp 106b" from port 40106 (0x9caa) to port 7 and the reply:

Ethernet    02 00 00 00 01 06 02 00 00 00 00 01 08 00
IPv4        45 00 00 2a 95 08 40 00 40 11 8c b4 0a 6a 02 01 0a 6a 02 32
UDP         9c aa 00 07 00 16 ea 2a
данные      73 74 6d 33 32 20 75 64 70 20 31 30 36 62

Ethernet    02 00 00 00 00 01 02 00 00 00 01 06 08 00
IPv4        45 00 00 2a 01 1d 40 00 40 11 20 a0 0a 6a 02 32 0a 6a 02 01
UDP         00 07 9c aa 00 16 6a 8b
данные      53 54 4d 33 32 20 55 44 50 20 31 30 36 42

The frames are 56 bytes each: on tap nobody pads them to 60, on the wire the MAC does it. In the reply the MAC addresses, IP addresses and ports swapped places. The packet number became ours (0x011d), the IP checksum is 0x20a0, the UDP checksum is 0x6a8b. The kernel checked them and passed the data to the socket.


106б.6 On the F407: the descriptor ring and checksums in the MAC

On the F407 the same stack core sits between the receive ring from section 106а and a single transmit descriptor. The ring is polled in the main loop:

void eth_poll(void)
{
    eth_desc_t *d = &rx_ring[rx_cur];
    uint32_t s;
    while (!((s = d->des0) & OWN)) {
        unsigned n = eth_rx_len(d) - 4u;                    /* FL — вместе с FCS */
        if ((s & (RX_ES | RX_FS | RX_LS)) == (RX_FS | RX_LS) && n <= BUF_SIZE && !(tx_desc.des0 & OWN)) {
            unsigned r = net_input(&net, d->buf, n, (uint8_t *)tx_buf);
            if (r) {
                tx_desc.des1 = r;                           /* до 60 байт дополнит и FCS добавит MAC */
                tx_desc.des0 = OWN | TX_FS | TX_LS | TX_TCH | (NET_HW_CSUM ? TX_CIC : 0u);
                ETH->DMATPDR = 0;                           /* передатчик: «есть кадр» */
            }
        }
        d->des0 = OWN;                                      /* буфер — обратно DMA */
        rx_cur = (rx_cur + 1u) % NRX;
        d = &rx_ring[rx_cur];
    }
    if (ETH->DMASR & ETH_DMASR_RBUS) {                      /* приём стоял: свободных буферов не было */
        ETH->DMASR = ETH_DMASR_RBUS;
        ETH->DMARPDR = 0;
    }
}

The length FL from the descriptor already includes the 4 FCS bytes, so they are subtracted. A frame with an error, or one spread over several buffers, is skipped. Until the transmitter has taken the previous reply (OWN in the transmit descriptor), a new one is not built. The receive buffer is returned to DMA in any case. Frames with someone else's MAC usually do not reach the ring: a frame that fails the address filter is dropped by the MAC itself (RM0090). The MAC check in net_input is a second one, in case all frames are received.

Checksums in the MAC. Bits 23:22 CIC of the TDES0 word tell the MAC which checksums to insert on transmission (RM0090). The value 11 means the IP header checksum and the ICMP, UDP or TCP checksum including the pseudo-header. In HAL this is ETH_DMATXDESC_CIC_TCPUDPICMP_FULL = 0x00C00000. Two conditions from RM0090:

On receive the program checks the checksums anyway. Hardware receive checking (IPCO) requires enhanced descriptors, and the 106а ring is made of normal ones.

The armclang build for the F407 (-O2, no warnings; _Static_assert on header sizes and descriptor bits):

What Code, bytes Data, bytes
stack core, checksums computed by the program 2862 —
stack core, checksums inserted by the MAC 2444 —
106а driver, ring polling and transmission 614 7764 zero-initialized + 44 net_t

Hardware checksums save 418 bytes of code and a pass over every transmitted frame. Of the 7764 bytes of RAM, 6144 are taken by four receive buffers of 1536 each, another 1536 by the transmit buffer.


106б.7 Pitfalls

The EtherType check never matches. The field is compared with 0x0806 "as it is", but memory holds the bytes 08 06, and Cortex-M reads them as 0x0608. Compare with a byte-swapped constant instead of swapping the field.

UsageFault while parsing a header. A pointer to an ordinary (unpacked) structure is set to offset 14 or 26, and the compiler merged two reads into an LDRD. Pack the structure or read through memcpy.

Ping answers, but UDP does not. The computed UDP checksum came out as zero and went out as 0x0000: the receiver decided there is no checksum, which is only half the trouble. Worse is when the checksum was computed without the pseudo-header: the kernel silently drops the datagram.

Extra zeros ended up in the UDP data. The length was taken from the frame, and a short frame is padded to 60 bytes. Take the length from the IP and UDP headers.

With CIC = 11 ping stopped answering. The ICMP checksum field still holds the checksum of the request: the MAC requires 0x0000 there. Or TSF is not set, and insertion silently does not work.

The CIC bits are looked for in the wrong place. In the checksum insertion overview RM0090 names bits 28:27 of the TDES1 word, while in the descriptor description and in HAL these are bits 23:22 of the TDES0 word. Trust the descriptor description and the ETH_DMATXDESC_CIC mask.

Endless TCP replies. The stack answers an RST with an RST, and two hosts keep tossing resets back and forth. Do not answer a segment that carries RST.


106б.8 Summary

Full sources

pkt106c.h is the stack header: Ethernet, ARP, IPv4, ICMP and UDP frame structures.

/* 106б: минимальный стек без ОС — Ethernet II, ARP, IPv4, ICMP echo, UDP и ответ RST на TCP.
   Без привязки к железу: собирается gcc под Linux (tap) и armclang под STM32F407. */
#ifndef PKT106C_H
#define PKT106C_H
#include <stddef.h>
#include <stdint.h>

#define PACKED __attribute__((packed))

#ifndef NET_HW_CSUM
#define NET_HW_CSUM 0                   /* 1 — суммы при передаче вставляет MAC F407 (CIC = 11), поля — нули */
#endif

_Static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, "код рассчитан на младший байт вперёд: x86 и Cortex-M");

/* порядок байт: в сети — старший вперёд; переставляем константы при компиляции, а не данные */
#define NET16(x) ((uint16_t)((((x) & 0xFFu) << 8) | (((x) >> 8) & 0xFFu)))

typedef struct PACKED {
    uint8_t  dst[6], src[6];
    uint16_t type;                      /* EtherType: 0x0800 — IPv4, 0x0806 — ARP */
} eth_hdr_t;

typedef struct PACKED {
    uint16_t htype, ptype;              /* 1 — Ethernet, 0x0800 — IPv4 */
    uint8_t  hlen, plen;                /* 6 и 4 */
    uint16_t op;                        /* 1 — запрос, 2 — ответ */
    uint8_t  sha[6], spa[4];            /* MAC и IP отправителя */
    uint8_t  tha[6], tpa[4];            /* MAC и IP искомого */
} arp_pkt_t;

typedef struct PACKED {
    uint8_t  vihl, tos;                 /* 0x45: версия 4, заголовок 5 слов */
    uint16_t len, id, frag;             /* frag: флаги и смещение фрагмента */
    uint8_t  ttl, proto;                /* proto: 1 — ICMP, 6 — TCP, 17 — UDP */
    uint16_t sum;
    uint8_t  src[4], dst[4];            /* адреса — байтами: не читать словом с невыровненного адреса */
} ip_hdr_t;

typedef struct PACKED {
    uint8_t  type, code;                /* 8 — эхо-запрос, 0 — эхо-ответ */
    uint16_t sum, id, seq;
} icmp_hdr_t;

typedef struct PACKED {
    uint16_t sport, dport, len, sum;
} udp_hdr_t;

typedef struct PACKED {
    uint16_t sport, dport;
    uint32_t seq, ack;
    uint8_t  off, flags;                /* off: длина заголовка в словах в старших 4 битах */
    uint16_t win, sum, urg;
} tcp_hdr_t;

_Static_assert(sizeof(eth_hdr_t) == 14, "заголовок Ethernet — 14 байт");
_Static_assert(sizeof(arp_pkt_t) == 28, "ARP — 28 байт");
_Static_assert(sizeof(ip_hdr_t) == 20, "IPv4 без параметров — 20 байт");
_Static_assert(sizeof(icmp_hdr_t) == 8, "ICMP echo — 8 байт");
_Static_assert(sizeof(udp_hdr_t) == 8, "UDP — 8 байт");
_Static_assert(sizeof(tcp_hdr_t) == 20, "TCP без параметров — 20 байт");
_Static_assert(offsetof(ip_hdr_t, src) == 12 && offsetof(tcp_hdr_t, flags) == 13, "смещения полей как в стандарте");

#define TCP_FIN 0x01u
#define TCP_SYN 0x02u
#define TCP_RST 0x04u
#define TCP_ACK 0x10u

typedef struct {
    uint8_t  mac[6], ip[4];
    uint16_t ip_id;                     /* номер следующего своего пакета */
    uint32_t rx, arp, icmp, udp, tcp_rst, bad_sum, not_ours, dropped;
} net_t;

uint32_t csum_add(uint32_t acc, const void *p, size_t n);
uint16_t csum_fin(uint32_t acc);
unsigned udp_build(net_t *nt, uint8_t *out, const uint8_t dmac[6], const uint8_t dip[4],
                   uint16_t sport, uint16_t dport, const uint8_t *data, unsigned n);
unsigned net_input(net_t *nt, const uint8_t *f, unsigned n, uint8_t *out);

#endif

pkt106c.c parses an incoming frame and replies: ARP, ping, UDP echo, RST to TCP.

/* 106б: разбор входящего кадра и ответ — ARP, ping, UDP-эхо, RST на TCP. Без ОС, без malloc, без железа.
   out — буфер передачи не меньше принятого кадра (1514 байт хватает на любой). */
#include <string.h>
#include "pkt106c.h"

#define NET32(x)   __builtin_bswap32(x)
#define ETH_ARP    NET16(0x0806)
#define ETH_IP     NET16(0x0800)
#define ECHO_PORT  7u

static const uint8_t BCAST[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

/* сумма в обратном коде (RFC 1071) словами по 32 бита в порядке процессора; порядок байт
   для этой суммы не важен — результат, записанный в пакет как есть, уже в порядке сети */
uint32_t csum_add(uint32_t acc, const void *p, size_t n)
{
    const uint8_t *b = p;
    uint64_t s = acc;
    for (; n >= 4; n -= 4, b += 4) {
        uint32_t w;
        memcpy(&w, b, 4);               /* одна LDR: невыровненный адрес Cortex-M4 допускает */
        s += w;
    }
    if (n >= 2) {
        uint16_t h;
        memcpy(&h, b, 2);
        s += h;
        b += 2;
        n -= 2;
    }
    if (n)
        s += *b;                        /* нечётный байт — старший байт последнего слова в сети */
    s = (s & 0xFFFFFFFFu) + (s >> 32);
    return (uint32_t)s + (uint32_t)(s >> 32);
}

uint16_t csum_fin(uint32_t acc)
{
    acc = (acc & 0xFFFFu) + (acc >> 16);
    acc = (acc & 0xFFFFu) + (acc >> 16);
    return (uint16_t)~acc;
}

/* RFC 1624: сумма после замены одного 16-битного слова m на m1 — без прохода по данным */
static uint16_t csum_update(uint16_t hc, uint16_t m, uint16_t m1)
{
    return csum_fin((uint32_t)(uint16_t)~hc + (uint16_t)~m + m1);
}

/* сумма UDP и TCP: псевдозаголовок (адреса, протокол, длина) и сам сегмент */
static uint16_t l4_sum(const ip_hdr_t *ip, const void *seg, unsigned len, uint8_t proto)
{
    uint32_t acc = csum_add(NET16(proto) + NET16(len), ip->src, 8);   /* src и dst лежат подряд */
    return csum_fin(csum_add(acc, seg, len));
}

static void eth_fill(uint8_t *out, const uint8_t dst[6], const uint8_t src[6], uint16_t type)
{
    eth_hdr_t *e = (eth_hdr_t *)out;
    memcpy(e->dst, dst, 6);
    memcpy(e->src, src, 6);
    e->type = type;
}

static void ip_fill(net_t *nt, ip_hdr_t *ip, const uint8_t dst[4], uint8_t proto, unsigned l4len)
{
    ip->vihl  = 0x45;
    ip->tos   = 0;
    ip->len   = NET16(20u + l4len);
    ip->id    = NET16(nt->ip_id);
    nt->ip_id++;
    ip->frag  = NET16(0x4000);          /* «не дробить» */
    ip->ttl   = 64;
    ip->proto = proto;
    ip->sum   = 0;
    memcpy(ip->src, nt->ip, 4);
    memcpy(ip->dst, dst, 4);
    ip->sum   = NET_HW_CSUM ? 0 : csum_fin(csum_add(0, ip, 20));
}

unsigned udp_build(net_t *nt, uint8_t *out, const uint8_t dmac[6], const uint8_t dip[4],
                   uint16_t sport, uint16_t dport, const uint8_t *data, unsigned n)
{
    ip_hdr_t  *ip = (ip_hdr_t *)(out + 14);
    udp_hdr_t *u  = (udp_hdr_t *)(out + 34);
    if (data)                           /* NULL — данные уже на месте, в out + 42 */
        memcpy(out + 42, data, n);
    eth_fill(out, dmac, nt->mac, ETH_IP);
    ip_fill(nt, ip, dip, 17, 8u + n);
    u->sport = NET16(sport);
    u->dport = NET16(dport);
    u->len   = NET16(8u + n);
    u->sum   = 0;
    uint16_t c = NET_HW_CSUM ? 0 : l4_sum(ip, u, 8u + n, 17);
    u->sum   = (c || NET_HW_CSUM) ? c : 0xFFFFu;    /* ноль в поле значит «суммы нет» */
    return 42u + n;
}

static unsigned arp_input(net_t *nt, const uint8_t *f, unsigned n, uint8_t *out)
{
    const arp_pkt_t *a = (const arp_pkt_t *)(f + 14);
    if (n < 14 + 28 || a->htype != NET16(1) || a->ptype != ETH_IP || a->hlen != 6 || a->plen != 4) {
        nt->dropped++;
        return 0;
    }
    if (a->op != NET16(1) || memcmp(a->tpa, nt->ip, 4)) {
        nt->not_ours++;                 /* спрашивают не нас или это чужой ответ */
        return 0;
    }
    arp_pkt_t *r = (arp_pkt_t *)(out + 14);
    eth_fill(out, a->sha, nt->mac, ETH_ARP);
    r->htype = NET16(1);
    r->ptype = ETH_IP;
    r->hlen  = 6;
    r->plen  = 4;
    r->op    = NET16(2);
    memcpy(r->sha, nt->mac, 6);
    memcpy(r->spa, nt->ip, 4);
    memcpy(r->tha, a->sha, 6);
    memcpy(r->tpa, a->spa, 4);
    nt->arp++;
    return 14 + 28;
}

static unsigned icmp_input(net_t *nt, const eth_hdr_t *e, const ip_hdr_t *ip, const uint8_t *l4, unsigned len, uint8_t *out)
{
    const icmp_hdr_t *q = (const icmp_hdr_t *)l4;
    if (len < 8 || q->type != 8 || q->code != 0) {
        nt->dropped++;
        return 0;
    }
    if (csum_fin(csum_add(0, q, len)) != 0) {
        nt->bad_sum++;
        return 0;
    }
    icmp_hdr_t *r = (icmp_hdr_t *)(out + 34);
    uint16_t m, m1;
    memcpy(r, q, len);                  /* ответ — копия запроса: id, номер и данные те же */
    memcpy(&m, r, 2);
    r->type = 0;
    memcpy(&m1, r, 2);
    r->sum = NET_HW_CSUM ? 0 : csum_update(q->sum, m, m1);  /* тип 8 → 0: поправить, а не считать заново */
    eth_fill(out, e->src, nt->mac, ETH_IP);
    ip_fill(nt, (ip_hdr_t *)(out + 14), ip->src, 1, len);
    nt->icmp++;
    return 34u + len;
}

static unsigned udp_input(net_t *nt, const eth_hdr_t *e, const ip_hdr_t *ip, const uint8_t *l4, unsigned len, uint8_t *out)
{
    const udp_hdr_t *u = (const udp_hdr_t *)l4;
    unsigned ul = len >= 8 ? NET16(u->len) : 0;
    if (ul < 8 || ul > len) {
        nt->dropped++;
        return 0;
    }
    if (u->sum && l4_sum(ip, u, ul, 17) != 0) {     /* ноль — отправитель сумму не считал */
        nt->bad_sum++;
        return 0;
    }
    if (u->dport != NET16(ECHO_PORT)) {
        nt->dropped++;
        return 0;
    }
    unsigned n = ul - 8;
    for (unsigned i = 0; i < n; i++) {      /* эхо заглавными буквами — видно, что ответил наш код */
        uint8_t c = l4[8 + i];
        out[42 + i] = (c >= 'a' && c <= 'z') ? (uint8_t)(c - 32) : c;
    }
    nt->udp++;
    return udp_build(nt, out, e->src, ip->src, ECHO_PORT, NET16(u->sport), NULL, n);
}

/* своего TCP нет: на любой сегмент — сброс, как у узла без слушающего порта */
static unsigned tcp_input(net_t *nt, const eth_hdr_t *e, const ip_hdr_t *ip, const uint8_t *l4, unsigned len, uint8_t *out)
{
    const tcp_hdr_t *t = (const tcp_hdr_t *)l4;
    unsigned hl = len >= 20 ? (t->off >> 4) * 4u : 0;
    if (hl < 20 || hl > len || (t->flags & TCP_RST)) {
        nt->dropped++;                  /* на сброс не отвечают */
        return 0;
    }
    if (l4_sum(ip, t, len, 6) != 0) {
        nt->bad_sum++;
        return 0;
    }
    tcp_hdr_t *r = (tcp_hdr_t *)(out + 34);
    memset(r, 0, sizeof *r);
    r->sport = t->dport;
    r->dport = t->sport;
    r->off   = 5u << 4;
    if (t->flags & TCP_ACK) {
        r->seq   = t->ack;
        r->flags = TCP_RST;
    } else {                            /* SYN и FIN занимают по номеру */
        uint32_t used = len - hl + !!(t->flags & TCP_SYN) + !!(t->flags & TCP_FIN);
        r->ack   = NET32(NET32(t->seq) + used);
        r->flags = TCP_RST | TCP_ACK;
    }
    eth_fill(out, e->src, nt->mac, ETH_IP);
    ip_fill(nt, (ip_hdr_t *)(out + 14), ip->src, 6, 20);
    r->sum = NET_HW_CSUM ? 0 : l4_sum((const ip_hdr_t *)(out + 14), r, 20, 6);
    nt->tcp_rst++;
    return 54;
}

static unsigned ip_input(net_t *nt, const uint8_t *f, unsigned n, uint8_t *out)
{
    const eth_hdr_t *e = (const eth_hdr_t *)f;
    const ip_hdr_t *ip = (const ip_hdr_t *)(f + 14);
    unsigned tot = n >= 34 ? NET16(ip->len) : 0;
    if (tot < 20 || tot > n - 14 || ip->vihl != 0x45) {     /* параметры IP не поддерживаем */
        nt->dropped++;
        return 0;
    }
    if (csum_fin(csum_add(0, ip, 20)) != 0) {
        nt->bad_sum++;
        return 0;
    }
    if (memcmp(ip->dst, nt->ip, 4) || (e->src[0] & 1u)) {
        nt->not_ours++;
        return 0;
    }
    if (ip->frag & NET16(0x3FFF)) {     /* фрагменты не собираем */
        nt->dropped++;
        return 0;
    }
    const uint8_t *l4 = f + 34;         /* дополнение кадра до 60 байт отбрасывает длина из IP */
    switch (ip->proto) {
    case 1:
        return icmp_input(nt, e, ip, l4, tot - 20, out);
    case 17:
        return udp_input(nt, e, ip, l4, tot - 20, out);
    case 6:
        return tcp_input(nt, e, ip, l4, tot - 20, out);
    default:
        nt->dropped++;
        return 0;
    }
}

unsigned net_input(net_t *nt, const uint8_t *f, unsigned n, uint8_t *out)
{
    const eth_hdr_t *e = (const eth_hdr_t *)f;
    nt->rx++;
    if (n < sizeof *e) {
        nt->dropped++;
        return 0;
    }
    if (memcmp(e->dst, nt->mac, 6) && memcmp(e->dst, BCAST, 6)) {
        nt->not_ours++;                 /* MAC обычно отсеивает такие сам — фильтр по адресу */
        return 0;
    }
    if (e->type == ETH_ARP)
        return arp_input(nt, f, n, out);
    if (e->type == ETH_IP)
        return ip_input(nt, f, n, out);
    nt->dropped++;                      /* IPv6, VLAN и прочее — не наше */
    return 0;
}

pkt106c_f407.c is the stack on the F407: frames from the descriptor ring and the reply through the transmit descriptor.

/* 106б на F407: кадры из кольца дескрипторов 106а — в net_input, ответ — дескриптором передачи.
   armclang -DSTM32F407xx -include stm32f4xx.h -I _verify/eth106a; ядро стека — pkt106c.c. */
#include "eth106a.c"                    /* кольцо приёма rx_ring, OWN, NRX, BUF_SIZE, eth_rx_len — раздел 106а */
#include "pkt106c.h"

#define RX_ES   (1u << 15)              /* RDES0: сводка ошибок */
#define RX_FS   (1u << 9)               /* первый буфер кадра */
#define RX_LS   (1u << 8)               /* последний буфер кадра */
#define TX_LS   (1u << 29)              /* TDES0 */
#define TX_FS   (1u << 28)
#define TX_CIC  (3u << 22)              /* суммы IP и ICMP/UDP/TCP с псевдозаголовком — считает MAC */
#define TX_TCH  (1u << 20)              /* next — адрес следующего дескриптора */

_Static_assert(TX_CIC == 0x00C00000u && TX_TCH == 0x00100000u && TX_FS == 0x10000000u, "TDES0: CIC 23:22, TCH 20, FS 28");
_Static_assert(RX_ES == 0x8000u && RX_FS == 0x200u && RX_LS == 0x100u, "RDES0: ES 15, FS 9, LS 8");

static eth_desc_t tx_desc;
static uint32_t tx_buf[BUF_SIZE / 4];
static uint32_t rx_cur;
static net_t net = {.mac = {0x02, 0x00, 0x00, 0x00, 0x01, 0x06}, .ip = {10, 106, 2, 50}};

void eth_tx_init(void)
{
    tx_desc.buf  = (uint8_t *)tx_buf;
    tx_desc.next = &tx_desc;            /* кольцо из одного дескриптора */
    tx_desc.des0 = TX_TCH;              /* OWN = 0: дескриптор у программы */
    ETH->DMATDLAR = (uint32_t)&tx_desc;
    if (NET_HW_CSUM)
        ETH->DMAOMR |= ETH_DMAOMR_TSF;  /* вставка сумм работает только с накоплением кадра целиком */
}

void eth_poll(void)
{
    eth_desc_t *d = &rx_ring[rx_cur];
    uint32_t s;
    while (!((s = d->des0) & OWN)) {
        unsigned n = eth_rx_len(d) - 4u;                    /* FL — вместе с FCS */
        if ((s & (RX_ES | RX_FS | RX_LS)) == (RX_FS | RX_LS) && n <= BUF_SIZE && !(tx_desc.des0 & OWN)) {
            unsigned r = net_input(&net, d->buf, n, (uint8_t *)tx_buf);
            if (r) {
                tx_desc.des1 = r;                           /* до 60 байт дополнит и FCS добавит MAC */
                tx_desc.des0 = OWN | TX_FS | TX_LS | TX_TCH | (NET_HW_CSUM ? TX_CIC : 0u);
                ETH->DMATPDR = 0;                           /* передатчик: «есть кадр» */
            }
        }
        d->des0 = OWN;                                      /* буфер — обратно DMA */
        rx_cur = (rx_cur + 1u) % NRX;
        d = &rx_ring[rx_cur];
    }
    if (ETH->DMASR & ETH_DMASR_RBUS) {                      /* приём стоял: свободных буферов не было */
        ETH->DMASR = ETH_DMASR_RBUS;
        ETH->DMARPDR = 0;
    }
}

/* выравнивание: адреса IP в кадре — со смещения 26, не кратного 4 */
typedef struct {
    uint32_t src, dst;
} ip_pair_t;                            /* обычная структура: компилятор считает адрес кратным 4 */

typedef struct PACKED {
    uint32_t src, dst;
} ip_pair_packed_t;                     /* упакованная: адрес может быть любым */

uint32_t pair_xor(const ip_pair_t *p)
{
    return p->src ^ p->dst;
}

uint32_t pair_xor_packed(const ip_pair_packed_t *p)
{
    return p->src ^ p->dst;
}

Next: section 106в "lwIP on STM32: buffers, three APIs and memory": a ready-made stack that does the same plus TCP, and how much F407 memory it costs.

Open in the interactive handbook