«С опытом приходит стандартный научный подход к расчету правильного размера стека: возьмите случайное число и надейтесь на лучшее».
— Джек Ганссл, «Искусство проектирования встраиваемых систем» Привет, Хабр! Как ни странно, в подавляющем большинстве виденных мной «учебников для начинающих» по STM32 в частности и микроконтроллерам, как правило, вообще ничего нет о таких вещах, как распределение памяти, размещение стека и, самое главное, избегание памяти.
перелив - в результате чего одна область трется о другую и все рушится, обычно с феерическими эффектами.
Частично это объясняется простотой учебных проектов, выполняемых на отладочных платах с относительно толстыми микроконтроллерами, на которых довольно сложно столкнуться с нехваткой памяти при мигании светодиодом - однако в последнее время даже среди начинающих любителей я все чаще видел упоминания, например, о контроллерах типа STM32F030F4P6, просты в установке, стоят копейки, но имеют всего несколько килобайт памяти.
Такие контроллеры позволяют делать вполне серьёзные вещи (ну вот у нас есть, например, такие вполне хорошее измерение сделаны на STM32F042K6T6 с 6 КБ ОЗУ, из которых свободными остается чуть больше 100 байт), но при обращении с памятью требуется определенная осторожность при работе с ними.
Именно об этой аккуратности я и хочу поговорить.
Статья будет короткой, профессионалы не узнают ничего нового – а вот новичкам эти знания иметь крайне рекомендуется.
В типовом проекте на микроконтроллере на базе ядра Cortex-M оперативная память условно разделена на четыре раздела:
data — данные, инициализированные определенным значением bss — данные инициализируются нулем heap — куча (динамическая область, из которой память выделяется явно с помощью malloc) stack — стек (динамическая область, из которой компилятор неявно выделяет память) Изредка также можно встретить область noinit (неинициализированные переменные — они удобны тем, что сохраняют свое значение между перезагрузками), и еще реже — какие-то другие области, выделенные под конкретные задачи.В физической памяти они располагаются довольно специфическим образом — дело в том, что стек в микроконтроллерах на ядрах ARM растет сверху вниз.
Поэтому он расположен отдельно от остальных блоков памяти, в конце ОЗУ:
По умолчанию его адрес обычно равен самому последнему адресу ОЗУ, а оттуда он снижается по мере роста — и из этого вырастает одна крайне неприятная особенность стека: он может дойти до bss и перезаписать его вершину, а вы этого не сделаете.
знать об этом любым очевидным способом.
Статические и динамические области памяти.
Вся память делится на две категории - статически выделенную, т.е.
память, общий объем которой очевиден из текста программы и не зависит от порядка ее выполнения, и динамически выделенную, требуемый объем которой зависит от прогресс программы.
Последний включает в себя кучу (из которой мы берем кусочки с помощью malloc и возвращаем с помощью free) и стек, который растет и сжимается «сам по себе».
Вообще говоря, используйте malloc на микроконтроллерах.
настоятельно не рекомендуется если только вы не знаете абсолютно точно, что делаете.
Основная проблема, которую он создает, — это фрагментация памяти — если выделить 10 кусков по 10 байт, а затем освободить каждый второй, свободных 50 байт вы не получите.
Вы получите 5 бесплатных кусков по 10 байт. Кроме того, на этапе компиляции программы компилятор не сможет автоматически определить, сколько памяти потребуется вашему malloc (особенно с учетом фрагментации, которая зависит не просто от размера запрашиваемых кусков, а от последовательности об их выделении и освобождении), и поэтому не сможет предупредить вас, если в конечном итоге памяти не хватит. Есть способы обойти эту проблему - специальные реализации malloc, работающие внутри статически выделенной области, а не всей оперативной памяти, осторожное использование malloc с учетом возможной фрагментации на уровне логики программы и т.д. - но в целом Malloc лучше не трогать .
Все области памяти с границами и адресами прописываются в файле с расширением LD, на который ориентируется компоновщик при сборке проекта.
Статически выделенная память
Итак, из статически выделенной памяти у нас есть две области — bss и data, которые отличаются лишь формально.При инициализации системы блок данных копируется из flash, где сохраняются необходимые для него значения инициализации, блок bss просто заполняется нулями (по крайней мере заполнять его нулями считается хорошим тоном).
И то, и другое - копирование из флешки и заполнение нулями - выполняются в коде программы.
явно , но не в вашем main(), а в отдельном файле, который выполняется первым, пишется один раз и просто перетаскивается из проекта в проект. Однако нас сейчас интересует не это — а то, как мы поймем, помещаются ли вообще наши данные в оперативную память нашего контроллера.
Это можно узнать очень просто — с помощью утилиты Arm-none-eabi-size с единственным параметром — скомпилированный ELF-файл нашей программы (часто ее вызов вставляют в конец Makefile, потому что это удобно):
Здесь текст — это объем данных программы, хранящихся во флэш-памяти, а bss и data — это наши статически выделенные области в оперативной памяти.
Последние два столбца нас не касаются — это сумма первых трех, практического смысла она не имеет. Итак, статически в оперативной памяти нам нужны bss + байты данных, в данном случае — 5324 байта.
В контроллере 6144 байт оперативной памяти, malloc не используем, осталось 820 байт. Этого должно быть достаточно для нашего стека.
Но достаточно ли этого? Потому что если нет, то наш стек вырастет до наших собственных данных, а потом сначала данные перезапишет, потом данные перезапишут их, а потом все рухнет. Более того, между первым и вторым пунктом программа может продолжать работать, не осознавая, что обрабатываемые ею данные — мусор.
В худшем случае это будут данные, которые вы записали, когда со стеком все было в порядке, а сейчас вы их только читаете - например, параметры калибровки какого-то датчика - и тогда у вас нет очевидного способа понять, что с ними все плохо, когда в этом случае программа продолжит выполняться как ни в чем не бывало, выдавая вам на выходе мусор.
Динамически выделяемая память
И тут начинается самое интересное – если свести сказку к одной фразе, то Заранее определить размер стека практически невозможно..
Чисто в теории , вы можете попросить компилятор предоставить вам размер стека, используемый каждой отдельной функцией, затем попросить его предоставить вам дерево выполнения вашей программы, и для каждой ветки в нем вычислить сумму стеков всех функций, присутствующих в этом дереве .
Одно это отнимет у вас немало времени для любой более-менее сложной программы.
Тогда вы помните, что в любой момент может произойти прерывание, обработчику которого тоже нужна память.
Потом — что могут случиться два-три вложенных прерывания, обработчики которых.
В общем, вы поняли.
Попытка вычислить стек для конкретной программы — занятие увлекательное и в целом полезное, но делать это придется нечасто.
Поэтому на практике используется один прием, позволяющий хоть как-то понять, все ли в нашей жизни хорошо, – так называемая «картина памяти».
Чем удобен этот метод, так это тем, что он никак не зависит от используемых вами средств отладки, а если в системе есть хоть какие-то средства вывода информации, то позволяет обойтись вообще без средств отладки.
Суть его в том, что мы заполняем весь массив от конца bss до начала стека где-то на самом раннем этапе выполнения программы, когда стек еще очень мал, одним и тем же значением.
Далее, проверив, по какому адресу уже исчезло это значение, мы понимаем, куда делся стек.
Поскольку после того, как раскраска будет стерта, она не восстановится сама по себе, проверку можно производить время от времени - она покажет достигнутый максимальный размер стека.
Определимся с цветом краски – конкретное значение не имеет значения, внизу я просто постучал двумя пальцами левой руки.
Главное не выбирать 0 и FF:
В самом-самом начале программы, прямо в файле запуска, заполним всю свободную память этой краской:#define STACK_CANARY_WORD (0xCACACACAUL)
volatile unsigned *top, *start;
__asm__ volatile ("mov %[top], sp" : [top] "=r" (top) : : );
start = &_ebss;
while (start < top) {
*(start++) = STACK_CANARY_WORD;
}
Что мы здесь сделали? Ассемблерная вставка присвоила переменной top значение, равное текущему адресу стека — чтобы случайно не перезаписать его; в стартовой переменной — адрес конца bss-блока (переменная, в которой он хранится, — Я заметил в скрипте компоновщика *.
ld
— в данном случае это из библиотеки libopencm3).Далее просто заполняем все от конца bss до начала стека одним и тем же значением.
После этого мы можем сделать это в любое время: unsigned check_stack_size(void) {
/* top of data section */
unsigned *addr = &_ebss;
/* look for the canary word till the end of RAM */
while ((addr < &_stack) && (*addr == STACK_CANARY_WORD)) {
addr++;
}
return ((unsigned)&_stack - (unsigned)addr);
}
Здесь переменная _ebss нам уже знакома, а переменная _stack — из тот же скрипт компоновщика , в нем имеется в виду верхний адрес стека, то есть в данном случае как раз конец ОЗУ.
Эта функция вернет максимальный записанный размер стека в байтах.
Дальнейшая логика довольно проста — где-то в теле программы мы периодически вызываем check_stack_size() и выводим ее вывод в консоль, на экран или туда, где нам удобно его отобразить, и запускаем устройство в работоспособную работу.
на период, который мы считаем достаточно длительным.
Периодически смотрите на размер стека.
В этом случае различными хаотичными действиями с устройством можно довести его до 712 байт — то есть из изначально имеющихся 6 КБ ОЗУ у нас остается запас целых 108 байт.
Предупреждение
Экспериментальный метод определения размера стопки прост, эффективен, но не на 100% надежен.Всегда может возникнуть ситуация, когда очень редкий стечение обстоятельств, наблюдаемое, например, раз в год, приведет к незапланированному увеличению этого размера.
Однако в общем случае и при грамотно написанной прошивке можно предположить, что маловероятно, что произойдет что-либо, превышающее фиксированный размер более чем на 10-20%, поэтому мы спокойны с нашими 108 байтами резерва с высоким степень уверенности.
В большинстве случаев такое квазипрофилирование, которое легко и просто выполняется практически на любой системе и независимо от используемых средств разработки, позволяет с высокой достоверностью определить эффективность использования памяти и поймать проблему со стеком на ранних стадиях.
, особенно при работе на младших контроллерах с оперативной памятью размером килобайта P.S. В многозадачных системах ОСРВ в большинстве случаев имеется множество стеков — помимо основного стека MSP, растущего от верхнего края оперативной памяти вниз, существуют отдельные стеки процессов PSP. Их размер четко задается программистом, что не мешает процессу выходить за их границы — поэтому и методы управления у них одинаковые.
Теги: #Производство и разработка электроники #микроконтроллеры #stm32 #Электроника для начинающих #cortex #cortex-m3
-
Знакомство С Corel Painter Ix
19 Oct, 24 -
«Джон Макафи Похож На Рыбку Дори».
19 Oct, 24 -
Турнир
19 Oct, 24 -
Домашний Кинотеатр Бедного Компьютерщика
19 Oct, 24 -
Рынок Технологий Превысил Миллиард
19 Oct, 24