Перейти к содержанию

Код запуска (startup)

Функция main — не первое, что выполняет микроконтроллер после сброса. До неё работает код запуска: он готовит указатель стека и память так, чтобы программа на C увидела глобальные переменные с правильными начальными значениями.

Обновлено Редакция GAW

Код запуска (startup code, crt0) — короткая программа, которая выполняется после сброса до функции main. Её задача — подготовить среду, которую ожидает программа на C: стек, глобальные переменные с начальными значениями, нулевые переменные, конструкторы. Обычно она приходит готовой: файл startup_stm32f103xb.s у STM32 или crt1 из avr-libc у AVR.

Cortex-M (шаблон ST для STM32F103)

  1. Ядро само загружает указатель стека из слова 0 таблицы векторов и переходит по адресу из слова 1 — на Reset_Handler.
  2. SystemInit — начальная настройка тактирования.
  3. Копирование секции .data из флеш-памяти в ОЗУ.
  4. Обнуление .bss.
  5. __libc_init_array — конструкторы C++ и функции инициализации библиотеки.
  6. Переход в main.

Про .data и .bss подробно — в термине «Секции .data и .bss». Стандарт вызова AAPCS32 требует, чтобы на границе вызова функций указатель стека был кратен 8 (SP mod 8 = 0). В шаблоне ST для STM32F103 из cmsis-device-f1 стоит _estack = 0x20004FFF: ядро обнулит два младших бита и получит 0x20004FFC, что не кратно 8 (наш расчёт по правилу B1.5.5). Надёжнее задавать вершину стека адресом конца ОЗУ, кратным 8, например 0x20005000 — это наша рекомендация, а не значение из шаблона.

AVR (avr-libc)

Код запуска разбит на секции .init0–.init9, которые выполняются по порядку: .init2 ставит указатель стека на __stack (по умолчанию последний адрес ОЗУ, RAMEND), .init4 инициализирует .data и .bss, .init9 вызывает main. Свою функцию можно поместить в свободную секцию — .init1, .init3, .init5, .init7 или .init8 — с атрибутами naked, used и section: avr-libc требует, чтобы такой код не содержал возврата и никогда не вызывался явно, а порядок нескольких функций в одной секции не определён. Код на C ставят не раньше .init3.

Не путать код запуска прошивки с кодом запуска отдельной периферии — например, последовательностью включения сторожевого таймера, которую генерирует калькулятор IWDG и WWDG STM32. И не путать с загрузчиком: загрузчик — отдельная программа, которая принимает и записывает прошивку, а код запуска — часть каждой прошивки. Как символы _estack, _sidata, _sbss попадают в код запуска из скрипта компоновщика — в статье «Скрипт компоновщика GNU ld», типы и переменные в C для микроконтроллеров — в статье «Язык C для микроконтроллеров».

Частые вопросы

Что делает Reset_Handler у STM32?

В шаблоне ST для STM32F103: вызывает SystemInit (начальная настройка тактирования), копирует начальные значения .data из флеш-памяти в ОЗУ, заполняет .bss нулями, вызывает __libc_init_array — конструкторы C++ и функции инициализации — и переходит в main. Указатель стека к этому моменту уже загружен ядром из слова 0 таблицы векторов.

Кто устанавливает указатель стека на Cortex-M?

Само ядро: по руководству ARMv7-M при сбросе оно берёт начальное значение SP из первого слова таблицы векторов. В шаблоне ST туда записан символ _estack — вершина ОЗУ. У AVR указатель стека ставит код запуска в секции .init2, по умолчанию на последний адрес ОЗУ.

Можно ли выполнить код до обнуления .bss?

Да, но осторожно: глобальные переменные в этот момент ещё не инициализированы. У STM32 для этого есть SystemInit. У AVR код помещают в свободные секции .init1, .init3, .init5, .init7 или .init8: функция с атрибутами naked, used и section, без возврата, и её никогда не вызывают явно; код на C — не раньше .init3, потому что указатель стека и нулевой регистр готовит .init2. Так, по FAQ avr-libc, рано настраивают MCUCR или сторожевой таймер.

Источники

  1. STMicroelectronics. cmsis-device-f1, коммит c8e9a4a: Source/Templates/gcc/linker/STM32F103XB_FLASH.ld и Source/Templates/gcc/startup_stm32f103xb.s — Reset_Handler: вызов SystemInit, копирование .data словами, обнуление .bss, __libc_init_array, переход в main; g_pfnVectors: слово 0 — _estack, слово 1 — Reset_Handler; _estack = 0x20004FFF в шаблоне Ac6
  2. Arm. ARMv7-M Architecture Reference Manual, DDI 0403E.e, 2021 — B1.5.5: при сбросе SP_main = слово 0 таблицы векторов с обнулёнными двумя младшими битами
  3. Arm. Procedure Call Standard for the Arm Architecture (AAPCS32), github.com/ARM-software/abi-aa, коммит 7850367 — Stack constraints at a public interface: SP mod 8 = 0, стек выровнен на двойное слово
  4. AVR-LibC 2.3.2: Memory Sections (doc/api/sections.dox), crt1/gcrt1.S — секции .init0–.init9: .init2 — указатель стека = __stack (по умолчанию RAMEND), .init3 — раскраска ОЗУ (если используется), .init4 — инициализация .data и .bss, .init9 — переход в main

Нашли ошибку?