Нежданно-негаданно, благодаря теме на форуме, произошел возврат к одной из предыдущих тем, а именно к связке STM32 и ENC28J60 и Ethernet. И в этой, пятой, части обработаем принятый UDP-пакет и отправим на него ответ. Причем в отличие от предыдущих частей я не буду делать обширную врезку с теорией касаемо UDP, может быть потом, в отдельной статье, но вряд ли ) Просто и кратко дополним созданный ранее проект необходимым функционалом и посмотрим на результат.
Но для начала освежим в памяти предыдущие части:
- Технология Ethernet. Обзор, описание, формат кадра.
- STM32 и Ethernet. Часть 1. Подключение и настройка ENC28J60.
- STM32 и Ethernet. Часть 2. ENC28J60. Прием и передача кадров.
- STM32 и Ethernet. Часть 3. Канальный уровень. Протокол ARP.
- STM32 и Ethernet. Часть 4. Сетевой уровень. Протоколы IP и ICMP.
Итак, протокол UDP относится к транспортному уровню, а значит мы снова движемся вверх по модели OSI:

Отличительной особенностью UDP является то, что для обмена сообщениями-датаграммами устройствам не требуется никакой предварительной подготовки. То есть никаких установок соединений, рукопожатий, открытия каналов связи и тому подобного. Данные могут спокойно потеряться по пути, порядок следования датаграмм может быть нарушен, все это не контролируется. Возникает резонный вопрос - зачем тогда это нужно? А отчет достаточно прост - например, для очень чувствительных ко времени систем, систем реального времени. В таком случае в жертву приносятся дополнительные проверки и контроль, из-за чего и получаем выигрыш во времени. Если потери пакетов, ошибки, дублирования критичны, то тут уже вступает в дело, к примеру, TCP, но сегодня не об этом.
Для обмена данными по UDP необходимо знать ip-адрес и порт участников обмена. Порт представляет из себя целое число от 0 до 65535 (16-ти битное значение). Собственно, в практическом примере увидим этот механизм наглядно.
Каждая датаграмма включает в себя заголовок и непосредственно данные:

Пробежимся по полям заголовка:
- Source port - порт отправителя
- Destination port - порт получателя
- Length - длина датаграммы, причем включает в себя и заголовок и данные(!)
- Checksum - контрольная сумма
При этом для IPv4 контрольная сумма и порт отправителя являются необязательными полями. Все, что нам потребуется на практике, выяснили, добавляем в проект два новых файла:
- udp.c
- udp.h

Первым делом, определим структуру для UDP датаграмм в udp.h, выглядит она следующим образом:
typedef struct UDP_Frame
{
uint16_t srcPort;
uint16_t destPort;
uint16_t len;
uint16_t checkSum;
uint8_t data[];
} UDP_Frame;
Прекрасно, здесь же зададим номер порта, который используем для тестирования:
#define UDP_DEMO_PORT 33333
Для примера реализуем простой механизм: инкрементируем все байты в принятой датаграмме и отправим обратно уже измененными. Материализуется все в следующий код:
/*----------------------------------------------------------------------------*/
uint16_t UDP_Process(UDP_Frame* udpFrame, uint16_t frameLen)
{
uint16_t newFrameLen = 0;
uint16_t destPort = ntohs(udpFrame->destPort);
uint16_t len = ntohs(udpFrame->len);
uint16_t dataLen = len - sizeof(UDP_Frame);
if (destPort == UDP_DEMO_PORT)
{
for(uint16_t i = 0 ; i < dataLen - 1; i++)
{
udpFrame->data[i]++;
}
}
uint16_t swapPort = udpFrame->destPort;
udpFrame->destPort = udpFrame->srcPort;
udpFrame->srcPort = swapPort;
udpFrame->checkSum = 0;
newFrameLen = len;
return newFrameLen;
}
/*----------------------------------------------------------------------------*/
Здесь мы предварительно проверяем, что это наш пакет, то есть Destination port в заголовке соответствует тому, который мы для себя выбрали - UDP_DEMO_PORT. Вызов же этой функции поместим в обработчик принятых пакетов на сетевом уровне, то есть в IP_Process():
/*----------------------------------------------------------------------------*/
uint16_t IP_Process(IP_Frame* ipFrame, uint16_t frameLen)
{
uint16_t newFrameLen = 0;
if (memcmp(ipFrame->destIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM) == 0)
{
uint16_t rxCheckSum = ipFrame->checkSum;
ipFrame->checkSum = 0;
uint16_t calcCheckSum = IP_CalcCheckSum((uint8_t*)ipFrame, sizeof(IP_Frame));
if (rxCheckSum == calcCheckSum)
{
uint16_t dataLen = frameLen - sizeof(IP_Frame);
uint16_t newDataLen = 0;
if (ipFrame->protocol == IP_FRAME_PROTOCOL_ICMP)
{
newDataLen = ICMP_Process((ICMP_EchoFrame*)ipFrame->data, dataLen);
}
if (ipFrame->protocol == IP_FRAME_PROTOCOL_UDP)
{
newDataLen = UDP_Process((UDP_Frame*)ipFrame->data, dataLen);
}
newFrameLen = newDataLen + sizeof(IP_Frame);
ipFrame->len = htons(newFrameLen);
ipFrame->fragId = 0;
ipFrame->fragOffset = 0;
memcpy(ipFrame->destIpAddr, ipFrame->srcIpAddr, IP_ADDRESS_BYTES_NUM);
memcpy(ipFrame->srcIpAddr, ipAddr, IP_ADDRESS_BYTES_NUM);
ipFrame->checkSum = IP_CalcCheckSum((uint8_t*)ipFrame, sizeof(IP_Frame));
}
}
return newFrameLen;
}
/*----------------------------------------------------------------------------*/
IP_FRAME_PROTOCOL_UDP соответственно определяем в ip.h:
/* Declarations and definitions ----------------------------------------------*/ #define IP_FRAME_PROTOCOL_ICMP 1 #define IP_FRAME_PROTOCOL_UDP 17
В целом, все прекрасно вписалось в ту изначальную архитектуру, которую мы запланировали. Вроде бы я ничего не забыл, компилируем, прошиваем и можно переходить к проверке.
Для тестирования я использую утилиту ncat, в командной строке выполняем:
.\ncat.exe -u 169.254.191.22 33333
Ключ -u сигнализирует о том, что будет использован UDP, далее следует IP-адрес платы и номер порта получателя. Получателями же и являются в данном случае STM32 и ENC28J60. После ввода команды можем отправлять сами данные:

В результате видим, что работает, как и задумано - отправленные данные инкрементированы и высланы обратно (две нижние строки, первая - отправленные, вторая - принятые). Вот на этом позитивном моменте сегодняшний пост и закончим, всех благодарю за внимание, оставайтесь на связи 🤝
/**
******************************************************************************
* @file : udp.c
* @brief : UDP driver
* @author : MicroTechnics (microtechnics.ru)
******************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
#include "udp.h"
#include "ip.h"
/* Declarations and definitions ----------------------------------------------*/
/* Functions -----------------------------------------------------------------*/
/*----------------------------------------------------------------------------*/
uint16_t UDP_Process(UDP_Frame* udpFrame, uint16_t frameLen)
{
uint16_t newFrameLen = 0;
uint16_t destPort = ntohs(udpFrame->destPort);
uint16_t len = ntohs(udpFrame->len);
uint16_t dataLen = len - sizeof(UDP_Frame);
if (destPort == UDP_DEMO_PORT)
{
for(uint16_t i = 0 ; i < dataLen - 1; i++)
{
udpFrame->data[i]++;
}
}
uint16_t swapPort = udpFrame->destPort;
udpFrame->destPort = udpFrame->srcPort;
udpFrame->srcPort = swapPort;
udpFrame->checkSum = 0;
newFrameLen = len;
return newFrameLen;
}
/*----------------------------------------------------------------------------*/
/**
******************************************************************************
* @file : udp.h
* @brief : UDP driver interface
* @author : MicroTechnics (microtechnics.ru)
******************************************************************************
*/
#ifndef UDP_H
#define UDP_H
/* Includes ------------------------------------------------------------------*/
#include "stm32f1xx_hal.h"
#include "common.h"
/* Declarations and definitions ----------------------------------------------*/
#define UDP_DEMO_PORT 33333
typedef struct UDP_Frame
{
uint16_t srcPort;
uint16_t destPort;
uint16_t len;
uint16_t checkSum;
uint8_t data[];
} UDP_Frame;
/* Functions -----------------------------------------------------------------*/
extern uint16_t UDP_Process(UDP_Frame* udpFrame, uint16_t frameLen);
#endif // #ifndef UDP_H
Ссылка на проект - MT_ENC28J60_Part_5.




Крутой гайд, спасибо. Было бы неплохо если бы вы выложили ни гитхаб пятую (финальную) версию проекта, а то там только четвёртая. Пришлось вручную поправить несколько несостыковок, но ничего страшного 🙂
Все никак руки не дойдут... Выложу )
Спасибо за курс полезных статей! Но я так и не смог допереть, как вы задаете данные, которые собираетесь отправлять помимо всех хедеров и служебных байтов? Через ethFrame, которая задается непосредственно в Eth_Process или есть иной способ?
Просто у вас в коде вы берете UDP_PRocess и конкретно в ней по идее инкрементируете все полученные данные, но как конкретно данные, которые я хочу отправить, задаваются в коде?
Доброго времени суток!
Да, тут для демонстрации максимально просто сделал, а так в целом в зависимости от задачи - если нужно ответный фрейм со своими данными сформировать, то тут же, в UDP_Process() формируем фрейм и помещаем в него нужные данные.
спасибо большое, получилось!
Отлично!
Добрый день, опять появилась проблема) : при отправке через nmap запроса -sU -p 33333 и IP я в ответ получаю лишь 40 байт информации поля data. Где можно явно задать число передаваемых байтов?
А UDP_Process() сколько возвращает (newDataLen)?
Лучшие статьи с красивым и понятным кодом. Не ожидается финалочка для TCP? Пытаюсь уже неделю на этой основе установить хотя бы соединение, но с TCP всё посложнее, нежели с UDP. SYN получаю, а вот ACK+SYN уже не принимается. Контрольная там вроде как считается ещё от куска IpFrame. В общем...продолжаем. Как бы на IwIP не пришлось бы уходить.
Да планов много на новые статьи, но руки все никак не дойдут, уже лет 5 наверно...
Согласен с Егором. действительно лучшие статьи! Не нашел во всей сети более понятного обЪяснения!
У меня вопрос: при подкдючении в сетевых подключениях пишет неопозанная сеть, Имя сети можно указать где то в коде?
Согласен с Егором. самые лучшие статьи на эту тему. Спасбо большое автору за этот замечательный труд! У меня вопрос : в сетевых подключениях пишет "неопознанная сеть", возможно в коде указать имя сети?
Спасибо за отличный отзыв!
Имя сети по идее непосредственно в windows можно задать.