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

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

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

 

 

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

 

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

6 комментариев на “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 — удаление одного и файлов решило проблему. Проект компилируется успешно.

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

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

*