STM Урок 98. LAN8720. LWIP. TCP Server. Часть 1

 

 

 

Сегодня мы продолжим работу по программированию микросхемы LAN8720 с использованием библиотеки стека протоколов LWIP и теперь мы попытаемся создать с помощью неё простенький TCP-сервер. Признаться, я думал, что это будет гораздо легче, чем написать клиент, но не тут-то было. Всё дело в том, что структура TCP, которую мы сохраняем на этапе прослушивания порта, затем теряется после того, как клиент к нам подключится и мне долго пришлось искать, как её поймать. А всё потому, что при прослушке мы ещё не знаем адресов клиента, который к нам подключится. Но затем я догадался постепенно, как всё-таки всё это подловить. Может это и не правильно, но всё корректно работает.

Ну что ж, начнём!

Проект будет сделан полностью из проекта урока 96 LAN8720_TCP_CLIENT и назван LAN8720_TCP_SERVER.

Запустим его в Cube MX.

Так как я решил сегодня использовать плату STM32F4DIS-BB, что намного удобнее, чем модуль на проводах, то я всего лишь изменю адрес в интерфейсе ETH

 

 

Сгенерируем проект и откроем его в System Workbench.

Настроим его как обычно, убрав отладочный пункт и установив уровень оптимизации 1.

Дальше начинаем править код.

Откроем файл net.c и исправим имя функции tcp_client_connect на tcp_server_init

 

void tcp_server_init(void)

 

Создадим для данной функции прототип и вызовем её в файле main.c

 

net_ini();

tcp_server_init();

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

 

Удалим функции port_extract, ip_extract и net_cmd.

Также удалим вызов функции net_cmd из функции string_parse. В данной функции останется только вот это

 

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

void string_parse(char* buf_str)

{

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

  sendstring(buf_str);

  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12|GPIO_PIN_13, GPIO_PIN_RESET);

}

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

 

Исправим глобальную структуру

 

static struct tcp_pcb *server_pcb;

 

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

 

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

enum server_states

{

  ES_NONE = 0,

  ES_ACCEPTED,

  ES_RECEIVED,

  ES_CLOSING

};

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

struct server_struct

{

  u8_t state; /* current connection state */

  u8_t retries;

  struct tcp_pcb *pcb; /* pointer on the current tcp_pcb */

  struct pbuf *p; /* pointer on the received/to be transmitted pbuf */

};

struct server_struct *ss;

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

 

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

Функции tcp_client_connected, tcp_client_connection_close, tcp_client_recv, tcp_client_send, tcp_client_sent и tcp_client_poll мы удалим. Проще их удалить и написать заново, чем править код. Также удалим и их прототипы.

После функции tcp_server_init добавим функцию, которая будет вызываться, когда мы дадим команду нашему серверу слушать соединение

 

static err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err)

{

  err_t ret_err;

  return ret_err;

}

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

 

Создадим прототип для данной функции в текущем файле

 

struct server_struct *ss;

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

static err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err);

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

 

Далее заменим код в функции tcp_server_init

 

void tcp_server_init(void)

{

  server_pcb = tcp_new();

  if (server_pcb != NULL)

  {

    err_t err;

    err = tcp_bind(server_pcb, IP_ADDR_ANY, 7);

    if (err == ERR_OK)

    {

      server_pcb = tcp_listen(server_pcb);

      tcp_accept(server_pcb, tcp_server_accept);

    }

    else

    {

      memp_free(MEMP_TCP_PCB, server_pcb);

    }

  }

}

 

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

Сначала мы создаём экземпляр структуры TCP, в случае успешного создания заносим с помощью специальной функции в данную структуру наш IP-адрес и адрес нашего порта, пусть будет 7. Затем, если всё нормально присвоилось, то мы даём команду на прослушку нашего порта, а затем указываем нашей структуре функцию-обработчик (вернее её адрес), которая и будет вызываться в случае попытки клиента соединиться с нашим сервером. И прежде, чем писать код тела данной функции, давайте после её тела добавим пока каркасы функций-обработчиков, которые нам затем потребуются

 

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

static void tcp_server_error(void *arg, err_t err)

{

}

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

static err_t tcp_server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)

{

err_t ret_err;

return ret_err;

}

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

static err_t tcp_server_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)

{

return ERR_OK;

}

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

static err_t tcp_server_poll(void *arg, struct tcp_pcb *tpcb)

{

err_t ret_err;

return ret_err;

}

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

 

Также создадим на эти обработчики прототипы в текущем файле

 

static err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err);

static void tcp_server_error(void *arg, err_t err);

static err_t tcp_server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);

static err_t tcp_server_sent(void *arg, struct tcp_pcb *tpcb, u16_t len);

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

 

Из функции sendstring пока удалим весь код, чтобы компилятор не ругался при сборке, так как там используется много старых вызовов.

 

 

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

 

err_t ret_err;

HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, SET);

return ret_err;

 

Теперь попробуем собрать код, прошить контроллер, запустить программу WIreShark, отфильтровав её по адресу IP нашего сервера, а также программу Putty, в которой мы и попытаемся соединиться с нашим портом. Как добавить настройки для соединения с сервером TCP в Putty — я рассказывал в одном из своих предыдущих уроков. Я думаю, что вы их уже все посмотрели. Ну а если кто считает, что он уже достиг большего, то значит программа Putty ему тем более знакома.

Пробуем соединиться с сервером.

Соединение прошло успешно

 

 

Также мы это можем наблюдать и в окне вывода WireShark

 

Отлично! Всё корректно и светодиод горит.

Разъединимся с сервером, закрыв программу Putty. Посмотрим, как у нас проходит процесс разрыва соединения в WireShark

 

 

Вместо четырёх шагов разъединение происходит в три шага. Нет, я не забыл захватить в скриншот ещё одну строчку, просто сервер наш пока не умеет подтверждать процесс разрыва соединения. ну хоть отвечает разъединением — и то хорошо! Остальное вылечим чуть позже.

После функции tcp_server_accept. добавим функцию завершения соединения

 

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

static void tcp_server_connection_close(struct tcp_pcb *tpcb, struct server_struct *es)

{

  // remove all callbacks

  tcp_arg(tpcb, NULL);

  tcp_sent(tpcb, NULL);

  tcp_recv(tpcb, NULL);

  tcp_err(tpcb, NULL);

  tcp_poll(tpcb, NULL, 0);

  if (es != NULL)

  {

    mem_free(es);

  }

  tcp_close(tpcb);

}

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

 

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

Для данной функции мы также создадим прототип

 

static err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err);

static void tcp_server_connection_close(struct tcp_pcb *tpcb, struct server_struct *es);

 

Теперь напишем тело нашей функции tcp_server_accept. Удалим из неё код включения светодиода и вставим настоящий код

 

err_t ret_err;

struct server_struct *es;

LWIP_UNUSED_ARG(arg);

LWIP_UNUSED_ARG(err);

tcp_setprio(newpcb, TCP_PRIO_MIN);

es = (struct server_struct *)mem_malloc(sizeof(struct server_struct));

ss = (struct server_struct *)mem_malloc(sizeof(struct server_struct));

if (es != NULL)

{

  es->state = ES_ACCEPTED;

  es->pcb = newpcb;

  ss->pcb = newpcb;

  es->retries = 0;

  es->p = NULL;

  tcp_arg(newpcb, es);

  tcp_recv(newpcb, tcp_server_recv);

  tcp_err(newpcb, tcp_server_error);

  tcp_sent(newpcb, tcp_server_sent);

  tcp_poll(newpcb, tcp_server_poll, 0);

  ret_err = ERR_OK;

}

else

{

  tcp_server_connection_close(newpcb, es);

  ret_err = ERR_MEM;

}

return ret_err;

 

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

Собирать код и прошивать контроллер мы пока не будем. Пока ничего мы для этого ещё не сделали.

После функции tcp_server_recv мы добавим функции отправки пакета клиенту

 

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

static void tcp_server_send(struct tcp_pcb *tpcb, struct server_struct *es)

{

  struct pbuf *ptr;

  err_t wr_err = ERR_OK;

  while ((wr_err == ERR_OK) &&

    (es->p != NULL) &&

    (es->p->len <= tcp_sndbuf(tpcb)))

  {

    ptr = es->p;

    wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);

    if (wr_err == ERR_OK)

    {

      u16_t plen;

      u8_t freed;

      plen = ptr->len;

      es->p = ptr->next;

      if(es->p != NULL)

      {

        pbuf_ref(es->p);

      }

      do

      {

        freed = pbuf_free(ptr);

      }

      while(freed == 0);

      tcp_recved(tpcb, plen);

    }

    else if(wr_err == ERR_MEM)

    {

      es->p = ptr;

    }

    else

    {

      //other problem

    }

  }

}

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

 

Данная функция особых изменений не претерпела со времён урока по клиенту, поэтому в объяснении кода не нуждается.

 

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

 

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

 

 

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

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

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

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

 

 

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

 

STM LAN8720. LWIP. TCP Server

3 комментария на “STM Урок 98. LAN8720. LWIP. TCP Server. Часть 1
  1. Alex:

    Уважаемый автор, поправьте ссылку в «Программирование МК STM32» 99 урока часть 1, а то она ведет на этот урок.

  2. Serg_vrn:

    После добавления в tcp_server_accept:
    err_t ret_err;
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, SET);
    return ret_err;
    соединение не устанавливается. Точнее устанавливается и тут же разрывается сервером. Причина объявление в подпрограмме локальной err_t ret_err; без инициализации вызывает непредсказуемый результат. Вот у меня в отладке ret_err=0xC4 был. Вот после ret_err=ERR_OK; сброс исчез. Как-то так?
    Да, и в этом проекте вот после утреннего включения питания плата НЕ пинговалась. А нажал Reset и пингуется….

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

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

*