Урок 82
LAN. ENC28J60. Удалённый доступ
До сих пор мы при помощи нашего модуля ENC28J60 могли соединяться, принимать и передавать данные только с узлов, находящихся в локальной сети. Я думаю, что настал тот момент, когда мы должны попробовать с помощью него «пообщаться» с сетью внешней — с сетью «Интернет». Также внешняя сеть может быть не только интернетом, но и какой-то другой сетью. Самое главное то, что мы будем уже обращаться к узлам, находящимся за пределами нашего маршрутизатора. Я думаю, все знают, каким образом происходит маршрутизация и опрос узлов, находящихся вне сети, ну или может узнать.. Данной информации очень предостаточно на просторах глобальной сети. А вот как соединиться с ними при помощи нашей микросхемы и как это организовать в коде — знают не все, Хотя информация все равно есть, но везде разная. Я хочу дать свой вариант ответа на этот вопрос.
Тем не менее, я немного все равно поведаю о том, как происходит опрос внешней сети. Например, мы назначили нашему модулю адрес IP 192.168.1.197, а подключен он в сеть с маской 255.255.255.0, у маршрутизатора адрес 192.168.1.1. В этом случае, если мы обратимся к устройству в нашей сети, имеющему адрес от 192.168.1.1 до адреса 192.168.1.254, то будет считаться, что мы обратились к локальному сетевому устройству и мы имеем полное право знать его MAC-адрес, узнать который мы можем, послав ARP-запрос. А если устройство будет иметь IP-адрес, отличный от вышеуказанных (например 8.143.111.23), то будет считаться, что мы уже обратились к устройству внешней сети и маршрутизатор будет обязан узнать маршрут к данному устройству и предоставить нам определённый к нему доступ. Только MAC-адрес внешнего устройства маршрутизатор нам сообщать уже не будет обязан, поэтому на канальном уровне мы будем работать уже с MAC-адресом маршрутизатора. Поэтому мы должны организовать наш код так, чтобы этот код сначала выяснил, что устройство находится за пределами нашей сети и уже после этого выяснения вернул нам MAC-адрес маршрутизатора, или как принято называть — шлюза или гейта (Gateway), Кратко вообщем вот так.
Конечная цель нашего сегодняшнего урока — послать ICMP-запрос или PING внешнему узлу и дождаться от него ответа, тем самым подтвердить, что мы можем с этим узлом нормально общаться на разных сетевых уровнях.
Поэтому давайте сразу же от теории перейдём к делу.
Создадим проект с именем ENC28J60_REMOTE, переработав его из проекта прошлого занятия с именем ENC28J60_UDPC.
Запустим проект в генераторе Cube MX и, ничего там не трогая, сгенерируем проект для среды программирования Keil, откроем его, произведём настройки программатора на автоперезагрузку, а также подключим наши библиотеки в дерево проекта.
Первым делом в файле net.h добавим две макроподстановки для маски подсети и адреса маршрутизатора (шлюза). У Вас они могут отличаться от моих. Думаю, свои сетевые параметры вы знаете, если не знаете, то вы их можете увидеть в свойствах ваших сетевых адаптеров. Только это будет иметь место в случае использования статической адресации, в случае использования динамической адресации (DHCP), можно всё это узнать, введя команду ipconfig в командной строке
Прочитав свои параметры, добавим макросы в вышеуказанное место
#include "enc28j60.h"
//--------------------------------------------------
#define IP_ADDR {192,168,1,197}
#define IP_GATE {192,168,1,1}
#define IP_MASK {255,255,255,0}
Также добавим глобальные массивы для наших сетевых адресов в файле net.c
uint8_t ipaddr[4]=IP_ADDR;
uint8_t ipgate[4]=IP_GATE;
uint8_t ipmask[4]=IP_MASK;
Затем подключим эти массивы в файле arp.c
extern uint8_t ipaddr[4];
extern uint8_t ipgate[4];
extern uint8_t ipmask[4];
Далее при попытке послать запрос ARP сетевому узлу мы обязаны определить, к какой сети данный узел принадлежит — к локальной или внешней (удалённой). Поэтому перейдём в файл arp.c и в функции arp_request сначала добавим локальный массив для хранения IP-адреса, по которому мы будем определять MAC-адрес с помощью ARP-запроса, а также целочисленную переменную, которая будет аккумулировать в себе результаты логических операций над адресами и маской
uint8_t arp_request(uint8_t *ip_addr)
{
uint8_t i, j;
uint8_t ip[4];
uint8_t iptemp = 0;
Затем уже с помощью нехитрого цикла определим принадлежность к локальной сети
uint8_t iptemp = 0;
for(i=0;i<4;i++)
{
iptemp += (ip_addr[i] ^ ipaddr[i]) & ipmask[i];
}
А затем добавим следующее условие
enc28j60_frame_ptr *frame=(void*)net_buf;
//проверим принадлежность адреса к локальной сети
if( iptemp == 0 ) memcpy(ip,ip_addr,4);
else memcpy(ip,ipgate,4);
То есть, если условие не подтвердится, то есть адрес будет не локальный, то мы будем просить MAC-адрес у маршрутизатора и все дальнейшие пакеты, предназначенные для узла мы будем отправлять на физический адрес маршрутизатора (или роутера).
Соответственно, в дальнейшем коде в теле функции мы исправим использование массива на наш локальный
if(!memcmp(arp_rec[j].ipaddr,ip,4))
...
memcpy(msg->ipaddr_dst,ip,4);
Давайте соберём код, прошьём контроллер и проверим наш код, послав в терминальной программе сначала запрос по IP-адресу сначала в локальную сеть, а затем в удалённую. Для удалённой сети будем использовать для примера IP-адрес сайта «www.yandex.ru«, узнать который мы можем, пропинговав его в командной строке
Вот что мы должны увидеть в терминальной программе после посылки ARP-запросов
Судя по отчёту в программе, мы всё сделали правильно и на внешний IP-адрес наш роутер нам вернул свой MAC-адрес.
Хорошо. Часть дела сделана. Теперь наша задача добиться того, чтобы не маршрутизатор, а внешний узел нам сам что-то вернул. Поэтому давайте попробуем его «пропинговать», послав ему ICMP-запрос, ну или несколько. Мы этого пока не делали, мы только отвечали на такие запросы, поэтому нам нужна будет отдельная функция, которую мы добавим в файле net.c после функции ip_read
//-----------------------------------------------
uint8_t icmp_request(uint8_t* ip_addr)
{
uint8_t res=0;
return res;
}
//--------------------------------------------------
Также, чтобы контролировать работу данной функции в процессе написания в её тело кода, надо её где-то вызвать. Для этого мы в обработчик от USART добавим ещё одно условие, с помощью которого мы будем посылать ICMP запросы. Ослеживаться они будут с помощью встретившегося посе строки с ip-адресом символа 'p' от слова «ping»
net_cmd();
}
else if (b=='p')
{
usartprop.is_ip=4;//статус попытки отправить ICMP-пакет
net_cmd();
}
Вернёмся в файл net.c, только уже в функцию net_cmd и по аналогии с посылкой запроса UDP, добавим посылку запроса ICMP
usartprop.is_ip=0;
}
else if(usartprop.is_ip==4)//статус попытки отправить ICMP-пакет
{
ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt,ip);
usartprop.is_ip=5;//статус отправки ICMP-пакета
usartprop.usart_cnt=0;
arp_request(ip);//узнаем mac-адрес
}
else if(usartprop.is_ip==5)//статус отправки ICMP-пакета
{
icmp_request(ip);
usartprop.is_ip=0;
}
}
Таким же образом, как обычно, мы сначала посылаем ARP-запрос, затем возвращаемся сюда со следующим статусом по факту ARP-ответа либо при обнаружении готовой пары адресов в таблице ARP. Поэтому мы должны также добавить новый статус в местах возврата в функцию net_cmd.
Сначала в функции eth_read, а затем в фунции arp_request в файле arp.c
if((usartprop.is_ip==3)||(usartprop.is_ip==5))//статус отправки UDP- или ICMP-пакета
Ну и, возвратившись теперь не с пустыми руками в функцию net_cmd, мы смело посылаем запрос ICMP, вызвав соответствующую функцию, код тела которой мы сейчас и продолжим писать.
Но прежде чем продолжить, сначала добавим глобальную переменную для подсчёта отправленных ICMP-пакетов, так как для такой цифры существует отдельное поле в заголовке
char str1[60]={0};
uint32_t ping_cnt=0;//счетчик отправленных пингов
Вернёмся в функцию icmp_request и сначала, как водится, создадим переменную для рассчёта длины пакета, а затем расставим указатели на заголовки пакетов в соответствующие им места
uint8_t res=0;
uint16_t len;
enc28j60_frame_ptr *frame=(void*) net_buf;
ip_pkt_ptr *ip_pkt = (void*)(frame->data);
icmp_pkt_ptr *icmp_pkt = (void*)ip_pkt->data;
Заполним ICMP-заголовок, занеся нужные значения в его поля, не забывая также инкрементировать номера пакетов, а также про строку данных, которая имеет определённый состав, который можно прочитать в утилите анализа сетевого трафика WireShark в составе любого пакета ICMP
icmp_pkt_ptr *icmp_pkt = (void*)ip_pkt->data;
//Заполним заголовок пакета ICMP
icmp_pkt->msg_tp = 8;
icmp_pkt->msg_cd = 0;
icmp_pkt->id = be16toword(1);
icmp_pkt->num = be16toword(ping_cnt);
ping_cnt++;
strcpy((char*)icmp_pkt->data,"abcdefghijklmnopqrstuvwabcdefghi");
icmp_pkt->cs = 0;
len = strlen((char*)icmp_pkt->data) + sizeof(icmp_pkt_ptr);
icmp_pkt->cs=checksum((void*)icmp_pkt,len,0);
Далее по иерархии заполняем заголовок IP
icmp_pkt->cs=checksum((void*)icmp_pkt,len,0);
//Заполним заголовок пакета IP
len+=sizeof(ip_pkt_ptr);
ip_pkt->len=be16toword(len);
ip_pkt->id = 0;
ip_pkt->ts = 0;
ip_pkt->verlen = 0x45;
ip_pkt->fl_frg_of=0;
ip_pkt->ttl=128;
ip_pkt->cs = 0;
ip_pkt->prt=IP_ICMP;
memcpy(ip_pkt->ipaddr_dst,ip_addr,4);
memcpy(ip_pkt->ipaddr_src,ipaddr,4);
ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);
И, по окончанию — заголовок пакета Ethernet, который затем отправляем в сеть
ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);
//Заполним заголовок пакета Ethernet
memcpy(frame->addr_src,macaddr,6);
frame->type=ETH_IP;
enc28j60_packetSend((void*)frame,len + sizeof(enc28j60_frame_ptr));
return res;
}
Думаю, с кодом всё ясно, мы уже не раз такой код разжёвывали. Ну оно и хорошо, теперь мы зато не по наслышке знаем о сетевых пакетах, об их формировании, считывании, составе их заголовков и способах отправки.
Соберём код, прошьём контроллер, и пока попробуем отправить пинг нашему компьютеру, так как если мы его отправим во внешнюю сеть, толку от этого не будет, так как мы не увидим ответа, а послав компьютеру, мы его увидим хотя бы в ути лите WireShark. Пошлём несколько запросов ICMP в терминальной программе
и посмтрим результат в утилите (нажмите на картинку для увеличения изображения)
Мы видим, что все наши пакеты пришли и на все на них компьютер отправил ответ.
Следующая задача — ответ этот увидеть в терминальной программе, ну и, конечно не только этот, а и ответы от удалённых узлов.
Для этого перейдём в функцию icmp_read и отделим два условия друг от друга в теле функции
if(len>=sizeof(icmp_pkt_ptr))
{
if(icmp_pkt->msg_tp==ICMP_REQ)
{
icmp_pkt->msg_tp=ICMP_REPLY;
Добавим теперь в нижнем уровне вложенности условий ещё одно условие, которое будет реагировать на другой тип пакета — на PING-ответ
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);
}
else if (icmp_pkt->msg_tp==ICMP_REPLY)
{
sprintf(str1,"%d.%d.%d.%d-%d.%d.%d.%d icmp reply\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);
}
Вот теперь мы ответ должны будем увидеть в нашей терминальной программе.
Поэтому соберём наш код, прошьём наш контроллер и пошлём сначала несколько пингов локальному узлу, а затем несколько запросов — глобальному. Несколько потому, что мы должны убедиться, что это всё работает постоянно и независимо от того, посылаем ли мы запрос ARP или извлекаем готовый физический адрес из таблицы. Мы должны будем увидеть вот такие ответы в окне терминальной программы
Как мы можем наблюдать, пакеты к нам поступают как с локальных IP-адресов, так и с глобальных.
Таким образом, в ходе нашего сегодняшнего занятия мы научились общаться по сети теперь не только с устройствами, находящимися в нашей локальной сети, но также и с устройствами и узлами, которые расположены уже удалённо — в сети глобальной, в нашем случае — Интернет.
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Здравствуйте!
Большое спасибо за вашу работу!
Я продела ваши уроки (только без HAL на CMSIS) по сетям реализовывал данные протоколы в локально сети, всё отлично работает!
Не могли бы вы меня сориентировать вот в каком вопросе?
Допустим нужно передавать не большой объём данных через глобальную сеть с UDP — клиента реализованный аналогично как в ваших уроках (т.е. МК+ENC28J60) на UDP сервер (в виде компьютерного приложения реализованного на windows socket).
Скажите, на основании ваших учебных проектов и ENC28J60, такая затея осуществима?
Я побывал подключать клиент к одной локальной сети и передавать данные на другую локальную сеть на которой я настроил в роутере на переадресацию UDP пакетов в компьютер на порт приложения. Но увы так не заработало.
Я не пробовал, но думаю, что всё осуществимо. Если не получилось, смотрите, где именно ошибка, проанализируйте пакеты в WireShark на стороне сервера.
Добрый день! А почему icmp с компьютера видны в wireshark, а с контроллера нет?
видны только если пинговать контроллером компьютер.
то ли у автора антивирус нелицензионный), то ли у него линукс стоит, я в этом не разбираюсь). Но он забыл нас предупредить обо антивирусной защите (файерволах, брандмауэрах) которую следует отключить чтобы разрешить входящие на комп пинги
Если входящие ICMP-запросы к компу не видны в WireShark, то их следует разрешить в настройках системы/антивирусов.
Например для Касперского заходим в «Настройки» Касперского, выбираем «Сетевой экран», там выбираем «Пакетные правила», там ищем «Any incoming ICMP» и ставим галочку «Разрешить»