startup.s для Cortex-M в ARM компиляторе

Попытаюсь разобраться со структурой стартового файла startup.s для процессоров Cortex-M в среде Keil MDK-ARM и понять, как запускается процессор, на примере файла инициализации для отечественных процессоров 1986ВЕ1Т, которые являются аналогом ядра Cortex-M1.

Структура файла

Стартовый файл написан на ассемблере. Его можно разделить на несколько основных частей:

  1. Объявление области стека (stack)
  2. Объявление области кучи (heap)
  3. Таблица векторов прерываний
  4. Код обработчика сброса (reset handler)
  5. Код остальных обработчиков исключений

uVision позволяет настраивать часть параметров (размер стека и кучи) с помощью специального визуального редактора. Он активируется вкладкой с названием Configuration Wizard в нижней части окна.

Ниже я буду приводить небольшие участки кода из этого файла. Весь файл можно найти в программной библиотеке для этих процессоров.

Область стека

Ассемблерный код делиться на секции с использованием директивы AREA. Рассмотрим, как происходит объявление области стека:

Stack_Size      EQU     0x00001000

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

Первая строка объявляет константу Stack_Size равной 0x00001000 (4096 байт). Директива EQU подобна директиве препроцессора #define.

Далее идет объявление области стека. Директивой AREA создается отдельная секция в памяти с названием STACK. За названием следуют атрибуты:

  • NOINIT — данные в секции заполняются нулями
  • READWRITE — секция доступна на чтение и запись
  • ALIGN=3 — выравнивание секции по границе 8 байт (2^3)

Следующая строка выделяет пространство заданного размера в области стека. Директива SPACE просто резервирует заполненное нулями место в памяти.

Последняя строка создает метку __initial_sp, которая в дальнейшем будет использоваться в таблице векторов. Метка приравнивается следующему адресу после области стека. Так как стек растет вниз, она будет служить начальным значением указателя стека.

Область кучи

По тому же принципу выделяется место для области кучи:

Heap_Size       EQU     0x00001000

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

Heap_size — константа, определяющая размер кучи. Затем создает секция HEAP. Отличие заключается в том, что у кучи есть две метки, которые указывают на начало и на конце кучи.

В конце стартового файла есть код, который в зависимости от того, используется ли библиотека ARM Microlib, либо просто экспортирует ей указатели стека и кучи, либо выполняется ряд дополнительных манипуляций с этими указателями.

Таблица векторов

Следующая секция — таблица векторов прерываний. Секция называется RESET.

                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors

__Vectors       DCD     __initial_sp              ; Top of Stack
                DCD     Reset_Handler             ; Reset Handler
                DCD     NMI_Handler               ; NMI Handler
                DCD     HardFault_Handler         ; Hard Fault Handler

Ее атрибуты:

  • DATA — секция содержит данные, а не инструкции. Таблица векторов содержит только адреса обработчиков и начальное значение указателя стека.
  • READONLY — защищает эту секцию от перезаписи программой.

Секция размещается перед секцией CODE во Flash памяти. Обычно это адрес 0x08000000. Подробнее об адресации указывается в документации на процессор. Для 1986ВЕ1Т этот адрес равен 0x00000000. Так как таблица помещается в начале памяти, то с нее процессор и начинает свою работу.

Таблица векторов содержит:

  • Начальное значение указателя стека
  • Адрес обработчика сброса, то есть откуда будет выполняться код после сброса процессора
  • Адреса обработчиков всех остальных исключений и прерываний

Первая строка DCD __initial_sp сохраняет значение указателя стека. Инструкция DCD сохраняет 32-битное слово в памяти.

Следующая строка DCD Reset_Handler сохраняет адрес обработчика сброса. Это обработчик объявляется ниже в стартовом файле.

Следующие строки записывают адреса различных прерываний, таких как HardFault_Handler и другие. После этого идут «внешние» прерывания. Слово «внешние» относится к ядру ARM-процессора. К ним относятся все прерывания периферии: таймеры, DMA, интерфейсы связи и так далее.

Таблица векторов и, особенно, первые две строки являются неотъемлемой составляющей для запуска процессора, а также работы инструкций PUSH/POP. Это связано с тем, что процессор при запуске копирует первый элемент в регистр стека MSP (Main Stack Pointer), а вторую в регистр счетчика программы PC (Program Counter) и начинает выполнение программы с этого адреса.

Обработчик сброса

После объявления таблицы векторов начинается фактический код. Он находится в секции с атрибутом CODE.

                AREA    |.text|, CODE, READONLY


; Reset Handler

Reset_Handler   PROC
                EXPORT  Reset_Handler			[WEAK]
                IMPORT  SystemInit
                IMPORT  __main
                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0,=__main
                BX      R0
                ENDP

Директива AREA объявляет секцию .text, которая содержит содержит код программы. Название секции используется общепринятое, но может быть любым другим. Эта секция имеет атрибут «только для чтения», чтобы избежать перезаписи данных программой.

Сначала вызывается функция SystemInit, которая выполняет начальную инициализацию процессора, а затем вызывается функция main. Таким образом контроль передается функции main основной программы.

Обработчики исключений

В процессе выполнения программы могут возникать исключения и прерывания, которые нуждаются в обработке. В стартовом файле объявлены функции-заглушки для всех прерываний процессора. Например, NMI_Handler:

NMI_Handler     PROC
                EXPORT  NMI_Handler				[WEAK]
                B       .
                ENDP

Директива PROC объявляет начало функции. Следующая строка EXPORT делает доступной для остальной части программы метку этой функции. Атрибут [WEAK] говорит о том, что эта функция может быть переопределена где-нибудь в другом месте проекта. Это позволяет определять свои собственные обработчики прерываний.

Эти функции-заглушки имеют только одну инструкцию B. Она выполняет переход на свой же адрес, тем самым генерируя бесконечный цикл.

Директива ENDP обозначает конец функции.

Разное

Есть еще две директивы, которые не были упомянуты ранее.

  • PRESERVE8 — указывает компоновщику сохранять 8-байтное выравнивание стека. Это требование Arm Architecture Procedure Call Standard (AAPCS).
  • THUMB — указывает ассемблеру интерпретировать последующие инструкции, как THUMB-инструкции.

За основу этого текста была взята статья автора Gopal Amlekar с сайта ARM Community: Decoding the Startup file for Arm Cortex M4