+1
Для рабочей версии прошивки можно еще поставить маркеры в конце стека. Это не особо повлияет на расход памяти, зато будет сразу же видно, если маркер затерся (в том числе можно проверять это прямо из прошивки и глохнуть, выдавая ошибку «провал стека»). Подобные приемы нередко используются продвинутыми менеджерами памяти в прикладном программировании (скажем, FastMM для Delphi).
  • avatar
  • Vga
  • 23 декабря 2013, 02:27
+1
Что-то я на ночь глядя в него «въехать» не могу.
Бывает — дело житейское…
Объясню.
Функция FillStacks заполняет стек данных и возвратов заранее известным значениями('D' и 'R' соответственно). Если есть отладчик, то можно остановить МК и посмотреть содержимое стеков (меню Debug — Memory — выбрать тип памяти(SRAM) и стартовый адрес) и увидеть сколько осталось неиспорченным.
На этапе отладки в настройках проекта стеки, естественно, выделяются по-максимуму и по ходу дела можно смотреть какие значения какому стеку выделить.
Или можно написать функцию которая перидически или по запросу будет считывать данные их стеков и считать сколько байт изменили значения с 'R' или 'D' и выводить полученный размер куда-нибудь (в терминал или ещё куда). Это и будет минимально необходимый размер стека. Так сделано в scmrtos.
Если стека явно не хватает, то программа начнёт глючить (вплоть до сброса МК — если стек возвратов кончился). Опять же можно тормознуть МК и глянуть есть ли в нужных областях рама буквы R и D. Если нет, то увеличивать. Или если МК сбрасывается, то поставить breakpoint на старт программы, чтобы расмотреть содержимое до очистки SRAM-а и вызова FillStacks.
  • avatar
  • artjom
  • 23 декабря 2013, 01:49
0
Знание ассемблера облегчает жизнь.
Знание, вообще — сила…
Но причём тут ассемблер? Непонятно…
По делу — использую такую конструкцию, когда размеры стека не ясны:

#include <string.h>
#include <ioavr.h>
extern "C" {
	unsigned char *GetY(void)
	{
		asm("mov R16, R28");
		asm("mov R17, R29");
		asm("ret");
	}
};
#pragma segment="CSTACK"
#pragma segment="RSTACK"
unsigned char *dbg_ptr, *dbg_begin;
#pragma inline=forced 
inline void FillStacks()
{
	unsigned short stack_size;
	// заполнение стека данных
	stack_size=(unsigned char *)__segment_end("CSTACK")-(unsigned char *)__segment_begin("CSTACK");
	memset(__segment_begin("CSTACK"), 'D', stack_size-10);
	//// очистка стека возвратов
	dbg_ptr=reinterpret_cast<unsigned char *>(*((volatile unsigned short*)0x3D));
	dbg_ptr-=6;	
	dbg_begin=(unsigned char *)__segment_begin("RSTACK");
	// заполнение стека возвратов символом 'R'
	memset(dbg_begin, 'R', dbg_ptr-dbg_begin);
}
__noreturn int main(void)
{
    FillStacks();
    // .....
    while (1) {
         // .....
    }
}

Для GCC такое же сваять несложно…
  • avatar
  • artjom
  • 22 декабря 2013, 17:29
0
А-а! Да-да-да! Точно! Спасибо, neiver.

Я, работая в последнее время в основном с STM32 и MSP430, как-то забыл про этот AVR-кошмар — особенность регистра SP.

Действительно, регистр указателя стека у AVR состоит из двух однобайтовых бестий (ячеек). Эти ячейки точно такие же байтовые, как и другие, которые тоже располжены в адресном пространстве данных. Разница лишь в том, что регистры — это физически другая область на кремниевом чипе в отличие от обнобайтовых ячеек памяти ОЗУ. Хотя те и другие находятся в адресном пространстве данных.
Более того, даже регистр состояния — это ячейка в области памяти данных. Это даже не смешно!

А поскольку эта область (область памяти данных) имеет минимальную грануляцию — байтовую, то организовать в ней двухбайтовый регистр (в частности — регистр SP) ну никак нельзя. Отсюда и получилось, что регистр стека оказался состоящим из двух независимых половинок, которые приходится читать/писать индивидуально. Это уязвимость архитектуры.

Да. Это еще одна бомба, заложенная в архитектуру AVR изначально. Забыл я о ней.

Другая же бомба — это есть Гарвардская архитектура с разными областями памяти: память программ и память данных. Как следствие — невозможность использовать функции стандартной библиотеки Си для обработки данных, находящихся в программной памяти. Например, константные строки для сообщений об ошибках.

Возвращаюсь к разговору. Ну, хорошо. Ну, лоханулись разработчики AVR-архитектуры немного. Бывает. Заложили парочку фугасов в фундамент. Теперь пользователям этой архитектуры пришлось натянуть черно-оранжево-полосатые ленточки и оградить себя от случайного барабума. Программисты со временем смирились, привыкли к глупым ограничениям. За ленточки не заходят. Хотя, этот топик показывает, что иногда люди — да, бывает забывают или просто не ведают об опасностях. Но дело не в этом.

Дело в том, кто (какие фирмы) и как борются с косяками, заложенными в этой архитектуре. Одни создают два стека, и тем самым проблему переводят из одной плоскости в другую (суть этого топика). Другие — пытаются работать обеспечить жизнь в одностековой вселенной. При этом терпят несколько больший расход памяти (дополнительные «скобочки» CLI/SEI и танцы с копированием Y-регистра в SP). Ну и другие «прелести».

Ну, вот, теперь всё встает на свои места. И теперь понятно, откуда растут ноги.

C
  • avatar
  • zhevak
  • 09 декабря 2013, 16:19
+1
Вот так выглядит подготовка кадра стека в avr-gcc 4.6.2:
push	r28
push	r29
in	r28, 0x3d	; SP_HI
in	r29, 0x3e	; SP_LO
sbiw	r28, 0x0a	; 10
in	r0, 0x3f	; SREG
cli
out	0x3e, r29	; SP_LO
out	0x3f, r0	; SREG
out	0x3d, r28	; SP_HI

Указатель стека двухбайтный — читается-пишется двумя инструкциями в резистры r28-r29. Если между двумя инструкциями записи указателя стека произойдет прерывание, оно будет использовать указатель стека записанный на половину. Таким образом может затереть что-то нужное в стеке. По этому при записи указателя стека прерывания всегда запрещаются.
  • avatar
  • neiver
  • 09 декабря 2013, 14:56
0
Спасибо, но я не до конца понял про опасность «неатомарности сдвига стека».

На сколько я понимаю, как только возникает прерывание и процессор начинает подготовку его обслуживанию (запрещает прерывания, уходит по вектору и т.д.), то с точки зрения прерванной программы — тут вроде бы не должно быть подводных камней — программа (если у нее нет часов абсолютного времени, конечно) вообще не должна заметить, что ей «стукнули по башке» и она пролежала в бессознании 10 миллисекунд. Ведь по возврату из прерывания все регистры и флаги должны быть восстановлены. (Иначе это нужно считать кривой реализацией обработчика прерывания.)

Я не вижу опасности при работе со стеком. Можете чуть-чуть подробнее сказать или привести пример.

Спасибо.
  • avatar
  • zhevak
  • 09 декабря 2013, 12:27
0
Отдельный стек данных в IAR нужен как бы для оптимизации. Указатель стека в AVR находится в области портов ввода-вывода, и чтоб получить кадр стека нужно его атомарно сдвинуть, а потом атомарно вернуть на место. Атомарно — потому, что в любой момонт может возникнуть прерывание и изменить указатель стека. А когда стек данных отделный с указателем в регистровой паре, то его можно атомарно изменять одной инструкцией. Так что разработчики IAR посчитали, что эффективность использования стека перевешивает неудобство от двух раздельных областей стека.
  • avatar
  • neiver
  • 09 декабря 2013, 11:26
0
Каждый про своё, а вшивый про баню… (это я прежде всего про себя)

Много лет назад, когда я прочно (казалось бы!) сидел в Виндовс и компилил свои мега-проги в крякнутом IAR-e я тоже иногда сталкивался с такими же ошибками. Так что тема мне известна. Но я о другом.

Я не знаю почему, но в Виндовых кросс-компиляторах для AVR, отличие от родных комповых компиляторов (для Виндовс), при генерации кода используется двух-стековая система — один стек содержит адреса возвратов, другой — используется для данных. В традиционных программах для компов, и в кросс-компиляторах для других микроконтроллеров (MSP430, ARM) — используется единый стек на всё-про-всё.

Мы знаем, что стек «растет» от старших адресов к младшим — навстречу к данным. Когда эта встреча происходит — это очень неприятное известие. Это значит, вы исчерпали ВСЮ оперативную память и теперь у вас два пути — либо менять процессор на более мощный, либо заниматься оптимизацией программы вручную. Как говориться — то ещё счастье!

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

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

Приходится решать головоломку: какая область память быстрее исчерпается — та, где стек возвратов, или та, где стек данных? Куда что двигать?

Честно говоря, это какой-то дурдом! Хотя может быть я чего-то и не понял в этом архитектурном ансамбле.

Когда же я перешел в Линух и начал знакомиться с gcc (в частности — с arv-gcc), то меня для меня был реально праздник, когда я увидел, что gcc использует один стек для хранения данных и адресов возврата. Это произвело на меня настолько мощное положительное впечатление, что я даже сейчас не могу удержаться и не сообщить об этом.

И на самом деле, когда система создает единый стек, то танцы с бубном сами собой становятся как-то ни к чему. Возможно, это надо понять не умом, а прочувствовать на практике.
  • avatar
  • zhevak
  • 08 декабря 2013, 23:59