Стартовый файл и скрипт компоновщика
В первом уроке стартовый файл и скрипт компоновщика были просто скопированы. Теперь разберём их построчно: что ядро делает само до первой команды, откуда берутся 59 слов таблицы векторов, как переменные попадают в ОЗУ и что проверяет компоновщик.
Обновлено Редакция GAW
STM32 с нуляурок 2 из 835 мин
Цель урока: Понимать каждую строку своего startup.c и stm32f103c8.ld, проверять расположение секций утилитами objdump, nm и size и находить, какое прерывание попало в Default_Handler.
Между подачей питания и первой строкой main проходит заметная работа: ядро читает таблицу векторов, стартовый код готовит переменные. В обычном проекте IDE это делают файлы из шаблона, а у нас — startup.c и stm32f103c8.ld из урока 1. Общая теория кода запуска разобрана в статье «Код запуска микроконтроллера», а скриптов компоновщика — в статье «Скрипт компоновщика GNU ld»; здесь — то, что нужно именно нашему проекту.
Что делает ядро после сброса
Раздел 3.4 RM0008 (с. 60) описывает старт так: после сброса микроконтроллер защёлкивает выводы BOOT, и при BOOT0 = 0 начало флеш-памяти (0x0800 0000) становится видно ещё и с адреса 0x0000 0000. Затем ядро Cortex-M3 само, без единой команды программы:
- читает слово по адресу 0x0000 0000 и записывает его в указатель стека MSP;
- читает слово по адресу 0x0000 0004 — вектор сброса — и переходит по нему.
Младший бит вектора должен быть единицей: так ядро узнаёт, что обработчик — код Thumb (DUI 0552A, разд. 2.3.4), а другого Cortex-M3 и не выполняет. Если вектор чётный, ядро уйдёт в исключение, не выполнив ни одной команды программы. Компилятор и компоновщик ставят этот бит сами, когда в таблицу записан адрес функции, поэтому писать + 1 вручную не нужно.
Отсюда первое требование к проекту: в самом начале флеш-памяти должна лежать таблица, у которой слово 0 — вершина стека, а слово 1 — адрес Reset_Handler.
Таблица векторов: 59 слов
У ядра 16 позиций: начальный MSP, Reset и 14 системных исключений, из них пять зарезервированы. Дальше идут прерывания периферии. Таблица 63 RM0008 (с. 204–207) перечисляет их для всех STM32F10x, кроме connectivity line, но у STM32F103C8 средней плотности (medium-density) последнее — USBWakeup в позиции 42 по адресу 0x0000_00E8; позиции с 43-й (TIM8 и дальше) есть только у кристаллов большой плотности. Итого 16 + 43 = 59 слов, 236 байт. Тот же порядок — в стартовом файле ST startup_stm32f103xb.s, откуда взяты и имена обработчиков: они должны совпадать до буквы, потому что именно по имени ваша функция подменит обработчик по умолчанию.
Таблица описана структурой, а не массивом указателей на функции:
typedef struct {
uint32_t *initial_sp; /* 0x000: loaded into MSP on reset */
handler_t exceptions[15]; /* 0x004..0x03C: Reset .. SysTick */
handler_t irq[43]; /* 0x040..0x0E8: IRQ 0..42 */
} vector_table_t;
Первое слово — это адрес данных, а не функции, и в массиве handler_t его пришлось бы приводить к указателю на функцию — такое преобразование стандарт C не определяет. В структуре у первого поля свой тип, и приведение не нужно. Все поля по 4 байта, промежутков между ними нет, а _Static_assert(sizeof(vector_table_t) == 59 * 4, …) остановит сборку, если кто-то ошибётся в размере массивов.
Позиции номер 0 в инициализаторе exceptions — зарезервированные слова таблицы. Порядок там такой: Reset, NMI, HardFault, MemManage, BusFault, UsageFault, четыре резерва, SVCall, DebugMonitor, резерв, PendSV, SysTick — последний попадает по адресу 0x0000_003C, как в табл. 63.
Два атрибута держат таблицу на месте. section(".isr_vector") кладёт её в отдельную секцию, которую скрипт компоновщика ставит первой во флеш-память. used не даёт компилятору выбросить переменную, на которую никто не ссылается (руководство GCC, Common Attributes), а от сборщика мусора компоновщика --gc-sections секцию защищает KEEP в скрипте.
Слабые обработчики
#define WEAK __attribute__((weak, alias("Default_Handler")))
void SysTick_Handler(void) WEAK;
void USART1_IRQHandler(void) WEAK;
Каждое имя обработчика объявлено слабым псевдонимом Default_Handler — ровно так руководство GCC описывает пару атрибутов weak и alias. Пока в программе нет своей функции USART1_IRQHandler, в таблицу попадает адрес Default_Handler. Как только она появится в любом другом файле, компоновщик выберет её: обычный (сильный) символ побеждает слабый. Цель псевдонима должна быть определена в той же единице трансляции, поэтому Default_Handler написан в самом startup.c.
Обработчик по умолчанию — бесконечный цикл. Это удобно для отладки: если программа «зависла», остановите её в GDB и посмотрите номер исключения командой p/x $xpsr. Младшие 9 бит регистра — поле ISR_NUMBER (DUI 0552A, разд. 2.1.3): 3 — HardFault, 15 — SysTick, 16 и больше — прерывания периферии, номер IRQ равен числу минус 16. Опечатка USART1_IRQHandle без последней буквы видна сразу: в ISR_NUMBER будет 53, а ваша функция так и не вызвана.
Reset_Handler: .data и .bss
Глобальная переменная с начальным значением (magic = 0xC0FFEE42 из урока 1) живёт в ОЗУ, но её начальное значение после выключения питания должно где-то храниться — во флеш-памяти. Поэтому у секции .data два адреса: VMA — где переменная находится во время работы (ОЗУ), и LMA — где лежит образ начальных значений (флеш-память). Копирует образ на место первый цикл Reset_Handler:
uint32_t *src = _sidata; /* LMA of .data, in flash */
uint32_t *dst = _sdata; /* VMA of .data, in RAM */
while ((uintptr_t)dst < (uintptr_t)_edata)
*dst++ = *src++;
Переменные без начального значения и с нулевым значением попадают в .bss. Хранить нули во флеш-памяти незачем: второй цикл просто обнуляет диапазон от _sbss до _ebss. Символы _sidata, _sdata, _edata, _sbss, _ebss и _estack определяет скрипт компоновщика; в C они объявлены массивами, и программа пользуется только их адресами. Указатели сравниваются через uintptr_t: формально сравнивать указатели на разные объекты стандарт C не разрешает, а целые адреса — можно.
Оба цикла копируют словами по 4 байта, поэтому скрипт выравнивает границы секций на 4. С ключом -Os компилятор вправе заменить такие циклы вызовами memcpy и memset: оптимизация -ftree-loop-distribute-patterns включена на -O2 и выше, а -Os включает оптимизации -O2 (руководство GCC, Optimize Options). Поэтому в уроке 1 указан ключ -nostartfiles, а не -nostdlib: библиотека libc остаётся подключённой, и эти функции из неё найдутся. Сами memcpy и memset ни .data, ни .bss не используют, так что вызывать их до инициализации безопасно.
Чего в нашем Reset_Handler нет по сравнению со стартовым файлом ST:
SystemInit. В шаблоне ST для F1 она не включает ни PLL, ни кварц — тактирование настроим сами в уроке 4;__libc_init_array. Она вызывает конструкторы C++ и функции с атрибутомconstructor; в программе на C их нет;- возврата из
main. Возвращаться некуда, поэтому после вызова стоит бесконечный цикл.
Скрипт компоновщика по строкам
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* 0x20005000 */
MEMORY описывает две области: 64 КБ флеш-памяти с адреса 0x0800 0000 и 20 КБ ОЗУ с 0x2000 0000 — объёмы из таблицы 2 DS5319, адреса из раздела 3.4 RM0008. Суффикс K означает ×1024. Буквы в скобках — атрибуты: по ним компоновщик выбирает область для секций, которым она не указана явно.
_estack — адрес сразу за последним байтом ОЗУ, 0x20000000 + 20 × 1024 = 0x20005000 (расчёт наш). Стек Cortex-M растёт вниз и перед записью сначала уменьшает указатель, поэтому первое слово ляжет в 0x20004FFC. Почему вершина «на последнем байте», 0x20004FFF, — ошибка, подробно разобрано в статье о скриптах компоновщика.
| Секция | Куда | Что в ней |
|---|---|---|
.isr_vector | FLASH, с 0x08000000 | таблица векторов; KEEP не даёт --gc-sections её выбросить |
.text | FLASH | код (.text*) и константы (.rodata*) |
.data | VMA в RAM, LMA во FLASH (> RAM AT > FLASH) | переменные с начальными значениями |
.bss | RAM | переменные без начальных значений и нулевые, COMMON |
Конструкция > RAM AT > FLASH задаёт два адреса .data: VMA в ОЗУ и LMA во флеш-памяти — следующий свободный адрес области FLASH, выровненный по требованию секции (руководство GNU ld, Output Section LMA). LOADADDR(.data) возвращает этот LMA, и его получает символ _sidata для цикла копирования.
Последняя строка — проверка:
ASSERT(_ebss + 1K <= _estack, "less than 1 KB of RAM left for the stack")
Если глобальные переменные займут больше 19 КБ, компоновка остановится с этим сообщением. Стеку тогда осталось бы меньше килобайта — переполнение испортило бы .bss молча и в неожиданный момент. Сколько стека нужно на самом деле, компоновщик не знает; как это оценить, рассказано в статье «Переполнение стека на микроконтроллере».
Проверяем, что получилось
arm-none-eabi-objdump -h blink.elf # sections: size, VMA, LMA
arm-none-eabi-nm -n blink.elf | head # symbols sorted by address
arm-none-eabi-size -A -x blink.elf # size of every section
arm-none-eabi-objdump -s -j .isr_vector blink.elf | head -6
В выводе objdump -h у .isr_vector VMA и LMA равны 0x08000000; у .data VMA начинается с 0x20000000, а LMA — сразу за .text во флеш-памяти; у .bss VMA идёт за .data. Последняя команда покажет первые байты таблицы, строка начинается так: 8000000 00500020 f1000008 — 0x20005000 и 0x080000F1 в порядке little-endian. Первое слово — вершина стека, второе — адрес Reset_Handler с единицей в младшем бите (у вас адрес может отличаться, но младший бит будет 1). Сводку занятой памяти печатает уже ключ --print-memory-usage при каждой сборке; как читать map-файл и вывод size, разобрано в статье о скриптах компоновщика.
На STM32F4
Для STM32F411 стартовый файл ST startup_stm32f411xe.s первой командой Reset_Handler загружает SP из _estack, а таблица содержит 16 + 86 = 102 слова. В скрипте компоновщика меняются только длины: FLASH … LENGTH = 512K, RAM … LENGTH = 128K — по адресам из заголовка stm32f411xe.h. Главное добавление в Reset_Handler — включить FPU записью SCB->CPACR |= (0xFu << 20); (поля CP10 и CP11) и барьерами __DSB(); __ISB(); до первой команды с плавающей точкой, если программа собрана с -mfloat-abi=hard; иначе первая же такая команда вызовет исключение UsageFault. Эту последовательность — запись битов 20–23 CPACR, затем DSB и ISB — приводит руководство Arm DUI 0553B по Cortex-M4 (разд. 4.6.6), а доступ при запрещённом сопроцессоре оно описывает как исключение UsageFault с флагом NOCP.
Задание
- Уберите
KEEPиз строки.isr_vectorв скрипте компоновщика и соберите проект. Что изменится в выводеobjdump -hи что произойдёт с платой после прошивки? - Добавьте в
main.cглобальный массивuint8_t buffer[20000];, а в циклmain— строкуbuffer[counter % sizeof buffer] = 1u;, и соберите проект. Что сообщит компоновщик и почему? Что будет, если строку в цикле не добавлять?
Ответ и решение
-
На таблицу векторов никто не ссылается, и
--gc-sectionsвыбросит секцию.isr_vector: атрибутusedзащищает переменную только от компилятора. В начале флеш-памяти окажется код из.text. После сброса ядро загрузит в MSP и PC случайные слова этого кода, и программа не запустится — ядро уйдёт в исключение. Вернуть плату к жизни поможет повторная прошивка правильного файла (при необходимости с BOOT0 = 1). -
Массив без начального значения попадёт в
.bss: 20 000 байт больше, чем 20 × 1024 − 1024 = 19 456 байт, которые ASSERT оставляет переменным (расчёт наш). Компоновка остановится с сообщением «less than 1 KB of RAM left for the stack». Без ASSERT программа собралась бы, а стек при первом вызове функции начал бы затирать конец массива. Если же массивом нигде не пользоваться,--gc-sectionsвыбросит его секцию.bss.bufferцеликом, и сборка пройдёт без ошибки: компоновщик проверяет только то, что осталось в программе.
Что дальше
В уроке 3 включим тактирование порта C в RCC, настроим PC13 регистром CRH и наконец помигаем светодиодом. Термины урока коротко объясняют статьи глоссария «Таблица векторов», «Секция .bss», «Скрипт компоновщика» и «Map-файл». Таблица прерываний STM32F1 с номерами IRQn и обработчиками — в статье «Прерывания STM32».
Частые вопросы
Можно ли взять стартовый файл ST вместо своего?
Можно: startup_stm32f103xb.s из репозитория cmsis-device-f1 делает то же самое на ассемблере и дополнительно вызывает SystemInit и __libc_init_array. Тогда в проект нужно добавить system_stm32f1xx.c, а в скрипт компоновщика — секции .preinit_array и .init_array с символами начала и конца, как в шаблоне ST. Свой файл короче, и в нём нет ни одной строки, которую вы не понимаете.
Почему программа упала в Default_Handler?
Сработало прерывание или исключение, для которого не написан обработчик, — чаще всего из-за опечатки в имени функции: обработчик, названный иначе, чем в таблице, не заменяет слабый псевдоним. Остановите программу в GDB и выполните p/x $xpsr: младшие 9 бит — номер исключения, например 3 для HardFault или 16 + 37 = 53 для USART1.
Нужен ли __libc_init_array в программе на C?
В программе на чистом C — нет: он вызывает функции из секций .preinit_array и .init_array, а их заполняют конструкторы статических объектов C++ и функции с атрибутом constructor. Если в проекте появится C++ или такой атрибут, эти секции нужно описать в скрипте компоновщика и вызвать их функции из Reset_Handler до main.
Источники
- STMicroelectronics. RM0008 Rev 21. Reference manual STM32F101xx–STM32F107xx — разд. 3.4 (с. 60): после задержки запуска ядро читает вершину стека по адресу 0x0000 0000 и начинает выполнение с 0x0000 0004; при загрузке из флеш-памяти она видна с 0x0000 0000 и с 0x0800 0000; разд. 10.1.2, табл. 63 (с. 204–207): векторы устройств, кроме connectivity line, — Reset по 0x0000_0004, SysTick по 0x0000_003C, IRQ 0 (WWDG) по 0x0000_0040, последний для medium-density — USBWakeup, позиция 42, адрес 0x0000_00E8
- Arm. Cortex-M3 Devices Generic User Guide, DUI 0552A — разд. 2.3.4: младший бит каждого вектора должен быть 1 — обработчик в коде Thumb; разд. 2.1.3: поле ISR_NUMBER регистра IPSR — номер текущего исключения: 0 — режим Thread, 3 — HardFault, 15 — SysTick, 16 — IRQ0
- GNU ld 2.47 Documentation: MEMORY, Output Section LMA, Input Section Keep, Builtin Functions, Miscellaneous Commands — AT> region: LMA — следующий свободный адрес области, выровненный по требованию секции; LOADADDR; KEEP защищает секцию от --gc-sections; ASSERT(exp, message) завершает компоновку с ошибкой, если exp равно нулю; суффиксы K и M в константах
- GCC 16.2 Manual: Common Attributes (alias, weak, used, section), Optimize Options — пример weak + alias; цель alias должна быть определена в той же единице трансляции; used — переменная выводится, даже если на неё нет ссылок; -ftree-loop-distribute-patterns включён на -O2 и выше и превращает циклы инициализации в вызовы библиотеки; -Os включает все оптимизации -O2, кроме -falign-functions, -falign-jumps, -falign-labels, -falign-loops, -fprefetch-loop-arrays и -freorder-blocks-algorithm=stc
- Arm. Cortex-M4 Devices Generic User Guide, DUI 0553B — разд. 4.6.6, пример 4-1: включение FPU — биты 20–23 CPACR (CP10 и CP11), затем DSB и ISB; CPACR по адресу 0xE000ED88, сброс 0x00000000, 0b00 — доступ запрещён, обращение вызывает NOCP UsageFault
- STMicroelectronics. cmsis-device-f1 v4.3.5: Include/stm32f103xb.h, Source/Templates/gcc/startup_stm32f103xb.s — имена обработчиков и их порядок в таблице векторов; USART1_IRQn = 37, EXTI15_10_IRQn = 40, USBWakeUp_IRQn = 42
Нашли ошибку?
Смотрите также
- Код запускаТаблица векторов, Reset_Handler, .data и .bss, VTOR, FPU, .initN avr-libc
- Скрипт компоновщика GNU ldMEMORY, SECTIONS, VMA и LMA, KEEP, стек и куча, map-файл, переполнение
- Таблица векторов прерыванийАдреса обработчиков прерываний в начале флеш-памяти
- Секции .data и .bssГде лежат глобальные переменные: копирование и обнуление при старте
- Map-файлОтчёт компоновщика о том, где в памяти лежат секции, функции и переменные