Урок 89
LAN. ENC28J60. TCP WEB Server. Подключаем карту SD
Продолжаем подключать модуль LAN ENC28J60 к микроконтроллеру STM32F103, расположенному на одноимённой недорогой отладочной плате. Мы смогли ответить клиенту на запрос веб-страницы, но только передавать мы большие документы и изображения все равно не сможем ввиду того, что подошли мы к такому моменту, что на это у нас никакой памяти уже не хватит, ни оперативной, ни энергонезависимой. На помощь нам придёт старая добрая карта SD, которую мы подключим по интерфейсу SPI. Подключать карту по интерфейсу SPI с использованием библиотеки FATFS мы научились на предыдущем занятии, поэтому сегодняшняя задача — соединить наш проект последнего урока по LAN с проектом прошлого занятия.
Схема наша остаётся прежняя. Подключим обратно модуль LAN к контроллеру, так как когда мы занимались с картой SD, мы его отключали, а карту SD оставим там, где она и была. Теперь наша схема получится вот такого вида (нажмите на картинку для увеличения изображения)
Теперь проект. Проект сделан из проекта урока 87 с именем ENC28J60_HTTPS_LARGE и назван был ENC28J60_HTTPS_SD. Только единственное отличие — положил я его во вложенную папку WB, так как проект у нас разрабатываться будет уже в среде System Workbench для STM32, так как после подключения библиотеки FATFS и написания нескольких строк коду проект превысил заветные 32 килобайта кода, чего бесплатная лицензия Keil делать категорически запрещает. И, как вы знаете, права мы ничьи не нарушаем, поэтому прибегнуть пришлось к бесплатной среде разработки. Я пробовал CooCox, но это отдельная история, и я забросил данное занятие. Да и плюс ко всему, мне всё же удалось победить недостаточный размер кучи в System Workbench и я наконец-то возобновил работу внешнего сборщика. Если кому интересно, как я это сделал, пишите в комментариях. Вообще, выбор компилятора — это дело ваше, так что можете пользоваться любым. Разница между ними по настройке и по работе не столь велика. Открываем проект в Cube MX, включаем SPI2 и FATFS
Также включим на выход ножку PA3, так как это у нас Chip Select для карты SD
Переходим на вкладку с настройками интерфейсов (Configuration) и поначалу настроим SPI (делитель можно включить теперь 2 и скорость у нас возрастёт до 10 мбпс, так как карта на такой скорости работает отлично и нам не нужно теперь ничего анализировать логическим анализатором), А такие настройки у шины SPI Cube устанавливает автоматически, поэтому ничего не трогаем и уходим из данного диалога
Настроим также FATFS, добавив поддержку длинных имён, вдруг пригодится, а также увеличив максимальный размер сектора
Также не забываем включить среднюю скорость для ножки PA3 в GPIO
Зайдём в настройки проекта и изменим там среду разработки
Сохраним настройки, сгенерируем проект, перейдём в 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, о чём свидетелсьвует резко возросший размер прошивки
Вот поэтому мы сейчас и работаем со средой программирования 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,
Соберём код, прошьём контроллер и попробуем запросить различные документы с сервера.
Запросим главную страницу (нажмите на картинку для увеличения изображения)
Запросим другой файл, например index1.htm
Загрузим в папку IMG на карту SD несколько файлов в формате jpeg
Также откроем файл index.htm и добавим туда картинку из файла куда-нибудь в текст страницы
</ul></p>
<p>
<img src="/IMG/img02.jpg" />
</p>
<p>Memories</p>
Вставим карту обратно в наш самодельный картоприёмник, подключим питание и запросим опять главную страницу (нажмите на картинку для увеличения изображения)
Можно вставить картинку и поменьше и побольше но только одну, иначе браузер пытается открыть несколько соединений, а у нас такой поддержки нет. Я пытался добавить виртуальные сокеты, но видимо потому, что свободное количество памяти у нас уже стремится к нулю, номер сокета, а также некоторые переменные, хотя и с аттрибутом volatile начинают без моего ведома менять значение, поэтому я забросил это занятие. Здесь нужен уже контроллер помощнее. Но и так, я считаю неплохо. если же мы хотим всё равно добавить несколько рисунков, правда небольших, обойти этот нюанс можно с помощью внедрения данных картинок прямо в код html, то есть в саму страницу. Для этого существует несколько онлайн-сервисов, с помощью которых можно сделать эти преобразования. Код выглядит приблизительно так
<img width="102" height="58" title="" alt="" src="data:image/jpeg;base64,/9j/4RSXRXhpZgAASUkqA... и т.д.
Таким образом я добился вывода нескольких изображений на странице
Таким образом, с помощью сегодняшнего урока мы смогли улучшить наш HTTP-сервер, расширив его память для хранения файлов с помощью подключения карты SD и файловой системы. Хотя конечно скорость оставляет желать лучшего, так как карта у нас подключена по интерфейсу SPI, что не позволяет общаться с ней со скоростью превышающей 1 мегабит в секунду, тем не менее мы ещё раз проработали протокол HTTP, научившись также учитывать и размер окна, а также передавать другие документы кроме главной страницы, причём и других форматов.
Спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добрый день. Я начинающий в программировании 🙂 В последних версиях куба в структуре FIl Fatfs отсутствует fsize. Как с этим бороться? Собираю по вашему уроку на контроллере f407 с SDIO SD.
Еще раз здравствуйте. Проблему с Fatfs решил. Осталась 1 проблема. Ели функция сети вызывается в прерывании то зависает на открытии файла с SD. Если в основном цикле то все прекрасно работает. Не подскажите в какую сторону копать?
Hello sir,
Thank you for your help. It is great.
I want to use Ajax for transferring data from server continuously to client and plot it on the gragh.
I want to do this with ENC28J60.
Don't have a suggestion to do this?
Здравствуйте.
Я не могу понять откуда взялись команды «sd_read_block» и «sd_Write_block»
Привет
Ваше обучение было очень хорошим.
В продолжение этого проекта я хочу собрать локальный веб-сервер с ENC 28j 60 и отображать текущие значения нескольких датчиков на веб-странице в браузере.Значения датчиков меняются мгновенно и в режиме реального времени. Как мне это сделать? Пожалуйста, помогите мне.