Перейти к основному содержимому

Работа с энергонезависимой памятью

Устройство, построенное на базе любого микропроцессора, в своем конечном применении, как правило, работает без использования отладочных интерфейсов типа JTAG или подобных, через которые производится загрузка программы. Программа хранится в энергонезависимой памяти (например, флеш, EEPROM или другого типа), либо входящей в состав микросхемы процессора, либо подключенной к порту внешней памяти процессора. Процессоры серии «Мультикор» на данный момент не имеют в своем составе энергонезависимой памяти, поэтому любая система, построенная на этих процессорах, должна иметь в своем составе внешнюю энергонезависимую память. В данном документе рассмотрены особенности построения систем и написания программного обеспечения, связанные с энергонезависимой памятью.

Исключением могут являться процессоры серии «Мультикор», имеющие возможность загрузки через один из периферийных портов. Эти случаи также рассмотрены в данном документе.

Типы энергонезависимой памяти, поддерживаемые процессорами «Мультикор»

В зависимости от конкретного процессора, он может выполнять загрузку из энергонезависимой памяти с интерфейсом SRAM (это может быть NOR-флеш, EEPROM, однократно программируемое ПЗУ, MRAM, FRAM и т.д. Далее в тексте для данных типов памяти используется термин «параллельная флеш»), NAND-флеш или флеш-памяти с интерфейсом SPI.

Параллельная флеш-память

С точки зрения процессора и порта внешней памяти работа с параллельной флеш ничем не отличается от работы с обычной статической памятью (SRAM). Поэтому данный интерфейс и возможность загрузки из параллельной флеш имеются во всех процессорах серии «Мультикор», начиная с самых первых.

После снятия сигнала nRST процессор (CPU-ядро) вычитывает по виртуальному адресу 0xBFC0_0000 (физический адрес 0x1FC0_0000) инструкцию, выполняет ее, вычитывает следующую инструкцию по адресу 0xBFC0_0004, и так далее. Проще говоря, стартует с адреса 0xBFC0_0000.

Диапазону физических адресов 0x1C00_0000-0x1FFF_FFFF соответствует банк памяти, подключенный к выводу микросхемы nCS[3]. Это соответствие жестко «прошито» в конфигурации порта внешней памяти, и не может быть изменено, в отличие от других банков памяти, каждому из которых может соответствовать различная область физических адресов (задается настройками в соответствующих регистрах CSCON).

Таким образом, в режиме загрузки из параллельной флеш-памяти (в документации также может употребляться термин «NOR Flash» - имеется в виду именно работа с параллельной флеш) процессор начинает исполнение сразу из параллельной флеш. Задача программиста в этом случае – разместить по данному адресу требуемую программу.

Процедура загрузки из параллельной флеш

Любая программа работает с данными. Все данные располагаются в памяти процессора. Под них в коде выделяются переменные. Если происходит работа из энергонезависимой памяти, и данные расположены в ней же – программа не будет корректно работать, в случае, если мы работаем из однократно программируемого ПЗУ или памяти, требующей специальных процедур для перевода в режим записи, так как при попытке изменить значение переменной, фактически этого изменения не произойдет. Если происходит работа из энергонезависимой памяти, запись в которую происходит аналогично записи в обычную SRAM (например, часто такая процедура записи используется в MRAM или FRAM), вышеупомянутая проблема не так актуальна, но может вызывать какие-то иные проблемы, например, в части скорости обращений.

Если в случае написания программы на ассемблере задача выглядит несложно, то для программы на Си нужен ряд дополнительных действий. Для хранения переменных при работе программы выделяется область памяти, называемая стеком. Для архитектуры MIPS32 по соглашению ABI (application binary interface), адрес начала стека хранится в регистре $29 (sp). Доступ к переменным в стеке производится инструкциями вида LW/SW $reg,offset($29). Смещение «offset» вычисляется компилятором при сборке программы. Аналогичным образом предусматривается область памяти для так называемых «маленьких» переменных (в английской терминологии – small data), которые точно так же адресуются, но через регистр $28 (gp). «Small data» - это переменные, размером не превышающие заданное количество байт. Секции, содержащие «small data», в названии имеют префикс «s» - так, это секции .sdata и .sbss. Размер данных, попадающих в секции «small», определяется ключом компилятора -Gx. По умолчанию в настройках проекта применен ключ -G0, исключающий наличие переменных в «small»-секциях. Это связано с особенностями формирования линковочных скриптов по умолчанию.

Кроме того, в программе могут использоваться функции динамического распределения памяти – malloc() и так далее. Эти функции используют область памяти, называемую «кучей» heap в английской терминологии. Начало области памяти, используемой в качестве кучи, располагается сразу же за стеком.

При работе программы из флеш-памяти необходимо обеспечить расположение данных (как минимум тех, которые могут быть изменены) в оперативной памяти. Это можно сделать несколькими способами, одним из которых является изменение адреса символов _stack и _gp (определяющих адреса стека и области small-данных соответственно) в скрипте линковки. Вторым вариантом является изменение значений регистров $28 и $29 непосредственно в стартовом коде, выполняемом перед функцией main(), однако в этом случае исключается использование библиотеки newlib и функций динамического распределения памяти.

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

  • основная программа, выполняющая необходимую задачу, создается и отлаживается в адресах ОЗУ. При отладке, соответственно, никак не используется энергонезависимая память (если, конечно, этого не требуется для выполнения задачи);
  • после того, как программа отлажена, начиная с адреса 0xBFC0_0000 в параллельную флеш записывается загрузчик, а по другому адресу параллельной флеш – ELF-файл основной программы.

В этом случае происходит следующее: при снятии сигнала nRST процессор начинает выполнять программу-загрузчик. Загрузчик производит инициализацию PLL (то есть, задает рабочие значения частот процессора) и порта внешней памяти (при необходимости), после чего приступает к загрузке основной программы. В программе загрузчика жестко прописан адрес, начиная с которого в памяти располагается ELF-файл основной программы. Загрузчик, анализируя таблицу секций ELF-файла, загружает по адресам ОЗУ те секции основной программы, которые помечены как загружаемые. После этого загрузчик вычитывает из ELF-файла адрес точки входа и выполняет инструкцию перехода на этот адрес. После этого начинает выполняться основная программа.

Запись программы в параллельную флеш

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

Для процессоров серии «Мультикор» существует утилита MCPROG, обеспечивающая работу с обозначенным в документации на нее списком микросхем флеш-памяти, подключенных в 32-разрядном режиме (если в документации не указано иное). В первую очередь это типы памяти, используемые на отладочных модулях процессоров серии «Мультикор». Соответственно, в случае использования в разрабатываемом устройстве одного из поддерживаемых типов памяти в поддерживаемой конфигурации, можно рассчитывать на использование утилиты MCPROG.

В случае же, если используется память, не поддерживаемая утилитой MCPROG, или подключенная в неподдерживаемой конфигурации, остается следующий выход: должна быть создана небольшая программа для CPU-ядра, которая будет выполнять следующий набор действий:

  • инициализировать порт внешней памяти и PLL микросхемы (необязательно, но может быть желательно для ускорения процедуры «прошивки»);
  • останавливаться перед началом процедуры записи (по точке останова). После останова средствами отладчика в определенную область памяти (ОЗУ) процессора должны быть загружены данные, подлежащие записи во флеш-память;
  • после загрузки данных программа должна стартовать и осуществить запись во флеш-память.

Структура подобной программы может выглядеть следующим образом:

#define BOOTLDR_SRC_ADDR 0x80000000 // адрес в ОЗУ, куда загружается загрузчик
#define MAIN_SRC_ADDR 0x80001000 // адрес в ОЗУ, куда загружается основная программа
#define BOOTLDR_DST_ADDR 0xBFC00000 // адрес во флеш, куда записывается загрузчик
#define MAIN_DST_ADDR 0xBFC00800 // адрес во флеш, куда записывается основная программа

unsigned int BOOTLDR_LEN 1024 // длина загрузчика
unsigned int MAIN_LEN 2048 // длина основной программы
// флаг корректности записанных данных для загрузчика
unsigned int flag_correct_bootldr = 0;
// флаг корректности записанных данных для основной программы
unsigned int flag_correct_main = 0;

int main() {
Init(); // инициализация PLL и порта внешней памяти
EraseFlash(); // стирание области памяти, в которую будет производиться запись
asm volatile (.word 0x4d); // инструкция break – останов
// здесь необходимо средствами отладчика произвести загрузку файлов
// по нужным адресам ОЗУ
// запись загручика
WriteToFlash(BOOTLDR_SRC_ADDR, BOOTLDR_DST_ADDR, BOOTLDR_LEN);
// запись основной программы
WriteToFlash(MAIN_SRC_ADDR, MAIN_DST_ADDR, MAIN_LEN);
// проверка корректности записанных данных
flag_correct_bootldr = Verify( BOOTLDR_SRC_ADDR,
BOOTLDR_DST_ADDR,
BOOTLDR_LEN);
flag_correct_main = Verify( MAIN_SRC_ADDR,
MAIN_DST_ADDR,
MAIN_LEN);
asm volatile (.word 0x4d);
while (1) ;
}

Приведенный выше код имеет некоторые особенности, ненужные при исполнении из среды разработки MCStudio. В частности, это останов с помощью инструкции «Break 1» (она представлена как код команды 0x4D).

В среде разработки можно просто поставить точку останова на конкретной строке кода. Кроме того, значения длин загрузчика и основной программы обозначены как переменные типа unsigned int, хотя напрашивается использование директивы #define. Эти особенности введены с целью удобства работы с получившейся программой. Запись с ее помощью через MCStudio – недостаточно удобная процедура. Более удобно будет, после того, как данная программа отлажена, запускать ее с помощью скрипта отладчика MDB. В этом случае прошивка флеш-памяти будет слабоотличаться от таковой при использовании утилиты MCPROG. Скрипт отладчика MDB в этом случае будет выглядеть так:

reset
loadelf flasher.elf # загружаем ELF-файл программы, осуществляющей запись во флеш
run # запускаем программу и доходим до точки останова
set BOOTLDR_LEN 2048 # задаем нужный размер загрузчика
set MAIN_LEN 10240 # задаем нужный размер ELF-файла основной программы
loadbin elfloader.bin 0x80000000 # загружаем в ОЗУ бинарный файл загрузчика
loadbin main.elf 0x80001000 # загружаем в ОЗУ ELF-файл основной программы
run # запускаем запись во флеш и ждем останова
# выводим флаги корректности записи загрузчика и основной программы
show flag_correct_bootldr
show flag_correct_main

Запуск подобного скрипта может производиться следующим образом: mdb –u –f flasher.cfg, где -u - ключ, предписывающий работать через USB-JTAG, -f flasher.cfg - ключ, предписывающий исполнить скрипт flasher.cfg.

В отличие от основной программы, загрузчик должен записываться не в виде ELF-файла, а в виде «чистого» исполняемого кода, не обремененного никакими заголовками и прочим. Поэтому проект загрузчика должен создаваться в адресах 0xBFC0_0000, а финальный ELF-файл загрузчика должен быть преобразован в формат BIN (raw binary) с помощью утилиты OBJCOPY;

Отладка программы, исполняющейся из параллельной флеш-памяти

В свете вышесказанного, отладка, в сущности, необходима только загрузчику. Основная программа отлаживается еще до того, как начинается работа с флеш. Тем не менее, некоторые особенности стоит отметить.

Отладка программы, записанной в параллельную флеш, практически не отличается от отладки программы, находящейся в ОЗУ. Если говорить об отладке с помощью MDB – фактически, после исполнения команды reset, уже началась отладка программы. В среде разработки MCStudio необходимо установить флаг «Don’t load the project», так как к моменту старта отладки программа уже в памяти, и удалить настройки «Startup registers», чтобы достоверно воспроизвести режим загрузки устройства после ресета.

Кроме того, при работе в MCStudio необходимо использовать только аппаратные точки останова, так как программная точка останова – это замена инструкции в памяти инструкцией BREAK. Поскольку такая запись в энергонезависимую память невозможна – программные точки останова работать не будут.

SPI-флеш и NAND-флеш

Логика загрузки из SPI-флеш и NAND-флеш одинакова, поэтому ниже речь будет идти только про SPI-флеш. Но все сказанное актуально и для NAND-флеш. Важное схемотехническое уточнение, касающееся SPI-флеш: при загрузке из SPI-флеш, активизируются одновременно оба сигнала SS порта MFBSP0, что следует учитывать при разработке электрической принципиальной схемы устройства.

Процедура загрузки из SPI-флеш и NAND-флеш

В отличие от параллельной флеш, которая отображается на адресное пространство CPU-ядра, выполнение программы непосредственно из флеш-памяти с интерфейсом SPI или NAND невозможно. Для того, чтобы исполнить программу, содержащуюся в такой флеш, эта программа должна быть первоначально скопирована в ОЗУ процессора. С одной стороны, этот факт устраняет дилемму о том, должна ли основная программа исполняться из ОЗУ, с другой – несколько усложняет задачу написания загрузчика.

В режиме загрузки из SPI- или NAND-флеш (далее мы будем говорить про SPI-флеш, но все сказанное справедливо и для NAND) само CPU-ядро работает точно так же, как если бы загрузка выполнялась из параллельной флеш. То есть, исполнение программы начинается с адреса 0xBFC0_0000. Однако порт внешней памяти в этом режиме после снятия nRST переходит в состояние, когда вместо содержимого памяти, подключенной к выводу nCS[3], процессору выдается программа, осуществляющая копирование 64 слов (32-разрядных)из SPI-флеш в начало области памяти CRAM, то есть, начиная с физического адреса 0x1800_0000. После этого программа переводит порт внешней памяти в его обычный режим (когда при обращениях в область физических адресов 0x1C00_0000-0x1FFF_FFFF выдается содержимое памяти, подключенной к nCS[3]), и совершает переход на виртуальный адрес 0x1800_0000 (он в этот момент отображается на физический адрес 0x1800_0000).

Программа, осуществляющая копирование 64 слов из флеш в ОЗУ, «прошита» в микросхему, и не может быть изменена. Поэтому перед программистом встает задача реализовать в этих 64 словах (то есть, в 64 инструкциях) загрузчик, осуществляющий загрузку основной программы.

Реализовать в программе такого объема загрузчик ELF-файла невозможно, или, как минимум, крайне трудоемко. Поэтому варианта остается два:

  1. Записывать основную программу в SPI-флеш в виде бинарного файла. Соответственно, если основная программа имеет разные секции в разных регионах памяти (скажем, в программе с использованием DSP-ядер секции данных и кода довольно сильно разнесены в памяти между собой), то для сокращения объема бинарного файла необходимо располагать эти секции подряд, а в коде основной программы осуществлять копирование секций по нужным адресам. Для этого должна использоваться команда AT в скриптах линковки. Пример такого использования можно увидеть в скриптах линковки uOS Project в среде разработки MCStudio 4. Сама команда AT и скрипты линковщика подробно описаны в документации на компоновщик LD.
  2. Сразу после первых 64 слов (назовем их «первичный загрузчик») располагать в SPI-флеш «основной загрузчик» - более объемную программу, которая уже будет загружать из SPI-флеш ELF-файл (или SREC), анализировать его и загружать секции по нужным адресам, после чего совершать переход на адрес точки входа.

Запись программы в SPI-флеш и NAND-флеш

Для записи в SPI-флеш можно использовать ту же процедуру, что и для записи в параллельную флеш, только функции EraseFlash(), WriteToFlash() и Verify() должны быть переделаны для работы с данным типом памяти.

В утилите MCPROG (на момент написания актуальная версия – 1.85) работа с SPI- и NAND-флеш не реализована, но запланирована.

Отладка программы, записанной в SPI-флеш и NAND флеш

В сущности, отладка программы, записанной в SPI-флеш, ничем не отличается от отладки программы, записанной в ОЗУ, так как исполняться она начинает только после попадания в оперативную память. Кроме того, равно как и в случае с параллельной флеш, в отладке, связанной с SPI-флеш, нуждается только загрузчик, а основная программа отлаживается независимо от флеш-памяти.

Проблема только в том, чтобы отладчик не предпринимал никаких действий, пока загрузчик не будет загружен в ОЗУ. Средствами MDB это можно сделать так:

reset
bp 0 0x18000000 # ставим точку останова в начале CRAM
run # запускаем программу и доходим до точки останова

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

Этот же результат может быть достигнут в MCStudio. Необходимо только в окне «Breakpoints» установить аппаратную точку останова по адресу 0x1800_0000. Но и это необязательно – даже этот первичный загрузчик может быть отлажен сразу студией, без записи в SPI-флеш.

Загрузка через периферийные порты

На момент написания данного документа, загрузка через периферийные порты возможна у процессоров 1892ВМ5Я (PCI, линковый порт), 1892ВМ7Я (PCI).

Загрузка через LPORT

Режим загрузки через LPORT (линковый порт) аналогичен режиму загрузки из SPI-флеш, но имеет два отличия:

  • скорость приема данных не должна превышать 2.5 МГц;
  • принимается 256 слов (а не 64);

Наличие данного режима загрузки обусловлено специфичными требованиями к данной конкретной разработке. На момент написания документа не запланировано введения такого режима в перспективные процессоры.

Загрузка через PCI

Процессоры серии «Мультикор», имеющие в своем составе контроллер PCI, имеют также режим загрузки через эту шину. В случае, когда такой режим включен, процессор после снятия сигнала nRST находится в состоянии останова и ждет записи стартового адреса в специальный регистр. После записи процессор стартует с записанного адреса. Объем программы, загруженной через PCI, не ограничивается. Строго говоря, непосредственно загрузка программы необязательна – в качестве стартового адреса можно записать просто 0xBFC0_0000, после чего процессор штатно стартует.

Пример использования загрузчика

В данном примере используется следующее оборудование и ПО:

  1. Отладочный модуль NVCom-02TEM-3U rev.1.2;
  2. Среда разработки MCStudio 4 NVCom-02T ver.19.16;
  3. Утилита для работы с энергонезависимой памятью MCPROG ver.1.90;
  4. Эмулятор MC-USB-JTAG.

В данном примере в качестве загрузчика используется загрузчик ELF-объектов.

Стоит отметить, что утилита MCPROG работает только с определенными типами памяти.

В данном примере загружаемая программа реализует последовательное мерцание светодиодов VD3, VD4 на отладочном модуле NVCom-02TEM-3U. Исходный код данной программы доступен в примерах проектов в составе среды разработки MCStudio 4.

Для воспроизведения данного примера необходимо выполнить следующие действия:

  1. Загрузить пример проекта MFBSP_GPIO в MC Studio 4 NVCom-02T: «Help → Welcome → Примеры проектов → MFBSP_GPIO → Finish»
  2. Выполнить сборку проекта MFBSP_GPIO: «Project → Build Project»
  3. Импортировать проект загрузчика в MC Studio 4 NVCom-02T: В «File → Import → MultiCore → MCS3 Project → Next → Browse» указать путь к директории предварительно загруженному и распакованному проекту загрузчика, после чего нажать кнопку Finish.
  4. Добавить дополнительный шаг сборки «mipsel-elf32-objcopy -O srec elfloader_NVCOM01MEM3U.elf elfloader_NVCOM01MEM3U.srec»:
    • В окне Project Explorer выбрать проект elfloader_NVCOM01MEM3U;
    • Нажатием комбинации клавиш Alt+Enter вызвать окно изменения свойств проекта;
    • В открывшимся окне перейти в «С/С++ Build → Settings → Build Steps»;
    • В графу Post-build steps Command вписать mipsel-elf32-objcopy -O srec elfloader_NVCOM01MEM3U.elf elfloader_NVCOM01MEM3U.srec, а в графу Description «elf to srec».
    • Apply → ОК
  5. Изменить значение переменной ElfObjectPtr в файле main.s на 0x1C000000
  6. Выполнить сборку проекта: «Project → Build Project»;
  7. Записать полученные файлы workspace\MFBSP_GPIO\MultiCore_Configuration_Debug\MFBSP_GPIO.elf и workspace\elfloader, _NVCOM01MEM3U\MultiCore_Configuration_Debug\elfloader_NVCOM01MEM3U.srec в энергонезависимую память, с помощью утилиты MCPROG:
 mcprog.exe –e2 MFBSP_GPIO.elf 0x1C000000
mcprog.exe –e2 elfloader_NVCOM01MEM3U.srec

По нажатию кнопки reset пронаблюдать попеременное мерцание светодиодов VD3, VD4.