Привет, Хабровчане!
Это моя первая статья и у меня есть чем поделиться. Возможно мой велосипед не нов и этим способом пользуется каждый, но когда-то давно искал решения, с ходу найти не получилось.
О чем речь?
Задача состояла в подключении файлов: HTML, JS, CSS; без специальной подготовки. Так же неудобно подключать бинарные файлы (например картинки) конвертируя их в HEX. Так как не хотелось конвертировать в HEX или разделять на строки, искал способ подключения файла в адресное пространство программы.
Как обычно это выглядит
Пример, c разделением строк:
const char text[] =
"<html>" "\r\n"
"<body>Text</body>" "\r\n"
"</html>";
Пример, с HEX (больше подходит для бинарных данных):
const char text[] =
{
0x3C, 0x68, 0x74, 0x6D, 0x6C, 0x3E, 0x0A, 0x3C,
0x62, 0x6F, 0x64, 0x79, 0x3E, 0x54, 0x65, 0x78,
0x74, 0x3C, 0x2F, 0x62, 0x6F, 0x64, 0x79, 0x3E,
0x0A, 0x3C, 0x2F, 0x68, 0x74, 0x6D, 0x6C, 0x3E,
0
};
Видел даже такое:
#define TEXT "<html>\r\n<body>Text</body>\r\n</html>"
const char text[] = TEXT;
Все #define располагались в отдельном .h файле и подготавливались скриптом на Python. С аннотацией, что некоторые символы должны быть экранированы \ вручную в исходном файле. Честно немного волосы дыбом встали от такого мазохизма.
А хотелось, чтобы файлы можно было спокойно редактировать, просматривать и при компиляции всё само подключалось и было доступно, например так:
extern const char text[];
Оказалось всё просто, несколько строчек в Assembler.
Подключаем файл в Arduino IDE
Добавляем новую вкладку или создаём файл в папке проекта с названием text.S, там же размещаем файл text.htm.
Содержимое файла text.htm:
<html>
<body>Text</body>
</html>
Содержимое файла text.S:
.global text
.section .rodata.myfiles
text:
.incbin "text.htm"
.byte 0
Не забываем нулевой символ \0 в конце, он здесь в строке сам не добавится.
Сам скетч:
extern const char text[] PROGMEM;
void setup()
{
Serial.begin(115200);
Serial.println(text);
}
void loop() { }
Компилируем, загружаем и смотрим вывод:
Отлично, когда-то бы я от радости прыгал до потолка, от того что всё получилось.
Код работает в AVR8, но например в ESP8266 получим аппаратный сбой. Всё потому, что чтение из Flash доступно по 32 бита и по адресам кратным 32 бит. Чтобы было всё хорошо, каждому файлу требуется делать отступ для кратности, код будет выглядеть так:
.global text
.section .rodata.myfiles
.align 4
text:
.incbin "text.htm"
.byte 0
Загрузить можно в секцию кода: .irom.text, если не хватает места в .rodata.
Для STM32 так же рекомендуется выравнивать по 32 бита, но не обязательно.
А как записать размер данных во время компиляции? Например, для бинарных данных не получится остановится по нулевому символу. Так же просто:
.global text, text_size
.section .rodata.myfiles
text:
.incbin "text.htm"
text_end:
.byte 0
text_size:
.word (text_end - text)
Объявление:
extern const char text[] PROGMEM;
extern const uint16_t text_size PROGMEM;
Осталось написать макрос, для удобства подключения файлов:
.macro addFile name file
.global \name, \name\()_size
// .align 4
\name:
.incbin "\file"
\name\()_end:
.byte 0
// .align 4
\name\()_size:
.word (\name\()_end - \name)
.endm
.section .rodata.myfiles
addFile text1 1.txt
addFile text2 2.txt
addFile text3 3.txt
И макрос для объявления:
#define ADD_FILE(name) \
extern const char name[] PROGMEM; \
extern const uint16_t name##_size PROGMEM;
ADD_FILE(text1);
ADD_FILE(text2);
ADD_FILE(text3);
void setup()
{
Serial.begin(115200);
Serial.println(text1);
Serial.println(text1_size);
Serial.println(text2);
Serial.println(text2_size);
Serial.println(text3);
Serial.println(text3_size);
}
void loop() { }
Вывод:
Таким образом можно подключить любой файл и представить его любым типом, структурой или массивом.
Подключаем любой файл и не только, в GNU toolchain
Принцип тот же самый, ничем не отличается для Arduino. В принципе в Arduino используется тот же toolchain от Atmel.
Только здесь у нас в руках Makefile и мы можем до компиляции и сборки запустить какой-нибудь скрипт.
Для примера возьму код из готового моего проекта на STM32, где автоматически при компиляции увеличивается версия сборки. Так же включаются в проект WEB-интерфейс для последующего использования в LWIP / HTTPD.
Скрипт version.sh:
#!/bin/bash
# Version generator
# running script from pre-build
MAJOR=1
MINOR=0
cd "$(dirname $0)" &>/dev/null
FILE_VERSION="version.txt"
FILE_ASM="version.S"
BUILD=$(head -n1 "$FILE_VERSION" 2>/dev/null)
if [ -z "$BUILD" ]; then
BUILD=0
else
BUILD=$(expr $BUILD + 1)
fi
echo -n "$BUILD" >"$FILE_VERSION"
cat <<EOF >"$FILE_ASM"
/**
* no editing, automatically generated from version.sh
*/
.section .rodata
.global __version_major
.global __version_minor
.global __version_build
__version_major: .word $MAJOR
__version_minor: .word $MINOR
__version_build: .word $BUILD
.end
EOF
cd - &>/dev/null
exit 0
Создаётся файл version.S в который из version.txt загружается номер версии предыдущей сборки.
В Makefile добавляется цель pre-build:
#######################################
# pre-build script
#######################################
pre-build:
bash version.sh
В цель all надо дописать pre-build:
all: pre-build $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
Объявление и макросы для printf у меня в macro.h:
extern const uint16_t __version_major;
extern const uint16_t __version_minor;
extern const uint16_t __version_build;
#define FMT_VER "%u.%u.%u"
#define FMT_VER_VAL __version_major, __version_minor, __version_build
В HTTPD из LWIP немного был удивлён когда увидел, что содержимое файлов надо хранить вместе с заголовками HTTP. Чтобы не менять архитектуру, загрузку делал как это организовано в примере fsdata.c. Использовал fsdata_custom.c, для этого установлен флаг HTTPD_USE_CUSTOM_FSDATA.
Код в fsdata_custom.c:
#include "lwip/apps/fs.h"
#include "lwip/def.h"
#include "fsdata.h"
#include "macro.h"
extern const struct fsdata_file __fs_root;
#define FS_ROOT &__fs_root
Сборка файлов fsdata_make.S:
.macro addData name file mime
\name\():
.string "/\file\()"
\name\()_data:
.incbin "mime/\mime\().txt"
.incbin "\file\()"
\name\()_end:
.endm
.macro addFile name next
\name\()_file:
.word \next\()
.word \name\()
.word \name\()_data
.word \name\()_end - \name\()_data
.word 1
.endm
.section .rodata.fsdata
.global __fs_root
/* Load files */
addData __index_htm index.htm html
addData __styles_css styles.css css
addData __lib_js lib.js js
addData __ui_js ui.js js
addData __404_htm 404.htm 404
addData __favicon_ico img/favicon.ico ico
addData __logo_png img/logo.png png
/* FSDATA Table */
addFile __logo_png 0
addFile __favicon_ico __logo_png_file
addFile __404_htm __favicon_ico_file
addFile __ui_js __404_htm_file
addFile __lib_js __ui_js_file
addFile __styles_css __lib_js_file
__fs_root:
addFile __index_htm __styles_css_file
.end
В начале каждого файла загружается заголовок, пару примеров из папки mime.
Файл html.txt:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Connection: close
Файл 404.txt:
HTTP/1.1 404 Not found
Content-Type: text/plain; charset=UTF-8
Connection: close
Нужно обратить внимание на пустую строку, чего требует спецификация HTTP для обозначения конца заголовка. Каждая строка должна заканчиваться символом CRLF (\r\n).
P.S. Код проекта из ветхого сундука, так что в реализации мог забыть чего ни будь уточнить.
В завершении
Долго искал, что изложить в статье полезного. Надеюсь мой опыт пригодится новичку и гуру.
Спасибо за внимание, удачных разработок!