STM32. Процесс компиляции и сборки прошивки

в 14:01, , рубрики: arm-none-eabi, debug, gnu-arm, gnu-gcc, linker, linux, openocd, Preprocessor, st-flash, st-link, stm32, timeweb_статьи, Ubuntu
Многие из начинающих разработчиков софта для микроконтроллеров реализуют свои проекты исключительно в средствах разработки, которые предоставляются производителем. Многое скрыто от пользователя и очень хорошо скрыто, из-за чего некоторые воспринимают эти процессы сродни настоящей магии. Я, в свою очередь, как человек в пытливым умом и природной любознательностью, решил попробовать собрать проект без использования IDE и различного рода автоматизаций.

Так родилась идея для этой статьи: не используя ничего, кроме текстового редактора и командной строки, собрать проект мигания светодиодом на отладочной плате STM32F0-Discovery. Поскольку я не до конца понимал, как происходит процесс компиляции и сборки проекта, мне пришлось восполнять пробелы в знаниях. Разобравшись с этим вопросом, я подумал — а почему бы не рассказать другим об этом?

Всем кому интересно — добро пожаловать под кат! 🙂

STM32. Процесс компиляции и сборки прошивки - 1


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

Набор инструментов

Чтобы получить файл прошивки, который мы в итоге загружаем на микроконтроллер, помимо 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 — как поморгать светодиодом в автоматизированной среде разработки. Но теперь хочется разобраться, как под капотом происходит флоу трансформации исходных кодов в то, что может быть загружено в микроконтроллер.

Я накидал вот такую блок-схему:

STM32. Процесс компиляции и сборки прошивки - 2

На первом этапе подготавливаются исходные коды программ и библиотек. После того, как программа готова — идет процесс формирования файла, пригодного для исполнения на целевом микроконтроллере.

Сначала в работу включается препроцессор и готовит полный текст программы. Далее эта программа передается компилятору, который формирует ассемблерные листинги из которых получаются объектные файлы (либо сразу объектные файлы, минуя этап выдачи ассемблерных листингов), и, по заранее объявленным правилам, линковщик собирает из них выполняемый файл, который потом можно передать в программатор и залить на микроконтроллер. Возможны, конечно, и другие вариации этапов, но такой вариант более пригоден для того, чтобы подробно разобрать весь процесс.

Кажется, все вполне понятно и очевидно. Перейдем к деталям, коснувшись каждого этапа по отдельности.

Пример программы

Проще и нагляднее всего рассматривать работу каждого инструмента по отдельности и на конкретном примере. Давайте создадим простую программу, к которой подключим свою библиотеку.

Суть программы проста — инициализируем минимум необходимой периферии и мигаем светодиодом с помощью простой задержки. Максимально просто.

Создаем файл 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, в первую очередь в работу вступает препроцессор. Его задача — выполнить все указанные в исходном коде директивы, т.е. специальные команды, которые препроцессор распознает и исполняет:

  1. Удаление комментариев из кода;
  2. Подключение файлов через директивы #include, #include_next;
  3. Условное подключение/удаление фрагментов кода: #if, #ifdef, #ifndef, #else, #elif, #endif;
  4. Вывод диагностических сообщении: #error, #warning, #line;
  5. Передача инструкций компилятору: #pragma;
  6. Определение макросов: #define;
  7. Расстановка специальных маркеров, которые помогают передавать указания на конкретные строки (помогает указывать на строки в которых содержатся ошибки);
  8. Прочие служебные функции.

Давайте посмотрим, что получается в ходе работы препроцессора. Для этого необходимо вызвать компилятор с опцией -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 и найдем раздел описания архитектуры:

STM32. Процесс компиляции и сборки прошивки - 3

Видим, что адрес начала SRAM 0x2000 0000, а у Flash — 0x0800 0000. После найдем указание размера этих участков памяти:

STM32. Процесс компиляции и сборки прошивки - 4

Таким образом, размер 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:

STM32. Процесс компиляции и сборки прошивки - 5

Поскольку нам не понадобятся все прерывания от периферии, оставим только самые необходимые. Укажем их с 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();
  
}

Все. Теперь можем с уверенностью стартовать программу на МК. В итоге, если смешать все воедино: компиляцию, расположение секций, инициализацию, заполнение памяти и старт программы — получится следующая картина:

STM32. Процесс компиляции и сборки прошивки - 6

Перейдем к подготовке бинарного файла и заливке его в микроконтроллер.

Компиляция бинарного файла

Для того, чтобы откомпилировать полученные исходные файлы, необходимо выполнить уже знакомую нам команду:

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

К нему можно выполнять два разных типа подключения:

  1. В качестве GBD-клиента по порту 3333, используя для отладки какой-либо внешний софт (например, из IDE);
  2. 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 файле.

Разумеется, все перечисленное выше не часто будет применяться в работе с микроконтроллерами, но для общего развития, я думаю, будет очень полезно понимать, как происходит всё поэтапно и в деталях.


Возможно, захочется почитать и это:

STM32. Процесс компиляции и сборки прошивки - 7

Автор: Андрей

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js