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

106в. lwIP на STM32: буферы, три интерфейса и память

В разделе 106а кадры легли в кольцо дескрипторов DMA, в разделе 106б их разобрали по байтам: Ethernet, ARP, IPv4, ICMP, UDP и TCP. Всю эту работу в прошивке делает стек TCP/IP. У STM32 это обычно lwIP: его кладёт в проект CubeMX, на нём же стоят примеры ST. В разделе 156 lwIP спрятан внутри ESP-IDF. Здесь мы собираем его сами: как кадр становится буфером pbuf, какие настройки решают, сколько займёт ОЗУ, и чем отличаются три способа с ним говорить.

Опоры: исходники lwIP 2.1.2 из архива STM32Cube_FW_F4 V1.28.0 (opt.h, pbuf.c, tcp.c, api_msg.c, doc/doxygen/main_page.h) и примеры ST для платы STM324xG_EVAL — LwIP_TCP_Echo_Server (без ОС) и LwIP_HTTP_Server_Socket_RTOS (с FreeRTOS). Опыт — lwIP собран gcc 12 под Linux в контейнере и отвечает ядру Linux через интерфейс tap. Размеры — сборка armclang для F407 с драйвером из 106а. Платы в опыте нет.

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

Оригинал Перевод
netif сетевой интерфейс стека: адреса и функции приёма и передачи
ethernetif.c «клей» между драйвером MAC и lwIP в примерах ST
pbuf (packet buffer) буфер пакета; бывает цепочкой звеньев
memp пулы блоков одного размера
heap, MEM_SIZE куча lwIP — одна область, блоки разного размера
raw API, callback-style API обратные вызовы; работает без ОС
sequential API, netconn последовательный интерфейс поверх потоков
socket API сокеты в духе BSD поверх netconn
tcpip_thread поток ОС, в котором живёт ядро стека
core locking замок ядра: LOCK_TCPIP_CORE, UNLOCK_TCPIP_CORE
zero-copy приём без копирования кадра

106в.1 Из чего состоит lwIP

Схема lwIP на F407: кадр из кольца DMA через low_level_input становится pbuf, netif->input отдаёт его ядру, ядро через ip4_input и tcp_input передаёт данные одному из трёх интерфейсов — обратному вызову raw API, netconn_recv или lwip_recv; ответ через tcp_write, tcp_output и low_level_output уходит обратно в DMA

У lwIP три слоя.

Наш netif106d.c устроен так же, как ethernetif.c у ST, но проще: кадр копируется. Функция инициализации вешает две функции передачи — IP уходит через ARP, готовый кадр через драйвер:

err_t ethernetif_init(struct netif *netif)
{
    netif->name[0] = 's';
    netif->name[1] = 't';
    netif->output = etharp_output;           /* IP → ARP → кадр */
    netif->linkoutput = low_level_output;    /* кадр → провод */
    low_level_init(netif);
    return ERR_OK;
}

Кто зовёт netif->input, решает режим. Без ОС это ethernet_input: кадр разбирается сразу, в том же вызове. С ОС это tcpip_input: кадр кладётся в почтовый ящик, а разбирает его поток tcpip_thread.


106в.2 pbuf: типы, цепочки и ссылки

pbuf — заголовок (next, payload, tot_len, len, ref, тип) и данные. Длинный пакет может быть цепочкой: у каждого звена своя len, у первого tot_len — длина всего пакета. Откуда берётся память, задаёт тип.

Тип Заголовок и данные Для чего
PBUF_RAM одним куском из кучи (MEM_SIZE) всё, что отправляет TCP (tcp_write с копией)
PBUF_POOL звенья из пула PBUF_POOL по PBUF_POOL_BUFSIZE байт приём в драйвере с копированием
PBUF_ROM, PBUF_REF только заголовок из пула MEMP_NUM_PBUF, данные чужие константы во флеше, буфер DMA

Модель на ПК — сам lwIP, собранный с разным PBUF_POOL_BUFSIZE: сколько звеньев берёт кадр 1514 байт.

PBUF_POOL_BUFSIZE Кадр 1514 байт Кадр 60 байт
128 12 звеньев 1
256 6 звеньев 1
512 3 звена 1
1524 1 звено 1

Буфер 1524 байта держит полный кадр одним звеном, но короткий кадр занимает его целиком. У ST в эхо-примерах TCP и UDP без ОС PBUF_POOL_BUFSIZE 512, в остальных примерах 1524.

Другие опыты с pbuf на ПК:

Драйвер ST в CubeF4 1.28.0 принимает без копирования. В ethernetif.c объявлен свой пул RX_POOL: блок RxBuff_t — struct pbuf_custom и буфер для DMA, выровненный на 32 байта. Для F407 блок занимает 1568 байт (ETH_RX_BUF_SIZE 1528, округлено до 1536). Блоков 10 в LwIP_TCP_Echo_Server и 12 в LwIP_HTTP_Server_Socket_RTOS. HAL берёт блок в HAL_ETH_RxAllocateCallback, DMA пишет кадр прямо в него, стек получает pbuf типа PBUF_REF. pbuf_free_custom возвращает блок в RX_POOL.


106в.3 Память: куча, пулы и что значит каждое число

У lwIP два вида памяти. Куча размером MEM_SIZE отдаёт блоки любого размера: PBUF_RAM и копии данных для отправки. Пулы memp выдают блоки одного размера: pcb, сегменты, звенья PBUF_POOL. Пул не дробится, и его размер известен при сборке. Про кучи вообще — раздел 25.

Настройка Что значит (по opt.h) По умолчанию
MEM_SIZE размер кучи; больше, если много данных копируется на отправку 1600
MEMP_NUM_PBUF заголовки для PBUF_ROM и PBUF_REF 16
MEMP_NUM_TCP_PCB одновременно открытые соединения TCP 5
MEMP_NUM_TCP_PCB_LISTEN слушающие порты TCP 8
MEMP_NUM_TCP_SEG сегменты TCP в очередях 16
PBUF_POOL_SIZE звеньев в пуле PBUF_POOL 16
PBUF_POOL_BUFSIZE размер звена; по умолчанию — полный сегмент с заголовками MSS + 40 + 14
TCP_MSS наибольший сегмент TCP 536
TCP_WND окно приёма; не меньше 2 · TCP_MSS 4 · TCP_MSS
TCP_SND_BUF место в очереди отправки, байт; не меньше 2 · TCP_MSS 2 · TCP_MSS
TCP_SND_QUEUELEN то же в pbuf; не меньше 2 · TCP_SND_BUF / TCP_MSS формула

Для Ethernet берут TCP_MSS = 1500 − 40 = 1460: столько данных влезает в кадр после заголовков IP и TCP (раздел 106).

init.c проверяет настройки ещё при сборке и останавливает её через #error:


106в.4 Две конфигурации для F407: ОЗУ и код

Две lwipopts.h, обе собраны armclang для F407 (-Os) вместе с драйвером из 106а и эхо-сервером.

Пулы — по символам memp_memory_*_base из fromelf -s. К каждому пулу добавлены 3 байта на выравнивание.

Пул Блок, байт Без ОС С FreeRTOS
PBUF_POOL 16 + 1524 6 → 9243 8 → 12323
TCP_PCB 156 2 → 315 10 → 1563
TCP_PCB_LISTEN 32 1 → 35 5 → 163
TCP_SEG 16 8 → 131 12 → 195
PBUF (заголовки REF/ROM) 16 4 → 67 10 → 163
UDP_PCB 32 2 → 67 6 → 195
SYS_TIMEOUT 16 5 → 83 10 → 163
FRAG_PBUF (по умолчанию) 24 15 → 363 15 → 363
REASSDATA (по умолчанию) 32 5 → 163 5 → 163
NETCONN (по умолчанию) 44 — 4 → 179
NETBUF (по умолчанию) 16 — 2 → 35
TCPIP_MSG_API, TCPIP_MSG_INPKT 16 — 8 + 8 → 262
все пулы 10467 15767

Итог сборки (armlink --info sizes, только объекты lwIP):

Без ОС С FreeRTOS
Флеш lwIP (код + константы) 26429 35322
ОЗУ lwIP 14905 26597
из них куча ram_heap 4115 10259
из них пулы (ZI memp.o) 10503 15819
FreeRTOS: флеш / ОЗУ — 7392 / 25918

Где код без ОС: TCP — 12214 байт, DHCP — 4109, IPv4 с фрагментацией и ICMP — 3034, ARP и Ethernet — 2020. С ОС добавились netconn, сокеты и tcpip.c — 8257 байт. ОЗУ FreeRTOS почти всё — его куча configTOTAL_HEAP_SIZE 25 КБ. Из неё берутся стеки потоков: TCPIP_THREAD_STACKSIZE 1000 в примере ST — это слова FreeRTOS, 4000 байт.

Порт F407, netif106d.c и main без ОС добавляют к ОЗУ ещё 10896 байт: 4 буфера приёма и буфер передачи по 1536 байт с дескрипторами (106а), два промежуточных кадра по 1536 в netif106d.c и struct netif. Столько стоит копирование кадров.

Кусок пулов не заказывали: FRAG_PBUF и REASSDATA включены по умолчанию (IP_FRAG и IP_REASSEMBLY в opt.h равны 1) — 526 байт на фрагментацию IP, которая эхо-серверу не нужна.


106в.5 Без ОС: raw API и главный цикл

При NO_SYS=1 у lwIP нет потоков и замков. Всё вызывает главный цикл — как в main.c примера LwIP_TCP_Echo_Server:

    for (;;) {
        ethernetif_input(&nif);                /* кадры из кольца DMA — в стек */
        sys_check_timeouts();                  /* таймеры TCP, ARP, DHCP */
    }

sys_check_timeouts запускает просроченные таймеры: повторы TCP, ARP, продление DHCP. Время он берёт из sys_now() — в порту это счётчик миллисекунд SysTick. Документация lwIP (main_page.h) требует: в этом режиме lwIP никогда не зовут из прерывания. Прерывание Ethernet только кладёт кадр в очередь, а разбирает его главный цикл.

Интерфейс здесь один — raw API: программа вешает на соединение обратные вызовы, и ядро зовёт их из того же цикла. Обратный вызов приёма эхо-сервера (echo_raw.c):

static err_t echo_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
    struct pbuf *q = (struct pbuf *)arg;
    if (p == NULL) {                            /* сосед прислал FIN */
        if (q != NULL)
            pbuf_free(q);
        tcp_arg(pcb, NULL);
        return tcp_close(pcb);
    }
    if (err != ERR_OK) {
        pbuf_free(p);
        return err;
    }
    if (q == NULL)
        q = p;
    else
        pbuf_cat(q, p);                         /* в хвост очереди, без копирования */
    tcp_arg(pcb, echo_push(pcb, q));
    return ERR_OK;
}

Принятое не копируется: pbuf из стека встаёт в очередь соединения (arg). Отправляет echo_push:

static struct pbuf *echo_push(struct tcp_pcb *pcb, struct pbuf *q)
{
    while (q != NULL && q->len <= tcp_sndbuf(pcb)) {
        if (tcp_write(pcb, q->payload, q->len, TCP_WRITE_FLAG_COPY) != ERR_OK)
            break;                              /* кончилась куча или очередь сегментов */
        u16_t n = q->len;
        struct pbuf *next = q->next;
        if (next != NULL)
            pbuf_ref(next);                     /* хвост живёт дальше */
        pbuf_free(q);                           /* освобождается только голова */
        tcp_recved(pcb, n);                     /* окно — только после копирования */
        q = next;
    }
    tcp_output(pcb);
    return q;
}

tcp_write с TCP_WRITE_FLAG_COPY копирует данные в PBUF_RAM из кучи, после этого звено приёма свободно. tcp_recved сообщает стеку, что данные забраны: только тогда окно приёма снова растёт. Что не влезло в TCP_SND_BUF, ждёт обратного вызова echo_sent — сосед подтвердил приём, место освободилось.

Опыт на ПК: тот же echo_raw.c и netif106d.c, вместо кольца DMA — интерфейс tap0 ядра Linux (192.168.7.1), lwIP — 192.168.7.2. Клиент на Python со стороны Linux шлёт 8388608 байт кусками по 8192 и сверяет эхо. Три прогона, все данные вернулись без ошибок. Счётчики lwIP (LWIP_STATS):


106в.6 С ОС: tcpip_thread, netconn и сокеты

При NO_SYS=0 ядро живёт в одном потоке — tcpip_thread, его запускает tcpip_init. Поток драйвера кладёт кадры через tcpip_input в почтовый ящик, потоки программы общаются с ядром сообщениями. Порт под ОС — файл sys_arch.c: семафоры, мьютексы, почтовые ящики и потоки. У ST он сделан на обёртке CMSIS-RTOS над FreeRTOS.

raw API netconn сокеты
Нужна ОС нет да да
Где выполняется код программы в ядре: главный цикл или tcpip_thread в своём потоке в своём потоке
Как приходят данные обратный вызов с pbuf netconn_recv отдаёт netbuf lwip_recv копирует в свой буфер
Блокируется никогда да, ждёт ящик да
Заголовок lwip/tcp.h lwip/api.h lwip/sockets.h

Сокеты работают поверх netconn: lwip_recv берёт pbuf через netconn_recv_tcp_pbuf_flags и копирует данные в буфер программы.

Документация lwIP (main_page.h, раздел Multithreading) говорит прямо: функции raw API не защищены от одновременного доступа и при ОС вызываются только из tcpip_thread. Из других потоков можно звать только api.h, netbuf.h, netdb.h, netifapi.h, sockets.h, sys.h и выделение памяти. Два законных способа вызвать raw API из своего потока: tcpip_callback (функция выполнится в tcpip_thread) или замок ядра при LWIP_TCPIP_CORE_LOCKING=1 (так по умолчанию):

    LOCK_TCPIP_CORE();                     /* raw API и netif — только под замком ядра */
    netif_add(&nif, &ip, &mask, &gw, NULL, ethernetif_init, tcpip_input);
    netif_set_default(&nif);
    netif_set_link_up(&nif);
    netif_set_up(&nif);
    echo_raw_init(7);
    UNLOCK_TCPIP_CORE();

Из прерывания нельзя ни то, ни другое: замок — мьютекс ОС. Правила прерываний в RTOS — раздел 28.

Опыт: та же программа с ОС (потоки pthread вместо FreeRTOS) держит три эхо-сервера сразу: raw на порту 7, netconn на 8, сокеты на 9. Три прогона по 8388608 байт на каждый порт, все данные вернулись. Скорость на ПК — от 74,7 до 389,7 Мбит/с за все прогоны; это скорость процессора ПК, про F407 она ничего не говорит.

Говорит другое — число сегментов. Счётчик кадров на tap0 показал: данных lwIP отправил одинаково, 5746 сегментов, в среднем по 1459,9 байта. Но пустых сегментов (только ACK) у raw было 2 за прогон, а у netconn и сокетов — от 8591 до 8612, полтора на каждый сегмент данных. Причина — в tcp_recved. Если окно выросло не меньше чем на TCP_WND_UPDATE_THRESHOLD = min(TCP_WND / 4, 4 · TCP_MSS) — у нас 730 байт, — tcp_recved сразу шлёт ACK. В raw эхо данные уже стоят в очереди (tcp_write раньше tcp_recved), и ACK уходит вместе с ними. У netconn окно возвращает lwip_netconn_do_recv в момент, когда поток забрал данные, — ответа в очереди ещё нет, и ACK уходит пустым.

Счётчики с ОС: куча — до 4644 из 10240 байт (три сегмента по 1548), PBUF_POOL — до 3 из 8, NETCONN — 3 из 4 (два слушающих и одно соединение), TCPIP_MSG_INPKT — до 3 из 8.


106в.7 Грабли


106в.8 Итоги

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

netif106d.c — netif с копированием — те же три функции, что в ethernetif.c примеров ST.

/* 106в: netif с копированием — те же три функции, что в ethernetif.c примеров ST. */
#include "lwip/opt.h"
#include "lwip/netif.h"
#include "lwip/pbuf.h"
#include "lwip/etharp.h"
#include "lwip/stats.h"
#include "netif/ethernet.h"
#include "netif106d.h"

#define FRAME_MAX 1536
static uint8_t rx_frame[FRAME_MAX];      /* сюда порт кладёт кадр целиком */
static uint8_t tx_frame[FRAME_MAX];      /* сюда собирается цепочка pbuf на отправку */

static void low_level_init(struct netif *netif)
{
    port_hwaddr(netif->hwaddr);
    netif->hwaddr_len = ETH_HWADDR_LEN;
    netif->mtu = 1500;
    netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP;
}

static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
    (void)netif;
    u16_t n = pbuf_copy_partial(p, tx_frame, p->tot_len, 0);   /* цепочка → один буфер */
    port_frame_send(tx_frame, n);
    LINK_STATS_INC(link.xmit);
    return ERR_OK;
}

static struct pbuf *low_level_input(struct netif *netif)
{
    (void)netif;
    int len = port_frame_recv(rx_frame, FRAME_MAX);
    if (len <= 0)
        return NULL;
    struct pbuf *p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL);  /* цепочка из пула */
    if (p == NULL) {
        LINK_STATS_INC(link.memerr);
        LINK_STATS_INC(link.drop);
        return NULL;
    }
    pbuf_take(p, rx_frame, (u16_t)len);
    LINK_STATS_INC(link.recv);
    return p;
}

int ethernetif_input(struct netif *netif)
{
    int n = 0;
    struct pbuf *p;
    while ((p = low_level_input(netif)) != NULL) {
        n++;
        if (netif->input(p, netif) != ERR_OK)   /* ethernet_input или tcpip_input */
            pbuf_free(p);
    }
    return n;
}

err_t ethernetif_init(struct netif *netif)
{
    netif->name[0] = 's';
    netif->name[1] = 't';
    netif->output = etharp_output;           /* IP → ARP → кадр */
    netif->linkoutput = low_level_output;    /* кадр → провод */
    low_level_init(netif);
    return ERR_OK;
}

echo_raw.c — эхо-сервер TCP на raw API: только обратные вызовы, без ОС.

/* 106в: эхо-сервер TCP на raw API — только обратные вызовы, без ОС и без потоков.
   В arg соединения хранится очередь принятого, но ещё не отправленного. */
#include "lwip/tcp.h"
#include "echo_raw.h"

/* отправить из очереди, сколько влезет в буфер передачи; вернуть остаток */
static struct pbuf *echo_push(struct tcp_pcb *pcb, struct pbuf *q)
{
    while (q != NULL && q->len <= tcp_sndbuf(pcb)) {
        if (tcp_write(pcb, q->payload, q->len, TCP_WRITE_FLAG_COPY) != ERR_OK)
            break;                              /* кончилась куча или очередь сегментов */
        u16_t n = q->len;
        struct pbuf *next = q->next;
        if (next != NULL)
            pbuf_ref(next);                     /* хвост живёт дальше */
        pbuf_free(q);                           /* освобождается только голова */
        tcp_recved(pcb, n);                     /* окно — только после копирования */
        q = next;
    }
    tcp_output(pcb);
    return q;
}

static err_t echo_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
    struct pbuf *q = (struct pbuf *)arg;
    if (p == NULL) {                            /* сосед прислал FIN */
        if (q != NULL)
            pbuf_free(q);
        tcp_arg(pcb, NULL);
        return tcp_close(pcb);
    }
    if (err != ERR_OK) {
        pbuf_free(p);
        return err;
    }
    if (q == NULL)
        q = p;
    else
        pbuf_cat(q, p);                         /* в хвост очереди, без копирования */
    tcp_arg(pcb, echo_push(pcb, q));
    return ERR_OK;
}

static err_t echo_sent(void *arg, struct tcp_pcb *pcb, u16_t len)
{
    (void)len;                                  /* сосед подтвердил — место освободилось */
    if (arg != NULL)
        tcp_arg(pcb, echo_push(pcb, (struct pbuf *)arg));
    return ERR_OK;
}

static err_t echo_poll(void *arg, struct tcp_pcb *pcb)
{
    return echo_sent(arg, pcb, 0);              /* раз в секунду: вдруг место есть */
}

static void echo_err(void *arg, err_t err)
{
    (void)err;                                  /* pcb уже освобождён стеком */
    if (arg != NULL)
        pbuf_free((struct pbuf *)arg);
}

static err_t echo_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
    (void)arg;
    if (err != ERR_OK || pcb == NULL)
        return ERR_VAL;
    tcp_arg(pcb, NULL);
    tcp_recv(pcb, echo_recv);
    tcp_sent(pcb, echo_sent);
    tcp_poll(pcb, echo_poll, 2);                /* 2 × 500 мс */
    tcp_err(pcb, echo_err);
    return ERR_OK;
}

void echo_raw_init(u16_t port)
{
    struct tcp_pcb *pcb = tcp_new();
    tcp_bind(pcb, IP_ADDR_ANY, port);
    pcb = tcp_listen(pcb);                      /* слушающий pcb — из другого пула, поменьше */
    tcp_accept(pcb, echo_accept);
}

main_min.c — F407 без ОС (NO_SYS=1): главный цикл.

/* 106в: F407 без ОС (NO_SYS=1) — главный цикл, как в main.c примера ST LwIP_TCP_Echo_Server. */
#include "lwip/init.h"
#include "lwip/netif.h"
#include "lwip/timeouts.h"
#include "netif/ethernet.h"
#include "netif106d.h"
#include "echo_raw.h"

void port_start(void);
static struct netif nif;

int main(void)
{
    ip4_addr_t ip, mask, gw;
    port_start();
    lwip_init();
    IP4_ADDR(&ip, 192, 168, 7, 2);
    IP4_ADDR(&mask, 255, 255, 255, 0);
    IP4_ADDR(&gw, 192, 168, 7, 1);
    netif_add(&nif, &ip, &mask, &gw, NULL, ethernetif_init, ethernet_input);
    netif_set_default(&nif);
    netif_set_link_up(&nif);
    netif_set_up(&nif);
    echo_raw_init(7);
    for (;;) {
        ethernetif_input(&nif);                /* кадры из кольца DMA — в стек */
        sys_check_timeouts();                  /* таймеры TCP, ARP, DHCP */
    }
}

main_rtos.c — F407 с FreeRTOS (NO_SYS=0): tcpip_thread и три эхо-сервера — raw, netconn и сокеты.

/* 106в: F407 с FreeRTOS (NO_SYS=0) — tcpip_thread, три эхо-сервера: raw :7, netconn :8, сокеты :9. */
#include "lwip/tcpip.h"
#include "lwip/netif.h"
#include "lwip/api.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "netif106d.h"
#include "echo_raw.h"

void port_start(void);
static struct netif nif;

static void rx_thread(void *arg)          /* у ST — поток EthIf, будится семафором из прерывания */
{
    (void)arg;
    for (;;) {
        if (ethernetif_input(&nif) == 0)
            sys_msleep(1);
    }
}

static void netconn_echo(void *arg)
{
    (void)arg;
    struct netconn *l = netconn_new(NETCONN_TCP), *c;
    netconn_bind(l, IP_ADDR_ANY, 8);
    netconn_listen(l);
    while (netconn_accept(l, &c) == ERR_OK) {
        struct netbuf *nb;
        while (netconn_recv(c, &nb) == ERR_OK) {
            void *data;
            u16_t len;
            do {
                netbuf_data(nb, &data, &len);
                netconn_write(c, data, len, NETCONN_COPY);
            } while (netbuf_next(nb) >= 0);
            netbuf_delete(nb);
        }
        netconn_close(c);
        netconn_delete(c);
    }
}

static void socket_echo(void *arg)
{
    (void)arg;
    static char buf[1460];
    struct sockaddr_in a = { 0 };
    int l = lwip_socket(AF_INET, SOCK_STREAM, 0);
    a.sin_family = AF_INET;
    a.sin_port = lwip_htons(9);
    lwip_bind(l, (struct sockaddr *)&a, sizeof a);
    lwip_listen(l, 1);
    for (;;) {
        int c = lwip_accept(l, NULL, NULL), n;
        while ((n = lwip_recv(c, buf, sizeof buf, 0)) > 0)
            lwip_send(c, buf, (size_t)n, 0);
        lwip_close(c);
    }
}

static void start_thread(void *arg)
{
    ip4_addr_t ip, mask, gw;
    (void)arg;
    tcpip_init(NULL, NULL);
    IP4_ADDR(&ip, 192, 168, 7, 2);
    IP4_ADDR(&mask, 255, 255, 255, 0);
    IP4_ADDR(&gw, 192, 168, 7, 1);
    LOCK_TCPIP_CORE();
    netif_add(&nif, &ip, &mask, &gw, NULL, ethernetif_init, tcpip_input);
    netif_set_default(&nif);
    netif_set_link_up(&nif);
    netif_set_up(&nif);
    echo_raw_init(7);
    UNLOCK_TCPIP_CORE();
    sys_thread_new("rx", rx_thread, NULL, DEFAULT_THREAD_STACKSIZE, osPriorityRealtime);
    sys_thread_new("netconn", netconn_echo, NULL, DEFAULT_THREAD_STACKSIZE, osPriorityNormal);
    sys_thread_new("socket", socket_echo, NULL, DEFAULT_THREAD_STACKSIZE, osPriorityNormal);
    for (;;)
        sys_msleep(1000);
}

int main(void)
{
    port_start();
    sys_thread_new("start", start_thread, NULL, DEFAULT_THREAD_STACKSIZE, osPriorityNormal);
    osKernelStart();
    for (;;) {
    }
}

Дальше: раздел 106г «GSM-модем SIM800L по UART: AT-команды, SMS, звонок и TCP через GPRS — разбор ответов без блокировки» — когда провода нет и Wi-Fi не дотягивается: сотовая сеть через модем, которым управляют AT-командами по UART.

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