Возвращаемся к теме передачи данных по проводным каналам связи и также возвращаемся к модулю передачи данных по LAN — LAN8720.
И, прежде чем перейти к интерфейсу NETCONN API, давайте ещё раз поработаем с интерфейсом RAW API и напишем простенький UDP Client.
Во-первых, это вызвано огромным количеством просьб по данному протоколу, а во-вторых, мы, скорее всего, сначала в NETCONN API сначала будем работать с протоколом UDP и напишем сервер, и для этого нам обязательно потребуется и клиент. Вот мы всё это потом и соединим.
Плата для нашего урока у нас будет та же самая — STM32F4-Discovery, а микросхему LAN8720 мы будем использовать расположенную на плате DIS-BB. Вы, конечно же, можете использовать и отдельный модуль от WaveShare, только не забудьте в настройках в Cube включить единичку в поле «PHY Addres» в настройках ETH.
Так как мы уже давно прекрасно знаем, что из себя представляет протокол транспортного уровня UDP, а также неплохо себя чувствуем в программировании API RAW стека протоколов LWIP, то можем, в принципе, сразу перейти к проекту.
Чтобы нам не мучиться с настройками проекта, проект мы сделаем на основе проекта урока 96 LAN8720_TCP_CLIENT и назовём его LAN8720_UDP_CLIENT_RAW.
Откроем наш проект в Cube MX, перейдём в Configuration в настройки ETH, убедимся, что там стоит 0 в физическом адресе, а также немного изменим MAC-адрес
Затем перейдём настройки таймера и настроим там период приблизительно в 1 секунду
Также убедимся, что прерывания от таймера у нас включены.
Сгенерируем проект и откроем его в System Workbench.
Как обычно, настроим уровень оптимизации в 1, уберём отладочную конфигурацию при её наличии, сохраним настройки и попробуем собрать наш проект.
Если проект нормально собрался, то начнём с ним работать.
Перейдём в файл net.c и удалим там весь код кроме подключения заголовочного файла. Останется только это
#include "net.h"
//-----------------------------------------------
Добавим обработчик от таймера, в котором пока только изменим уровень ножки синего светодиода
//-----------------------------------------------
void TIM1_Callback(void)
{
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15);
}
//--------------------------------------------------
Добавим глобальную переменную указателя на структуру UDP, строковый массив, а также добавим функцию установки соединения UDP. Хотя данный протокол этого и не предусматривает, но у нас здесь будет в основном инициализация структуры для обмена по UDP, а также объявление функции-обработчика пришедших пакетов по UDP. Пока мы в функции только вызовем конструктор структуры
#include "net.h"
//-----------------------------------------------
struct udp_pcb *upcb;
char str1[30];
//-----------------------------------------------
void udp_client_connect(void)
{
ip_addr_t DestIPaddr;
err_t err;
upcb = udp_new();
}
//-----------------------------------------------
Перейдём в заголовочный файл net.h, уберём там ненужные прототипы функций и добавим прототипы добавленных нами функций
void net_ini(void);
void UART6_RxCpltCallback(void);
void udp_client_connect(void);
void TIM1_Callback(void);
Структуру, объявленную ниже также удалим.
Также поменяем подключаемую библиотеку tcp на udp
#include "lwip/udp.h"
Идём теперь в файл main.c, из функции main() уберём вызов функции инициализации и вызов функции приёма байта из USART, а вызовем вместо всего этого функцию организации соединения UDP
net_ini();
HAL_UART_Receive_IT(&huart6,(uint8_t*)str,1);
udp_client_connect();
Из функции-обработчика прерываний от USART удалим вызов нашей функции-обработчика
if(huart==&huart6)
{
UART6_RxCpltCallback();
}
После данной функции добавим ещё функцию-обработчик прерываний от таймера, в которой вызовем нашу самодельную функцию-обработчик
//-----------------------------------------------
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim2)
{
TIM1_Callback();
}
}
//-----------------------------------------------
На данном этапе давайте соберём проект, прошьём контроллер и попробуем попинговать нашу плату с целью проверки её доступности
Всё нормально! Модуль и плата доступны.
Вернёмся в файл net.c и добавим в ней прототип функции-обработчика пришедших пакетов по UDP
char str1[30];
//-----------------------------------------------
void udp_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port);
//-----------------------------------------------
Продолжаем писать функцию udp_client_connect.
Добавим условие проверки создания структуры
upcb = udp_new();
if (upcb!=NULL)
{
}
В теле данного условия инициализируем переменную IP сервера, которому мы будем отправлять пакеты, и от которого будем также их принимать. В качестве сервера у нас будет ПК, поэтому адрес будем использовать именно его
if (upcb!=NULL)
{
IP4_ADDR( &DestIPaddr, 192, 168, 1, 87);
}
Также запишем в структуру в соответствующее поле порт нашего клиента. К данному порту будет обращаться ПК, чтобы отправлять пакеты
IP4_ADDR( &DestIPaddr, 192, 168, 1, 87);
upcb->local_port = 1555;
Вызовем функцию соединения, в которой в качестве одного из входных аргументов передадим также и порт сервера (в нашем случае программы на ПК)
upcb->local_port = 1555;
err= udp_connect(upcb, &DestIPaddr, 1556);
Если соединение нормально инициализировалось, то также объявим имя функции—обработчика пришедших пакетов по UDP
err= udp_connect(upcb, &DestIPaddr, 1556);
if (err == ERR_OK)
{
udp_recv(upcb, udp_receive_callback, NULL);
}
После данной функции добавим функцию передачи пакета, в теле которой пока только объявим указатель на структуру буфера
//-----------------------------------------------
void udp_client_send(void)
{
struct pbuf *p;
}
//-----------------------------------------------
После функции передачи пакета добавим функцию—обработчик пришедших пакетов по UDP
//-----------------------------------------------
void udp_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
strncpy(str1,p->payload,p->len);
str1[p->len]=0;
pbuf_free(p);
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
//-----------------------------------------------
В данной функции мы скопируем пришедшие данные из буфера в строковый массив, завершим его нулём, освободим память буфера и мигнём зелёным светодиодом. Отображать данные мы пока нигде не будем. Это нам потребуется в следующих занятиях, а пока это не цель нашего урока, но данные эти мы немного позже все равно как-то посмотрим.
Вызовем функцию передачи пакета из функции-обработчика прерываний таймера
void TIM1_Callback(void)
{
udp_client_send();
Теперь вернёмся в функцию передачи пакета udp_client_send и что-нибудь попробуем там передать.
Сначала подготовим строку для передачи, занеся в неё количество системных тиков, прошедших с момента включения или перезагрузки контроллера
struct pbuf *p;
sprintf(str1,"%lu\r\n",HAL_GetTick());
Выделим память под данные в буфере
sprintf(str1,"%lu\r\n",HAL_GetTick());
p = pbuf_alloc(PBUF_TRANSPORT, strlen(str1), PBUF_POOL);
Если выделение памяти прошло успешно, то передадим нашу строку серверу и освободим буфер
p = pbuf_alloc(PBUF_TRANSPORT, strlen(str1), PBUF_POOL);
if (p != NULL)
{
pbuf_take(p, (void *) str1, strlen(str1));
udp_send(upcb, p);
pbuf_free(p);
}
Соберём код, прошьём контроллер и перейдём к ПК.
Зайдём в каталог с программой netcat и запустим команду cmd.
затем в командной строке мы запустим следюущую команду
Значение после параметра -p — это номер порта программы netcat. Оказывается, его также можно назначать, я только недавно узнал, изучая встроенную справку. Больше я такую информацию нигде не видел, а затем уже идёт IP-адрес платы, а затем номер порта нашего соединения в плате.
И, как мы можем наблюдать, пакеты приходят нормально. Вследствие очень сильной загруженности сети, в которой я всё это проверял, некоторые пакеты теряются. Мы это отчётливо видим. Например, после пакета со строкой «20792» идёт сразу пакет со строкой «22792», а это значит, что пакет «21792» у нас потерялся. А вообще, благодаря использованию таймера, у нас пакеты отправляются в строго заданное время, о чём свидетельствуют неизменные последние три цифры. Следовательно, таймер мы настроили правильно.
Теперь давайте что-нибудь передадим из ПК в плату. Для этого введём в командной строке, не разрывая соединение какую-нибудь строку и нажмём клавишу «Enter»
После этого на плате должен засветиться зелёный светодиод
Если мы отправим ещё пакет, то светодиод должен будет потухнуть.
Давайте всё-таки прочитаем из нашего контроллера переданную в него из ПК строку. Для этого поставим точку останова в функции-обработчике приёма пакета вот в этом месте
Запустим отладку с помощью вот такой кнопочки
Затем запустим проект на выполнение в отладке
Проект начнёт работать, пакеты также будут уходить в ПК по сети.
Отправим из ПК такую же строку. После этого мы должны будем попасть в точку останова
Добавим наш строчный массив в Expression и посмотрим его содержимое
Всё соответствует.
Остановим отладку, вернёмся в обычное представление и запустим наш проект на выполнение. Теперь мы можем дальше наслаждаться приёмом отправленных строк в ПК.
Таким образом, сегодня мы написали несложный клиент для работы с протоколом UDP.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь STM32F4-DISCOVERY
Модуль LAN можно приобрести здесь: LAN8720
Плату расширения можно приобрести здесь: STM32F4DIS-BB
Смотреть ВИДЕОУРОК (нажмите на картинку)
Передаю большой привет всем счастливым обладателям STM32H745Zit6!
Если вы, как и я, наслаждаетесь настройкой LwIP'a на этом МК, то для вас у меня ряд хороших новостей:
Для настройки я прошел путь как одном из моих комметов (урок 96. LAN8720 https://narodstream.ru/stm-urok-96-lan8720-lwip-tcp-client-chast-2/ ), однако перенастроился на работу без DHCP — пример из гитхаба в том уроке работает на DHCP, это позволяет вставлять и успешно работать с, например, роутером. Т.к. в моем проекте данные должны идти максимально быстро на ПК, то я настроился на статический IP.
Тут вылез нюанс номер раз:
В CubeMX при генерации кода без DHCP у вас в функции MX_LWIP_Init(), которая находится в файлике lwip.c, сгенерируется три массива для статических IP, а именно uint8_t IP_ADDRESS[4],
uint8_t NETMASK_ADDRESS[4] и uint8_t GATEWAY_ADDRESS[4], однако даже если вы заполните в CubeMX эти поля, в массивы они не передадутся. Придется эти массивы заполнять ручками.
Дальше. Если вы в своем проекте примените код один в один как у автора, вы попадете на следующие грабли: STM будет отсылать пакет всего лишь один раз. Больше не будет до перезагрузки или включения-отключения Ethernet-кабеля. Дебаг будет честно сообщать в терминал что все хорошо и исправно отсылается. Тем не менее даже в Wireshark вы ни одного пакета не увидите. При этом прием данных от ПК исправный.
Дело в том, что у автора в коде применена функция p = pbuf_alloc(PBUF_TRANSPORT, strlen(str1), PBUF_POOL);
Тем не менее один из её аргументов — PBUF_POOL для нас, счастливых владельцев STM32H7, не подходит. Вам нужно поменять это значение на PBUF_RAM.
Цитирую документацию: «pbuf data is stored in RAM, used for TX mostly, struct pbuf and its payload are allocated in one piece of contiguous memory (so the first payload byte can be calculated from struct pbuf). pbuf_alloc() allocates PBUF_RAM pbufs as unchained pbufs (although that might change in future versions). This should be used for all OUTGOING packets (TX).»
Форумы ST честно сообщают, что код для LwIP писали безграмотные индусы. Так что свой экземпляр программы загружу на git и отправлю ссылку сюда сразу как закончу проект. 🙂
Пожалуйста, здесь не надо постить ссылки на сторонние ресурсы.
Принял, не буду.
В целом описанного здесь более чем хватает для того, чтобы заставить работать.
не могли бы вы привести пример связи по протоколу SNMP.
Ваши материалы — просто кладезь знаний какой-то!
Всё заработало с первого раза.
Единственно, стоит отметить, что в моём случае пакеты дольше «прожёвывались» сетью и начали поступать в неткат спустя 15 секунд.