STM Урок 89. LAN. ENC28J60. TCP WEB Server. Подключаем карту SD

 

 

 

 

Урок 89

 

LAN. ENC28J60. TCP WEB Server. Подключаем карту SD

 

 

Продолжаем подключать модуль LAN ENC28J60 к микроконтроллеру STM32F103, расположенному на одноимённой недорогой отладочной плате. Мы смогли ответить клиенту на запрос веб-страницы, но только передавать мы большие документы и изображения все равно не сможем ввиду того, что подошли мы к такому моменту, что на это у нас никакой памяти уже не хватит, ни оперативной, ни энергонезависимой. На помощь нам придёт старая добрая карта SD, которую мы подключим по интерфейсу SPI. Подключать карту по интерфейсу SPI с использованием библиотеки FATFS мы научились на предыдущем занятии, поэтому сегодняшняя задача — соединить наш проект последнего урока по LAN с проектом прошлого занятия.

Схема наша остаётся прежняя. Подключим обратно модуль LAN к контроллеру, так как когда мы занимались с картой SD, мы его отключали, а карту SD оставим там, где она и была. Теперь наша схема получится вот такого вида (нажмите на картинку для увеличения изображения)

 

image00_0500

 

Теперь проект. Проект сделан из проекта урока 87 с именем ENC28J60_HTTPS_LARGE и назван был ENC28J60_HTTPS_SD. Только единственное отличие — положил я его во вложенную папку WB, так как проект у нас разрабатываться будет уже в среде System Workbench для STM32, так как после подключения библиотеки FATFS и написания нескольких строк коду проект превысил заветные 32 килобайта кода, чего бесплатная лицензия Keil делать категорически запрещает. И, как вы знаете, права мы ничьи не нарушаем, поэтому прибегнуть пришлось к бесплатной среде разработки. Я пробовал CooCox, но это отдельная история, и я забросил данное занятие. Да и плюс ко всему, мне всё же удалось победить недостаточный размер кучи в System Workbench и я наконец-то возобновил работу внешнего сборщика. Если кому интересно, как я это сделал, пишите в комментариях. Вообще, выбор компилятора — это дело ваше, так что можете пользоваться любым. Разница между ними по настройке и по работе не столь велика. Открываем проект в Cube MX, включаем SPI2 и FATFS

 

index07   index19

 

Также включим на выход ножку PA3, так как это у нас Chip Select для карты SD

 

index12

 

Переходим на вкладку с настройками интерфейсов (Configuration) и поначалу настроим SPI (делитель можно включить теперь 2 и скорость у нас возрастёт до 10 мбпс, так как карта на такой скорости работает отлично и нам не нужно теперь ничего анализировать логическим анализатором), А такие настройки у шины SPI Cube устанавливает автоматически, поэтому ничего не трогаем и уходим из данного диалога

 

Image01

 

Настроим также FATFS, добавив поддержку длинных имён, вдруг пригодится, а также увеличив максимальный размер сектора

 

Image02

 

Также не забываем включить среднюю скорость для ножки PA3 в GPIO

 

Image03

 

Зайдём в настройки проекта и изменим там среду разработки

 

Image04

 

Сохраним настройки, сгенерируем проект, перейдём в System Workbench, подключим наш свежесгенерированный проект и в свойствах убедимся, что у нас нет готоых настроек дебагеров, а если есть, то удалим, как всегда. Сброщик на внутренний мы теперь не меняем. Открываем main.c и собираем наш проект. Если всё нормально собралось, то можно попробовать его запустить и проверить что у нас всё по прежнему работает, например, пропинговав наш модуль.

В папки Inc и Src проекта соответственно скопируем файлы нашей библиотеки для SD-карты, которые мы создали на прошлом занятии из проекта SD_FATFS sd.c и sd.h. Обновим дерево проектов в среде разработки (Refresh). Перейдём в файл sd.c и глобальной строковой переменной добавим extern и уберём ее инициализацию

 

extern char str1[60]={0};

 

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

 

uint8_t SD_SPIx_WriteRead(uint8_t Byte)

{

 

void SD_SPI_SendByte(uint8_t bt)

{

 

uint8_t SD_SPI_ReceiveByte(void)

{

 

Соответственно, нам также необходимо будет такой же префикс добавить во всех местах вызова данных функций ниже.

Также удалим функцию SD_PowerOn и её прототип, так как нам не нужно будет ждать в инициализации карты 20 милисекунд, они и так у нас пройдут с лихвой, так как сначала будет инициализация модуля LAN, потом когда-то мы получим запрос документа от клиента, и вот тогда только будет происходить первичная инициализация карты.

Сответственно, тогда удалим и глобальную переменную для таймера

 

extern volatile uint16_t Timer1;

 

Теперь перейдём в файл user_diskio.c, удалим там полностью весь код и вставим его из одноименного файла проекта SD_FATFS. В теле функции USER_initialize удалим вызов функции SD_PowerOn.

 

SD_PowerOn();

if(sd_ini()==0) {Stat &= ~STA_NOINIT;} //сбросим статус STA_NOINIT

 

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

Теперь переходим в файл tcp.c и начнём работать над тем, как нам теперь запрошенные документы вызывать из файловой системы SD-карты.

В функции tcp_read закомментируем вывод информации в USART, чтобы не тормозило. Если что-то не пойдёт, то мы всегда можем это раскомментировать

 

/*

sprintf(str1,"%d.%d.%d.%d-%d.%d.%d.%d %d tcp\r\n",

ip_pkt->ipaddr_src[0],ip_pkt->ipaddr_src[1],ip_pkt->ipaddr_src[2],ip_pkt->ipaddr_src[3],

ip_pkt->ipaddr_dst[0],ip_pkt->ipaddr_dst[1],ip_pkt->ipaddr_dst[2],ip_pkt->ipaddr_dst[3], len_data);

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

*/

 

//HAL_UART_Transmit(&huart1,(uint8_t*)"ACK\r\n",5,0x1000);

 

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

 

volatile uint16_t tcp_mss = 458;

volatile uint16_t tcp_size_wnd = 8192;

 

В функции tcp_header_prepare изменим строку

 

tcp_pkt->size_wnd = be16toword(tcp_size_wnd);

 

И теперь, чтобы нам при приближении количества переданных данных к размеру окна передавать в пакете с данными флаг PCH, нам потребуется переменная в структуре tcp_prop. Для этого перейдём в файл tcp.h и добавим её

 

volatile uint16_t cnt_rem_data_part;//количество оставшихся частей данных для передачи

volatile uint16_t cnt_size_wnd;//количество переданных байтов окна

 

Также в эту же структуру, раз уж мы собираемся брать данные для передачи из файла, добавим массив для хранения имени файла

 

  char fname[20];//имя файла (документа)

} tcp_prop_ptr;

 

Вернёмся в файл tcp.c и проинициализируем поле с количеством переданных байтов окна в функции tcp_read в запросе документа клиентом

 

if (strncmp((char*)tcp_pkt->data,"GET /", 5) == 0)

{

  //инициализируем количество переданных байтов окна

  tcpprop.cnt_size_wnd = 0;

 

Теперь нам нужно найти место, где мы можем превысить размер окна и передать там соответствующий флаг.

Функция передачи страницы размером в один пакет нас вообще не интересует, там мы точно ничего не превысим.

Зайдём в функцию передачи первого пакета многопакетной страницы и нарастим там наш счётчик на размер сегмента

 

tcpprop.cnt_rem_data_part--;

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

tcpprop.cnt_size_wnd += tcp_mss;

 

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

 

len=len_tcp + tcp_mss;

//Узнаем, не подолши ли мы к предельному размеру окна

if ((tcp_size_wnd - tcpprop.cnt_size_wnd) > tcp_mss)

{

  tcp_header_prepare(tcp_pkt, port, TCP_ACK, len_tcp, len);

}

else

{

  tcp_header_prepare(tcp_pkt, port, TCP_PSH|TCP_ACK, len_tcp, len);

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

  tcpprop.cnt_size_wnd = 0;

}

len+=sizeof(ip_pkt_ptr);

 

Продвинемся по функции немного ниже и также нарастим счётчик

 

tcpprop.cnt_rem_data_part--;

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

tcpprop.cnt_size_wnd += tcp_mss;

 

Перейдём в заголовочный файл tcp.h и несколько изменим макросы вариантов документов, так как теперь мы будем передавать не только главную страницу и страницу ошибки, а все файлы документов, которые запросит у нас клиент, если они будут присутствовать на нашей SD-карте

 

//Варианты документов HTTP

#define EXISTING_HTML 0

#define E404_HTML 1

#define EXISTING_JPG 2

 

Теперь начнём работать непосредственно с SD-картой.

Вернёмся в файл tcp.c и в функции tcp_read добавим ещё три локальные переменные

 

uint16_t i=0;

char *ss1;

int ch1=' ';

int ch2='.';

 

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

 

if((char)tcp_pkt->data[5]==' ')

{

  strcpy(tcpprop.fname,"index.htm");

  tcpprop.http_doc = EXISTING_HTML;

}

else

{

  //скопируем 20 байтов из запроса после символа '/' в поле fname

  memcpy((void*)tcpprop.fname,(void*)(tcp_pkt->data+5),20);

  //найдём пробел и заменим его нулём

  ss1 = strchr(tcpprop.fname,ch1);

  ss1[0] = 0;

}

 

Дальше во всех местах файла, где встречался макрос INDEX_HTML, заменим его на новый макрос EXISTING_HTML, чтобы при сборке кода не было ошибок.

В файле tcp.h подключим библиотеку FATFS

 

#include "net.h"

#include "fatfs.h"

 

Вернёмся в файл tcp.c и добавим несколько глобальных переменные для работы с файловой системой

 

extern uint8_t ipaddr[4];

FATFS SDFatFs;//указатель на объект

extern char USER_Path[4]; /* logical drive path */

FIL MyFile;

FRESULT result; //результат выполнения

uint32_t bytesread;

 

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

 

const char http_header[] = {"HTTP/1.0 200 OK\r\nServer: nginx\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"};

const char jpg_header[] = {"HTTP/1.0 200 OK\r\nServer: nginx\r\nContent-Type: image/jpeg\r\nConnection: close\r\n\r\n"};

const char error_header[] = {"HTTP/1.0 404 File not found\r\nServer: nginx\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"};

 

Затем продолжим писать код функции tcp_read.

 

 

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

 

  ss1[0] = 0;

}

HAL_UART_Transmit(&huart1,(uint8_t*)tcpprop.fname,strlen(tcpprop.fname),0x1000);

HAL_UART_Transmit(&huart1,(uint8_t*)"\r\n",2,0x1000);

result=f_mount(&SDFatFs,(TCHAR const*)USER_Path,0);

sprintf(str1,"f_mount: %d\r\n",result);

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

result=f_open(&MyFile,tcpprop.fname,FA_READ); //Попытка открыть файл

sprintf(str1,"f_open: %d\r\n",result);

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

sprintf(str1,"f_size: %lu\r\n",MyFile.fsize);

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

tcpprop.cnt_rem_data_part = tcpprop.data_size / tcp_mss + 1;

 

И после добавления данного кода почему-то при сборке я получил ошибку об отстуствии кода реализаций функций, связанных с длинными именами

 

C:\ISH\ARM\MYLESSON\CUBE\F103\WB\ENC28J60_HTTPS_SD\Debug/../Middlewares/Third_Party/FatFs/src/ff.c:1943: undefined reference to ff_convert'

C:\ISH\ARM\MYLESSON\CUBE\F103\WB\ENC28J60_HTTPS_SD\Debug/../Middlewares/Third_Party/FatFs/src/ff.c:1997: undefined reference to ff_convert'

Middlewares/Third_Party/FatFs/src/ff.o: In function cmp_lfn':

C:\ISH\ARM\MYLESSON\CUBE\F103\WB\ENC28J60_HTTPS_SD\Debug/../Middlewares/Third_Party/FatFs/src/ff.c:1361: undefined reference to ff_wtoupper'

C:\ISH\ARM\MYLESSON\CUBE\F103\WB\ENC28J60_HTTPS_SD\Debug/../Middlewares/Third_Party/FatFs/src/ff.c:1362: undefined reference to `ff_wtoupper'

 

Мы сталкивались в прошлом занятии с такой ошибкой и присоединяли файл ccsbcs.c, который у нас генерировался, но не был присоединён. В этот раз у меня почему-то данный файл даже не сгенерировался, поэтому возьмём его из предыдущего проекта и положим в папку с "Src", затем сделаем refresh в проекте, откроем этот файл и уберём там точки из строки с подключением заголовочного файла

 

#include "ff.h"

 

После этого обновим дерево проекта и проект нормально соберётся.

То есть только сейчас мы начали пользоваться библиотекой FATFS, о чём свидетелсьвует резко возросший размер прошивки

 

Image05

 

Вот поэтому мы сейчас и работаем со средой программирования System WorkBench.

Затем случае положительного результата открытия файла (если файл существует на карте SD мы сначала изучим его расширение и уже основываясь на этом будем использовать соответствующий заголовок для пакета HTTP, а в противном случае мы передадим документ с ошибкой

 

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

if (result==FR_OK)

{

  //изучим расширение файла

  ss1 = strchr(tcpprop.fname,ch2);

  ss1++;

  if (strncmp(ss1,"jpg", 3) == 0)

  {

    tcpprop.http_doc = EXISTING_JPG;

    //сначала включаем в размер размер заголовка

    tcpprop.data_size = strlen(jpg_header);

  }

  else

  {

    tcpprop.http_doc = EXISTING_HTML;

    //сначала включаем в размер размер заголовка

    tcpprop.data_size = strlen(http_header);

  }

  //затем размер самого документа

  tcpprop.data_size += MyFile.fsize;

}

else

{

  tcpprop.http_doc = E404_HTML;

  //сначала включаем в размер размер заголовка

  tcpprop.data_size = strlen(error_header);

  //затем размер самого документа

  tcpprop.data_size += sizeof(e404_htm);

}

tcpprop.cnt_rem_data_part = tcpprop.data_size / tcp_mss + 1;

 

С данной функцие мы закончили, теперь переходим к функциям передачи. начнём с функции tcp_send_http_one.

В данной функции заменим код в том месте где мы наполняем поле структуры, предназначенное для данных

 

//Отправляем страницу

if ((tcpprop.http_doc==EXISTING_HTML)||(tcpprop.http_doc==EXISTING_JPG))

{

  result=f_mount(&SDFatFs,(TCHAR const*)USER_Path,0);

  sprintf(str1,"f_mount: %d\r\n",result);

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

  result=f_open(&MyFile,tcpprop.fname,FA_READ); //Попытка открыть файл

  sprintf(str1,"f_open: %d\r\n",result);

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

  sprintf(str1,"f_size: %lu\r\n",MyFile.fsize);

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

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

  sprintf(str1,"f_lseek: %d\r\n",result);

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

  if (tcpprop.http_doc==EXISTING_HTML)

  {

    strcpy((char*)tcp_pkt->data,http_header);

    result=f_read(&MyFile,(void*)(tcp_pkt->data+strlen(http_header)),(uint16_t)MyFile.fsize,(UINT *)&bytesread);

  }

  else

  {

    strcpy((char*)tcp_pkt->data,jpg_header);

    result=f_read(&MyFile,(void*)(tcp_pkt->data+strlen(jpg_header)),(uint16_t)MyFile.fsize,(UINT *)&bytesread);

  }

}

else

 

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

Аналогичные действия проделаем в следующей функции — tcp_send_http_first

 

//Отправляем первую часть страницы

if ((tcpprop.http_doc==EXISTING_HTML)||(tcpprop.http_doc==EXISTING_JPG))

{

  strcpy((char*)tcp_pkt->data,http_header);

  result=f_mount(&SDFatFs,(TCHAR const*)USER_Path,0);

  result=f_open(&MyFile,tcpprop.fname,FA_READ); //Попытка открыть файл

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

  if (tcpprop.http_doc==EXISTING_HTML)

  {

    strcpy((char*)tcp_pkt->data,http_header);

    result=f_read(&MyFile,(void*)(tcp_pkt->data+strlen(http_header)),tcp_mss-strlen(http_header),(UINT *)&bytesread);

  }

  else

  {

    strcpy((char*)tcp_pkt->data,jpg_header);

    result=f_read(&MyFile,(void*)(tcp_pkt->data+strlen(jpg_header)),tcp_mss-strlen(jpg_header),(UINT *)&bytesread);

  }

}

else

 

Следующая функция — tcp_send_http_middle

 

if ((tcpprop.http_doc==EXISTING_HTML)||(tcpprop.http_doc==EXISTING_JPG))

{

  if (tcpprop.http_doc==EXISTING_HTML)

  {

    result=f_lseek(&MyFile,((uint32_t)tcp_mss*(tcpprop.cnt_data_part-tcpprop.cnt_rem_data_part))-strlen(http_header)); //Установим курсор чтения в файле

  }

  else

  {

    result=f_lseek(&MyFile,((uint32_t)tcp_mss*(tcpprop.cnt_data_part-tcpprop.cnt_rem_data_part))-strlen(jpg_header)); //Установим курсор чтения в файле

  }

  result=f_read(&MyFile,(void*)tcp_pkt->data,tcp_mss,(UINT *)&bytesread);

}

else

 

Здесь практически то же самое, только указатель мы уже устанавливаем не на 0, а в соответствующее место в файле, а также мы перед этим не монтируем файловую систему и не открываем файл, так как это у нас уже сделано.

Ну и последняя функция — tcp_send_http_last

 

if ((tcpprop.http_doc==EXISTING_HTML)||(tcpprop.http_doc==EXISTING_JPG))

{

  if (tcpprop.http_doc==EXISTING_HTML)

  {

    result=f_lseek(&MyFile,(tcp_mss*(tcpprop.cnt_data_part-1))-strlen(http_header)); //Установим курсор чтения в файле

  }

  else

  {

    result=f_lseek(&MyFile,(tcp_mss*(tcpprop.cnt_data_part-1))-strlen(jpg_header)); //Установим курсор чтения в файле

  }

  sprintf(str1,"f_lseek: %d\r\n",result);

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

  result=f_read(&MyFile,(void*)tcp_pkt->data,tcpprop.last_data_part_size,(UINT *)&bytesread);

}

else

 

Массив с главной страницей теперь можно удалить

 

const uint8_t index_htm[] = {0x3c,

 

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

Запросим главную страницу (нажмите на картинку для увеличения изображения)

 

Image06_0500

 

Запросим другой файл, например index1.htm

 

Image07

 

Загрузим в папку IMG на карту SD несколько файлов в формате jpeg

 

Image08

 

Также откроем файл index.htm и добавим туда картинку из файла куда-нибудь в текст страницы

 

</ul></p>
<p>
  <img src="/IMG/img02.jpg" />
</p>

<p>Memories</p>

 

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

 

Image09_0500

 

Можно вставить картинку и поменьше и побольше но только одну, иначе браузер пытается открыть несколько соединений, а у нас такой поддержки нет. Я пытался добавить виртуальные сокеты, но видимо потому, что свободное количество памяти у нас уже стремится к нулю, номер сокета, а также некоторые переменные, хотя и с аттрибутом volatile начинают без моего ведома менять значение, поэтому я забросил это занятие. Здесь нужен уже контроллер помощнее. Но и так, я считаю неплохо. если же мы хотим всё равно добавить несколько рисунков, правда небольших, обойти этот нюанс можно с помощью внедрения данных картинок прямов код html, то есть в саму страницу. Для этого существует несколько онлайн-сервисов, с помощью которых можно сделать эти преобразования. Код выглядит приблизительно так

 

<img width="102" height="58" title="" alt="" src="data:image/jpeg;base64,/9j/4RSXRXhpZgAASUkqA... и т.д.

 

Таким образом я добился вывода нескольких изображений на странице

 

Image10

 

Таким образом, с помощью сегодняшнего урока мы смогли улучшить наш HTTP-сервер, расширив его память для хранения файлов с помощью подключения карты SD и файловой системы. Хотя конечно скорость оставляет желать лучшего, так как карта у нас подключена по интерфейсу SPI, что не позволяет общаться с ней со скоростью превышающей 1 мегабит в секунду, тем не менее мы ещё раз проработали протокол HTTP, научившись также учитывать и размер окна, а также передавать другие документы кроме главной страницы, причём и других форматов.

Спасибо за внимание!

 

Предыдущий урок Программирование МК STM32 Следующий урок

 

Исходный код

 

 

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

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

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

 

 

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

 

STM LAN. ENC28J60. TCP WEB Server. Подключаем карту SD

 

2 комментария на “STM Урок 89. LAN. ENC28J60. TCP WEB Server. Подключаем карту SD
  1. SmNikolay:

    Добрый день. Я начинающий в программировании 🙂 В последних версиях куба в структуре FIl Fatfs отсутствует fsize. Как с этим бороться? Собираю по вашему уроку на контроллере f407 с SDIO SD.

  2. SmNikolay:

    Еще раз здравствуйте. Проблему с Fatfs решил. Осталась 1 проблема. Ели функция сети вызывается в прерывании то зависает на открытии файла с SD. Если в основном цикле то все прекрасно работает. Не подскажите в какую сторону копать?

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

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

*