STM Урок 96. LAN8720. LWIP. TCP Client. Часть 2

 

 

 

В предыдущей части урока мы познакомились с микросхемой LAN8720, с межканальными интерфейсами и со стеком протоколов LWIP.

 

Ну, что ж. Давайте наконец-то перейдём к нашему проекту.

Наша задача — создать клиент TCP, который будет уметь по своей инициативе соединяться и разъединяться с сервером TCP, а также наша программа должна уметь принимать и видеть пришедшие данные в виде текста, а также отправлять аналогичные данные на сервер TCP. То есть должен будет получиться своего рода чат с использованием протокола TCP.

Проект мы создадим новый. В качестве контроллера выберем, соответственно, наш контроллер STM32F407VGTx

 

image07

 

Затем настроим наш проект.

Первым делом RCC

 

image08

 

Затем программатор

 

image09

 

Включим USART

 

image10

 

Включим таймер на будущее, возможно в данном проекте он нам и не пригодится

 

image11

 

Теперь Ethernet

 

image12

 

Затем подключим библиотеку стека протоколов LWIP

 

image13

 

Включим ножки светодиодов на выход, которые внесут некоторое удобство и наглядность в отладке проекта

 

image14

 

Перейдём в Clock Configuration и настроим делители и умножители для установки требуемых частот (нажмите на картинку для увеличения изображения)

 

image15_0500

 

Переходим в Configuration.

Если у вас в USART6 такие параметры, то здесь ничего не трогаем

 

image16

 

Перейдём в USART6 закладку с прерываниями и включим их

 

image17

 

Затем давайте настроим наш таймер

 

image18

 

Также включим у таймера прерывания

 

image19

 

Теперь ETH. Закладка с параметрами

 

image20

 

Устанавливаем там желаемый MAC-адрес, а также адрес регистра для доступа к микросхеме по RMII. Оставляем 1. А вот оно и отличие!!! 1 — это в случае использования модуля от WaveShare, а 0 — в случае использования платы расширения STM32F4DIS-BB.

Теперь настроим LWIP. Сначала закладка General Settings. Отключим DHCP, использовать будем статическую адресацию, чтобы постоянно не узнавать на другом узле адрес нашего узла. Ну и также в будующих целях. Мы же когда-то будем напрямую друг к другу подключать два контроллера по интерфейсу LAN и вряд ли другой контроллер корректно раздаст IP нашему контроллеру

 

image21

 

Затем перейдём в закладку Key Options, проследим, чтобы галка с расширенными настройками была установлены и изменим здесь некоторые параметры, чтобы сдерать размер окна привычный для нас — 2048

 

image22

 

image23

 

Никакие протоколы больше не трогаем и не подключаем.

Зайдём в настройки проекта, увеличим стек и кучу и настроим генерацию проекта для System Workbench, ну и дадим нашему проекту имя

 

image24

 

Применим настройки, сгенерируем проект, перейдём в System Workbench, подключим там наш сгенерированный проект, зайдём в его настройки, уберём всю отладочную конфигурацию, если таковая имеется, а также уровень оптимизации, как обычно, установим в 1.

Попробуем собрать наш проект.

Перейдём в файл main.c и подключим структуру для нашего сетевого интерфейса

 

/* USER CODE BEGIN PV */

/* Private variables ---------------------------------------------------------*/

extern struct netif gnetif;

/* USER CODE END PV */

 

Впишем вот такой стандартный код в бесконечный цикл для обеспечения постоянной работы нашего стека

 

/* USER CODE BEGIN 3 */

  ethernetif_input(&gnetif);

  sys_check_timeouts();

}

/* USER CODE END 3 */

 

Подключим нашу схему, соберём код, прошьём контроллер и попробуем попинговать наш модуль

 

image25

 

Создадим два файла для работы с сетью — net.h и net.c со следующим первоначальным содержимым

 

net.h:

#ifndef NET_H_

#define NET_H_

//-----------------------------------------------

#include "stm32f4xx_hal.h"

#include <string.h>

#include <stdlib.h>

#include <stdint.h>

#include "lwip.h"

#include "lwip/tcp.h"

//-----------------------------------------------

#endif /* NET_H_ */

 

net.c

#include "net.h"

//-----------------------------------------------

extern UART_HandleTypeDef huart6;

//-----------------------------------------------

 

Подключим также нашу библиотеку в файле main.c

 

/* USER CODE BEGIN Includes */

#include "net.h"

/* USER CODE END Includes */

 

Перейдём теперь в заголовочный файл net.h.

Так как мы пишем клиент TCP, то нам как-то надо будет дать команду на соединение с сервером TCP. Для этого мы должны будем знать его IP-адрес, а также адрес порта. Узнать их, конечно, не тяжело, но они могут каждый раз различаться, поэтому мы не можем жёстко занести их в наш проект. Мы должны их как-то каждый раз проекту передавать в виде параметров команды на соединение. Для этого нам будет служить наша шина USART и делать мы это будем из терминальной программы, в последствии в проекте производя разбор пришедшей оттуда строки. Поэтому для начала давайте напишем структуру со свойствами нашего интерфейса USART

 

#include "lwip/tcp.h"

//-----------------------------------------------

typedef struct USART_prop{

uint8_t usart_buf[26];

uint8_t usart_cnt;

uint8_t is_tcp_connect;//статус попытки создать соединение TCP с сервером

uint8_t is_text;//статус попытки передать текст серверу

} USART_prop_ptr;

//-----------------------------------------------

 

Перейдём теперь в файл net.c и создадим переменную типа нашей структуры

 

extern UART_HandleTypeDef huart6;

USART_prop_ptr usartprop;

 

Также создадим функцию инициализации нашей сети, обнулив в ней значения полей структуры

 

USART_prop_ptr usartprop;

//-----------------------------------------------

void net_ini(void)

{

  usartprop.usart_buf[0]=0;

  usartprop.usart_cnt=0;

  usartprop.is_tcp_connect=0;

  usartprop.is_text=0;

}

//-----------------------------------------------

 

Добавим для нашей функции прототип в заголовочном файле и вызовем её в main() в файле main.c, заодно включим там наш таймер

 

/* USER CODE BEGIN 2 */

HAL_TIM_Base_Start_IT(&htim2);

net_ini();

/* USER CODE END 2 */

 

Вернёмся в net.c и добавим функцию-обработчик прерывания от USART по приёму данных

 

//-----------------------------------------------

void UART6_RxCpltCallback(void)

{

}

//-----------------------------------------------

 

Создадим на эту функцию прототип, перейдём опять в main.c и добавим там настоящий обработчик данного прерывания, в котором мы нашу функцию и вызовем

 

/* USER CODE BEGIN 4 */

//-----------------------------------------------

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

{

  if(huart==&huart6)

  {

    UART6_RxCpltCallback();

  }

}

//-----------------------------------------------

/* USER CODE END 4 */

 

Вернёмся в файл net.c и добавим глобальную переменную в виде строчного массива

 

USART_prop_ptr usartprop;

char str[30];

 

Опять вернёмся в файл main.c и подключим там наш строчный массив

 

extern struct netif gnetif;

extern char str[30];

 

В функции main() вызовем команду на приём символа, иначе наш USART никогда не начнёт приём

 

net_ini();

HAL_UART_Receive_IT(&huart6,(uint8_t*)str,1);

 

Затем вернёмся в файл net.c, перейдём в наш обработчик, создадим там переменную и обработаем случай превышения размера строчного буфера, а также по окончанию проинкрементируем наш счётчик байтов в буфере и вызовем опять команду приёма данных

 

void UART6_RxCpltCallback(void)

{

  uint8_t b;

  b = str[0];

  //если вдруг случайно превысим длину буфера

  if (usartprop.usart_cnt>25)

  {

    usartprop.usart_cnt=0;

    HAL_UART_Receive_IT(&huart6,(uint8_t*)str,1);

    return;

  }

  usartprop.usart_cnt++;

  HAL_UART_Receive_IT(&huart6,(uint8_t*)str,1);

}

 

После проверки превышения буфера занесём принятый символ в буфер

 

  return;

}

usartprop.usart_buf[usartprop.usart_cnt] = b;

usartprop.usart_cnt++;

 

Если мы встретим символ перевода строки, то занесём в следующий за ним элемент строчного массива символ окончания строки

 

usartprop.usart_buf[usartprop.usart_cnt] = b;

if(b==0x0A)

{

  usartprop.usart_buf[usartprop.usart_cnt+1]=0;

}

 

Выше добавим функцию разбора нашей принятой строки

 

//-----------------------------------------------

void string_parse(char* buf_str)

{

}

//-----------------------------------------------

 

Вернёмся в наш обработчик и вызовем данную функцию в условии

 

if(b==0x0A)

{

  usartprop.usart_buf[usartprop.usart_cnt+1]=0;

  string_parse((char*)usartprop.usart_buf);

 

Затем в этом же условии мы после разбора строки обнуляем наш счётчик, посылаем команду приёма символа в шину USART, которая будет ждать следующего символа из шины, и выходим из нашей функции

 

  string_parse((char*)usartprop.usart_buf);

  usartprop.usart_cnt=0;

  HAL_UART_Receive_IT(&huart6,(uint8_t*)str,1);

  return;

}

 

В функции разбора строки отобразим принятую строку в терминальной программе, чтобы увидеть, что мы всё правильно приняли

 

void string_parse(char* buf_str)

{

  HAL_UART_Transmit(&huart6, (uint8_t*)buf_str,strlen(buf_str),0x1000);

}

 

Соберём код, прошьём контроллер, запустим терминальную программу. Я использовал в этот раз Cool Term, так как там есть возможность гибко настроить передачу данных.

Зайдём в настройки данной программы, настроим наш порт и скорость (это, я думаю, не стоит показывать — справитесь самостоятельно), а также настроим передачу по окончанию строки символов возврата каретки и перевода строки

 

image26

 

Соединимся с портом и попробуем что-нибудь передать

 

image27

 

Как мы видим, эхо у нас рабоает, значит строку мы поймали на контроллере правильно. Уточнить, что нам также приходят, а также что мы их не теряем — символ возврата каретки и перевода строки можно, передав следующую строку, она должна будет отобразиться эхом с новой строки, а также нажав кнопку View Hex в тулбаре терминальной программы, и тогда мы увидим коды пришедших символов

 

image28

 

Мы видим, что символы приходят.

 

 

Вернёмся в нашу функцию разбору строки и продолжим писать её тело.

А прежде чем писать код тела функции, давайте договоримся, что мы будем передавать следующую команду на соединение с сервером

t:IP-адрес сервера:адрес порта сервера

А разъединяться с помощью подобной команды

c:IP-адрес сервера:адрес порта сервера

По-идее для разъёдинения не нужно указывать адреса, но вдруг у нас будет несколько соединений и мы захотим разорвать именно определённое из них. Пока у нас будет только одно, но в будущем всё возможно.

А теперь тело

 

  HAL_UART_Transmit(&huart6, (uint8_t*)buf_str,strlen(buf_str),0x1000);

  // если команда попытки соединения ("t:")

  if (strncmp(buf_str,"t:", 2) == 0)

  {

  }

  //статус попытки разорвать соединение ("c:")

  else if (strncmp(buf_str,"c:", 2) == 0)

  {

  }

  else

  {

  }

}

 

Пока у нас будет только три варианта строк — команда на соединение, команда на разъединение, а также обычный текст для чата.

Выше добавим функцию обработки команд

 

//-----------------------------------------------

void net_cmd(char* buf_str)

{

}

//-----------------------------------------------

 

Обработаем команду на соединение с сервером, занеся определённый код в тело соответствующего условия в функции разбора строки

 

if (strncmp(buf_str,"t:", 2) == 0)

{

  usartprop.usart_cnt-=1;

  usartprop.is_tcp_connect=1;//статус попытки создать соединение TCP с сервером

  net_cmd(buf_str+2);

  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);

}

 

Подобную операцию проделаем и в условии для разъединения с сервером

 

else if (strncmp(buf_str,"c:", 2) == 0)

{

  usartprop.usart_cnt-=1;

  usartprop.is_tcp_connect=2;//статус попытки разорвать соединение TCP с сервером

  net_cmd(buf_str+2);

  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, GPIO_PIN_SET);

}

 

Со строкой разберёмся позже. А пока перейдём в функцию обработки команд, добавим там массив из четырёх элементов для IP-адреса, переменную и два условия

 

void net_cmd(char* buf_str)

{

  uint8_t ip[4];

  uint16_t port;

  if(usartprop.is_tcp_connect==1)//статус попытки создать соединение TCP с сервером

  {

  }

  if(usartprop.is_tcp_connect==2)//статус попытки разорвать соединение TCP с сервером

  {

  }

}

 

Выше добавим функции разбора строк со значением IP-адреса и порта, Мы такие функции уже использовали в ранних проектах, поэтому осуждать их не имеет никакого смысла. Поэтому я просто приведу их код

 

port_extract

//-----------------------------------------------

uint16_t port_extract(char* ip_str, uint8_t len)

{

  uint16_t port=0;

  int ch1=':';

  char *ss1;

  uint8_t offset = 0;

  ss1=strchr(ip_str,ch1);

  offset=ss1-ip_str+1;

  ip_str+=offset;

  port = atoi(ip_str);

  return port;

}

//--------------------------------------------------

 

ip_extract

void ip_extract(char* ip_str, uint8_t len, uint8_t* ipextp)

{

  uint8_t offset = 0;

  uint8_t i;

  char ss2[5] = {0};

  char *ss1;

  int ch1 = '.';

  int ch2 = ':';

  for(i=0;i<3;i++)

  {

    ss1 = strchr(ip_str,ch1);

    offset = ss1-ip_str+1;

    strncpy(ss2,ip_str,offset);

    ss2[offset]=0;

    ipextp[i] = atoi(ss2);

    ip_str+=offset;

    len-=offset;

  }

  ss1=strchr(ip_str,ch2);

  if (ss1!=NULL)

  {

    offset=ss1-ip_str+1;

    strncpy(ss2,ip_str,offset);

    ss2[offset]=0;

    ipextp[3] = atoi(ss2);

    return;

  }

  strncpy(ss2,ip_str,len);

  ss2[len]=0;

  ipextp[3] = atoi(ss2);

}

//-----------------------------------------------

 

Создадим также глобальные переменные для хранения адресов нашего сервера, к которому мы, надеюсь, вскоре подключимся

 

#include "net.h"

//-----------------------------------------------

uint8_t ipaddr_dest[4];

uint16_t port_dest;

 

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

 

char str[30];

char str1[100];

 

В самом верху после объявления различных переменных добавим функцию соединения с сервером по протоколу TCP

 

char str1[100];

//-----------------------------------------------

void tcp_client_connect(void)

{

}

//-----------------------------------------------

 

Пусть пока лежит пустотелая, просто нам нужно что-то вызывать в нашей функции обработки команд net_cmd, в которую мы и вернёмся, чтобы написать тело условия соединения с сервером

 

if(usartprop.is_tcp_connect==1)//статус попытки создать соединение TCP с сервером

{

  ip_extract(buf_str,usartprop.usart_cnt-1,ipaddr_dest);

  port_dest=port_extract(buf_str,usartprop.usart_cnt-1);

  usartprop.usart_cnt=0;

  usartprop.is_tcp_connect=0;

  tcp_client_connect();

  sprintf(str1,"%d.%d.%d.%d:%u\r\n", ipaddr_dest[0],ipaddr_dest[1],ipaddr_dest[2],ipaddr_dest[3],port_dest);

  HAL_UART_Transmit(&huart6,(uint8_t*)str1,strlen(str1),0x1000);

  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, GPIO_PIN_SET);

}

 

В теле данного условия мы получим адреса сервера из строки, записав их в глобальные переменные, сбросим статусные переменные, вызовем функцию соединения с сервером, отобразим в терминальной программе адреса, и зажжём светодиод.

Над функцией инициализации добавим ещё одну функцию, которая будет вызываться при создании соединения с сервером, так называемая callback-функция)

 

//-----------------------------------------------

static err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err)

{

  return err;

}

//-----------------------------------------------

 

В качестве аргументов в данную функцию придет указатель на аргументы, также указатель на структуру TCP-соединения и статус возможной ошибки, который мы также вернём обратно по выходу из данной функции.

Добавим для данной функции прототип в этом же файле (не в хедере) повыше

 

char str1[100];

//-----------------------------------------------

static err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err);

//-----------------------------------------------

 

Добавим также глобальную переменную типа структуры соединения TCP и счётчик сообщений

 

char str1[100];

struct tcp_pcb *client_pcb;

__IO uint32_t message_count=0;

 

А также добавим массив для данных

 

char str1[100];

u8_t data[100];

 

Вернёмся теперь писать тело функции соединения с сервером tcp_client_connect

 

void tcp_client_connect(void)

{

  ip_addr_t DestIPaddr;

  client_pcb = tcp_new();

  if (client_pcb != NULL)

  {

    IP4_ADDR( &DestIPaddr, ipaddr_dest[0], ipaddr_dest[1], ipaddr_dest[2], ipaddr_dest[3]);

    tcp_connect(client_pcb,&DestIPaddr,port_dest,tcp_client_connected);

    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, GPIO_PIN_SET);

  }

}

 

Мы создали экземпляр структуры соединения, затем занесли в переменную IP-адрес сервера и вызвали функцию соединения с сервером, в качестве параметров мы в данную функцию передали экземпляр структуры, адрес сервера, порт сервера, а также callback-функцию, которая будет вызываться в случае соединения с сервером. Хоть нам ещё писать много функционала, обеспечивающего надёжную работу клиента, но если мы сейчас соберём код и попробуем соединиться с сервером, то нам скорее всего это уже удастся. Давайте попробуем.

Собираем код, прошиваем контроллер, затем запустим программку netcat на ПК для того чтобы создать TCP-сервер, который будет слушать определённый порт

 

image29

 

Запустим также анализатор трафика Wireshark и дадим команду в терминальной программе на соединение с портом 3333 сервера

 

image30

 

Посмотрим в WireShark

 

image31

 

Мы успешно соединились с сервером. Значит идём по правильному пути.

 

В следующей части урока мы напишем ряд функций и научимся также корректно разъединяться с клиентом, а также передавать ему и принимать с него данные.

 

Предыдущая часть Программирование МК STM32 Следующая часть

 

 

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

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

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

Переходник USB to TTL можно приобрести здесь ftdi ft232rl

 

 

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

 

STM LAN8720. LWIP. TCP Client

7 комментариев на “STM Урок 96. LAN8720. LWIP. TCP Client. Часть 2
  1. Alex:

    PHY adress для F746-дискавери должен быть 0

  2. Семён:

    Добрый день. У меня плата NUCLEO-746ZG Попробовал повторить код:
    /* USER CODE BEGIN 3 */
    ethernetif_input(&gnetif);
    sys_check_timeouts();
    }
    /* USER CODE END 3 */
    И пингонуть, но почему то плата не пингуется. Ни кто не подскажет, почему это может быть?
    Проект делал в кубе, настройки для LwIP стека не менял, так как формировал проект для платы.
    Заранее спасибо.

  3. Семён:

    Огромное спасибо!
    Но 1 ставит кубик для этой платы (:

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

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

*