4в. Сборка без среды: GCC, Makefile, стартовый код и свой скрипт компоновщика
Среда разработки удобна, но не обязательна. Прошивку для STM32 можно
написать в любом редакторе, а собрать одной командой в терминале. Для этого
нужны четыре файла: программа, стартовый код, скрипт компоновщика и
Makefile. Раздел показывает все четыре с нуля — без HAL, без библиотеки C и
без единой строки ассемблера. Главное здесь — скрипт компоновщика *.ld:
как написать его самому и как проверить, что он сделал то, что задумано.
Всё проверено сборкой: arm-none-eabi-gcc 12.2, GNU Make 4.3, запуск в
QEMU 7.2 на плате netduinoplus2 (STM32F405 — ядро и память как у F407),
отладка GDB (_verify/bare4v/). Для F103 меняются только размеры памяти в
скрипте и ключ ядра — об этом в 4в.9.
Как это называется в документации
Оригинал Перевод toolchain набор инструментов: компилятор, ассемблер, компоновщик, утилиты linker script скрипт компоновщика: где какая память и что куда кладётся MEMORY,SECTIONSописание областей памяти и раскладка секций output section, input section секция образа и секции из объектных файлов VMA (virtual memory address) адрес, по которому секция работает LMA (load memory address) адрес, по которому секция хранится в образе location counter ( .)текущий адрес внутри секции KEEP,--gc-sections«не выбрасывать» и удаление неиспользуемых секций NOLOADсекция без содержимого в образе vector table, reset handler таблица векторов, обработчик сброса weak alias слабый псевдоним: имя, которое можно переопределить map file карта памяти после компоновки freestanding программа без операционной системы и без полной библиотеки C
4в.1 Что происходит между исходником и прошивкой
Одна команда arm-none-eabi-gcc на самом деле запускает цепочку программ:
| Шаг | Программа | Из чего | Во что |
|---|---|---|---|
| препроцессор | cpp (внутри gcc) |
.c с #include и #define |
чистый C |
| компилятор | cc1 |
C | ассемблер |
| ассемблер | as |
ассемблер | объектный файл .o |
| компоновщик | ld (через collect2) |
все .o + скрипт .ld |
app.elf |
| преобразование | objcopy |
app.elf |
app.bin или app.hex для прошивки |
app.elf — полный образ с адресами, символами и отладочными данными: его
грузит отладчик. app.bin — только байты флеш-памяти подряд: его пишет
программатор. Среда вызывает те же программы, и больше ничего не делает.
4в.2 Программа без HAL
Программа печатает строку в USART1 и проверяет, что стартовый код отработал: переменная с начальным значением должна его получить, а массив без значения — оказаться нулями:
static uint32_t counter = 0x12345678; /* .data */
uint32_t zeros[4]; /* .bss; не static — иначе компилятор подставит 0 и секция опустеет */
static const char greeting[] = "bare metal: hello\n"; /* .rodata */
Регистры — прямо по адресам, как в разделе 8:
#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR (*(volatile uint32_t *)0x40011000)
#define USART1_DR (*(volatile uint32_t *)0x40011004)
#define USART1_BRR (*(volatile uint32_t *)0x40011008)
#define USART1_CR1 (*(volatile uint32_t *)0x4001100C)
RCC_APB2ENR |= 1u << 4; /* такт USART1 */
USART1_BRR = 0x008B; /* 115200 бод при 16 МГц: 16 000 000 / 115 200 = 138,9 */
USART1_CR1 = (1u << 13) | (1u << 3); /* UE — USART включён, TE — передатчик */
Эмулятору выводы не нужны, а на плате USART1 без настройки PA9 молчит.
Перед ним включают порт: такт GPIOA — бит 0 в RCC_AHB1ENR
(0x40023830); в GPIOA_MODER (0x40020000) для PA9 биты 19:18 = 10 —
альтернативная функция; в GPIOA_AFRH (0x40020024) биты 7:4 = 7 — AF7,
это USART1_TX (раздел 8).
Каждый регистр — с volatile (раздел 3.4). Без него ожидание флага TXE
при -Os читает регистр один раз: arm-none-eabi-gcc 12.2 сделал из цикла
ldr r1, [r2], lsls r1, r1, #24, bmi и переход сам на себя. Стоит
прочитать TXE = 0 — и программа висит навсегда, а на плате вывод
обрывается на первых символах. QEMU этого не показывает: передатчик у него
свободен всегда, и прогон без volatile тоже проходит.
Про zeros без static — не случайность. С static компилятор при -Os
видит весь файл, понимает, что в массив никто не пишет, и подставляет вместо
чтения ноль: секция .bss оказывается пустой, и проверять нечего. У
глобальной переменной другие файлы могут писать в неё, и компилятор обязан
читать память.
4в.3 Скрипт компоновщика с нуля
Скрипт отвечает на три вопроса: какая память есть, что куда класть и откуда
стартовому коду брать начальные значения. Ниже он идёт четырьмя кусками
подряд — вместе они и есть весь linker.ld, со всеми скобками. Сначала
память и точка входа:
ENTRY(Reset_Handler) /* с чего начинается программа — для отладчика и ELF */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* вершина стека: стек растёт вниз от конца ОЗУ */
_stack_size = 0x800; /* сколько ОЗУ оставить под стек — проверяется в ASSERT */
MEMORY задаёт области: имя, права (r — чтение, w — запись, x —
исполнение), начало и размер. Суффикс K — килобайты. _estack —
обычный символ скрипта: у него есть адрес, но нет памяти, и программа видит
его как extern.
Дальше секции. Первой во флеш-памяти обязана лежать таблица векторов: ядро
после сброса читает начальный SP и адрес Reset_Handler из первых двух слов
по адресу 0x00000000. При BOOT0 = 0 туда отображена Flash с 0x08000000
(раздел 1.4), поэтому таблицу кладут в её начало:
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector)) /* таблица векторов — первой, с адреса 0x08000000 */
} > FLASH
.text :
{
*(.text .text.*) /* код */
*(.rodata .rodata.*) /* константы и строки */
. = ALIGN(4);
_etext = .;
} > FLASH
SECTIONS { открывается здесь, а закрывается в последнем куске, после .bss.
*(.text .text.*) — «секции .text и .text.что-угодно из всех
объектных файлов». Имена вида .text.main получаются от ключа
-ffunction-sections: каждая функция в своей секции, и компоновщик может
выбросить неиспользуемые (--gc-sections). KEEP запрещает выбрасывать
таблицу векторов: на неё не ссылается ни одна функция. Точка . — текущий
адрес; . = ALIGN(4) выравнивает его на слово, _etext = . запоминает
конец кода.
Самое важное — .data. У переменной с начальным значением два адреса:
работает она в ОЗУ, а значение до включения питания хранится во флеш-памяти:
.data :
{
_sdata = .; /* где .data работает — адрес в ОЗУ (VMA) */
*(.data .data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH /* а хранится во флеш-памяти сразу за кодом (LMA) */
_sidata = LOADADDR(.data); /* откуда стартовый код копирует начальные значения */
> RAM — адрес работы (VMA), AT > FLASH — адрес хранения (LMA).
LOADADDR(.data) отдаёт второй. .bss хранить не нужно — там одни нули:
.bss (NOLOAD) :
{
_sbss = .; /* нули: во флеш-памяти не хранятся вовсе */
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}
NOLOAD — у секции есть адреса, но нет байтов в образе. Строка
*(.bss .bss.*) обязательна: без неё переменные без начального значения
уйдут в секции-сироты, и стартовый код их не обнулит — та же ловушка, что
с .data в 4в.10. ASSERT
останавливает сборку, если данные и стек не помещаются в ОЗУ: ошибку видно
при компоновке, а не при зависании платы.
4в.4 Стартовый код на C
Стартовый файл не обязан быть на ассемблере. Cortex-M сам загружает SP из
таблицы векторов, поэтому Reset_Handler может быть обычной функцией C:
#include <stdint.h>
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss; /* символы из linker.ld */
int main(void);
void Reset_Handler(void)
{
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) /* .data: начальные значения из флеш-памяти в ОЗУ */
*dst++ = *src++;
for (dst = &_sbss; dst < &_ebss;) /* .bss: обнулить */
*dst++ = 0;
main();
for (;;) {
}
}
До этих двух циклов ни одна глобальная переменная не имеет правильного
значения. Поэтому Reset_Handler не читает глобальных переменных, а
символы скрипта берёт только по адресу — &_sdata.
Из Reset_Handler возвращаться некуда: после сброса в LR лежит
0xFFFFFFFF. Если main вернётся, ядро перейдёт по этому адресу — в область,
откуда команды не выполняются, — и попадёт в HardFault. В опыте без вечного
цикла так и вышло: HFSR = 0x40000000, CFSR = 0x00000001 — нарушение
выборки команды, поднятое до HardFault, адрес в кадре — 0xFFFFFFFE. Поэтому
после вызова стоит вечный цикл.
Обработчики, которых программа не написала, — слабые псевдонимы одной
функции-ловушки. Стоит определить в программе функцию SysTick_Handler, и
компоновщик возьмёт её, а не псевдоним:
void Default_Handler(void)
{
for (;;) { /* сюда попадает любое прерывание без своего обработчика */
}
}
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));
Таблица векторов — массив указателей в своей секции:
typedef void (*vector_t)(void);
__attribute__((section(".isr_vector"), used))
const vector_t vectors[16] = {
[0] = (vector_t)&_estack, /* начальное значение SP — не функция, а адрес */
[1] = Reset_Handler,
[3] = HardFault_Handler,
[15] = SysTick_Handler,
};
Три листинга подряд — уже целый стартовый файл: он собирается и запускается
с программой из 4в.2. Здесь 16 векторов ядра, из них заполнены четыре;
в startup.c — десять: SP, Reset_Handler и восемь исключений ядра. У настоящей
F407 за ними ещё 82 вектора периферии — в стартовом файле ST их 98 (раздел 4б).
4в.5 Makefile
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
CFLAGS = -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
-Os -g3 -Wall -Wextra -ffreestanding \
-ffunction-sections -fdata-sections
LDFLAGS = -T linker.ld -nostdlib -Wl,--gc-sections -Wl,-Map=build/app.map -Wl,--print-memory-usage
build/app.bin: build/app.elf
$(OBJCOPY) -O binary $< $@
build/app.elf: startup.c main.c linker.ld | build
$(CC) $(CFLAGS) startup.c main.c $(LDFLAGS) -o $@
$(SIZE) $@
build:
mkdir -p build
clean:
rm -rf build
.PHONY: clean
| Ключ | Зачем |
|---|---|
-mcpu=cortex-m4 -mthumb |
ядро и набор команд Thumb — другого у Cortex-M нет |
-mfpu=fpv4-sp-d16 -mfloat-abi=hard |
FPU F4 и передача float в его регистрах |
-ffreestanding |
без ОС: компилятор не считает main и библиотеку особенными |
-ffunction-sections -fdata-sections |
каждая функция и переменная в своей секции |
-T linker.ld |
свой скрипт компоновщика |
-nostdlib |
без библиотеки C и без стартовых файлов компилятора |
-Wl,--gc-sections |
выбросить неиспользуемые секции |
-Wl,-Map=…, --print-memory-usage |
карта памяти и строка заполнения областей |
linker.ld стоит в зависимостях цели: правка скрипта пересобирает образ.
| build — зависимость только по порядку: каталог создаётся до сборки, но
его дата образ не пересобирает. make clean удаляет всё собранное.
Команды в Makefile начинаются с символа табуляции, а не пробелов — в
листинге перед каждой командой стоит именно табуляция. Пробелы вместо неё —
частая ошибка при наборе в простом редакторе.
4в.6 Сборка и проверка: карта, секции, образ
Memory region Used Size Region Size %age Used
FLASH: 480 B 1 MB 0.05%
RAM: 20 B 128 KB 0.02%
text data bss dec hex filename
476 4 16 496 1f0 build/app.elf
Флеш-память — 480 байт: 476 кода с константами и 4 байта начального
значения counter. ОЗУ — 20 байт: 4 байта .data и 16 байт .bss.
Секции с адресами работы (VMA) и хранения (LMA) — objdump -h:
.isr_vector 00000040 08000000 08000000
.text 0000019c 08000040 08000040
.data 00000004 20000000 080001dc
.bss 00000010 20000004 080001e0
У .data адреса разные: работает с 0x20000000, хранится по 0x080001dc —
сразу за кодом. Символы скрипта сходятся:
08000044 T Reset_Handler
080001dc A _sidata
20000000 D _sdata
20000004 D _edata
20000014 B _ebss
20020000 R _estack
Таблица векторов — 0x40 = 64 байта: 16 слов. Первые два слова образа:
000000 20020000 08000045
Начальный SP — 0x20020000, конец ОЗУ. Адрес Reset_Handler —
0x08000045, хотя функция лежит по 0x08000044: младший бит 1 означает
Thumb. Компоновщик ставит его сам, потому что в таблице — указатель на
функцию. Запиши адрес числом 0x08000044, и ядро упадёт в HardFault на
первой команде.
Программа заголовков ELF показывает, что именно попадёт в память:
Entry point 0x8000045
LOAD 0x001000 0x08000000 0x08000000 0x001dc 0x001dc R E 0x1000
LOAD 0x002000 0x20000000 0x080001dc 0x00004 0x00014 RW 0x1000
Второй сегмент — ОЗУ: 4 байта в файле (.data) и 0x14 = 20 байт в памяти
(.data + .bss). Разницу, 16 байт нулей, заполняет стартовый код.
app.bin — 480 байт, ровно размер флеш-памяти из отчёта.
4в.7 Запуск и отладка без среды
Запуск в эмуляторе и вывод программы:
qemu-system-arm -M netduinoplus2 -nographic -kernel build/app.elf -serial stdio
bare metal: hello
counter (.data) = 0x12345678
zeros (.bss) = 0x00000000
&counter = 0x20000000, greeting = 0x080001c6
done
Стартовый код скопировал 0x12345678, обнулил массив. Переменная работает
в ОЗУ, строка осталась во флеш-памяти.
Отладка: эмулятор с ключами -S -gdb tcp::1234 ждёт отладчика, gdb
подключается командой target remote :1234. С платой то же самое, только
вместо QEMU — openocd или st-util, а порт — их:
Reset_Handler () at startup.c:11
sp 0x20020000 0x20020000
pc 0x8000044 0x8000044 <Reset_Handler>
0x8000000 <vectors>: 0x20020000 0x08000045 0x08000041 0x08000041
Breakpoint 1, main () at main.c:38
0x20000000 <counter>: 0x12345678
$1 = {0x0, 0x0, 0x0, 0x0}
sp 0x2001fff8 0x2001fff8
До первой команды ядро само взяло SP и PC из таблицы. На входе в main
counter уже содержит значение, zeros — нули, а стек опустился на 8 байт.
Вектора 2 и 3 — 0x08000041: NMI_Handler и HardFault_Handler — один и тот
же Default_Handler.
4в.8 Две ошибки в скрипте, которые молчат или кричат
Мало памяти — кричит. Тот же проект со скриптом, где ОЗУ — 1 КБ:
RAM too small for .data + .bss + stack
collect2: error: ld returned 1 exit status
Сработал ASSERT: 20 байт данных и 0x800 байт стека в килобайт не лезут.
Без этой строки сборка прошла бы, а программа затёрла бы свои переменные
стеком.
Нет KEEP и ENTRY — молчит. Уберите из скрипта ENTRY(Reset_Handler)
и KEEP у таблицы векторов — и соберите с --gc-sections:
без KEEP и ENTRY: text 0
Ни одной ошибки и пустой образ. Компоновщик ищет, до чего можно дойти по ссылкам от точки входа. Точки входа нет, на таблицу векторов никто не ссылается — и он выбросил всё. Прошивка из 0 байт зальётся на плату без возражений. Поэтому в каждом скрипте есть обе строки, а после сборки смотрят отчёт о памяти.
4в.9 F103 и другие камни
Для F103C8 (BluePill) меняется больше, чем кажется: ключи ядра
-mcpu=cortex-m3 -mthumb без FPU, размеры в MEMORY (FLASH 64K,
RAM 20K), адреса регистров (USART1 — 0x40013800, RCC_APB2ENR —
0x40021018, такт USART1 — бит 14, порта A — бит 2), делитель скорости
(HSI у F1 — 8 МГц: 8 000 000 / 115 200 = 69,4, BRR = 0x45) и настройка
PA9: в GPIOA_CRH (0x40010804) биты 7:4 = 1011 — альтернативный
двухтактный выход 50 МГц. Если нужны библиотека C,
printf и malloc, вместо -nostdlib добавляют --specs=nano.specs и
пишут _sbrk и _write сами — так устроены sysmem.c и syscalls.c в
проекте CubeIDE (раздел 4б). Тогда в скрипте появляются секции
.init_array для конструкторов и символ end для кучи.
4в.10 Грабли
| Симптом | Причина |
|---|---|
| прошивка залита, плата молчит, образ 0 байт | нет KEEP(*(.isr_vector)) или ENTRY, а --gc-sections включён |
| HardFault на первой команде | в таблице адрес без младшего бита: 0x08000044 вместо указателя на функцию |
| глобальная переменная без начального значения | .data без AT > FLASH или стартовый код не копирует её |
в .bss мусор |
стартовый код не обнуляет от _sbss до _ebss |
undefined reference to memcpy или __aeabi_… |
-nostdlib убрал и библиотеку, и libgcc; добавить -lgcc или свои функции |
| переменные портятся при глубоких вызовах | стек налез на данные; нет ASSERT с запасом под стек |
make: *** missing separator |
в команде Makefile пробелы вместо табуляции |
| в QEMU всё печатается, а на плате вывод обрывается на первых символах | регистры объявлены без volatile: ожидание TXE прочитало флаг один раз и крутится вечно |
правка .ld не пересобирает образ |
скрипт не указан в зависимостях цели |
.bss пустая, хотя массив объявлен |
static-массив, в который никто не пишет, компилятор заменил нулём |
| переменная с начальным значением читается нулём или мусором | в скрипте *(.data) без .data.*, а собрано с -fdata-sections: переменная ушла в секцию-сироту |
HardFault на первой операции с float, CFSR = 0x00080000 |
FPU не разрешён в CPACR: ключи -mfpu блок не включают |
Две последние строки — прогоном в QEMU. Скрипт, где в .data взяли
только *(.data), с -fdata-sections собрался без единого предупреждения.
Переменная counter = 0x12345678 ушла в отдельную секцию .data.counter;
компоновщик поставил её в ОЗУ по адресу 0x20000000, но в .data она не
вошла — _sdata и _edata совпали, и стартовый код ничего не скопировал.
Программа напечатала counter = 0x00000000: в QEMU ОЗУ обнулено, на плате
там будет то, что осталось после включения. Ключ компоновщика
--orphan-handling=warn такие секции показывает, но среди десятка служебных.
Программа с float без записи в CPACR напечатала «before float» и упала:
CFSR = 0x00080000 — бит NOCP, сопроцессор недоступен, HFSR = 0x40000000
— отказ поднят до HardFault. С записью CPACR |= 0xF << 20 и барьерами
DSB/ISB перед первым float она дошла до «after float: 3». Ключи
-mfpu=fpv4-sp-d16 -mfloat-abi=hard лишь разрешают компилятору команды
FPU; включает блок стартовый код — в CMSIS это делает SystemInit.
4в.11 Итоги
- Цепочка: препроцессор → компилятор → ассемблер → компоновщик →
objcopy; среда вызывает те же программы. - Скрипт:
ENTRY,MEMORY,SECTIONS;.data—> RAM AT > FLASHиLOADADDR;.bss—NOLOAD;KEEPдля векторов;ASSERTдля места. - Регистры по адресам — только через
volatile, иначе ожидание флага читает его один раз. - Стартовый код на C: копия
.data, обнуление.bss,main; векторы — массив в секции.isr_vector, обработчики — слабые псевдонимы. - Пример: флеш-память 480 байт (476 + 4), ОЗУ 20 байт (4 + 16),
app.bin480 байт. .data: VMA0x20000000, LMA0x080001dc; первые слова образа —0x20020000и0x08000045(бит Thumb).- Без
KEEPиENTRYобраз пустой без единой ошибки; без места в ОЗУASSERTостанавливает сборку.
Полные исходники
linker.ld — скрипт компоновщика с нуля для STM32F405/F407.
/* Раздел 4в: скрипт компоновщика с нуля для STM32F405/F407 — 1 МБ флеш-памяти, 128 КБ ОЗУ.
* Каждая строка отвечает на один вопрос: где лежит, где работает, откуда берутся начальные значения. */
ENTRY(Reset_Handler) /* с чего начинается программа — для отладчика и ELF */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* вершина стека: стек растёт вниз от конца ОЗУ */
_stack_size = 0x800; /* сколько ОЗУ оставить под стек — проверяется в ASSERT */
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector)) /* таблица векторов — первой, с адреса 0x08000000 */
} > FLASH
.text :
{
*(.text .text.*) /* код */
*(.rodata .rodata.*) /* константы и строки */
. = ALIGN(4);
_etext = .;
} > FLASH
.data :
{
_sdata = .; /* где .data работает — адрес в ОЗУ (VMA) */
*(.data .data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH /* а хранится во флеш-памяти сразу за кодом (LMA) */
_sidata = LOADADDR(.data); /* откуда стартовый код копирует начальные значения */
.bss (NOLOAD) :
{
_sbss = .; /* нули: во флеш-памяти не хранятся вовсе */
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}
startup.c — стартовый код на C: таблица векторов и Reset_Handler.
/* Раздел 4в: стартовый код на C — таблица векторов и Reset_Handler без единой строки ассемблера. */
#include <stdint.h>
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss; /* символы из linker.ld */
int main(void);
void Reset_Handler(void)
{
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) /* .data: начальные значения из флеш-памяти в ОЗУ */
*dst++ = *src++;
for (dst = &_sbss; dst < &_ebss;) /* .bss: обнулить */
*dst++ = 0;
main();
for (;;) {
}
}
void Default_Handler(void)
{
for (;;) { /* сюда попадает любое прерывание без своего обработчика */
}
}
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void) __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));
typedef void (*vector_t)(void);
__attribute__((section(".isr_vector"), used))
const vector_t vectors[16] = {
[0] = (vector_t)&_estack, /* начальное значение SP — не функция, а адрес */
[1] = Reset_Handler,
[2] = NMI_Handler,
[3] = HardFault_Handler,
[4] = MemManage_Handler,
[5] = BusFault_Handler,
[6] = UsageFault_Handler,
[11] = SVC_Handler,
[14] = PendSV_Handler,
[15] = SysTick_Handler,
};
main.c — программа без HAL: печать в USART1 и проверка .data и .bss.
/* Раздел 4в: программа без HAL и без библиотеки C — печать в USART1 и проверка,
* что стартовый код скопировал .data и обнулил .bss. */
#include <stdint.h>
#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR (*(volatile uint32_t *)0x40011000)
#define USART1_DR (*(volatile uint32_t *)0x40011004)
#define USART1_BRR (*(volatile uint32_t *)0x40011008)
#define USART1_CR1 (*(volatile uint32_t *)0x4001100C)
static uint32_t counter = 0x12345678; /* .data */
uint32_t zeros[4]; /* .bss; не static — иначе компилятор подставит 0 и секция опустеет */
static const char greeting[] = "bare metal: hello\n"; /* .rodata */
static void putc1(char c)
{
while (!(USART1_SR & (1u << 7))) { /* TXE: передатчик свободен */
}
USART1_DR = (uint32_t)c;
}
static void puts1(const char *s)
{
while (*s)
putc1(*s++);
}
static void puthex(uint32_t v)
{
static const char hex[] = "0123456789abcdef";
puts1("0x");
for (int i = 28; i >= 0; i -= 4)
putc1(hex[(v >> i) & 0xF]);
}
int main(void)
{
RCC_APB2ENR |= 1u << 4; /* такт USART1 */
USART1_BRR = 0x008B; /* 115200 бод при 16 МГц: 16 000 000 / 115 200 = 138,9 */
USART1_CR1 = (1u << 13) | (1u << 3); /* UE — USART включён, TE — передатчик */
puts1(greeting);
puts1("counter (.data) = ");
puthex(counter);
puts1("\nzeros (.bss) = ");
puthex(zeros[0] | zeros[1] | zeros[2] | zeros[3]);
puts1("\n&counter = ");
puthex((uint32_t)&counter);
puts1(", greeting = ");
puthex((uint32_t)greeting);
puts1("\ndone\n");
for (;;) {
}
}
data_main.c — опыт из «Граблей»: переменная в секции-сироте .data.counter.
/* Подраздел 4в.10: переменная с начальным значением и -fdata-sections попадает в секцию
* .data.counter. Скрипт с «*(.data)» её не берёт — она становится сиротой. */
#include <stdint.h>
#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR (*(volatile uint32_t *)0x40011000)
#define USART1_DR (*(volatile uint32_t *)0x40011004)
#define USART1_BRR (*(volatile uint32_t *)0x40011008)
#define USART1_CR1 (*(volatile uint32_t *)0x4001100C)
uint32_t counter = 0x12345678; /* .data.counter при -fdata-sections */
static void putc1(char c)
{
while (!(USART1_SR & (1u << 7))) {
}
USART1_DR = (uint32_t)c;
}
static void puthex(uint32_t v)
{
static const char hex[] = "0123456789abcdef";
putc1('0');
putc1('x');
for (int i = 28; i >= 0; i -= 4)
putc1(hex[(v >> i) & 0xF]);
}
int main(void)
{
RCC_APB2ENR |= 1u << 4;
USART1_BRR = 0x008B;
USART1_CR1 = (1u << 13) | (1u << 3);
const char *s = "counter = ";
while (*s)
putc1(*s++);
puthex(counter);
putc1('\n');
for (;;) {
}
}
fpu_main.c — опыт из «Граблей»: float без разрешения FPU в CPACR.
/* Подраздел 4в.10: FPU включён ключами компилятора, но не разрешён в CPACR.
* Собирается со стартовым кодом и скриптом раздела 4в; с -DENABLE_FPU перед первым float
* пишет CPACR. HardFault_Handler здесь свой: печатает CFSR и HFSR и останавливается. */
#include <stdint.h>
#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR (*(volatile uint32_t *)0x40011000)
#define USART1_DR (*(volatile uint32_t *)0x40011004)
#define USART1_BRR (*(volatile uint32_t *)0x40011008)
#define USART1_CR1 (*(volatile uint32_t *)0x4001100C)
#define SCB_CPACR (*(volatile uint32_t *)0xE000ED88)
#define SCB_CFSR (*(volatile uint32_t *)0xE000ED28)
#define SCB_HFSR (*(volatile uint32_t *)0xE000ED2C)
static void putc1(char c)
{
while (!(USART1_SR & (1u << 7))) {
}
USART1_DR = (uint32_t)c;
}
static void puts1(const char *s)
{
while (*s)
putc1(*s++);
}
static void puthex(uint32_t v)
{
static const char hex[] = "0123456789abcdef";
puts1("0x");
for (int i = 28; i >= 0; i -= 4)
putc1(hex[(v >> i) & 0xF]);
}
void HardFault_Handler(void)
{
puts1("HardFault CFSR=");
puthex(SCB_CFSR);
puts1(" HFSR=");
puthex(SCB_HFSR);
puts1("\n");
for (;;) {
}
}
volatile float k = 1.5f;
int main(void)
{
RCC_APB2ENR |= 1u << 4;
USART1_BRR = 0x008B;
USART1_CR1 = (1u << 13) | (1u << 3);
#ifdef ENABLE_FPU
SCB_CPACR |= 0xFu << 20; /* CP10 и CP11 — полный доступ */
__asm volatile("dsb\n isb");
#endif
puts1("before float\n");
volatile float x = k * 2.0f; /* первая команда FPU */
puts1("after float: ");
putc1((char)('0' + (int)x));
puts1("\n");
for (;;) {
}
}
Дальше: раздел 5 «Три уровня абстракции ST» — CMSIS, HAL и LL.