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



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

 

Теперь надо как-то разъединиться, а мы ещё не писали код. В командной строке с netcat применим комбинацию клавиш Ctrl+C и сервер разорвёт соединение с клиентом

 

image32

 

Давайте попробуем разъединиться с нашим сервером.

Для этого в net_cmd напишем код тела второго условия.

 

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

{

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

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

  usartprop.usart_cnt=0;

  usartprop.is_tcp_connect=0;

  //проверим что IP правильный

  if(!memcmp(ip,ipaddr_dest,4))

  {

    //также проверим, что порт тоже правильный

    if(port==port_dest)

    {

      /* close tcp connection */

      tcp_recv(client_pcb, NULL);

      tcp_sent(client_pcb, NULL);

      tcp_poll(client_pcb, NULL,0);

      tcp_close(client_pcb);

      HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_SET);

    }

  }

}

 

Здесь мы также извлекаем из строки IP-адрес и номер порта сервера, заносим их в локальные переменные, сравниваем с глобальными, и если сходится то разъединяемся с сервером, уничтожив сначала адреса callback-функций соединения.

Давайте соберём код, прошьём контроллер и проверим, корректно ли происходит разрыв соединения с сервером по соответствующей команде.

Опять таким же образом с помощью команды соединяемся с сервером, а затем с помощью команды «c:» просим клиент разорвать соединение

 

image33

 

Мы видим, что разрыв соединения произошел корректно.

Отлично! Осталось нам научить наш клиент принимать и передавать данные.

Сначала давайте допишем наши все служебные функции.

Для этого создадим ещё четыре callback-функции, добавив их над функцией инициализации

 

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

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

{

  err_t ret_err;

  return ret_err;

}

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

static void tcp_client_send(struct tcp_pcb *tpcb, struct client_struct * es)

{

}

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

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

{

  return ERR_OK;

}

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

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

{

  err_t ret_err;

  return ret_err;

}

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

 

С назначением данных функций мы познакомимся попозже.

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

 

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

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

static void tcp_client_send(struct tcp_pcb *tpcb, struct client_struct * es);

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

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

 

Создадим перечисление для состояний соединения

 

struct tcp_pcb *client_pcb;

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

enum client_states

{

  ES_NOT_CONNECTED = 0,

  ES_CONNECTED,

  ES_RECEIVED,

  ES_CLOSING,

};

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

 

Затем ещё структуру для свойств клиентского соединения

 

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

struct client_struct

{

  enum client_states state; /* connection status */

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

  struct pbuf *p_tx; /* pointer on pbuf to be transmitted */

};

struct client_struct *cs;

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

 

Назначения полей расшифрованы в комментариях.

Теперь добавим функцию для корректного разрыва соединения с сервером после функции tcp_client_connected

 

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

static void tcp_client_connection_close(struct tcp_pcb *tpcb, struct client_struct * es)

{

  /* remove callbacks */

  tcp_recv(tpcb, NULL);

  tcp_sent(tpcb, NULL);

  tcp_poll(tpcb, NULL,0);

  if (es != NULL)

  {

    mem_free(es);

  }

  /* close tcp connection */

  tcp_close(tpcb);

}

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

 

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

Добавим для этой функции также прототип в текущем файле

 

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

static void tcp_client_connection_close(struct tcp_pcb *tpcb, struct client_struct * es);

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

 

Теперь начнём писать тело функции tcp_client_connected, которая уже давно у нас скучает без тела

 

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

{

  struct client_struct *es = NULL;

  if (err == ERR_OK)

  {

  }

  else

  {

    tcp_client_connection_close(tpcb, es);

  }

  return err;

}

 

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

темерь начнем писать тело условия в случае положительного результата

 

if (err == ERR_OK)

{

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

  if (es != NULL)

  {

  }

  else

  {

    tcp_client_connection_close(tpcb, es);

    return ERR_MEM;

  }

}

 

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

Теперь пишем код тела условия выделения памяти в случае истинного результата

 

if (es != NULL)

{

  es->state = ES_CONNECTED;

  es->pcb = tpcb;

  es->p_tx = pbuf_alloc(PBUF_TRANSPORT, strlen((char*)data) , PBUF_POOL);

}

 

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

 

 

Не выходя из условия, пишем код дальше

 

es->p_tx = pbuf_alloc(PBUF_TRANSPORT, strlen((char*)data) , PBUF_POOL);

if (es->p_tx)

{

  /* copy data to pbuf */

  pbuf_take(es->p_tx, (char*)data, strlen((char*)data));

  /* pass newly allocated es structure as argument to tpcb */

  tcp_arg(tpcb, es);

  /* initialize LwIP tcp_recv callback function */

  tcp_recv(tpcb, tcp_client_recv);

  /* initialize LwIP tcp_sent callback function */

  tcp_sent(tpcb, tcp_client_sent);

  /* initialize LwIP tcp_poll callback function */

  tcp_poll(tpcb, tcp_client_poll, 1);

  /* send data */

  tcp_client_send(tpcb,es);

  return ERR_OK;

}

 

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

Теперь начнём писать тело callback-функции приёма пакетов TCP.

Также добавляем переменную нашей структуры

 

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

{

  struct client_struct *es;

  err_t ret_err;

 

Передаём ей адрес аргументов, которые являются аналогичной стркутурой

 

err_t ret_err;

es = (struct client_struct *)arg;

 

Добавим код с несколькими условиями на разные случаи жизни

 

es = (struct client_struct *)arg;

if (p == NULL)

{

}

else if(err != ERR_OK)

{

}

else if(es->state == ES_CONNECTED)

{

}

else if (es->state == ES_RECEIVED)

{

}

else

{

}

return ret_err;

 

Обработаем первое условие — получение пакета без данных со статусом разъединения. Это как правило пакет с флагом FIN от сервера

 

if (p == NULL)

{

  es->state = ES_CLOSING;

  if(es->p_tx == NULL)

  {

    tcp_client_connection_close(tpcb, es);

  }

  ret_err = ERR_OK;

}

 

В этом случае мы разъединяемся с сервером.

Напишем код тела следующего условия — пришёл непустой пакет со статусом ошибки

 

else if(err != ERR_OK)

{

  if (p != NULL)

  {

    pbuf_free(p);

  }

  ret_err = err;

}

 

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

Обрабатываем следующее условие — пришел пакет при нормальном соединении

 

else if(es->state == ES_CONNECTED)

{

  message_count++;

  tcp_recved(tpcb, p->tot_len);

  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_SET);

  es->p_tx = p;

  strncpy(str1,es->p_tx->payload,es->p_tx->len);

  str1[es->p_tx->len] = '\0';

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

  ret_err = ERR_OK;

}

 

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

Пишем код тела следующего условия — успешного приёма данных

 

else if (es->state == ES_RECEIVED)

{

  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, GPIO_PIN_SET);

  ret_err = ERR_OK;

}

 

В данном случае мы просто зажигаем светодиод и уходим с успешным статусом.

 

 

И последнее тело — это случай, который не подходит под все остальные — как правило это подтверждение приёма данных

 

else

{

  /* Acknowledge data reception */

  tcp_recved(tpcb, p->tot_len);

  /* free pbuf and do nothing */

  pbuf_free(p);

  ret_err = ERR_OK;

}

return ret_err;

 

Пишем код следующей callback-фукции, вызываемой при отправке данных серверу

 

static void tcp_client_send(struct tcp_pcb *tpcb, struct client_struct * es)

{

  struct pbuf *ptr;

  err_t wr_err = ERR_OK;

  while ((wr_err == ERR_OK) &&

  (es->p_tx != NULL) &&

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

  {

  }

}

 

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

Теперь пишем тело нашего цикла

 

while ((wr_err == ERR_OK) &&

(es->p_tx != NULL) &&

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

{

  ptr = es->p_tx;

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

 

Мы получим указатель на буфер и вызовем функцию отправки в буфер содержимого для отправки. Последняя единичка — это флаг. Если единичка — то пакет будет отправлен без флага PSH, а если двойка — то с ним.

Пишем тело цикла дальше. Создадим несколько условий

 

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

if (wr_err == ERR_OK)

{

}

else if(wr_err == ERR_MEM)

{

}

else

{

/* other problem ?? */

}

 

Напишем код тела первого условия — если функция записи в буфер нам вернёт нормальный результат

 

if (wr_err == ERR_OK)

{

  es->p_tx = ptr->next;

  if(es->p_tx != NULL)

  {

    pbuf_ref(es->p_tx);

  }

  pbuf_free(ptr);

}

 

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

Обработаем следующее условие — если ошибка памяти.

 

else if(wr_err == ERR_MEM)

{

  es->p_tx = ptr;

}

 

Мы присваиваем соответствующему полю структуры указатель на буфер, то есть откладываем передачу. То есть у нас буфер пока занят.

Ну а противный случай оставим пока пустым.

Работаем с телом следующей callback-функции — tcp_client_sent, которая вызывается после передачи буфера серверу.

 

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

{

  struct client_struct *es;

  LWIP_UNUSED_ARG(len);

  es = (struct client_struct *)arg;

  if(es->p_tx != NULL)

  {

    tcp_client_send(tpcb, es);

  }

  return ERR_OK;

 

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

И последняя callback-функция, тело которой мы начнём писать это функция tcp_client_poll, которая вызывается постоянно и занимается мониторингом сети, даже если нет никакого обмена

 

err_t ret_err;

struct client_struct *es;

es = (struct client_struct*)arg;

HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14);

 

Мы также создадим указатель на структуру, передадим ему аргументы и изменим состояние светодиода.

Пишем код тела функции дальше

 

HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14);

if (es != NULL)

{

}

else

{

}

return ret_err;

 

Мы создали условие, которое проверит что указатель на структуру соединения существует.

Напишем сначала тело положительного результата условия

 

if (es != NULL)

{

  if (es->p_tx != NULL)

  {

  }

  else

  {

    if(es->state == ES_CLOSING)

    {

      tcp_client_connection_close(tpcb, es);

    }

  }

  ret_err = ERR_OK;

}

 

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

Теперь противный случай

 

else

{

  tcp_abort(tpcb);

  ret_err = ERR_ABRT;

}

return ret_err;

 

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

С функциями-обработчиками событий мы закончили.

Осталось нам обрабоать строку из терминала, не являющуюся командой.

Для этого мы над функцией разбора строки string_parse напишем функцию передачи строки серверу

 

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

void sendstring(char* buf_str)

{

  tcp_sent(client_pcb, tcp_client_sent);

  tcp_write(client_pcb, (void*)buf_str, strlen(buf_str), 1);

  tcp_output(client_pcb);

  usartprop.usart_cnt=0;

  usartprop.is_text=0;

}

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

 

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

Ну и понятно, функция никогда не будет работать, если её не вызвать.

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

 

else

{

  usartprop.is_text=1;

  sendstring(buf_str);

  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12|GPIO_PIN_13, GPIO_PIN_RESET);

}

 

Вот вроде бы и всё с кодом. Осталось посмотреть результат наших трудов.

Соберём код, прошьём контроллер, запустим netcat на прослушку порта, соединимся с ним с помощью специальной команды и попробуем что-нибудь послат компьютеру их терминальной программы

 

image34

 

Посмотрим, пришла ли строка в консоль netcat

 

image35

 

Всё пришло! Отлично!

Теперь наоборот, передадим строку из консоли в наш МК

 

image36

 

Посмотрим, пришло ли сообщение в контроллер, в окне терминальной программы

 

image37

 

Всё пришло.

Передадим команду на разъединение.

Клиент с сервером успешно разъединились.

Посмотрим обмен в WireShark

 

image38

 

Обмен происходил успешно. Можно также посмотреть. все ли байты пришли, но мы этого делать не будем, так как я это неоднократно проверял, и если и были ошибки, я их уже устранил по мере сочинения кода.

Таким образом, мы узнали, как можно организовать простейший TCP-клиент с использованием стека протоколов LWIP при помощи микросхемы LAN8720, включающей в себя только физический уровень.

Спасибо за внимание!

 

 

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

 

Исходный код

 

 

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

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

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

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

 

 

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

 

STM LAN8720. LWIP. TCP Client

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

    «которая уже давно у нас скучает без тела» — минутка шутки юмора)))

  2. Спасибо большое за урок, все заработало.Скажите, а каким образом IP address назначения можно в общепринятом виде задать типа narodstream.ru ?

  3. Максим:

    У вас ошибка в коде на сайте. А в архиве ошибки нет.
    str1[es->p_tx->len] = »;
    , а должно быть
    str1[es->p_tx->len] = '\0';

  4. Сергей:

    добрый день.
    Соединение на момент написания поста не удается установить коннект stm32f407 c netcat x64 запущенной на ОС Windows 10 с последними обновлением. Акула фиксирует
    870 159.220181 192.168.1.193 192.168.1.35 TCP 60 49153 → 3333 [SYN] Seq=0 Win=2048 Len=0 MSS=460
    871 159.220377 192.168.1.35 192.168.1.193 TCP 58 3333 → 49153 [SYN, ACK] Seq=0 Ack=1 Win=65392 Len=0 MSS=1460
    872 159.221911 192.168.1.193 192.168.1.35 TCP 60 49153 → 3333 [ACK] Seq=1 Ack=1 Win=2048 Len=0
    И тут сервер разрывает соединение
    873 159.224575 192.168.1.35 192.168.1.193 TCP 54 3333 → 49153 [RST, ACK] Seq=1 Ack=1 Win=0 Len=0
    Танцы с бубном ничего не дали. В итоге я поднял linux там запустил netcat и коннект и разъединение происходит корректно.

    • Сергей:

      Такое ощущение что блокирует firewall виндовс блокирует корректное TCP соединение У кого нибудь были такие траблы?

  5. ola amigo!
    eu consigo usar tcp cliente e tcp server juntos no mesmo LAN8720?

  6. YRabbit:

    Последняя единичка – это флаг. Если единичка – то пакет будет отправлен без флага PSH, а если двойка – то с ним.»

    Как бы правильно насчёт PSH, но вот единичка как бы независимый флажок:)

    ===== include/lwip/tcp.h =====
    /* Flags for «apiflags» parameter in tcp_write */
    #define TCP_WRITE_FLAG_COPY 0x01
    #define TCP_WRITE_FLAG_MORE 0x02
    ================================

  7. timoflekser1:

    После отправок с сервера 17-ти штук «Hello» STM32 разрывает соединение, соответственно, больше не принимает сообщения от сервера. В чем дело не могу понять. Всегда именно после 17-го «Hello».
    – NUCLEO-STM32F746
    – CoolTerm
    – Hercules
    Скрин не прикрепляю, потому что комментарий уходит в спам.
    В чем дело, подскажите. Хотя бы направьте. Ощущение, что некий буфер переполняется и выходит в ошибку. Не могу найти, что и где ломается.

    • timoflekser1:

      Нашел, в чем была проблема. Не знаю, правильно ли её решил, но суть такая:
      — в фале net.c в функции static err_t tcp_client_recv(…) в условии нормального приема посылки нужно вставить pbuf_free(p).
      Не уверен, что это на 100% правильно, но по крайней мере завелось и заработало корректно.
      Кусок области кода:
      else if(es->state == ES_CONNECTED)
      {
      message_count++;
      tcp_recved(tpcb, p->tot_len);
      HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
      es->p_tx = p;
      strncpy(str1,es->p_tx->payload,es->p_tx->len);
      str1[es->p_tx->len] = '\0';
      HAL_UART_Transmit(&huart3, (uint8_t*)str1,strlen(str1),0x1000);
      pbuf_free(p);
      ret_err = ERR_OK;
      }

      • timoflekser1:

        И все бы ничего, но после нескольких отправок по TCP на STM32 и обратно снова происходит дисконнект. И снова непонятно, в чем причина.

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

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

*