Top.Mail.Ru

STM32 и Ethernet. Часть 4. Сетевой уровень. Протоколы IP и ICMP.

Пока воспоминания о предыдущем проекте не канули в лету, займемся завершением начатого. Собственно, мы осуществили обработку ARP запросов, поступающих при отправке команды ping нашему устройству. Логически верным шагом будет реализовать и ответ на вышеупомянутую команду, к чему и переходим. Работаем по классической схеме - первичные теоретические сведения, которые впоследствии буду сопровождены практическим примером для STM32 и ENC28J60.

Небольшой внос дополнительной систематизации:

Итак, продолжаем продвигаться по стеку протоколов к верхним уровням и сегодня работаем на сетевом уровне:

Сетевой уровень.

Протокол IP.

Протокол IP является ключевым, если говорить о функционировании сети Интернет в целом. Именно этот протокол позволяет пакетам циркулировать между сетями, то есть осуществляет объединение различных сетевых сегментов в одну единую Сеть. Каким бы не было количество промежуточных маршрутизаторов между двумя узлами сети, протокол обеспечит доставку информации по адресу.

Между тем, сам по себе протокол IP не гарантирует надежной доставки пакета, в отличие, например, от стандартного примера надежности - TCP.

Важным свойством является возможность фрагментации пакетов. Если размер пакета превышает максимально допустимый для того или иного узла, который встретился на пути следования пакета, то пакет разделяется на несколько частей. Соответственно, каждый из фрагментов содержит в себе свой номер-идентификатор для последующей "сборки" данных воедино. Мы сегодня фрагментацию обрабатывать не будем, оставим это на будущее (если понадобится), поскольку размер наших пакетов будет объективно невелик.

Формат пакетов IP базируется на привычной структуре - байты заголовка, за которыми следует смысловая нагрузка в виде данных (рассматривается версия IPv4):

  • Version - версия протокола, для IPv4 - 0x04.
  • IHL (Internet Header Length) - длина заголовка IP-пакета в 32-битных словах.
  • DSCP (Differentiated Services Code Point) - ранее это поле называлось ToS (Type of Service) - используется для разделения на разные классы обслуживания пакетов.
  • ECN (Explicit Congestion Notification) - опциональная функция - служит для информирования узлов о перегрузке сети.
  • Total Length - полный размер пакета в байтах. "Полный" подразумевает, что включены не только данные, но и заголовок.
  • Identification - идентификатор.
  • Flags - три бита флагов:
    • Бит 0 (старший) - смысловой нагрузки не несет - всегда нулевой.
    • Бит 1 - бит DF (Don't Fragment) - определяет, возможна ли фрагментация как таковая для данного пакета.
    • Бит 2 (младший) - бит MF (More Fragments) - для фрагментированных пакетов указывает, является ли текущая часть последней.
  • Fragment Offset - смещение фрагмента - 13-ти битное значение, которое определяет позицию данных текущего фрагмента относительно изначального пакета данных. Причем значение не в байтах, а в блоках по 8 байт. Для примера, если первый фрагмент имел 64 байта данных (8 восьми-байтовых блоков), то у следующего фрагмента это поле будет иметь значение 8. У самого же первого пакета смещение равно 0.
  • TTL (Time To Live) - время жизни пакета - максимальное количество маршрутизаторов, которые может пройти пакет на своем пути.
  • Protocol - идентификатор сетевого протокола следующего уровня. Если поле данных IP пакета инкапсулирует в себе ICMP пакет, то значение поля - 0x01, для TCP - 0x06, UDP - 0x11.
  • Header Checksum - контрольная сумма заголовка. Ее расчет осуществим в практической части чуть ниже.
  • Source IP Address - IP-адрес отправителя.
  • Destination IP Address - IP-адрес получателя.

Формат пакетов протокола IP разобрали, дополним текущий проект функционалом для их обработки.

Придерживаемся нашей четко структурированной архитектуры и начинаем с добавления в проект двух новых файлов:

  • ip.c
  • ip.h
STM32, протокол IP.

Второй шаг - определение структуры для работы с IP фреймами. Данное действие происходит в файле ip.h:

typedef struct IP_Frame
{
  uint8_t verHeaderLen;
  uint8_t diffServices;
  uint16_t len;
  uint16_t fragId;
  uint16_t fragOffset;
  uint8_t timeToLive;
  uint8_t protocol;
  uint16_t checkSum;
  uint8_t srcIpAddr[IP_ADDRESS_BYTES_NUM];
  uint8_t destIpAddr[IP_ADDRESS_BYTES_NUM];
  uint8_t data[];
} IP_Frame;

Поля выстроены в соответствии с рассмотренным выше порядком их следования. Сразу же добавим функцию для расчета контрольной суммы:

/*----------------------------------------------------------------------------*/
uint16_t IP_CalcCheckSum(uint8_t* data, uint16_t len)
{
  uint32_t res = 0;
  uint16_t* ptr = (uint16_t*)data;

  while (len > 1) 
  {
    res += *ptr;
    ptr++;
    len -= 2;
  }
  
  if (len > 0) 
  {
    res += *(uint8_t*)ptr;
  }
  
  while (res > 0xffff)
  {
    res = (res >> 16) + (res & 0xFFFF);
  }
  
  return ~((uint16_t)res);
}



/*----------------------------------------------------------------------------*/

В принципе, на этом вся подготовительная деятельность завершена, добавляем основную функцию для обработки IP пакетов и формирования ответных:

/*----------------------------------------------------------------------------*/
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);
      }
      
      ipFrame->len = htons(newDataLen + sizeof(IP_Frame));
      
      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));
      
      newFrameLen = newDataLen + sizeof(ETH_Frame) + sizeof(IP_Frame);
    }
  }
  
  return newFrameLen;
}



/*----------------------------------------------------------------------------*/

Общая концепция совпадает с использованной нами в модуле протокола ARP. В качестве аргумента получаем frameLen - длину IP фрейма. Возвращать будем обновленную длину, соответствующую отправляемому пакету. Эта величина будет храниться в newFrameLen.

Проверяем, что указанный IP в пакете соответствует нашему адресу:

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)
{   
  // ...
}

Опять же в случае корректности, то есть совпадения значений, продолжаем работать с принятыми данными. В frameLen на данный момент полная длина IP пакета (включая заголовок). Рассчитаем размер поля данных (dataLen), который будем использовать в дальнейшем. Поскольку у нас в планах также протокол ICMP, проверим поле protocol заголовка IP:

uint16_t dataLen = frameLen - sizeof(IP_Frame);
uint16_t newDataLen = 0;

if (ipFrame->protocol == IP_FRAME_PROTOCOL_ICMP)
{  
  // ICMP processing should be there
}

В это специально и предусмотрительно отведенное место мы впоследствии поместим обработку ICMP фреймов, а пока закончим с IP... Предполагая, что newDataLen будет изменено впоследствии в части ICMP, рассчитываем обновленную длину полного IP пакета и заносим ее в ipFrame->len:

newFrameLen = newDataLen + sizeof(IP_Frame);
ipFrame->len = htons(newFrameLen);

Фрагментацию пакетов не затрагиваем, так что:

ipFrame->fragId = 0;
ipFrame->fragOffset = 0;

Меняем местами IP-адреса отправителя и получателя, а точнее в IP отправителя заносим наш адрес - ipAddr. Завершаем формирование пакета подсчетом контрольной суммы:

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));

На этом все. Идем на уровень ниже, в файл ethernet.c, в функцию ETH_Process() и по аналогичному с ARP принципу осуществляем обработку IP пакетов:

// IP protocol
if (etherType == ETH_FRAME_TYPE_IP)
{
  responseSize = IP_Process((IP_Frame*)ethFrame->data, ethDataLen);
}

Суммарно имеем:

/*----------------------------------------------------------------------------*/
void ETH_Process(ENC28J60_Frame* encFrame)
{
  uint16_t responseSize = 0;
  uint16_t requestSize = ENC28J60_ReceiveFrame(encFrame);

  if (requestSize > 0)
  {
    ETH_Frame* ethFrame = (ETH_Frame*)encFrame->data;
    uint16_t etherType = ntohs(ethFrame->etherType);
    uint16_t ethDataLen = requestSize - sizeof(ETH_Frame);
    
    // ARP protocol
    if (etherType == ETH_FRAME_TYPE_ARP)
    {
      responseSize = ARP_Process((ARP_Frame*)ethFrame->data, ethDataLen);
    }
    
    // IP protocol
    if (etherType == ETH_FRAME_TYPE_IP)
    {
      responseSize = IP_Process((IP_Frame*)ethFrame->data, ethDataLen);
    }

    if (responseSize > 0)
    {
      ETH_Response(ethFrame, responseSize);
    }
  }
}



/*----------------------------------------------------------------------------*/

Разобрались с пакетами IP, теперь разберемся и с содержащимися в них данными. И как уже обсудили, в первую очередь, уделим внимание ICMP.

Протокол ICMP.

ICMP, в основном, используется для сигнализирования о возникновении исключительных ситуаций - ошибок, недоступности узлов и т. д. Кроме того, что на данный момент нас интересует в первую очередь, протокол ICMP используется для работы утилиты ping. Собственно, при попытке пинговать узел, происходит отправка эхо-запроса, и по наличию или отсутствию эхо-ответа определяется доступность или недоступность целевого узла 👍 Таким образом, нам нужно принять этот самый запрос и отправить корректный ответ.

Суть формирования пакетов остается такой же, как мы обсудили в предыдущей части:

Протоколы Ethernet, IP, ICMP.

Обработав IP пакет, извлекаем из него данные, который уже являются ICMP пакетом, так же имеющим свой собственный заголовок. Формат ICMP пакета таков:

Формат пакета протокола ICMP.

Снова проходим по полям заголовка в обозначенном порядке:

  • Type - тип пакета. На данный момент наша область интереса: эхо-запрос - код 0x08, эхо-ответ - код 0x00.
  • Code - в кооперации с полем Type дает полную информацию о типе пакета. И для эхо-запроса, и для ответа код - 0x00. А, например, если ICMP пакет сигнализирует о недостижимости узла, то: Type = 0x03, Code = 0x01. Если смысл пакета в недостижимости порта, то уже так: Type = 0x03, Code = 0x03. В общем, суть такая.
  • Checksum - контрольная сумма. Рассчитывается также, как и для IP пакета.

После заголовка следуют данные. Причем формат данных также может отличаться для разных значений полей Type и Code. В частности, формат ICMP эхо-запросов и ответов:

ICMP эхо-запрос.

При формировании ответа на эхо-запрос мы будем менять только поле Type. Снова возвращаемся к проекту, добавляем файлы и определяем структуру для хранения специализированных данных:

  • icmp.c
  • icmp.h
STM32, протокол ICMP.
typedef struct ICMP_EchoFrame
{
  uint8_t type;
  uint8_t code;
  uint16_t checkSum;
  uint16_t id;
  uint16_t seqNum;
  uint8_t data[];
} ICMP_EchoFrame;

Придерживаемся устоявшейся архитектуры, добавляем функцию ICMP_Process():

/*----------------------------------------------------------------------------*/
uint16_t ICMP_Process(ICMP_EchoFrame* icmpFrame, uint16_t frameLen)
{
  uint16_t newFrameLen = 0;
  
  uint16_t rxCheckSum = icmpFrame->checkSum;
  icmpFrame->checkSum = 0;
  uint16_t calcCheckSum = IP_CalcCheckSum((uint8_t*)icmpFrame, frameLen);
  
  if (rxCheckSum == calcCheckSum)
  {
    if (icmpFrame->type == ICMP_FRAME_TYPE_ECHO_REQUEST)
    {
      icmpFrame->type = ICMP_FRAME_TYPE_ECHO_REPLY;
      icmpFrame->checkSum = IP_CalcCheckSum((uint8_t*)icmpFrame, frameLen);
      newFrameLen = frameLen;
    }
  }
  
  return newFrameLen;
}



/*----------------------------------------------------------------------------*/

Алгоритм схож с рассмотренным ранее для IP пакетов, так что не буду даже отдельно описывать ) По итогу мы имеем обработанный пакет, готовый к последующей отправке. Остается в заранее подготовленном месте осуществить вызов ныне добавленной функции:

/*----------------------------------------------------------------------------*/
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);
      }
      
      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;
}



/*----------------------------------------------------------------------------*/

Систематизируя графически реализованное, получаем:

Работа с сетью на микроконтроллере STM32.

Собираем проект, прошиваем и анализируем полученный результат, отправляя команду ping на наш ip:

ping 169.254.191.22
Запуск проекта.

Все пошло по плану, наблюдаем успешные ответы от узла. Аналогично, можно посмотреть в WireShark:

WireShark, прием ICMP пакетов

Вишенка на торте - посмотрим на принятые байты. Поставим брейк-поинт, к примеру, здесь:

STM32, отладка Ethernet.

И далее осуществляем анализ имеющихся байтов:

Протокол IP и ICMP.

Поля контрольных сумм нулевые, поскольку мы их собственноручно обнулили в коде. Собственно, вот и все, на этом на сегодня заканчиваем 👋

/**
  ******************************************************************************
  * @file           : ip.c
  * @brief          : IP driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "ip.h"
#include "icmp.h"
#include "ethernet.h"
#include <string.h>



/* Declarations and definitions ----------------------------------------------*/




/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
uint16_t IP_CalcCheckSum(uint8_t* data, uint16_t len)
{
  uint32_t res = 0;
  uint16_t* ptr = (uint16_t*)data;

  while (len > 1) 
  {
    res += *ptr;
    ptr++;
    len -= 2;
  }
  
  if (len > 0) 
  {
    res += *(uint8_t*)ptr;
  }
  
  while (res > 0xffff)
  {
    res = (res >> 16) + (res & 0xFFFF);
  }
  
  return ~((uint16_t)res);
}



/*----------------------------------------------------------------------------*/
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);
      }
      
      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;
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : ip.h
  * @brief          : IP driver interface
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef IP_H
#define IP_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f1xx_hal.h"
#include "common.h"



/* Declarations and definitions ----------------------------------------------*/

#define IP_FRAME_PROTOCOL_ICMP                                  0x01



typedef struct IP_Frame
{
  uint8_t verHeaderLen;
  uint8_t diffServices;
  uint16_t len;
  uint16_t fragId;
  uint16_t fragOffset;
  uint8_t timeToLive;
  uint8_t protocol;
  uint16_t checkSum;
  uint8_t srcIpAddr[IP_ADDRESS_BYTES_NUM];
  uint8_t destIpAddr[IP_ADDRESS_BYTES_NUM];
  uint8_t data[];
} IP_Frame;



/* Functions -----------------------------------------------------------------*/

extern uint16_t IP_CalcCheckSum(uint8_t* data, uint16_t len);
extern uint16_t IP_Process(IP_Frame* ipFrame, uint16_t frameLen);



#endif // #ifndef IP_H
/**
  ******************************************************************************
  * @file           : icmp.c
  * @brief          : ICMP driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "icmp.h"
#include "ip.h"



/* Declarations and definitions ----------------------------------------------*/



/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
uint16_t ICMP_Process(ICMP_EchoFrame* icmpFrame, uint16_t frameLen)
{
  uint16_t newFrameLen = 0;
  
  uint16_t rxCheckSum = icmpFrame->checkSum;
  icmpFrame->checkSum = 0;
  uint16_t calcCheckSum = IP_CalcCheckSum((uint8_t*)icmpFrame, frameLen);
  
  if (rxCheckSum == calcCheckSum)
  {
    if (icmpFrame->type == ICMP_FRAME_TYPE_ECHO_REQUEST)
    {
      icmpFrame->type = ICMP_FRAME_TYPE_ECHO_REPLY;
      icmpFrame->checkSum = IP_CalcCheckSum((uint8_t*)icmpFrame, frameLen);
      newFrameLen = frameLen;
    }
  }
  
  return newFrameLen;
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : icmp.h
  * @brief          : ICMP driver interface
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef ICMP_H
#define ICMP_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f1xx_hal.h"
#include "common.h"



/* Declarations and definitions ----------------------------------------------*/

#define ICMP_FRAME_TYPE_ECHO_REQUEST	                        0x08
#define ICMP_FRAME_TYPE_ECHO_REPLY	                        0x00



typedef struct ICMP_EchoFrame
{
  uint8_t type;
  uint8_t code;
  uint16_t checkSum;
  uint16_t id;
  uint16_t seqNum;
  uint8_t data[];
} ICMP_EchoFrame;



/* Functions -----------------------------------------------------------------*/

extern uint16_t ICMP_Process(ICMP_EchoFrame* icmpFrame, uint16_t frameLen);



#endif // #ifndef ICMP_H

Ссылка на проект - MT_ENC28J60_Part_4

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

2 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Петя
Петя
2 лет назад

Хотелось бы реализацию FTP увидеть,а еще лучше SFTP.

Антон
Антон
1 год назад

Спасибо большое, ранее реализовывал всё это на asm для PIC, а сейчас для stm32 на си. Очень хотелось бы про http клиент/сервер, и про mqtt.

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