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

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

530

Причина, по которой это происходит, очевидна: мы определили две одинаковые глобальные переменные в двух разных файлах с исходным кодом. Но что если мы объявим два символьных имени как неинициализированные глобальные переменные?

Файл A.c

int globalVar[3];

...

Файл B.c

int globalVar[6];

...

Если вы попытаетесь сгенерировать конечный бинарный файл, то обнаружите, что компоновщик не генерирует ошибок. Почему компоновщик не жалуется на оба определения символьных имен? Потому что стандарт Си ничего не говорит о необходимости запрета этого. Но если язык сам по себе позволяет многократно определять глобальную неинициализированную переменную, то сколько же памяти будет выделено? (то есть, globalVar будет массивом, содержащим 3 или 6 элементов?). Данный случай оставляют за реализацией компилятора. Последние версии GCC помещают все неинициализированные глобальные переменные (не объявленные как static) внутрь всей «общей» секции, и объем выделенной на данное символьное имя памяти будет принимать значение наибольшего (в нашем случае, массив будет занимать место шести элементов типа int

– то есть 12 Байт).

Итак, подведем итог: статические глобальные неинициализированные переменные являются локальными для его перемещаемого объекта и, следовательно, помещаются в их секцию .bss; глобальные неинициализированные переменные являются глобальными для всего приложения и помещаются в общую секцию. Предыдущий скрипт компоновщика помещает оба типа глобальных неинициализированных переменных в секцию .bss, которая обнуляется во время выполнения процедурой __initialize_bss().

Это поведение можно изменить, указав опцию -fno-common для команды GCC. GCC разместит глобальные неинициализированные переменные в секцию .data, инициализируя их нулями. Это означает, что если мы объявляем неинициализированный глобальный массив из 1000 элементов, то секция .data будет содержать тысячу раз значение 0: это приведет к потере большого количества Flash-памяти. Поэтому для встроенных приложений лучше избегать использования этой опции командной строки.

20.2.3. Секция .rodata

Программа обычно использует неизменяемые (постоянные) данные. Строки и числовые константы – это всего лишь два примера, при этом большие массивы данных также могут быть инициализированы в виде констант (например, HTML-файл, используемый для создания веб-страниц, может быть сконвертирован в массив с использованием таких инструментов, как команда UNIX xxd). Будучи неизменяемыми, постоянные данные могут быть помещены во внутреннюю Flash-память (или во внешние Flash-памяти, подключенные к микроконтроллеру через интерфейс Quad-SPI) для экономии места в SRAM. Это может быть просто достигнуто путем определения секции .rodata в скрипте компоновщика:

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

531

/* Постоянные данные помещаются во Flash-память */

.rodata : ALIGN(4)

 

{

 

*(.rodata)

/* секции .rodata (константы) */

*(.rodata*)

/* секции .rodata* (строки, и т.п.) */

} >FLASH

 

Например, рассмотрим этот код Си:

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

76const char msg[] = "Hello World!";

77const float vals[] = {3.14, 0.43, 1.414};

79int main() {

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

81*RCC_APB1ENR = 0x1 | 0x4;

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

84while(vals[0] >= 3.14) {

85*GPIOA_ODR = 0x20;

86delay(200000);

87*GPIOA_ODR = 0x0;

88delay(200000);

89}

90}

В нем и строка msg, и массив vals помещаются во Flash-память, как показывает инстру-

мент objdump:

# ~/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

00000590

08000000

08000000

00008000

2**3

 

 

CONTENTS, ALLOC, LOAD, READONLY, CODE

 

1

.rodata

00000024

08000590

08000590

00008590

2**2

 

 

CONTENTS, ALLOC, LOAD, READONLY, DATA

 

2

.comment

00000070

00000000

00000000

000085b4

2**0

 

 

CONTENTS, READONLY

 

 

 

3

.ARM.attributes 00000033 00000000 00000000

00008624 2**0

 

 

CONTENTS, READONLY

 

 

 

Указатели на постоянные данные

Обратите внимание, что объявление строки следующим образом:

char *msg = "Hello World!";

...

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

532

полностью отличается от ее объявления другим способом:

char msg[] = "Hello World!";

...

В первом случае мы объявляем указатель на постоянный массив, и это подразумевает, что в секции .data будет выделено слово для хранения раположения строки "Hello World!" во Flash-памяти. Во втором случае, напротив, мы корректно определяем массив символов. Запомните, что в языке Си массивы – не указатели.

20.2.4. Области Стека и Кучи

На рисунке 1 мы уже видели, что куча и стек являются двумя динамическими областями памяти SRAM, растущие в противоположных направлениях. Стек является нисходящей структурой, которая растет от конца SRAM до конца секции .bss или до конца кучи, если она используется. Куча растет в обратном направлении. Хотя стек является обязательной структурой в Си, куча используется, только если требуется динамическое выделение памяти. В некоторых областях применения (например, в автомобильной сфере) динамическое выделение памяти не используется или, в крайнем случае, настоятельно не рекомендуется ее использование из-за сопутствующего риска. Активное использование кучи приводит к большим потерям производительности, и она является источником возможных утечек и фрагментации памяти.

Однако если вашему приложению необходимо выделять динамически какие-либо части памяти, вы можете использовать классическую процедуру malloc()15 из библиотеки Си. Давайте рассмотрим следующий пример:

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

107int main() {

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

109*RCC_APB1ENR = 0x1 | 0x4;

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

111

112char *heapMsg = (char*)malloc(sizeof(char)*strlen(msg));

113strcpy(heapMsg, msg);

114

115while(strcmp(heapMsg, msg) == 0) {

116*GPIOA_ODR = 0x20;

117delay(200000);

118*GPIOA_ODR = 0x0;

119delay(200000);

120}

121}

Приведенный выше код достаточно прост. heapMsg – указатель на область памяти, динамически выделяемую функцией malloc(). Мы просто копируем содержимое строки msg и проверяем, равны ли обе строки. Если равны, то светодиод LD2 начинает мигать.

15 Однако существуют и другие лучшие альтернативы. Мы рассмотрим их в Главе 23.

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

533

Если вы попытаетесь скомпилировать приведенный выше код, вы увидите следующую ошибку компоновки:

Invoking: Cross ARM C++ Linker arm-none-eabi-g++ ... ./src/ch10/main-ex5.o

/../../../../arm-none-eabi/lib/armv7e-m/libg_nano.a(lib_a-sbrkr.o): In function `_sbrk_r': sbrkr.c:(.text._sbrk_r+0xc): undefined reference to `_sbrk'

collect2: error: ld returned 1 exit status

Что же происходит? Функция malloc() использует процедуру _sbrk(), которая зависит от ОС и архитектуры. Библиотека newlib оставляет за пользователем ответственность за предоставление данной функции. _sbrk() – это процедура, которая принимает количество байт, выделяемых внутри памяти кучи, и возвращает указатель на начало этой непрерывной «порции» памяти. Алгоритм, лежащий в основе функции _sbrk(), довольно прост:

1.Во-первых, необходимо проверить, достаточно ли места для выделения нужного объема памяти. Чтобы выполнить эту задачу, нам нужен способ предоставить процедуре _sbrk() максимальный размер кучи.

2.Если в куче достаточно места для выделения необходимой памяти, то она увеличивает текущий размер кучи и возвращает указатель на начало нового блока памяти.

3.Если в куче недостаточно места (переполнение кучи), то _sbrk() завершается с ошибкой, и пользователь должен реализовать сообщение об ошибке.

Следующий код показывает возможную реализацию процедуры _sbrk(). Давайте проанализируем этот код.

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

81void *_sbrk(int incr) {

82extern uint32_t _end_static; /* определена компоновщиком */

83extern uint32_t _Heap_Limit;

84

85static uint32_t *heap_end;

86uint32_t *prev_heap_end;

88if (heap_end == 0) {

89heap_end = &_end_static;

90}

91prev_heap_end = heap_end;

93#ifdef __ARM_ARCH_6M__ // Если у нас микроконтроллер Cortex-M0/0+

94incr = (incr + 0x3) & (0xFFFFFFFC); /* Это гарантирует, что порции памяти

95

всегда кратны 4 */

96#endif

97if (heap_end + incr > &_Heap_Limit) {

98asm("BKPT");

99}

100

101heap_end += incr;

102return (void*) prev_heap_end;

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

534

Значения _end_static и _Heap_Limit предоставляются компоновщиком и соответствуют концу секции .bss и максимальному адресу памяти для области кучи (то есть _Heap_Limit - _end_static – это размер кучи). Через некоторое время мы увидим, как

 

они определяются в скрипте компоновщика. heap_end – это статически выделенная пе-

 

ременная, которая используется для отслеживания первой свободной ячейки памяти в

 

куче. Поскольку это статическая неинициализированная локальная переменная, в соот-

 

ветствии с таблицей 1 она помещается в секцию .bss и, следовательно, обнуляется во

 

время выполнения. Таким образом, при первом вызове _sbrk() она равна нулю, и, сле-

 

довательно, она инициализируется значением переменной _end_static. Условие if в

 

строке 97 гарантирует, что в памяти кучи достаточно места. Если нет, то вызывается ин-

 

струкция BKPT ассемблера ARM, в результате чего отладчик останавливает выполнение16.

 

Командами в строках [93:96] представлена сложная часть. Макрос препроцессора прове-

 

ряет, является ли архитектура ARM архитектурой ARMv6-M, т.е. архитектурой процессо-

 

ров на базе Cortex-M0/0+. Эти процессоры фактически не позволяют невыровненный до-

 

ступ к памяти. Команда в строке 94 гарантирует, что выделенная память всегда кратна

 

4 Байт.

 

Нам осталось проанализировать скрипт компоновщика. Интересующая нас часть начи-

 

нается со строки 51.

 

Имя файла: src/ ldscript5.ld

 

 

51

_end_static = _ebss;

52

_Heap_Size = 0x190;

53

_Heap_Limit = _end_static + _Heap_Size;

 

 

 

_end_static – это не что иное, как псевдоним (alias) ячейки памяти _ebss, то есть конца

 

секции .bss. _Heap_Size зафиксирован нами и устанавливает размер кучи (400 Байт).

 

Наконец, _Heap_Limit содержит не что иное, как конечный адрес памяти кучи.

Примечание о символьных именах скрипта компоновщика

В данной главе мы широко использовали символьные имена, определенные в скриптах компоновщика из исходного кода Си. Для каждого символьного имени мы определили соответствующую переменную extern uint32_t _symbol. Каждый раз, когда нам необходимо получить доступ к содержимому этого символьного имени, мы используем синтаксис &_symbol. Это может быть источником путаницы.

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

extern uint32_t _symbol; uint32_t symbol_value = _symbol;

16 Здесь мы можем использовать другой способ сообщить о переполнении кучи. Например, можно вызвать глобальную функцию error() и выполнить там соответствующие действия. Тем не менее, это часто лишь стиль программирования, поэтому не стесняйтесь перестраивать этот код под свои нужды.