В программе у меня есть множество массивов строк разной длины, и каждый массив объявляется как массив указателей на эти строки, например:
static const char * num_tab[] = {"First", "Second", "Third"};
static const char * day_tab[] = {"Sunday", "Monday", "Tuesday"};
static const char * random_tab[] = {"Strings and arrays can have", "diferent", "lenghts"};
Строки (указатель на) возвращаются из простых функций, таких как:
const char * dayName(int index) {
return day_tab[index];
}
В архитектуре AVR эти строки должны храниться в памяти программы. Я понимаю, что функции необходимо изменить, чтобы они работали также и на AVR (им нужно скопировать строку из памяти программы в буфер в оперативной памяти и вместо этого вернуть на нее указатель).
Как я могу изменить инициализацию массивов для использования PROGMEM без необходимости называть каждую отдельную строку?
Единственный способ, который я нашел, — это определить каждую строку с именем (и PROGMEM) и определить массив указателей, инициализированный указателями на эти строки:
static const char d1[] PROGMEM = "First";
static const char d2[] PROGMEM = "Second";
static const char d3[] PROGMEM = "Third";
const char * const day_tab[] = {d1, d2, d3}; // only needs PROGMEM for large arrays
Это работает, но для больших массивов разного размера код меняется с нескольких строк на сотни, что делает обслуживание практически невозможным. Кроме того, добавление или удаление значения из массива потребует перенумерации всех следующих элементов.
Не static const char PROGMEM * const num_tab[] = { "First", "Second", "Third"}; работает?
@TedLyngmo: нет, это поместит массив указателей в программную память, а строки будут в оперативную память. На самом деле не имеет значения, куда вы поместите макрос PROGMEM.
@JohnBode: Я ищу способ заставить его работать без слишком большого изменения кода или использования внешних скриптов.
В Си нельзя. Как строковые литералы PROGMEM, так и указатели PROGMEM на них должны быть доступны с помощью инструкции lpm, например с семейством функций pgm_read.
Это ограничение четко описано в документации , обойти его невозможно. Однако ПРОГРАММА больше не нужна для современных AVR, выпущенных с 2017 года, таких как tinyAVR 0, 1 и 2-series , серии AVRDx (т. е. AVRDA - AVRDD).
Есть разные хитрости. Например, вы можете поместить const char * const strings[] в отдельный модуль компиляции strings.c, скомпилировать его без LTO, переместить все разделы .rodata* в новый объектный файл и поставить перед ними префикс .progmem: avr-objcopy -j .rodata* --prefix-sections=.progmem strings.o strings_text.o. Тогда свяжите остальное с помощью strings_text.o. Теперь все из strings.c помещено в память программы и доступно с помощью функций pgm_.
@hcheung: спасибо за ссылку, она точно описывает мою проблему :)
@dimich: Могу ли я использовать этот трюк в make-файле? Я имею в виду, что для исходных файлов с определениями строк AVR должны добавить после компиляции шаг по замене сгенерированного объектного файла новым и собрать все одинаково? (конечно, добавив в код с помощью #ifdefs функцию извлечения строк). Позволит ли это мне использовать определения строк дословно, как сейчас?
Проблема скорее в том, что не существует такого понятия, как «современный AVR». Есть много 8-горьких ужасных историй о памяти, речь идет о процессоре гарвардской модели, но вы также получаете аналогичные проблемы, когда выходите за пределы 64 КБ памяти, поскольку адресная шина обычно 16-битная. А еще на многих 8-биттерах есть «нулевая страница», требующая специальной обработки снежинок. Разумное решение — не использовать старые 8-битные микроконтроллеры.
@RobertWallner Да, его можно использовать в make-файле. Я даже начал писать ответ, но решение от emacs-dead-me-nuts с составными литералами и именованным адресным пространством гораздо красивее и эффективнее.





Поскольку ваш вопрос помечен как «C», GNU-C и именованные адресные пространства согласно ISO/IEC DTR 18037 могут быть подходящим вариантом. Скомпилируйте с -std=gnu99 или выше:
#define F(X) ((const __flash char[]) { X })
static const __flash char *const __flash nums[] =
{
F("first"), F("second"), F("third")
};
#include <stdio.h>
void print_num (int id)
{
printf ("num = %S\n", nums[id]);
}
Сгенерированный код для массива nums[]:
.section .progmem.data,"a",@progbits
.type nums, @object
.size nums, 6
nums:
.word __compound_literal.0
.word __compound_literal.1
.word __compound_literal.2
.type __compound_literal.2, @object
.size __compound_literal.2, 6
__compound_literal.2:
.string "third"
.type __compound_literal.1, @object
.size __compound_literal.1, 7
__compound_literal.1:
.string "second"
.type __compound_literal.0, @object
.size __compound_literal.0, 6
__compound_literal.0:
.string "first"
Код для печати строки использует LPM для чтения nums[id], который снова находится в программной памяти и печатается с использованием спецификатора формата %S:
print_num:
lsl r24 ; 33 [c=8 l=2] *ashlhi3_const/1
rol r25
movw r30,r24 ; 26 [c=4 l=1] *movhi/0
subi r30,lo8(-(nums)) ; 8 [c=8 l=2] *addhi3/1
sbci r31,hi8(-(nums))
lpm r24,Z+ ; 27 [c=8 l=2] *movhi/2
lpm r25,Z+
push r25 ; 11 [c=4 l=1] pushqi1/0
push r24 ; 13 [c=4 l=1] pushqi1/0
ldi r24,lo8(.LC0) ; 14 [c=4 l=2] *movhi/4
ldi r25,hi8(.LC0)
push r25 ; 16 [c=4 l=1] pushqi1/0
push r24 ; 19 [c=4 l=1] pushqi1/0
call printf ; 20 [c=16 l=2] call_value_insn/1
; SP += 4 ; 21 [c=4 l=4] *addhi3_sp
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
ret ; 31 [c=0 l=1] return
Примечание. Вы можете перенести это на арки без __flash с помощью:
#ifndef __FLASH
#define __flash /* empty */
#endif
Макрос __FLASH является встроенным макросом в avr-gcc и определяется только при наличии адресного пространства __flash.
Модификатор печати %S печатает строку в progmem/flash. (На совместимых платформах это означает широкую строку.) Поэтому вместо этого вам придется использовать %s.
ДА! Это ответ, который я искал. На самом деле вопрос заключался в том, «как я могу хранить массивы строк в progmem на avrs, чтобы код по-прежнему работал на не-avrs с как можно меньшим количеством изменений». Должен ли я редактировать вопрос? Это решение также имеет дополнительный бонус: массив указателей на строки также хранится в progmem, и единственное необходимое изменение в коде — это перенос строковых констант с помощью F().
Примечательно, что при этом тратится дополнительная память, поскольку вам придется хранить во флэш-памяти как сам строковый литерал, так и указатель. У компоновщика действительно должна быть опция, указывающая, как обращаться со строковыми литералами, добавляя их, например, .text вместо .rodata или чего-то в этом роде.
@Lundin: Это неизбежно: либо у вас есть массив с указателями на строковые литералы, которые могут иметь разную длину (и, следовательно, нужен массив указателей), либо у вас есть массив литералов, которые все должны иметь одинаковую длину (что может привести к потере пространства когда литералы имеют разную длину). И это ничем не отличается от арок, которые с самого начала имеют .rodata во флэш-памяти.
Я могу придумать несколько грязных трюков, чтобы избежать этого, если вспышка бесценна. Например, вы знаете, насколько велика каждая строка с самого начала, поэтому вы можете разместить их в соседней памяти и при этом получать к ним доступ по смещению памяти. Может выглядеть немного некрасиво, но не намного хуже, чем тот собственный язык макросов, который вы здесь изобрели.
Один-единственный макрос не является «языком». Он просто помещает составной литерал в именованное адресное пространство. Возможно, существует более простой синтаксис, поэтому вы сможете объяснить, как это сделать лучше.
Что ж, специальные распределители для составных литералов предполагают, что и для строковых литералов должно быть что-то подобное. Или еще лучше, если все это будет обрабатываться компоновщиком (скриптом), а не кодом C. Что касается грязных приемов, которые я имел в виду, они определенно гораздо более неясны и включают в себя X-макросы - хотя таким образом вы сможете избавиться от всей таблицы поиска указателей, сохраняя при этом расположение строк рядом. Таким образом, в этом случае это будет экстремальная ручная оптимизация для экономии памяти, возможно, за счет более медленного поиска во время выполнения.
Я опубликовал альтернативный ответ с примерами X-макросов на тот случай, если размер флэш-памяти очень важен.
Макрос F является частью Arduino. Если это источник вдохновения, пожалуйста, скажите об этом и процитируйте его. Это описано внизу этой страницы здесь: arduino.cc/reference/en/language/variables/utilities/progmem
@GabrielStaples: Это не от Arduino, и я не использую Arduino. Более того, Arduino — это C++/avr-g++, который не поддерживает именованные адресные пространства. Вот почему я упомянул C и тег c вопроса. Этот макрос даже не скомпилируется с C++, так почему вы вообще думаете о том, что я его украл или что-то в этом роде... Макрос F из Arduino очень похож на макрос PSTR из pgmspace.h AVR-LibC. И нет, AVR-LibC не украл его у Arduino.
@emacsdrivesmenuts: #if defined(__AVR__) #define F(X) ((const __flash char[]) {X}) #else #define F(X) X #endif отлично компилируется и работает как на avr/не-avr, так и на C/C++.
@RobertWallner: Существуют устройства AVR без именованных адресных пространств, поэтому правильным условием будет #if defined(__AVR__) && !defined (__AVR_TINY__)
Исправление AVR PROGMEM можно скрыть за макросом, например:
#ifdef __AVR__
#define MEM PROGMEM
#else
#define MEM /* dummy macro */
#endif
Что касается производительности и распределения:
Преимущество таблицы поиска на основе указателей, как в первом примере, заключается в скорости выполнения: вы сможете быстро получить каждую строку без каких-либо накладных расходов во время выполнения.
Обратной стороной является то, что вам также придется выделять сами указатели, поэтому тратится дополнительная флэш-память или, в худшем случае, ОЗУ + флэш-память в случае, если таблица указателей копируется из ПЗУ в ОЗУ во время запуска.
Этот ответ применим только в том случае, если вам действительно нужно сохранить флэш-память поверх всего остального. Но также и в том случае, если вы хотите централизовать обслуживание кода в одном списке. Затем вы можете состряпать что-нибудь зловещее, используя «X-макросы», опционально с именем для каждой строки:
#define STR_LIST(X) \
X(d1, "first") \
X(d2, "second") \
X(d3, "third") \
Затем вы можете разместить эти элементы рядом в одном большом массиве, например:
static const char STRINGS[] MEM =
{
#define STR_ALLOC(name, str) str "\0"
STR_LIST(STR_ALLOC)
};
Для удобства здесь используется конкатенация строк, но с добавлением нулевых терминаторов вручную, поскольку в противном случае конкатенация строк удалила бы их. Он расширяется до:
"first" "\0" "second" "\0" "third" "\0"
И объединяется с:
"first\0second\0third\0"
(В конце мы получим дополнительный нулевой терминатор, но это может быть удобно для целей «сигнального значения».)
Различные грязные хаки для «именованного» доступа во время выполнения (оптимизация размера флэш-памяти по скорости) – полный пример кода:
#define STR_LIST(X) \
X(d1, "first") \
X(d2, "second") \
X(d3, "third") \
#ifdef __AVR__
#define MEM PROGMEM
#else
#define MEM /* dummy macro */
#endif
static const char STRINGS[] MEM =
{
#define STR_ALLOC(name, str) str "\0"
STR_LIST(STR_ALLOC)
};
typedef enum
{
#define STR_ENUM(name, str) STR_ENUM_##name,
STR_LIST(STR_ENUM)
STR_ENUM_N
} str_enum_t;
static str_enum_t key;
#define STR_COUNT(name, str) +(STR_ENUM_##name<key ? sizeof(str) : 0)
#define STR_GET_POS(name) (key=STR_ENUM_##name, STR_LIST(STR_COUNT))
#include <stdio.h>
int main (void)
{
puts("Memory dump, | marks null terminators:");
for(size_t i=0; i<sizeof(STRINGS); i++)
{
printf("%c", STRINGS[i]=='\0' ? '|' : STRINGS[i]);
}
puts("");puts("");
puts("What the strings are named:");
#define STR_PRINT_NAME(name, str) printf("%s: %s\n", #name, str);
STR_LIST(STR_PRINT_NAME)
puts("");
puts("Where the strings are found:");
printf("%s %2zu ", "d1", STR_GET_POS(d1));
printf("%s\n", &STRINGS[STR_GET_POS(d1)]);
printf("%s %2zu ", "d2", STR_GET_POS(d2));
printf("%s\n", &STRINGS[STR_GET_POS(d2)]);
printf("%s %2zu ", "d3", STR_GET_POS(d3));
printf("%s\n", &STRINGS[STR_GET_POS(d3)]);
}
Выход:
Memory dump, | marks null terminators:
first|second|third||
What the strings are named:
d1: first
d2: second
d3: third
Where the strings are found:
d1 0 first
d2 6 second
d3 13 third
Итак, где находится PROGMEM, и чтобы получить к нему доступ, вам нужны макросы pgm_read_xxx из #include <avr/pgmspace.h>. Также вы не можете получить к нему доступ как к массиву, или я что-то упускаю?
Это работает, если идентификатор строки известен во время компиляции (если вы получаете доступ к &STRINGS[STR_GET_POS(d1)] с помощью pgm_read_byte()) или при последовательном доступе. Вы не можете получить доступ к строке по произвольному индексу во время выполнения с помощью O (1).
@emacsdrivesmenuts Если для доступа к памяти необходимы дополнительные возможности, специфичные для платформы, просто добавьте их. Это простой массив. Вы можете получить доступ к каждой строке как к массиву или ко всему набору через STRINGS.
@dimich Как было подчеркнуто в ответе: «Этот ответ применим только в том случае, если вам действительно нужно сохранить флэш-память поверх всего остального». Размер ПЗУ превышает ручную оптимизацию скорости выполнения ради экономии флэш-памяти.
@Лундин, я понимаю. Нечто подобное IRCC использовалось в ZX Spectrum 48k ROM для базовых токенов, но старший бит инвертировался в последнем символе в качестве индикатора конца слова вместо нулевого байта.
Это может быть задание для сценария Python или Perl (или другой программы на языке C) по созданию объявлений из списка строк.