Преодолевая пределы. Часть третья. Библиотеки в IAR.
Во второй части я предложил способ формирования ресурсов в среде IAR. Но не всегда достаточно выделения только ресурсов. Порой объем кода, требуемого для реализации сложного алгоритма, составляет десятки килобайт и выделить ресурсы данных при этом либо не представляется возможным, либо не дает желаемого результата. Не плохим выходом можно считать формирование "ресурсов кода".
Первая приходящая на ум идея - виртуальная машина, к сожалению, имеет больше минусов: создание самой ВМ, компилятора к ней, необходимость тщательной отладки, ну и естественно низкая скорость выполнения.
Гораздо интереснее выглядит вторая идея – библиотеки кода. Что если создать некое подобие *.dll/*.so файлов? Естественно на динамическую загрузку в полной мере я не претендую. Но реализовать аналог ROM с собственным API нам вполне по силам.
Про экспериментальную версию формирования библиотек кода, по аналогии с ресурсами, я вам расскажу в данной статье.
Теория (вода)
В целом создать библиотеку получается просто и быстро, но для того чтобы уберечь себя от ошибок, и "делать правильно" понадобится почитать документацию. Потребуются понимание архитектуры, немного ассемблера и умение работы со средой разработки.
Из архитектуры нам потребуется карта памяти контроллера (RM0016.pdf) и информация по семействам контроллеров (если верить PM0051.pdf).
Так для Low density и Medium density контроллеров флэш-память не более 32кбайт и не выходит за пределы 0xFFFF. Это сильно упрощает нам задачу: все указатели у нас будут __near, то есть размером 2 байта и для перехода используется соответствующие инструкции с 2-хбайтовым аргументом.
Следующим моментом является оперативная память: переменные, стек и куча. Общие принципы таковы, что данные и куча располагаются в младших адресах RAM, а стек в стерших. Рост кучи идет в сторону старшего адреса, рост стеку в сторону младшего, и где-то по середине они могут и встретится. Рассмотрим ситуацию, когда требуется зарезервировать для себя немного оперативной памяти:
- младшие адреса использовать не желательно: там располагается область Tiny данных, адрес которых составляет всего 1 байт. Код работы с ними меньшего размера, и данная область используется для передачи параметров и локальных переменных;
- середину брать мы не можем: либо стек ограничим больше требуемого, либо кучу. Точный размер стека и кучи нам не известен, да и они вполне могут пересекаться;
- старшие адреса наиболее подходящее место. Надо только сместить стек вниз для освобождения в верхних адресах требуемого нам объема данных.
Ну и последний, уже упоминавшийся, момент. Нам надо знать, как происходит вызов функций и передача им параметров. Работа с отладчиком и дизассемблером поистине увлекает. В целом вызов похож на register call, но со своими отклонениями:
- параметры помещаются в регистры (A – 1 байтные; X, Y – 2 байтные);
- следующие параметры помещаются в первые 8 байт памяти, с учетом выравнивания на границу размера;
- всё что не поместилось помещается в стек, без выравнивания.
Как видно из рисунка, параметры не обязательно идут друг за другом в памяти.
Локальные переменные раскидываются аналогичным образом, с использованием следующих 8 байт младших адресов.
Такой сложный формат используется для повышения производительности, но в некоторых случаях порождает монструозные коды "без толку" гоняющие данные между стеком и областью младших данных.
Создаем проект библиотеки
Создать библиотеку не так то и сложно. Нам достаточно:
- поместить в проект реализацию функций (перенести в него файл lcd.c);
- создать таблицу экспорта;
- доработать созданные по умолчанию настройки проекта.
Что такое таблица экспорта и зачем она нужна? Всё очень просто, так как при компиляции проекта функции могут размещаться по разным адресам. Что бы ни искать постоянно этот новый адрес и не прописывать его в основном проекте, вынесем адреса всех экспортируемых (доступных извне) функций в одной таблице, расположение которой будет постоянным и известным.
Для таблицы экспорта применяем массив записей следующего формата:
typedef struct {
char _code; // JP code, always 0xCC
void* _pfn; // function real address
char _name[13]; // function name as string
} LibraryExport;
поле _code всегда будет содержать код 0xСС – код команды безусловного перехода для STM8. Сразу после кода идет адрес реального расположения функции _pfn. Данные два поля размером в три байта сформируют в памяти команду безусловного перехода на код функции.
Таким образом, когда мы "вызовем запись массива" выполнится команда перехода и управление будет передано реальной функции. Переход выполняется за 2 такта, что не занимает много времени, зато разработка гораздо удобнее.
Заполняется структура довольно просто:
#define EXPORT_FUNC(func, name) \
{0xCC, (void*)(func), name},
#pragma location=".e.table"
const LibraryExport Nokia3310_export_table[] = {
EXPORT_FUNC(LCD_init , "Lib3310 v0.1")
EXPORT_FUNC(LCD_writeData , "_wrData")
EXPORT_FUNC(LCD_writeCommand, "_wrCommand")
EXPORT_FUNC(LCD_clear , "clear")
EXPORT_FUNC(LCD_gotoXY , "gotoXY")
EXPORT_FUNC(LCD_writeChar , "writeChar")
EXPORT_FUNC(LCD_writeString , "writeString")
EXPORT_FUNC(LCD_screen , "writeScreen")
};
Указав в качестве расположения таблицы секцию .e.table мы и указали "известный адрес размещения всех функций" (см. главу "Договариваемся с линкером"): адрес таблицы известен, размер записи известен, зная номер функции в таблице экспорта мы получаем адрес функции.
Последнее поле структуры _name не обязательное и может быть смело опущено. Оно предназначено для хранения "читаемого" имени функции/библиотеки. Добавлено оно для удобства анализа прошивки контроллера.
Заполненную структуру указываем в качестве точки входа для проекта библиотеки.
Это гарантирует, что ни одна из экспортируемых функций не будет исключена при сборке.
Так же следует обратить внимание на наличие флажка "Automatic runtime library selection", он требуется в частности для задания правил передачи и параметров в функции. Это слабое место в данной модели. Требуется, чтобы для всех проектов использовались одинаковые библиотеки, а если верить документации, то компилятор "самостоятельно выберет версию библиотеки, наиболее подходящую для проекта". Для такого простого примера проблемы не возникает, но в дальнейшем надо поразмыслить.
После произведённых манипуляций библиотека собирается и даже работает, но код располагается по адресу 0x8000, который "принадлежит" основному проекту. Вдобавок адреса переменных так же пересекаются с переменными из основного проекта. Следовательно, нам снова требуется обратиться к линкеру.
Договариваемся с линкером
На этот раз нам требуется чуть более глубокое знание структуры линкер файла. Полистав довольно небольшую документацию по нему, я открыл для себя много нового и интересного. Не хочу лишать вас сего удовольствия и отправлю непосредственно к документации, а в статье приведу лишь готовый результат. Вариант не оптимальный и не лучший (как минимум есть вариант сделать всё одним файлом), но "я не волшебник, я только учусь".
Для начала надо вынести "общую часть" для всех проектов в один файл настроек линкера. В нем будет содержаться:
- определения всех регионов – что бы ни пришлось при перемещении образа в памяти контроллера править десяток разнесенных файлов;
- размещение ресурсов – нет смысла под каждый ресурс выделять отдельный файл правил;
- правила инициализации (тут ещё надо подумать верно ли выбрано);
- Tiny блок данных - используется для передачи параметров и локальных переменных функций;
- прочие мелочи.
Таким образом, общий файл lnkodo.icf (по совместительству подключаемый в настройках линкера всех проектов ресурсов) приобретает следующий вид:
define memory with size = 16M;
define region TinyData = [from 0x00 to 0xFF];
define region NearData = [from 0x0000 to 0x07EF];
define region LcdData = [from 0x07F0 to 0x07FF];
define region Eeprom = [from 0x4000 to 0x43FF];
define region BootROM = [from 0x6000 to 0x67FF];
define region NearFuncCode = [from 0x8000 to 0x9FFF];
define region ResourceZone1 = [from 0xA000 to 0xA1FF];
define region ResourceZone2 = [from 0xA200 to 0xA3FF];
define region ResourceZone3 = [from 0xA400 to 0xA5FF];
define region LcdCode = [from 0xA600 to 0xAFFF];
define region FarFuncCode = [from 0xB000 to 0xFFFF];
define region HugeFuncCode = [from 0xB000 to 0xFFFF];
// Initialization
initialize by copy { rw section .far.bss,
rw section .far.data,
rw section .far_func.textrw,
rw section .huge.bss,
rw section .huge.data,
rw section .huge_func.textrw,
rw section .iar.dynexit,
rw section .near.bss,
rw section .near.data,
rw section .near_func.textrw,
rw section .tiny.bss,
rw section .tiny.data,
ro section .tiny.rodata };
do not initialize { rw section .eeprom.noinit,
rw section .far.noinit,
rw section .huge.noinit,
rw section .near.noinit,
rw section .tiny.noinit,
rw section .vregs };
// Placement for resources form other projects
place at start of ResourceZone1 { ro section .recource1 };
place at start of ResourceZone2 { ro section .recource2 };
place at start of ResourceZone3 { ro section .recource3 };
// Placement
place at start of TinyData { rw section .vregs };
place in TinyData { rw section .tiny.bss,
rw section .tiny.data,
rw section .tiny.noinit,
rw section .tiny.rodata };
Для основного проекта файл линкера содержит:
- подключение общей части правил;
- размещение для таблицы векторов прерываний;
- размещение для стека;
- размещение для кучи;
- размещение для данных;
- размещение для кода.
Файл lnkodomain.icf, подключаемый в настройках линкера основного проекта, достаточно прост:
include "lnkodo.icf";
define block INTVEC with size = 0x80 { ro section .intvec };
define block HEAP with size = _HEAP_SIZE {};
define block CSTACK with size = _CSTACK_SIZE {};
// Placement
place at end of NearData { block CSTACK };
place in NearData { block HEAP,
rw section .far.bss,
rw section .far.data,
rw section .far.noinit,
rw section .far_func.textrw,
rw section .huge.bss,
rw section .huge.data,
rw section .huge.noinit,
rw section .huge_func.textrw,
rw section .iar.dynexit,
rw section .near.bss,
rw section .near.data,
rw section .near.noinit,
rw section .near_func.textrw };
place at start of NearFuncCode { block INTVEC };
place in NearFuncCode { ro section .far.data_init,
ro section .far_func.textrw_init,
ro section .huge.data_init,
ro section .huge_func.textrw_init,
ro section .iar.init_table,
ro section .init_array,
ro section .near.data_init,
ro section .near.rodata,
ro section .near_func.text,
ro section .near_func.textrw_init,
ro section .tiny.data_init,
ro section .tiny.rodata_init };
place in FarFuncCode { ro section .far.rodata,
ro section .far_func.text };
place in HugeFuncCode { ro section .huge.rodata,
ro section .huge_func.text };
place in Eeprom { rw section .eeprom.noinit };
Для проекта библиотеки файл линкера содержит:
- подключение общей части правил;
- размещение для таблицы экспорта;
- размещение для данных;
- размещение для кода.
Файл lnkodolcd.icf, подключаемый в настройках линкера библиотеки дисплея:
include "lnkodo.icf";
define block EXPORTTABLE { ro section .e.table };
place in LcdData { block HEAP,
rw section .far.bss,
rw section .far.data,
rw section .far.noinit,
rw section .far_func.textrw,
rw section .huge.bss,
rw section .huge.data,
rw section .huge.noinit,
rw section .huge_func.textrw,
rw section .iar.dynexit,
rw section .near.bss,
rw section .near.data,
rw section .near.noinit,
rw section .near_func.textrw };
place at start of LcdCode { block EXPORTTABLE };
place in LcdCode { ro section .far.data_init,
ro section .far_func.textrw_init,
ro section .huge.data_init,
ro section .huge_func.textrw_init,
ro section .iar.init_table,
ro section .init_array,
ro section .near.data_init,
ro section .near.rodata,
ro section .near_func.text,
ro section .near_func.textrw_init,
ro section .tiny.data_init,
ro section .tiny.rodata_init };
После данных манипуляций у нас код и данные проектов будут размещены каждый в свой области.
Использование библиотеки
Как известно из второй части для указания компилятору конкретного адреса переменной/константы можно воспользоваться #pragma location. Только для функций, не смотря на упоминание в документации, сделать этого, мы не можем.
void LCD_init (void *cfg) @0xA600;
// Приводит к ошибке
Попытка задания константного указателя на функцию так же не увенчалась успехом. Компилятор не считает его константой и выделяет под него память:
typedef void (*PFN)(void);
const PFN foo = (PFN)0xA600;
foo();
// при вызове приводит к генерации косвенного вызова
008082 72CD81FB CALL [foo.w]
Зато нам всегда доступна возможность задания адреса через #define:
// Задали базовый адрес расположения библиотеки в памяти контроллера
#define LCDLIB_BASE 0xA600
// простой для понимания вариант
typedef void (*PFNinit) (void*);
#define LCD_init ((PFNinit)(LCDLIB_BASE+0x00))
// или одной строкой
#define LCD_writeData ((void (*) (unsigned char)) (LCDLIB_BASE + 0x10))
Дорабатывать код вызова функций никак не требуется. Нам всё равно, где находится функция и как для неё задан адрес. Генерируемый код вызова так же абсолютно идентичен.
Запуск
Как и прежде при запуске мы можем наблюдать корректную работу примера.
Как и прежде нам доступна отладка. Как и прежде подсветка исходного кода доступна только в текущем проекте, для остальных присутствует только код дизассемблера. При этом дизассемблер ведёт себя несколько странно, и не желает из библиотеки разбирать код основного проекта по командам, считая, что там расположен просто массив данных. В прочем, на мой взгляд, это не так страшно.
Вместо заключения
Это только экспериментальная, хоть и вполне рабочая, версия формирования библиотек в среде IAR. Применять её надо с крайней осторожностью и как говорится "на свой страх и риск". Из приятного: не требуется выделять такие двоичные библиотеки заранее, достаточно "просто" перенести имеющийся проект под данную модель.
Свои изыскания на эту тему я приостановил: пока в них нет необходимости, но на случай чего я готов.
P.S.: Хотелось бы услышать ваше мнение: отзывы, мнения, предложения. Только, пожалуйста, "и нафига этот велосипед?" просьба писать в развернутой форме, а именно: почему это велосипед и какие вы можете предложить альтернативные решения.
Файлы: odo_iar_libs.zip, complite_iar_lib.s19.zip
< Предидущая