Как управлять смешанными указателями на 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) и использовать эту информацию для выбора правильного метода извлечения значения. Но я не уверен, как это технически реализовать.

Будем очень благодарны за любые советы или примеры!

, 👍1

Обсуждение

Вы можете использовать полиморфизм или просто перейти на любую 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


2 ответа


Лучший ответ:

4

Наконец, я не смог устоять перед соблазном попробовать это. Пока я писал, в комментарии 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(){}
,

0

Большое спасибо за ваш ответ — это именно то, что мне было нужно.

Мне просто потребовалось некоторое время, чтобы полностью это понять и осмыслить.

Здесь на 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!
,