STM32. Процесс компиляции и сборки прошивки
Простой 30 мин Блог компании Timeweb Cloud Программирование микроконтроллеров *Производство и разработка электроники *Электроника для начинающих Туториал Многие из начинающих разработчиков софта для микроконтроллеров реализуют свои проекты исключительно в средствах разработки, которые предоставляются производителем. Многое скрыто от пользователя и очень хорошо скрыто, из-за чего некоторые воспринимают эти процессы сродни настоящей магии. Я, в свою очередь, как человек в пытливым умом и природной любознательностью, решил попробовать собрать проект без использования IDE и различного рода автоматизаций.
Так родилась идея для этой статьи: не используя ничего, кроме текстового редактора и командной строки, собрать проект мигания светодиодом на отладочной плате STM32F0-Discovery. Поскольку я не до конца понимал, как происходит процесс компиляции и сборки проекта, мне пришлось восполнять пробелы в знаниях. Разобравшись с этим вопросом, я подумал — а почему бы не рассказать другим об этом?
Всем кому интересно — добро пожаловать под кат! 🙂
Дисклеймер. Сразу же хотелось бы оставить за собой право на ошибки, а также размытые и не полные интерпретации вещей, о которых собираюсь рассказать т.к. я не являюсь профессиональным программистом и специалистом в Embedded-системах. Но с другой стороны, любые конструктивные замечания или исправления дадут почву для саморазвития и самокоррекции, и ваша обоснованная обратная связь будет очень ценной для меня.
Содержание статьи:
- 1 ❯ Набор инструментов
- 2 ❯ Процесс билда прошивки
- 3 ❯ Пример программы
- 4 ❯ Препроцессор
- 5 ❯ Компиляция
- 6 ❯ Ассемблерный листинг
- 7 ❯ ELF-файлы
- 8 ❯ Программные секции
- 9 ❯ Таблица символов
- 10 ❯ Линковка
- 11 ❯ Таблица векторов прерываний и Startup-файл
- 12 ❯ Компиляция бинарного файла
- 13 ❯ Дебаггер и выполнение программы по шагам
- 14 ❯ Заключение
❯ Набор инструментов
Чтобы получить файл прошивки, который мы в итоге загружаем на микроконтроллер, помимо IDE, используется определенный набор инструментов. Опишу кратко, что представляет из себя этот набор инструментов.
Поскольку микроконтроллеры STM32 изготовлены с использованием процессорного ядра ARM Cortex-M, одним из вариантов набора используемых инструментов является ARM GNU Toolchain, который сможет подготовить исполняемые файлы на x86/64 платформе для платформы ARM. В составе тулчейна имеется все необходимое: компилятор, ассемблер, линковщик и целая куча других полезных утилит.
Ранее я задавался вопросом: а почему компилятор так называется — gcc-arm-none-eabi? Оказалось, что все эти слова несут за собой вполне конкретный и определенный смысл:
- gcc — это название компилятора;
- arm — целевая архитектура процессора, под которую будет производиться компиляция;
- none — означает, что компилятор не вносит никакого дополнительного bootstrap-кода от себя;
- eabi — сообщает, что код соответствует спецификации двоичного интерфейса EABI.
Помимо набора утилит, для загрузки и проверки полученного исполняемого файла нам понадобится набор утилит ST-Link и сервер отладки OpenOCD.
Процесс установки и настройки тулчейна и программатора с отладчиком под LInux я описал в прошлой статье.
❯ Процесс билда прошивки
Итак. Представим, что вы разобрались в том, как сделать первый микроконтроллерный Hello World — как поморгать светодиодом в автоматизированной среде разработки. Но теперь хочется разобраться, как под капотом происходит флоу трансформации исходных кодов в то, что может быть загружено в микроконтроллер.
Я накидал вот такую блок-схему:
На первом этапе подготавливаются исходные коды программ и библиотек. После того, как программа готова — идет процесс формирования файла, пригодного для исполнения на целевом микроконтроллере.
Сначала в работу включается препроцессор и готовит полный текст программы. Далее эта программа передается компилятору, который формирует ассемблерные листинги из которых получаются объектные файлы (либо сразу объектные файлы, минуя этап выдачи ассемблерных листингов), и, по заранее объявленным правилам, линковщик собирает из них выполняемый файл, который потом можно передать в программатор и залить на микроконтроллер. Возможны, конечно, и другие вариации этапов, но такой вариант более пригоден для того, чтобы подробно разобрать весь процесс.
Кажется, все вполне понятно и очевидно. Перейдем к деталям, коснувшись каждого этапа по отдельности.
❯ Пример программы
Проще и нагляднее всего рассматривать работу каждого инструмента по отдельности и на конкретном примере. Давайте создадим простую программу, к которой подключим свою библиотеку.
Суть программы проста — инициализируем минимум необходимой периферии и мигаем светодиодом с помощью простой задержки. Максимально просто.
Создаем файл main.c:
#include <stdint.h> #include «delay.h» /* Clock */ #define RCC_APB1ENR *((volatile uint32_t*) (0x4002101C)) #define RCC_AHBENR *((volatile uint32_t*) (0x40021014)) /* GPIO C */ #define GPIOC_MODER *((volatile uint32_t*) (0x48000800)) #define GPIOC_ODR *((volatile uint32_t*) (0x48000814)) /* Global initialized variable */ uint32_t loop_enable = 1; int main() { RCC_APB1ENR |= (1 << 28); /* Enable clock on Power Interface */ RCC_AHBENR |= (0x00080014); /* Enable clock on GPIOC */ GPIOC_MODER |= (1 << (9*2)); /* Set GPIO PC9 to Output Mode */ while(loop_enable) { GPIOC_ODR = 0x100; delay(); GPIOC_ODR = 0x200; delay(); } return 0; }
Далее создаем два файла: delay.h и delay.c.
В delay.h вставляем:
#define DELAY_FUNCTIONS_ON #ifdef DELAY_FUNCTIONS_ON /* Simple delay function */ void delay(); #endif
В delay.c вставляем:
#include <stdint.h> /* Global Read-only variable */ const uint32_t DELAY_MAX = 0x7A120; /* Global Uninitialized variable */ uint32_t delay_conter; void delay() { for(delay_conter = DELAY_MAX; delay_conter—;); }
Для начала просто попробуем скомпилировать полученное без каких-либо дополнительных опций:
arm-none-eabi-gcc main.c delay.c -o main.elf
И из этого ничего не выйдет, потому что GCC по умолчанию линкует приложение с libc из newlib и в нем не может найти имплементацию целого вороха функций, которые реализуют системные вызовы:
/opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/lib/libc.a(libc_a-exit.o): in function `exit’: exit.c:(.text.exit+0x28): undefined reference to `_exit’ /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/lib/libc.a(libc_a-writer.o): in function `_write_r’: writer.c:(.text._write_r+0x24): undefined reference to `_write’ /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/lib/libc.a(libc_a-closer.o): in function `_close_r’: closer.c:(.text._close_r+0x18): undefined reference to `_close’ /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/lib/libc.a(libc_a-lseekr.o): in function `_lseek_r’: lseekr.c:(.text._lseek_r+0x24): undefined reference to `_lseek’ /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/lib/libc.a(libc_a-readr.o): in function `_read_r’: readr.c:(.text._read_r+0x24): undefined reference to `_read’ /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/lib/libc.a(libc_a-sbrkr.o): in function `_sbrk_r’: sbrkr.c:(.text._sbrk_r+0x18): undefined reference to `_sbrk’ collect2: error: ld returned 1 exit status
Поскольку мы пишем приложение baremetal, то необходимо явно указать компилятору, чтобы он не использовал стандартные библиотеки, а взял только заглушки для оных:
arm-none-eabi-gcc main.c delay.c -o main.elf -nostdlib
Останется другая ошибка, которую мы пофиксим позже, когда настроим скрипт линковки:
/opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld: warning: cannot find entry symbol _start; defaulting to 00008000
Так. Исходники компилируются. Идём дальше.
❯ Препроцессор
После запуска процесса компиляции в IDE, в первую очередь в работу вступает препроцессор. Его задача — выполнить все указанные в исходном коде директивы, т.е. специальные команды, которые препроцессор распознает и исполняет:
- Удаление комментариев из кода;
- Подключение файлов через директивы #include, #include_next;
- Условное подключение/удаление фрагментов кода: #if, #ifdef, #ifndef, #else, #elif, #endif;
- Вывод диагностических сообщении: #error, #warning, #line;
- Передача инструкций компилятору: #pragma;
- Определение макросов: #define;
- Расстановка специальных маркеров, которые помогают передавать указания на конкретные строки (помогает указывать на строки в которых содержатся ошибки);
- Прочие служебные функции.
Давайте посмотрим, что получается в ходе работы препроцессора. Для этого необходимо вызвать компилятор с опцией -E:
arm-none-eabi-gcc main.c delay.c -nostdlib -E > pp.out
Теперь можно посмотреть, что получилось. Пролистайте файл pp.out — тут можно увидеть много интересного. Он, по своей сути, представляет листинг всего необходимого для работы программы в одном файле.
Первое, на что можно обратить внимание — объявление всех используемых типов данных, которые будут понятны компилятору, причем можно наблюдать те самые маркеры строк из п.7 (см. список выше), в которых указаны те или иные объявления. Приведу некоторые кусочки из этого внушительного списка:
# 41 «/opt/gcc-arm-none-eabi/arm-none-eabi/include/machine/_default_types.h» 3 4 typedef signed char __int8_t; typedef unsigned char __uint8_t; # 55 «/opt/gcc-arm-none-eabi/arm-none-eabi/include/machine/_default_types.h» 3 4 typedef short int __int16_t; typedef short unsigned int __uint16_t; # 77 «/opt/gcc-arm-none-eabi/arm-none-eabi/include/machine/_default_types.h» 3 4 typedef long int __int32_t; typedef long unsigned int __uint32_t; # 103 «/opt/gcc-arm-none-eabi/arm-none-eabi/include/machine/_default_types.h» 3 4 typedef long long int __int64_t; typedef long long unsigned int __uint64_t; # 134 «/opt/gcc-arm-none-eabi/arm-none-eabi/include/machine/_default_types.h» 3 4 typedef signed char __int_least8_t; typedef unsigned char __uint_least8_t; # 160 «/opt/gcc-arm-none-eabi/arm-none-eabi/include/machine/_default_types.h» 3 4 typedef short int __int_least16_t;
Видно, как препроцессор “раздел” константы и подставил в них объявленные значения:
int main() { *((volatile uint32_t*) (0x4002101C)) |= (1 << 28); *((volatile uint32_t*) (0x40021014)) |= (0x00080014); *((volatile uint32_t*) (0x48000800)) |= (1 << (9*2)); while(loop_enable) { *((volatile uint32_t*) (0x48000814)) = 0x100; delay(); *((volatile uint32_t*) (0x48000814)) = 0x200; delay(); } return 0; }
В общем и целом, получается очень наглядное представление того, что получается после сборки исходного кода в один файл. Можно поэкспериментировать с добавлением разных директив препроцессора и посмотреть, какой получается результат.
Например, добавим в код main.c следующее:
#define DUMMY_FUNCTION 0 #ifdef DUMMY_FUNCTION void dummy_defined(){ return 0; } #else void dummy_notdefined(){ return 0; } #endif
При просмотре результата работы препроцессора вы найдете функцию dummy_defined(), но не найдете dummy_notdefined(), и наоборот — если убрать объявление константы DUMMY_FUNCTION то в коде появится функция dummy_notdefined() и пропадет dummy_defined(). Очень наглядный эксперимент, но идём дальше.
❯ Компиляция
Перейдем к этапу компиляции. Просто скомпилировать полученный вывод препроцессора не получится, будет выдана ошибка. И, чтобы скомпилировать полученный результат работы препроцессора, нужно скормить файл c выводом и добавить флаг, что препроцессинг уже пройден:
arm-none-eabi-gcc main.c delay.c -nostdlib -E > main.i arm-none-eabi-gcc main.i -o main.o -nostdlib -fpreprocessed
По итогу, будет сформированы объектные файлы, которые можно отправлять линковщику на компоновку. Но это не все, что нам необходимо. Чтобы был скомпилирован корректный файл, компилятору нужно указать некоторые флаги, которые влияют на конечный результат. Рассмотрим эти параметры.
Архитектура. Для указания целевой архитектуры можно использовать опции -march= или -mcpu= с аргументом cortex-m0. И, поскольку Cortex-M0 поддерживает только набор команд Thumb, обязательно нужно использовать опцию -mthumb. В дополнение к этому, необходимо указать, что работа с числами с плавающей запятой осуществляется софтовым образом (т.к. в Cortex-M0 процессорах нет аппаратного Floating Point Unit) через опцию -mfloat-abi=soft.
Стандарт GNU. Указывается очень просто: -std=gnu11.
Библиотеки. Библиотеки GNU ARM используют newlib для обеспечения стандартной реализации библиотек C. Чтобы уменьшить размер кода и сделать его независимым от аппаратного обеспечения, в микроконтроллерах используется облегченная версия newlib-nano. Однако newlib-nano не предоставляет реализацию низкоуровневых системных вызовов, которые используются стандартными библиотеками C, такими как print() или scan(), но позволяет существенно сократить размер исполняемого файла. Соответственно чтобы использовать библиотеку newlib-nano и nosys нужно добавить следующее: —specs=nano.specs —specs=nosys.specs.
Предупреждения во время компиляции. Чтобы видеть потенциальные ошибки, необходимо включить выдачу всех предупреждений во время компиляции: -Wall.
Уровень отладки. Для того, чтобы включить отладку, нужно добавить флаг -g.
Получится достаточно длинная строка с параметрами:
arm-none-eabi-gcc main.i -o main.o -nostdlib -fpreprocessed -mcpu=cortex-m0 -mthumb -mfloat-abi=soft -std=gnu11 -Wall —specs=nano.specs —specs=nosys.specs -g
❯ Ассемблерный листинг
Также можно рассмотреть результат трансляции нашего кода в код на языке ассемблера, выполнив команду:
arm-none-eabi-gcc -s main.c -o main.s -S -nostdlib -mcpu=cortex-m0 -mthumb -mfloat-abi=soft -std=gnu11 -Wall —specs=nano.specs —specs=nosys.specs
Результат на языке ассемблера .cpu cortex-m0 .arch armv6s-m .fpu softvfp .eabi_attribute 20, 1 .eabi_attribute 21, 1 .eabi_attribute 23, 3 .eabi_attribute 24, 1 .eabi_attribute 25, 1 .eabi_attribute 26, 1 .eabi_attribute 30, 6 .eabi_attribute 34, 0 .eabi_attribute 18, 4 .file «main.c» .text .global loop_enable .data .align 2 .type loop_enable, %object .size loop_enable, 4 loop_enable: .word 1 .text .align 1 .global main .syntax unified .code 16 .thumb_func .type main, %function main: @ args = 0, pretend = 0, frame = 0 @ frame_needed = 1, uses_anonymous_args = 0 push {r7, lr} add r7, sp, #0 ldr r3, .L5 ldr r2, [r3] ldr r3, .L5 movs r1, #128 lsls r1, r1, #21 orrs r2, r1 str r2, [r3] ldr r3, .L5+4 ldr r2, [r3] ldr r3, .L5+4 ldr r1, .L5+8 orrs r2, r1 str r2, [r3] ldr r3, .L5+12 ldr r2, [r3] ldr r3, .L5+12 movs r1, #128 lsls r1, r1, #11 orrs r2, r1 str r2, [r3] b .L2 .L3: ldr r3, .L5+16 movs r2, #128 lsls r2, r2, #1 str r2, [r3] bl delay ldr r3, .L5+16 movs r2, #128 lsls r2, r2, #2 str r2, [r3] bl delay .L2: ldr r3, .L5+20 ldr r3, [r3] cmp r3, #0 bne .L3 movs r3, #0 movs r0, r3 mov sp, r7 @ sp needed pop {r7, pc} .L6: .align 2 .L5: .word 1073877020 .word 1073877012 .word 524308 .word 1207961600 .word 1207961620 .word loop_enable .size main, .-main .ident «GCC: (Arm GNU Toolchain 13.2.rel1 (Build arm-13.7)) 13.2.1 20231009»
Разбирать содержимое мы не будем, но будем помнить, что именно таким образом можно посмотреть ассемблерный исходник, преобразующийся в ELF-файл, который, по идее чтобы стать работоспособным, должен быть правильно слинкован.
❯ ELF-файлы
Следующий этап, который происходит при компиляции — формирование объектных ELF-файлов с разрешением *.o. Рассмотрим содержимое полученного ELF-файла по частям. Кстати, подробнее про ELF-файлы можно почитать тут.
Первый составной элемент ELF-файла, полученного после компиляции — заголовок. Вывести его не сложно:
arm-none-eabi-readelf -h main.o ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2’s complement, little endian Version: 1 (current) OS/ABI: UNIX — System V ABI Version: 0 Type: REL (Relocatable file) Machine: ARM Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 632 (bytes into file) Flags: 0x5000000, Version5 EABI Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 10 Section header string table index: 9
Разбирать структуру и значения полей не будем, иначе статья превратится в книгу. Идем дальше 🙂.
❯ Программные секции
Очень интересную информацию о составе полученного объектного файла можно получить, используя программу arm-none-eabi-objdump. Выполним сначала операцию отдельно для main.c, скомпилировав его:
arm-none-eabi-gcc -c main.c -o main.o -nostdlib -mcpu=cortex-m0 -mthumb -mfloat-abi=soft -std=gnu11 -Wall —specs=nano.specs —specs=nosys.specs
arm-none-eabi-objdump -h main.o main.o: file format elf32-littlearm Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000070 00000000 00000000 00000034 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000004 00000000 00000000 000000a4 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 00000000 00000000 000000a8 2**0 ALLOC 3 .comment 00000045 00000000 00000000 000000a8 2**0 CONTENTS, READONLY 4 .ARM.attributes 0000002c 00000000 00000000 000000ed 2**0 CONTENTS, READONLY
И для файла delay.c:
arm-none-eabi-gcc -c delay.c -o delay.o -nostdlib -mcpu=cortex-m0 -mthumb -mfloat-abi=soft -std=gnu11 -Wall —specs=nano.specs —specs=nosys.specs
arm-none-eabi-objdump -h delay.o delay.o: file format elf32-littlearm Sections: Idx Name Size VMA LMA File off Algn 0 .text 0000002c 00000000 00000000 00000034 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 00000000 00000000 00000060 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000004 00000000 00000000 00000060 2**2 ALLOC 3 .rodata 00000004 00000000 00000000 00000060 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .comment 00000045 00000000 00000000 00000064 2**0 CONTENTS, READONLY 5 .ARM.attributes 0000002c 00000000 00000000 000000a9 2**0 CONTENTS, READONLY
Внесу немного ясности и поясню, что тут выведено. Если говорить простым языком — это вывод информации о том, каким образом разложен код функций, переменные и константы по полученному объектному файлу, который превратится в будущем в прошивку. У каждого полученного объектного файла свой набор программных секций, которые потом соединяются воедино.
Каждая из секций имеет собственное уникальное имя и набор атрибутов, определяющих целый ряд параметров:
- Тип содержимого, который определяет где секция будет размещаться, в памяти программ или данных;
- Тип позиционирования секции в пределах своей области памяти, она может быть абсолютной или относительной;
- Адрес размещения секции, по абсолютному адресу или по смещению;
- Режим соединения одноименных секций из разных исходных файлов;
- Размер самой секции.
Кратко рассмотрим эти секции.
Секция .text. Код и данные. Она содержит код выполняемых инструкций, которые будут располагаться во Flash памяти и константные значения, закодированные в “сырые” байты в конце функций.
Секция .data. Инициализированные переменные. Она содержит переменные, которые проинициализированы на старте и при запуске программы будут перенесены в RAM. Например, переменная uint32_t loop_enable = 1 будет аккурат размещена в этой секции. Размер данной секции обычно равен сумме размеров инициализированных переменных.
Секция .bss. Неинициализированные переменные. Некоторые переменные не имеют значения на старте и нет необходимости сохранять их значения — под них нужно лишь зарезервировать память. Например, переменная uint32_t delay_conter будет размещена в этой секции и будет занимать 4 байта.
Секция .rodata. Данные только для чтения. Содержит переменные с постоянным значением, которые будут сложены во Flash-памяти. Например, в этой секции будет сохранено значение переменной const uint32_t DELAY_MAX = 0x7A120;
Секция .comment. Содержит информацию о версии компилятора.
Секция .ARM.attributes. Содержит служебные сведения, которые в конечной прошивке не используются.
Могут быть сгенерированы так же и другие секции, но мы их пока рассматривать не будем т.к. необходимо будет достаточно глубоко заныривать в процесс работы тулчейна. Поэтому идем дальше.
Для того, чтобы просмотреть содержимое конкретной секции в объектном файле, можно сделать следующее:
arm-none-eabi-objdump -s -j .text main.o main.o: file format elf32-littlearm Contents of section .text: 0000 80b500af 144b1a68 134b8021 49050a43 …..K.h.K.!I..C 0010 1a60124b 1a68114b 11490a43 1a60114b .`.K.h.K.I.C.`.K 0020 1a68104b 8021c902 0a431a60 0be00e4b .h.K.!…C.`…K 0030 80225200 1a60fff7 feff0b4b 80229200 .»R..`…..K.».. 0040 1a60fff7 feff094b 1b68002b efd10023 .`…..K.h.+…# 0050 1800bd46 80bdc046 1c100240 14100240 …F…F…@…@ 0060 14000800 00080048 14080048 00000000 …….H…H….
❯ Таблица символов
Помимо секций, в объектном файле так же имеется таблица символов. Вывести ее не сложно:
arm-none-eabi-objdump —syms main.o main.o: file format elf32-littlearm SYMBOL TABLE: 00000000 l df *ABS* 00000000 main.c 00000000 l d .text 00000000 .text 00000000 l d .data 00000000 .data 00000000 l d .bss 00000000 .bss 00000000 l d .comment 00000000 .comment 00000000 l d .ARM.attributes 00000000 .ARM.attributes 00000000 g O .data 00000004 loop_enable 00000000 g F .text 00000070 main 00000000 *UND* 00000000 delay
Данная таблица содержит информацию, необходимую для поиска и перемещения символьных определений и ссылок программы.
Конечно, это не все, что содержится в ELF-файле, но для общего развития пока этого будет достаточно. Идём дальше.
❯ Линковка
С этим набором объектных файлов, у каждого из которых есть свои секции, необходимо что-то делать, чтобы всё заработало на целевой платформе. Конечно же, если попробовать превратить этот объектный файл в бинарь — ничего работать не будет.
Настало время рассмотреть, что такое линкер. Линкер используется для того, чтобы соединить секции в нескольких объектных файлах и правильно разместить их в памяти.
Например, после компиляции у нас получается следующий набор секций в файле main.o:
main.c —> main.o { .text, .data, .bss, .rodata }
И такой же набор секций в файле delay.o:
delay.c —> delay.o { .text, .data, .bss, .rodata }
Их необходимо правильно “склеить”, чтобы получить финальный исполняемый файл:
blink.elf = main.o + delay.o = { .text = .text(main) + .text(delay)} .data = .data(main) + .data(delay)} .bss = .bss(main) + .bss(delay)} .rodata = .rodata(main) + .rodata(delay)} }
Для этого необходимо написать скрипт линковки и описать в нем, как будет это все размещено в памяти. Давайте по шагам разберем как это делается. Для того, чтобы правильно составить скрипт, необходимо создать файл линковки linker.ld и начать вносить в него содержимое.
Файл состоит из трех секций — ENTRY, MEMORY, SECTIONS. Начнем накидывать наш скрипт линковки, описав каждую из них.
Секция ENTRY. Она сообщает точку входа и указывает первую инструкцию, которая должна быть исполнена. В нашем случае это будет функция reset_handler, которую мы опишем позже:
ENTRY(reset_handler)
Секция MEMORY. Описывает различные участки памяти целевой системы, такие как SRAM и Flash. Откроем Datasheet на STM32F051R8T6 и найдем раздел описания архитектуры:
Видим, что адрес начала SRAM 0x2000 0000, а у Flash — 0x0800 0000. После найдем указание размера этих участков памяти:
Таким образом, размер SRAM у данного микроконтроллера 8KB, размер Flash — 64KB. Укажем это:
MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 8K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K }
В скобках описания отдельного региона памяти указывается режим доступа к памяти:
- x — доступно для исполнения;
- r — доступно для исполнения;
- w — доступно для записи.
Далее указывается аппаратный адрес, где размещается данный блок памяти и его размер. Всё просто.
После необходимо передать указатель адреса “Stack Pointer”, который будет использоваться для инструкций PUSH и POP:
_estack = ORIGIN(RAM) + LENGTH(RAM);
Секция SECTIONS. Создает раскладку содержания секций объектных файлов в памяти и указывает, каким образом данные секций будут расположены, и как будут загружаться. Общий синтаксис описания содержимого данной секции выглядит следующим образом:
SECTIONS { <symbol> = LOADADDR(<symbol>); .<section>: { <symbol> = .; *(.sub_section); . = ALIGN(n); } ><Run Location> [AT> Storage Location] }
Если вспомнить то, о чем я писал в разделе описания секций, получается следующее:
- Секция .isr_vector — это служебная секция, которая не создается по умолчанию и ее необходимо создать вручную. По сути, в ней указывается вектор обработчика прерываний ISR, который должен находиться по адресу 0x0000 0000;
- Секция .text — это исполняемый код, находящийся во Flash;
- Секция .data — это переменные, размещенные в SRAM;
- Секция .rodata — это константы, размещенные в Flash;
- Секция .bss — это объявленные, но не инициализированные переменные, то есть с нулевым значением при старте, которые будут размещены в SRAM.
Поскольку специальных инструкций для указания адреса мы не используем, то секции будут располагаться в порядке их описания:
SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } >FLASH .text : { . = ALIGN(4); *(.text) . = ALIGN(4); _etext = .; } >FLASH .rodata : { . = ALIGN(4); *(.rodata) } >FLASH .data : { . = ALIGN(4); _sdata = .; *(.data) . = ALIGN(4); _edata = .; } >RAM AT> FLASH .bss : { . = ALIGN(4); _sbss = .; *(.bss) . = ALIGN(4); _ebss = .; } >RAM }
В этом скрипте, помимо объявленных секций, также объявляются важные служебные символы:
- _etext — конец секции .text;
- _sdata — старт секции .data;
- _edata — конец секции .data;
- _sbss — старт секции .bss;
- _ebss — конец секции .bss.
К каждому символу идет соответствующее определение, использующее счетчик местоположения памяти. Эти значения мы будем применять в startup-файле, чтобы правильно скопировать данные программы в RAM и занулить область секции .bss. Также, при создании скрипта линковки, необходимо указать инструкции выравнивания по 4-байтовой границе, чтобы предотвратить неверный доступ к памяти и не вызвать исключение, которое приведет к остановке программы.
❯ Таблица векторов прерываний и Startup-файл
Так. С этапом линковки разобрались. Теперь нужно настроить стартовую инициализацию. После сброса микроконтроллера и сигнала BOOT0, выставленного в значение логического нуля, происходит отражение региона памяти Flash 0x0800 0000 на начало адресного пространства 0x0000 0000 и считывается значение по адресу 0x0000 0000, а после это значение подставляется в MSP (указатель основного стека).
Затем контроллер прерываний NVIC начинает отрабатывать вектор RESET, который загружает в регистр PC адрес вектора reset_handler, находящийся по адресу 0x0000 0004 и передает управление ядру. После этого ядро считает команду по адресу, на который указывает регистр PC, и начнет выполнение программы.
В первую очередь, адрес основного указателя стека должен быть сохранен как первое слово в таблице векторов прерываний.
Помимо начального адреса указателя основного стека, таблица векторов прерываний должна содержать 15 слов для системных обработчиков прерываний ядра Cortex-M, и плюсом, столько же слов, сколько периферийных блоков используется в конкретной реализации микроконтроллера, для обработки прерываний и от них тоже. Иногда указываются зарезервированные вектора, то там необходимо проставить нули вместо этих индексов.
Информацию о прерываниях можно найти в Reference Manual на используемый микроконтроллер, в разделе в котором описаны прерывания. Например, для STM32F0 — найти все необходимые значения можно в таблице Vector Table:
Поскольку нам не понадобятся все прерывания от периферии, оставим только самые необходимые. Укажем их с weak-инструкцией, чтобы потом, при необходимости, можно было бы их переобъявить в коде основной программы.
Заметьте, что в коде, приведенном ниже, указан атрибут section, присваивающий содержимое секции isr_vector, чтобы функция гарантировано попала в нужный раздел памяти.
И последнее, что необходимо сделать — реализовать reset_handler который указан в качестве точки входа в скрипте компоновщика. Первым делом необходимо при старте скопировать содержимое .data-секции из Flash в SRAM, начиная с _sdata и заканчивая _edata, начав запись с адреса _etext, а после записать нули в адресное пространство размером отведенным под секцию .bss, с _sbss до _ebss.
Ну и последним шагом нужно вызвать функцию main.
Создадим файл startup.c, который выполнит все, что нам нужно:
#include <stdint.h> #define SRAM_START (0x20000000U) // Адрес начала SRAM #define SRAM_SIZE (8U * 1024U) // Размер SRAM #define SRAM_END (SRAM_START + SRAM_SIZE) // Конец SRAM #define STACK_POINTER_INIT_ADDRESS (SRAM_END) // Указатель стека #define ISR_VECTOR_SIZE_WORDS 48 // Количество векторов прерываний // Объявление векторов прерывания void default_handler(void); void reset_handler(void); void nmi_handler(void) __attribute__((weak, alias(«default_handler»))); void hard_fault_handler(void) __attribute__((weak, alias(«default_handler»))); void svcall_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»))); // Объявим функцию которая будет расположена в секции .isr_vector uint32_t isr_vector[ISR_VECTOR_SIZE_WORDS] __attribute__((section(«.isr_vector»))) = { STACK_POINTER_INIT_ADDRESS, (uint32_t)&reset_handler, (uint32_t)&nmi_handler, (uint32_t)&hard_fault_handler, 0, 0, 0, 0, 0, 0, 0, (uint32_t)&svcall_handler, 0, 0, (uint32_t)&pendsv_handler, (uint32_t)&systick_handler, // Можно продолжить описание остальных периферийных векторов… }; // Объявим обработчик прерываний по умолчанию void default_handler(void) { while(1); } extern uint32_t _etext; extern uint32_t _sdata; extern uint32_t _edata; extern uint32_t _sbss; extern uint32_t _ebss; // Подключаем функцию main extern int main(void); // Объявим функцию которая будет выполнена после сброса и передаст управление в main void reset_handler(void) { // Копируем .data из FLASH в SRAM uint32_t data_size = (uint32_t)&_edata — (uint32_t)&_sdata; uint8_t *flash_data = (uint8_t*) &_etext; uint8_t *sram_data = (uint8_t*) &_sdata; for (uint32_t i = 0; i < data_size; i++) { sram_data[i] = flash_data[i]; } // Заполняем нулями .bss секцию в SRAM uint32_t bss_size = (uint32_t)&_ebss — (uint32_t)&_sbss; uint32_t *bss = (uint32_t*) &_sbss; for (uint32_t i = 0; i < bss_size; i++) { bss[i] = 0; } // Переходим в main-функцию main(); }
Все. Теперь можем с уверенностью стартовать программу на МК. В итоге, если смешать все воедино: компиляцию, расположение секций, инициализацию, заполнение памяти и старт программы — получится следующая картина:
Перейдем к подготовке бинарного файла и заливке его в микроконтроллер.
❯ Компиляция бинарного файла
Для того, чтобы откомпилировать полученные исходные файлы, необходимо выполнить уже знакомую нам команду:
arm-none-eabi-gcc main.c delay.c startup.c -T linker.ld -o blink.elf -nostdlib -mcpu=cortex-m0 -mthumb -mfloat-abi=soft -std=gnu11 -Wall
При компиляции будет выдано предупреждение:
ld: warning: blink.elf has a LOAD segment with RWX permissions
Оно, в нашем случае, является безвредным, и его можно проигнорировать. Теперь можно полученный файл конвертировать в bin-файл:
arm-none-eabi-objcopy -O binary blink.elf blink.bin
И прошить его в наш микроконтроллер:
st-flash write blink.bin 0x08000000
После чего, можем наблюдать, что программа запустилась и светодиод начал радостно моргать. Профит.
❯ Дебаггер и выполнение программы по шагам
Теперь разберем, что получилось. В первую очередь пробежимся еще раз по получившемуся ELF-файлу. Как мы разбирали выше — данный файл является обёрткой для bin-файла и содержит кучу служебной информации, такой как, например, таблица символов. Давайте посмотрим, как это выглядит:
arm-none-eabi-nm blink.elf 0800015c W bus_fault_handler 0800015c W debug_monitor_handler 0800015c T default_handler 08000130 T delay 20000004 B delay_conter 080001e8 R DELAY_MAX 20000008 B _ebss 20000004 D _edata 20002000 D _estack 080001e8 T _etext 0800015c W hard_fault_handler 08000000 D isr_vector 20000000 D loop_enable 080000c0 T main 0800015c W nmi_handler 0800015c W pendsv_handler 08000164 T reset_handler 20000004 B _sbss 20000000 D _sdata 0800015c W svcall_handler 0800015c W systick_handler 0800015c W usage_fault_handler
Пользуясь данной таблицей, можно просмотреть значения переменных или адреса функций. Например, функция reset_handler находится по адресу 0x0800 0164, а обработчик исключительных ситуаций default_handler находится по адресу 0x0800 015c.
Давайте заглянем внутрь бинарного файла. В нем например, можно найти isr_vector по адресу 0x0800 0000 и посмотреть его содержимое:
xxd -g4 -e -s0 -l32 blink.bin
Команда выведет значение группами по 4 байта, используя формат little-endian от 0 до 32 байта:
00000000: 20002000 08000165 0800015d 0800015d . . e…]…]… 00000010: 0800015d 0800015d 00000000 00000000 ]…]………..
Мы видим, что значение указателя стека MSP, находящегося по адресу 0x0000 0000, имеет значение 0x2000 2000 и указывает на конечный адрес RAM. Плюсом, reset_handler, который является точкой входа, записанный по адресу 0x0000 0004 указывает на адрес 0x0800 0165, что на единицу больше, чем это указано в таблице символов. LSB выставленный в логическую единицу указывает, что процессор запускается с набором команд Thumb.
Также, можно посмотреть значение константы DELAY_MAX по адресу 0x0800 01E8, которая используется для задержки:
# cat delay.c | grep DELAY_MAX -m 1 const uint32_t DELAY_MAX = 0x1A120; # xxd -g4 -e -s0x1e8 -l4 blink.bin 000001e8: 0001a120 …
Можно еще раз просмотреть получившийся ассемблерный листинг и сравнить его с тем, который был вначале:
arm-none-eabi-objdump —disassemble blink.elf
Длинный листинг на языке ассемблера blink.elf: file format elf32-littlearm Disassembly of section .text: 080000c0 <main>: 80000c0: b580 push {r7, lr} 80000c2: af00 add r7, sp, #0 80000c4: 4b14 ldr r3, [pc, #80] @ (8000118 <main+0x58>) 80000c6: 681a ldr r2, [r3, #0] 80000c8: 4b13 ldr r3, [pc, #76] @ (8000118 <main+0x58>) 80000ca: 2180 movs r1, #128 @ 0x80 80000cc: 0549 lsls r1, r1, #21 80000ce: 430a orrs r2, r1 80000d0: 601a str r2, [r3, #0] 80000d2: 4b12 ldr r3, [pc, #72] @ (800011c <main+0x5c>) 80000d4: 681a ldr r2, [r3, #0] 80000d6: 4b11 ldr r3, [pc, #68] @ (800011c <main+0x5c>) 80000d8: 4911 ldr r1, [pc, #68] @ (8000120 <main+0x60>) 80000da: 430a orrs r2, r1 80000dc: 601a str r2, [r3, #0] 80000de: 4b11 ldr r3, [pc, #68] @ (8000124 <main+0x64>) 80000e0: 681a ldr r2, [r3, #0] 80000e2: 4b10 ldr r3, [pc, #64] @ (8000124 <main+0x64>) 80000e4: 2180 movs r1, #128 @ 0x80 80000e6: 02c9 lsls r1, r1, #11 80000e8: 430a orrs r2, r1 80000ea: 601a str r2, [r3, #0] 80000ec: e00b b.n 8000106 <main+0x46> 80000ee: 4b0e ldr r3, [pc, #56] @ (8000128 <main+0x68>) 80000f0: 2280 movs r2, #128 @ 0x80 80000f2: 0052 lsls r2, r2, #1 80000f4: 601a str r2, [r3, #0] 80000f6: f000 f81b bl 8000130 <delay> 80000fa: 4b0b ldr r3, [pc, #44] @ (8000128 <main+0x68>) 80000fc: 2280 movs r2, #128 @ 0x80 80000fe: 0092 lsls r2, r2, #2 8000100: 601a str r2, [r3, #0] 8000102: f000 f815 bl 8000130 <delay> 8000106: 4b09 ldr r3, [pc, #36] @ (800012c <main+0x6c>) 8000108: 681b ldr r3, [r3, #0] 800010a: 2b00 cmp r3, #0 800010c: d1ef bne.n 80000ee <main+0x2e> 800010e: 2300 movs r3, #0 8000110: 0018 movs r0, r3 8000112: 46bd mov sp, r7 8000114: bd80 pop {r7, pc} 8000116: 46c0 nop @ (mov r8, r8) 8000118: 4002101c .word 0x4002101c 800011c: 40021014 .word 0x40021014 8000120: 00080014 .word 0x00080014 8000124: 48000800 .word 0x48000800 8000128: 48000814 .word 0x48000814 800012c: 20000000 .word 0x20000000 08000130 <delay>: 8000130: b580 push {r7, lr} 8000132: af00 add r7, sp, #0 8000134: 4a07 ldr r2, [pc, #28] @ (8000154 <delay+0x24>) 8000136: 4b08 ldr r3, [pc, #32] @ (8000158 <delay+0x28>) 8000138: 601a str r2, [r3, #0] 800013a: 46c0 nop @ (mov r8, r8) 800013c: 4b06 ldr r3, [pc, #24] @ (8000158 <delay+0x28>) 800013e: 681b ldr r3, [r3, #0] 8000140: 1e59 subs r1, r3, #1 8000142: 4a05 ldr r2, [pc, #20] @ (8000158 <delay+0x28>) 8000144: 6011 str r1, [r2, #0] 8000146: 2b00 cmp r3, #0 8000148: d1f8 bne.n 800013c <delay+0xc> 800014a: 46c0 nop @ (mov r8, r8) 800014c: 46c0 nop @ (mov r8, r8) 800014e: 46bd mov sp, r7 8000150: bd80 pop {r7, pc} 8000152: 46c0 nop @ (mov r8, r8) 8000154: 0001a120 .word 0x0001a120 8000158: 20000004 .word 0x20000004 0800015c <default_handler>: 800015c: b580 push {r7, lr} 800015e: af00 add r7, sp, #0 8000160: 46c0 nop @ (mov r8, r8) 8000162: e7fd b.n 8000160 <default_handler+0x4> 08000164 <reset_handler>: 8000164: b580 push {r7, lr} 8000166: b088 sub sp, #32 8000168: af00 add r7, sp, #0 800016a: 4a1a ldr r2, [pc, #104] @ (80001d4 <reset_handler+0x70>) 800016c: 4b1a ldr r3, [pc, #104] @ (80001d8 <reset_handler+0x74>) 800016e: 1ad3 subs r3, r2, r3 8000170: 617b str r3, [r7, #20] 8000172: 4b1a ldr r3, [pc, #104] @ (80001dc <reset_handler+0x78>) 8000174: 613b str r3, [r7, #16] 8000176: 4b18 ldr r3, [pc, #96] @ (80001d8 <reset_handler+0x74>) 8000178: 60fb str r3, [r7, #12] 800017a: 2300 movs r3, #0 800017c: 61fb str r3, [r7, #28] 800017e: e00a b.n 8000196 <reset_handler+0x32> 8000180: 693a ldr r2, [r7, #16] 8000182: 69fb ldr r3, [r7, #28] 8000184: 18d2 adds r2, r2, r3 8000186: 68f9 ldr r1, [r7, #12] 8000188: 69fb ldr r3, [r7, #28] 800018a: 18cb adds r3, r1, r3 800018c: 7812 ldrb r2, [r2, #0] 800018e: 701a strb r2, [r3, #0] 8000190: 69fb ldr r3, [r7, #28] 8000192: 3301 adds r3, #1 8000194: 61fb str r3, [r7, #28] 8000196: 69fa ldr r2, [r7, #28] 8000198: 697b ldr r3, [r7, #20] 800019a: 429a cmp r2, r3 800019c: d3f0 bcc.n 8000180 <reset_handler+0x1c> 800019e: 4a10 ldr r2, [pc, #64] @ (80001e0 <reset_handler+0x7c>) 80001a0: 4b10 ldr r3, [pc, #64] @ (80001e4 <reset_handler+0x80>) 80001a2: 1ad3 subs r3, r2, r3 80001a4: 60bb str r3, [r7, #8] 80001a6: 4b0f ldr r3, [pc, #60] @ (80001e4 <reset_handler+0x80>) 80001a8: 607b str r3, [r7, #4] 80001aa: 2300 movs r3, #0 80001ac: 61bb str r3, [r7, #24] 80001ae: e007 b.n 80001c0 <reset_handler+0x5c> 80001b0: 687a ldr r2, [r7, #4] 80001b2: 69bb ldr r3, [r7, #24] 80001b4: 18d3 adds r3, r2, r3 80001b6: 2200 movs r2, #0 80001b8: 701a strb r2, [r3, #0] 80001ba: 69bb ldr r3, [r7, #24] 80001bc: 3301 adds r3, #1 80001be: 61bb str r3, [r7, #24] 80001c0: 69ba ldr r2, [r7, #24] 80001c2: 68bb ldr r3, [r7, #8] 80001c4: 429a cmp r2, r3 80001c6: d3f3 bcc.n 80001b0 <reset_handler+0x4c> 80001c8: f7ff ff7a bl 80000c0 <main> 80001cc: 46c0 nop @ (mov r8, r8) 80001ce: 46bd mov sp, r7 80001d0: b008 add sp, #32 80001d2: bd80 pop {r7, pc} 80001d4: 20000004 .word 0x20000004 80001d8: 20000000 .word 0x20000000 80001dc: 080001e8 .word 0x080001e8 80001e0: 20000008 .word 0x20000008 80001e4: 20000004 .word 0x20000004
Пробежимся по нему дебаггером при выполнении на реальной железке. Запустим сервер отладки OpenOCD, который мы устанавливали в прошлой статье:
openocd -f /usr/local/share/openocd/scripts/interface/stlink.cfg -f /usr/local/share/openocd/scripts/board/stm32f0discovery.cfg
К нему можно выполнять два разных типа подключения:
- В качестве GBD-клиента по порту 3333, используя для отладки какой-либо внешний софт (например, из IDE);
- Telnet-клиентом по порту 4444, отправляя команды отладчику напрямую.
Попробуем оба:
telnet localhost 4444 Trying 127.0.0.1… Connected to localhost. Escape character is ‘^]’. Open On-Chip Debugger > flash write_image erase /home/megalloid/STM32/manual-build/blink.elf device id = 0x20006440 flash size = 64 KiB Adding extra erase range, 0x080001f0 .. 0x080003ff auto erase enabled wrote 496 bytes from file /home/megalloid/STM32/manual-build/blink.elf in 0.078516s (6.169 KiB/s) > reset halt Unable to match requested speed 1000 kHz, using 950 kHz Unable to match requested speed 1000 kHz, using 950 kHz [stm32f0x.cpu] halted due to debug-request, current mode: Thread xPSR: 0xc1000000 pc: 0x08000164 msp: 0x20002000 > resume
Тем временем OpenOCD выведет свои сообщения о происходящем:
Open On-Chip Debugger 0.12.0+dev-01496-gea2e26f7d (2024-01-20-20:28) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Warn : Interface already configured, ignoring Error: already specified hl_layout stlink Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD srst_only separate srst_nogate srst_open_drain connect_deassert_srst Info : Listening on port 6666 for tcl connections Info : Listening on port 4444 for telnet connections Info : clock speed 1000 kHz Info : STLINK V2J37S0 (API v2) VID:PID 0483:3748 Info : Target voltage: 2.874616 Info : [stm32f0x.cpu] Cortex-M0 r0p0 processor detected Info : [stm32f0x.cpu] target has 4 breakpoints, 2 watchpoints Info : [stm32f0x.cpu] Examination succeed Info : starting gdb server for stm32f0x.cpu on 3333 Info : Listening on port 3333 for gdb connections [stm32f0x.cpu] halted due to breakpoint, current mode: Thread xPSR: 0x61000000 pc: 0x080000fa msp: 0x20001fd0 Info : accepting ‘telnet’ connection on tcp/4444 Info : device id = 0x20006440 Info : flash size = 64 KiB Warn : Adding extra erase range, 0x080001f0 .. 0x080003ff Info : Unable to match requested speed 1000 kHz, using 950 kHz Info : Unable to match requested speed 1000 kHz, using 950 kHz [stm32f0x.cpu] halted due to debug-request, current mode: Thread xPSR: 0xc1000000 pc: 0x08000164 msp: 0x20002000
Теперь попробуем отладиться через GDB-клиент. Для этого, в первую очередь, необходимо сбилдить прошивку с debug-опций, которая создаст соответствующие секции и все необходимое для отладки:
arm-none-eabi-gcc main.c delay.c startup.c -T linker.ld -o blink-debug.elf -nostdlib -mcpu=cortex-m0 -mthumb -mfloat-abi=soft -std=gnu11 -Wall -g
После запускаем GDB-отладчик с указанием пути к ELF-файлу, в котором будут содержаться необходимые данные для отладки — такие, например, как таблица символов:
arm-none-eabi-gdb blink-debug.elf
Подключаемся к OpenOCD-серверу:
(gdb) target extended-remote localhost:3333
Запишем в микроконтроллер прошивку и выполним несколько интересных команд:
(gdb) monitor flash write_image erase /home/megalloid/STM32/manual-build/blink.elf Adding extra erase range, 0x080001f0 .. 0x080003ff auto erase enabled wrote 496 bytes from file /home/megalloid/STM32/manual-build/blink.elf in 0.080308s (6.031 KiB/s)
После отправим сигнал на сброс и на старт программы:
(gdb) monitor reset halt Unable to match requested speed 1000 kHz, using 950 kHz Unable to match requested speed 1000 kHz, using 950 kHz [stm32f0x.cpu] halted due to debug-request, current mode: Thread xPSR: 0xc1000000 pc: 0x08000164 msp: 0x20002000 (gdb) monitor resume
Светодиод начнет моргать, но нам интереснее выполнить программу по шагам. Для начала, после сброса, можно прочитать по 8 байтов по адресам 0x0000 0000 и 0x0800 0000:
(gdb) monitor reset halt (gdb) x/2z 0x00000000 0x0: 0x20002000 0x08000165 (gdb) x/2z 0x08000000 0x8000000: 0x20002000 0x08000165
Данной командой x (eXamine) можно прочитать значения памяти по указанному адресу. Через слэш указываем формат вывода, и сообщаем, что хотим прочитать 2 4-байтовых значения и вывести их в 16-ричном формате. По обоим адресам лежат одинаковые данные, и, если верить описанию старта микроконтроллера, в регистре SP должно быть значение 0x2000 2000, а в регистре PC — значение 0x0800 0165:
(gdb) print/z $sp $1 = 0x20002000 (gdb) print/z $pc $2 = 0x08000164
Все верно. Теперь можно выполнить дизассемблирование инструкции, которая сейчас указана в PC-регистре:
(gdb) x/i $pc => 0x800016a <reset_handler+6>: ldr r2, [pc, #104] @ (0x80001d4 <reset_handler+112>)
Добавим breakpoint в функции main и скажем, чтобы она выполнялась по шагам, отправляя команду n:
Длинный отладочный листинг (gdb) monitor reset halt Unable to match requested speed 1000 kHz, using 950 kHz Unable to match requested speed 1000 kHz, using 950 kHz [stm32f0x.cpu] halted due to debug-request, current mode: Thread xPSR: 0xc1000000 pc: 0x08000164 msp: 0x20002000 (gdb) br main Breakpoint 2 at 0x80000c4: file main.c, line 17. (gdb) stepi halted: PC: 0x08000166 halted: PC: 0x08000168 halted: PC: 0x0800016a reset_handler () at startup.c:52 52 uint32_t data_size = (uint32_t)&_edata — (uint32_t)&_sdata; (gdb) n halted: PC: 0x0800016c halted: PC: 0x0800016e halted: PC: 0x08000170 halted: PC: 0x08000172 53 uint8_t *flash_data = (uint8_t*) &_etext; (gdb) n halted: PC: 0x08000174 halted: PC: 0x08000176 54 uint8_t *sram_data = (uint8_t*) &_sdata; (gdb) n halted: PC: 0x08000178 halted: PC: 0x0800017a 56 for (uint32_t i = 0; i < data_size; i++) (gdb) n halted: PC: 0x0800017c halted: PC: 0x0800017e halted: PC: 0x08000196 halted: PC: 0x08000198 halted: PC: 0x0800019a halted: PC: 0x0800019c halted: PC: 0x08000180 58 sram_data[i] = flash_data[i]; (gdb) n halted: PC: 0x08000182 halted: PC: 0x08000184 halted: PC: 0x08000186 halted: PC: 0x08000188 halted: PC: 0x0800018a halted: PC: 0x0800018c halted: PC: 0x0800018e halted: PC: 0x08000190 56 for (uint32_t i = 0; i < data_size; i++) (gdb) n halted: PC: 0x08000192 halted: PC: 0x08000194 halted: PC: 0x08000196 halted: PC: 0x08000198 halted: PC: 0x0800019a halted: PC: 0x0800019c halted: PC: 0x08000180 58 sram_data[i] = flash_data[i]; (gdb) n halted: PC: 0x08000182 halted: PC: 0x08000184 halted: PC: 0x08000186 halted: PC: 0x08000188 halted: PC: 0x0800018a halted: PC: 0x0800018c halted: PC: 0x0800018e halted: PC: 0x08000190 56 for (uint32_t i = 0; i < data_size; i++) (gdb) n halted: PC: 0x08000192 halted: PC: 0x08000194 halted: PC: 0x08000196 halted: PC: 0x08000198 halted: PC: 0x0800019a halted: PC: 0x0800019c halted: PC: 0x08000180 58 sram_data[i] = flash_data[i]; (gdb) n halted: PC: 0x08000182 halted: PC: 0x08000184 halted: PC: 0x08000186 halted: PC: 0x08000188 halted: PC: 0x0800018a halted: PC: 0x0800018c halted: PC: 0x0800018e halted: PC: 0x08000190 56 for (uint32_t i = 0; i < data_size; i++) (gdb) n halted: PC: 0x08000192 halted: PC: 0x08000194 halted: PC: 0x08000196 halted: PC: 0x08000198 halted: PC: 0x0800019a halted: PC: 0x0800019c halted: PC: 0x08000180 58 sram_data[i] = flash_data[i]; (gdb) n halted: PC: 0x08000182 halted: PC: 0x08000184 halted: PC: 0x08000186 halted: PC: 0x08000188 halted: PC: 0x0800018a halted: PC: 0x0800018c halted: PC: 0x0800018e halted: PC: 0x08000190 56 for (uint32_t i = 0; i < data_size; i++) (gdb) n halted: PC: 0x08000192 halted: PC: 0x08000194 halted: PC: 0x08000196 halted: PC: 0x08000198 halted: PC: 0x0800019a halted: PC: 0x0800019c halted: PC: 0x0800019e Breakpoint 1, reset_handler () at startup.c:62 62 uint32_t bss_size = (uint32_t)&_ebss — (uint32_t)&_sbss; (gdb) n halted: PC: 0x080001a0 halted: PC: 0x080001a2 halted: PC: 0x080001a4 halted: PC: 0x080001a6 63 uint8_t *bss = (uint8_t*) &_sbss; (gdb) n halted: PC: 0x080001a8 halted: PC: 0x080001aa 65 for (uint32_t i = 0; i < bss_size; i++) (gdb) n halted: PC: 0x080001ac halted: PC: 0x080001ae halted: PC: 0x080001c0 halted: PC: 0x080001c2 halted: PC: 0x080001c4 halted: PC: 0x080001c6 halted: PC: 0x080001b0 67 bss[i] = 0; (gdb) n halted: PC: 0x080001b2 halted: PC: 0x080001b4 halted: PC: 0x080001b6 halted: PC: 0x080001b8 halted: PC: 0x080001ba 65 for (uint32_t i = 0; i < bss_size; i++) (gdb) n halted: PC: 0x080001bc halted: PC: 0x080001be halted: PC: 0x080001c0 halted: PC: 0x080001c2 halted: PC: 0x080001c4 halted: PC: 0x080001c6 halted: PC: 0x080001b0 67 bss[i] = 0; (gdb) n halted: PC: 0x080001b2 halted: PC: 0x080001b4 halted: PC: 0x080001b6 halted: PC: 0x080001b8 halted: PC: 0x080001ba 65 for (uint32_t i = 0; i < bss_size; i++) (gdb) n halted: PC: 0x080001bc halted: PC: 0x080001be halted: PC: 0x080001c0 halted: PC: 0x080001c2 halted: PC: 0x080001c4 halted: PC: 0x080001c6 halted: PC: 0x080001b0 67 bss[i] = 0; (gdb) n halted: PC: 0x080001b2 halted: PC: 0x080001b4 halted: PC: 0x080001b6 halted: PC: 0x080001b8 halted: PC: 0x080001ba 65 for (uint32_t i = 0; i < bss_size; i++) (gdb) n halted: PC: 0x080001bc halted: PC: 0x080001be halted: PC: 0x080001c0 halted: PC: 0x080001c2 halted: PC: 0x080001c4 halted: PC: 0x080001c6 halted: PC: 0x080001b0 67 bss[i] = 0; (gdb) n halted: PC: 0x080001b2 halted: PC: 0x080001b4 halted: PC: 0x080001b6 halted: PC: 0x080001b8 halted: PC: 0x080001ba 65 for (uint32_t i = 0; i < bss_size; i++) (gdb) n halted: PC: 0x080001bc halted: PC: 0x080001be halted: PC: 0x080001c0 halted: PC: 0x080001c2 halted: PC: 0x080001c4 halted: PC: 0x080001c6 halted: PC: 0x080001c8 70 main(); (gdb) n halted: PC: 0x080000c0 Breakpoint 2, main () at main.c:17 17 RCC_APB1ENR |= (1 << 28); /* Enable clock on Power Interface */ (gdb) n halted: PC: 0x080000c6 halted: PC: 0x080000c8 halted: PC: 0x080000ca halted: PC: 0x080000cc halted: PC: 0x080000ce halted: PC: 0x080000d0 halted: PC: 0x080000d2 18 RCC_AHBENR |= (0x00080014); /* Enable clock on GPIOC */ (gdb) n halted: PC: 0x080000d4 halted: PC: 0x080000d6 halted: PC: 0x080000d8 halted: PC: 0x080000da halted: PC: 0x080000dc halted: PC: 0x080000de 20 GPIOC_MODER |= (1 << (9*2)); /* Set GPIO PC9 to Output Mode */ (gdb) n halted: PC: 0x080000e0 halted: PC: 0x080000e2 halted: PC: 0x080000e4 halted: PC: 0x080000e6 halted: PC: 0x080000e8 halted: PC: 0x080000ea halted: PC: 0x080000ec 22 while(loop_enable) { (gdb) n halted: PC: 0x08000106 halted: PC: 0x08000108 halted: PC: 0x0800010a halted: PC: 0x0800010c halted: PC: 0x080000ee 24 GPIOC_ODR = 0x100; (gdb) n halted: PC: 0x080000f0 halted: PC: 0x080000f2 halted: PC: 0x080000f4 halted: PC: 0x080000f6 25 delay(); (gdb) n halted: PC: 0x08000130 ^[[A27 GPIOC_ODR = 0x200; (gdb) n halted: PC: 0x080000fc halted: PC: 0x080000fe halted: PC: 0x08000100 halted: PC: 0x08000102 28 delay(); (gdb)
❯ Заключение
Казалось бы, зачем все эти заморочки, ведь современные IDE могут всё это генерить автоматом и не потребуется никаких ковыряний и такого объема работ. Но с другой стороны — теперь предельно ясно, что происходит под капотом, и как это все хозяйство собирается.
Мы разобрались в тонкостях процесса компиляции и того, что происходит перед началом выполнения программы, которую мы пишем в main.c файле.
Разумеется, все перечисленное выше не часто будет применяться в работе с микроконтроллерами, но для общего развития, я думаю, будет очень полезно понимать, как происходит всё поэтапно и в деталях.
Возможно, захочется почитать и это:
- ➤ Создаем I2C Master Controller на Verilog. Проверим работу на реальном железе
- ➤ Универсальная плата для E-Ink экранов. Ч1. Разработка системы питания
- ➤ Оживляем автоинформатор из подмосковного автобуса
- ➤ Полупроводниковая монополия: как одна голландская компания держит под контролем мировую индустрию микросхем
- ➤ Как создавалась Call of Duty 2
Теги:
- timeweb_статьи
- STM32
- linux
- ubuntu
- openocd
- st-link
- st-flash
- debug
- preprocessor
- linker
- gnu-arm
- gnu-gcc
- arm-none-eabi
Хабы:
- Блог компании Timeweb Cloud
- Программирование микроконтроллеров
- Производство и разработка электроники
- Электроника для начинающих
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку Задонатить