Урок 44
LAN. ENC28J60. UDP Server
Ну вот и настало время нам передать через локальную сеть с помощью нашего модуля ENC28J60 какие-нибудь осознанные данные. Для этого нам потребуется обернуть наши данные в какой-нибудь транспортный протокол. Транспортный уровень — это уровень модели OSI, отвечающий за передачу данных к конкретному приложению узла, а не просто узлу. Для этого существуют порты и транспортный уровень уже передаёт данные на сетевой адрес, адресуя их определённому порту.
Основных транспортных протоколов существует два — TCP и UDP. Первый из них более сложный, требует соединения, проверки, но зато поддерживает передачу больших объёмов данных, объединяя пакеты в сегменты, а также в окна. Также TCP поддерживает гарантированную доставку пакетов данных, так как после передачи последних мы ждём подтверждения от приёмника данных. UDP же не обладает такой «силой», здесь мы уже передаём данные короткими пакетами (дейтаграммами), вслествие чего он и имеет название User Datagram Protocol (протокол пользовательских датаграмм), и подтверждения не требует.
Но зато в связи с этим данный протокол обладает простотой реализации, а также из-за ненадобности подтверждений и большей скоростью и бесперебойностью. А уж какой из этих протоколов вы будете использовать согласно вашим задачам — решать вам.
Мы пойдём по пути последовательного развития и рассмотрим пока более простой протокол — UDP.
Заголовок UDP очень простой (всего лишь 4 поля) и имеет следующий вид:
Порт отравителя — значение порта источника данных,
Порт получателя — значение порта приёмника данных,
Длина UDP — количество байт во всей дейтаграмме, включая заголовок,
Котнрольная сумма — это таким же образом рассчитанная контрольная сумма, как и в случае уже рассмотренных нами протоколов, только считаются байты не только заголовка, но и данных. Причём заголовок в расчет берётся не только протокола UDP, но и ещё кое-что к нему прикрепляется сверху, и всё вместе это носит название — псевдозаголовок. Туда входят ещё:
IP-адрес источника, IP-адрес приёмника, ну нули можно не считать, идентификатор протокола (в нашем случае 17 — идентификатор UDP), а также длина пакета UDP вместе с данными, то есть та же самая величина, что и в третьем поле самого заголовка UDP.
Если заголовок структурировать, он будет выглядеть примерно таким образом
Слегка всё запутано, но так оно принято и никуда не денешься, иначе наш пакет не будет идентифицирован и рискует быть непропущенным через какой нибудь файервол. Так что контрольная сумма должна сходиться.
Ну ничего, будем писать код — разберёмся.
Вот этим мы как раз сейчас и займёмся, хватит теории, пора от неё и отдохнуть.
Создадим новый проект в Atmel Studio под названием ENC28J60_UDPS, как обычно, выбрав наш контроллер.
Скопируем в папку с проектом наши все файлы из предыдущего проекта ENC28J60_INT урока 43, корме main.c. А в main.c, как всегда добавим содержимое одноимённого файла из предыдущего проекта. Соберём наш код, прошьём контроллер и проверим, что всё у нас работает, отправив пинги нашему модулю, а также в терминальной программе послав несколько ARP-запросов.
Если всё нормально, то продолжим.
Для начала давайте немного откорректируем наш проект.
У нас не совсем правильно организована обработка сети.
Мы всё делаем в одном обработчике прерывания от ножки INT микросхемы, причём arp-запросы из терминальной программы также посылаем тут же. Это непорядок. А вдруг в это время нам вообще пакет не прийдёт и мы тогда не попадём в этот обработчик. Мы рискуем не отправить наш запрос очень долгое время. Поэтому я решил посылать его непосредственно из обработчика прерывания USART по приёму. Но тут вопрос. А вдруг в это время мы будем обрабатывать запрос от INT? Ничего страшного. USART терпеливо дождётся своей очереди, то есть прерывания очень тактичные и не лезут в чужие дела.
Ну а если, что ещё круче, эти два типа прерывания произойдут одновременно. Что тогда? А ничего. В нашем контроллере это продумано и для этого существую приоритеты прерываний. У кого выше приоритет, то и обработается. А у кого он выше? Тоже вопрос. Есть ответ. А у кого адрес вектора меньше, тот и круче — у того и выше приоритет. Вот таблица векторов прерываний Atmega328
То есть у внешнего прерывания очень высокий приоритет, выше только у RESET. Поэтому, если нужно будет принимать пакет и обработать прием USART, то сначала будем принимать пакет, так что всё нормально.
Поэтому давайте в net.c закомментируем в функции net_poll всё, что связано с отправкой по команде от USART пакета ARP
void net_pool(void)
{
uint16_t len;
// uint32_t ip=0;
enc28j60_frame_ptr *frame=(void*)net_buf;
while ((len=enc28j60_packetReceive(net_buf,sizeof(net_buf))))
{
eth_read(frame,len);
}
// if(usartprop.is_ip==1)//статус отправки ARP-запроса
// {
// 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.is_ip=0;
// usartprop.usart_cnt=0;
// }
}
//--------------------------------------------------
А ниже добавим новую функцию и добавим в неё весь этот код, который мы выше закомментировали
//--------------------------------------------------
void net_cmd(void)
{
uint32_t ip=0;
if(usartprop.is_ip==1)//статус отправки ARP-запроса
{
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.is_ip=0;
usartprop.usart_cnt=0;
}
}
Добавим на эту функцию прототип в net.h и вызовем её в usart.c в функции ISR(USART_RX_vect)
else if (b=='a')
{
usartprop.is_ip=1;//статус отправки ARP-запроса
net_cmd();
}
Вот и всё, можно было вообще в структуре убрать теперь флаг и тип запросов передавать аргументом, но пока оставим на будущее, мало ли обнаружатся траблы.
Соберём код и проверим всё это. Для полноты картины давайте запустим бесконечный пинг. Ну верней не бесконечный а зададим большое количество для того, чтобы пинги отправлялись постоянно, а не 4 штуки
А в это время пошлём запрос ARP из терминальной программы
Всё прекрасно срабатывает. Ну и отлично!
Теперь давайте займёмся непосредственно темой занятия — UDP.
Cоздадим ещё два файла — udp.c и udp.h со следующим содержимым
udp.h
#ifndef UDP_H_
#define UDP_H_
#include "enc28j60.h"
#include "net.h"
#include "usart.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 в нашу функцию 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);
USART_TX((uint8_t*)str1,strlen(str1));
ip_pkt_ptr *ip_pkt = (void*)(frame->data);
sprintf(str1,"%ld.%ld.%ld.%ld-%ld.%ld.%ld.%ld udp request\r\n",
ip_pkt->ipaddr_src & 0x000000FF,(ip_pkt->ipaddr_src>>8) & 0x000000FF,
(ip_pkt->ipaddr_src>>16) & 0x000000FF, ip_pkt->ipaddr_src>>24,
ip_pkt->ipaddr_dst & 0x000000FF,(ip_pkt->ipaddr_dst>>8) & 0x000000FF,
(ip_pkt->ipaddr_dst>>16) & 0x000000FF, ip_pkt->ipaddr_dst>>24);
USART_TX((uint8_t*)str1,strlen(str1));
Теперь пришло время пакета UDP.
В файле udp.h добавим для него структуру
#include "usart.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, а также длину дейтаграммы. Ну и заодно отобразим пришедшие данные в виде строки
USART_TX((uint8_t*)str1,strlen(str1));
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));
USART_TX((uint8_t*)str1,strlen(str1));
USART_TX(udp_pkt->data,len-sizeof(udp_pkt_ptr));
USART_TX((uint8_t*)"\r\n",2);
Давайте соберём код, прошьём контроллер и попробуем послать UDP пакет с помощью утилиты netcat, версию для Windows которой не сложно найти в интернете.
Перед этим запустим ещё кроме командной строки и терминальной программы ещё и утилиту анализа сетевых пакетов Wireshark, настроив там фильтр на наш модуль
Передадим какую-нибудь строку
Вот что мы увидим в терминальной программе
А вот это в Wireshark
Отсюда следует, что пакет нам отправился, и мало того, мы его получили.
Теперь надо как-то на это ответить.
Добавим функцию ответа на UDP-запрос
//--------------------------------------------------
uint8_t udp_reply(enc28j60_frame_ptr *frame, uint16_t len)
{
uint8_t res=0;
return res;
}
//-------------------------------
Вызовем её в функции udp_read
USART_TX((uint8_t*)"\r\n",2);
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 eth_send(enc28j60_frame_ptr *frame, uint16_t len);
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>1)
Немного объясню, а то сразу непонятно. Мы прибавляем идентификатор типа протокола (в данный момент 17), длину, уменьшенную на 8, так как передавать мы будем длину, увеличенную на 8. Хитрость эта нужна для того, чтобы вместе с заголовком UDP зацепить при передаче ещё и IP-адреса, находящиеся в конце IP-заголовка, который предшествуюет в пакете заголовку UDP. Вот так!
Ну, теперь вернёмся в функцию 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. Если всё нормально, то получим там ответ от модуля вот такого вида
Посмотрим наш обмен также в утилите анализа сетевых пакетов Wireshark
Как мы видим, у нас всё нормально передалось и принялось. Если бы было что-то не так, то Wirechark написал бы ошибку контрольной суммы. Значит мы всё нормально рассчитали.
Таким образом, теперь мы можем принимать и передавать UDP запросы, что даёт нам возможность передачи осознанных данных (полезной нагрузки).
Предыдущий урок Программирование МК STM32 Следующий урок
Приобрести плату Arduino UNO R3 можно здесь.
Приобрести программатор USBASP USBISP с адаптером можно здесь USBASP USBISP 3.3 с адаптером
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добавить комментарий