STM Урок 123. LAN87XX. LWIP. NETCONN. UDP. Соединяем три контролера. Часть 1



В данном занятии мы попробуем соединить три контроллера по сети 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 Следующая часть

 

Исходный код сервера

Исходный код первого клиента

 

 

Отладочную плату можно приобрести здесь 32F746G-DISCOVERY

Отладочную плату можно приобрести здесь: STM32F4-DISCOVERY

Модуль LAN можно приобрести здесь: LAN8720

Плату расширения можно приобрести здесь: STM32F4DIS-BB

Дисплей LCD 16×2 можно приобрести тут: LCD 16×2

Переходник I2C to LCD1602 2004 можно приобрести здесь: I2C to LCD1602 2004

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM LAN87XX. LWIP. NETCONN. UDP. Соединяем три контролера

2 комментария на “STM Урок 123. LAN87XX. LWIP. NETCONN. UDP. Соединяем три контролера. Часть 1
  1. Сергей:

    У меня данный пример не работает. Ну я, правда под Кейлом.
    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 работает.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*