Урок 71
Часть 3
LAN. ENC28J60. ARP
В предыдущей части нашего урока мы напиcали код по приёму желаемого адреса IP для запроса ARP из терминальной программы в строковом виде, затем преобразовали данный адрес в числовой массив и начали писать функцию отправки запроса ARP в сеть.
Вернёмся в arp.c и подключим глобальную переменную нашего буфера, чтобы опять же сэкономить на памяти, а также добавим нулевой и широковещательный MAC-адреса
extern char str1[60];
extern uint8_t net_buf[ENC28J60_MAXFRAME];
uint8_t macbroadcast[6]=MAC_BROADCAST;
uint8_t macnull[6]=MAC_NULL;
Продолжим нашу функцию отправки ARP-запроса.
После того, как мы создали переменную, мы должны будем сначала определить, что в нашей ARP-таблице, которую мы создадим чуть позже нет пока записи соответствия исследуемому IP-адреса готового MAC-адреса, который мы уже когда-то определили и сохранили в таблицу. Но пока у нас таблицы нет, мы пропустим этот код, а добавим мы его несколько позднее, так как заполняться таблица будет несколько в другом случае, и, соответственно нам там пока брать нечего. Поэтому сформируем пакет для ARP-запроса и отправим его в сеть
uint8_t i;
enc28j60_frame_ptr *frame=(void*)net_buf;
arp_msg_ptr *msg = (void*)frame->data;
msg->net_tp = ARP_ETH;
msg->proto_tp = ARP_IP;
msg->macaddr_len = 6;
msg->ipaddr_len = 4;
msg->op = ARP_REQUEST;
memcpy(msg->macaddr_src,macaddr,6);
memcpy(msg->ipaddr_src,ipaddr,4);
memcpy(msg->macaddr_dst,macnull,6);
memcpy(msg->ipaddr_dst,ip_addr,4);
memcpy(frame->addr_dest,macbroadcast,6);
memcpy(frame->addr_src,macaddr,6);
frame->type = ETH_ARP;
enc28j60_packetSend((void*)frame,sizeof(arp_msg_ptr) + sizeof(enc28j60_frame_ptr));
return 1;
В принципе, мы уже можем проверить отправку пакета с помощью программы Wireshark. Для этого соберём код, прошьём контроллер и повторим отправку строки с адресом из терминальной программы, перед этим не забыв запустить программу Wireshark и применить там фильтр arp так же, как мы делали на прошлом занятии. Вот что мы должны увидеть
Мы видим, что запрос наш дошёл благополучно, а также компьютер послал ответ, который мы пока не обрабатывали. Посмотрим, что ответ отправлен именно нам
Мы видим в качестве приёмника наши физический и локальный адреса. Отлично!
Теперь нам нужно будет обработать ответ ПК.
Для этого зайдём в файле arp.c и в функции arp_read мы несколько изменим вот этот код
if((msg->op==ARP_REQUEST)&&(!memcmp(msg->ipaddr_dst,ipaddr,4)))
{
. . .
}
Мы отделим каждое условие друг от друга. Получится вот так
if(!memcmp(msg->ipaddr_dst,ipaddr,4))
{
if(msg->op==ARP_REQUEST)
{
. . .
}
}
После тела условия наличия ARP-запроса мы создадим ещё одно условие наличия ARP-ответа. Код будет несколько похожим, только переменную res мы выставим в 2
res=1;
}
else if(msg->op==ARP_REPLY)
{
sprintf(str1,"rnreplyrnmac_src %02X:%02X:%02X:%02X:%02X:%02Xrn",
msg->macaddr_src[0],msg->macaddr_src[1],msg->macaddr_src[2],
msg->macaddr_src[3],msg->macaddr_src[4],msg->macaddr_src[5]);
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);
sprintf(str1,"ip_src %d.%d.%d.%drn",
msg->ipaddr_src[0],msg->ipaddr_src[1],msg->ipaddr_src[2],msg->ipaddr_src[3]);
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);
sprintf(str1,"mac_dst %02X:%02X:%02X:%02X:%02X:%02Xrn",
msg->macaddr_dst[0],msg->macaddr_dst[1],msg->macaddr_dst[2],
msg->macaddr_dst[3],msg->macaddr_dst[4],msg->macaddr_dst[5]);
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);
sprintf(str1,"ip_dst %d.%d.%d.%drn",
msg->ipaddr_dst[0],msg->ipaddr_dst[1],msg->ipaddr_dst[2],msg->ipaddr_dst[3]);
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);
res=2;
}
}
}
}
return res;
Соберём код, прошьём контроллер, отправим ту же строку в терминальной программе, в результате чего мы должны наблюдать вот такую вот картину
Отлично! Мы получили ответ с ПК, ну или с любого устройства, который умеет отвечать на ARP-запросы.
Вернёмся в файл net.c и несколько подправим функцию чтения пакетов Ethernet eth_read. Сначала добавим в неё локальную переменную
void eth_read(enc28j60_frame_ptr *frame, uint16_t len)
{
uint8_t res=0;
Затем исправим код вот здесь
if(frame->type==ETH_ARP)
{
. . .
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);
res = arp_read(frame,len-sizeof(enc28j60_frame_ptr));
if(res==1)
{
arp_send(frame);
}
else if(res==2)
{
}
}
Теперь нам потребуется функция заполнения arp-таблицы. Но сначала нам нужно создать данную таблицу.
В ARP-таблице хранятся IP-адреса уже известных устройств с соответствующими им MAC-адресами. Данные таблицы бывают разной размерностью. Учитывая наши запросы и наши вычислительные мощности, мы сделаем эту таблицу на 5 записей. В записи будет храниться IP-адрес и MAC-адрес устройства и время в секундах, прошедшее с момента включения нашего контроллера. Время нужно для того, чтобы не хранить слишком устаревшие записи, так как за это время IP-адрес может быть присвоен другому устройству. Таблицу мы будем хранить в обычной оперативной памяти. Нам нет нужды хранить её в энергонезависимой памяти из-за ненадобности. Также такая память менее быстрая и обращение в эту таблицу займёт уже другое время да и код получится труднее. Вы, конечно, вполне можете данный механизм реализовать, так как мы работать с такой памятью умеем.
Сначала создадим структуру для таблицы в файле arp.h
uint8_t arp_request(uint8_t *ip_addr);
//--------------------------------------------------
typedef struct arp_record{
uint8_t ipaddr[4];
uint8_t macaddr[6];
uint32_t sec; //какое было количество секунд в переменной clock_cnt, когда была сделана запись
} arp_record_ptr;
//--------------------------------------------------
Затем создадим массив из 5 элементов в файле arp.c, а также ещё одну переменную для хранения текущего позиции заполнения в таблице
uint8_t macnull[6]=MAC_NULL;
arp_record_ptr arp_rec[5];
uint8_t current_arp_index=0;
//--------------------------------------------------
Теперь добавим функцию заполнения данной таблицы
//--------------------------------------------------
void arp_table_fill(enc28j60_frame_ptr *frame)
{
uint8_t i;
}
//--------------------------------------------------
Добавим на данную функцию прототип и вызовем её в net.c в функции eth_read в том месте, где у нас осталось незаполненным тело условия
else if(res==2)
{
arp_table_fill(frame);
}
Вернёмся в arp.c и продолжим писать нашу функцию заполнения ARP-таблицы.
Добавим указатель на ARP-сообщение
uint8_t i;
arp_msg_ptr *msg = (void*)frame->data;
Начнём добавлять запись
arp_msg_ptr *msg = (void*)frame->data;
memcpy(arp_rec[current_arp_index].ipaddr,msg->ipaddr_src,4);
memcpy(arp_rec[current_arp_index].macaddr,msg->macaddr_src,6);
Теперь мы дошли до того момента, когда нам нужно будет в строку нашей таблицы ARP добавить количество секунд. прошедших с момента включения питания контроллера. Можно воспользоваться несколькими способами — включить RTC, включить таймер, воспользоваться переменной, которая считает циклы и т.д. Я решил воспользоваться таймером, заодно и повторим работу с таймером. Может быть впоследствии мы будем пользоваться временем из интернета, полученным с помощью NTP, но пока таймер, так как это уже другие уровни, а до них мы ещё не дошли.
Сохраним и закроем проект в Keil и зайдём в проект Cube MX. Включим там таймер 2
Настроим его в «Configuration» следующим образом
Включим прерывания
Сгенерируем проект для Keil, откроем его и на всякий случай соберём.
Вернёмся в net.c и добавим глобальную переменную
uint8_t ipaddr[4]=IP_ADDR;
uint32_t clock_cnt=0;//счетчик секунд
Добавим функцию-обработчик прерывания по совпадению данного таймера в этом же файле
//-----------------------------------------------
void TIM_PeriodElapsedCallback(void)
{
//считаем секунды и записываем их в clock_cnt
clock_cnt++;
}
//-----------------------------------------------
Сделаем на данную функцию прототип и добавим ещё настоящий обработчик прерывания в файле main.c, в котором и вызовем наш обработчик
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim2)
{
TIM_PeriodElapsedCallback();
}
}
/* USER CODE END 4 */
Также не забываем наш таймер запустить в функции main()
HAL_UART_Receive_IT(&huart1,(uint8_t*)str,1);
HAL_TIM_Base_Start_IT(&htim2);
/* USER CODE END 2 */
В следующей части урока мы закончим функцию отправки запроса ARP, а также напишем код, который не допустит дублирования и просрочивания записей в таблице ARP.
Предыдущая часть Программирование МК STM32 Следующая часть
Техническая документация:
Документация на микросхему ENC28J60
Перечень ошибок ENC28J60 (Errata)
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
How to config timer init int stm32f4. With clk APB1 Timer clk =80Mhz, the same with APB2=80Mhz. I config PSC = 4000; counter (Up) = 1999; but clock_cnt not update. Thanks