Привет, Хабровчане!
Это моя первая статья и у меня есть чем поделиться. Возможно мой велосипед не нов и этим способом пользуется каждый, но когда-то давно искал решения, с ходу найти не получилось.
О чем речь?
Задача состояла в подключении файлов: 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.myfilestext: .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 4text: .incbin "text.htm" .byte 0
Загрузить можно в секцию кода: .irom.text, если не хватает места в .rodata.
Для STM32 так же рекомендуется выравнивать по 32 бита, но не обязательно.
А как записать размер данных во время компиляции? Например, для бинарных данных не получится остановится по нулевому символу. Так же просто:
.global text, text_size.section .rodata.myfilestext: .incbin "text.htm" text_end: .byte 0text_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.myfilesaddFile text1 1.txtaddFile text2 2.txtaddFile 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-buildMAJOR=1MINOR=0cd "$(dirname $0)" &>/dev/nullFILE_VERSION="version.txt"FILE_ASM="version.S"BUILD=$(head -n1 "$FILE_VERSION" 2>/dev/null)if [ -z "$BUILD" ]; thenBUILD=0elseBUILD=$(expr $BUILD + 1)fiecho -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.endEOFcd - &>/dev/nullexit 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 htmladdData __styles_css styles.css cssaddData __lib_js lib.js jsaddData __ui_js ui.js jsaddData __404_htm 404.htm 404addData __favicon_ico img/favicon.ico icoaddData __logo_png img/logo.png png/* FSDATA Table */addFile __logo_png 0addFile __favicon_ico __logo_png_fileaddFile __404_htm __favicon_ico_fileaddFile __ui_js __404_htm_fileaddFile __lib_js __ui_js_fileaddFile __styles_css __lib_js_file__fs_root:addFile __index_htm __styles_css_file.end
В начале каждого файла загружается заголовок, пару примеров из папки mime.
Файл html.txt:
HTTP/1.1 200 OKContent-Type: text/html; charset=UTF-8Connection: close
Файл 404.txt:
HTTP/1.1 404 Not foundContent-Type: text/plain; charset=UTF-8Connection: close
Нужно обратить внимание на пустую строку, чего требует спецификация HTTP для обозначения конца заголовка. Каждая строка должна заканчиваться символом CRLF (\r\n).
P.S. Код проекта из ветхого сундука, так что в реализации мог забыть чего ни будь уточнить.
В завершении
Долго искал, что изложить в статье полезного. Надеюсь мой опыт пригодится новичку и гуру.
Спасибо за внимание, удачных разработок!