STM Урок 81. LAN. ENC28J60. UDP Client



 

Урок 81

 

LAN. ENC28J60. UDP Client

 

В предыдущем уроке мы познакомились с новым для нас протоколом транспортного уровня UDP и написали примитивный эхо-сервер. Сегодня мы поставим обратную задачу — написать клиент. Как мы уже знаем из нашей практики, что клиент писать зачастую сложнее, чем сервер, так как мы обращаемся по сетевому адресу, не зная при этом адреса физического и это накладывает определённые неудобства, так как нам нужно данный адрес сначала узнать, а потом уже слать на него какие-то пакеты. И, мало того, пакет приходится формировать полностью, что в сервере как правило не требуется, достаточно в пришедшем пакете заменить лишь некоторые поля в заголовках. Но клиент зато требуется чаще и нам от этого уйти не получится. А UDP-клиент тоже очень пользуется спросом. Впоследствии мы попробуем запросить из интернета текущее время с помощью такого клиента. Но там уже будет другая проблема. Мы пока работаем только в локальной сети, с глобальной будет немного по-другому. Но тем не менее всё это мы осилим.

А пока давайте создадим проект таким же образом, как и всегда, из проекта прошлого занятия ENC28J60_UDPS. Проект будет называться ENC28J60_UDPC. Изменилась только одна буква, так как у нас теперь клиент.

Запустим проект в Cube MX и, ничего там не трогая, сгенерируем проект и запустим его в Keil. Затем как обычно, подключим к проекту наши библиотеки и настроим программатор на автоперезагрузку.

Зайдём первым делом в net.h и увеличим немного строку для строки ввода из терминальной программы, так как в данную строку мы должны теперь будем писать ещё и порт, а он порой бывает даже пятисимвольный

 

typedef struct USART_prop{

  uint8_t usart_buf[25];

 

Соответственно, в файле net.c тоже сделаем корректировку в функции обработки прерывания от USART

 

//если вдруг случайно превысим длину буфера

if(usartprop.usart_cnt>25)

 

UDP-пакеты мы будем посылать, как вы поняли, также из терминальной строки, только после IP-адреса мы через двоеточие (:)  будем писать номер порта и заканчиваться строка будет уже не буквой a, а буквой u по понятным причинам.

В этой же функции добавим ещё одно условие

 

  net_cmd();

}

else if (b=='u')

{

  usartprop.is_ip=2;//статус попытки отправить UDP-пакет

  net_cmd();

}

 

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

Переходим в функцию net_cmd и пока удалим там весь вывод информации в USART

 

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

 

Добавим здесь ещё одно условие

 

  usartprop.usart_cnt=0;

}

else if(usartprop.is_ip==2)//статус попытки отправить UDP-пакет

{

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

  usartprop.is_ip=3;//статус отправки UDP-пакета

  usartprop.usart_cnt=0;

  arp_request(ip);//узнаем mac-адрес

}

 

После обработки строки с IP-адресом мы установим другой статус, также обнулим счётчик байтов USART, ну и отправим также ARP-запрос.

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

 

int ch1 = '.';

int ch2 = ':';

 

Скорректируем имя также и в коде

 

ss1 = strchr(ip_str,ch1);

 

После тела цикла for узнаем вхождение нашего символа

 

  len-=offset;

}

ss1=strchr(ip_str,ch2);

 

Далее с помощью условия узнаем, есть ли у нас двоеточие, извлечём в его теле строку с цифрами последнего байта IP-адреса, преобразуем её в число и выйдем из функции, а дальнейший код у нас выполняться уже не будет

 

ss1=strchr(ip_str,ch2);

if (ss1!=NULL)

{

  offset=ss1-ip_str+1;

  strncpy(ss2,ip_str,offset);

  ss2[offset]=0;

  ipextp[3] = atoi(ss2);

  return;

}

strncpy(ss2,ip_str,len);

 

Давайте проверим работу данного кода. Соберём код, прошьём контроллер, запустим терминальную программу. а также утилиту WireShark, только давайте сегодня отфильтруем вывод уже не по IP, а по MAC. Делается это следующим образом

 

Image00

 

В терминальной программе попробуем ввести строку следующего содержания

 

Image01

 

Как мы видим, всё сработало и пакет ARP у нас также отправился и компьютер на него ответил. Также можно проверить, что у нас не перестал работать запрос ARP по-старому, без порта и с буквой a.

 

 

Также мы видим, что в утилите WireShark у нас также видны отправленные и принятые пакеты ARP

 

Image02

 

Теперь нам необходимо из строки извлечь ещё и порт.

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

 

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

uint16_t port_extract(char* ip_str, uint8_t len)

{

  uint16_t port=0;

  int ch1=':';

  char *ss1;

  uint8_t offset = 0;

  ss1=strchr(ip_str,ch1);

  offset=ss1-ip_str+1;

  ip_str+=offset;

  port = atoi(ip_str);

  return port;

}

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

 

Но вызывать мы данную функцию пока не будем, так как рано. А рано потому что все равно толку не будет никакого ибо мы пока находимся в обработчике приёма по USART и по определению мы ещё не приняли ответ ARP от узла и поэтому MAC-адрес мы его не знаем, да и мало того, возможно он у нас есть уже в ARP-таблице. Вот такая вот засада образовалась. Нам надо будет оперативно покинуть обработчик и ждать ответа, а потом вовремя в функцию net_cmd вернуться. Ну ничего, мы всё разрулим. Где наша не пропадала?

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

 

void net_cmd(void)

{

  static uint8_t ip[4]={0};

  static uint16_t port=0;

 

Также подготовим здесь условие для следующего статуса, чтобы нам было куда вернуться

 

    arp_request(ip);//узнаем mac-адрес

  }

  else if(usartprop.is_ip==3)//статус отправки UDP-пакета

  {

    usartprop.is_ip=0;

  }

}

 

Теперь будем думать, откуда именно нам сюда возвращаться, так как перед нами есть условие — в этот момент мы обязательно должны знать mac-адрес.

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

 

 

Вторая засада. В функции eth_send мы перед отправкой пакета узлу просто меняем местами MAC-адреса приёмника и источника. Но у нас не всегда адрес источника будет становиться адресом приёмника. Как раз отправка UDP-пакета от клиента именно этот случай. Нам ведь не приходило образцовых пакетов UDP и никакого источника у нас нет. Поэтому мы удалим первую строку из данной функции

 

void eth_send(enc28j60_frame_ptr *frame, uint16_t len)

{

  memcpy(frame->addr_dest,frame->addr_src,6);

 

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

Будем вспоминать всё сначала, как мы создавали наш этот проект и как он у нас развивался, какие мы отправляли сначала пакеты, какие потом.

Сначала мы отвечали на ARP-запрос. Поэтому зайдём в функцию arp_send в файле arp.c и добавим нашу строку перед отправкой пакета

 

memcpy(frame->addr_dest,frame->addr_src,6);

eth_send(frame,sizeof(arp_msg_ptr));

 

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

Затем мы отвечали на ICMP-запрос. В функции icmp_read также вставим нашу строку в подобающее место, потому что функцию ip_send нам такими вещами засорять никак нельзя, так как в неё нужно уже прийти с нужным адресом, ибо он будет везде формироваться по-разному, а нам ведь не хочется там плодить разные кейсы, так что прийдём туда уже подготовленными к отправке пакета

 

memcpy(frame->addr_dest,frame->addr_src,6);

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

 

Далее мы посылали ARP-запрос. А происходило это в функции arp_request в файле arp.c. Поэтому перейдём в соответствующий файл и сначала также очистим его от закомментированного кода, а затем уже перейдём в функцию arp_request и … ничего там не будем трогать, так как там мы напрямую вызываем функцию enc28j60_packetSend, как говорится, без посредников.

Ну и также мы на прошлом занятии отправляли UDP-пакет в ответ на пришедший в качестве эхо-ответа. Это происходило в функции udp_reply в файле udp.c. Добавим код в данной функции

 

memcpy(frame->addr_dest,frame->addr_src,6);

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

 

Ух ты! три раза написали одно и то же. Ну и ладно. Зато легко проделать. Надеюсь, все проверили работоспособность, которая теперь, как мы убедились у нас не пострадала.

Ну теперь начнём потихоньку выполнять нашу основную задачу: «Вернуться в функцию net_cmd из нужных мест и отправить оттуда UDP-пакет, вооружившись перед этим значением IP-адреса и номера порта приёмника.

Для функции net_cmd создадим прототип в файле net.h и будем искать место для для возврата в эту функцию.

Оказывается, есть такое местечко и прячется оно не очень далеко, а совсем рядом. Это функция eth_read в файле net.c. После вызова функции заполнении ARP-таблицы мы и осуществим возврат в нашу функцию net_cmd, но только при условии, что у нас подобающий статус

 

else if(res==2)

{

  arp_table_fill(frame);

  if(usartprop.is_ip==3)//статус отправки UDP-пакета

  {

    memcpy(frame->addr_dest,frame->addr_src,6);

    net_cmd();

  }

}

 

Также мы видим, что вернёмся мы не с пустыми руками, а уже с MAC-адресом.

Но этого мало. Это один случай. Это когда мы таблицу заполняли. А если у нас запись уже была? То тут другая фунция. А функция это arp_request в файле arp.c, в которую мы сейчас перейдём и, прежде чем вызовем нашу любимую net_cmd, мы тут внесём некоторые коррективы, поработав над ошибками. У нас используется сразу в двух циклах одна и та же переменная для счёта циклов, хотя один цикл у нас вложен в другой. Я до сих пор не понимаю, как вообще работал код. Исправим эту ситуацию, добавив сначала ещё одну переменную

 

uint8_t arp_request(uint8_t *ip_addr)

{

  uint8_t i,j;

 

Далее заменим в главном цикле переменную во всех местах (выделено, как обычно, жирным шрифтом)

 

for(j=0;j<5;j++)

{

  //Если записи уже более 12 часов, то удалим её

  if((clock_cnt-arp_rec[j].sec)>43200)

  {

    memset(arp_rec+(sizeof(arp_record_ptr)*j),0,sizeof(arp_record_ptr));

  }

  if(!memcmp(arp_rec[j].ipaddr,ip_addr,4))

  {

  //смотрим ARP-таблицу

 

Перенесём код установки указателя на пакет в начало функции

 

uint8_t i,j;

enc28j60_frame_ptr *frame=(void*)net_buf;

 

Подключим в файле arp.c глобальную переменную структуры

 

uint8_t current_arp_index=0;

extern USART_prop_ptr usartprop;

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

 

Ну и теперь в необходимом месте в функции arp_request вызовем нашу net_cmd

 

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

  }

  memcpy(frame->addr_dest,arp_rec[j].macaddr,6);

  if(usartprop.is_ip==3)//статус отправки UDP-пакета

  {

    net_cmd();

  }

  return 0;

}

 

Перейдём в файл udp.с и создадим функцию для отправки UDP-пакета в самом верху после глобальных объявлений

 

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

uint8_t udp_send(uint8_t *ip_addr, uint16_t port)

{

  uint8_t res=0;

  return res;

}

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

 

Напишем в заголовочном файле прототип на данную функцию и в файле net.c в функции net_cmd данную функцию вызовем, узнав перед этим номер порта

 

else if(usartprop.is_ip==3)//статус отправки UDP-пакета

{

  port=port_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);

  udp_send(ip,port);

  usartprop.is_ip=0;

}

 

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

А прежде, чем перейдём, добавим глобальный массив буфера

 

extern char str1[60];

extern uint8_t net_buf[ENC28J60_MAXFRAME];

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

 

А вот теперь перейдём

Создадим переменную для длины и получим указатели на все необходимые пакеты

 

uint8_t res=0;

uint16_t len;

enc28j60_frame_ptr *frame=(void*) net_buf;

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

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

 

В файле udp.h заведём переменную для постоянного локального порта нашего приложения в контроллере

 

#include "net.h"

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

#define LOCAL_PORT 333

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

 

У вас может быть любое своё значение.

Вернёмся в нашу функцию отправки пакета UDP и заполним в заголовке поля портов источника и приёмника, помня о том, что эти значения в полях хранятся в формате big endian

 

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

udp_pkt->port_dst = be16toword(port);

udp_pkt->port_src = be16toword(LOCAL_PORT);

 

Занесём какую-нибудь строку в поле данных и после узнаем длину пакета UDP

 

udp_pkt->port_src = be16toword(LOCAL_PORT);

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

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

 

Занесём длину в соответствующее поле заголовка

 

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

udp_pkt->len = be16toword(len);

 

Затем обнулим поле с контрольной суммой и, вызвав необходимую функцию, занесём туда новую контрольную сумму

 

udp_pkt->len = be16toword(len);

udp_pkt->cs=0;

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

 

С UDP-пакетом вроде всё. Теперь обернём его в заголовок IP, такой порядок, что поделать. Для этого заполним необходимые поля заголовка IP

 

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

memcpy(ip_pkt->ipaddr_src,ip_addr,4);

ip_pkt->prt = IP_UDP;

ip_pkt->id = 0;

ip_pkt->ts = 0;

ip_pkt->verlen = 0x45;

 

Далее пакет IP заворачиваем в заголовок Ethernet, для чего нам надо заполнить его поля. MAC-адрес источника у нас уже занесен, а адрес приёмника заносится в функции отправки пакета Ethernet. Поэтому нам остаётся лишь заполнить тип пакета

 

ip_pkt->verlen = 0x45;

frame->type=ETH_IP;

 

Ну и, понятное дело, не забываем вызвать функцию отправки пакета

 

  frame->type=ETH_IP;

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

  return res;

}

 

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

 

Image03

 

Убедимся, что наши пакеты пришли благополучно

 

Image05

 

Прекрасно, пакеты все дошли. Убедимся, также что они дошли полностью

 

Image06

 

Также можно убедиться ещё и в утилите netcat, отправив всё-таки там сначала UDP-пакет нашему контроллеру (я говорю контроллеру, потому что уже не модулю. Мы уже посылаем пакеты приложению, так как используем номер порта и транспортный протокол). Это нужно для того, чтобы узнать сначала порт, на который отправлять пакеты.

 

Image07

 

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

 

Image08

 

И теперь отправим несколько раз пакет именно на этот порт

 

Image10

 

И пакеты пришли, правда почему-то не все, но это, видимо, какие-то издержки утилиты

 

Image09

 

Итак, мы сегодня научились посылать UDP-пакеты на определённый узел, используя определённый порт, таким образом создав UDP-клиент, хотя это оказалось и не так легко, как кажется на первый взгляд. Всё это говорит о растущей у нас квалификации, что подтверждает плоды предыдущих занятий, что они не прошли даром. В следующем занятии мы попытаемся уже отправить какие-нибудь пакеты не в локальную сеть, а уже в глобальную. Я думаю, и это у нас обязательно получится!

 

 

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

 

Исходный код

 

 

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

Программатор недорогой можно купить здесь ST-Link V2

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

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

 

 

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

 

STM LAN. ENC28J60. UDP Client

5 комментариев на “STM Урок 81. LAN. ENC28J60. UDP Client
  1. Vi Thanh:

    Hi, i made the data transfer from ENC client to ENC sever continuously. (Client read temperature and transmission to sever). The ENCs and computer are connected by a Switch. Transmission does not occur error. But when the transmission time is more than 2 minutes, the MCU is suspended. Or when I ping IP from computer, both MCUs are suspended. please debug help me. thank you

  2. Сергей:

    в функции arp_request в этой строчке ошибка…

    if(!memcmp(arp_rec[j].ipaddr,ip_addr,4))
    {…}

    должно быть так…

    if(memcmp(arp_rec[j].ipaddr,ip_addr,4))
    {…}
    это соответствует логике программы…
    если есть совпадение по адресам, функция вернет 1 и будет выполняться
    код внутри скобок, в том числе и будет отправлен UDP-пакет на имеющийся MAC-адрес

  3. Narod Fan:

    Please, i need TCP Client.

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

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

*