STM Урок 80. LAN. ENC28J60. UDP Server

 

 

 

 

Урок 80

 

LAN. ENC28J60. UDP Server

 

 

Ну вот и настало время нам передать через локальную сеть с помощью нашего модуля ENC28J60 какие-нибудь осознанные данные. Для этого нам потребуется обернуть наши данные в какой-нибудь транспортный протокол. Транспортный уровень — это уровень модели OSI, отвечающий за передачу данных к конкретному приложению узла, а не просто узлу. Для этого существуют порты и транспортный уровень уже передаёт данные на сетевой адрес, адресуя их определённому порту.

Основных транспортных протоколов существует два — TCP и UDP. Первый из них более сложный, требует соединения, проверки, но зато поддерживает передачу больших объёмов данных, объединяя пакеты в сегменты, а также в окна. Также TCP поддерживает гарантированную доставку пакетов данных, так как после передачи последних мы ждём подтверждения от приёмника данных. UDP же не обладает такой "силой", здесь мы уже передаём данные короткими пакетами (дейтаграммами), вслествие чего он и имеет название User Datagram Protocol (протокол пользовательских датаграмм), и подтверждения не требует.

Но зато в связи с этим данный протокол обладает простотой реализации, а также из-за ненадобности подтверждений и большей скоростью и бесперебойностью. А уж какой из этих протоколов вы будете использовать согласно вашим задачам — решать вам.

Мы пойдём по пути последовательного развития и рассмотрим пока более простой протокол — UDP.

Заголовок UDP очень простой (всего лишь 4 поля) и имеет следующий вид:

 

image00

 

Порт отравителя — значение порта источника данных,

Порт получателя — значение порта приёмника данных,

Длина UDP — количество байт во всей дейтаграмме, включая заголовок,

Котнрольная сумма — это таким же образом рассчитанная контрольная сумма, как и в случае уже рассмотренных нами протоколов, только считаются байты не только заголовка, но и данных. Причём заголовок в расчет берётся не только протокола UDP, но и ещё кое-что к нему прикрепляется сверху, и всё вместе это носит название — псевдозаголовок. Туда входят ещё:

IP-адрес источника, IP-адрес приёмника, ну нули можно не считать, идентификатор протокола (в нашем случае 17 — идентификатор UDP), а также длина пакета UDP вместе с данными, то есть та же самая величина, что и в третьем поле самого заголовка UDP.

Если заголовок структурировать, он будет выглядеть примерно таким образом

 

image101

 

Слегка всё запутано, но так оно принято и никуда не денешься, иначе наш пакет не будет идентифицирован и рискует быть непропущенным через какой нибудь файервол. Так что контрольная сумма должна сходиться.

Ну ничего, будем писать код — разберёмся.

Вот этим мы как раз сейчас и займёмся, хватит теории, пора от неё и отдохнуть.

Проект сделан на основе проекта урока 77 под названием ENC28J60_INT и назван ENC28J60_UDPS.

Откроем данный проект в Cube MX и, ничего в нём не трогая, сгенерируем проект, откроем его в Keil, подключим все наши библиотеки, а также настроим программатор на авторезет. Попробуем собрать проект, прошить его и проверить работоспособность пингов и наших ARP-запросов в терминальной программе.

Если всё работает, то продолжим писать код.

Но прежде ещё стоит сказать, что контакт RESET с модуля я подключил на ножку R платы, которая, судя по схеме платы, подключена к кнопке RESET и также подтянута к шине питания через резистор 10 килоом, что освобождает нас от установки дополнительного резистора и. что самое важное, при перезагрузке контроллера позволяет синхронно перезагрузить и модуль LAN. Также данная ножка соединена, ясное дело, с ножкой NRST контроллера

 

  image02-1 image03-1 image04-1

 

Ну, теперь перейдём к коду.

Для начала давайте немного откорректируем наш проект.

У нас не совсем правильно организована обработка сети.

Мы всё делаем в одном обработчике прерывания от ножки INT микросхемы, причём arp-запросы из терминальной программы также посылаем тут же. Это непорядок. А вдруг в это время нам вообще пакет не прийдёт и мы тогда не попадём в этот обработчик. Мы рискуем не отправить наш запрос очень долгое время. Поэтому я решил посылать его непосредственно из обработчика прерывания USART по приёму. Но тут вопрос. А вдруг в это время мы будем обрабатывать запрос от INT? Ничего страшного. USART терпеливо дождётся своей очереди, то есть прерывания очень тактичные и не лезёт в чужие дела.

Ну а если, что ещё круче, эти два типа прерывания произойдут одновременно. Что тогда? А ничего. В нашем контроллере это продумано и для этого существуют приоритеты прерываний. У кого выше приоритет, то и обработается. А у кого он выше? Тоже вопрос. Есть ответ. А у кого адрес вектора меньше, тот и круче — у того и выше приоритет. Вот таблица векторов прерываний STM32F103x8,  которые представлены в Reference manual. Нам нужны только EXTI и USART, поэтому всю таблицу показывать не буду ибо она значительно больше чем, у контроллеров AVR

 

image05

 

image06

 

Вообщем, как мы можем наблюдать, у внешнего прерывания приоритет выше. Поэтому, если нужно будет принимать пакет и обработать прием USART, то сначала будем принимать пакет, так что всё нормально.

Правда, есть у нас ещё шестой таймер, но у него приоритет даже ниже чем у USART1 (вектор 0x0000_0118). Но, так как у таймера миссия не самая важная, он только считает псевдо-секунды для времени жизни записи ARP-таблицы, то тоже всё нормально.

А если вдруг нам не понравятся приоритеты, то мы спокойно можем их изменить в Cube MX и назначить там для наших прерываний другие. Но так как нам этого делать не надо, да и сталкивались мы уже с изменением приоритетов, то в целях экономии места на старнице, рассматривать мы это не будем, да и не совсем оно по теме.

Итак, немного поэтому мы код переработаем.

Давайте в net.c закомментируем в функции net_poll всё, что связано с отправкой по команде от USART пакета ARP

 

void net_poll(void)

{

  uint16_t len;

  // uint8_t ip[4]={0};

  enc28j60_frame_ptr *frame=(void*)net_buf;

  while((len=enc28j60_packetReceive(net_buf,sizeof(net_buf)))>0)

  {

    eth_read(frame,len);

  }

  // if(usartprop.is_ip==1)//статус отправки ARP-запроса

  // {

  //   HAL_UART_Transmit(&huart1,usartprop.usart_buf,usartprop.usart_cnt,0x1000);

  //   HAL_UART_Transmit(&huart1,(uint8_t*)"\r\n",2,0x1000);

  //   ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt,ip);

  //   arp_request(ip);

  //   usartprop.is_ip = 0;

  //   usartprop.usart_cnt=0;

  // }

}

 

А ниже добавим новую функцию и добавим в неё весь этот код, который мы выше закомментировали

 

//-----------------------------------------------

void net_cmd(void)

{

  uint8_t ip[4]={0};

  if(usartprop.is_ip==1)//статус отправки ARP-запроса

  {

    HAL_UART_Transmit(&huart1,usartprop.usart_buf,usartprop.usart_cnt,0x1000);

    HAL_UART_Transmit(&huart1,(uint8_t*)"\r\n",2,0x1000);

    ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt,ip);

    arp_request(ip);

    usartprop.is_ip = 0;

    usartprop.usart_cnt=0;

  }

}

//-----------------------------------------------

 

Вызовем эту функцию в обработчике USART ниже

 

else if(b == 'a')
{
  usartprop.is_ip=1;//статус отправки ARP-запроса
  net_cmd();
}

 

Вот и всё, можно было вообще в структуре убрать теперь флаг и тип запросов передавать аргументом, но пока оставим на будущее, мало ли обнаружатся траблы.

Соберём код и проверим всё это. Для полноты картины давайте запустим бесконечный пинг. Ну верней не бесконечный, а зададим большое количество для того, чтобы пинги отправлялись постоянно, а не 4 штуки

 

image07

 

А в это время пошлём запрос ARP из терминальной программы

 

image08

 

Всё прекрасно срабатывает. Ну и отлично!

Теперь давайте займёмся непосредственно темой занятия — UDP.

Cоздадим ещё два файла — udp.c и udp.h со следующим содержимым

 

udp.h:

 

#ifndef UDP_H_

#define UDP_H_

//--------------------------------------------------

#include "stm32f1xx_hal.h"

#include <string.h>

#include <stdlib.h>

#include <stdint.h>

#include "enc28j60.h"

#include "net.h"

//--------------------------------------------------

 

//--------------------------------------------------

#endif /* UDP_H_ */

 

udp.c:

 

#include "udp.h"

//--------------------------------------------------

 

Добавим функцию чтения пакетов UDP в файл udp.c также стандартного содержания

 

//--------------------------------------------------

uint8_t udp_read(enc28j60_frame_ptr *frame, uint16_t len)

{

uint8_t res=0;

 

return res;

}

//--------------------------------------------------

 

Создадим на эту функцию прототип.

 

 

Тажке подключим глобальный строковый массив

 

#include "udp.h"

//--------------------------------------------------

extern char str1[60];

//--------------------------------------------------

 

В файле net.h внизу подключим нашу библиотеку udp

 

#include "arp.h"

#include "udp.h"

 

В файле net.c в функции ip_read вызовем нашу функцию в теле соответствующего условия

 

else if(ip_pkt->prt==IP_UDP)

{

  udp_read(frame,len);

}

 

Вернёмся в файл udp.c и подключим глобальную переменную

 

#include "udp.h"

//--------------------------------------------------

extern UART_HandleTypeDef huart1;

//--------------------------------------------------

 

 

В функции udp_read добавим отображение в терминальной программе некоторых полей заголовков пакетов, добавив ссылку на IP-пакет

 

uint8_t res=0;

sprintf(str1,"%02X:%02X:%02X:%02X:%02X:%02X-%02X:%02X:%02X:%02X:%02X:%02X; %d; ip\r\n",

  frame->addr_src[0],frame->addr_src[1],frame->addr_src[2],

  frame->addr_src[3],frame->addr_src[4],frame->addr_src[5],

  frame->addr_dest[0],frame->addr_dest[1],frame->addr_dest[2],

  frame->addr_dest[3],frame->addr_dest[4],frame->addr_dest[5],len);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

ip_pkt_ptr *ip_pkt = (void*)(frame->data);

sprintf(str1,"%d.%d.%d.%d-%d.%d.%d.%d udp request\r\n",

  ip_pkt->ipaddr_src[0],ip_pkt->ipaddr_src[1],ip_pkt->ipaddr_src[2],ip_pkt->ipaddr_src[3],

  ip_pkt->ipaddr_dst[0],ip_pkt->ipaddr_dst[1],ip_pkt->ipaddr_dst[2],ip_pkt->ipaddr_dst[3]);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

 

Теперь пришло время пакета UDP.

В файле udp.h добавим для него структуру

 

//--------------------------------------------------

typedef struct udp_pkt {

  uint16_t port_src;//порт отправителя

  uint16_t port_dst;//порт получателя

  uint16_t len;//длина

  uint16_t cs;//контрольная сумма заголовка

  uint8_t data[];//данные

} udp_pkt_ptr;

//--------------------------------------------------

 

Вернёмся в нашу функцию udp_read в файл udp.c и аналогичным образом отобразим в терминальной программе порты UDP, а также длину дейтаграммы. Ну и заодно отобразим пришедшие данные в виде строки

 

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

udp_pkt_ptr *udp_pkt = (void*)(ip_pkt->data);

sprintf(str1,"%u-%u\r\n", be16toword(udp_pkt->port_src),be16toword(udp_pkt->port_dst));

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

HAL_UART_Transmit(&huart1,udp_pkt->data,len-sizeof(udp_pkt_ptr),0x1000);

HAL_UART_Transmit(&huart1,(uint8_t*)"\r\n",2,0x1000);

 

Давайте соберём код, прошьём контроллер и попробуем послать UDP пакет с помощью утилиты netcat, версию для Windows которой не сложно найти в интернете.

Перед этим запустим ещё кроме командной строки и терминальной программы ещё и утилиту анализа сетевых пакетов Wireshark, настроив там фильтр на наш модуль

 

image09

 

Передадим нашему модулю какую-нибудь строку

 

image10

 

Вот что мы увидим в терминальной программе

 

image11

 

А вот это в Wireshark

 

image13

image14

 

Отсюда следует, что пакет нам отправился, и мало того, мы его получили.

Теперь надо как-то на это ответить.

Добавим функцию ответа на UDP-запрос

 

//--------------------------------------------------

uint8_t udp_reply(enc28j60_frame_ptr *frame, uint16_t len)

{

uint8_t res=0;

 

return res;

}

//-------------------------------

 

Вызовем её в функции udp_read

 

HAL_UART_Transmit(&huart1,(uint8_t*)"\r\n",2,0x1000);

udp_reply(frame,len);

return res;

 

Инициализируем в функции udp_reply указатели на пакеты и поменяем порты наоборот (источник и приёмник)

 

uint8_t res=0;

uint16_t port;

ip_pkt_ptr *ip_pkt = (void*)(frame->data);

udp_pkt_ptr *udp_pkt = (void*)(ip_pkt->data);

port = udp_pkt->port_dst;

udp_pkt->port_dst = udp_pkt->port_src;

udp_pkt->port_src = port;

 

Добавим свою строку в качестве данных пакета UDP

 

udp_pkt->port_src = port;

strcpy((char*)udp_pkt->data,"UDP Reply:\r\nHello from UDP Server to UDP Client!!!\r\n");

 

Заново померяем длину пакета, так как днина данных скорее всего изменилась и занесём её в соответствующее поле заголовка

 

strcpy((char*)udp_pkt->data,"UDP Reply:\r\nHello from UDP Server to UDP Client!!!\r\n");

len = strlen((char*)udp_pkt->data) + sizeof(udp_pkt_ptr);

udp_pkt->len = be16toword(len);

 

Не стоит также забывать о контрольной сумме. Можно её конечно рассчитать и проще, прибавив или отняв изменение длины данных, но мы всё-таки напишем универсальный алгоритм.

Так как выше было указано, что контрольная сумма в UDP-пакете рассчитывается по-особенному, то внесём некоторые поправки в функцию её расчёта в файле net.c.

Во-первых, добавим ещё один аргумент типа пакета. Пока условимся, что все пакеты, рассмотренные ранее у нас будут иметь тип 0, а пакет UDP — 1

 

uint16_t checksum(uint8_t *ptr, uint16_t len, uint8_t type)

{

 

Также нам на эту функцию теперь потребуется прототип, а ещё нам потребуется прототип функции ip_send. Добавим их в net.h

 

void TIM_PeriodElapsedCallback(void);

uint8_t ip_send(enc28j60_frame_ptr *frame, uint16_t len);

uint16_t checksum(uint8_t *ptr, uint16_t len, uint8_t type);

 

Вернёмся в net.c и, так как мы изменили состав входящих аргументов в функции расчёта контрольной суммы, добавим в двух местах в вызовах данной функции третий аргумент

 

ip_pkt->cs=checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);

 

icmp_pkt->cs=checksum((void*)icmp_pkt,len,0);

 

Кроме третьего аргумента в функции расчёта контрольной суммы добавим реакцию на тип 1

 

uint32_t sum=0;

if(type==1)

{

  sum+=IP_UDP;

  sum+=len-8;

}

while(len>0)

 

Немного объясню, а то сразу непонятно. Мы прибавляем идентификатор типа протокола (в данный момент 17), длину, уменьшенную на 8, так как передавать мы будем длину, увеличенную на 8. Хитрость эта нужна для того, чтобы вместе с заголовком UDP зацепить при передаче ещё и IP-адреса, находящиеся в конце IP-заголовка, который предшествует в пакете заголовку UDP. Вот так!

Ну, теперь вернёмся в файл udp.c функцию udp_reply и посчитаем там контрольную сумму, заранее её обнулив

 

udp_pkt->len = be16toword(len);

udp_pkt->cs=0;

udp_pkt->cs=checksum((uint8_t*)udp_pkt-8, len+8, 1);

 

Соответственно, указатель мы сдвигаем влево, цепляя при этом из заголовка IP сетевые адреса источника и приёмника, ну а длину, ясное дело увеличиваем на 8.

Осталось нам только отправить пакет получателю

 

udp_pkt->cs=checksum((uint8_t*)udp_pkt-8, len+8, 1);

ip_send(frame,len+sizeof(ip_pkt_ptr));

return res;

 

Соберём код, прошьём контроллер и ещё раз отправим строку в модуль из утилиты netcat. Если всё нормально, то получим там ответ от модуля вот такого вида

 

image15

 

Посмотрим наш обмен также в утилите анализа сетевых пакетов Wireshark

 

image16

 

Как мы видим, у нас всё нормально передалось и принялось. Если бы было что-то не так, то Wirechark написал бы ошибку контрольной суммы. Значит мы всё нормально рассчитали.

Таким образом, теперь мы можем принимать и передавать UDP запросы, что даёт нам возможность передачи осознанных данных (полезной нагрузки).

 

Предыдущий урок Программирование МК STM32 Следующий урок

 

Исходный код

 

 

Отладочную плату можно приобрести здесь STM32F103C8T6

Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN

Переходник USB to TTL можно приобрести здесь USB to TTL ftdi ft232rl

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM LAN. ENC28J60. UDP Server

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*