Embedded HandbookПриложения
Все разделы › XII. Сети и DevOps для инженера
STM32F407 · уровень средний · 25 мин чтения
Открыть в интерактивном справочнике

106б. Пакет по байтам: Ethernet, ARP, IPv4, ICMP, UDP и TCP глазами микроконтроллера

В разделе 105 наш код сам спрашивал ядро Linux — слал ARP-запрос и ping и разбирал ответы. В разделе 106 он провёл рукопожатие TCP. В разделе 106а кадр дошёл из провода в буфер F407 по кольцу дескрипторов. Теперь всё наоборот: спрашивают нас. Здесь — самый маленький стек, который отвечает на ARP, ping и UDP, без ОС и без выделения памяти. Он работает прямо в буфере, куда DMA положил кадр, и собирает ответ в буфере передачи.

Опоры: справочное руководство RM0090 (дескриптор передачи и вставка контрольных сумм), заголовок HAL stm32f4xx_hal_eth.h из пака Keil STM32F4xx_DFP, opt.h из lwIP в архиве STM32CubeF4 1.28.0. Код стека один и тот же для ПК и для F407. На ПК его собирает gcc, и он отвечает настоящему ядру Linux через интерфейс tap. Для F407 его собирает armclang; листинги — дизассемблер fromelf.

Как это называется в документации

Оригинал Перевод
EtherType поле типа кадра: 0x0800 — IPv4, 0x0806 — ARP
echo request, echo reply эхо-запрос и эхо-ответ ICMP — это и есть ping
network byte order порядок байт сети: старший вперёд
packed structure упакованная структура: поля без промежутков, адрес любой
incremental checksum update поправка суммы после замены одного слова
checksum insertion control (CIC) биты дескриптора передачи: какие суммы вставит MAC
store-and-forward (TSF) передача после накопления кадра в FIFO целиком
tap interface виртуальный интерфейс Linux: кадры Ethernet идут в программу

106б.1 Один вызов на кадр

Весь стек — одна функция. Она получает принятый кадр и буфер для ответа и возвращает длину ответа или 0:

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

Внутри — развилки по полям кадра. MAC получателя: наш или широковещательный. EtherType: ARP или IPv4. У IPv4 — сумма заголовка, адрес получателя, фрагмент ли это, и номер протокола: 1 — ICMP, 17 — UDP, 6 — TCP. На каждый выброшенный кадр — свой счётчик в net_t: чужой, неверная сумма, не разобран. Очередей, таймеров и выделения памяти нет: кадр пришёл — ответ собран — кадр ушёл.

Кадр UDP-эха по байтам: Ethernet 14 байт, IPv4 20 байт, адреса IP со смещений 26 и 30, UDP 8 байт со смещения 34, данные с 42 — в буфере, выровненном на 4, эти смещения дают адрес с остатком 2; ниже путь кадра через net_input: кольцо приёма, проверка MAC получателя, EtherType, затем ARP-ответ или IPv4 и номер протокола, ответ собирается в буфере передачи


106б.2 Заголовки: структуры, порядок байт, выравнивание

Заголовки описаны упакованными структурами. Размеры и смещения сторожит компилятор, а не комментарий:

_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, "смещения полей как в стандарте");

Порядок байт. В сети старший байт идёт первым, а Cortex-M и x86 хранят числа младшим вперёд. Переставлять можно данные при каждом чтении, а можно константу — один раз, при компиляции. Стек сравнивает поле как есть с переставленной константой: e->type == NET16(0x0806). На каждом кадре байты не переставляются вовсе.

Выравнивание. Заголовок Ethernet — 14 байт, поэтому всё, что за ним, сдвинуто на 2 от границы слова. Буфер DMA выровнен на 4, а адреса IP начинаются со смещений 26 и 30, UDP — с 34, данные — с 42: у всех остаток 2. Обычная структура обещает компилятору выровненный адрес, упакованная — нет. Разница видна в коде одной и той же функции 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

Для обычной структуры armclang склеил два чтения в одно LDRD. Невыровненный LDRD на Cortex-M4 — отказ UsageFault всегда, при любых настройках (разделы 7б и 35). Для упакованной — два LDR, их M4 выполняет с любого адреса, пока в CCR не включён бит UNALIGN_TRP. Поэтому в стеке структуры упакованы, адреса IP хранятся байтами, а 32-битные слова суммы читаются через memcpy — его armclang превращает в тот же одиночный LDR (листинг в 106б.3). У lwIP для того же есть параметр ETH_PAD_SIZE — сколько байт добавить перед заголовком Ethernet, чтобы выровнять то, что после него. По умолчанию он 0 (раздел 106в).


106б.3 Контрольная сумма: словами по 32 бита

Сумма та же, что в разделе 105: сложение с переносом по кругу и инверсия. Но складывать можно не 16-битными словами, а 32-битными. Переносы копятся в старшей половине 64-битного накопителя. Порядок байт сумме безразличен: результат, записанный в пакет как есть, уже в порядке сети.

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 развернул цикл на 4 слова. На каждое слово — LDR, ADDS и ADC: сложение и перенос в старшую половину. Ещё 4 команды — счётчик и переход на все 16 байт:

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 команд на 16 байт — 4 команды на 32-битное слово.

UDP и TCP считают сумму ещё и по псевдозаголовку (раздел 106.1). В заголовке IP адреса отправителя и получателя лежат подряд, поэтому псевдозаголовок не собирают отдельно — берут 8 байт прямо из пакета:

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));
}

Для эхо-ответа ICMP сумму вообще не пересчитывают. Ответ — копия запроса, в которой тип 8 стал 0. Сумму поправляют на разницу одного 16-битного слова (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);
}

В опыте (106б.5) у запроса в поле суммы 0x9d7b, у ответа 0xa57b. Слово «тип, код» уменьшилось на 0x0800, и поле выросло на столько же. Для ping на 1472 байта данных это правка трёх слов вместо прохода по 1480 байтам.


106б.4 Ответы: ARP, ping, UDP и сброс TCP

ARP. На запрос «у кого 10.106.2.50» отвечаем своим MAC. В ответе операция 2, отправитель — мы, искомый — тот, кто спрашивал. Ответ — 42 байта: 14 заголовка и 28 ARP.

Ping. Эхо-ответ — копия запроса: тот же идентификатор, тот же порядковый номер, те же данные, тип 0. Новый только заголовок IP — со своим номером пакета и флагом «не дробить». Сумма ICMP — поправкой из 106б.3.

UDP. Порт 7 — эхо: данные возвращаются заглавными буквами, так в опыте видно, что ответил наш код, а не ядро. Ноль в поле суммы у UDP значит «суммы нет». Такой пакет принимаем без проверки, а посчитанный ноль отправляем как 0xFFFF. Длину берём из заголовка UDP и сверяем с IP, а не с длиной кадра. Короткий кадр в проводе дополнен до 60 байт, и хвост из нулей данными не считается.

TCP. Заголовок разобран — порты, номера, длина заголовка, флаги, — но соединений нет. На любой сегмент, кроме сброса, ответ — RST, как у узла без слушающего порта. На SYN — RST+ACK с подтверждением «номер + 1»: SYN занимает один номер. Свой TCP для прибора писать не стоит. Нужны повторы по тайм-ауту, окно, порядок сегментов, номера по кругу и память на каждое соединение (раздел 106) — это и есть готовый стек. lwIP разбирается в разделе 106в.


106б.5 Опыт: тот же код против ядра Linux

Код стека не знает про железо, поэтому на ПК он собирается gcc без изменений. Вместо кольца дескрипторов у него интерфейс tap: ядро Linux отдаёт в программу кадры Ethernet целиком (IFF_TAP | IFF_NO_PI) и принимает от неё ответы как из провода. Контейнер запускается без сети (--network none), но с правом создать tap0. По одну сторону tap0 — наш стек (10.106.2.50), по другую — ядро (10.106.2.1). ARP-запросы и ping шлют программы BusyBox, UDP и TCP — сокеты ядра из Python.

Что Отправлено Ответов Время ответа, мс (мин./сред./макс.)
arping 3 3 —
ping, 56 байт данных 20 20 0,067 / 0,101 / 0,295
ping, 1472 байта данных 3 3 0,088 / 0,190 / 0,394
UDP-эхо на порт 7 20 20 0,076 / 0,111 / 0,137

Ping на 1472 байта данных — пакет IP в 1500 байт, кадр 1514 байт: самый длинный без дробления. Ядро приняло и его. Соединение TCP на порт 80 получило отказ через 0,251 мс: ядро поверило нашему RST. Время ответа — это ядро, tap и программа на ПК, а не микроконтроллер. Оно показывает только, что ответ не ждёт тайм-аута.

Потом «петля»: Python собирает кадры UDP сам, на struct и со своей суммой, и шлёт их в tap0 сырым сокетом. Верный кадр получил ответ. Кадр с нулём в поле суммы UDP — тоже. С испорченной суммой UDP, с испорченной суммой IP и с чужим MAC получателя ответов нет. Все 50 кадров нашего стека отдельный сокет поймал и пересчитал заново: 4 ARP, 23 ICMP, 22 UDP и 1 TCP, ни одной неверной суммы.

Счётчики стека после опыта: принято 60 кадров. Ответов: ARP — 4 (3 от arping и 1 от самого ядра перед первым ping), ICMP — 23, UDP — 22, RST — 1. Выброшено за неверную сумму — 2, чужих — 8: 7 кадров IPv6, которые ядро шлёт на групповые адреса, и 1 кадр петли с чужим MAC. Неразобранных — 0.

Обмен ARP байт в байт — запрос ядра и наш ответ:

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

И обмен UDP — датаграмма «stm32 udp 106b» с порта 40106 (0x9caa) на порт 7 и ответ:

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

Кадры по 56 байт: на tap дополнять до 60 некому, в проводе это сделает MAC. В ответе поменялись местами MAC, адреса IP и порты. Номер пакета стал нашим (0x011d), сумма IP — 0x20a0, сумма UDP — 0x6a8b. Ядро их проверило и отдало данные сокету.


106б.6 На F407: кольцо дескрипторов и суммы в MAC

На F407 то же ядро стека стоит между кольцом приёма из раздела 106а и одним дескриптором передачи. Опрос кольца — в главном цикле:

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;
    }
}

В длине FL из дескриптора уже есть 4 байта FCS, их отнимают. Кадр с ошибкой или разложенный по нескольким буферам пропускается. Пока передатчик не забрал прошлый ответ (OWN у дескриптора передачи), новый не собирается. Буфер приёма в любом случае возвращается DMA. Кадры с чужим MAC до кольца обычно не доходят: не прошедший фильтр адресов кадр MAC выбрасывает сам (RM0090). Проверка MAC в net_input — вторая, на случай приёма всех кадров подряд.

Суммы в MAC. Биты 23:22 CIC слова TDES0 говорят MAC, какие суммы вставить при передаче (RM0090). Значение 11 — сумма заголовка IP и сумма ICMP, UDP или TCP вместе с псевдозаголовком. В HAL это ETH_DMATXDESC_CIC_TCPUDPICMP_FULL = 0x00C00000. Два условия из RM0090:

На приёме суммы всё равно проверяет программа. Аппаратная проверка приёма (IPCO) требует расширенных дескрипторов, а кольцо 106а — из обычных.

Сборка armclang для F407 (-O2, без предупреждений; _Static_assert на размеры заголовков и биты дескрипторов):

Что Код, байт Данные, байт
ядро стека, суммы считает программа 2862 —
ядро стека, суммы вставляет MAC 2444 —
драйвер 106а, опрос кольца и передача 614 7764 обнуляемых + 44 net_t

Аппаратные суммы экономят 418 байт кода и проход по каждому передаваемому кадру. Из 7764 байт ОЗУ 6144 занимают четыре буфера приёма по 1536, ещё 1536 — буфер передачи.


106б.7 Грабли

Проверка EtherType никогда не срабатывает. Поле сравнивают с 0x0806 «как есть», а в памяти лежат байты 08 06, и Cortex-M читает их как 0x0608. Сравнивать с переставленной константой, а не переставлять поле.

UsageFault на разборе заголовка. Указатель на обычную (неупакованную) структуру поставлен на смещение 14 или 26, и компилятор склеил два чтения в LDRD. Упаковать структуру или читать через memcpy.

Ping отвечает, а UDP нет. Посчитанная сумма UDP вышла нулём и ушла как 0x0000: получатель решил, что суммы нет, — это ещё полбеды. Хуже, когда сумму считали без псевдозаголовка: ядро выбрасывает датаграмму молча.

В данные UDP попали лишние нули. Длину взяли из кадра, а короткий кадр дополнен до 60 байт. Длину брать из заголовков IP и UDP.

С CIC = 11 ping перестал отвечать. В поле суммы ICMP осталась сумма запроса: MAC требует там 0x0000. Или не выставлен TSF, и вставка молча не работает.

Биты CIC ищут не там. В обзоре вставки сумм RM0090 называет биты 28:27 слова TDES1, а в описании дескриптора и в HAL это биты 23:22 слова TDES0. Верить описанию дескриптора и маске ETH_DMATXDESC_CIC.

Ответы на TCP без конца. Стек отвечает RST на RST — два узла перебрасываются сбросами. На сегмент с RST не отвечают.


106б.8 Итоги

Полные исходники

pkt106c.h — заголовок стека: структуры кадров Ethernet, ARP, IPv4, ICMP, UDP.

/* 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 — разбор входящего кадра и ответ — ARP, ping, UDP-эхо, RST на 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 — стек на F407: кадры из кольца дескрипторов и ответ дескриптором передачи.

/* 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;
}

Дальше: раздел 106в «lwIP на STM32: буферы, три интерфейса и память» — готовый стек, который делает то же и ещё TCP, и сколько он стоит в памяти F407.

Открыть в интерактивном справочнике