LINUX.ORG.RU

Осваиваем STM32 снизу: часть 4

 ,


1

1

Часть 1 Часть 2 Часть 3 Часть 4 Часть 5 Часть 6 Часть 7 Часть 8 Часть 9

Часть 4: Начинаем работать с C

Знание ассемблера важно, но многие программы разумней писать на C. В этой части мы напишем простую программу на C, скомпилируем её, исследуем получившийся объектный файл, правильно скомпонуем и запустим. После этого ещё немного изучим gdb.

Вот наша простая программа:

loopc.c

#include <stdint.h>

static const uint32_t loop_start = 0x12345678;
static const uint32_t loop_increment = 3;
static uint32_t loop_value_1 = loop_start;
static uint32_t loop_value_2;

void start(void)
{
    for (;;)
    {
        loop_value_2 = loop_value_1 + loop_increment;
        loop_value_1 = loop_value_2;
    }
}

В этой программе имеются следующие элементы:

  1. Глобальные константы.
  2. Инициализированная глобальная переменная.
  3. Неинициализированная глобальная переменная. Согласно стандарту языка C, неинициализированные глобальные переменные при запуске должны иметь нулевое значение.
  4. Ну и, конечно, код. Обратим внимание, что этот код никогда не завершает своё выполнение.

Скомпилируем эту программу:

arm-none-eabi-gcc -mcpu=cortex-m3 -g -O0 -c -o loopc.o loopc.c

Параметр -mcpu=cortex-m3 указывает компилятору, что мы хотим сгенерировать код для ARM Cortex M3.

Параметр -g указвыает компилятору, что мы хотим добавить в объектный файл отладочную информацию. Она пригодится поздней, когда мы будем запускать программу под отладкой.

Параметр -O0 указывает компилятору, что мы не хотим оптимизировать код. Для реальных программ стоит указывать параметр -Os, -O2 или -O3, в зависимости от того, хотите ли вы оптимизировать выходной код по размеру или скорости. Но для нашего случая оптимизация совсем ни к чему и только помешает.

Параметр -c указывает компилятору, что мы хотим только скомпилировать файл. Без него компилятор вызовет линкер, это нам не нужно.

После компиляции исследуем получившийся объектный файл:

arm-none-eabi-objdump -D loopc.o

loopc.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <start>:
   0:	b480      	push	{r7}
   2:	af00      	add	r7, sp, #0
   4:	4b05      	ldr	r3, [pc, #20]	@ (1c <start+0x1c>)
   6:	681b      	ldr	r3, [r3, #0]
   8:	2203      	movs	r2, #3
   a:	4413      	add	r3, r2
   c:	4a04      	ldr	r2, [pc, #16]	@ (20 <start+0x20>)
   e:	6013      	str	r3, [r2, #0]
  10:	4b03      	ldr	r3, [pc, #12]	@ (20 <start+0x20>)
  12:	681b      	ldr	r3, [r3, #0]
  14:	4a01      	ldr	r2, [pc, #4]	@ (1c <start+0x1c>)
  16:	6013      	str	r3, [r2, #0]
  18:	e7f4      	b.n	4 <start+0x4>
  1a:	bf00      	nop
	...

Disassembly of section .data:

00000000 <loop_value_1>:
   0:	12345678 	eorsne	r5, r4, #120, 12	@ 0x7800000

Disassembly of section .bss:

00000000 <loop_value_2>:
   0:	00000000 	andeq	r0, r0, r0

Disassembly of section .rodata:

00000000 <loop_start>:
   0:	12345678 	eorsne	r5, r4, #120, 12	@ 0x7800000

00000004 <loop_increment>:
   4:	00000003 	andeq	r0, r0, r3

...

Нас интересуют первые четыре секции: .text, .data, .bss и .rodata. Остальные секции содержат отладочную и прочую служебную инфомацию и в конечный файл не попадут.

Как видно, секция .text содержит машинный код нашей функции. Компилятор без оптимизации компилирует весьма многословный код и разбираться в нём мы не будем, хотя в целом там нет ничего сложного.

Секция .data содержит инициализированные глобальные переменные. В нашем случае это переменная loop_value_1.

Секция .bss содержит неинициализированные глобальные переменные. В нашем случае это переменная loop_value_2.

Секция .rodata содержит константы. В нашем случае это константы loop_start и loop_increment.

Теперь давайте подумаем, как эти секции должны располагаться в памяти. Секция .text по смыслу полностью аналогична секции code из второй части. Её мы просто запишем на флеш-память после таблицы векторов. Секция .rodata по сути ничем не отличается, её тоже запишем на флеш-память. Секция .bss содержит переменные. Для работы с переменными нужно использовать SRAM, поэтому секция .bss будет расположена именно там. Также потребуется написать код, который будет инициализировать эту секцию нулевыми байтами.

А вот с секцией .data всё совсем непросто. С одной стороны эта секция содержит переменные, поэтому её нужно расположить в SRAM. С другой стороны эти переменные при старте программы должны быть инициализированы определёнными значениями, которые мы указали в тексте программы. Но когда наша программа запускается, содержимое SRAM не определено, а эти начальные значения можно сохранить только во флеш-памяти.

Решение тут такое:

  1. Секцию .data нужно разместить во флеш-памяти, чтобы там хранились инициализированные значения.
  2. Также секцию .data нужно разместить в SRAM и все адреса в коде должны указывать именно в SRAM.
  3. Необходимо в самом начале работы программы, ещё перед вызовом нашей функции start скопировать секцию .data из флеш-памяти в SRAM.

В итоге мы напишем код на языке ассемблера, который будет вызван в самом начале, обнулит секцию .bss, скопирует секцию .data и передаст управление нашей функции на C.

Вот наш линкер скрипт:

linker.ld:

MEMORY
{
    Flash : ORIGIN = 0x08000000, LENGTH = 64K
    SRAM : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS {
    .isr_vector :
    {
        LONG(0x20000000 + 20K);
        LONG(_reset_exception_handler | 1);
        . = 0x130;
    } > Flash

    .text :
    {
        . = ALIGN(4);
        *(.text)
        . = ALIGN(4);
    } > Flash

    .rodata :
    {
        . = ALIGN(4);
        *(.rodata)
        . = ALIGN(4);
    } > Flash

    .bss : {
        . = ALIGN(4);
        _bss_start = .;
        *(.bss)
        . = ALIGN(4);
        _bss_end = .;
    } > SRAM

    .data : {
        . = ALIGN(4);
        _flash_data_start = LOADADDR(.data);
        _sram_data_start = .;
        *(.data)
        . = ALIGN(4);
        _sram_data_end = .;
    } > SRAM AT> Flash
}

Он уже гораздо сложней предыдущих. Во-первых в нём появился раздел MEMORY, в котором описываются регионы памяти. Без них нам не расположить .data в двух местах. Во-вторых мы добавили множество выходных секций с именами, идентичными входным. В-третьих мы добавили инструкции . = ALIGN(4);, которые выравнивают начало и конец каждой выходной секции по границе, кратной четырём байтам. И в-чётвёртых у нас появилась довольно сложная секция .data.

Для начала разберём подробней описание секции .bss. В ней располагаются переменные, которые нужно инициализировать нулевыми значениями. Давайте разберём её по строкам. . = ALIGN(4); эта строка выравнивает текущий адрес. _bss_start = . эта строка объявляет новый символ _bss_start со значением текущего адреса, т.е. начала выходной секции .bss. *(.bss) эта строка копирует содержимое секций .bss из всех входных файлов в текущую выходную секцию. . = ALIGN(4) эта строка опять выравнивает окончание секции до адреса, кратного четырём. _bss_end = . эта строка объявляет новый символ _bss_end со значением текущего адреса, т.е. конца выходной секции .bss. > SRAM эта строка означает, что все символы, располагающиеся в секции .bss, после компоновки будут указывать в область SRAM.

В дальнейшем мы сможем использовать значения символов _bss_start и _bss_end в коде, который будет копировать начальные значения для переменных из секции .data из флеш-памяти в SRAM.

Теперь перейдём к самой сложной секции .data. Начнём с последней строки: > SRAM AT> Flash. Эта строка означает, что секция располагается в регионе SRAM, но загружается в регион Flash. Что значит «располагается в регионе SRAM»? Это означает, что все символы, располагающиеся в секции .data, после компоновки будут указывать в SRAM. Иными словами, когда наша программа будет менять значения переменной loop_value_1, она будет это делать в SRAM, а не пытаться менять значения во флеш-памяти. А что значит «загружается в регион Flash»? Это означает, что в выходном файле значения, которыми должны инициализироваться переменные, будут записаны во флеш-память.

На выравниваниях не будем останавливаться, тут ничего сложного. Посмотрим на строчку _flash_data_start = LOADADDR(.data). Эта строчка объявляет символ _flash_data_start и со значением адреса начала секции .data во флеш-памяти. Далее идёт строчка _sram_data_start = .. Она объявляет символ _sram_data_start со значением адреса начала секции .data в SRAM. Потом идёт *(.data), с этим синтаксисом мы уже знакомы, все секции с именем .data из всех входных файлов (в нашем случае это только loopc.o) будут скопированы в выходную секцию .data. И наконец идёт строчка _sram_data_end = .. Она объявляет символ _sram_data_end и присваивает ему адрес конца секции .data в SRAM.

В дальнейшем мы сможем использовать значения символов _flash_data_start, _sram_data_start и _sram_data_end в коде, который будет копировать начальные значения для переменных из секции .data из флеш-памяти в SRAM.

Важно понимать, что все эти символы вычисляются линкером во время компоновки конечного файла и в нём будут присутствовать в виде готовых констант. Как, наверное, уже видно, линкер начинает выполнять весьма нетривиальную роль. И если в прошлых программах при большом желании без него можно было бы и обойтись, собрав конечный файл по кусочкам, то в этой программе без линкера никуда.

Выполнение программы начинается с кода, адрес которого обозначен символом _reset_exception_handler, а наш код на C объявляет функцию start. Задача кода _reset_exception_handler состоит в инициализации секций .bss, .data и переходу к start. Стоит отметить, что использование переменных с подчёркиванием в коде на C не рекомендуется, все переменные такого рода считаются зарезервированными для деталей реализации. Именно поэтому мы и объявляем такие символы, в корректном коде на C они не должны появиться. Если бы мы использовали имя reset_exception_handler или flash_data_start, то в коде на C ничего бы не мешало объявить функцию с таким же именем, и получилась бы неприятная коллизия. Конечно в нашем простом случае это не случится, но в общем случае стоит иметь это в виду. Наш линкер и наш будущий код инициализации секций как раз относятся к таким деталям реализации.

Итак пора написать код _reset_exception_handler. Мы это сделаем на языке ассемблера, чтобы к моменту запуска кода на C всё уже было инициализировано и готово к использованию.

reset_exception_handler.s:

.cpu cortex-m3
.syntax unified
.thumb

.global _reset_exception_handler

.text

_reset_exception_handler:
// zero out .bss section
mov r0, #0
ldr r1, =_bss_start
ldr r2, =_bss_end

copy_bss_loop:
cmp r1, r2
bge copy_bss_end
str r0, [r1], #4
b copy_bss_loop
copy_bss_end:

// copy .data section from flash to sram
ldr r0, =_flash_data_start
ldr r1, =_sram_data_start
ldr r2, =_sram_data_end

copy_data_loop:
cmp r1, r2
bge copy_data_end
ldr r3, [r0], #4
str r3, [r1], #4
b copy_data_loop

copy_data_end:
b start

Псевдокод, соответствующий этому коду, выглядит так:

_reset_exception_handler:
// zero out .bss section
r0 := 0
r1 := _bss_start
r2 := _bss_end

copy_bss_loop:
if r1 >= r2 then goto copy_bss_end
memory[r1] := r0
goto copy_bss_loop
copy_bss_end:

// copy .data section from flash to sram
r0 := _flash_data_start
r1 := _sram_data_start
r2 := _sram_data_end

copy_data_loop:
if r1 >= r2 then goto copy_data_end
r3 := memory[r0]
r0 := r0 + 4
memory[r1] := r3
r1 := r1 + 4
goto copy_data_loop

copy_data_end:
goto start

Работа по программированию на этом закончена. Makefile приводить не будем, там всё тривиально. Соберём программу, прошьём её в микроконтроллер и приступим к отладке:

$ make flash
arm-none-eabi-gcc -mcpu=cortex-m3 -g -O0 -c -o loopc.o loopc.c
arm-none-eabi-ld -T linker.ld -o loopc.elf reset_exception_handler.o loopc.o
arm-none-eabi-objcopy -O binary loopc.elf loopc.bin
st-flash --connect-under-reset write loopc.bin 0x08000000
st-flash 1.7.0
...
2023-09-11T23:48:28 INFO common.c: Flash written and verified! jolly good!

$ st-util --connect-under-reset
st-util
2023-09-11T23:50:13 WARN common.c: NRST is not connected
2023-09-11T23:50:13 INFO common.c: F1xx Medium-density: 20 KiB SRAM, 64 KiB flash in at least 1 KiB pages.
2023-09-11T23:50:13 INFO gdb-server.c: Listening at *:4242...
$ arm-none-eabi-gdb
GNU gdb (Arm GNU Toolchain 12.3.Rel1 (Build arm-12.35)) 13.2.90.20230627-git
...
(gdb) target remote 127.0.0.1:4242
Remote debugging using 127.0.0.1:4242
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x08000130 in ?? ()

Значение регистра $pc должно быть равно 0x0800_0130.

Теперь загрузим программу, чтобы gdb мог прочитать символы и отладочную информацию:

(gdb) symbol-file loopc.elf
Reading symbols from loopc.elf...

Задача этой отладочной секции - удостовериться в корректности работы кода по инициализации секций.

Проверим, что сейчас в этих секциях находится:

(gdb) print/z &_bss_start
$5 = 0x20000000
(gdb) print/z &_bss_end
$4 = 0x20000004
(gdb) print/z &_sram_data_start
$2 = 0x20000004
(gdb) print/z &_sram_data_end
$3 = 0x20000008
(gdb) print/z &_flash_data_start
$1 = 0x0800019c
(gdb) x/2z 0x20000000
0x20000000 <loop_value_2>:	0x124beaa5	0x124beaa5
(gdb) x/1z 0x0800019c
0x800019c:	0x12345678

Секция .bss располагается от адреса 0x2000_0000 до адреса 0x2000_0004 (не включительно). Секция .data в SRAM располагается от адреса 0x2000_0004 до адреса 0x2000_0008. По этим адресам в SRAM находятся какие-то странные значения 0x124beaa5. Секция .data во флеш-памяти начинается с адреса 0x0800019c и по этому адресу действительно располагается то значение, которым мы инициализировали переменную loop_start в нашем коде на C.

У gdb есть очень полезная команда display. Мы ей указываем формат и выражение, аналогично команде print, а она распечатывает это выражение при каждом шаге программы. Воспользуемся этой командой для того, чтобы дизассемблировать выполняющийся код:

(gdb) display/6i $pc - 2
1: x/6i $pc - 2
   0x800012e:	movs	r0, r0
=> 0x8000130 <_reset_exception_handler>:	mov.w	r0, #0
   0x8000134 <_reset_exception_handler+4>:	ldr	r1, [pc, #36]	@ (0x800015c <copy_data_end+6>)
   0x8000136 <_reset_exception_handler+6>:	ldr	r2, [pc, #40]	@ (0x8000160 <copy_data_end+10>)
   0x8000138 <copy_bss_loop>:	cmp	r1, r2
   0x800013a <copy_bss_loop+2>:	bge.n	0x8000142 <copy_bss_end>

Можно распознать в этом коде начало кода из файла reset_exception_handler.s. Также можно обратить внимание, что рядом с адресами появились имена символов, которые соответствуют этим адресам.

Сделаем шаг:

(gdb) stepi
0x08000134 in _reset_exception_handler ()
1: x/6i $pc - 2
   0x8000132 <_reset_exception_handler+2>:	movs	r0, r0
=> 0x8000134 <_reset_exception_handler+4>:	ldr	r1, [pc, #36]	@ (0x800015c <copy_data_end+6>)
   0x8000136 <_reset_exception_handler+6>:	ldr	r2, [pc, #40]	@ (0x8000160 <copy_data_end+10>)
   0x8000138 <copy_bss_loop>:	cmp	r1, r2
   0x800013a <copy_bss_loop+2>:	bge.n	0x8000142 <copy_bss_end>
   0x800013c <copy_bss_loop+4>:	str.w	r0, [r1], #4

Как и ожидалось, команда display распечатала обновлённый код. Теперь нажмём <Enter> ничего не вводя:

(gdb)
0x08000136 in _reset_exception_handler ()
1: x/6i $pc - 2
   0x8000134 <_reset_exception_handler+4>:	ldr	r1, [pc, #36]	@ (0x800015c <copy_data_end+6>)
=> 0x8000136 <_reset_exception_handler+6>:	ldr	r2, [pc, #40]	@ (0x8000160 <copy_data_end+10>)
   0x8000138 <copy_bss_loop>:	cmp	r1, r2
   0x800013a <copy_bss_loop+2>:	bge.n	0x8000142 <copy_bss_end>
   0x800013c <copy_bss_loop+4>:	str.w	r0, [r1], #4
   0x8000140 <copy_bss_loop+8>:	b.n	0x8000138 <copy_bss_loop>

Это повторило предыдущую инструкцию и сделало ещё один шаг вперёд.

Распечатаем листинг всего начального кода:

(gdb) x/16i 0x08000130
   0x8000130 <_reset_exception_handler>:	mov.w	r0, #0
   0x8000134 <_reset_exception_handler+4>:	ldr	r1, [pc, #36]	@ (0x800015c <copy_data_end+6>)
=> 0x8000136 <_reset_exception_handler+6>:	ldr	r2, [pc, #40]	@ (0x8000160 <copy_data_end+10>)
   0x8000138 <copy_bss_loop>:	cmp	r1, r2
   0x800013a <copy_bss_loop+2>:	bge.n	0x8000142 <copy_bss_end>
   0x800013c <copy_bss_loop+4>:	str.w	r0, [r1], #4
   0x8000140 <copy_bss_loop+8>:	b.n	0x8000138 <copy_bss_loop>
   0x8000142 <copy_bss_end>:	ldr	r0, [pc, #32]	@ (0x8000164 <copy_data_end+14>)
   0x8000144 <copy_bss_end+2>:	ldr	r1, [pc, #32]	@ (0x8000168 <copy_data_end+18>)
   0x8000146 <copy_bss_end+4>:	ldr	r2, [pc, #36]	@ (0x800016c <copy_data_end+22>)
   0x8000148 <copy_data_loop>:	cmp	r1, r2
   0x800014a <copy_data_loop+2>:	bge.n	0x8000156 <copy_data_end>
   0x800014c <copy_data_loop+4>:	ldr.w	r3, [r0], #4
   0x8000150 <copy_data_loop+8>:	str.w	r3, [r1], #4
   0x8000154 <copy_data_loop+12>:	b.n	0x8000148 <copy_data_loop>
   0x8000156 <copy_data_end>:	b.w	0x8000170 <start>

Можно увидеть, что после того, как _reset_exception_handler обнулит секцию .bss и скопирует нашу секцию .data, он перейдёт на инструкцию по адресу 0x800_0156 copy_data_end . Не будем шагать дальше, а поставим отладочную точку (breakpoint, брейкпоинт) на этот адрес и запустим выполнение программы до остановки:

(gdb) break copy_data_end
Breakpoint 1 at 0x8000156
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) continue
Continuing.

Breakpoint 1, 0x08000156 in copy_data_end ()
1: x/6i $pc - 2
   0x8000154 <copy_data_loop+12>:	b.n	0x8000148 <copy_data_loop>
=> 0x8000156 <copy_data_end>:	b.w	0x8000170 <start>
   0x800015a <copy_data_end+4>:	movs	r0, r0
   0x800015c <copy_data_end+6>:	movs	r0, r0
   0x800015e <copy_data_end+8>:	movs	r0, #0
   0x8000160 <copy_data_end+10>:	movs	r4, r0

Следующей инструкцей мы перейдём уже в скомпилированный код функции start. А перед этим проверим, действительно ли секции .bss и .data в SRAM инициализированы.

(gdb) x/2z 0x20000000
0x20000000 <loop_value_2>:	0x00000000	0x12345678

Видно, что в секции .bss по адресу 0x2000_0000 появился 0, а значение 0x1234_5678 действительно было скопировано с секцию .data. Также gdb знает про символ loop_value_2, т.е. переменную из кода на C.

Теперь перейдём в функцию start:

(gdb) stepi
start () at loopc.c:9
9	{
   0x800016e <copy_data_end+24>:	movs	r0, #0
=> 0x8000170 <start>:	push	{r7}
   0x8000172 <start+2>:	add	r7, sp, #0
   0x8000174 <start+4>:	ldr	r3, [pc, #20]	@ (0x800018c <start+28>)
   0x8000176 <start+6>:	ldr	r3, [r3, #0]
   0x8000178 <start+8>:	movs	r2, #3

Обратите внимание, что произошла очень важная вещь. gdb распечатал название нашей функции start, название файла, где эта функция определена loopc.c и номер строки 9.

Командой list можно распечетать исходный код в окрестности выполняемого кода:

(gdb) list
4	static const uint32_t loop_increment = 3;
5	static uint32_t loop_value_1 = loop_start;
6	static uint32_t loop_value_2;
7
8	void start(void)
9	{
10	    for (;;)
11	    {
12	        loop_value_2 = loop_value_1 + loop_increment;
13	        loop_value_1 = loop_value_2;

Теперь можно убрать отображение дизассемблированного кода и добавить отображение значений переменных из кода на C:

(gdb) delete display 1
(gdb) display loop_value_1
2: loop_value_1 = 305419896
(gdb) display loop_value_2
3: loop_value_2 = 0

Далее будем вместо команды stepi (step instruction) использовать команду step, которая шагает по строкам C, а не отдельным инструкциям.

(gdb) step
12	        loop_value_2 = loop_value_1 + loop_increment;
2: loop_value_1 = 305419896
3: loop_value_2 = 0
(gdb) step
13	        loop_value_1 = loop_value_2;
2: loop_value_1 = 305419896
3: loop_value_2 = 305419899
(gdb) step
12	        loop_value_2 = loop_value_1 + loop_increment;
2: loop_value_1 = 305419899
3: loop_value_2 = 305419899

На этом данную часть можно считать завершённой. Мы научились компилировать и, что куда более важно, компоновать код на C, а также отлаживать его.

Полный код доступен на гитхабе.

★★★★

Проверено: hobbit ()
Последнее исправление: vbr (всего исправлений: 3)

Теперь перейдём к самой сложной секции .data. Начнём с последней строки: > SRAM AT> Flash. Эта строка означает, что секция располагается в регионе SRAM, но загружается в регион Flash. Что значит «располагается в регионе SRAM»? Это означает, что все символы, располагающиеся в секции .bss, после компоновки будут указывать в SRAM.

Здесь опечатка, имелась в виду .data, видимо.

Barracuda72 ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.