Несмотря на указание 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".
|
Комментарии
2014-09-1014:01:51 Пример с массивом не работает, gcc выдает: предупреждение: атрибут __progmem__ проигнорирован -Wattributes. А так - спасибо, позволяет экономить кучу памяти в строках.
2012-05-0121:01:18 Спасибо большое за статью, расчистил ОЗУ в своем проекте.
Подскажите, если я объявляю два массива констант примерно вот так:
u8 Const1[] =
{
0×1, 0×02, 0×03, 0×04, 0×05, 0×06
};
u8 Const2[] PROGMEM =
{
0×1, 0×02, 0×03, 0×04, 0×05, 0×06
};
то можно ли как-то в процедуре, которой передается указатель на константу, понять в какой памяти она расположена? Т. е. сейчас проблема в том, что пришлось сделать две процедуры, одна работает с константами в ОЗУ, другая с PROGMEM.
microsin: к сожалению, мне неизвестен универсальный способ обращения из одной процедуры и к RAM, и к flash (PROGMEM). Наверное, простого способа сделать это нет, так как адресное пространство и RAM, и flash совершенно разное, и компилятор генерирует для каждого случая индивидуальный код. Т. е. метод адресации выбирается на этапе компиляции. Выбор метода адресации на этапе выполнения все равно будет связан с потерей и кода, и что самое неприятное - быстродействия.
2011-07-1501:49:50 Там в исходниках есть такая вот фича:
/* The real thing. */
# define PSTR(s) (__extension__( {static char __c[] PROGMEM = (s); &__c[0];}))
Эта фича позволяет писать внутри функций так:
char * mystr = PSTR( ":)" );
Небольшая такая автоматизация. Увы, но это мало помогает, если пишёшь для IAR и GCC сразу. В IAR'е очень удобно можно объявлять строки прямо в коде функций. PSTR() тут поможет, но флеш-строки порою используются в флеш-структурах, где, увы, такой фокус уже не проходит.
Это единственная неудобная вещь, когда пишешь код, который одновременно может компилиться в IAR и GCC (на EC++ без специфики).
Если кому интересно, вот недоделанная пока заготовка кросс-компиляторного шаблона:
https://mysvn.ru/cop/Example/
Компилируется в IAR 5.51 и AVR-GCC. Для переключения компилятора комментим три строки в Defines.h. Открывать проект в VS2008 (c WinAVR-20100110) либо в IAR 5.51.
2011-03-2211:37:22 Спасибо за перевод! Но вот хочется разместить не только во Flash, но еще и с заданного адреса во Flash. Может кто-нибудь подсказать как такое сделать в AVR Studio +gcc.
microsin: в документации к Вашему WinAVR все подробно на пальцах описано, как это делать - см. doc\avr-libc\avr-libc-user-manual.pdf, FAQ "How do I relocate code to a fixed address?". Или, как обычно, воспользуйтесь Google, ключевые слова для поиска "__attribute__" section "section-start="
2011-03-1614:27:43 Огромное спасибо!
2011-01-1723:00:14 Спасибо большое, помогли решить проблему!
2010-08-0216:50:34 Огромное вам спасибо за ваши труды, статья помогла при разработке очень важного проекта!!!
2010-04-1912:12:52 Спасибо за грамотный перевод, ибо я в общих чертах понял особенности работы с Flash, при прочтении на английском, но в вашем переводе все значительно упростилось!