В данной статье нам предстоит выяснить что такое и для чего предназначен Stack и Heap.

Начнем со стека. Стек для микроконтроллеров представляет собой область памяти для хранения адресов возврата из вызванной функции, а так же для хранения переменных объявленных на уровне функции и аргументов вызываемой функции. Если вы используете высокоуровневый язык программирования, за размещение вышеперечисленных данных отвечает компилятор в зависимости от сложности функции и его алгоритма. При использовании ассемблера, вы сами решаете что помещать в стек и когда (команды push и pop).

Структура стека устроена по принципу LIFO (last in — first out), т.е. последним зашел - первым вышел. Из за этой особенности и было использовано название Stack (Стопка).

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

В микроконтроллерах ARM Cortex M работа со стеком организована на уровне ядра. Список элементов размещается во внутренней оперативной памяти. Для МК STM32 размер стека указывается в файле startup_stm32fxxx.s.

Обратите внимание на то что при использовании сторонних библиотек, нужно так же обращать внимание на минимальный размер стека. Например в документации библиотек от Keil (раздел Resource Requirements) содержится необходимая информация. Помимо этого при работе с RTOS необходимо учитывать, что для каждой задачи выделяется свой стек (его программная реализация), в таком случае стек указанный в файле startup, используется функциями обработки исключений и прерываний драйверов и его размер будет зависеть от максимальной вложенности используемых обработчиков.

В МК с ядром Cortex-M адрес вершины стека хранится в регистре R13 (SP). В режиме отладки вы можете понаблюдать как изменяется его значения при переходе в функции (для Keil окно Registers).

Перейдем к Heap (куча). Куча - это выделенная область оперативной памяти для дальнейшей динамической работы с ней в процессе выполнения приложения. Динамическое выделение памяти, подразумевает временное выделение памяти для нужд той или иной задачи.

Например, для выполнения некой задачи необходим массив размером 1024 байта, мы создаем данный массив из адресного пространства кучи, копируем в него данные, выполняем задачу, сохраняем или выводим результат и удаляем массив, тем самым освободив пространство для других задач.

Тут же может возникнуть вопрос, зачем все это? Необходимость в динамическом выделении памяти возникает тогда, когда перед нами стоит множество задач, каждая из которых требует приличное количество памяти, а в нашем МК она ограничена. И если мы не влезаем по памяти, имеется два решения:

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

2. Если нет принципиальной важности выполнять задачи одновременно, то можно применить динамическое выделение памяти, т.е. выделять необходимые объемы памяти только для выполняемой задачи.

Предполагается, что ваш код достаточно оптимизирован и недостаток памяти неизбежен.

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

Кучу в основном можно охарактеризовать двумя параметрами. Это ее размер и метод выделения. Если с размером все понятно, то о методах стоит немного рассказать.

Проблема при динамическом выделении памяти состоит в том, что при работе с данными разных размеров начнется неизбежная фрагментация памяти. Например, освободив маленький участок памяти, Вы не сможете поместить в освободившееся место более большой объект, придется найти для него свободное место, где он сможет поместиться. Таким образом память будет расходоваться менее эффективно. Для решения данной проблемы было разработано множество алгоритмов. Естественно, чем сложнее алгоритм, тем он более затратен по времени обработки процессором. С различными методами, можно познакомиться на примере FreeRTOS (heap_1, heap_2...).

В языке Си для работы с динамической памятью предназначены функции malloc() и free(). Работают данные функции с использованием указателей, т.е. при выделении памяти функция malloc вернет вам указатель на начало выделенного участка, соответственно для удаления необходимо передать данный указатель в функцию free. В C++ используются операторы new и delete для работы с объектами.  

Как же обстоят дела в STM32? Здесь нам предоставляют только возможность выбрать размер кучи, а метод зарыт глубоко в поставляемых с средой разработки библиотеках, которые соответствуют стандарту Си/C++.

Размер кучи, так же как размер стека указан в файле startup.

Закончить статью предлагаю несколькими советами.

1. Не усложняйте проект, если в этом нет необходимости. Если вам более чем достаточно памяти для решения задачи, нет необходимости использовать динамическую память.

2. В RTOS есть специальный функционал для передачи данных между задачами (mail box или т.п.). Если вам необходимо передать большой объем данных между задачами, используйте динамическое выделение памяти. Например, создаем объект, заполняем его данными и передаем не объект целиком, а указатель на объект во вторую задачу. Далее в первой задаче начинаем работать с новым созданным объектом. Во второй задаче принимаем указатель, обрабатываем данные и удаляем объект. Применяя такую логику вы избежите не нужного выделения памяти из RTOS и лишних копирований данных из одной области памяти, в другую.

3. Как можно контролировать занятое пространство динамической памяти? Переопределить методы работы с памятью. Например так:

/*----------------------------------------------------------------------------*/
volatile uint32_t memSize = 0;
/*----------------------------------------------------------------------------*/    
void *sec_malloc(size_t size)
{
    /* Store the memory area size in the beginning of the block */
    uint8_t *ptr = malloc(size + 8);
    *((size_t *)ptr) = size;
    memSize += size + 8;
    return ptr + 8;
}
//============================================================================//
void sec_free(void *ptr)
{
  size_t size;
    uint8_t * p = ptr;

  p -= 8;
  size = *((size_t *)p);

    guaranteed_memset(p, 0, size + 8);
    free(p);
    memSize -= (size + 8);
}
//============================================================================//

Данные методы позволят иметь полное представления о том что происходит с динамической памятью. В глобальной переменной memSize вы будете видеть сколько на данный момент памяти используется. Имея такие данные Вам будет проще отслеживать утечки памяти. Методы рекомендую использовать только для отладки, в релизе, желательно от них избавиться.

Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.


© Copyright 2017. Все права защищены.
Яндекс.Метрика