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

14 комментариев на “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 ставит кубик для этой платы (:

  4. Serg_vrn:

    Собрал. Светодиоды загораются. В шарке вижу четыре попытки платы соединиться с сервером:
    7 2.653839 192.168.137.254 192.168.137.101 TCP 60 49153 → 3333 [SYN] Seq=0 Win=2048 Len=0 MSS=460
    10 5.653704 192.168.137.254 192.168.137.101 TCP 60 [TCP Retransmission] 49153 → 3333 [SYN] Seq=0 Win=2048 Len=0 MSS=460
    11 8.653609 192.168.137.254 192.168.137.101 TCP 60 [TCP Retransmission] 49153 → 3333 [SYN] Seq=0 Win=2048 Len=0 MSS=460
    12 11.653494 192.168.137.254 192.168.137.101 TCP 60 [TCP Retransmission] 49153 → 3333 [SYN] Seq=0 Win=2048 Len=0 MSS=460
    а подтверждения нет. Из-за чего эт может быть? Может быть nc в 64 разрядной системе не работает? И почему Seq=0?

    • От ОС не зависит. У Вас модуль или плата расширения? Если модуль то адрес PHY надо выставлять в 1, а если плата — то в 0.
      Хотя по ходу пакеты из неё у Вас в ПК идут. странно, изучайте эти пакеты. Мы на данном этапе хорошо знаем проядок следования пакетов и вообще всё по транспортным протоколам, думаю, разберётесь.

  5. Serg_vrn:

    Ура! Это был Firewall от EsetNode

  6. Serg_vrn:

    Замечено и не только мной (нас тут двое на предприятии и Дискавери тоже несколько), что иногда Ethernet не стартует. Плата не пингуется. А потом нажмешь на кнопочку Resrt и все заработало. С прошивкой не связано. Если что — галочка Reset&Run проверено. Что это?

  7. Yes:

    Hello I have followed same procedure and generate code for stm32f407 with LAN8720A. and put below function in loop. It is compiled successfully but it did't take ip and can't ping the module. I have done proper connection between stm32f407 and LAN8720. So can you help me. If you have sample code with same lan8720a module kindly share with me.

    ethernetif_input(&gnetif);

    sys_check_timeouts();

    Thanks,
    Yesh Valiya

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

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

*