Продолжаем работать с протоколом TCP и сегодня мы попытаемся уже созданные нами на прошлых занятиях сервер и клиент соединить между собой и научить их обмениваться данными. Думаю, такой урок может пригодиться в будущем, так как зачастую приходится соединять контроллеры между собой, не прибегая вообще к участию в сети компьютеров. Тем более, мы такими вещами уже неоднократно занимались, соединяя между собой платы, используя прежде изученные интерфейсы библиотеки стека протоколов LWIP. Только попробуем мы соединить не два, а три контроллера. Этим мы также занимались, правда использовали мы при этом интерфейс NETCONN и протокол UDP в уроке 123.
Начнём мы по традиции с сервера.
Плата для сервера будет использоваться STM32F746G-DISCOVERY, а проект за основу возьмём из урока 133 с именем LAN8742_TCP_SERVER_SOCKET. Имя проекту мы присвоим LAN8742_TCP_SERVER_SOCKET1, чтобы хоть немного были изменения и впоследствии данные проекты из разных занятий не перепутать.
Запустим наш проект в Cube MX и, ничего в нём не меняя, запустим генерацию проекта для System Workbench, в котором затем удалим при наличии отладочную конфигурацию, а также включим уровень оптимизации традиционно в 1. В файле main.c мы сначала уберём так же, как и в прошлом занятии, массивы на SDRAM.
Удалим здесь
unsigned char *out_buffer;
char *str_usart;
char *str_out;
unsigned char *send_ws_buf;
char* str_buf1;
char* str_buf2;
char* str_buf3;
Вместо этого добавим глобальные массивы в обычной ОЗУ
1 2 3 4 |
#define MAIL_SIZE (uint32_t) 5 char str_usart[512] = {}; char str_out[32] = {}; char str_buf1[30] = {}; |
Также в функции main() удалим инициализацию массивов в памяти SDRAM
//str_usart 512 bytes
str_usart = (char *) 0xC0400000;
//str_out 32 bytes
str_out = (char *) 0xC0400200;
//out_buffer 192 bytes
out_buffer = (uint8_t *) 0xC0400220;
memset(out_buffer,0,192);
//send_ws_buf 1312 bytes
send_ws_buf = (uint8_t *)0xC04002E0;
//str_buf1 256 bytes
str_buf1 = (char *) 0xC0400800;
//str_buf2 256 bytes
str_buf2 = (char *) 0xC0400900;
//str_buf2 1024 bytes
str_buf3 = (char *) 0xC0400A00;
Также в функции задачи вывода строк на дисплей очистим память очереди
1 2 |
TFT_DisplayString(qstruct->x_pos, qstruct->y_pos, (uint8_t *)str_out, LEFT_MODE); osMailFree(strout_Queue, qstruct); |
Для чего мы это делаем, я объяснял.
Из тела функции tcp_thread удалим вывод в USART информации
sprintf(str_usart,» socket: %d\r\n», accept_sock);
HAL_UART_Transmit(&huart1,(uint8_t*)str_usart,strlen(str_usart),0x1000);
В функции client_socket_thread добавим переменную для хранения количества системных квантов, которые нам передадут наши клиенты, и добавим указатель на структуру для очереди
1 2 3 |
int ret, accept_sock; struct_out *qstruct; uint32_t syscnt = 0; |
Далее в этой же функции изменим имя массива для сохранения данных из приёмного буфера
ret = recvfrom(accept_sock, str_buf1, buflen, 0, (struct sockaddr *)&remotehost, &sockaddrsize);
Удалим полностью весь код из тела этого условия
1 2 3 |
if(ret > 0) { } |
Заберём число из буфера
1 2 3 |
if(ret > 0) { syscnt = *(uint32_t*) str_buf1; |
Мы будем просить с клиентов закрывать соединение другим способом. Когда это потребуется, мы передадим ноль в качестве числа.
Поэтому на сервере обработаем такой случай, закрыв при этом соединение и уничтожив текущую задачу
1 2 3 4 5 6 |
syscnt = *(uint32_t*) str_buf1; if(syscnt==0) { close(accept_sock); osThreadTerminate(NULL); } |
Выведем пришедшее число с клиента на дисплее, а также номер сокета и порта клиента. Для этого мы сначала сформируем строку, и, в зависимости от того, с какого клиента пришел пакет (это мы узнаем по номеру порта), мы выведем нашу строку позицию с определённой вертикальной координатой. А если вдруг пакет придёт какой-то левый, либо с неизвестного порта, то мы очищаем память элемента очереди
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
osThreadTerminate(NULL); } qstruct = osMailAlloc(strout_Queue, osWaitForever); sprintf(qstruct->str,"%d %5u %10lu", accept_sock, ntohs(remotehost.sin_port), syscnt); qstruct->x_pos = 10; qstruct->sfont = Font24; if(ntohs(remotehost.sin_port)==1551) { qstruct->y_pos = arg_client_socket->y_pos; osMailPut(strout_Queue, qstruct); } else if(ntohs(remotehost.sin_port)==1552) { qstruct->y_pos = arg_client_socket->y_pos+40; osMailPut(strout_Queue, qstruct); } else { osMailFree(strout_Queue, qstruct); } |
Передадим в качестве ответа серверу число, в 1000 раз меньше, чем пришло от него, то есть преобразуем его в секунды
1 2 3 4 |
osMailFree(strout_Queue, qstruct); } syscnt /= 1000; sendto(accept_sock,(void *) &syscnt,4,0,(struct sockaddr *)&remotehost, sockaddrsize); |
Соберём код, прошьём контроллер. Отключим плату сервера от ПК и запитаем её от независимого источника.
Возьмём такую же плату для первого клиента (вы можете взять другую), подключим её к ПК по USB и также соединим платы между собой, используя коммутатор, так как нам ещё подсоединять третью
Проект для клиента мы возьмём из прошлого занятия с именем LAN8742_TCP_CLIENT_SOCKET и присвоим ему имя LAN8742_TCP_CLIENT_SOCKET1.
Запустим наш проект в Cube MX и, ничего в нём не меняя, запустим генерацию проекта для System Workbench, в котором затем удалим при наличии отладочную конфигурацию, а также включим уровень оптимизации в 1.
В файле main.c в функции send_thread удалим буфер
char buf[15] = {};
Также изменим порт сервера
remotehost.sin_port = htons(7);
Изменим также и сетевой адрес
ip4addr_aton("192.168.1.191",(ip4_addr_t*)&remotehost.sin_addr);
Удалим вывод информации в USART
sprintf(str_usart,«Connected\r\n»);
HAL_UART_Transmit(&huart1,(uint8_t*)str_usart,strlen(str_usart),0x1000);
Если количество системных квантов превысит число 50000, то, кроме того, что мы закроем соединение и уничтожим задачу, мы сначала ещё пошлём серверу ноль, чтобы он знал, что мы собираемся с ним разъединяться и принял определённые меры, которые мы ему уже прописали выше
1 2 3 4 |
if(syscnt>50000) { syscnt = 0; write(sock,(void *) &syscnt,4); |
Также исправим координату Y для вывода информации о разъединении
qstruct->y_pos = arg_sock->y_pos + 80;
Количество системных квантов в том случае, если мы не попали в условие разрыва соединения, мы будем уже выводить на дисплей, а не в терминальную программу. Поэтому сначала подготовим буфер для вывода несколько по-другому, нам же не надо переводить строку и возвращать каретку, а также и буфер уже другой. Также предварительно подготовим память для элемента очереди
1 2 3 4 5 |
osDelay(1); } } qstruct = osMailAlloc(strout_Queue, osWaitForever); sprintf(qstruct->str,"%10lu",syscnt); |
А подготовку буфера старую и вывод в терминальную программу удалим
sprintf(buf,«%lu\r\n»,syscnt);
HAL_UART_Transmit(&huart1,(uint8_t*)buf,strlen(buf),0x1000);
А вместо этого теперь мы в нужную позицию выведем нашу информацию на дисплей
1 2 3 4 5 |
sprintf(qstruct->str,"%10lu",syscnt); qstruct->y_pos = arg_sock->y_pos; qstruct->x_pos = 10; qstruct->sfont = Font24; osMailPut(strout_Queue, qstruct); |
Отправку также немного подправим, так как мы уже отправляем не строку, а непосредственно число
write(sock,(void *) &syscnt, 4);
С отправкой закончили, поэтому переходим к приёму.
В теле функции recv_thread добавим локальную переменную
1 2 |
struct_recv_socket *arg_recv_socket; uint32_t syscnt = 0; |
Удалим ноль
data_buffer[recv_data-1] = 0;
Вместо этого заберём из буфера число, пришедшее с сервера в ответ на переданный пакет
1 2 3 |
if(recv_data > 0) { syscnt = *(uint32_t*) data_buffer; |
Отображение на дисплее также немного исправим, так как у нас была строка.
Во-первых, исправим строку
sprintf(qstruct->str,"%10lu", syscnt);
Во-вторых исправим вертикальную координату
qstruct->y_pos = arg_recv_socket->y_pos + 40;
Соберём код, прошьём контроллер и посмотрим результат
По окончании передачи дисплей также нам отчитался
Теперь переходим ко второму клиенту.
В качестве второго клиента мы будем использовать плату STM32F4-Discovery вместе с платой расширения DIS-BB., а также по I2C подключим к ней дисплей LCD1602.
Плату первого клиента отключим от ПК, запитаем от независимого источника, а плату второго клиента теперь соединим по USB с ПК, а также по интерфейсу LAN с коммутатором
Проект для второго клиента возьмём из урока 123 с именем LAN8720_UDP_CLIENT_NETCONN и присвоим ему имя LAN8720_TCP_CLIENT_SOCKET2.
Откроем наш проект в Cube MX и в настройках FREERTOS немного исправим размер кучи (я так понял, в схеме распределения памяти heap_5 это размер одного региона), а также изменим схему распределения памяти
Сохраним настройки.
Зайдём теперь в настройки LWIP и кое-что там изменим в разделе Key Options, чтобы они были одинаковыми у всех участников нашей сети, так лучше.
Добавим размер кучи
В TCP установим следующие параметры
Немного вернёмся назад и количество одновременно открытых сегментов убавим до 16
Сгенерируем проект для System Workbench, откроем его там, затем удалим при наличии отладочную конфигурацию, а также включим уровень оптимизации в 1.
В файле main.c подключим библиотеку API SOCKET
1 2 |
#include "lwip/api.h" #include "lwip/sockets.h" |
Для пущего порядка поднимем макрос на самый верх секции
1 2 |
/* Private variables ---------------------------------------------------------*/ #define MAIL_SIZE (uint32_t) 5 |
Добавим идентификатор задачи приёма пакетов
1 2 |
#define MAIL_SIZE (uint32_t) 5 osThreadId ThreadRecvHandle; |
Старую структуру соединения удалим вместе с переменной её типа
typedef struct struct_conn_t {
uint32_t conn;
uint32_t buf;
} struct_conn;
struct_conn conn01;
Вместо этого добавим две новые структуры с переменными, как и в проекте для первого клиента
1 2 3 4 5 6 7 8 9 |
} struct_out; typedef struct struct_recv_socket_t { int sock; } struct_recv_socket; struct_recv_socket recv_socket01; typedef struct struct_sock_t { uint16_t port; } struct_sock; struct_sock sock01; |
Мы в основном весь основной код теперь заимствуем из проекта первого клиента.
Объявим массивы регионов для кучи
1 2 3 |
struct_sock sock01; static __attribute__ ((used,section(".user_heap_stack"))) uint8_t heap_sram1[32*1024]; uint8_t heap_sram2[32*1024]; |
Удалим вот этот прототип
void udp_receive_callback(struct netconn* conn, enum netconn_evt evt, u16_t len);
Также удалим и одноимённую функцию udp_receive_callback вместе с телом.
В функции вывода строки на дисплей TaskStringOut очистим память элемента очереди
1 2 |
LCD_String(str1); osMailFree(strout_Queue, qstruct); |
В main() не забываем проинициализировать нашу кучу
1 2 3 4 5 6 7 8 |
/* USER CODE BEGIN 1 */ HeapRegion_t xHeapRegions[] = { { heap_sram1, sizeof(heap_sram1) }, { heap_sram2, sizeof(heap_sram2) }, { NULL, 0 } }; vPortDefineHeapRegions( xHeapRegions ); |
Выше функции send_thread добавим функцию для задачи приёма пакетов от сервера сразу с кодом, так как он практически полностью взят из проекта первого клиента
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//--------------------------------------------------------------- static void recv_thread(void *arg) { struct_out *qstruct; struct_recv_socket *arg_recv_socket; uint32_t syscnt = 0; arg_recv_socket = (struct_recv_socket*) arg; int recv_data; char data_buffer[30] = {}; for(;;) { recv_data = recv(arg_recv_socket->sock,data_buffer,sizeof(data_buffer),0); if(recv_data > 0) { syscnt = *(uint32_t*) data_buffer; qstruct = osMailAlloc(strout_Queue, osWaitForever); sprintf(qstruct->str,"%10lu", syscnt); qstruct->y_pos = 1; osMailPut(strout_Queue, qstruct); } } } //--------------------------------------------------------------- |
В функции send_thread также заменим тело
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
static void send_thread(void *arg) { struct_out *qstruct; struct_sock *arg_sock; int sock; struct sockaddr_in localhost, remotehost; arg_sock = (struct_sock*) arg; uint32_t syscnt = 0; if ((sock = socket(AF_INET,SOCK_STREAM, 0)) >= 0) { memset(&localhost, 0, sizeof(struct sockaddr_in)); localhost.sin_family = AF_INET; localhost.sin_port = htons(arg_sock->port); localhost.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr *)&localhost, sizeof(struct sockaddr_in)) == 0) { memset(&remotehost, 0, sizeof(struct sockaddr_in)); remotehost.sin_family = AF_INET; remotehost.sin_port = htons(7); ip4addr_aton("192.168.1.191",(ip4_addr_t*)&remotehost.sin_addr); if (connect(sock, (struct sockaddr *)&remotehost,sizeof(struct sockaddr_in)) >= 0) { recv_socket01.sock = sock; ThreadRecvHandle = sys_thread_new("recv_thread", recv_thread, (void*)&recv_socket01, DEFAULT_THREAD_STACKSIZE, osPriorityNormal); for(;;) { syscnt = osKernelSysTick(); if(syscnt>60000) { syscnt = 0; write(sock,(void *) &syscnt,4); close(sock); qstruct = osMailAlloc(strout_Queue, osWaitForever); sprintf(qstruct->str,"Disconnected"); qstruct->y_pos = 1; osMailPut(strout_Queue, qstruct); osThreadTerminate(ThreadRecvHandle); for(;;) { osDelay(1); } } qstruct = osMailAlloc(strout_Queue, osWaitForever); sprintf(qstruct->str,"%10lu", syscnt); qstruct->y_pos = 0; osMailPut(strout_Queue, qstruct); write(sock,(void *) &syscnt,4); osDelay(900); } } } else { close(sock); return; } } } |
Функцию udp_thread удалим вместе с телом.
В функции StartDefaultTask удалим создание задачи и добавим туда другой код
1 2 3 |
/* USER CODE BEGIN 5 */ sock01.port = 1552; sys_thread_new("send_thread", send_thread, (void*)&sock01, DEFAULT_THREAD_STACKSIZE * 2, osPriorityNormal); |
Соберём код, прошьём контроллер, На дисплее нашего клиента мы видим, что пакеты наши отлично передаются на сервер и пакеты с сервера также возвращаются
Также по окончанию передачи дисплей перед нами отчитается об этом
Перезагрузим оба клиента одновременно, передача прекрасно видна на сервере с обоих клиентов
Таким образом, мы сегодня создали сеть из 3 узлов на платах с контроллерами STM32 и убедились в её работоспособности.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь 32F746G-DISCOVERY
Отладочную плату можно приобрести здесь: STM32F4-DISCOVERY
Модуль LAN можно приобрести здесь: LAN8720
Плату расширения можно приобрести здесь: STM32F4DIS-BB
Дисплей LCD 16×2 можно приобрести тут: LCD 16×2
Переходник I2C to LCD1602 2004 можно приобрести здесь: I2C to LCD1602 2004
Смотреть ВИДЕОУРОК (нажмите на картинку)
Очень информационно насыщенные и так необходимые многим уроки. Спасибо большое от читателей.
А автор планирует рассказать о протоколе SNMP, которому незаслуженно мало уделяется внимания особенно на уровне «железа»…?
Слышал про такой протокол, но пока не разбирался, но на заметку возьму, так как в вопросах уже звучал.