STM Урок 86. LAN. ENC28J60. TCP WEB Server. Передаём малую страницу. Часть 2



 

Урок 86

 

Часть 2

 

LAN. ENC28J60. TCP WEB Server. Передаём малую страницу

 

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

 

Теперь наконец-то перейдём к работе с протоколом HTTP и, вернувшись в файл tcp.с подумаем, как нам добавить массив для заголовка ответного пакета HTTP, при этом не «съев» внушительное количество оперативной памяти.

А воспользуемся мы для этого местом во флеш-памяти, в которой у нас запаса свободного места побольше.

Для этого применим объявление const, объявив следующий глобальный массив

 

volatile uint8_t tcp_stat = TCP_DISCONNECTED;

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

const char http_header[] = {"HTTP/1.1 200 OKrnServer: nginxrnContent-Type: text/htmlrnConnection: keep-alivernrn"};

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

 

После этого данная строка будет храниться именно во флеш-памяти и оттуда и будет вызываться при необходимости.

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

У нас всего три строки, я думаю для того, чтобы клиентский браузер понял, что в заголовке, этого достаточно.

1)  HTTP/1.1 200 OK

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

Коды всех сообщений можно найти в официальных документах описания протокола HTTP. Скажу только то, что коды начинающиеся с цифры 2 — это группа кодов, которые информируют о случаях успешного принятия и обработки запроса клиента. Если код начинается с 1, то это значит, что сервер информирует о процессе передачи, если с 3 — то это перенаправление и клиент должен будет сделать другой запрос, а если 4, то сервер таким образом говорит клиенту об ошибке. Мы часто при ошибочном наборе адреса видим сообщение 404 — страница не найдена. Это один из примеров ошибки. Как правило, на сервере хранится страница, в которой находится удобочитаемая информация об отсутствии страницы, и тогда, когда клиент запрашивает несуществующий документ на сервере, то сервер после передачи такого кода передаёт также эту страницу.

2) Server: nginx

Тип сервера. В нашем случае это не так важно и я передал самый часто используемый тип.

3) Connection: keep-alive

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

Теперь в функции tcp_read в том месте, где мы вызываем функцию передачи данных, немного изменим код, в котором определим, какие именно у нас данные — обычные или запрос HTTP

 

if (tcp_pkt->fl&TCP_ACK)

{

  //Если строка "GET / ", то значит это запрос HTTP главной страницы, пока будем работать только с одной

  if (strncmp((char*)tcp_pkt->data,"GET / ", 6) == 0)

  {

  }

  //Иначе обычные данные

  else

  {

    tcp_send_data(frame, ip_pkt->ipaddr_src, tcpprop.port_dst);

  }

}

 

Пока условие оставим с пустым телом, так как далее в этом теле нужно написать код, который определит размер нашего пакета HTTP, который мы будем отправлять с целью, чтобы узнать, влезет ли у нас данный пакет в один пакет TCP или их будет несколько, а если несколько, то сколько именно и сколько именно байт в последнем пакете. А, как известно в пакет HTTP кроме заголовка также входят и данные, в качестве которых в нашем случае будет главная страница, которую мы пока не подготовили. Мы также разместим её содержимое в память FLASH, но только не в текстовом виде, так как в тексте может быть разные символы, в том числе и те, которые не очень понравятся компилятору, а в виде массива целых 8-битных чисел, который мы создадим с помощью утилиты makefsdata.exe, которую несложно найти в интернете и которая поставляется в виде одного исполняемого файла, также данная утилита требует установленных библиотек Microsoft Visual C++ Redistributable. Если пакет не установлен, то утилита запросит недостающую библиотеку. Ну это я думаю для нас не проблема.

Поместим файл makefsdata.exe в отдельную папку, также создадим в данной папке ещё одну папку с именем «fs» и поместим в созданную папку нужную страницу index.html, например с таким содержимым

 

<html><body><h1 style="text-align: center;">STM32F103x8<br><br>WEB Server</h1>

<p></p>

<h2>Features</h2>

<p>ARM® 32-bit Cortex®-M3 CPU Core</p>

</body></html>

 

Запустим утилиту и у нас должен будет создаться файл с именем fsdata.c.

Прежде чем его открывать заготовим место, куда мы поместим данные созданного массива в нашем проекте сразу же после объявления массива заголовка HTTP

 

const char http_header[] = {"HTTP/1.1 200 OKrnServer: nginxrnContent-Type: text/htmlrnConnection: keep-alivernrn"};

const uint8_t index_htm[] = {};

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

 

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

Теперь откроем файл fsdata.c, найдём там вот такоей место

 

/* raw file data (160 bytes) */

 

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

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

 

const uint8_t index_htm[] = {

0x3c,0x68,0x74,0x6d,0x6c,0x3e,0x3c,0x62,0x6f,0x64,0x79,0x3e,0x3c,0x68,0x31,0x20,

0x73,0x74,0x79,0x6c,0x65,0x3d,0x22,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,

0x6e,0x3a,0x20,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x22,0x3e,0x53,0x54,0x4d,0x33,

0x32,0x46,0x31,0x30,0x33,0x78,0x38,0x3c,0x62,0x72,0x3e,0x3c,0x62,0x72,0x3e,0x57,

0x45,0x42,0x20,0x53,0x65,0x72,0x76,0x65,0x72,0x3c,0x2f,0x68,0x31,0x3e,0x0a,0x3c,

0x70,0x3e,0x3c,0x2f,0x70,0x3e,0x0a,0x3c,0x68,0x32,0x3e,0x46,0x65,0x61,0x74,0x75,

0x72,0x65,0x73,0x3c,0x2f,0x68,0x32,0x3e,0x0a,0x3c,0x70,0x3e,0x41,0x52,0x4d,0xc2,

0xae,0x20,0x33,0x32,0x2d,0x62,0x69,0x74,0x20,0x43,0x6f,0x72,0x74,0x65,0x78,0xc2,

0xae,0x2d,0x4d,0x33,0x20,0x43,0x50,0x55,0x20,0x43,0x6f,0x72,0x65,0x3c,0x2f,0x70,

0x3e,0x0a,0x3c,0x2f,0x62,0x6f,0x64,0x79,0x3e,0x3c,0x2f,0x68,0x74,0x6d,0x6c,0x3e

};

 

Вот это и есть текст нашей странице, преобразованный в массив.

 

 

Вернёмся теперь в функцию tcp_read в то место, где у нас осталось пустое тело условия наличия пакета HTTP в пакете TCP, измерим там отправляемый пакет и определим во сколько он сегментов влезет, а также длину данных в последнем сегменте и отобразим всё это, включая ещё и порт отправителя, в терминальной программе

 

if (strncmp((char*)tcp_pkt->data,"GET / ", 6) == 0)

{

  tcpprop.data_size = strlen(http_header) + sizeof(index_htm);

  tcpprop.cnt_data_part = tcpprop.data_size / tcp_mss + 1;

  tcpprop.last_data_part_size = tcpprop.data_size % tcp_mss;

  sprintf(str1,"data size:%lu; cnt data part:%u; last_data_part_size:%urnport dst:%urn",

  (unsigned long)tcpprop.data_size, tcpprop.cnt_data_part, tcpprop.last_data_part_size,tcpprop.port_dst);

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

}

 

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

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

Мы видим в терминальной программе следующую информацию

 

image05

 

Теперь в файле tcp.h после статусов TCP добавим ещё некоторые статусы передачи данных

 

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

//Статусы передачи данных

#define DATA_COMPLETED 0 //передача данных закончена

#define DATA_ONE 1 //передаём единственный пакет

#define DATA_FIRST 2 //передаём первый пакет

#define DATA_MIDDLE 3 //передаём средний пакет

#define DATA_LAST 4 //передаём последний пакет

#define DATA_END 5 //закрываем соединение после передачи данных

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

 

Вернёмся в tcp.c туда же, откуда и ушли, и вставим там код для установки статуса передачи

 

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

  if (tcpprop.cnt_data_part==1)

  {

    tcpprop.data_stat = DATA_ONE;

  }

  else if (tcpprop.cnt_data_part>1)

  {

    tcpprop.data_stat = DATA_FIRST;

  }

}

 

Над функцией tcp_read добавим функцию отправки ответа на HTTP-запрос в случае, если он умещается в один пакет, наполнив её тело пока стандартным содержимым

 

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

/*Отправка однопакетного ответа HTTP*/

uint8_t tcp_send_http_one(enc28j60_frame_ptr *frame, uint8_t *ip_addr, uint16_t port)

{

  uint8_t res=0;

  uint16_t len=0;

  uint16_t sz_data=0;

  ip_pkt_ptr *ip_pkt = (void*)(frame->data);

  tcp_pkt_ptr *tcp_pkt = (void*)(ip_pkt->data);

  return res;

}

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

 

 

Опять вернёмся туда, где мы определили размер и установили статусы в функции tcp_read, и вызовем там нашу функцию, если у нас данных именно на один пакет

 

    tcpprop.data_stat = DATA_FIRST;

  }

  if(tcpprop.data_stat==DATA_ONE)

  {

    tcp_send_http_one(frame, ip_pkt->ipaddr_src, tcpprop.port_dst);

  }

}

 

Продолжим писать тело нашей функции tcp_send_http_one.

На всякий случай ещё раз имерим наши данные

 

tcp_pkt_ptr *tcp_pkt = (void*)(ip_pkt->data);

//Отправим сначала подтверждение на пакет запроса

sz_data = be16toword(ip_pkt->len)-20-(tcp_pkt->len_hdr>>2);

 

Заполним некоторые поля и вызовем функцию подготовки заголовка TCP

 

sz_data = be16toword(ip_pkt->len)-20-(tcp_pkt->len_hdr>>2);

tcpprop.seq_num = tcp_pkt->num_ask;

tcpprop.ack_num = be32todword(be32todword(tcp_pkt->bt_num_seg) + sz_data);

len = sizeof(tcp_pkt_ptr);

tcp_header_prepare(tcp_pkt, port, TCP_ACK, len);

 

Затем, подготовив заголовок IP, отправим наш пакет

 

tcp_header_prepare(tcp_pkt, port, TCP_ACK, len);

len+=sizeof(ip_pkt_ptr);

ip_header_prepare(ip_pkt, ip_addr, IP_TCP, len);

//Заполним заголовок Ethernet

memcpy(frame->addr_dest,frame->addr_src,6);

eth_send(frame,ETH_IP,len);

 

Можно считать, что подтверждение мы отправили. Теперь в этой же функции отправим и ответ клиенту в виде заголовка и самой страницы.

Для этого сформируем сначала поле данных в заголовке TCP. мы не будем создавать отдельную структуру для заголовка и данных HTTP ввиду неструктурированности последнего

 

eth_send(frame,ETH_IP,len);

//Отправляем страницу

strcpy((char*)tcp_pkt->data,http_header);

memcpy((void*)(tcp_pkt->data+strlen(http_header)),(void*)index_htm,sizeof(index_htm));

 

Сначала мы копируем заголовок, а затем по адресу, увеличенному на размер заголовка, саму страницу.

Установим нужные флаги, посчитаем длину пакета TCP, включая данные, но так как данные не входят в поле, предназначенное для них в заголовке TCP, то мы его не заполняем, так как оно уже заполнено. Затем пересчитаем контрольную сумму TCP, так как в её расчёте участвуют и данные

 

memcpy((void*)(tcp_pkt->data+strlen(http_header)),(void*)index_htm,sizeof(index_htm));

len = sizeof(tcp_pkt_ptr);

len+=tcpprop.data_size;

tcp_pkt->fl = TCP_PSH|TCP_ACK;

tcp_pkt->cs = 0;

tcp_pkt->cs=checksum((uint8_t*)tcp_pkt-8, len+8, 2);

 

Далее мы рассчитаем длину пакета IP, занесём её в соответствующее поле заголовка, пересчитаем контрольную сумму заголовка IP и отправим наш пакет, опять же не забывая о том, что адреса MAC мы местами не меняем, так как это сделано выше. И по окончанию мы устанавливаем нужный статус, который нам поможет отправить запрос на завершение соединения после приёма пакета с подтверждением от клиента

 

tcp_pkt->cs=checksum((uint8_t*)tcp_pkt-8, len+8, 2);

len+=sizeof(ip_pkt_ptr);

ip_pkt->len=be16toword(len);

ip_pkt->cs = 0;

ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);

//Заполним заголовок Ethernet

eth_send(frame,ETH_IP,len);

tcpprop.data_stat=DATA_END;

return res;

 

Можно сейчас, в принципе собрать код и прошить контроллер. Хоть мы страницу в браузере не увидим, так как браузер её отобразит только по завершению соединения, но хотя бы посмотрим наши пакеты в Wireshark. Поэтому попробуем ещё раз запросить в браузере нашу страницу (нажмите на картинку для увеличения изображения)

 

image06_0500

 

Мы видим, что наше подтверждение и наш ответ HTTP клиент получил.

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

 

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

/*Разъединяемся после получения подтверждения на последний пакет данных*/

uint8_t tcp_send_http_dataend(enc28j60_frame_ptr *frame, uint8_t *ip_addr, uint16_t port)

{

  uint8_t res=0;

  uint16_t len=0;

  ip_pkt_ptr *ip_pkt = (void*)(frame->data);

  tcp_pkt_ptr *tcp_pkt = (void*)(ip_pkt->data);

  tcpprop.seq_num = tcp_pkt->num_ask;

  tcpprop.ack_num = tcp_pkt->bt_num_seg;

  len = sizeof(tcp_pkt_ptr);

  tcp_header_prepare(tcp_pkt, port, TCP_FIN|TCP_ACK, len);

  len+=sizeof(ip_pkt_ptr);

  ip_header_prepare(ip_pkt, ip_addr, IP_TCP, len);

  //Заполним заголовок Ethernet

  memcpy(frame->addr_dest,frame->addr_src,6);

  eth_send(frame,ETH_IP,len);

  tcpprop.data_stat=DATA_COMPLETED;

  tcp_stat = TCP_DISCONNECTED;

  return res;

}

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

 

Осталось нам теперь только эту функцию в необходимом месте вызвать. А это необходимое место находится в функции tcp_read в самом низу файла

 

else if (tcp_pkt->fl == TCP_ACK)

{

  if (tcpprop.data_stat==DATA_END)

  {

    tcp_send_http_dataend(frame, ip_pkt->ipaddr_src, tcpprop.port_dst);

  }

  HAL_UART_Transmit(&huart1,(uint8_t*)"ACKrn",5,0x1000);

}

 

Соберём код, прошьём контроллер и посмотрим результат нашей нелёгкой работы в браузере

 

image08

 

Также мы видим что в WireShark у нас тоже всё нормально (нажмите на картинку для увеличения изображения)

 

image09_0500

 

Таким образом, мы создали очень простенький веб-сервер, который может отдавать клиенту пока маленькие странички в качестве ответа на HTTP-запрос.

 

 

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

 

Исходный код

 

 

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

Программатор недорогой можно купить здесь ST-Link V2

Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.

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

 

 

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

STM LAN. ENC28J60. TCP WEB Server. Передаём малую страницу

 

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

STM LAN. ENC28J60. TCP WEB Server. Передаём малую страницу

7 комментариев на “STM Урок 86. LAN. ENC28J60. TCP WEB Server. Передаём малую страницу. Часть 2
  1. Андрей:

    Вопрос не в тему и все же. Как зарегистрироваться на форуме что бы что то обсудить. Представленная форма для авторизации не содержит ссылки "зарегистрироваться" только логин и пароль, а как же насчет новых пользователей? Спасибо.

    PS. Ранее занимался видео монтажом и понимаю чего оно стоит в плане времени. Где Вы его находите! Ведь есть еще основная работа… семья….

    Это я к чему — к тому что ГИГАНТСКОЕ  Вам спасибо за то что Вы делаете!

  2. Здравствуйте!

    Я часто учусь на ваших уроках, за что вам огромное спасибо!
    Сейчас дошел до модуля ENC28J60 (еще есть модуль W5500, он тоже на очереди изучения).
    У меня возникла проблема с компиляцией вашего примера для ENC28J60.
    Проблема в том, что у меня был MxCube 4.16.0, а ваш пример для MxCube 4.22.0 у меня не открывался.
    Я обновил MxCube до сейчас последней версии 4.25.0 — он открывается, генерирует проект для Keil 5.15, но при компиляции выдает такие ошибки. Помогите советом в как исправить эти ошибки. Это может быть полезно и другим вашим подписчикам. В любом случае спасибо вам за пример. Я его просмотрел, он достаточно минимальный в отличии от других примеров в интернете.

    Список ошибок при компиляции:

    linking…
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol __asm___18_system_stm32f1xx_c_5d646a67____REV16 multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol __asm___18_system_stm32f1xx_c_5d646a67____REVSH multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol __asm___18_system_stm32f1xx_c_5d646a67____RRX multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol AHBPrescTable multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol APBPrescTable multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol SystemCoreClock multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol SystemCoreClockUpdate multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    ENC28J60_HTTPS\ENC28J60_HTTPS.axf: Error: L6200E: Symbol SystemInit multiply defined (by system_stm32f1xx_1.o and system_stm32f1xx.o).
    Not enough information to list image symbols.
    Not enough information to list the image map.
    Finished: 2 information, 0 warning and 8 error messages.
    «ENC28J60_HTTPS\ENC28J60_HTTPS.axf» — 8 Error(s), 0 Warning(s).
    Target not created.

  3. Решил проблему. Может еще кому пригодится.
    В папке проекта Drivers/CMSIS после обновления MxCube создал два одинаковых файла system_stm32f1xx.c — удаление одного и файлов решило проблему. Проект компилируется успешно.

  4. Ирина:

    Добрый вечер. Такая ситуация. Работаю в условиях отсутствия интернета и когда в строке поисковика задаю адрес модуля (192.168.0.197) в терминале полностью не отражаются данные тсп- пакета, только часть а дальше символы . Хотя размер данных я получаю в терминале такой же, как у вас в уроке. И ещё когда проверяю пришедший пакет, в уроке написано «GET / «, но с таким условием у меня не срабатывает, данные в терминат не получаю. Получаю только при условии «GET» и , как сказала ранее, получаю часть данных. Подскажите, пожалуйста, в чем может быть причина.

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

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

*