AVR Studio +gcc: как разместить строки и константы на flash
Написал microsin   
12.01.2010

Несмотря на указание const при декларации констант, компилятор все равно для их хранения использует ОЗУ (при старте программы они просто копируются из flash в RAM). Это несомненно полезно с точки зрения быстродействия кода, но для программ, активно использующих ОЗУ и/или имеющих большой объем констант (например, строковых), памяти ОЗУ может оказаться недостаточно. Обойти проблему позволяет атрибут PROGMEM и подпрограммы для работы с данными из flash. Их можно использовать, если включить заголовочный файл <avr\pgmspace.h>.

Функции заголовка подробно описаны в C:\WinAVR-20090313\doc\avr-libc\avr-libc-user-manual\pgmspace_8h.html. Принцип работы gcc, описание проблемы подробно описаны в файле C:\WinAVR-20090313\doc\avr-libc\avr-libc-user-manual\pgmspace.html. Далее дан его почти дословный перевод.

[Данные в памяти программ]

Многие микроконтроллеры AVR имеют недостаточно памяти RAM для сохранения в нем данных и констант, однако они менют в своем распоряжении горяздо больший объем памяти программ (flash). Во flash вполне могли бы поместиться константы, что съэкономит драгоценное место в RAM. Однако микроконтроллеры AVR имеют гарвардскую архитектуру, в которой четко разделены память программ (flash) и память данных (RAM), и каждая имеет свое отдельное адресное пространство. Имеется связанная с этим некоторая проблема, чтобы сохранять данные констант во flash, и затем считывать эти данные в программе AVR.

Проблема усугубляется еще тем, что язык C был разработан не для гарвардской архитектуры, а для архитектуры фон Неймана, где код и данные сосуществуют в едином, общем адресном пространстве. Поэтому любой компилятор для гарвардской архитектуры, например AVR, должен иметь разные методы для работы с разными адресными пространствами.

Некоторые компиляторы C (например IAR Embedded Workbench for AVR) используют нестандартные ключевые слова, либо расширяют стандартный синтаксис. Набор инструментов WinAVR/gcc используют другой способ.

Компилятор GCC имеет специальное ключевое слово __attribute__, которе используется для подсоединения различных атрибутов к функциям, определениям, переменным и типам. Это ключевое слово сопровождается спецификацией атрибута в двойных круглых скобках. В AVR GCC имеется специальный атрибут progmem. Он используется при декларации данных, и говорит комилятору поместить данные в памяти программ (flash).

Библиотека AVR-Libc предоставляет простой макрос PROGMEM, который задает синтаксис GCC-атрибута progmem. Эта макрокоманда была создана для удобства конечного пользователя, как мы увидим далее. Макрос PROGMEM задан в заголовочном файле <avr/pgmspace.h>. Поскольку сложно модифицировать GCC для создания нового расширения синтаксиса C, вместо этого avr-libc имеет макросы для получения данных их flash. Они также размещены в заголовке <avr/pgmspace.h>.

[О ключевом слове const]

Многие пользователи полагают, что использование ключевого слова const декларирует размещение данных в памяти программ (flash). Это происходит из-за неверного понимания назначения ключевого слова const.

Ключевое слово const говорит компилятору, что данные "только для чтения", и не более того. Это упрощает для компилятора некоторые преобразования, и предотвращает некорректное использование этих переменных. Например, const используется многими функциями в качестве модификатора типа параметра. Это говорит компилятору, что функция будет использовать этот параметр только для чтения, и не будет изменять содержимое параметра.

Таким образом, const всего лишь указывает на метод использования данных, и совсем не говорит о том, где должны эти данные храниться. Если это слово использовать как средство определить хранение данных, то мы окажемся в проигрыше, так как это изменит его семантику в других ситуациях, например в параметре функции.

[Сохранение данных в памяти программ и получение их оттуда]

Предположим, у Вас есть некоторые глобальные данные:

unsigned char mydata[11][10] =
{
        {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09},
        {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13},
        {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D},
        {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27},
        {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31},
        {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B},
        {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45},
        {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F},
        {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59},
        {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63},
        {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D}
};

И далее код будет получать эти данные, например так:

byte = mydata[i][j];

Теперь Вы хотите сохранить данные в памяти программ (flash). Используйте макрос PROGMEM и поместите его в декларацию переменной, но перед инициализатором:

#include <avr/pgmspace.h>
.
.
.
unsigned char mydata[11][10] PROGMEM =
{
        {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09},
        {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13},
        {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D},
        {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27},
        {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31},
        {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B},
        {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45},
        {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F},
        {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59},
        {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63},
        {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D}
};

Теперь Ваши данные хранятся в памяти программ. Можно скомпилировать, слинковать, и проверить карту памяти - массив mydata будет лежать в правильной секции.

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

Решение проблемы довольно простое. Сначала нам надо получить адрес необходимых данных. Он равен &(mydata[i][j]). После этого можно использовать макрос для чтения данных из памяти программ по этому адресу:

byte = pgm_read_byte(&(mydata[i][j]));

Имеются различные макросы pgm_read_* для чтения данных разного типа и размера. Все они принимают адрес, указывающий на память программ (flash), и возвращают данные, сохраненные по этому адресу. Макросы обеспечивают для этого генерацию корректного кода.

[Сохранение строк в памяти программ и получение их оттуда]

Предположим, у нас есть массив строк:

char *string_table[] =
{
    "String 1",
    "String 2",
    "String 3",
    "String 4",
    "String 5"
};

Теперь добавляем макро PROGMEM:

char *string_table[] PROGMEM =
{
    "String 1",
    "String 2",
    "String 3",
    "String 4",
    "String 5"
};

Верно? Нет! К сожалению, атрибуты GCC затрагивают только объявление, к которому они присоединены. В этом случае мы действительно поместили переменную string_table, т. е. сам массив, в память программ, но не сами строки. Строки так и остались в памяти данных (RAM), что наверное не совсем то, то Вы хотели. Чтобы поместить строки во flash, нужно явно объявить каждую строку:

char string_1[] PROGMEM = "String 1";
char string_2[] PROGMEM = "String 2";
char string_3[] PROGMEM = "String 3";
char string_4[] PROGMEM = "String 4";
char string_5[] PROGMEM = "String 5";

И потом использовать новые символы в массиве:

PGM_P string_table[] PROGMEM =
{
    string_1,
    string_2,
    string_3,
    string_4,
    string_5
};

Теперь мы разместили массив string_table во flash, и массив string_table является массивом указателей на строки. Каждый указатель при этом указывает на строку во flash, где строка и хранится.

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

void foo(void)
{
    char buffer[10];
   
    for (unsigned char i = 0; i < 5; i++)
    {
        strcpy_P(buffer, (PGM_P)pgm_read_word(&(string_table[i])));
       
        // Display buffer on LCD.
    }
    return;
}

Смысл приведенного кода очевиден - получение данных из массива происходит через указатель, выбираемый как 16-битное беззнаковое целое макросом pgm_read_word. Далее строка копируется функцией strcpy_P. Имеется множество функций для манипуляции строками в памяти программ с индексом _P, работающих так же, как и обычные строковые функции. Все эти функции с индексом _P также определены в заголовке <avr/pgmspace.h>.

[Предостережение]

Макрос и функции, используемые для получения данных из flash, генерируют некоторый дополнительный код, который больше по объему, чем код доступа к памяти RAM. Таким образом, это создает дополнительный расход памяти программ и замедление работы кода. Этот дополнительный расход и замедление достаточно малы, поэтому выигрыш при размещении данных во flash получается значительный. Однако об этом необходимо знать, чтобы при необходимости минимизировать количество обращений к памяти внутри одной функции и/или цикла. В этом может помочь поучительный просмотр дизассемблированного кода компилятора.

При написании статьи с удовольствием слушался Jo Manji "Beyond The Sunset" на "Radio Jazz".

Последнее обновление ( 02.08.2010 )