Нежданно-негаданно, благодаря теме на форуме, произошел возврат к одной из предыдущих тем, а именно к связке 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.
Крутой гайд, спасибо. Было бы неплохо если бы вы выложили ни гитхаб пятую (финальную) версию проекта, а то там только четвёртая. Пришлось вручную поправить несколько несостыковок, но ничего страшного 🙂
Все никак руки не дойдут... Выложу )