Можно ли загружать и запускать код из EEPROM во время загрузки?

У меня возникли проблемы при написании пользовательского загрузчика, так что он загружает код из EEPROM (на данный момент внутренний, поскольку у меня нет внешней памяти в моих руках) и записывает во флэш-память. После ~ 2 недель борьбы я столкнулся с этими вопросами и ответами. После этого я понял, что совершил ошибку, не проверив возможность этого. Тем не менее, я все еще в замешательстве по поводу нескольких вещей:

  1. Если Гарвардская архитектура ограничивает то, что я хочу, почему optiboot может прошивать память из входящих данных UART, которые даже не являются памятью?

  2. Если UART считывает и прошивает память соответствующим образом, не могу ли я сделать то же самое с EEPROM? Считывание байтов, стирание флэш-памяти, запись байтов.

  3. Если это невозможно, учитывая два ответа в приведенной выше ссылке, то как это работает? Я предполагаю, что это та же логика, что и я, используя внешнюю память и перезаписывая flash. Означает ли это, что Гарвардская архитектура ограничивает только обмен внутренней памятью?

ОБНОВЛЕНИЕ: Вот что я сделал на данный момент:

В отдельном скетче EEPROM заполняется двоичным кодом. Загрузчик (отредактированный optiboot v8):

  // Настройка сторожевого таймера для запуска после желаемого тайм-аута
  watchdogConfig(WDTPERIOD);
#if BIGBOOT

  if ((eeprom_read(0x000) == 0x0C) && (eeprom_read(0x001) == 0x94))
  {
    //while (EECR & (1 << EEPE)); //подождите, если предыдущие данные все еще записываются
    uint8_t i, j;

    for (i = 0; i < 8; ++i) // всего 8 страниц
    {
      uint8_t *code;
      code = buff.bptr;
      address.word = (i * 128);

      for (j = 0; j < 128; ++j)  // 128 слов за раз
      {     
        if (j % 64 == 0)     
          LED_PORT ^= _BV(LED);

        *code++ = eeprom_read(address.word + j);
      }

#ifdef VIRTUAL_BOOT_PARTITION
      virtualBootPartition(buff, address);
#endif 

      writebuffer(0x46, buff, address, 128);
    }

    while(1); // дождитесь WDT
  }
#endif


#if (LED_START_FLASHES > 0) || LED_DATA_FLASH || LED_START_ON
  /* Set LED pin as output */
  LED_DDR |= _BV(LED);
#endif

в то время как моя функция выглядит следующим образом:

#if BIGBOOT 
static uint8_t eeprom_read(uint16_t addr)
{
  EEAR = addr; //считывать с этого адреса
  EECR |= (1 << EERE); //включить чтение

  return EEDR;
}
#endif

Я использую make atmega328 BIGBOOT=1 для создания шестнадцатеричного файла. Это не работает + загрузка UART также завершается неудачей. Если я установлю для BIGBOOT значение 0 и сгенерирую, загрузка UART будет работать нормально.

P.S. virtualBootPartition - это функция, которая вызывается в случае STK_PROG_PAGE. Чтобы не повторять один и тот же фрагмент кода, я собрал эту часть в функцию.

#ifdef VIRTUAL_BOOT_PARTITION
/*
 * Как работает виртуальный загрузочный раздел:
 * В начале обычной программы AVR находится набор векторов, которые
 * реализовать механизм прерывания.  Каждый вектор обычно представляет собой один
 * инструкция, которая отправляется в соответствующий ISR.
 * Инструкция обычно представляет собой rjmp (на AVR с 8k или менее флэш-памяти)
 * или jmp-инструкция, и 0-й вектор выполняется при сбросе и переходит
 * к запуску пользовательской программы:
 * векторы: запуск jmp
 * jmp ISR1
 * jmp ISR2
 * : ;; и т.д
 * jmp lastvector
 * Для реализации "Виртуального загрузочного раздела" Optiboot определяет, когда
программируется страница * flash, содержащая векторы, и заменяет
вектор * startup переходом к началу Optiboot.  Тогда это спасает
 * вектор запуска приложения в другом (должен быть неиспользуем приложением
*) и, наконец, программирует страницу с измененными векторами.
 * После этого, при сбросе, вектор будет отправлен в начало
 * Optiboot.  Когда Optiboot решит, что он запустит пользовательское приложение,
 * он извлекает сохраненный начальный адрес из неиспользуемого вектора и переходит
 * там.
 * Логика зависит от размера flash и от того, находится ли вектор сброса
* на той же странице flash, что и сохраненный начальный адрес.
 */

#if FLASHEND > 8192
/*
 * AVR с 4-байтовыми векторами ISR и "jmp"
 * ПРЕДУПРЕЖДЕНИЕ: это работает только до 128 КБ флэш-памяти!
 */
#if FLASHEND > (128*1024)
#error "Can't use VIRTUAL_BOOT_PARTITION with more than 128k of Flash"
#endif
  if (address.word == RSTVEC_ADDRESS) {
    // Это страница сброса вектора. Нам нужно обновить код
    //, чтобы загрузчик запускался первым.
    //
    // Сохранить цели jmp (для "Проверки")
    rstVect0_sav = buff.bptr[rstVect0];
    rstVect1_sav = buff.bptr[rstVect1];

        // Добавить "перейти к Optiboot" при СБРОСЕ вектора
        // ПРЕДУПРЕЖДЕНИЕ: это работает до тех пор, пока "main" находится в первом разделе
    buff.bptr[rstVect0] = ((uint16_t)pre_main) & 0xFF;
    buff.bptr[rstVect1] = ((uint16_t)pre_main) >> 8;

#if (SAVVEC_ADDRESS != RSTVEC_ADDRESS)
// save_vector необязательно находится на той же флэш-странице, что и сброс
// вектор.  Если это не так, то мы ждем, чтобы действительно написать это.
  } 
  else if (address.word == SAVVEC_ADDRESS) {
      // Сохранить старые значения для проверки
      saveVect0_sav = buff.bptr[saveVect0 - SAVVEC_ADDRESS];
      saveVect1_sav = buff.bptr[saveVect1 - SAVVEC_ADDRESS];

      // Переместить цель СБРОСА jmp в вектор "сохранения"
      buff.bptr[saveVect0 - SAVVEC_ADDRESS] = rstVect0_sav;
      buff.bptr[saveVect1 - SAVVEC_ADDRESS] = rstVect1_sav;
  }
#else 
        // Сохранить старые значения для проверки
        saveVect0_sav = buff.bptr[saveVect0];
    saveVect1_sav = buff.bptr[saveVect1];

        // Переместить цель СБРОСА jmp в вектор "сохранения"
        buff.bptr[saveVect0] = rstVect0_sav;
        buff.bptr[saveVect1] = rstVect1_sav;
}
#endif

#else
/*
 * AVR with 2-byte ISR Vectors and rjmp
 */
  if (address.word == rstVect0) {
    // Это страница сброса вектора. Нам нужно жить-исправлять
    // код, чтобы загрузчик запускался первым.
    //
    // Переместить вектор СБРОСА в вектор "сохранения"
      // Сохранить цели jmp (для "Проверки")
    rstVect0_sav = buff.bptr[rstVect0];
    rstVect1_sav = buff.bptr[rstVect1];
    addr16_t vect;
    vect.word = ((uint16_t)pre_main-1);
    // Инструкция - это относительный переход (rjmp), поэтому пересчитайте.
    // инструкция RJMP равна 0b1100xxxx xxxxxxxx, поэтому мы должны иметь возможность
    // выполнять математические вычисления для смещений, не маскируя их сначала.
    // Обратите внимание, что rjmp относится к уже увеличенному ПК, поэтому смещение
    // на единицу меньше, чем вы могли бы ожидать.
    buff.bptr[0] = vect.bytes[0]; // rjmp для запуска загрузчика
    buff.bptr[1] = vect.bytes[1] | 0xC0;  // создать "rjmp"
#if (SAVVEC_ADDRESS != RSTVEC_ADDRESS)
  } 
  else if (address.word == SAVVEC_ADDRESS) {
    addr16_t vect;
    vect.bytes[0] = rstVect0_sav;
    vect.bytes[1] = rstVect1_sav;
    // Сохранить старые значения для проверки
    saveVect0_sav = buff.bptr[saveVect0 - SAVVEC_ADDRESS];
    saveVect1_sav = buff.bptr[saveVect1 - SAVVEC_ADDRESS];

    vect.word = (vect.word-save_vect_num); //вычтите позицию прерывания 'save'
  // Переместить цель СБРОСА jmp в вектор "сохранения"
  buff.bptr[saveVect0 - SAVVEC_ADDRESS] = vect.bytes[0];
  buff.bptr[saveVect1 - SAVVEC_ADDRESS] = (vect.bytes[1] & 0x0F)| 0xC0;  // создать "rjmp"
  }
#else

  // Сохранить старые значения для проверки
  saveVect0_sav = buff.bptr[saveVect0];
  saveVect1_sav = buff.bptr[saveVect1];

  vect.bytes[0] = rstVect0_sav;
  vect.bytes[1] = rstVect1_sav;
  vect.word = (vect.word-save_vect_num); //вычтите позицию прерывания 'save'
  // Переместить цель СБРОСА jmp в вектор "сохранения"
  buff.bptr[saveVect0] = vect.bytes[0];
  buff.bptr[saveVect1] = (vect.bytes[1] & 0x0F)| 0xC0;  // создать "rjmp"
  // Добавьте rjmp в загрузчик при СБРОСЕ вектора
  vect.word = ((uint16_t)pre_main-1); // (main) всегда <= 0x0FFF; маскировка не требуется.
  buff.bptr[0] = vect.bytes[0]; // инструкция rjmp 0x1c00
}
  
#endif
#endif // ПРОШИВКА
#endif // VBP

, 👍1

Обсуждение

Вы можете запускать код только из флэш-памяти. Только загрузчик может писать во флэш-память. Когда загрузчик «выходит», он запускает код на «обычном» разделе флэш-памяти. Загрузчик может получить код из ввода, который он хочет, UART, IR, I2C, SPI. Он просто должен реализовать его и, возможно, какой-то протокол поверх него, чтобы он знал, когда данные программы запускаются и останавливаются. Он может читать из EEPROM, но это будет означать, что ваш код может быть только размером с EEPROM, который составляет 1 КБ, если я правильно помню, для 328, а флэш-память составляет 32 КБ (минус размер вашего загрузчика)., @Gerben

Я нашел эти загрузчики: https://github.com/mysensors/DualOptiboot. Он получает свою программу из SPI-EEPROM (где основная программа скачивает ее откуда-то и помещает в EEPROM. Затем, после «перезагрузки», загрузчик переносит ее из EEPROM во флэш-память)., @Gerben

Спасибо за ответ. Я тщательно проанализировал последовательную связь между ПК и Arduino при загрузке. Размер кода для atmega328p (для других MCU не знаю) ровно 1К. На этот раз я написал EEPROM в другом скетче, который является бинарным файлом. Затем я отредактировал загрузчик, чтобы, если в EEPROM что-то есть, прочитать и загрузить его, как это происходит при получении последовательности UART. Но, к сожалению, безуспешно. Думаю, внешняя память решит эту проблему., @Miradil Zeynalli

вы также можете сохранить этот 1 КБ во флэш-памяти. см. функцию do_spm в Optiboot 7 https://github.com/Optiboot/optiboot/blob/master/optiboot/examples/demo_dospm/demo_dospm.ino, @Juraj

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

@Juraj Я не думаю, что это сработает, если вы хотите переписать весь скетч. Но, может быть, это сработает для ОП., @Gerben

@Gerben, чтобы переписать скетч, есть моя функция copy_flash_pages в загрузчике для загрузки с верхней половины флэш-памяти. https://github.com/jandrassy/ArduinoOTA/blob/master/src/InternalStorageAVR.cpp https://github.com/Optiboot/optiboot/pull/269, @Juraj

Связано: [Можно ли использовать дополнительную флэш-память AVR в качестве энергонезависимой флэш-памяти, подобной EEPROM?](https://arduinoprosto.ru/q/51277/7727)., @Gabriel Staples

То, что вы пытаетесь сделать, может потребовать редактирования скрипта компоновщика (в данный момент я даже не уверен, где он находится для Arduino)., @Gabriel Staples

Похоже, что файлы скриптов компоновщика находятся, например, в: **arduino-1.8.13/hardware/tools/avr/avr/lib/ldscripts/avr1.x**. Однако я хватаюсь за соломинку - не знаю, как определить, какая из них для ATmega328. Кто-нибудь знает? Кроме того, вот код optiboot. Я думаю, стоит изучить, чтобы увидеть, как именно это работает, попробовать и посмотреть, как это работает, каковы его ограничения, и перейти оттуда: https://github.com/arduino/ArduinoCore-avr/blob/master/bootloaders/ optiboot/optiboot.c. Вероятно, у него будут подсказки, которые можно найти в таблицах данных и официальных документах Atmel/Microchip., @Gabriel Staples

Я отредактировал свой пост и добавил дополнительные пояснения и фрагменты кода., @Miradil Zeynalli

@GabrielStaples, зачем мне скрипт компоновщика?, @Miradil Zeynalli

чтобы использовать BIGBOOT с 328p, вы должны изменить фьюзы https://github.com/jandrassy/my_boards/blob/14f363387bbe70bfe66e1b6c2b7a398338d92533/avr/boards.txt#L89 при записи загрузчика, @Juraj

А я то думал, почему BIGBOOT без моего кода не работает). Спасибо!, @Miradil Zeynalli

прочитайте мой ответ ниже для получения дополнительных ноу-хау, @Juraj


1 ответ


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

1

Если у вас есть sketch, который может записывать свою обновленную версию в EEPROM ATmega328P, а затем загружать из EEPROM, то вы можете использовать flash для той же цели. ATmega328P имеет 32 КБ флэш-памяти и только 1 КБ EEPROM.

Для моей библиотеки ArduinoOTA я разработал способ сохранения загруженного двоичного файла в верхней половине флэш-памяти, а затем позволил загрузчику скопировать двоичный файл по адресу 0 и сбросить MCU.

Optiboot 8 имеет функцию do_spm(). Эта функция может быть вызвана из скетча для записи во флэш-память.

Чтобы скопировать двоичный файл, хранящийся в верхней половине флэш-памяти, по адресу 0, я написал новую функцию для Optiboot. Он называется copy_flash_pages и имеет дополнительный логический параметр для запроса сброса MCU после копирования страниц.

Для сетевой загрузки размер скетча больше половины флэш-памяти 328p из-за сетевой библиотеки. Таким образом, ArduinoOTA предназначена только для ATmega с флэш-памятью более 64 КБ.

Если у вас есть способ получить двоичный файл размером менее половины флэш-памяти 328p, то вы можете записать его в верхнюю половину флэш-памяти, а затем использовать мою функцию copy_flash_pages для его загрузки.

Ресурсы:

  • файл заголовка для доступа к функциям Optiboot из
  • определения sketch boards и загрузчика для 328p с помощью Optiboot с функцией copy_flash_pages в my_boards (обратите внимание на измененные предохранители в boards.txt для Uno / Nano / Mini)
  • класс InternalStorageAVR в библиотеке ArduinoOTA в качестве примера того, как хранить и применять двоичный файл

Пример использования внутреннего хранилища:

  if (!InternalStorage.open(length)) {
    Serial.println("There is not enough space to store the update. Can't continue with update.");
    return;
  }
  byte b;
  while (length > 0) {
    if (!source.readBytes(&b, 1)) // чтение байта с таймаутом
      break;
    InternalStorage.write(b);
    length--;
  }
  InternalStorage.close();
  if (length > 0) {
    Serial.print("Timeout downloading update file at ");
    Serial.print(length);
    Serial.println(" bytes. Can't continue with update.");
    return;
  }
  Serial.println("Sketch update apply and reset.");
  InternalStorage.apply(); // это не возвращает
,

Это почти решает мою проблему. Однако я не понял этого: "Если у вас есть способ получить двоичный файл размером меньше половины 328p флэш-памяти, то вы можете записать его в верхнюю половину флэш-памяти, а затем использовать мою функцию copy_flash_pages для загрузки это.". ATmega328p имеет флэш-память 32 КБ, а размер двоичного файла составляет 1 КБ., @Miradil Zeynalli

@MiradilZeynalli, для следующей загрузки загруженный код должен поддерживать загрузку. можно сделать этот код таким маленьким?, @Juraj

Более того, я в основном хотел бы решить это с помощью загрузчика, как следует из моего вопроса. Потому что наличие двоичного файла во флэш-памяти + самой программы + загрузчика может иметь ограничения для больших программ (без использования вашей сетевой библиотеки). В моем случае я хотел бы использовать загрузчик максимум 1k (я даже планирую удалить поддержку STK500, поэтому я могу уменьшить его до 0,5k)., @Miradil Zeynalli

тогда почему вы хотите перейти на EEPROM? загрузчик может писать напрямую во флэш-память. Я не понимаю процесса, которого вы пытаетесь достичь. как передается бинарник?, @Juraj

Я просто должен проверить это, если честно :). Но, я думаю, исходя из вашего опыта, вы этого не сделали. Тогда, пожалуйста, поправьте меня, если я вас неправильно понял. Даже если у меня уже есть массив значений (скажем, я получил его из bluetooth) бинарного файла, я могу прошить память и загрузить код с вашим классом InternalStorageAVR и его функциями, @Miradil Zeynalli

да. аналогично этому примеру https://github.com/jandrassy/ArduinoOTA/blob/master/examples/Advanced/OTASketchDownload/OTASketchDownload.ino, @Juraj

Я пока использую внутреннюю EEPROM, так как у меня сейчас нет внешней. Чего я хочу добиться, так это прочитать из eeprom и перезаписать флэш-память ВО ВРЕМЯ загрузчика. В коде приложения прочитать двоичный файл - сохранить двоичный файл в EEPROM - перезагрузить себя., @Miradil Zeynalli

Я проанализировал ваш код и, как вы сказали, он хорош для небольших программ или больших микроконтроллеров флэш-памяти. Если я правильно понимаю ваш код, вы используете верхнюю половину флэш-памяти в качестве временного хранилища для кода, а затем копируете его в реальный раздел программы, поэтому вам нужно, чтобы ваш код был меньше размера флэш-памяти. Тогда у меня такой вопрос: можно ли хранить бинарник не во флеше, а в EEPROM (или в другом внешнем запоминающем устройстве) и потом копировать его во флешь своими функциями?, @Miradil Zeynalli

код для записи в EEPROM прост. затем в загрузчике вы проверяете, содержит ли EEPROM новый двоичный файл, и копируете его во флэш-память. и я так понимаю код у тебя есть, а новый бинарник не запускается?, @Juraj

проблема с внешним устройством заключается в том, что вы должны кодировать связь через SPI или аналогичный, и это делает код больше, @Juraj

Да, я пытаюсь читать из eeprom во время загрузчика и записывать во флэш-память (как это делает optiboot). Но, кажется, это не удается. Я включил все необходимые фрагменты кода в вопрос., @Miradil Zeynalli

Все работало правильно, я просто неправильно тестировал. Как вы сказали, с вашей библиотекой я могу хранить более тяжелый код, чем EEPROM. Я все равно буду использовать внешний EEPROM, но все же ваш ответ правильный, поэтому я его отметил (несмотря на то, что прошло 6 месяцев), @Miradil Zeynalli