Что такое стек микроконтроллера
Перейти к содержимому

Что такое стек микроконтроллера

  • автор:

Стек

Расположение стека в памяти данных

Стек представляет собой область памяти, которую ЦПУ использует для сохранения и восстановления адресов возврата из подпрограмм.
Практически у всех микроконтроллеров AVR стек размещается в SRAM. Для адресации текущего элемента (вершины стека) используется указатель стека SP (Stack Pointer). Это однобайтовый РВВ SPL у моделей с объемом памяти данных до 256 б, или двухбайтовый SPH:SPL (SPH – старший байт, SPL – младший байт). Когда микропроцессор встречает одну из инструкций вызовов rcall/call/ecall/icall/eicall, то адрес следующего за ними слова в памяти программ аппаратно копируется в стек. В момент выхода из подпрограммы по команде ret адрес возврата восстанавливается из стека в программный счетчик. В моделях с объемом памяти программ 128 и 256 к/слов для сохранения PC в стеке потребуется 3 байта, для всех остальных – 2 байта. При сохранении каждого байта содержимое SP уменьшается на единицу, а при восстановлении, соответственно увеличивается.
Рис.9 Расположение стека в памяти данных Программист должен самостоятельно определить местоположение стека в самом начале программы. С точки зрения максимальной его глубины, вершину стека нужно поместить в самом конце SRAM, как это показано на рис.9:

.include "m8def.inc" ldi temp,low(RAMEND) ;устанавливаем SP = RAMEND out SPL,temp ;для ATmega8 SP = 0x045F ldi temp,high(RAMEND) out SPH,temp

Константа RAMEND из стандартного заголовочного файла m8def.inc имеет значение адреса последней ячейки SRAM. В диапазоне адресов SRAM между РВВ и текущим положением SP размещаются переменные прикладной программы. Поэтому очень важно предварительно оценить максимальный размер стека (глубину стека). Может случиться так, что вершина стека поднимется слишком высоко и начнет “затирать” пользовательские данные, а это одна из самых сложно-выявляемых ошибок! Стек AVR, помимо сохранения адресов возврата, имеет еще одно очень важное предназначение. Он позволяет сохранять любые данные специально предназначенными для этого командами push Rr (загрузка в стек) и pop Rd (выгрузка из стека). Каждый раз при выполнении push Rr содержимое Rr копируется в стек, после чего SP уменьшается на единицу. При выполнении pop Rr содержимое ячейки стека, на которую указывает SP, восстанавливается в Rr, а само значение SP инкрементируется. Стек подобного рода имеет организацию Last In First Out (Последний Вошел Первый Вышел): регистр, сохраненный последней командой push, будет восстановлен первой командой pop:

; SP Уровень стека после команды push R16 ;сохраняем R16 0x045F R16 ? ? push R17 ;сохраняем R17 0x045E R16 R17 ? push R18 ;сохраняем R18 0x045D R16 R17 R18 ̣̣̣̣̣̣̣̣ pop R18 ;восстанавливаем R18 0x045D R16 R17 ? pop R17 ;восстанавливаем R17 0x045E R16 ? ? pop R16 ;восстанавливаем R16 0x045F ? ? ?

Через стек очень просто можно обменять содержимое регистров местами:

; Обмен R16 R17 SP Уровень стека после команды push R16 ;сохраняем R16 0x045F R16 ? push R17 ;сохраняем R17 0x045E R16 R17 pop R16 ;восстанавливаем R16 0x045E R16 ? pop R17 ;восстанавливаем R17 0x045F ? ?

Пример работы стека

Рис.10 Пример работы стека На рис.10 приведен небольшой фрагмент кода, в котором пошагово рассмотрен процесс изменения стека при входе и выходе из подпрограммы toggle и сохранении и восстановлении регистра R17. Это типичный пример, где могут понадобиться инструкции push/pop. Подпрограмма toggle использует РОН R17 в своих нуждах, но этот- же регистр может использоваться и в ходе основной программы. Поэтому, во избежание повреждения данных, R17 перед модификацией загружается в стек и восстанавливается из него перед командой ret. У некоторых устаревших моделей ATtiny отсутствует SRAM. Поэтому стек таких микропроцессоров имеет совсем другое устройство. Он реализован аппаратно в виде недоступной для программиста области памяти. Глубина стека – всего три уровня вложения, что, соответственно, позволяет вызвать не более трех подпрограмм. Аппаратный стек не предназначен для сохранения данных. Перейти к следующей части: Прерывания

Теги:

Котов Игорь Юрьевич Опубликована: 2012 г. 0 0

Вознаградить Я собрал 0 0

Профилирование памяти на STM32 и других микроконтроллерах: статический анализ размера стека

В прошлой статье и я сам упоминал, и в комментариях спрашивали — ок, хорошо, методом научного тыка мы подобрали размер стека, вроде ничего не падает, а можно как-то надёжнее оценить, чему он равен и кто вообще столько сожрал?

Отвечаем коротко: да, но нет.

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

Ответ немного длиннее — под катом.

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

При выполнении функции компилятор добавляет в стеке место под нужные ей переменные, при завершении — освобождает это место обратно. Казалось бы, всё просто, но — и это очень жирное но — у нас есть несколько проблем:

  1. функции вызывают внутри другие функции, которым тоже нужен стек
  2. иногда функции вызывают другие функции не прямым их упоминанием, а по указателю на функцию
  3. в принципе возможен — хотя его стоит избегать всеми средствами — рекурсивный вызов функций, когда A зовёт B, B зовёт C, а C внутри себя опять зовёт A
  4. в любой момент может случиться прерывание, обработчик которого — та же функция, желающая свой кусок стека
  5. если у вас есть иерархия прерываний, внутри прерывания может случиться другое прерывание!

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

  • функция A, съевшая 100 байт в стеке, зовёт функцию B, которой нужно 50 байт
  • на момент выполнения B сама A, очевидно, ещё не завершилась, поэтому её 100 байт не освобождены, поэтому у нас уже 150 байт в стеке
  • функция B зовёт функцию C, причём делает это по указателю, который в зависимости от логики программы может указывать на полдесятка разных функций, потребляющих от 5 до 50 байт стека
  • во время выполнения C случается прерывание с тяжёлым обработчиком, работающим относительно долго и потребляющим 20 байт стека
  • во время обработки прерывания случается другое прерывание, с более высоким приоритетом, обработчик которого хочет 10 байт стека

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

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

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

Тем не менее, статическая оценка размера стека может быть очень полезна при оптимизации ПО — хотя бы с банальной целью понять, кто сколько жрёт, и не многовато ли.

Для этого в тулчейне GNU/gcc есть два крайне полезных средства:

  • флаг -fstack-usage
  • утилита cflow
target.c:159:13:save_settings 8 static target.c:172:13:disable_power 8 static target.c:291:13:adc_measure_vdda 32 static target.c:255:13:adc_measure_current 24 static target.c:76:6:cpu_setup 0 static target.c:81:6:clock_setup 8 static target.c:404:6:dma1_channel1_isr 24 static target.c:434:6:adc_comp_isr 40 static target.c:767:6:systick_activity 56 static target.c:1045:6:user_activity 104 static target.c:1215:6:gpio_setup 24 static target.c:1323:6:target_console_init 8 static target.c:1332:6:led_bit 8 static target.c:1362:6:led_num 8 static 

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

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

Для понимания полного потребления нам необходимо построить дерево вызовов и суммировать стеки всех входящих в каждую его ветку функций. Сделать это можно, например, утилитой GNU cflow, натравив её на один или несколько файлов.

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

olegart@oleg-npc /mnt/c/Users/oleg/Documents/Git/dap42 (umdk-emb) $ cflow src/stm32f042/umdk-emb/target.c adc_comp_isr() : TIM_CR1() ADC_DR() ADC_ISR() DMA_CCR() GPIO_BSRR() GPIO_BRR() ADC_TR1() ADC_TR1_HT_VAL() ADC_TR1_LT_VAL() TIM_CNT() DMA_CNDTR() DIV_ROUND_CLOSEST() NVIC_ICPR() clock_setup() : rcc_clock_setup_in_hsi48_out_48mhz() crs_autotrim_usb_enable() rcc_set_usbclk_source() dma1_channel1_isr() : DIV_ROUND_CLOSEST() gpio_setup() : rcc_periph_clock_enable() button_setup() : gpio_mode_setup() gpio_set_output_options() gpio_mode_setup() gpio_set() gpio_clear() rcc_peripheral_enable_clock() tim2_setup() : rcc_periph_clock_enable() rcc_periph_reset_pulse() timer_set_mode() timer_set_period() timer_set_prescaler() timer_set_clock_division() timer_set_master_mode() adc_setup_common() : rcc_periph_clock_enable() gpio_mode_setup() adc_set_clk_source() adc_calibrate() adc_set_operation_mode() adc_disable_discontinuous_mode() adc_enable_external_trigger_regular() ADC_CFGR1_EXTSEL_VAL() adc_set_right_aligned() adc_disable_temperature_sensor() adc_disable_dma() adc_set_resolution() adc_disable_eoc_interrupt() nvic_set_priority() nvic_enable_irq() dma_channel_reset() dma_set_priority() dma_set_memory_size() dma_set_peripheral_size() dma_enable_memory_increment_mode() dma_disable_peripheral_increment_mode() dma_enable_transfer_complete_interrupt() dma_enable_half_transfer_interrupt() dma_set_read_from_peripheral() dma_set_peripheral_address() dma_set_memory_address() dma_enable_circular_mode() ADC_CFGR1() memcpy() console_reconfigure() tic33m_init() strlen() tic33m_display_string()

И это даже не половина дерева.

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

И при этом мы ещё не учтём вызовы функций по указателям и прерывания, в т.ч. вложенные (а конкретно в этом коде они могут быть вложенными).

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

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

  • Программирование микроконтроллеров
  • Производство и разработка электроники
  • Электроника для начинающих

3.8. Стек и указатель стека

Стек необходим для запоминания адресов возврата из процедур обработки прерывания и подпрограмм. Его также можно использовать для временного хранения данных. Регистр указателя стека (SP) постоянно указывает на вершину стека. Он реализован в виде двух 8-битных регистров, доступных в пространстве памяти ввода-вывода. Данные помещаются в стек и извлекаются из него посредством инструкций PUSH и POP. Увеличение стека происходит в направлении от старших ячеек памяти к младшим. Таким образом, при помещении данных в стек содержимое регистра SP уменьшается, а при извлечении данных — увеличивается. После сброса, SP автоматически инициализируется значением, которое равно максимальному адресу внутреннего SRAM. При необходимости изменения SP нужно учитывать, что помещаемый в него адрес должен лежать выше 0x2000, а само изменение нужно выполнить перед вызовом каких-либо подпрограмм или разрешением прерываний.

При вызове подпрограмм или при переходе по вектору прерываний адрес возврата автоматически помещается в стек. Адрес возврата может быть представлен двумя или тремя байтами, что зависит от размера памяти микроконтроллера. У МК с памятью программ 128 кбайт и менее адрес возврата двухбайтный, поэтому, указатель стека декрементируется/инкрементируется на два. У тех же микроконтроллеров, которые оснащены памятью программ размером более 128 кбайт, адрес возврата трехбайтный, а декрементирование/инкрементирование SP выполняется на три. Адрес возврата извлекается из стека при выходе из прерывания по инструкции RETI, а из подпрограммы по инструкции RET.

Если инструкцией PUSH в стек помещаются данные, то SP декрементируется на единицу. Аналогичным образом, при извлечении данных из стека инструкцией POP содержимое SP инкрементируется на единицу.

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

Указатель стека AVR

В Контакте Ютуб Почта

Стек в основном используется для хранения временных данных, локальных переменных и обратных адресов после прерываний и вызовов подпрограмм. Регистр Указателя Стека (Stack Pointer Register) всегда указывает на верхнюю часть стека.

Обратите внимание, что Стек выполнен как растущий из более высоких расположений памяти в более низкие. То есть визуально Стек как бы перевёрнут (см. рис.). Это означает, что команда PUSH уменьшает указатель стека.

Стек AVR

Указатель Cтека (Stack Pointer) указывает на область данных SRAM Stack (статическая память, выделенная для стека), в которой расположены стеки подпрограмм и прерываний.

Указатель Стека — это область стека в данных SRAM, которая определяется автоматически. Указатель стека содержит последний адрес в SRAM, записанный туда перед выключением питания.

Указатель стека должен быть установлен выше 0x60.

Указатель стека уменьшается на единицу, когда данные помещаются в стек с помощью инструкции PUSH, и на два, когда возвращаемый адрес помещается в стек с помощью команды PUSH подпрограммой вызова или прерывания.

Указатель стека увеличивается на единицу, когда данные извлекаются из стека с помощью инструкции POP, и увеличивается на два, когда данные извлекаются из стека при возвращении из подпрограммы с помощью инструкции RET, или при возвращении из прерывания с помощью инструкции RETI.

Указатель стека AVR выполнен в виде двух 8-разрядных регистров в пространстве ввода-вывода. Это регистры SPL (Stack Pointer Low) и SPH (Stack Pointer High).

Количество фактически используемых разрядов зависит от исполнения. Обратите внимание, что пространство данных в некоторых исполнениях архитектуры AVR настолько мало, что требуется только младший регистр указателя стека (Stack Pointer Low — SPL). В этом случае регистр SPH (старший регистр указателя стека) отсутствует.

Stack Pointer Low - SPL - младший регистр указателя стека

На рисунке показана структура регистра SPL, который расположен по адресу 0х3D. Все биты регистра доступны как для чтения, так и для записи. Начальные значения битов (Initial Value) также показаны на рисунке.

В регистры SPL и SPH необходимо загружать адрес вершины стека (адрес верхней границы ОЗУ).

Адрес вершины стека хранится в константе RAMEND, которая определена в подключаемом файле для данной модели микроконтроллера. Например, для микроконтроллера ATtiny13A в начале файла исходного кода программы надо подключить файл tn13def.inc.

Младший байт константы RAMEND надо загрузить в регистр SPL, а старший — в регистр SPH. Это установит указатель стека на конец ОЗУ. Сделать это можно, например, так:

.def temp = r16 ldi temp, LOW(RAMEND) ;Указатель стека указывает out SPL, temp ;на последний адрес ОЗУ ldi temp, HIGH(RAMEND) ;out SPH, temp ;. Не для всех моделей МК

Обратите внимание на последнюю строку. Поскольку регистр SPH используется не во всех моделях серии AVR, то в некоторых случаях (например, для ATtiny13A) это приведёт к ошибке компиляции (в моделях с объёмом ОЗУ до 128 байт — ATtiny13A имеет память 64 байта).

Этот код надо разместить до вызова каких-либо подпрограмм.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *