Top.Mail.Ru

STM32 и Ethernet. Часть 5. Транспортный уровень. Протокол UDP.

Нежданно-негаданно, благодаря теме на форуме, произошел возврат к одной из предыдущих тем, а именно к связке STM32 и ENC28J60 и Ethernet. И в этой, пятой, части обработаем принятый UDP-пакет и отправим на него ответ. Причем в отличие от предыдущих частей я не буду делать обширную врезку с теорией касаемо UDP, может быть потом, в отдельной статье, но вряд ли ) Просто и кратко дополним созданный ранее проект необходимым функционалом и посмотрим на результат.

Но для начала освежим в памяти предыдущие части:

Итак, протокол UDP относится к транспортному уровню, а значит мы снова движемся вверх по модели OSI:

UDP протокол

Отличительной особенностью UDP является то, что для обмена сообщениями-датаграммами устройствам не требуется никакой предварительной подготовки. То есть никаких установок соединений, рукопожатий, открытия каналов связи и тому подобного. Данные могут спокойно потеряться по пути, порядок следования датаграмм может быть нарушен, все это не контролируется. Возникает резонный вопрос - зачем тогда это нужно? А отчет достаточно прост - например, для очень чувствительных ко времени систем, систем реального времени. В таком случае в жертву приносятся дополнительные проверки и контроль, из-за чего и получаем выигрыш во времени. Если потери пакетов, ошибки, дублирования критичны, то тут уже вступает в дело, к примеру, TCP, но сегодня не об этом.

Для обмена данными по UDP необходимо знать ip-адрес и порт участников обмена. Порт представляет из себя целое число от 0 до 65535 (16-ти битное значение). Собственно, в практическом примере увидим этот механизм наглядно.

Каждая датаграмма включает в себя заголовок и непосредственно данные:

UDP frame

Пробежимся по полям заголовка:

  • 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. После ввода команды можем отправлять сами данные:

STM32 и UDP

В результате видим, что работает, как и задумано - отправленные данные инкрементированы и высланы обратно (две нижние строки, первая - отправленные, вторая - принятые). Вот на этом позитивном моменте сегодняшний пост и закончим, всех благодарю за внимание, оставайтесь на связи 🤝

/**
  ******************************************************************************
  * @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.

Подписаться
Уведомить о
guest

10 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Shen
Shen
11 месяцев назад

Крутой гайд, спасибо. Было бы неплохо если бы вы выложили ни гитхаб пятую (финальную) версию проекта, а то там только четвёртая. Пришлось вручную поправить несколько несостыковок, но ничего страшного 🙂

Владимир
Владимир
6 месяцев назад

Спасибо за курс полезных статей! Но я так и не смог допереть, как вы задаете данные, которые собираетесь отправлять помимо всех хедеров и служебных байтов? Через ethFrame, которая задается непосредственно в Eth_Process или есть иной способ?

Просто у вас в коде вы берете UDP_PRocess и конкретно в ней по идее инкрементируете все полученные данные, но как конкретно данные, которые я хочу отправить, задаваются в коде?

Владимир
Владимир
Ответ на комментарий  Aveal
6 месяцев назад

спасибо большое, получилось!

Владимир
Владимир
Ответ на комментарий  Aveal
5 месяцев назад

Добрый день, опять появилась проблема) : при отправке через nmap запроса -sU -p 33333 и IP я в ответ получаю лишь 40 байт информации поля data. Где можно явно задать число передаваемых байтов?

Егор
Егор
5 месяцев назад

Лучшие статьи с красивым и понятным кодом. Не ожидается финалочка для TCP? Пытаюсь уже неделю на этой основе установить хотя бы соединение, но с TCP всё посложнее, нежели с UDP. SYN получаю, а вот ACK+SYN уже не принимается. Контрольная там вроде как считается ещё от куска IpFrame. В общем...продолжаем. Как бы на IwIP не пришлось бы уходить.

10
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x