Определение кучи и размера стека для микроконтроллера ARM Cortex-M4?

11

Я работал над проектами небольших встроенных систем и выключал их. В некоторых из этих проектов использовался базовый процессор ARM Cortex-M4. В папке проекта находится файл startup.s . Внутри этого файла я отметил следующие две командные строки.

;******************************************************************************
;
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Stack   EQU     0x00000400

;******************************************************************************
;
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Heap    EQU     0x00000000

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


Ссылки:

Махендра Гунавардена
источник

Ответы:

12

Стек и куча - это концепции программного обеспечения, а не концепции аппаратного обеспечения. Аппаратное обеспечение - это память. Определение зон памяти, одна из которых называется «стеком», а другая - «кучей», является выбором вашей программы.

Аппаратное обеспечение помогает со стеками. Большинство архитектур имеют выделенный регистр, называемый указателем стека. Его предполагаемое использование заключается в том, что когда программа выполняет вызов функции, параметры функции и адрес возврата помещаются в стек, и они выталкиваются, когда функция завершается и возвращается к вызывающей стороне. Вставка в стек означает запись по адресу, указанному указателем стека, и соответственно уменьшение указателя стека (или увеличение в зависимости от направления роста стека). Popping означает увеличение (или уменьшение) указателя стека; адрес возврата читается из адреса, указанного указателем стека.

Некоторые архитектуры (но не ARM) имеют инструкцию вызова подпрограммы, которая объединяет переход с записью по адресу, указанному указателем стека, и инструкцию возврата подпрограммы, которая объединяет чтение по адресу, заданному указателем стека, и переход по этому адресу. В ARM сохранение и восстановление адреса выполняется в регистре LR, инструкции вызова и возврата не используют указатель стека. Однако существуют инструкции для облегчения записи или чтения нескольких регистров по адресу, указанному указателем стека, для проталкивания и извлечения аргументов функции.

Чтобы выбрать кучу и размер стека, единственная информация, относящаяся к оборудованию, - это сколько у вас общего объема памяти. Затем вы делаете свой выбор в зависимости от того, что вы хотите сохранить в памяти (с учетом кода, статических данных и других программ).

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

В коде, который вы просматриваете , Stack_Sizeконстанта используется для резервирования блока памяти в области кода (через SPACEдирективу в сборке ARM). Верхнему адресу этого блока присваивается метка __initial_sp, и он сохраняется в таблице векторов (процессор использует эту запись для установки SP после сброса программного обеспечения), а также экспортируется для использования в других исходных файлах. Heap_SizeКонстанта аналогично используется для резервирования блока памяти и метку к ее границам ( __heap_baseи __heap_limit) экспортируется для использования в других исходных файлах.

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

…
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler

…

                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
Жиль "ТАК - перестань быть злым"
источник
Знаете ли вы, как определяются эти значения 0x00200 и 0x000400
Махендра Гунавардена
@MahendraGunawardena Вы должны определить их, исходя из потребностей вашей программы. Ответ Найла дает несколько советов.
Жиль "ТАК - перестань быть злым"
7

Размеры стека и кучи определяются вашим приложением, а не в таблице данных микроконтроллера.

Стек

Стек используется для хранения значений локальных переменных внутри функций, предыдущих значений регистров ЦП, используемых для локальных переменных (чтобы их можно было восстановить при выходе из функции), адреса программы, к которому необходимо вернуться при выходе из этих функций, плюс некоторые накладные расходы на управление самим стеком.

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

Альтернативный метод оценки (где ОЗУ не ограничена) состоит в том, чтобы выделить намного больше стекового пространства, чем вам когда-либо понадобится, заполнить стек значением часового значения, а затем отслеживать, сколько вы фактически используете во время выполнения. Я видел отладочные версии среды выполнения языка Си, которые сделают это автоматически. Затем, когда вы закончите разработку, вы можете уменьшить размер стека, если хотите.

Куча

Вычислить размер кучи, которая вам нужна, может быть сложнее. Куча используется для динамически размещаемых переменных, поэтому, если вы используете malloc()и free()в программе на языке C, или newи deleteв C ++, именно там находятся эти переменные.

Однако, особенно в C ++, может происходить некоторое скрытое динамическое распределение памяти. Например, если у вас есть статически размещенные объекты, язык требует, чтобы их деструкторы вызывались при выходе из программы. Мне известна как минимум одна среда выполнения, в которой адреса деструкторов хранятся в динамически распределенном связанном списке.

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

Найл С.
источник
Спасибо за ответ, мне нравится, как определить конкретное число, например 0x00400 и т. Д.
Махендра Гунавардена
5

В дополнение к другим ответам, я хотел бы добавить, что при разделении ОЗУ между стеком и пространством кучи также необходимо учитывать пространство для статических неконстантных данных (например, глобальные переменные файлов, статические функции и масштаб всей программы). глобальные с точки зрения C и, вероятно, другие для C ++).

Как работает распределение стека / кучи

Стоит отметить, что файл сборки запуска является одним из способов определения региона; набор инструментов (как среда сборки, так и среда выполнения) в основном заботятся о символах, которые определяют начало стекового пространства (используется для хранения начального указателя стека в таблице векторов), а также начало и конец пространства кучи (используется динамическим распределитель памяти, обычно предоставляемый вашим libc)

В примере OP определены только 2 символа: размер стека в 1 КБ и размер кучи в 0 Б. Эти значения используются в другом месте для создания стека и пространства кучи

В примере @Gilles размеры определяются и используются в файле сборки, чтобы установить пространство стека, начинающееся везде и продолжительное время, определяемое символом Stack_Mem и устанавливающее метку __initial_sp в конце. Аналогично для кучи, где пробел является символом Heap_Mem (размером 0,5 кБ), но с метками в начале и конце (__heap_base и __heap_limit).

Они обрабатываются компоновщиком, который не будет выделять что-либо в пространстве стека и пространстве кучи, потому что эта память занята (символами Stack_Mem и Heap_Mem), но он может разместить эти воспоминания и все глобальные переменные везде, где это необходимо. Метки заканчивают тем, что были символами без длины в данных адресах. __Initial_sp используется непосредственно для таблицы векторов во время соединения, а __heap_base и __heap_limit - вашим кодом времени выполнения. Фактические адреса символов назначаются компоновщиком в зависимости от того, где он их разместил.

Как я упоминал выше, эти символы на самом деле не обязательно должны быть из файла startup.s. Они могут исходить из вашей конфигурации компоновщика (Scatter Load file в Keil, linkerscript в GNU), и в тех, которые вы можете иметь более точный контроль над размещением. Например, вы можете сделать так, чтобы стек находился в начале или конце ОЗУ, или держал глобалы в стороне от кучи или чего угодно. Вы даже можете указать, что HEAP или STACK просто занимают все ОЗУ, оставшиеся после размещения глобалов. Обратите внимание, что вы должны быть осторожны, так как добавление большего количества статических переменных уменьшит вашу другую память.

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

Размер стека

Что касается определения размера стека, многие цепочки инструментов могут дать вам максимальную глубину стека, анализируя деревья вызовов функций вашей программы, ЕСЛИ вы не используете рекурсию или указатели на функции. Если вы их используете, оценивая размер стека и предварительно заполняя его кардинальными значениями (возможно, с помощью функции ввода перед main), а затем проверяйте после того, как ваша программа некоторое время работала, где была максимальная глубина (где кардинальные значения конец). Если вы полностью исчерпали свою программу до предела, вы будете достаточно точно знать, сможете ли вы уменьшить размер стека или, если ваша программа потерпит крах или не останется кардинальных значений, вам нужно увеличить стек и повторить попытку.

Размер кучи

Определение размера кучи зависит от приложения. Если вы выполняете динамическое распределение только во время запуска, вы можете просто добавить пространство, необходимое в вашем коде запуска (плюс некоторые накладные расходы на управление памятью). Если у вас есть доступ к источнику вашего менеджера памяти, вы можете точно знать, что такое издержки, и, возможно, даже написать код для обхода памяти, чтобы дать вам информацию об использовании. Для приложений, которым требуется динамическая память времени выполнения (например, выделение буферов для входящих кадров Ethernet), лучшее, что я могу предложить, - это тщательно отточить размер стека и дать куче все, что осталось после стека и статики.

Заключительная записка (RTOS)

Вопрос OP был помечен для голого металла, но я хочу добавить примечание для RTOS. Часто (всегда?) Каждой задаче / процессу / потоку (я просто напишу здесь задачу для простоты) будет назначен размер стека при создании задачи, в дополнение к стекам задач, вероятно, будет небольшая ОС стек (используется для прерываний и тому подобное)

Структуры учета задач и стеки должны быть выделены откуда-то, и это часто будет из общего пространства кучи вашего приложения. В этих случаях ваш начальный размер стека часто не имеет значения, потому что ОС будет использовать его только во время инициализации. Я видел, например, что ВСЕ оставшееся пространство во время компоновки должно быть выделено для HEAP и помещено начальный указатель стека в конец кучи для роста в кучу, зная, что ОС будет выделять, начиная с начала кучи, и выделит стек ОС непосредственно перед тем, как покинуть стек initial_sp. Затем все пространство используется для выделения стеков задач и другой динамически выделяемой памяти.

Джон ОМ.
источник