В статье предпринята попытка разобраться в содержимом 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,     READONLY

RESET – это всего лишь имя секции. Атрибут 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 файла немного лучше. Любые комментарии и замечания приветствуются.