Как управлять смешанными указателями на RAM и PROGMEM на Arduino?
Я работаю над проектом, в котором хочу реализовать что-то вроде интерпретатора FORTH на Arduino. Интерпретатор состоит из «СЛОВ», где каждое СЛОВО имеет имя, заголовок и массив указателей на другие СЛОВА, из определений которых оно состоит.
Есть много стандартных СЛОВ, которые я хотел бы сохранить в PROGMEM, чтобы сэкономить ОЗУ для СЛОВ, определяемых пользователем.
Как эффективно управлять смешанными указателями на ОЗУ и PROGMEM в такой системе? В частности, как хранить данные в PROGMEM и указывать на них, одновременно обрабатывая данные из ОЗУ в той же структуре?
Структура будет выглядеть так:
struct node {
char name[8];
uint8_t flags;
node* ptr_array[]; // например [p0, p1, p2, p3, p4, ...]
// где некоторые указатели (px) указывают на узлы в оперативной памяти,
// и другие указывают на узлы в PROGMEM
};
Меня беспокоит, что мне, возможно, придётся хранить тип каждого указателя (RAM или PROGMEM) и использовать эту информацию для выбора правильного метода извлечения значения. Но я не уверен, как это технически реализовать.
Будем очень благодарны за любые советы или примеры!
@gilhad, 👍1
Обсуждение2 ответа
Лучший ответ:
Наконец, я не смог устоять перед соблазном попробовать это. Пока я писал,
в комментарии GCC поддерживает __memx именованное адресное пространство,
который автоматизирует то, что вы в противном случае управляли бы вручную: он хранит
указатели в виде трех байтов, где самый старший бит — это флаг, который
Различает адреса флэш-памяти и адреса ОЗУ. Каждый раз такой указатель
разыменовывается, компилятор генерирует код, который проверяет этот флаг и
переход к подходящей машинной инструкции (lpm для флэш-памяти и ld
для оперативной памяти).
К сожалению, __memx поддерживается только при компиляции чистого C. В
C++, вы не только не можете определить __memx, вы даже не можете объявить
extern "C" элементы, в которых упоминается __memx. Использование этой функции из C++
тогда это немного сложнее, так как вам нужно инкапсулировать все, что
упоминает __memx внутри единицы компиляции C.
Вот небольшой, но успешный тест. Это код на языке C:
static __flash const char flash1[] = "A message in flash...";
static __flash const char flash2[] = "Another message in flash.";
static const char ram1[] = "A message in RAM...";
static const char ram2[] = "Another message in RAM.";
static __memx const char *messages[4];
/* По какой-то причине messages[] не может быть статически инициализирован. */
void init_messages(void)
{
messages[0] = flash1;
messages[1] = flash2;
messages[2] = ram1;
messages[3] = ram2;
}
void print_message(int which, void (*write)(char))
{
__memx const char *s = messages[which];
while (*s)
write(*s++);
write('\r');
write('\n');
}
Здесь, в init_messages(), указатели флэш-памяти и ОЗУ повышаются до
__memx. Компилятор, зная тип исходных указателей, принимает
Позаботьтесь о правильной установке флагов. Очень удобно. Внутри
print_message(), поскольку указатель s разыменовывается, компилятор
Автоматически выдаёт код, который проверяет флаг флэш-памяти/ОЗУ. Гораздо больше
удобнее, чем условно вызывать pgm_read_byte().
А вот и набросок .ino:
extern "C" {
void init_messages(void);
void print_message(int, void (*)(char));
}
void setup() {
Serial.begin(9600);
init_messages();
for (int i = 0; i < 4; i++)
print_message(i, [](char c){ Serial.write(c); });
}
void loop(){}
Большое спасибо за ваш ответ — это именно то, что мне было нужно.
Мне просто потребовалось некоторое время, чтобы полностью это понять и осмыслить.
Здесь на GitHub: https://github.com/githubgilhad/memxFORTH-init моя первая успешная попытка использования указателей __memx для управления словарем слов для чего-то вроде интерпретатора Форта.
Для обработки указателей я использую объединение ptr24_u, которое позволяет выполнять преобразования между указателями __memx, uint32_t и сырыми 3/4-байтовыми представлениями.
Каждое слово (СЛОВО) в памяти состоит из:
- указатель на предыдущее слово (для обхода словаря),
- флаги,
- длина имени,
- строка имени,
- кодовое слово (указатель на исполняемый код),
- опционально может следовать список ссылочных слов (для скомпилированных определений — каждое из них также является 24-битным указателем на кодовое слово).
Слово WORDS динамически создается в оперативной памяти во время запуска программы.
Слова TEST, EXIT, DEBUG, TROUBLE, DOUBLE, + и DUP хранятся во флэш-памяти. (TROUBLE скрыт для проверки обработки флагов.)
Я использую файл ассемблера (asm.S) для размещения определений во FLASH.
Обратите внимание, что при сохранении адресов функций как данных (.word) ассемблер сохраняет их как адреса байтов, тогда как для переходов/вызовов он ожидает адреса слов. Таким образом, вам необходимо:
- делить на 2 при чтении адресов из разделов данных,
- Умножайте на 2 при обратной записи в структуры ОЗУ для сохранения согласованности.
Пример:
7c 06 ; .word f_docol
0e 94 3e 03 ; call 0x67c <f_docol>
Видите, .word сохраняет значение 0x067C, а call использует для внутренних целей значение 0x033E (поскольку адреса вызова находятся в словах, а не в байтах). Значение 0x033E — это то, что должен содержать регистр Z (для косвенных переходов), но дизассемблер также переводит комментарии в байтовое представление.
Для чтения одного байта через указатель __memx AVR-GCC (на ATmega328P — Arduino Nano, UNO) генерирует такой код:
f3 01 movw r30, r6 ; move 2B address to Z
84 91 lpm r24, Z ; read byte from FLASH
87 fc sbrc r8, 7 ; test top bit of 3rd byte
80 81 ld r24, Z ; if set, load from RAM instead
- Адреса от 0x000000 до 0x7FFFFF считываются из FLASH.
- Адреса от 0x800000 до 0xFFFFFF считываются из ОЗУ.
Для многобайтового чтения используются встроенные функции (например, __xload_#), которые сначала проверяют верхний байт адреса, а затем выбирают между операциями LPM и LD.
Краткое описание проекта
Этот репозиторий (memxFORTH-init) — это минимальное, работающее доказательство концепции:
- это не особо помогает,
- выводит много отладочной информации, показывающей состояние переменных и памяти,
- демонстрирует, как объединить доступ к словам ОЗУ и FLASH через __memx.
Мой основной проект позже будет размещен в другом репозитории (развивающемся на этой основе, с большим количеством слов и более сложным поведением).
Если есть интерес, я мог бы немного расширить эту демонстрацию — но основная техническая цель уже достигнута.
Забавная заметка
I threw 0x21 onto the FORTH stack together with the definition : DOUBLE DUP + ;
After execution, 0x42 remained on the stack.
I’ll consider that a perfect hexadecimal answer!
- Инициализировать и читайть из массива указателей PROGMEM на массивы PROGMEM.
- Записать во флэш-память с помощью PROGMEM
- Почему считается плохой практикой использовать ключевое слово "new" в Arduino?
- Работает с gcc, но не с Arduino. ошибка: taking address of temporary array
- Как передать статический константный (программный) массив в функцию
- Проблемы с преобразованием uint32_t в char*
- ArduinoJSON v6 – передача буфера как параметра функции
- Существует ли какой-либо рабочий аналог пары std::function и std::bind в Arduino?
Вы можете использовать полиморфизм или просто перейти на любую Arduino, которой не нужен PROGMEM (с AVR это только Nano Every с Atmega4809, ARM, ...), @KIIV
Компилятор GCC поддерживает [именованное адресное пространство
__memx](https://gcc.gnu.org/onlinedocs/gcc-13.3.0/gcc/Named-Address-Spaces.html#AVR-Named-Address-Spaces-1), которое представляет собой «24-битное адресное пространство, линеаризирующее флэш-память и ОЗУ». Возможно, оно работает только на чистом C (это касается адресного пространства__flash). Никогда им не пользовался. Если вам удастся это реализовать, мне было бы интересно прочитать ваш ответ., @Edgar Bonet@EdgarBonet Да, это работает в C, но не в C++. Кроме того, его нельзя использовать с указателями RAM, так как это «другой» тип (PROGMEM — это, по сути, просто атрибут размещения)., @KIIV
nanoFORTH — это легковесный интерпретатор Форта, разработанный специально для платформ Arduino Nano и UNO. Некоторые идеи можно найти здесь: https://docs.arduino.cc/libraries/nanoforth/, @liaifat85