Урок 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, также мы должны увидеть весь текст запроса в терминальной программе благодаря коду, написанному выше нашего условия.
Мы видим в терминальной программе следующую информацию
Теперь в файле 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. Поэтому попробуем ещё раз запросить в браузере нашу страницу (нажмите на картинку для увеличения изображения)
Мы видим, что наше подтверждение и наш ответ 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);
}
Соберём код, прошьём контроллер и посмотрим результат нашей нелёгкой работы в браузере
Также мы видим что в WireShark у нас тоже всё нормально (нажмите на картинку для увеличения изображения)
Таким образом, мы создали очень простенький веб-сервер, который может отдавать клиенту пока маленькие странички в качестве ответа на HTTP-запрос.
Предыдущая часть Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Вопрос не в тему и все же. Как зарегистрироваться на форуме что бы что то обсудить. Представленная форма для авторизации не содержит ссылки "зарегистрироваться" только логин и пароль, а как же насчет новых пользователей? Спасибо.
PS. Ранее занимался видео монтажом и понимаю чего оно стоит в плане времени. Где Вы его находите! Ведь есть еще основная работа… семья….
Это я к чему — к тому что ГИГАНТСКОЕ Вам спасибо за то что Вы делаете!
Настроил, форма другая, авторизацию включил.
Письмо с паролем может прийти в спам
Здравствуйте!
Я часто учусь на ваших уроках, за что вам огромное спасибо!
Сейчас дошел до модуля 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.
Здравствуйте!
У Вас всё написано в ошибках. Двойное объявление функций.
Указано в каких именно модулях.
Ищем и исправляем.
Решил проблему. Может еще кому пригодится.
В папке проекта Drivers/CMSIS после обновления MxCube создал два одинаковых файла system_stm32f1xx.c — удаление одного и файлов решило проблему. Проект компилируется успешно.
Да, бывает такое.
Добрый вечер. Такая ситуация. Работаю в условиях отсутствия интернета и когда в строке поисковика задаю адрес модуля (192.168.0.197) в терминале полностью не отражаются данные тсп- пакета, только часть а дальше символы . Хотя размер данных я получаю в терминале такой же, как у вас в уроке. И ещё когда проверяю пришедший пакет, в уроке написано «GET / «, но с таким условием у меня не срабатывает, данные в терминат не получаю. Получаю только при условии «GET» и , как сказала ранее, получаю часть данных. Подскажите, пожалуйста, в чем может быть причина.