VisualDSP: использование секций памяти Печать
Добавил(а) microsin   

Рабочее окружение выполнения кода реального времени C/C++ требует использования в программе специальных секций памяти, чтобы код был правильно распределен в нужных местах адресного пространства. В модулях программы на языке ассемблера эти имена используются как метки для директивы .SECTION. В модулях программы на языке C/C++ для этого используются специальные операторы и директивы (об этом далее). В файле .ldf (linker definition file, файл управления линкером) имена секций используются как метки для выходных имен секций в команде SECTIONS{}.

Примечание: подробную информацию по синтаксису файла .ldf и другую информацию, относящуюся к линкеру, см. в руководстве "VisualDSP++ Linker and Utilities Manual". Также информацию по директивам управления линкером можно получить в справочной системе VisualDSP, меню Help -> Contents... -> раздел справки Manuals -> Software Tool Manuals -> Blackfin C/C++ Compiler and Library Manual -> 1 Compiler -> C/C++ Compiler Language Extensions -> Pragmas -> Linker Control Pragmas -> #pragma section/#pragma default_section.

У процессоров Blackfin есть несколько видов памяти, различающиеся размером и скоростью работы (SRAM L1, SDRAM, FLASH). В коде программы можно управлять работой компилятора, чтобы он разместил Ваш код в той памяти, которая нужна. Это делается с помощью секций памяти и специальных расширений языка, доступных через директиву #pragma.

Компилятор для Blackfin C/C++ поддерживает прагмы (pragma). Это специальная директива в коде программы, указанная с определенными параметрами. Прагмы отражают управлением специфическими особенностями процессора, которые не поддерживаются стандартными средствами языка C/C++, и они модифицируют поведение компилятора. Есть два типа использования pragma: директивы pragma и операторы pragma.

Директивы pragma имеют следующий синтаксис:

#pragma pragma-директива pragma-директива-операнды новая-строка

Операторы pragma имеют следующий синтаксис:

_Pragma (string-literal)

Когда обрабатывается оператор pragma, компилятор превращает его в директиву pragma, используя не-строковую версию для string-literal. Это означает, что следующая директива pragma:

#pragma linkage_name mylinkname

может быть эквивалентной следующему оператору pragma:

_Pragma ("linkage_name mylinkagename")

Примеры в руководстве обычно используют форму директивы pragma.

Компилятор языка C поддерживает прагмы для:

• Управления выравниванием данных.
• Определение функций, которые должны работать как обработчики прерываний (ISR).
• Изменения уровня оптимизации внутри модуля кода.
• Изменения внешней видимости функции, когда она линкуется.
• Предоставление конфигурации для заголовочных файлов и свойств.
• Предоставления дополнительной информации по использованию циклов и улучшению оптимизации.

Компилятор выдает предупреждение, когда он встречает в коде не распознаваемую директиву pragma или нераспознаваемый оператор pragma.

[Секции памяти, используемые компилятором по умолчанию]

Место для кода программы (Code Storage). Секция кода program это то место, куда компилятор поместит исполняемые инструкции, генерируемые при компиляции исходного кода программы. Секция cplb_code существует для того, чтобы подпрограммы управления защитой памяти могли поместить код в области памяти, которые всегда сконфигурированы как доступные. Секция noncache_code отображается на память, которая не может быть сконфигурирована как кэш. Секция noncache_code используется библиотекой реального времени выполнения (run-time library, RTL).

Место для данных (Data Storage). Секция данных data1 это то место в памяти, куда компилятор поместит глобальные и статические данные. Секция данных constdata это то место, где компилятор помещает данные, декларированные с атрибутом const. По умолчанию компилятор помещает глобальные инициализированные нулями переменные в секцию "BSS-стиля", называемую bsz, за исключением случаев, когда компилятор запущен с опцией -no-bss. Секция cplb_data существует для того, чтобы таблицы конфигурации, использованные для управления защитой памяти, могли бы быть размещены в областях памяти, которые всегда помечены как доступные.

Ключ командной строки компилятора -no-bss дает указание компилятору размещать и zero-initialized и non-zero-initialized данные в одной и той же секции данных, а не в раздельных секциях, когда zero-initialized данные помещаются в отдельную, с так называемую секцию BSS-style. См. также описание опции –bss.

Ключ командной строки компилятора -bss дает указание компилятору размещать и zero-initialized и данные в секцию BSS-style (с именем bsz), а не в обычную секцию для данных. См. также описание опции –no-bss.

Run-Time Stack. Стек времени выполнения размещается в секции памяти stack, что требуется для правильного функционирования программы. Секция stack должна быть отображена в файле .ldf. Стек времени выполнения является структурой шириной в 32 бита, которая растет от памяти с большими адресами в сторону памяти с меньшими адресами. Компилятор использует run-time стек как область для хранения локальных переменных и для адресов возврата из подпрограмм. Дополнительно про управление стеком см. раздел руководства "Managing the Stack".

Run-Time Heap Storage. Секция heap предназначена для размещения памяти кучи времени выполнения (run-time heap), это то место в памяти, которую используют подпрограммы динамического выделения памяти и сборщик мусора (если это все используется). Когда происходит линковка, используйте Ваш файл .ldf для отображения в нем секции heap. Для динамического выделения, перераспределения и освобождения блоков памяти имеются библиотечные функции C run-time library: malloc(), calloc(), realloc(), free(). Также кучу использует C++ своими операторами new и delete для создания и удаления переменных и экземпляров класса. По умолчанию все выделения кучи делаются в секции памяти heap. В файле .ldf должны быть заданы символические константы ldf_heap_space, ldf_heap_end и ldf_heap_length, чтобы могли работать подпрограммы обслуживания кучи.

[Ключевое слово section()]

Ключевое слово section() указывает компилятору разместить объект или функцию (на языке ассемблера это директива .SECTION) в нужной секции объектного файла. Параметром ключевого слова section() будет строковый литерал, который задает имя секции - т. е. имя секции, указанное в двойных кавычках (на языке ассемблера имя секции идет после директивы .SECTION без кавычек). Если Вы не укажете ключевое слово section() для объекта при его декларации, то компилятор будет использовать секцию по умолчанию (default section), которую сам подберет по контексту. Файл .ldf, который предоставляется для линкера, также должен быть обновлен, чтобы в нем содержались используемые дополнительные именованные секции.

Применение section() имеет значение только тогда, когда компилятор может разместить заданный элемент данных в именованной секции. Применяйте section() только на верхнем уровне для именованных объектов, которые имеют постоянное время жизни (например, для объектов, явно определенных как static, или для глобальных объектов, для которых определены ссылки на объект как на внешний, external).

Следующий пример декларирует статическую переменную x, которая будет размещена в секции памяти с именем bingo.

static section("bingo") int x;

Ключевое слово section() имеет ограничение: спецификаторы инициализации секции не могут использоваться в строке имени секции. Компилятор может генерировать метки, содержащие эту строку, так что неправильно указанное имя может привести к ошибкам синтаксиса. Также ключевое слово не совместимо с любыми директивами pragma, которые предшествуют объекту или функции. Для точного управления размещением секций и совместимости с другими pragma-директивами, используйте #pragma section.

Примечание: ключевое слово section заменяет ключевое слово segment, которое применялось в предыдущих версиях компилятора. Хотя ключевое слово segment() поддерживается компилятором в текущем релизе, компания Analog Devices рекомендует провести ревизию Вашего старого кода и заменить segment на section.

[#pragma section / #pragma default_section]

Прагмы компилятора section и default_section предоставляют качественный контроль над секциями памяти, куда компилятор помещает символы.

Прагма section(СТРОКАСЕКЦИИ [, QUALIFIER, ...]) используется для переопределения целевой секции для любого глобального или статического символа, который сразу за ней следует. Прагма позволяет лучше управлять квалификатором секции в сравнении с ключевым словом section.

Прагма default_section(ВИДСЕКЦИИ [, СТРОКАСЕКЦИИ [, QUALIFIER, ...]]) используется для переназначения секций по умолчанию, в которые компилятор помещает свои символы.

Секции по умолчанию попадают в категории, перечисленные под ВИДСЕКЦИИ. За исключением категории STI, эта прагма остается в силе для категории секции до её следующего использования с отдельной категорией, или до конца файла. STI является исключением, только в одном STI можно определить default_section, и его область действия весь файл, не только часть после использования STI. Будет выдаваться предупреждение, если в одном и том же файле будет указано несколько секций STI.

Пропуск имени секции приводит к тому, секция по умолчанию будет сброшена к секции, которая была в использовании с начала файла, которая может быть либо значением по умолчанию для компилятора, либо значением, установленным пользователем через опцию командной строки -section (например, -section ВИДСЕКЦИИ=СТРОКАСЕКЦИИ).

Во всех случаях (включая STI), прагма default_section перезаписывает значение, указанное опцией -section в командной строке.

#pragma default_section(DATA, "NEW_DATA1")
int x;
#pragma default_section(DATA, "NEW_DATA2")
int x=5;
#pragma default_section(DATA, "NEW_DATA3")
int x;

В этом случае x размещается в NEW_DATA2 потому, что определение x попадает в её действие. Прагма default_section может быть использована только в глобальной области действия, где разрешены глобальные переменные.

ВИДСЕКЦИИ может быть одним из ключевых слов, перечисленных в таблице 1-29.

Таблица 1-29. Виды секций для параметра ВИДСЕКЦИИ.

Ключевое слово Описание
CODE Секция используется для кода процедур и функций программы.
ALLDATA Краткое указание на секции DATA, CONSTDATA, BSZ, STRINGS и AUTOINIT.
DATA Секция, которая содержит "обычные" данные (переменные).
CONSTDATA Секция, которая содержит данные только для чтения (константы).
BSZ Секция, которая содержит не инициализируемые данные.
SWITCH Секция используется для таблиц перехода реализации операторов switch языка C/C++.
VTABLE Секция используется для таблиц виртуальных функций C++.
STI Секция содержит код, необходимый для выполнения инициализаций C++. Подробнее см. раздел справки Constructors and Destructors of Global Class Instances.
STRINGS Секция, которая содержит строковые литералы.
AUTOINIT Секция содержит данные, используемые для инициализации агрегатов C++.

СТРОКАСЕКЦИИ является строкой, заключенной в двойные кавычки, содержащей имя секции, полностью как она появляется в файле ассемблера.

Изменение одного вида секции не влияет на другие виды секции. Например, даже при том, что виды секций STRINGS и CONSTDATA по умолчанию помещаются компилятором в одну и ту же секцию, если изменена секция по умолчанию для CONSTDATA, то это никак не повлияет на данные STRINGS.

Обратите внимание, что ALLDATA не реальная секция, а псевдо-вид, который соответствует DATA, CONSTDATA, STRINGS, AUTOINIT и BSZ. Изменение ALLDATA эквивалентно изменению всех этих видов секций. Таким образом,

#pragma default_section(ALLDATA, params)

будет эквивалентно последовательности:

#pragma default_section(DATA, params)
#pragma default_section(CONSTDATA, params)
#pragma default_section(STRINGS, params)
#pragma default_section(AUTOINIT, params)
#pragma default_section(BSZ, params)

QUALIFIER может быть одним из ключевых слов, перечисленных в таблице 1-30.

Таблица 1-30. Ключевые слова для QUALIFIER.

Ключевое слово Описание
ZERO_INIT Секция инициализируется нулями при запуске программы.
NO_INIT Секция не инициализируется при запуске программы.
RUNTIME_INIT Секция, инициализируемая пользователем при запуске программы.
DOUBLE32 Секция может содержать 32-разрядные, но не 64-разрядные числа типа double.
DOUBLE64 Секция может содержать 64-разрядные, но не 32-разрядные числа типа double.
DOUBLEANY Секция может содержать либо 32-разрядные, либо 64-разрядные числа типа double.

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

В следующем коде указано, что f() должна быть размещена в секции foo, квалифицированной как DOUBLEANY:

#pragma section("foo", DOUBLEANY)
void f() {}

Компилятор всегда пытается соблюдать прагму section как с высшим приоритетом, и прагма default_section всегда имеет приоритет меньше. Например, следующий код в результате приведет к тому, что функция f будет размещена в секции foo:

#pragma default_section(CODE, "bar")
#pragma section("foo")
void f() {}

В следующем коде x будет помещен в секции zeromem:

#pragma default_section(BSZ, "zeromem")
int x;

Примечание: в случаях, когда объект C++ STL требуется разместить в какой-то определенной секции памяти, использование #pragma section/default_section не работает. Вместо этого должна использоваться куча не по умолчанию (non-default heap), как это разъяснено в руководстве "Allocating C++ STL Objects to a Non-Default Heap".

[Использование памяти]

В этом разделе приведено несколько кратких рекомендаций по оптимальному размещению данных с целью повышения быстродействия.

Попробуйте поместить массивы в разных секциях памяти, что позволит более эффективно выполнять манипуляции с содержимым памяти. Аппаратура процессора может позволить одновременное выполнение 2 операций с памятью в одной команде ассемблера, эти операции выполняются за выполнение одной инструкции. Такие двойные операции будут выполняться за 1 такт только в том случае, если 2 адреса будут расположены в разных блоках памяти. Если оба доступа будут осуществляться в одном блоке, то процессор будет тратить такты на приостановку.

Рассмотрим ниже пример цикла скалярного произведения. Из-за того, что данные в каждой итерации цикла загружаются как из массива a, так и из массива b, может быть полезным обеспечить размещение этих массивов в разных блоках памяти. Таким образом, если никак не позабоиться о размещении массивов, то компилятор разместит оба массива в одном и том же блоке.

// ПЛОХАЯ РЕАЛИЗАЦИЯ: компилятор предполагает, что оба доступа вместе
// могут приводить к приостановке.
for (i=0; i < 100; i++)
   sum += a[i] * b[i];

Для того, чтобы разместить массивы в разных блоках, сначала нужно задать 2 банка в области MEMORY файла .ldf. Пример:

MEMORY {
   BANK_A1 {
      TYPE(RAM) WIDTH(8)
      START(start_address_1) END(end_address_1)
   }
   BANK_A2 {
      TYPE(RAM) WIDTH(8)
      START(start_address_2) END(end_address_2)
   }
}

Затем нужно сконфигурировать часть SECTIONS, чтобы указать линкеру разместить секции данных в специально заданных банках памяти.

SECTIONS {
   bank_a1 {
      INPUT_SECTION_ALIGN(4)
      INPUT_SECTIONS($OBJECTS(bank_a1))
   } >BANK_A1
   bank_a2 {
      INPUT_SECTION_ALIGN(4)
      INPUT_SECTIONS($OBJECTS(bank_a2))
   } >BANK_A2
}

В исходном коде на языке C выполните декларацию массивов с использованием конструкции section("section_name"), которая должна предшествовать декларации буфера; в нашем случае это будет так:

section("bank_a1") short a[100];
section("bank_a2") short b[100];

Теперь гарантируется, что 2 массива в цикле будут использоваться без лишних тактов приостановки.

Линковщик может генерировать карту памяти приложения (memory map) в формате XML. Этот файл по умолчанию будет находиться в выходной папке компилятора (обычно в папках Debug или Release, находящихся в корневом каталоге проекта), и файл будет называться имяпроекта.map.xml. Чтобы задать генерацию карты памяти, зайдите в настройки опций проекта, меню Project -> Project Options... -> Link -> General, поставьте галочки Generate symbol map (будет генерироваться карта памяти в файле имяпроекта.map.xml) и Generate xref (в файле XRef.xml будет генерироваться информация о перекрестных ссылках).

VisualDSP Link memory map options

Распределение памяти в программе задается с помощью обработки LDF-файла, привязанного к проекту. Привязка может осуществляться двумя способами:

Привязка LDF по умолчанию. Работает в том случае, если в проекте не выбрана опция генерации/добавления индивидуального файла управления линкером (*.ldf). В этом случае применяются настройки LDF-файла, расположенного в каталоге установки VisualDSP++. Например, если в проекте используется процессор ADSP-BF538, то будет использоваться файл линкера "%ProgramFiles%\Analog Devices\VisualDSP 5.0\Blackfin\ldf\ADSP-BF538.ldf".

Отдельный файл LDF. Индивидуальный файл настройки линкера можно добавить, если в настойках проекта выбрать пункт Add Startup Code/LDF. В этом случае в опциях проекта появится отдельный раздел настроек LDF Settings, где можно управлять распределением памяти системы. Также после того, как сделаете все настройки и нажмете кнопку OK, в корневом каталоге проекта появится файл имяпроекта.ldf, который можно редактировать текстовым редактором. В этом случае, если Вы хотите сохранить эти сделанные вручную изменения, то нужно в разделе настроек Remove Startup Code/LDF -> выбрать вариант Leave files in the project, but stop regenerating them.

[Ссылки]

1. VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors site:analog.com.