Первый старт с STM32-Discovery [Часть 3]
Первый проект.
Начитавшись форумов и глянув пару примеров, можно приступать к первому проекту.
При использовании платных компиляторов большую часть работы по созданию проекта за нас уже как правило сделали, т.е. нам достаточно указать тип МК, нажать ОК и мы получим заготовку проекта с файлами начальной инициализации и прочими сопутствующими данными. Конечно есть заготовки и для 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