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;

}

 

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

 

image30

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

 

image31

 

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

 

image32

 

И прекратить этот "бесконечный цикл" удаётся только перезагрузкой контроллера.

Но хотя бы мы видим, что запрос к нам пришёл.

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

 

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

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);

 

Соберём код, прошьём контроллер и ещё раз запросим документ в браузере. А вот результат

 

image33

 

Адрес у нас пока нулевой. Ну оно и понятно. Это самый первый запрос и он не обработан. Поэтому адрес меняться пока не будет.

Добавим глобавльный массив для временного буфера

 

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);

 

Соберём код, прошьём контроллер и попробуем запросить сначала главную страницу, а затем какую-нибудь другую, затем посмотрим результат в терминале

 

image34  image35

 

Всё нормально читается.

Теперь в файле 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();

 

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

 

image36

 

Всё нормально примонтировалось.

Вернёмся в файл 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);

 

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

Опять соберём проект, прошьём контроллер и попробуем запросить какой-ниубдь существующий документ

 

image37

 

Размер определяется нормально.

Я создал несколько страниц 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, а также напишем функцию передачи клиенту документа, умещающегося вместе с заголовком в одно окно.

 

 

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

 

 

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

и здесь Nucleo STM32F401RE

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

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

 

 

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

 

STM LAN. W5500. HTTP Server

6 комментариев на “STM Урок 91. LAN. W5500. HTTP Server. Часть 3
  1. userius:

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

    Откуда поле "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;

    • userius:

      А вот и ответ нашелся.

      В новой версии библиотеки ввели #define f_size(fp) ((fp)->obj.objsize)

      Спасибо автору за очень полезную работу.

    • Оттуда, что Вы, видимо обновили версию Cube MX, чего не в коем случае делать на нужно, не сохранив перед этим дистрибутив предыдущей версии.

      В новой версии 4.23 был добавлен новый релиз библиотеки FATFS, который кардинально отличается от предыдущего, и, по всей видимости он ещё не адаптирован нормально к библиотеке HAL. Размер можно померить и по-другому, но если Вы уберёте вообще эти строки с размерами, то Вы увидите, что файлы у Вас вообще не открываются. Это происходит потому, что в новой библиотеке FATFS сначала происходит определение статуса диска, а только потом его инициализации, а как известно именно в инициализации диска происходит инициализация самой шины. То есть шина у нас неинициализирована, а мы уже туда какие-то команды подаём. Можете проверить логическим анализатором, что у Вас вообще нет никакого обмена по шине SDIO. Так что откатывайтесь на 4.22.1 и работайте с этим уроком. Так как там прикручена такая библиотека FATFS, где у структуры FIL есть поле с размером.

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

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

*