В данном занятии мы попробуем соединить три контроллера по сети LAN с использованием протокола UDP.
У нас для этого уже есть все условия, так как мы писали сервер с поддержкой двух одновременных соединений, для которых были организованы раздельные задачи (потоки).
Только мы, конечно, не просто подсоединим ещё одну плату к нашей сети, иначе урок бы получился совершенно неинтересным. Мы на клиентах также будем использовать интерфейс NETCONN вместо RAW.
И оказалось, что это не совсем просто, но, думаю, мы преодолеем все трудности и всё у нас будет работать.
Начнём с сервера. Код сервера у нас изменений вообще никаких не претерпел. Правда, мы всё равно создадим проект, чтобы хотя бы изменилось имя и чтобы нам не путать впоследствии его с проектом предыдущего урока, из которого мы возьмём проект сервера с именем LAN87XX_UDP_SERVER и назовём его LAN8742_UDP_SERVER_NETCONN.
Подключим к ПК нашу плату, которая у нас предназначена для сервера.
Запустим наш преобразованный проект в Cube MX и, ничего в нём не меняя, запустим генерацию проекта для System Workbench, в котором затем удалим при наличии отладочную конфигурацию, а также включим уровень оптимизации традиционно в 1. В файле main.c мы как всегда закомментируем неизвестные компилятору строки, касающиеся видеоускорителя, соберём проект и прошьём контроллер.
Теперь отключим от ПК плату сервера и подключим плату, предназначенную для клиента. У нас будет та же плата, которую мы использовали на предыдущем уроке — STM32F4-Discovery вместе с платой расширения DIS-BB (нажмите на картинку для увеличения изображения)
Так как мы подключаем более двух плат, то обычным кабелем мы не обойдёмся. Поэтому мы подключили коммутатор. Жёлтый цвет на каналах плат говорит о том, что скорость нашими сетевыми микросхемами поддерживается только 100 mbps, а не 1000.
Только для этой платы мы проект создадим заново, так как будет использоваться не только совершенно другой API, но ещё работать проект будет с использованием операционной системы FREERTOS.
Запустим Cube MX и выберем контроллер

Подключим кварцевый резонатор

Выберем отладчик и системный таймер, так как стандартный Systick не любит FREERTOS

Включим шину I2C для индикатора
![]()
Для удобства общения по данной шине переопределим ножки

Включим сетевой интерфейс

Включим использование операционной системы
![]()
Подключим библиотеку стека протоколов LWIP
![]()
Задействуем ножки портов, к которым подключены светодиоды

Перейдём в System Configuration и настроим там всё следующим образом (нажмите на картинку для увеличения изображения)
Перейдём в Configuration и первым делом убедимся, что у нас правильно настроена шина I2C

Настроим также сетевой интерфейс, назначив микросхеме желаемый MAC-адрес, а также присвоив физический адрес 0 (в случае использования отдельного модуля — 1)

Перейдём в настройки LWIP и сначала настроим там статическую адресацию

Пройдя по закладке Key Options, внесём также некоторые привычные поправки и здесь


Также внесём некоторые настройки FREERTOS, добавив немного размер кучи и включив некоторые отладочные возможности, знакомые нам из предыдущих уроков

Перейдя по закладке Tasks and Queues, увеличим также стек задачи по умолчанию

Занесём данные в свойства проекта, увеличив и здесь значение стека и кучи, и сохраним их

Сгенерируем проект для System Workbench, запусти его, удалим при наличии отладочную конфигурацию, а также включим уровень оптимизации в 1. Затем попробуем собрать проект.
Если всё нормально собралось, то Скопируем из проекта прошлого занятия файлы библиотеки для символьного дисплея lcd.h и lcd.c.
Обновим дерево проекта и подключим данную библиотеку в файле main.c, а заодно и библиотеку для работы со строками, а также библиотеки для работы со стеком протоколов
/* USER CODE BEGIN Includes */
#include "lcd.h"
#include "string.h"
#include "lwip/opt.h"
#include "lwip/arch.h"
#include "lwip/api.h"
/* USER CODE END Includes */
В функции main вызовем инициализацию дисплея, переждав некоторое время перед этим, чтобы дать дисплею включиться
/* USER CODE BEGIN 2 */
HAL_Delay(200);
LCD_ini();
/* USER CODE END 2 */
Добавим глобальный строковый массив
/* Private variables ---------------------------------------------------------*/
char str1[30];
Добавим хендлы для очереди и задачи вывода информации на дисплей
char str1[30];
osMailQId strout_Queue;
osThreadId TaskStringOutHandle;
Добавим структуры для очереди и для соединения, а также создадим переменную типа структуры соединения
osThreadId TaskStringOutHandle;
typedef struct struct_out_t {
uint16_t y_pos;
char str[30];
} struct_out;
typedef struct struct_conn_t {
uint32_t conn;
uint32_t buf;
} struct_conn;
struct_conn conn01;
Продефайним размер нашей очереди
struct_conn conn01;
#define MAIL_SIZE (uint32_t) 5
Добавим прототип функции задачи вывода информации на дисплей
/* Private function prototypes -----------------------------------------------*/
void TaskStringOut(void const * argument);
Проинициализируем в main() задачу вывода информации на дисплей
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
osThreadDef(tskstrout, TaskStringOut, osPriorityBelowNormal, 0, 256);
TaskStringOutHandle = osThreadCreate(osThread(tskstrout), NULL);
/* USER CODE END RTOS_THREADS */
Также проинициализируем очередь
/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
osMailQDef(stroutqueue, MAIL_SIZE, struct_out);
strout_Queue = osMailCreate(osMailQ(stroutqueue), NULL);
/* USER CODE END RTOS_QUEUES */
Добавим функцию задачи вывода информации на дисплей
/* USER CODE BEGIN 4 */
//---------------------------------------------------------------
void TaskStringOut(void const * argument)
{
osEvent event;
struct_out *qstruct;
for(;;)
{
event = osMailGet(strout_Queue, osWaitForever);
if (event.status == osEventMail)
{
qstruct = event.value.p;
sprintf(str1,"%s", qstruct->str);
LCD_SetPos(0,qstruct->y_pos);
LCD_String(str1);
}
}
}
//---------------------------------------------------------------
/* USER CODE END 4 */
Код в данной функции нам знаком из прошлых уроков и в объяснении не нуждается.
Добавим функцию задачи для соединения UDP
//---------------------------------------------------------------
static void udp_thread(void *arg)
{
err_t err;
struct netconn *conn;
ip_addr_t DestIPaddr;
for(;;)
{
osDelay(1);
}
}
//---------------------------------------------------------------
Это пока только заготовка функции.
В функции задачи по умолчанию StartDefaultTask создадим данную задачу, чтобы она у нас запустилась
/* USER CODE BEGIN 5 */
sys_thread_new("udp_thread1", udp_thread, NULL, DEFAULT_THREAD_STACKSIZE, osPriorityNormal );
После функции задачи соединения udp_thread добавим функцию обратного вызова для приёма пакетов UDP
//---------------------------------------------------------------
void udp_receive_callback(struct netconn* conn, enum netconn_evt evt, u16_t len)
{
}
//---------------------------------------------------------------
Создадим в данной функции некоторые локальные переменные, а также указатели на структуры
void udp_receive_callback(struct netconn* conn, enum netconn_evt evt, u16_t len)
{
struct_out *qstruct;
uint32_t syscnt;
unsigned short port;
err_t recv_err;
struct netbuf *buf;
Если нам пришел пакет, то примем его
struct netbuf *buf;
if(evt==NETCONN_EVT_RCVPLUS)
{
recv_err = netconn_recv(conn, &buf);
}
Если пакет нормально принят, то узнаем адрес порта, с которого пришел пакет
recv_err = netconn_recv(conn, &buf);
if (recv_err == ERR_OK)
{
port = netbuf_fromport(buf);
}
Используя очередь, выведем на дисплей значение 32-битной целочисленной переменной, которая пришла в буфере и номер порта отправителя пакета, после чего освободим память, отведённую под структуру очереди и под структуру буфера приёма пакета
port = netbuf_fromport(buf);
qstruct = osMailAlloc(strout_Queue, osWaitForever);
qstruct->y_pos = 1;
syscnt = *(uint32_t*) buf->p->payload;
sprintf(qstruct->str,"%5u %7lu", port, syscnt);
osMailPut(strout_Queue, qstruct);
osMailFree(strout_Queue, qstruct);
netbuf_delete(buf);
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15);
}
Создадим на данную функцию прототип
void TaskStringOut(void const * argument);
void udp_receive_callback(struct netconn* conn, enum netconn_evt evt, u16_t len);
Продолжим писать тело функции задачи соединения udp_thread.
Создадим структуру соединения, объявив также и функцию обратного вызова для приёма пакетов
ip_addr_t DestIPaddr;
conn = netconn_new_with_callback(NETCONN_UDP,udp_receive_callback);
Занесём в структуру IP-адрес сервера
conn = netconn_new_with_callback(NETCONN_UDP,udp_receive_callback);
IP4_ADDR(&DestIPaddr, 192, 168, 1, 191);
Проверим, успешно ли создалась структура для соединения
IP4_ADDR(&DestIPaddr, 192, 168, 1, 191);
if (conn!= NULL)
{
}
for(;;)
Назначим порт нашему клиенту и свяжем его со структурой соединения
if (conn!= NULL)
{
err = netconn_bind(conn, NULL, 1555);
В случае удачного выполнения данной функции попытаемся соединиться с сервером, а в обратном случае освободим память, отведённую под структуру соединения
err = netconn_bind(conn, NULL, 1555);
if (err == ERR_OK)
{
err = netconn_connect(conn, &DestIPaddr, 7);
}
else
{
netconn_delete(conn);
}
}
Функцию мы пока не совсем закончили, но сейчас мы добавим выше данной функции функцию задачи отправки пакета серверу, добавив сразу ряд локальных переменных и указателей на различные структуры
//---------------------------------------------------------------
static void send_thread(void *arg)
{
struct_conn *arg_conn;
struct_out *qstruct;
struct netconn *conn;
struct netbuf *buf;
uint32_t syscnt = 0;
arg_conn = (struct_conn*) arg;
conn = (void*)arg_conn->conn;
}
//---------------------------------------------------------------
Мы также получили в качестве параметра указатель на структуру нашего соединения.
Организуем в данной функции цикл, в котором мы регулярно будем отправлять пакеты серверу
conn = (void*)arg_conn->conn;
for(;;)
{
osDelay(1000);
}
Как обычно, в составе пакета у нас будет 32-битное целочисленное значение количества системных квантов, прошедших с момента включения или перезагрузки контроллера.
Поэтому получим сначала данное значение
for(;;)
{
syscnt = osKernelSysTick();
Инициализируем структуру под буфер передачи и зарезервируем под неё память
syscnt = osKernelSysTick();
buf = netbuf_new();
netbuf_alloc(buf, 4);
Отправим в буфер значение количества системных квантов и отправим пакет на сервер
netbuf_alloc(buf, 4);
pbuf_take(buf->p, (void *) &syscnt, 4);
netconn_send(conn,buf);
Освободим память структуры буфера и переключим состояние светодиода
netconn_send(conn,buf);
netbuf_delete(buf);
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
Выведем на дисплей значение, которое мы передали серверу и освободим память под очередь
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
qstruct = osMailAlloc(strout_Queue, osWaitForever);
qstruct->y_pos = 0;
sprintf(qstruct->str,"%10lu", syscnt);
osMailPut(strout_Queue, qstruct);
osMailFree(strout_Queue, qstruct);
osDelay(1000);
Ну и, в принципе, нам осталось создать данную задачу в задаче соединения udp_thread
err = netconn_connect(conn, &DestIPaddr, 7);
if (err == ERR_OK)
{
conn01.conn = conn;
sys_thread_new("send_thread1", send_thread, (void*)&conn01, DEFAULT_THREAD_STACKSIZE, osPriorityNormal );
}
}
else
Соберём код, прошьём контроллер и убедимся, что пакеты от нас на сервер уходят, и затем приходят обратно уже с поделённым на 1000 значением (нажмите на картинку для увеличения изображения)
Всё отлично работает!
В следующей части нашего урока мы мы создадим проект для второго клиента и проверим совместную работу всех трёх узлов нашей сети.
Предыдущий урок Программирование МК STM32 Следующая часть
Отладочную плату можно приобрести здесь STM32F746G-DISCOVERY
Отладочную плату можно приобрести здесь STM32F4-DISCOVERY
Модуль LAN можно приобрести здесь: LAN8720
Плату расширения можно приобрести здесь: STM32F4DIS-BB
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)






У меня данный пример не работает. Ну я, правда под Кейлом.
1. conn01.conn = conn; дает ошибку, пробовал: conn01.conn =(uint32_t) conn;
либо объявить вот так:
typedef struct struct_conn_t {
// uint32_t conn;
struct netconn *conn;
uint32_t buf;
} struct_conn;
либо объявить struct netconn *conn; как глобальную. Но это не решает основной проблемы: err=netconn_send(conn_gl, buf); в результате err = /** Illegal value. */ ERR_VAL = -6, хоть ты тресни. Аналогичная строка из кода NETCONN_SERVER работает.
В кейле построже с типами.