STM Урок 91. LAN. W5500. HTTP Server. Часть 4



 

Урок 91

 

Часть 4

 

LAN. W5500. HTTP Server

 

В предыдущей части урока мы исследовали пришедший от клиента запрос HTTP и начали формировать на него ответ.

 

Добавим глобальную переменную для размера окна

 

uint32_t bytesread;

volatile uint16_t tcp_size_wnd = 2048;

 

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

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

Продолжаем функцию ответа на запрос HTTP. Посчитаем количество окон (количество сегментов нам считать не надо, на сегменты микросхема делит буфер сама без нашего участия).

 

  httpsockprop[tcpprop.cur_sock].data_size += sizeof(e404_htm);

}

httpsockprop[tcpprop.cur_sock].cnt_rem_data_part = httpsockprop[tcpprop.cur_sock].data_size / tcp_size_wnd + 1;

httpsockprop[tcpprop.cur_sock].last_data_part_size = httpsockprop[tcpprop.cur_sock].data_size % tcp_size_wnd;

//борьба с неправильным расчётом, когда общий размер делится на минимальный размер окна без остатка

if(httpsockprop[tcpprop.cur_sock].last_data_part_size==0)

{

  httpsockprop[tcpprop.cur_sock].last_data_part_size=tcp_size_wnd;

  httpsockprop[tcpprop.cur_sock].cnt_rem_data_part--;

}

httpsockprop[tcpprop.cur_sock].cnt_data_part = httpsockprop[tcpprop.cur_sock].cnt_rem_data_part;

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

(unsigned long)httpsockprop[tcpprop.cur_sock].data_size, httpsockprop[tcpprop.cur_sock].cnt_rem_data_part, httpsockprop[tcpprop.cur_sock].last_data_part_size);

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

 

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

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

 

image38

 

Всё считается. Отлично! Проверять, я думаю, рассчёт не нужно. Так как я уже много раз это делал, прежде чем дать урок. Если что-то и будет не так, то мы это увидим затем в виде искажённого документа в браузере.

В зависимости от результата установим статус передачи данных

 

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

if (httpsockprop[tcpprop.cur_sock].cnt_rem_data_part==1)

{

  httpsockprop[tcpprop.cur_sock].data_stat = DATA_ONE;

}

else if (httpsockprop[tcpprop.cur_sock].cnt_rem_data_part>1)

{

  httpsockprop[tcpprop.cur_sock].data_stat = DATA_FIRST;

}

 

Здесь всё просто. Если у нас ответ HTTP, включающий документ вместе с заголовком, влезает в одно окно (буфер), значит это один статус. А если нет — то другой.

И теперь на основании данных статусов мы будем вызывать определённые функции. Пока мы только добавим условия

 

  httpsockprop[tcpprop.cur_sock].data_stat = DATA_FIRST;

}

if(httpsockprop[tcpprop.cur_sock].data_stat==DATA_ONE)

{

}

else if(httpsockprop[tcpprop.cur_sock].data_stat==DATA_FIRST)

{

}

 

Добавим выше функцию отправки документа, который умещается в единственный пакет вместе с заголовком

 

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

void tcp_send_http_one(void)

{

  uint16_t i=0;

  uint16_t data_len=0;

  uint16_t header_len=0;

  uint16_t end_point;

  uint8_t num_sect=0;

  uint16_t len_sect;

}

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

 

В файле w5500.c после функции SocketListenWait добавим ещё пару функций

 

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

void SocketClosedWait(uint8_t sock_num)

{

  uint8_t opcode=0;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  while(1)

  {

    if(w5500_readReg(opcode, Sn_SR)==SOCK_CLOSED)

    {

      break;

    }

  }

}

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

void DisconnectSocket(uint8_t sock_num)

{

  uint8_t opcode=0;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  w5500_writeReg(opcode, Sn_CR, 0x08); //DISCON

}

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

 

Первая функция ждёт закрытия сокета, а втораяя — даёт команду на разъединение.

Добавим прототипы на данные функции в заголовочном файле, вернёмся в файл httpd.c и обработаем условие передачи документа вместе с заголовком, состоящего из единственного окна

 

if(httpsockprop[tcpprop.cur_sock].data_stat==DATA_ONE)

{

  tcp_send_http_one();

  DisconnectSocket(tcpprop.cur_sock); //Разъединяемся

  SocketClosedWait(tcpprop.cur_sock);

  OpenSocket(tcpprop.cur_sock,Mode_TCP);

  //Ждём инициализации сокета (статус SOCK_INIT)

  SocketInitWait(tcpprop.cur_sock);

  //Продолжаем слушать сокет

  ListenSocket(tcpprop.cur_sock);

  SocketListenWait(tcpprop.cur_sock);

}

 

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

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

 

 

Также посмотрим результат в анализаторе трафика

 

image39

 

На запрос документа мы просто закрываем соединение корректным образом. Это уже хорошо.

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

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

 

uint16_t len_sect;
if ((httpsockprop[tcpprop.cur_sock].http_doc==EXISTING_HTML)||

  (httpsockprop[tcpprop.cur_sock].http_doc==EXISTING_JPG)||

  (httpsockprop[tcpprop.cur_sock].http_doc==EXISTING_ICO))

{

  switch(httpsockprop[tcpprop.cur_sock].http_doc)

  {

    case EXISTING_HTML:

      header = (void*)http_header;

      break;

    case EXISTING_ICO:

      header = (void*)icon_header;

      break;

    case EXISTING_JPG:

      header = (void*)jpg_header;

      break;

  }

}

else

{

}

 

В файле w5500.c над функцией инициализации добавим ещё две функции

 

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

uint16_t GetWritePointer(uint8_t sock_num)

{

  uint16_t point;

  uint8_t opcode;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  point = (w5500_readReg(opcode,Sn_TX_WR0)<<8|w5500_readReg(opcode,Sn_TX_WR1));

  return point;

}

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

void SetWritePointer(uint8_t sock_num, uint16_t point)

{

  uint8_t opcode;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  w5500_writeReg(opcode, Sn_TX_WR0, point>>8);

  w5500_writeReg(opcode, Sn_TX_WR1, (uint8_t)point);

}

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

 

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

Добавим на данные функции прототипы и продолжим писать функцию tcp_send_http_one в файле httpd.c, продолжая тело нашего условия

 

  break;

}

header_len = strlen(header);

data_len = (uint16_t)MyFile.fsize;

end_point = GetWritePointer(tcpprop.cur_sock);

end_point+=header_len+data_len;

 

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

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

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

 

 

В файле w5500.c добавим ещё один глобальный массив для сектора, который мы потом будем привязыать к определённой структуре

 

char tmpbuf[30];

uint8_t sect[515];

 

Также в этом файле после функции w5500_writeReg добавим ещё две функии

 

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

void w5500_writeBuf(data_sect_ptr *datasect, uint16_t len)

{

  SS_SELECT();

  HAL_SPI_Transmit(&hspi1, (uint8_t*) datasect, len, 0xFFFFFFFF);

  SS_DESELECT();

}

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

void w5500_writeSockBuf(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_TX)<<3)|(RWB_WRITE<<2)|OM_FDM0;

  datasect->addr = be16toword(point);

  w5500_writeBuf(datasect,len+3);//3 служебных байта

}

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

 

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

Причём буфер мы привязывем к структуре datasect, первые 2 байта которой содержат адрес памяти (регистра), причем сначала старший байт, а потом младший, а третий байт — это опкод. Поэтому всё мы это должны подготовить заранее.

Во второй функции мы это и делаем. Байты в адресе меняем местами с помощью макроса be16toword. А сами данные мы в буфере подготавливаем до вызова этой функции, причём для удобства сразу с адреса четвёртого байта.

Создадим на вторую функцию прототип в заголовочном файле, вернёмся в файл httpd.c и подключим наш буфер и там

 

extern char tmpbuf[30];

extern uint8_t sect[515];

 

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

 

end_point+=header_len+data_len;

//Заполним данными буфер для отправки пакета

SetWritePointer(tcpprop.cur_sock, end_point);

end_point = GetWritePointer(tcpprop.cur_sock);

memcpy(sect+3,header,header_len);

w5500_writeSockBuf(tcpprop.cur_sock, end_point, (uint8_t*)sect, header_len);

end_point+=header_len;

 

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

Затем мы посчитаем количество секторов по 512 байт, добавим цикл, в котором прочитаем их с карты SD и запишем в буфер для передачи

 

  end_point+=header_len;

  num_sect = data_len / 512;

  for(i=0;i<=num_sect;i++)

  {

    //не последний сектор

    if(i<(num_sect-1)) len_sect=512;

    else len_sect=data_len;

    result=f_lseek(&MyFile,i*512); //Установим курсор чтения в файле

    sprintf(str1,"f_lseek: %drn",result);

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

    result=f_read(&MyFile,sect+3,len_sect,(UINT *)&bytesread);

    w5500_writeSockBuf(tcpprop.cur_sock, end_point, (uint8_t*)sect, len_sect);

    end_point+=len_sect;

    data_len -= len_sect;

  }

}

else

 

Перейдём в файл w5500.c и добавим там ещё две функции для отправки пакета после функции GetSocketStatus

 

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

void RecvSocket(uint8_t sock_num)

{

  uint8_t opcode;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  w5500_writeReg(opcode, Sn_CR, 0x40); //RECV SOCKET

}

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

void SendSocket(uint8_t sock_num)

{

  uint8_t opcode;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  w5500_writeReg(opcode, Sn_CR, 0x20); //SEND SOCKET

}

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

 

Первая функция подготавливает отправку, передвигая все указатели буферов, а вторая непосредственно отправляет буфер сетевому устройству, с которым создано соединение.

Создадим на эти функции прототипы и вернёмся в нашу недописанную функцию в файл httpd.c.

Пока противную часть нашего условия, в которой мы будем отправлять ответ с ошибкой, мы пропустим и в конце функции отправим наш буфер

 

  else

  {

  }

  //отправим данные

  RecvSocket(tcpprop.cur_sock);

  SendSocket(tcpprop.cur_sock);

  httpsockprop[tcpprop.cur_sock].data_stat = DATA_COMPLETED;

}

 

Соберём код, прошьём контроллер, затем попробуем запросить сначала документ который умещается не только в один буфер (окно), но даже в один сегмент

 

image40

 

Мы видим, что документ нормально передался. Также передалась и иконка, которую я подклчил в документ.

Посмотрим анализатор трафика

 

image41

 

Здесь также всё хорошо.

Теперь запросим такую страницу, которая в одно окно (буфер) влезет, а в сегмент — нет.

Она также передалась, думаю показывать нет смысла. Посмотрим только вывод в WireShark

 

image42

 

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

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

 

data_len -= len_sect;

}

}

else

{

  header_len = strlen(error_header);

  data_len = sizeof(e404_htm);

  end_point = GetWritePointer(tcpprop.cur_sock);

  end_point+=header_len+data_len;

  SetWritePointer(tcpprop.cur_sock, end_point);

  end_point = GetWritePointer(tcpprop.cur_sock);

  //Заполним данными буфер для отправки пакета

  memcpy(sect+3,error_header,header_len);

  w5500_writeSockBuf(tcpprop.cur_sock, end_point, (uint8_t*)sect, header_len);

  end_point+=header_len;

  memcpy(sect+3,e404_htm,data_len);

  w5500_writeSockBuf(tcpprop.cur_sock, end_point, (uint8_t*)sect, data_len);

  end_point+=data_len;

}

//отправим данные

 

Код здесь аналогичен коду истинного тела и объяснять тут, я думаю, нечего.

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

 

image43

 

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

 

image44

 

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

 

 

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

 

 

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

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

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

 

 

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

STM LAN. W5500. HTTP Server

 

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

STM LAN. W5500. HTTP Server

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

    Hello,

    I am sorry but I cant speak russki very well. Can you just please tell me how to write something on the webserver? Just one letter or title will be sufficient. I see you use SD-Card, but I don not have SDIO active, I just want to print letter and values. Can you tell me where to send it (on which place in your code ? )

  2. Сергей:

    Так на каком либо другом кубе, кроме 4.22.1 можно этот проект создать?

    • Немного по-другому пойдёт инициализация FATFS, также немного подправятся некоторые функции. Там структуры изменились. Это было связано с тем, что автор библиотеки FATFS обновил свою библиотеку с определёнными изменениями. Сразу это не расскажешь, нужен будет отдельный урок.

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

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

*