STM Урок 91. LAN. W5500. HTTP Server. Часть 3
Урок 91
Часть 3
LAN. W5500. HTTP Server
В предыдущей части урока мы написали функцию инициализации микросхемы, а также начали писать функцию приёма и обработки сетевых пакетов.
Так как мы сочиняем сервер HTTP, то давайте всю работу с данным протоколом вынесем в отдельный модуль для чего создадим файлы httpd.h и httpd.c следующего содержания
httpd.h
#ifndef HTTPD_H_
#define HTTPD_H_
//--------------------------------------------------
#include "stm32f4xx_hal.h"
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "fatfs.h"
#include "w5500.h"
//--------------------------------------------------
#endif /* HTTPD_H_ */
httpd.c
#include "httpd.h"
//-----------------------------------------------
extern UART_HandleTypeDef huart2;
//-----------------------------------------------
extern char str1[60];
//-----------------------------------------------
Также подключим данную библиотеку в файле w5500.h в самом низу файла
void w5500_packetReceive(void);
//--------------------------------------------------
#include "httpd.h"
//--------------------------------------------------
В файле httpd.h добавим структуру для свойств пакета HTTP, а также макросы для типов документов
#include "w5500.h"
//--------------------------------------------------
typedef struct http_sock_prop {
volatile uint8_t data_stat;//статус передачи данных
volatile uint32_t data_size;//размер данных для передачи
volatile uint16_t last_data_part_size;//размер последней части данных для передачи
volatile uint16_t cnt_data_part;//общее количество частей данных для передачи
volatile uint16_t cnt_rem_data_part;//количество оставшихся частей данных для передачи
volatile uint32_t total_count_bytes;//количество переданных байтов документа
volatile uint8_t http_doc;//вариант документа для передачи
volatile uint8_t prt_tp;//вариант документа для передачи
char fname[20];//имя файла (документа)
} http_sock_prop_ptr;
//--------------------------------------------------
//Варианты документов HTTP
#define EXISTING_HTML 0
#define E404_HTML 1
#define EXISTING_JPG 2
#define EXISTING_ICO 3
//--------------------------------------------------
В файле httpd.c добавим переменную нашей структуры, а также подключим структуру свойств TCP
extern char str1[60];
//-----------------------------------------------
http_sock_prop_ptr httpsockprop[2];
extern tcp_prop_ptr tcpprop;
В файле w5500.c подключим также переменную структуры свойств HTTP
tcp_prop_ptr tcpprop;
extern http_sock_prop_ptr httpsockprop[2];
В файле w5500.h добавим макроподстановки для состояний передачи данных
} tcp_prop_ptr;
//--------------------------------------------------
//Статусы передачи данных
#define DATA_COMPLETED 0 //передача данных закончена
#define DATA_ONE 1 //передаём единственный пакет
#define DATA_FIRST 2 //передаём первый пакет
#define DATA_MIDDLE 3 //передаём средний пакет
#define DATA_LAST 4 //передаём последний пакет
#define DATA_END 5 //закрываем соединение после передачи данных
//--------------------------------------------------
В файле w5500.c в функции приёма пакета в теле нашего условия состояния сокета добавим ещё одно условие состояния передачи данных
if(GetSocketStatus(tcpprop.cur_sock)==SOCK_ESTABLISHED)
{
if(httpsockprop[tcpprop.cur_sock].data_stat == DATA_COMPLETED)
{
}
}
В теле данного условия мы будем принимать запрос HTTP при условии, что данные от нас все переданы. И, так как макрос данного состояния у нас имеет значение 0, то инициализировать состояние при старте программы нам не нужно и мы сразу же войдём в тело данного условия.
Над функцией инициализации добавим новую функцию определения размера принятых данных в TCP-пакете, используя для этого специальный регистр сокета
//-----------------------------------------------
uint16_t GetSizeRX(uint8_t sock_num)
{
uint16_t len;
uint8_t opcode=0;
opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;
len = (w5500_readReg(opcode,Sn_RX_RSR0)<<8|w5500_readReg(opcode,Sn_RX_RSR1));
return len;
}
//-----------------------------------------------
Далее в теле уже нового условия мы узнаем размер принятых данных, отобразим его в терминальной программе, и если он будет нулевой, то есть никаких данных в TCP-пакете нет, то мы выходим из функции
if(httpsockprop[tcpprop.cur_sock].data_stat == DATA_COMPLETED)
{
//Отобразим размер принятых данных
len = GetSizeRX(0);
sprintf(str1,"len_rx_buf:0x%04Xrn",len);
HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);
//Если пришел пустой пакет, то уходим из функции
if(!len) return;
}
Соберём код, прошьём контроллер и попробуем запросить любую страницу. Хотя, конечно, наш сервер пока никак не ответит, но мы хотя бы увидим, что пришел пакет определённой длины

В терминальной программе мы видим бесконечно-поступающие данные с размером длины данных, то есть, пока мы данные не обработаем, мы так и будем попадать в это условие. причём это независимо, закроется ли или нет наше соединение

Мы также видим в WareShark, что запрос от клиента был только один

И прекратить этот «бесконечный цикл» удаётся только перезагрузкой контроллера.
Но хотя бы мы видим, что запрос к нам пришёл.
Также выше функции инициализации напишем функцию, которая будет возвращать адрес данных в приёмном буфере, также используя при этом соответствующий регистр сокета
//-----------------------------------------------
uint16_t GetReadPointer(uint8_t sock_num)
{
uint16_t point;
uint8_t opcode;
opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;
point = (w5500_readReg(opcode,Sn_RX_RD0)<<8|w5500_readReg(opcode,Sn_RX_RD1));
return point;
}
//-----------------------------------------------
Добавим для данной функции прототип. Он нам пригодится в другом модуле.
Давайте также в функции чтения и обработки сетевых пакетов узнаем адрес данных в буфере их приёма в теле того же условия, также отобразив его в терминальной программе
if(!len) return;
//здесь обмениваемся информацией: на запрос документа от клиента отправляем ему запрошенный документ
//указатель на начало чтения приёмного буфера
point = GetReadPointer(tcpprop.cur_sock);
sprintf(str1,"Sn_RX_RD:0x%04Xrn",point);
HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);
Соберём код, прошьём контроллер и ещё раз запросим документ в браузере. А вот результат

Адрес у нас пока нулевой. Ну оно и понятно. Это самый первый запрос и он не обработан. Поэтому адрес меняться пока не будет.
Добавим глобальный массив для временного буфера
extern char str1[60];
char tmpbuf[30];
В файле w5500.h добавим структуру для буфера передачи
} tcp_prop_ptr;
//--------------------------------------------------
typedef struct data_sect {
volatile uint16_t addr;
volatile uint8_t opcode;
uint8_t data[];
} data_sect_ptr;
//--------------------------------------------------
Также добавим макросы для вариантов протоколов TCP. Пока их будет только два — неизвестный и HTTP
#define DATA_END 5 //закрываем соединение после передачи данных
//--------------------------------------------------
//Варианты протоколов TCP
#define PRT_TCP_UNCNOWN 0
#define PRT_TCP_HTTP 1
//--------------------------------------------------
Вернёмся в файл w5500.c и после функции чтения регистра добавим ещё несколько функций чтения
//-----------------------------------------------
void w5500_readBuf(data_sect_ptr *datasect, uint16_t len)
{
SS_SELECT();
HAL_SPI_Transmit(&hspi1, (uint8_t*) datasect, 3, 0xFFFFFFFF);
HAL_SPI_Receive(&hspi1, (uint8_t*) datasect, len, 0xFFFFFFFF);
SS_DESELECT();
}
//-----------------------------------------------
uint8_t w5500_readSockBufByte(uint8_t sock_num, uint16_t point)
{
uint8_t opcode, bt;
opcode = (((sock_num<<2)|BSB_S0_RX)<<3)|OM_FDM1;
bt = w5500_readReg(opcode, point);
return bt;
}
//-----------------------------------------------
void w5500_readSockBuf(uint8_t sock_num, uint16_t point, uint8_t *buf, uint16_t len)
{
data_sect_ptr *datasect = (void*)buf;
datasect->opcode = (((sock_num<<2)|BSB_S0_RX)<<3)|OM_FDM0;
datasect->addr = be16toword(point);
w5500_readBuf(datasect,len);
}
//-----------------------------------------------
Первая из этих функций читает из буфера, используя метод чтения данных переменной длины, Вторая — читает один байт из буфера определённого сокета, а третья — несколько байт из определённого сокета.
На вторую функцию — w5500_readSockBufByte — добавим прототип в заголовочном файле.
Теперь в функции чтения и обработки сетевых пакетов добавим ещё код в наше условие, который будет определять, что это именно запрос HTTP
HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);
w5500_readSockBuf(tcpprop.cur_sock, point, (uint8_t*)tmpbuf, 5);
if (strncmp(tmpbuf,"GET /", 5) == 0)
{
HAL_UART_Transmit(&huart2,(uint8_t*)"HTTPrn",6,0x1000);
httpsockprop[tcpprop.cur_sock].prt_tp = PRT_TCP_HTTP;
}
При совпадении строк мы отобразим аббревиатуру «HTTP» в терминальной программе, а также инициализируем поле структуры определённым протоколом.
В файле httpd.c добавим функцию ответа на запрос HTTP, в котором найдём адрес вхождения символа «/«
extern tcp_prop_ptr tcpprop;
//-----------------------------------------------
void http_request(void)
{
uint16_t point;
uint8_t RXbyte;
uint16_t i=0;
char *ss1;
int ch1='.';
// ищем первый "/" в HTTP заголовке
point = GetReadPointer(tcpprop.cur_sock);
i = 0;
while (RXbyte != (uint8_t)'/')
{
RXbyte = w5500_readSockBufByte(tcpprop.cur_sock,point+i);
i++;
}
}
//-----------------------------------------------
Создадим для данной функции прототип и вызовем её в функции чтения пакета в нашем условии в файле w5500.c
httpsockprop[tcpprop.cur_sock].prt_tp = PRT_TCP_HTTP;
http_request();
Чтобы в дальнейшем нам не запутаться в местах добавления условий, давайте выйдем сразу из тел двух условий и добавим ещё два условия заранее
http_request();
}
}
else if(httpsockprop[tcpprop.cur_sock].data_stat==DATA_MIDDLE)
{
if(httpsockprop[tcpprop.cur_sock].prt_tp == PRT_TCP_HTTP)
{
}
}
else if(httpsockprop[tcpprop.cur_sock].data_stat==DATA_LAST)
{
if(httpsockprop[tcpprop.cur_sock].prt_tp == PRT_TCP_HTTP)
{
}
}
}
}
Данные условия будут срабатывать, если будет установлен один из статусов передачи — передача среднего пакета и или передача последнего пакета данных. Аналогично мы работали с пакетами в уроках по протоколу HTTP с модулем enc28j60.
В файле httpd.c подключим временный буфер
extern char str1[60];
extern char tmpbuf[30];
Продолжим писать функцию ответа на запрос HTTP.
Считаем следующий символ после слеша и, если будет пробел — то это запрос главной страницы. Установим соответствующий тип документа в поле структуры
i++;
}
point+=i;
RXbyte = w5500_readSockBufByte(tcpprop.cur_sock,point);
if(RXbyte==(uint8_t)' ')
{
strcpy(httpsockprop[tcpprop.cur_sock].fname,"index.htm");
httpsockprop[tcpprop.cur_sock].http_doc = EXISTING_HTML;
}
else
{
}
Напишем тело условия, работающее в противном случае
else
{
// ищем следующий пробел (" ") в HTTP заголовке, таким образом считывая имя документа из запроса
i=0;
while (1)
{
tmpbuf[i] = w5500_readSockBufByte(tcpprop.cur_sock, point+i);
if(tmpbuf[i] == (uint8_t)' ') break;
i++;
}
tmpbuf[i] = 0; //закончим строку
strcpy(httpsockprop[tcpprop.cur_sock].fname,tmpbuf);
}
В этом теле мы получим все байты до следующего пробела. Это и будет имя документа.
Продолжаем функцию дальше. Отобразим имя файла, которое запросил клиент, в терминальной программе
strcpy(httpsockprop[tcpprop.cur_sock].fname,tmpbuf);
}
HAL_UART_Transmit(&huart2,(uint8_t*)httpsockprop[tcpprop.cur_sock].fname,strlen(httpsockprop[tcpprop.cur_sock].fname),0x1000);
HAL_UART_Transmit(&huart2,(uint8_t*)"rn",2,0x1000);
Соберём код, прошьём контроллер и попробуем запросить сначала главную страницу, а затем какую-нибудь другую, затем посмотрим результат в терминале

Всё нормально читается.
Теперь в файле main.c добавим некоторые глобальные переменные для работы с файловой системой, а также подключим строковый массив
/* Private variables ---------------------------------------------------------*/
extern char SDPath[4]; /* logical drive path */
FATFS SDFatFs;//указатель на объект
extern char str1[60];
/* USER CODE END PV */
В функции main() также добавим локальную переменную для результата
/* USER CODE BEGIN 1 */
FRESULT result; //результат выполнения
/* USER CODE END 1 */
В этой же функции примонтируем наш носитель на карте SD и отобразим результат в терминальной программе
/* USER CODE BEGIN 2 */
result=f_mount(&SDFatFs,(TCHAR const*)SDPath,0);
sprintf(str1,"f_mount: %drn",result);
HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);
net_ini();
Соберём код, прошьём контроллер и посмотрим результат в терминале
![]()
Всё нормально примонтировалось.
Вернёмся в файл httpd.c и добавим необходимые глобальные переменные для работы с файловой системой
extern tcp_prop_ptr tcpprop;
FIL MyFile;
FRESULT result; //результат выполнения
uint32_t bytesread;
//-----------------------------------------------
Продолжим нашу функцию ответа на запрос HTTP.
Откроем требуемый файл с карты и узнаем его размер. Также покажем это в терминальной программе
HAL_UART_Transmit(&huart2,(uint8_t*)"rn",2,0x1000);
f_close(&MyFile);
result=f_open(&MyFile,httpsockprop[tcpprop.cur_sock].fname,FA_READ); //Попытка открыть файл
sprintf(str1,"f_open: %drn",result);
HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);
sprintf(str1,"f_size: %lurn",MyFile.fsize);
HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);
Прежде чем открыть файл, мы закрываем предыдущий. Можно это делать и в других местах, но у меня лучше всего работает именно так.
Опять соберём проект, прошьём контроллер и попробуем запросить какой-ниубдь существующий документ

Размер определяется нормально.
Я создал несколько страниц html различного размера, чтобы протестировать наш проект с запросом документов, состоящих из разного количества пакетов.
Добавим несколько типов заголовков HTTP для ответа в виде голобальных массивов, а также массив со страницей, передаваемой в случае отстуствия запрашиваемого документа
uint32_t bytesread;
//-----------------------------------------------
const char http_header[] = { "HTTP/1.1 200 OKrnContent-Type: text/htmlrnrn"};
const char jpg_header[] = {"HTTP/1.0 200 OKrnServer: nginxrnContent-Type: image/jpegrnConnection: closernrn"};
const char icon_header[] = { "HTTP/1.1 200 OKrnContent-Type: image/x-iconrnrn"};
const char error_header[] = {"HTTP/1.0 404 File not foundrnServer: nginxrnContent-Type: text/htmlrnConnection: closernrn"};
char *header;
//-----------------------------------------------
const uint8_t e404_htm[] = {
0x3c,0x68,0x74,0x6d,0x6c,0x3e,0x0a,0x20,0x20,0x3c,0x68,0x65,0x61,0x64,0x3e,0x0a,
0x20,0x20,0x20,0x20,0x3c,0x74,0x69,0x74,0x6c,0x65,0x3e,0x34,0x30,0x34,0x20,0x4e,
0x6f,0x74,0x20,0x46,0x6f,0x75,0x6e,0x64,0x3c,0x2f,0x74,0x69,0x74,0x6c,0x65,0x3e,
0x0a,0x20,0x20,0x3c,0x2f,0x68,0x65,0x61,0x64,0x3e,0x0a,0x3c,0x62,0x6f,0x64,0x79,
0x3e,0x0a,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,0x34,0x30,0x34,0x20,0x45,0x72,0x72,0x6f,0x72,0x20,0x46,0x69,0x6c,0x65,
0x20,0x4e,0x6f,0x74,0x20,0x46,0x6f,0x75,0x6e,0x64,0x3c,0x2f,0x68,0x31,0x3e,0x0a,
0x3c,0x68,0x32,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,
0x20,0x54,0x68,0x65,0x20,0x70,0x61,0x67,0x65,0x20,0x79,0x6f,0x75,0x20,0x61,0x72,
0x65,0x20,0x6c,0x6f,0x6f,0x6b,0x69,0x6e,0x67,0x20,0x66,0x6f,0x72,0x20,0x6d,0x69,
0x67,0x68,0x74,0x20,0x68,0x61,0x76,0x65,0x20,0x62,0x65,0x65,0x6e,0x20,0x72,0x65,
0x6d,0x6f,0x76,0x65,0x64,0x2c,0x20,0x3c,0x62,0x72,0x20,0x2f,0x3e,0x68,0x61,0x64,
0x20,0x69,0x74,0x73,0x20,0x6e,0x61,0x6d,0x65,0x20,0x63,0x68,0x61,0x6e,0x67,0x65,
0x64,0x2c,0x20,0x6f,0x72,0x20,0x69,0x73,0x20,0x74,0x65,0x6d,0x70,0x6f,0x72,0x61,
0x72,0x69,0x6c,0x79,0x20,0x75,0x6e,0x61,0x76,0x61,0x69,0x6c,0x61,0x62,0x6c,0x65,
0x2e,0x3c,0x2f,0x68,0x32,0x3e,0x0a,0x3c,0x2f,0x62,0x6f,0x64,0x79,0x3e,0x3c,0x2f,
0x68,0x74,0x6d,0x6c,0x3e};
//-----------------------------------------------
Продолжаем писать нашу функцию ответа на запрос HTTP. Если документ открылся, то мы изучим его расширение и выставим необходимый статус, а если нет, то установим статус ошибки существования документа на сервере. Также подсчитаем заранее длину ответа HTTP для того, чтобы определить потом, во сколько пакетов и окон он уместится
HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);
if (result==FR_OK)
{
//изучим расширение файла
ss1 = strchr(httpsockprop[tcpprop.cur_sock].fname,ch1);
ss1++;
if (strncmp(ss1,"jpg", 3) == 0)
{
httpsockprop[tcpprop.cur_sock].http_doc = EXISTING_JPG;
//сначала включаем в размер размер заголовка
httpsockprop[tcpprop.cur_sock].data_size = strlen(jpg_header);
}
if (strncmp(ss1,"ico", 3) == 0)
{
httpsockprop[tcpprop.cur_sock].http_doc = EXISTING_ICO;
//сначала включаем в размер размер заголовка
httpsockprop[tcpprop.cur_sock].data_size = strlen(icon_header);
}
else
{
httpsockprop[tcpprop.cur_sock].http_doc = EXISTING_HTML;
//сначала включаем в размер размер заголовка
httpsockprop[tcpprop.cur_sock].data_size = strlen(http_header);
}
//затем размер самого документа
httpsockprop[tcpprop.cur_sock].data_size += MyFile.fsize;
}
else
{
httpsockprop[tcpprop.cur_sock].http_doc = E404_HTML;
//сначала включаем в размер размер заголовка
httpsockprop[tcpprop.cur_sock].data_size = strlen(error_header);
//затем размер самого документа
httpsockprop[tcpprop.cur_sock].data_size += sizeof(e404_htm);
}
В следующей части урока мы продолжим функцию ответа на запрос HTTP, а также напишем функцию передачи клиенту документа, умещающегося вместе с заголовком в одно окно.
Отладочную плату можно приобрести здесь Nucleo STM32F401RE
Ethernet LAN Сетевой Модуль можно купить здесь W5500 Ethernet LAN
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)



Здравствуйте.
Откуда поле "MyFile.fsize"?
Тип: FIL <C:\…\STM32Cube_FW_F4_V1.17.0\Middlewares\Third_Party\FatFs\src\ff.h>
typedef struct {
_FDID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */
BYTE flag; /* File status flags */
BYTE err; /* Abort flag (error code) */
FSIZE_t fptr; /* File read/write pointer (Zeroed on file open) */
DWORD clust; /* Current cluster of fpter (invalid when fptr is 0) */
DWORD sect; /* Sector number appearing in buf[] (0:invalid) */
#if !_FS_READONLY
DWORD dir_sect; /* Sector number containing the directory entry */
BYTE* dir_ptr; /* Pointer to the directory entry in the win[] */
#endif
#if _USE_FASTSEEK
DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */
#endif
#if !_FS_TINY
BYTE buf[_MAX_SS]; /* File private data read/write window */
#endif
} FIL;
А вот и ответ нашелся.
В новой версии библиотеки ввели #define f_size(fp) ((fp)->obj.objsize)
Спасибо автору за очень полезную работу.
Всегда пожалуйста!
У Вас заработал проект на версии Cube MX 4.23?
Оттуда, что Вы, видимо обновили версию Cube MX, чего не в коем случае делать на нужно, не сохранив перед этим дистрибутив предыдущей версии.
В новой версии 4.23 был добавлен новый релиз библиотеки FATFS, который кардинально отличается от предыдущего, и, по всей видимости он ещё не адаптирован нормально к библиотеке HAL. Размер можно померить и по-другому, но если Вы уберёте вообще эти строки с размерами, то Вы увидите, что файлы у Вас вообще не открываются. Это происходит потому, что в новой библиотеке FATFS сначала происходит определение статуса диска, а только потом его инициализации, а как известно именно в инициализации диска происходит инициализация самой шины. То есть шина у нас неинициализирована, а мы уже туда какие-то команды подаём. Можете проверить логическим анализатором, что у Вас вообще нет никакого обмена по шине SDIO. Так что откатывайтесь на 4.22.1 и работайте с этим уроком. Так как там прикручена такая библиотека FATFS, где у структуры FIL есть поле с размером.
Проект с fatfs через sdio под версией куба 4.23 у меня ни в какую не запускается. А откуда взять 4.22 не знаю.
4.22 могу выслать, если ещё не поздно.
дошел до открытия страницы в браузере и посылки запроса через адресную строку.
1. Комп посылает данные на порт 443. почему так?
2. Не понял в какой момент МК попадает ф функцию w5500_packetReceive? Вроде все по шагам, согласно тексту делал. Добывил вызов этой функции в главном теле main.c, что-то заработало, порт поменял в программе на тот по которому стучит комп — 443.
3. стучусь по 192.168.0.2/1 в wireshark вижу отправляет len=1, but in terminal I see.. а в терминале вижу 0x0205 ааа??!!!, пойду возьму бубен…
sprintf(str1,»Sn_RX_RD:0x%04Xrn»,point); не надо ли тут слеши? /r/n
и в терминал выводит len_rx_buf:0x 0205 только один раз и все, даже если еще раз в браузере зайти. повторно отправка идет вижу в Wireshare, длину не выводит в терминал