Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кармин Новиелло - Освоение STM32.pdf
Скачиваний:
2743
Добавлен:
23.09.2021
Размер:
47.68 Mб
Скачать

Организация памяти

523

20.2.2. Инициализация секций .data и .bss

Давайте произведем небольшую модификацию предыдущего примера.

36 volatile uint32_t dataVar = 0x3f;

37

38int main() {

39/* Разрешение подачи тактирования на периферийные устройства GPIOA и GPIOC */

40*RCC_APB1ENR = 0x1 | 0x4;

41*GPIOA_MODER |= 0x400; // Установка MODER[11:10] = 0x1

42

43while(dataVar == 0x3f) { // Условие цикла всегда истинно

44*GPIOA_ODR = 0x20;

45delay(200000);

46*GPIOA_ODR = 0x0;

47delay(200000);

48}

49}

На этот раз мы используем глобальную инициализированную переменную dataVar, чтобы начать цикл мигания. Переменная была объявлена как volatile просто для того, чтобы компилятор не оптимизировал ее (однако при компиляции этого примера отключите все оптимизации [-ON] в настройках проекта). Взглянув на код, можно прийти к выводу, что он делает то же самое, что и в предыдущем примере. Однако, если вы попытаетесь загрузить его в Nucleo, то увидите, что светодиод LD2 не мигает. Почему так?

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

...

uint32_t globalVar = 0x3f;

void foo() {

volatile uint32_t localVar = 0x4f;

while(localVar--);

}

Здесь у нас две переменные: одна определена в глобальной области видимости, другая

– в локальной. Переменная localVar инициализируется значением 0x4f. Что конкретно при этом происходит? Инициализация выполняется при вызове процедуры foo(), как показано в следующем ассемблерном коде:

1

void foo() {

 

 

 

2

0:

b480

push

{r7}

;Сохранить текущий указатель стекового кадра

3

2:

b083

sub

sp, #12

;Выделить 12 Байт в стеке

4

4:

af00

add

r7, sp, #0

;Сохранить новый указатель стекового кадра

5

 

volatile uint32_t localVar = 0x4f;

 

6

6:

234f

movs

r3, #79

;Поместить 0x4f в r3

 

Организация памяти

 

524

7

8:

607b

str

r3, [r7, #4] ;Записать r3 (то есть 0x4f) в 4-й Байт

8

 

 

 

 

9

 

while(localVar--);

 

10

a:

bf00

nop

 

11

c:

687b

ldr

r3, [r7, #4]

12

e:

1e5a

subs

r2, r3, #1

13

10:

607a

str

r2, [r7, #4]

14

12:

2b00

cmp

r3, #0

15

14:

d1fa

bne.n

c <foo+0xc>

16

}

 

 

 

Строки [2:4] являются прологом функции. Каждая процедура отвечает за выделение своего собственного стекового кадра, сохраняя некоторые внутренние регистры ЦПУ. Это также называется соглашением о вызовах (calling convention), и способ его выполнения определен соответствующим стандартом (в случае процессоров на базе ARM оно опре-

деляется Стандартом вызова процедур архитектуры ARM (ARM Architecture Procedure Call Standard, AAPCS)). Мы не будем вдаваться в подробности данного вопроса, поскольку проанализируем соглашение о вызовах ARM лучше в Главе 24.

Интересующие нас команды в строках [5:6]. В них мы сохраняем значение 0x4f (которое составляет 79 в десятичной системе счисления) в регистре общего назначения R3, а затем перемещаем его содержимое во второе слово в стеке, которое соответствует переменной

localVar13.

Оставшаяся часть ассемблерного кода содержит while(localVar--) и эпилог функции (здесь не показан), который отвечает за восстановление состояния перед возвратом в вызывающую функцию.

Таким образом, соглашение о вызовах определяет, что локальные переменные автоматически инициализируются при вызове функции. А что насчет глобальных переменных? Поскольку они не участвуют в вызывающем процессе, они должны быть инициализированы каким-то определенным кодом при сбросе микроконтроллера (вспомните, что SRAM является энергозависимым, а его содержимое после сброса не определено). Это означает, что мы должны предоставить специальную функцию инициализации.

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

void __initialize_data (unsigned int* flash_begin, unsigned int* data_begin,

 

unsigned int* data_end) {

unsigned

int *p = data_begin;

while (p

< data_end)

*p++ =

*flash_begin++;

}

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

Организация памяти

525

Рисунок 3: Процесс копирования инициализированных данных из Flash-памяти в память SRAM

Прежде чем мы сможем использовать данную процедуру, нам нужно определить несколько других вещей. Прежде всего, нам нужно проинструктировать LD о необходимости хранения значений инициализации для каждой переменной, содержащейся в секции .data, в определенной области Flash-памяти, которая будет соответствовать адресу памяти LMA. Во-вторых, нам необходим способ передачи в функцию __initialize_data() начала и конца секции .data в SRAM (которые мы будем называть _sdata и _edata соответственно) и начального расположения (которое мы будем называть _sidata), где значения инициализации хранятся во Flash-памяти (важно подчеркнуть, что когда мы инициализируем переменную с заданным значением, нам нужно сохранить это значение где-то во Flash-памяти и использовать его для инициализации ячейки SRAM, соответствующей этой переменной). Рисунок 3 схематично отображает этот процесс.

Еще раз: все эти операции могут быть выполнены при помощи скрипта компоновщика, который мы можем модифицировать следующим образом:

25/* Используется при запуске для инициализации данных */

26_sidata = LOADADDR(.data);

27

28.data : ALIGN(4)

29{

30. = ALIGN(4);

31

_sdata = .;

/* создание глобального символьного имени в начале данных */

32

 

 

33*(.data)

34*(.data*)

36. = ALIGN(4);

37

_edata = .;

/* определение глобального символьного имени в конце данных */

38

} >SRAM AT>FLASH

 

Организация памяти

526

Команда в строке 26 определяет переменную _sidata, которая будет содержать адрес LMA секции .data (то есть начальный адрес Flash-памяти, содержащий значения инициализации). Команды в строках [30:31] используют специальный оператор: оператор “.”. Он называется счетчиком адресов (location counter) и является счетчиком, который отслеживает ячейку памяти, достигнутую во время генерации каждой секции. Счетчик адресов независимо отсчитывает ячейку памяти каждой секции памяти (SRAM, Flash-память и т. д.). Например, в приведенном выше коде он начинается с 0x2000 0000, поскольку секция .data является первой, загруженной в SRAM. Когда выполняются две команды *(.data) и *(.data*), счетчик адресов увеличивается на размер всех секций .data, содержащихся в файле. Командой . = ALIGN(4); мы просто заставляем счетчик адресов выравниваться по словам. Итак, подведем итог: _sdata будет содержать 0x2000 0000, а _edata будет равна размеру секции .data (в нашем примере секция .data содержит только одну переменную – dataVar – и, следовательно, ее размер равен 0x2000 0004). Наконец, директива >SRAM AT>FLASH сообщает компоновщику, что адрес VMA секции .data привязан к адресному пространству SRAM (т. е. 0x2000 0000), но адрес LMA (то есть, где хранятся значения инициализации) отображается внутри адресного пространства Flashпамяти.

Благодаря этой новой конфигурации организации памяти мы можем теперь перестроить файл main.c следующим образом:

Имя файла: src/main-ex2.c

22void _start (void);

23int main(void);

24void delay(uint32_t count);

26/* Минимальная таблица векторов */

27uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {

28(uint32_t *)SRAM_END, // указатель начала стека

29(uint32_t *)_start // main в качестве Reset_Handler

30};

31

32// Начальный адрес значений инициализации секции .data,

33// определенный в скрипте компоновщика.

34extern uint32_t _sidata;

35// Начальный адрес секции .data; определен в скрипте компоновщика

36extern uint32_t _sdata;

37// Конечный адрес секции .data; определен в скрипте компоновщика

38extern uint32_t _edata;

39

40

41 volatile uint32_t dataVar = 0x3f;

42

43inline void

44__initialize_data (uint32_t* flash_begin, uint32_t* data_begin,

45

uint32_t* data_end) {

46uint32_t *p = data_begin;

47while (p < data_end)

48*p++ = *flash_begin++;

49}

Организация памяти

527

50

51void __attribute__ ((noreturn,weak))

52_start (void) {

53__initialize_data(&_sidata, &_sdata, &_edata);

54main();

55

56for(;;);

57}

58

59 int main() {

60

61/* Разрешение подачи тактирования на периферийные устройства GPIOA и GPIOC */

62*RCC_APB1ENR = 0x1 | 0x4;

63*GPIOA_MODER |= 0x400; // Установка MODER[11:10] = 0x1

64

65while(dataVar == 0x3f) {

66*GPIOA_ODR = 0x20;

67delay(200000);

68*GPIOA_ODR = 0x0;

69delay(200000);

70}

71}

Точкой входа теперь является процедура _start(), которая используется в качестве обработчика исключения сброса Reset. Когда микроконтроллер сбрасывается, он вызывается автоматически и, в свою очередь, вызывает функцию __initialize_data() с передачей параметров _sidata, _sdata и _edata, вычисленных компоновщиком на этапе компоновки. Затем _start() вызывает процедуру main(), которая работает теперь как положено.

Используя инструмент objdump, мы можем проверить, как организованы секции в ELFфайле.

# ~/STM32Toolchain/gcc-arm/bin/arm-none-eabi-objdump -h nucleo-f401RE.elf

nucleo-f401RE.elf:

file format elf32-littlearm

 

Sections:

 

 

 

 

 

Idx Name

Size

VMA

LMA

File off

Algn

0

.text

000000c0

08000000

08000000

00008000

2**2

 

 

CONTENTS, ALLOC, LOAD, READONLY, CODE

 

1

.data

00000004

20000000

080000c0

00010000

2**2

 

 

CONTENTS, ALLOC, LOAD, DATA

 

 

2

.comment

00000070

00000000 00000000 00010004 2**0

 

 

 

CONTENTS, READONLY

 

 

 

3

.ARM.attributes 00000033 00000000 00000000

00010074 2**0

 

 

CONTENTS, READONLY

 

 

 

Как видите, инструмент подтверждает, что секция .data имеет размер, равный 4 Байт, адрес VMA, равный 0x2000 0000, и адрес LMA, равный 0x0800 00c0, что соответствует концу секции .text.

То же самое относится и к секции .bss, которая зарезервирована для неинициализированных переменных. В соответствии со стандартом ANSI C содержимое этой секции

Организация памяти

528

должно быть инициализировано нулем. Однако секция .bss не имеет соответствующей области Flash-памяти, содержащей все нули, поэтому она опять же зависит от кода запуска, инициализирующего эту область. Следующий фрагмент скрипта компоновщика показывает определение секции .bss14:

25/* Секция неинициализированных данных */

26.bss : ALIGN(4)

27{

28/* Это используется кодом запуска (startup) для инициализации секции .bss */

29

_sbss = .;

/* определение глобального символьного имени начала .bss */

30*(.bss .bss*)

31*(COMMON)

32

33 . = ALIGN(4);

34 _ebss = .; /* определение глобального символьного имени конца .bss */ 35 } >SRAM AT>SRAM

в то время как следующая процедура, всегда вызываемая из _start(), используется для обнуления области .bss в SRAM:

void __initialize_bss (unsigned int* bss_begin, unsigned int* bss_end) { unsigned int *p = bss_begin;

while (p < bss_end) *p++ = 0;

}

Изменение процедуры main() следующим образом позволяет нам убедиться в правильной работе скрипта:

Имя файла: src/main-ex3.c

76volatile uint32_t dataVar = 0x3f;

77volatile uint32_t bssVar;

78

79 int main() {

80

81/* Разрешение подачи тактирования на периферийные устройства GPIOA и GPIOC */

82*RCC_APB1ENR = 0x1 | 0x4;

83*GPIOA_MODER |= 0x400; // Установка MODER[11:10] = 0x1

84

85while(bssVar == 0) {

86*GPIOA_ODR = 0x20;

87delay(200000);

88*GPIOA_ODR = 0x0;

89delay(200000);

90}

91}

14 Обратите внимание, что порядок секций в скрипте компоновщика отражает их порядок расположения в памяти. Если у нас есть две секции с именами A и B, и обе загружаются в SRAM, при этом если секция A определена до B, то она будет помещена в SRAM до B.