Массивы символов в PROGMEM

В программе у меня есть множество массивов строк разной длины, и каждый массив объявляется как массив указателей на эти строки, например:

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

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

Это может быть задание для сценария Python или Perl (или другой программы на языке C) по созданию объявлений из списка строк.

John Bode 19.05.2024 14:51

Не static const char PROGMEM * const num_tab[] = { "First", "Second", "Third"}; работает?

Ted Lyngmo 19.05.2024 15:54

@TedLyngmo: нет, это поместит массив указателей в программную память, а строки будут в оперативную память. На самом деле не имеет значения, куда вы поместите макрос PROGMEM.

Robert Wallner 19.05.2024 18:00

@JohnBode: Я ищу способ заставить его работать без слишком большого изменения кода или использования внешних скриптов.

Robert Wallner 19.05.2024 18:02

В Си нельзя. Как строковые литералы PROGMEM, так и указатели PROGMEM на них должны быть доступны с помощью инструкции lpm, например с семейством функций pgm_read.

dimich 19.05.2024 20:04

Есть разные хитрости. Например, вы можете поместить 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_.

dimich 20.05.2024 05:44

@hcheung: спасибо за ссылку, она точно описывает мою проблему :)

Robert Wallner 20.05.2024 11:03

@dimich: Могу ли я использовать этот трюк в make-файле? Я имею в виду, что для исходных файлов с определениями строк AVR должны добавить после компиляции шаг по замене сгенерированного объектного файла новым и собрать все одинаково? (конечно, добавив в код с помощью #ifdefs функцию извлечения строк). Позволит ли это мне использовать определения строк дословно, как сейчас?

Robert Wallner 20.05.2024 11:11

Проблема скорее в том, что не существует такого понятия, как «современный AVR». Есть много 8-горьких ужасных историй о памяти, речь идет о процессоре гарвардской модели, но вы также получаете аналогичные проблемы, когда выходите за пределы 64 КБ памяти, поскольку адресная шина обычно 16-битная. А еще на многих 8-биттерах есть «нулевая страница», требующая специальной обработки снежинок. Разумное решение — не использовать старые 8-битные микроконтроллеры.

Lundin 20.05.2024 14:59

@RobertWallner Да, его можно использовать в make-файле. Я даже начал писать ответ, но решение от emacs-dead-me-nuts с составными литералами и именованным адресным пространством гораздо красивее и эффективнее.

dimich 21.05.2024 04:26
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
11
197
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Поскольку ваш вопрос помечен как «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().

Robert Wallner 20.05.2024 13:48

Примечательно, что при этом тратится дополнительная память, поскольку вам придется хранить во флэш-памяти как сам строковый литерал, так и указатель. У компоновщика действительно должна быть опция, указывающая, как обращаться со строковыми литералами, добавляя их, например, .text вместо .rodata или чего-то в этом роде.

Lundin 20.05.2024 15:06

@Lundin: Это неизбежно: либо у вас есть массив с указателями на строковые литералы, которые могут иметь разную длину (и, следовательно, нужен массив указателей), либо у вас есть массив литералов, которые все должны иметь одинаковую длину (что может привести к потере пространства когда литералы имеют разную длину). И это ничем не отличается от арок, которые с самого начала имеют .rodata во флэш-памяти.

emacs drives me nuts 20.05.2024 15:14

Я могу придумать несколько грязных трюков, чтобы избежать этого, если вспышка бесценна. Например, вы знаете, насколько велика каждая строка с самого начала, поэтому вы можете разместить их в соседней памяти и при этом получать к ним доступ по смещению памяти. Может выглядеть немного некрасиво, но не намного хуже, чем тот собственный язык макросов, который вы здесь изобрели.

Lundin 20.05.2024 15:29

Один-единственный макрос не является «языком». Он просто помещает составной литерал в именованное адресное пространство. Возможно, существует более простой синтаксис, поэтому вы сможете объяснить, как это сделать лучше.

emacs drives me nuts 20.05.2024 15:46

Что ж, специальные распределители для составных литералов предполагают, что и для строковых литералов должно быть что-то подобное. Или еще лучше, если все это будет обрабатываться компоновщиком (скриптом), а не кодом C. Что касается грязных приемов, которые я имел в виду, они определенно гораздо более неясны и включают в себя X-макросы - хотя таким образом вы сможете избавиться от всей таблицы поиска указателей, сохраняя при этом расположение строк рядом. Таким образом, в этом случае это будет экстремальная ручная оптимизация для экономии памяти, возможно, за счет более медленного поиска во время выполнения.

Lundin 20.05.2024 16:38

Я опубликовал альтернативный ответ с примерами X-макросов на тот случай, если размер флэш-памяти очень важен.

Lundin 20.05.2024 17:14

Макрос F является частью Arduino. Если это источник вдохновения, пожалуйста, скажите об этом и процитируйте его. Это описано внизу этой страницы здесь: arduino.cc/reference/en/language/variables/utilities/progmem

Gabriel Staples 20.05.2024 21:01

@GabrielStaples: Это не от Arduino, и я не использую Arduino. Более того, Arduino — это C++/avr-g++, который не поддерживает именованные адресные пространства. Вот почему я упомянул C и тег c вопроса. Этот макрос даже не скомпилируется с C++, так почему вы вообще думаете о том, что я его украл или что-то в этом роде... Макрос F из Arduino очень похож на макрос PSTR из pgmspace.h AVR-LibC. И нет, AVR-LibC не украл его у Arduino.

emacs drives me nuts 20.05.2024 23:01

@emacsdrivesmenuts: #if defined(__AVR__) #define F(X) ((const __flash char[]) {X}) #else #define F(X) X #endif отлично компилируется и работает как на avr/не-avr, так и на C/C++.

Robert Wallner 21.05.2024 15:08

@RobertWallner: Существуют устройства AVR без именованных адресных пространств, поэтому правильным условием будет #if defined(__AVR__) && !defined (__AVR_TINY__)

emacs drives me nuts 21.05.2024 17:11

Исправление 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>. Также вы не можете получить к нему доступ как к массиву, или я что-то упускаю?

emacs drives me nuts 20.05.2024 18:29

Это работает, если идентификатор строки известен во время компиляции (если вы получаете доступ к &STRINGS[STR_GET_POS(d1)] с помощью pgm_read_byte()) или при последовательном доступе. Вы не можете получить доступ к строке по произвольному индексу во время выполнения с помощью O (1).

dimich 21.05.2024 05:06

@emacsdrivesmenuts Если для доступа к памяти необходимы дополнительные возможности, специфичные для платформы, просто добавьте их. Это простой массив. Вы можете получить доступ к каждой строке как к массиву или ко всему набору через STRINGS.

Lundin 21.05.2024 08:18

@dimich Как было подчеркнуто в ответе: «Этот ответ применим только в том случае, если вам действительно нужно сохранить флэш-память поверх всего остального». Размер ПЗУ превышает ручную оптимизацию скорости выполнения ради экономии флэш-памяти.

Lundin 21.05.2024 08:20

@Лундин, я понимаю. Нечто подобное IRCC использовалось в ZX Spectrum 48k ROM для базовых токенов, но старший бит инвертировался в последнем символе в качестве индикатора конца слова вместо нулевого байта.

dimich 21.05.2024 09:38

Другие вопросы по теме