[Перевод] Расшифровка startup файла Arm Cortex-M
В статье предпринята попытка разобраться в содержимое 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 с присвоением ей значения 0×00000400. Директива EQU в ассемблере аналогична директиве #define языка С.
AREA STACK, NOINIT, READWRITE, ALIGN=3
Далее происходит декларация области стека. Для этого используется директива AREA, которая обозначает отдельную секцию в памяти. Слово STACK в данном случае, всего лишь имя данной секции. За именем секции следуют следующие атрибуты.
NOINIT обозначает, что данные секции инициализируются нулями;
READWRITE, очевидно, позволяет производить чтение и запись секции;
ALIGN = 3 выравнивает начало секции по границе байта (2^3 = 8).
Stack_Mem SPACE Stack_Size
__initial_sp
В данной строке в области памяти стека выделяется пространство размером Stack_Size (0×0400 байт). Директива SPACE служит для резервирования указанного размера памяти. __initial_sp представляет собой декларацию метки, которая впоследствии будет использована в таблице векторов. Данная метка будет равна адресу, следующему за областью стека. Поскольку стек организован сверху-вниз (уменьшение адресов), данная метка будет служить указателем на его начало.
Таблица векторов
На текущий момент опустим код, относящийся к декларации кучи (Heap) и рассмотрим таблицу векторов. Таблица векторов размещается в секции RESET, которая декларируется строчкой кода:
AREA RESET, DATA, READONLY
RESET — это всего лишь имя секции. Атрибут DATA указывает на то, что в секции будут сохранены данные, а не команды. Действительно, таблица векторов содержит лишь адреса указателей обработчиков прерываний и адрес начала стека. Атрибут READONLY защищает указанную область от случайной записи из кода программы.
Данная секция размещается в начале области CODE флеш памяти по адресу 0×8000000 для выбранного микроконтроллера. Карта памяти приводится в разделе «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 (Flash point Unit IRQ).
Таблица векторов, и в частности две первых записи, необходимы для того, чтобы ядро запустилось и обработало инструкции PUSH/POP. Дело в том, что когда ядро CortexM4 стартует, оно сперва копирует первую запись таблицы векторов по адресу указателя стека (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 файла немного лучше. Любые комментарии и замечания приветствуются.