Первый старт с STM32-Discovery [Часть 3]

6 Января 2011 К комментариям

Первый проект.

Начитавшись форумов и глянув пару примеров, можно приступать к первому проекту.

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

Введение

В состав GCC входят компилятор и компоновщик (линкер), задача компилятора прочитать наш программный код и сформировать из них набор инструкций, которые компоновщик соберет в конечный выходной файл годный для заливки в МК.

Исходя из того нам нужно каким-то образом объяснить компоновщику как устроен наш МК, т.е. написать некий сценарий, т.е. указать сколько у нас флеш и ОЗУ, по каким адресам расположены, а так же указать куда что «положить».

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

Сценарий

Необходимый минимум информации который должен содержать сценарий:

  • расположение и размер флеш и ОЗУ
  • расположение кода, данных
  • размер и расположение стека
  • имя точки входа Первым делом указываем расположение и размер флеш и ОЗУ:
MEMORY
{
  flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
  ram (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}

Рассмотрим подробнее указание флеш-памяти:

flash имя области памяти
(rx) атрибут означающий что область памяти доступна только на чтение (r), а так же можно запускать код на исполнение (x)
ORIGIN = 0x08000000 начальный адрес области
LENGTH = 128K размер области, при компоновке если будет превышен размер компоновщик сообщит об ошибке

Далее необходимо описать карту распределения памяти.

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

Вначале указываем расположение кода программы, таблицу векторов прерываний и данных (константы):

.text :
{
  . = ALIGN(4);
  KEEP(*(.interrupt_vector))
  *(.text)
  *(.text*)
  *(.rodata)
  *(.rodata*)
  . = ALIGN(4);
} > flash

Рассмотрим подробнее:

.text {} > flash оператор размещает все его элементы во флеш память с начального адреса
. = ALIGN(4) оператор точка определяет текущий адрес, оператор ALIGN(4) – предписываем компоновщику выравнивать адреса по 4 байта
KEEP(*(.interrupt_vector)) в самом начале флеш-памяти необходимо разместить таблицу прерываний (согласно документации для данного МК), оператор KEEP() говорит компоновщику что эту секцию нужно пропустить, а то ещё с оптимизирует ;)
*(.text)
*(.text*)
после таблицы прерываний как правило располагается программный код
*(.rodata)
*(.rodata*)
константы как правило располагаются после программного кода (rodata – сокращение от read-only-data)

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

_data_flash = .;

Раннее я писал что оператор «точка» определяет текущий адрес.

И сразу опишем секцию:

.data : AT ( _data_flash )
{
  . = ALIGN(4);
  _data_begin = .;
  *(.data)
  *(.data*)
  . = ALIGN(4);
  _data_end = .;
} > ram

Так же для того что бы при старте знать что куда копировать «запоминаем» начальный и конечный адрес области (_data_begin, _data_end).

Аналогично задаем секцию для переменных начальные значение которых равны нулю (обнуляем их сами при старте):

.bss :
{
  _bss_begin = .;
  __bss_start__ = _bss_begin;
  *(.bss)
  *(.bss*)
  *(COMMON)
  . = ALIGN(4);
  _bss_end = .;
  __bss_end__ = _bss_end;
} > ram

Ну и последним этапом необходимо указать размер и расположение стека:

_stack_size = 200;
_stack_end = 0x20000000 + 8K;
_stack_begin = _stack_end - _stack_size;
. = _stack_begin;
._stack :
{
  . = . + _stack_size;
} > ram

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

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

ENTRY(handler_reset)

Что бы лучше понять распределение памяти я нарисовал вот такую схемку:

распределение памяти

Инициализация

Осталось выполнить начальную инициализацию МК. Почему эти действия не заложены на автомате как например в AVR-GCC, я думаю из-за разных реализаций микроконтроллеров с ARM архитектурой.

Основные действия при инициализации:

  • инициализация переменных
  • указание стека
  • передача управления основной программе

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

Инициализируем переменные начальными значениями и выполняем переход в основную программу:

void handler_reset(void)
{
  unsigned long *source;
  unsigned long *destination;

  // копируем данные из флеки в память
  source = amp;_data_flash;
  for(destination = amp;_data_begin; destination < amp;_data_end; )
  {
    *(destination++) = *(source++);
  }

  // обнуляем
  for(destination = amp;_bss_begin; destination < amp;_bss_end; )
  {
    *(destination++) = 0;
  }

  // переход в основную программу
  main();
}

Со стеком интереснее, достаточно просто записать в начальный адрес флеш-памяти указатель на конец стека, а так как сразу после этого идет таблица прерываний, то удобнее описать все за одно “движение”:

__attribute__ ((section(".interrupt_vector")))
void (* const table_interrupt_vector[])(void) =
{
		(void *) &_stack_end, // 0 - stack
		handler_reset, // 1
		handler_default, // 2
		handler_default, // 3
		handler_default, // 4
		handler_default, // 5
		handler_default, // 6
		0, // 7
		0, // 8
		0, // 9
		0, // 10
		handler_default, // 11
		handler_default, // 12
		0, // 13
		handler_default, // 14
		handler_default, // 15
		// периферия
		handler_default, // 0
		handler_default, // 1
		handler_default, // 2
		handler_default, // 3
		handler_default, // 4
		handler_default, // 5
		handler_default, // 6
		handler_default, // 7
		handler_default, // 8
		handler_default, // 9
		handler_default, // 10
		handler_default, // 11
		handler_default, // 12
		handler_default, // 13
		handler_default, // 14
		handler_default, // 15
		handler_default, // 16
		handler_default, // 17
		handler_default, // 18
		handler_default, // 19
		handler_default, // 20
		handler_default, // 21
		handler_default, // 22
		handler_default, // 23
		handler_default, // 24
		handler_default, // 25
		handler_default, // 26
		handler_default, // 27
		handler_default, // 28
		handler_default, // 29
		handler_default, // 30
		handler_default, // 31
		handler_default, // 32
		handler_default, // 33
		handler_default, // 34
		handler_default, // 35
		handler_default, // 36
		handler_default, // 37
		handler_default, // 38
		handler_default, // 39
		handler_default, // 40
		handler_default, // 41
		handler_default, // 42
		handler_default, // 43
		handler_default, // 44
		handler_default, // 45
		handler_default, // 46
		handler_default, // 47
		handler_default, // 48
		handler_default, // 49
		handler_default, // 50
		handler_default, // 51
		handler_default, // 52
		handler_default, // 53
		handler_default, // 54
		handler_default, // 55
		handler_default, // 56
		handler_default, // 57
		handler_default, // 58
		handler_default, // 59
		handler_default // 60
};

Атрибут ((section(“.interrupt_vector”))) предписываем компоновщику расположить данную таблицу в секции «interrupt_vector».

С векторами сильно не мудрил взял документацию на МК, посмотрел таблицу прерываний и на место каждого прерывания воткнул заглушку:

void handler_default(void)
{
	while (1)
	{

	}
}

продолжение следует…



comments powered by Disqus