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 три слоя.
- netif — сетевой интерфейс. Его заполняет файл
ethernetif.c: три функцииlow_level_init(адрес MAC, MTU, флаги),low_level_output(цепочку pbuf — в DMA) иlow_level_input(кадр из DMA — в pbuf). - Ядро —
ip4.c,etharp.c,tcp_in.c,tcp_out.c,dhcp.cи другие. Функции ядра не защищены от одновременного вызова из разных мест. - Интерфейсы для программы: raw API, netconn и сокеты (106в.5 и 106в.6).
Наш 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 на ПК:
- PBUF_RAM на 100 байт с запасом под заголовки (PBUF_TRANSPORT) занял
в куче 188 байт: 8 — служебный заголовок блока кучи, 24 —
struct pbufна 64-битном ПК, 56 — место под заголовки 14 + 20 + 20 = 54 байта, выровненное до 4, и 100 данных. На F407struct pbuf— 16 байт (так выходит из пулов в сборке armclang, 106в.4). - PBUF_REF взял из пула только заголовок,
payloadуказывает на чужие данные. pbuf_refи потомpbuf_free:refстало 1, буфер жив. Второйpbuf_freeвернул его в пул — занятых 0.pbuf_catсклеил 1000 и 2000 байт в цепочку из 3 звеньев сtot_len3000 без копирования.
Драйвер 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:
- MEMP_NUM_TCP_SEG не меньше TCP_SND_QUEUELEN;
- TCP_SND_BUF не меньше 2 · TCP_MSS;
- TCP_SNDQUEUELOWAT меньше TCP_SND_QUEUELEN (порог по умолчанию — больше из TCP_SND_QUEUELEN / 2 и 5);
- TCP_WND не больше PBUF_POOL_SIZE · (PBUF_POOL_BUFSIZE − 54): окно должно помещаться в пул приёма.
106в.4 Две конфигурации для F407: ОЗУ и код
Две lwipopts.h, обе собраны armclang для F407 (-Os) вместе с
драйвером из 106а и эхо-сервером.
- Без ОС: NO_SYS=1, только raw API, MEM_SIZE 4 КБ, 6 звеньев по 1524, 2 соединения, TCP_WND и TCP_SND_BUF по 2 · MSS.
- С FreeRTOS: числа из
lwipopts.hпримера STLwIP_HTTP_Server_Socket_RTOS— MEM_SIZE 10 КБ, 8 звеньев по 1524, 10 соединений, TCP_SND_BUF 4 · MSS; netconn и сокеты включены.
Пулы — по символам 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):
- куча — не больше 3096 из 4096 байт: ровно два сегмента в очереди отправки, по 8 + 24 + 56 + 1460 = 1548 байт (TCP_SND_BUF = 2 · MSS);
- PBUF_POOL — не больше 2 из 6 звеньев, TCP_SEG — 2 из 8;
- SYS_TIMEOUT — 5 из 5: пул по умолчанию ровно под таймеры модулей;
- отказов памяти 0.
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 Грабли
- Сборка падает на
#error lwip_sanity_check. TCP_SND_BUF = 2 · MSS и TCP_SND_QUEUELEN по формуле из примера (2 · TCP_SND_BUF / TCP_MSS = 4) — меньше порога TCP_SNDQUEUELOWAT, а он не бывает меньше 5. Нужно TCP_SND_QUEUELEN от 6 или TCP_SND_BUF побольше. - Свой
sys_timeoutвозвращает ошибку. MEMP_NUM_SYS_TIMEOUT по умолчанию — ровно число таймеров включённых модулей; в опыте без ОС занято 5 из 5. У ST вlwipopts.hстоит 10. - Соединение встало после первых килобайт. Забыли
tcp_recved: окно не открывается, и после TCP_WND байт (у нас 2920) сосед ждёт. - Raw API из чужого потока. При NO_SYS=0
tcp_writeиз потока программы безLOCK_TCPIP_COREпортит списки ядра — не сразу и не всегда. ПроверкаLWIP_ASSERT_CORE_LOCKED()в lwIP 2.1.2 есть, но по умолчанию пустая. - lwIP из прерывания. Нельзя ни в каком режиме: прерывание только
будит поток или ставит флаг главному циклу. Исключение при ОС —
tcpip_callbackmsg_trycallback_fromisr: сообщение, заготовленное заранееtcpip_callbackmsg_new, уходит вtcpip_thread. - Пул приёма простаивает. Драйвер ST принимает в свой RX_POOL, а
ядро само берёт PBUF_POOL лишь в одном месте —
udp.c, копия широковещательной датаграммы для нескольких pcb. При таком драйвере PBUF_POOL_SIZE 8 по 1524 — это 12323 байта, которые приём не трогает. pbuf_freeна голове цепочки. Он освобождает всю цепочку, где счётчик ссылок дойдёт до нуля. Хвост, который ещё нужен, сначалаpbuf_ref.- Пул NETCONN по умолчанию — 4. Слушающий netconn и слушающий сокет уже заняли два; в опыте занято 3 из 4.
106в.8 Итоги
- lwIP — это netif (
low_level_init,low_level_output,low_level_input), ядро и три интерфейса: raw API, netconn, сокеты. - Кадр 1514 байт берёт 1, 3, 6 или 12 звеньев PBUF_POOL при PBUF_POOL_BUFSIZE 1524, 512, 256, 128. Драйвер ST принимает без копии, блоками RX_POOL по 1568 байт.
- Отправка TCP копирует данные в кучу: 1548 байт на полный сегмент на ПК, TCP_SND_BUF = 2 · MSS заняло 3096 байт.
- F407, сборка armclang: lwIP без ОС — 26429 байт флеша и 14905 байт ОЗУ; с FreeRTOS и настройками ST — 35322 и 26597, плюс FreeRTOS 7392 и 25918.
- NO_SYS=1: главный цикл,
ethernetif_inputиsys_check_timeouts, только raw API. NO_SYS=0: ядро вtcpip_thread, raw API — только оттуда или подLOCK_TCPIP_CORE. - netconn и сокеты на те же 5746 сегментов данных послали около 8600 пустых ACK против 2 у raw API.
Полные исходники
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.