Сегодня мы продолжим работу по программированию микросхемы 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
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Уважаемый автор, поправьте ссылку в «Программирование МК STM32» 99 урока часть 1, а то она ведет на этот урок.
Поправил. Спасибо за замечание!
После добавления в 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 и пингуется….
Hi, thanks for your lessons. I had a problem with this course. When i tried to connect the server with Putty, Putty gives me this error: «Network error: Software caused connection abort». I disabled the firewall and still gives me this error. Could you help me please?