AVR Урок 45. LAN. ENC28J60. UDP Client



 

Урок 45

 

LAN. ENC28J60. UDP Client

 

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

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

Итак, наш проект создан, библиотеки подключены, main.с имеет вид как и в прошлом проекте. Тогда попробуем его собрать и прошить и далее работаем с кодом.

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

 

typedef struct USART_prop{

  uint8_t usart_buf[25];

 

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

 

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

if (usartprop.usart_cnt>25)

 

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

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

 

else if (b=='a')

{

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

  net_cmd();

}

else if (b=='u')

{

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

  net_cmd();

}

else

 

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

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

 

USART_TX(usartprop.usart_buf,usartprop.usart_cnt);

USART_TX((uint8_t*)"\r\n",2);

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

sprintf(str1,"%lu",ip);

USART_TX((uint8_t*)str1,strlen(str1));

USART_TX((uint8_t*)"\r\n",2);

arp_request(ip);

 

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

 

  usartprop.usart_cnt=0;

}

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

{

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

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

strncpy(ss2,ip_str,len);

 

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

 

ss1=strchr(ip_str,ch2);

if (ss1!=NULL)

{

  offset=ss1-ip_str+1;

  strncpy(ss2,ip_str,offset);

  ss2[offset]=0;

  ip |= ((uint32_t)atoi(ss2))<<24;

  return ip;

}

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 uint32_t ip=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-адреса и номера порта приёмника.

Оказывается, есть такое местечко и прячется оно не очень далеко, а совсем рядом. Это функция eth_read. После вызова функции заполнении 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(uint32_t ip_addr)

{

  uint8_t i,j;

 

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

 

//проверим, может такой адрес уже есть в таблице ARP, а заодно и удалим оттуда просроченные записи

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 (arp_rec[j].ipaddr==ip_addr)

  {

    //смотрим 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

 

    USART_TX((uint8_t*)str1,strlen(str1));

  }

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

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

  {

    net_cmd();

  }

  return 0;

}

 

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

 

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

uint8_t udp_send(uint32_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 "usart.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);

ip_pkt->ipaddr_src = ip_addr;

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

 

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

 

image04

 

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

 

image05

 

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

 

image06

 

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

 

image07

 

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

 

image08

 

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

 

 image09

 

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

 

 

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

 

Исходный код

 

 

Приобрести плату Arduino UNO R3 можно здесь.

Приобрести программатор USBASP USBISP с адаптером можно здесь USBASP USBISP 3.3 с адаптером

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

 

 

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

AVR LAN. ENC28J60. UDP Client

 

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

AVR LAN. ENC28J60. UDP Client