AVR GCC: секции памяти (что такое .text, .data, .bss, .noinit?) Печать
Добавил(а) microsin   

Здесь представлен перевод документации AVR GCC [1], касающейся секций памяти платформы AVR (будут описаны различные секции, используемые компилятором AVR GCC). Секции иногда также называют сегментами, иногда регионами памяти.

[Общие проблемы при использовании FLASH и SRAM микроконтроллеров AVR]

Все микроконтроллеры AVR имеют память FLASH и память SRAM. Часто считается, что код размещен во FLASH, и данные размещены в SRAM, но это не совсем так.

Для того, чтобы понять, как используются разные виды памяти AVR в Вашем приложении, полезно добавить в makefile вызов утилиты avr-size. К примеру, Вы можете запустить её из директории, где собираете код проекта:

H:\src\nanduino\lcd>avr-size --mcu=at90usb162 --format=avr .build/firmware.elf
AVR Memory Usage
----------------
Device: at90usb162
Program: 1604 bytes (9.8% Full) (.text + .data + .bootloader)
Data: 62 bytes (12.1% Full) (.data + .bss + .noinit)

В отчете программы avr-size можно увидеть имена .text, .data, .bss и .noinit. Это сегменты памяти AVR GCC, вот краткое описание их назначения:

.text, .initN, .finiN Код выполняемой программы (FLASH).
.data, .bss, .noinit Данные переменных программы (SRAM).
.eeprom Данные энергонезависимых переменных (EEPROM).

Секция .text размещена только во FLASH, и представляет Ваш код плюс код инициализации (init code), сгенерированный AVR-GCC. Секция .data размещена как во FLASH, так и в SRAM, и она представляет данные, которые копируются из FLASH в SRAM после сброса или включения питания - это соответствует глобальным переменным, которые имеют инициализацию. Секция .bss появляется только в SRAM, и представляет ячейки SRAM, которые очищаются при сбросе или включении питания - это соответствует глобальным переменным без инициализации.

Реальную работу по копированию данных и инициализации ячеек SRAM при сбросе (включении питания) выполняют функции __do_clear_bss() и __do_copy_data() времени выполнения C. Это код, который выполняется после сброса, но до вызова функции main().

Обычно дешевые микроконтроллеры AVR имеют достаточно памяти FLASH, но довольно мало SRAM. Например, микроконтроллер AT90USB162, используемый в макетных платах AVR-USB162 и Nanduino, имеют 12 килобайт памяти (свободной от USB бутлоадера, которую можно использовать), но только 512 байт SRAM. Таким образом, иногда важна проблема экономии SRAM. К сожалению, SRAM часто используется в программе весьма неочевидными способами. Например:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "supergizmo.h"
#include "lcd.h"
#define STATE_IDLE 0 #define STATE_READING 1 #define STATE_WRITING 2
uint8 state = STATE_IDLE;
int main() { superGizmoInit(); lcdInit(); lcdPrintString("SuperGizmo v1.0\n"); for ( ; ; ) { switch ( state ) { case STATE_IDLE: : break; case STATE_READING: : break; case STATE_WRITING: : break; } } }

Вы могли бы подумать, что этот код использует только 1 байт SRAM для хранения байта состояния uint8 state. Но в действительности строка "SuperGizmo v1.0\n" будет размещена в секции .data, которая подразумевает изначальное хранение во FLASH, и последующее копирование в SRAM после сброса RESET. Эта строка займет 17 байт (15 символов, символ новой строки и терминатор NUL) драгоценной SRAM.

Чтобы обойти проблему, Вам нужно обеспечить хранение символов строки именно во FLASH, и исключить её копирование в SRAM. Вместо вызова:

lcdPrintString("SuperGizmo v1.0\n");

нужно сделать так (подробнее см. [4]):

#include <avr/pgmspace.h>
:
lcdPrintFlashString(PSTR("SuperGizmo v1.0\n"));

В результате строка будет размещена в секции .text вместе с кодом программы, и обращение к строке будет осуществлено через указатель на память программ (program memory pointer). Таким методом можно исключить лишние затраты SRAM, но к сожалению размещение данных в секции .text создает 2 проблемы.

Во-первых, поскольку AVR использует гарвардскую архитектуру, указатель на память программ (FLASH) абсолютно отличается от указателя на память данных (SRAM). AVR имеет специальную машинную инструкцию LPM (Load Program Memory, загрузка из памяти программ) для доступа к данным, сохраненным в FLASH, потому что обычные машинные инструкции для доступа к SRAM тут не работают. В результате нужно написать отдельные реализации функций - одна реализация для работы с указателями на обычные данные (data-memory), другая реализация для работы с указателями (program-memory):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include < avr/pgmspace.h >
void lcdPrintFlashString(const char *str) { char ch = pgm_read_byte(str); while ( ch ) { lcdPrintChar(ch); str++; ch = pgm_read_byte(str); } }
void lcdPrintString(const char *str) { while ( *str ) { lcdPrintChar(*str); str++; } }

Во-вторых, не будет оптимизации по нескольким одинаковым выражениям PSTR("..."). Каждый раз, когда в коде программы будет найдено такое определение строки, для каждого из них будет выделено отдельное место в FLASH, независимо от содержания строки. Многократного дублирования одинаковых данных можно избежать, если декларировать строку в одном месте, отдельно, и потом обращаться к ней по имени несколько раз:

#include <avr/pgmspace.h>
:
const char LONG_LINE[] PROGMEM = "Это очень большая строка, правда-правда!\n";
lcdPrintFlashString(LONG_LINE);
lcdPrintFlashString(LONG_LINE);

Эта техника сохранения статических данных в памяти FLASH полезна не только для символов и строк; её можно использовать для статических данных любого вида. Например, Вы можете написать эффективную подпрограмму вычисления контрольной суммы CRC32, которая использует предварительно скомпилированную таблицу, размещенную в памяти программ:

static const uint32 lookupTable[] PROGMEM =
{
    0x00000000,
    0x04C11DB7,
    0x09823B6E,
    0x0D4326D9,
    :
};
uint32 crc32Calc(const uint8 *buf, uint16 len) { const uint8 *p; uint32 crc = 0xffffffff; for ( p = buf; len > 0; ++p, --len ) { crc = (crc << 8) ^ pgm_read_dword(&lookupTable[(crc >> 24) ^ *p]); } return ~crc; }

[Секция .text]

Секция .text содержит реальные машинные инструкции, которые составляют Вашу программу. В эту секцию также входят секции .initN и .finiN, описанные далее.

Примечание: утилита avr-size (входящая в состав бинарных утилит binutils на платформе Unix, и которая также есть и в WinAVR), не учитывает место кода инициализации .data, входящего в .text. Поэтому чтобы узнать, сколько на самом деле занимает программа во FLASH нужно сложить размер .text и кода инициализации .data (но не .bss). Чтобы узнать занимаемое место в SRAM, нужно сложить размер .data и .bss.

[Секция .data]

Эта секция содержит статические инициализированные данные, которые были определены в Вашем коде. К примеру, следующие строки программы приведут к выделению пространства в сегменте .data:

char err_str[] = "Ваша программа умерла страшной смертью!";
struct point pt = { 1, 1 };

Можно указать линкеру, с какого адреса SRAM будет начинаться секция .data. Это достигается добавлением -Wl,-Tdata,addr к команде avr-gcc, используемой для линковки (link) Вашей программы. Адрес addr должен быть не просто смещением относительно начала SRAM, нужно к реальному адресу SRAM добавить 0x800000, чтобы линкер знал, что адрес принадлежит адресному пространству SRAM. К примеру, если хотите, чтобы секция .data начиналась с 0x1100, нужно линкеру передать адрес 0x801100.

Примечание: когда в программе используете вызовы malloc (они могут даже происходить из кода библиотеки), то понадобятся дополнительные настройки распределения памяти SRAM [2].

[Секция .bss]

В секции .bss появляются неинициализированные глобальные или статические переменные.

[Секция .eeprom]

В секции .eeprom размещаются переменные памяти EEPROM [3].

[Секция .noinit]

Секция .noinit входит в состав секции .bss (является её частью). В секции .noinit размещены переменные, которые заданы следующим образом:

int foo __attribute__ ((section (".noinit")));

Эти переменные не будут проинициализированы нулем при старте, как это делается с обычными данными .bss секции. В секцию .noinit можно поместить только неинициализированные переменные. К примеру, следующий код приведет к ошибке:

int bar __attribute__ ((section (".noinit"))) = 0xaa;

Можно явно указать линкеру, в какое место SRAM поместить секцию .noinit путем добавления в командную строку -Wl,--section-start=.noinit=0x802000 запуска avr-gcc на этапе линковки. Предположим, что Вам нужно поместить секцию .noinit в SRAM по адресу 0x2000:

$ avr-gcc ... -Wl,--section-start=.noinit=0x802000 ...

Примечание: поскольку для чипов AVR используется Гарвардская архитектура, Вам нужно вручную добавить 0x800000 к адресу при передаче его линкеру в качестве начала секции. Иначе линкер подумает, что надо поместить секцию .noinit в секцию .text вместо .data/.bss, и выдаст ошибку.

Альтернативно для автоматизации Вы можете написать свой собственный скрипт линковки.

[Секции .initN (.init0, .init1 и т. д.)]

Эти секции используются для определения кода запуска (startup code), который выполняется при сбросе AVR (включении питания) до входа в функцию main(). Все секции .initN являются составными частями секции .text. Назначение секций .initN - позволить специальным образом разместить некоторые специфичные части кода Вашей программы.

Примечание: иногда удобно думать о секциях .initN и .finiN как о функциях, но в реальности это просто символические имена, которые говорят линкеру, куда приклеить кусок кода, который в реальности не является функцией. Имейте в виду, что примеры для asm и C не могут быть вызваны как функции, в них должен быть произведен прямой переход.

Секции .initN выполняются в порядке от 0 до 9.

.init0:
Неочевидным образом привязано к __init(). Если пользователь определил функцию __init(), то сразу после сброса туда будет произведен безусловный переход.

.init1:
Не используется, задается пользователем.

.init2:
В программах на языке C неочевидным образом привязано к инициализации стека и к очистке регистра нуля __zero_reg__ (r1).

.init3:
Не используется, задается пользователем.

.init4:
Для чипов, у которых память программ ROM больше 64 килобайт, секция .init4 обозначает код, который заботится о копировании содержимого .data из памяти FLASH в память SRAM. Для всех других чипов (у которых размер кода меньше 64 килобайт), этот код, как и код обнуления переменных секции .bss, загружается из libgcc.a.

.init5:
Не используется, задается пользователем.

.init6:
Не используется в программах на C, но используется для конструкторов в программах C++.

.init7:
Не используется, задается пользователем.

.init8:
Не используется, задается пользователем.

.init9:
Делает переход в main().

[Секции .finiN (.fini0, .fini1 и т. д.)]

Эти секции используются для определения кода выхода (exit code), который выполняется после возврата из функции main() или при вызове функции exit(). Все секции .finiN являются составными частями секции .text.

Секции .finiN выполняются в обратном порядке, от 9 до 0.

.fini9:
Не используется, задается пользователем. В этом месте запускается _exit().

.fini8:
Не используется, задается пользователем.

.fini7:
Не используется, задается пользователем.

.fini6:
Не используется для программ на C, но используется для деструкторов программ C++.

.fini5:
Не используется, задается пользователем.

.fini4:
Не используется, задается пользователем.

.fini3:
Не используется, задается пользователем.

.fini2:
Не используется, задается пользователем.

.fini1:
Не используется, задается пользователем.

.fini0:
Переходит в бесконечный цикл после прерывания программы, и после завершения любого кода _exit() (также выполнился код всех секций от .fini9 до .fini1).

[Использование секций в коде ассемблера]

Пример:

#include < avr/io.h >
        .section .init1,"ax",@progbits
        ldi       r0, 0xff
        out       _SFR_IO_ADDR(PORTB), r0
        out       _SFR_IO_ADDR(DDRB), r0

Примечание: выражения "ax", @progbits говорят ассемблеру, что секция выделяемая (allocatable, "a"), выполняемая (executable, "x"), и содержит данные ("@progbits"). Для дополнительной инфрмации по директиве .section см. руководство пользователя.

[Использование секций в коде C]

Пример:

#include < avr/io.h >
void my_init_portb (void) __attribute__ ((naked)) \
    __attribute__ ((section (".init3")));
void
my_init_portb (void)
{
        PORTB = 0xff;
        DDRB = 0xff;
}

Примечание: в этом примере используется секция .init3, чем обеспечивается, что внутренний регистр __zero_reg__ уже установлен. Генерируемый компилятором код может слепо полагаться на то, что __zero_reg__ сброшен в 0.

[Ссылки]

1. AVR Libc memory sections site:nongnu.org.
2. AVR GCC: области памяти и использование malloc().
3. Сохранение констант в EEPROM при использовании WinAVR (GCC).
4. AVR Studio +gcc: как разместить строки и константы на flash.