В статье предпринята попытка разобраться в содержимом startup файла микроконтроллера STM32F4, построенного на базе ядра Arm Cortex M4. Для запуска ядра используется ассемблерный код, который и предстоит изучить. Для лучшего понимания материала необходимо иметь представление об архитектуре ядра Cortex M4. Сразу отмечу, что замечания и уточнения приветствуются, т. к. они позволят дополнить представленную информацию.
Я не стану приводить здесь код startup файла полностью, чтобы избежать загромождения текста. Указанный файл является частью стандартного пакета программного обеспечения от STMicroelectronics, поставляемого с KEIL MDK-Arm. Это означает, что код относится к ассемблеру от Arm и не подходит для ассемблера GNU.
На нумерацию строк кода, представленного далее, не нужно обращать внимания, т. к. она никак не соотносится с последовательностью кода startup файла.
Структура Startup файла
В Startup файле имеется пять основных секций кода:
1. Декларация области стека (Stack);
2. Декларация области кучи (Heap);
3. Таблица векторов прерываний (Vector table);
4. Код обработки сброса (Reset handler);
5. Код обработки прочих исключений.
Область стека
Ассемблерный код обычно разделяется на секции при помощи директивы AREA. Давайте посмотрим, как происходит декларация области стека.
Stack_Size EQU 0x00000400В данной строке происходит декларация константы Stack_Size с присвоением ей значения 0x00000400. Директива EQU в ассемблере аналогична директиве #define языка С.
AREA STACK, NOINIT, READWRITE, ALIGN=3Далее происходит декларация области стека. Для этого используется директива AREA, которая обозначает отдельную секцию в памяти. Слово STACK в данном случае, всего лишь имя данной секции. За именем секции следуют следующие атрибуты.
NOINIT обозначает, что данные секции инициализируются нулями;
READWRITE, очевидно, позволяет производить чтение и запись секции;
ALIGN = 3 выравнивает начало секции по границе 8 байт (2^3 = 8).
Stack_Mem SPACE Stack_Size
__initial_spВ данной строке в о��ласти памяти стека выделяется пространство размером Stack_Size (0x0400 байт). Директива SPACE служит для резервирования указанного размера памяти. __initial_sp представляет собой декларацию метки, которая впоследствии будет использована в таблице векторов. Данная метка будет равна адресу, следующему за областью стека. Поскольку стек организован сверху-вниз (уменьшение адресов), данная метка будет служить указателем на его начало.
Таблица векторов
На текущий момент опустим код, относящийся к декларации кучи (Heap) и рассмотрим таблицу векторов. Таблица векторов размещается в секции RESET, которая декларируется строчкой кода:
AREA RESET, DATA, READONLYRESET – это всего лишь имя секции. Атрибут DATA указывает на то, что в секции будут сохранены данные, а не команды. Действительно, таблица векторов содержит лишь адреса указателей обработчиков прерываний и адрес начала стека. Атрибут READONLY защищает указанную область от случайной записи из кода программы.
Данная секция размещается в начале области CODE флеш памяти по адресу 0x8000000 для выбранного микроконтроллера. Карта памяти приводится в разделе «FLASH memory organization» Reference Manual. Начальный адрес используется при линковке и берется из scatter-файла, либо задается в настройках линкера. Таблица векторов размещается в памяти без смещения, поскольку регистр VTOR по умолчанию имеет нулевое значение. При помощи данного регистра имеется возможность сместить таблицу векторов. В данном случае используются значения, указанные в startup.
Таблица векторов содержит:
Указатель начала стека;
Адрес обработчика сброса, т.е. код, который будет выполнен при перезагрузке микроконтроллера;
Адреса всех прочих исключений и прерываний, включающих NMI (Non-maskable interrupt), прерывание Hard fault и т. п.
DCD __initial_spВ данной строке сохраняется метка __initial_sp в области RESET. Директива DCD сохраняет слово (32 бит) в память.
DCD Reset_HandlerАналогично, следующей строкой сохраняется адрес обработчика сброса Reset_Handler. Это предварительное объявление, поскольку декларация метки Reset_Handler производится в другом месте кода. Файл ассемблерного кода обрабатывается в два прохода, благодаря чему предварительное объявление становится возможным.
Далее следуют сохранения меток прерываний с различными адресами, таких как NMI_Handler, HardFault_Handler и т. п. До обработчика SysTick_Handler идут исключения процессора Arm. Затем таблицу продолжают внешние прерывания. Речь идет о прерываниях, внешних по отношению к ядру Arm, а не микроконтроллеру STM32. Данные прерывания относятся к различной периферии микроконтроллера, например модулю Watchdog, DMA, RTC и т. д. Список прерываний продолжается до FPU_IRQHandler (Floating-point unit IRQ).
Таблица векторов, и в частности две первых записи, необходимы для того, чтобы ядро запустилось и обработало инструкции PUSH/POP. Дело в том, что когда ядро Cortex M4 стартует, первое 32-битное значение из таблицы прерываний записывается в регистр MSP (Main Stack Pointer). Затем происходит копирование следующей записи в счётчик команд PC (Program counter) и выполняет��я команда по указанному адресу. Поскольку мы указываем адрес обработчика сброса (Reset Handler), именно он и будет выполнен.
Обработчик сброса
После определения в стартап файле таблицы прерываний начинается непосредственно код. Сохранение кода выполняется в область CODE.
AREA |.text|, CODE, READONLYДанной строкой задается область памяти с именем .text, которая содержит код, предназначенный только для чтения. Имя области может быть каким угодно. Символ вертикальной черты необходим для соблюдения правил наименований, поскольку имя не может начинаться с точки.
В указанной области сначала вызывается функция SystemInit, которая настраивает частоту тактирования микроконтроллера. И только затем, управление микроконтроллером передается функцию main().
IMPORT SystemInitДанная строка кода ссылается на функцию SystemInit, которая определена где-то в проекте.
IMPORT __mainФункция __main библиотеки С в конечном счёте вызывает main(), определенную вами.
Если вы пишите код на ассемблере, то потребуется разместить директиву ENTRY в обработчике сброса из-за отсутствия __main. Это позволит линкеру установить точку входа в программу.
LDR R0, =SystemInitЭта строка является псевдоинструкцией ассемблера, которая загружает адрес функции SystemInit в регистр R0. Последующая инструкция BLX R0 приводит к выполнению кода программы с данного адреса.
Вызов функции main происходит аналогичным образом, после того как функция SystemInit возвращает управление программой.
Обработчики исключе��ий
После запуска кода программы могут возникнуть исключения, обработку которых также необходимо предусмотреть. Для примера посмотрим на обработчик NMI.
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ALIGN
ENDPПервая строка NMI_Handler служит меткой этой небольшой функции. Ассемблерная директива PROC обозначает старт процедуры или функции.
Строка EXPORT делает метку NMI_Handler доступной другим частям программы. Атрибут [WEAK] добавлен для того, что обработчик можно было переопределить в другом месте. Это позволяет иметь собственный обработчик в проекте или разные обработчики для разных проектов, при этом сохранив одинаковый startup файл. Чем-то это напоминает виртуальные функции языка C++. Разумеется, если вам нужен один и тот же обработчик для всех проектов, разумно модифицировать startup файл для вызова вашей собственной функции или добавить код непосредственно в startup.
По умолчанию обработчики определены как бесконечные циклы инструкцией B. Данная инструкция ведет на один и тот же адрес, тем самым создавая бесконечный цикл. Директива ENDP обозначает конец процедуры. Директива ALIGN выравнивает текущую область памяти к границе следующего слова. Если текущее положение уже соответствует границе, вставляется инструкция NOP (нулевые данные). Директиву можно использовать для выравнивания к различным границам и даже для вставки или дополнения определенных данных, вместо обычного NOP. Аналогичный код используется далее для обработчиков всех исключений.
Для внешних обработчиков прерываний в файле startup используется один бесконечный цикл, который обозначен как Default_Handler. Метки внешних прерываний ссылаются на данный обработчик. Это означает, что для любого исключения, произошедшего в периферии микроконтроллера будет выполнен один и тот же Default_Handler. И снова используется атрибут WEAK, что позволяет переопределить код самостоятельно.
Обратите внимание, что даже Reset_Handler объявлен с данным атрибутом, т.е. при желании можно задать собственный обработчик сброса.
Куча
Определение кучи аналогично коду, определяющему стек. Две метки __heap_base и __heap_limit обозначают соответственно адреса начала и конца кучи. При использовании Arm Microlib начальный указатель стека, указатели начала и конца кучи экспортируются при компоновке.
Дополнительно
Необходимо уделить внимание ещё двум директивам, используемым в startup файле. Директива PRESERVE8 приказывает линкеру сохранять выравнивание стека по длине в 8 байт. Это требование стандартной архитектуры ARM, так называемой Arm Architecture Procedure Call Standard (AAPCS). Директива THUMB указывает на режим процессоров ARM, в котором используется сокращённая система команд. Данный режим относится к ядрам Cortex-M.
Надеюсь, что представленная в публикации информация оказалась полезна и поможет понять код startup файла немного лучше. Любые комментарии и замечания приветствуются.