Первый старт с STM32-Discovery [Часть 3]
Первый проект.
Начитавшись форумов и глянув пару примеров, можно приступать к первому проекту.
При использовании платных компиляторов большую часть работы по созданию проекта за нас уже как правило сделали, т.е. нам достаточно указать тип МК, нажать ОК и мы получим заготовку проекта с файлами начальной инициализации и прочими сопутствующими данными. Конечно есть заготовки и для GCC, но так как мне интересен весь процесс я попробую разобраться что к чему.
Введение
В состав GCC входят компилятор и компоновщик (линкер), задача компилятора прочитать наш программный код и сформировать из них набор инструкций, которые компоновщик соберет в конечный выходной файл годный для заливки в МК.
Исходя из того нам нужно каким-то образом объяснить компоновщику как устроен наш МК, т.е. написать некий сценарий, т.е. указать сколько у нас флеш и ОЗУ, по каким адресам расположены, а так же указать куда что «положить».
Кроме того нужно выполнить начальную инициализацию МК. Почему эти действия не заложены на автомате как например в AVR-GCC, я думаю из-за разных реализаций микроконтроллеров с ARM архитектурой.
Сценарий
Необходимый минимум информации который должен содержать сценарий:
- расположение и размер флеш и ОЗУ
- расположение кода, данных
- размер и расположение стека
- имя точки входа Первым делом указываем расположение и размер флеш и ОЗУ:
Рассмотрим подробнее указание флеш-памяти:
flash | имя области памяти |
(rx) | атрибут означающий что область памяти доступна только на чтение (r), а так же можно запускать код на исполнение (x) |
ORIGIN = 0x08000000 | начальный адрес области |
LENGTH = 128K | размер области, при компоновке если будет превышен размер компоновщик сообщит об ошибке |
Далее необходимо описать карту распределения памяти.
Скажу сразу что вся область памяти для простоты разбивается на секции (программный код, константы, вектора прерываний, область загрузчика и т. д.), по умолчанию расположение секций в памяти идет согласно их порядку при описании, можно менять порядок (позже при указании стека мы этим воспользуемся).
Вначале указываем расположение кода программы, таблицу векторов прерываний и данных (константы):
Рассмотрим подробнее:
.text {} > flash | оператор размещает все его элементы во флеш память с начального адреса |
. = ALIGN(4) | оператор точка определяет текущий адрес, оператор ALIGN(4) – предписываем компоновщику выравнивать адреса по 4 байта |
KEEP(*(.interrupt_vector)) | в самом начале флеш-памяти необходимо разместить таблицу прерываний (согласно документации для данного МК), оператор KEEP() говорит компоновщику что эту секцию нужно пропустить, а то ещё с оптимизирует ;) |
*(.text)
*(.text*) |
после таблицы прерываний как правило располагается программный код |
*(.rodata)
*(.rodata*) |
константы как правило располагаются после программного кода (rodata – сокращение от read-only-data) |
Далее небольшой нюанс, так как у нас в программе возможно будут использоваться переменные которые могут иметь начальные значение, то не плохо бы хранить эти значения во флеш, а так же вынести в отдельную секцию и при старте просто копировать значения из флеш-памяти в ОЗУ. Целесообразно расположить эти данные в конце используемого пространства флеш-памяти (замечу не в конце флеш-памяти, можно было бы и в конце, но тогда бы пришлось прошивать данные в разные места флеш-памяти что согласитесь не очень удобно), для этого запомним последний используемый адрес флеш-памяти:
Раннее я писал что оператор «точка» определяет текущий адрес.
И сразу опишем секцию:
Так же для того что бы при старте знать что куда копировать «запоминаем» начальный и конечный адрес области (_data_begin, _data_end).
Аналогично задаем секцию для переменных начальные значение которых равны нулю (обнуляем их сами при старте):
Ну и последним этапом необходимо указать размер и расположение стека:
Можно было бы не указывать размер стека, а просто задать указатель на конец ОЗУ, но тогда компоновщик не предупредить нас нам не хватить памяти под стек. (Если быть точным то мы указали не фиксированный размер и минимальный, так как реально данные как правило занимает не всю доступную область ОЗУ, а лишь часть и стек может иметь больший размер).
Осталось указать точку входа (адрес с которого начинается выполнение программы), в нашем случае при старте нам необходимо произвести инициализацию переменных, а уже потом переходить к основной программе:
ENTRY(handler_reset)
Что бы лучше понять распределение памяти я нарисовал вот такую схемку:
Инициализация
Осталось выполнить начальную инициализацию МК. Почему эти действия не заложены на автомате как например в AVR-GCC, я думаю из-за разных реализаций микроконтроллеров с ARM архитектурой.
Основные действия при инициализации:
-
инициализация переменных
-
указание стека
-
передача управления основной программе
В примерах инициализация как правило написана на ассемблере, но что-то мне это не по душе и я переписал инициализацию на си, не знаю насколько корректно это сделал.
Инициализируем переменные начальными значениями и выполняем переход в основную программу:
Со стеком интереснее, достаточно просто записать в начальный адрес флеш-памяти указатель на конец стека, а так как сразу после этого идет таблица прерываний, то удобнее описать все за одно “движение”:
Атрибут ((section(“.interrupt_vector”))) предписываем компоновщику расположить данную таблицу в секции «interrupt_vector».
С векторами сильно не мудрил взял документацию на МК, посмотрел таблицу прерываний и на место каждого прерывания воткнул заглушку:
продолжение следует…
comments powered by Disqus