Как сделать свой bootloader. Изучаем работу SPIFI

ejsanyo

Active member
Предлагаю делиться здесь опытом, кто как активирует работу с внешней флешкой (уж вряд ли кто-то пользуется прямой загрузкой с неё и дефолтным режимом работы при этом?). А в перспективе, может, кто-то уже сделал подобное тому, что обсуждалось тут?

Кратко напомню суть:
Чтобы код из внешней флешки выполнялся быстро и хорошо, лучше загрузиться вначале со встроенной EEPROM, запихнув туда хотя бы некий код, который настроит работу SPIFI контроллера побыстрее, а затем сделает переход в начала основной прошивки.

В качестве основы, пообрежем как следует пример, который пришёл к нам вместе с "Mikron IDE", получив таким образом минимально необходимый загрузчик:
C:
//Исходник main.c для минимального bootloader-а
#include "mik32_hal.h"
#include "mik32_hal_spifi.h"

void SPIFI_Config();

void main()
{
    // Конфигурация кэш памяти
    // SPIFI переключаем в 4 битный режим
    SPIFI_Config();

    /* Загрузка из внешней flash по SPIFI */
    asm volatile( "la ra, 0x80000000\n\t"
                    "jalr ra"
                );

    while (1)
    {
    }
}

void SPIFI_Config()
{
    SPIFI_HandleTypeDef spifi;
    SPIFI_MemoryCommandTypeDef spifi_quad_addr_memory_read;
    SPIFI_MemoryModeConfig_HandleTypeDef spifi_mem_config;

    spifi.Instance = SPIFI_CONFIG;

    //сброс контроллера и его прерываний
    SPIFI_CONFIG->STAT |= SPIFI_CONFIG_STAT_INTRQ_M | SPIFI_CONFIG_STAT_RESET_M;

    //сбрасывает регистры на дефолтное значение
    SPIFI_CONFIG->ADDR = 0x00;
    SPIFI_CONFIG->IDATA = 0x00;
    SPIFI_CONFIG->CLIMIT = 0x00000000;

    //заполняем шаблон команды
    spifi_quad_addr_memory_read.InterimLength = 3;
    spifi_quad_addr_memory_read.FieldForm = SPIFI_FIELDFORM_OPCODE_SERIAL;
    spifi_quad_addr_memory_read.FrameForm = SPIFI_FRAMEFORM_OPCODE_3ADDR;
    spifi_quad_addr_memory_read.InterimData = 0;
    spifi_quad_addr_memory_read.OpCode = 0xEB; //команда "Quad I/O Fast Read"

    spifi_mem_config.CacheEnable = SPIFI_CACHE_ENABLE;
    spifi_mem_config.CacheLimit = 0x90000000; //не кэшировать всё, чей адрес выше 2,5 гигов?
    spifi_mem_config.Instance = SPIFI_CONFIG;
    spifi_mem_config.Command = spifi_quad_addr_memory_read;
    HAL_SPIFI_MemoryMode_Init(&spifi_mem_config); //забиваем настройки кэша и команду, которая будет делать чтение из флэхи
}

Рассмотрим, что у нас тут происходит:

После включения питания у нас автоматически уже активна работа встроенного RC-тактового генератора, а также тактирование от него блока SPIFI. Поскольку мы пока не используем, скажем, UART на высокой скорости, этого нам достаточно, и никаких настроек в этой части не требуется.
Следующим шагом мы делаем сброс контроллера SPIFI. Не уверен насколько это необходимо на самом деле, но в примере это было.
Дальше самое главное - нам нужно заколотить шаблон команды в регистры SPIFI, который он будет использовать каждый раз, когда ядро через него полезет читать виртуальное адресное пространство основной прошивки.

Для тех, кто совсем не в курсе, поясню:

Поскольку нельзя так просто взять и приделать сложную память с последовательной структурой к более-менее классическому процессорному ядру, то этот "костыль" как раз таки реализует SPIFI модуль. Он сам вычитывает содержимое флешки, после чего подсовывает полученные данные в общее адресное пространство начиная с адреса 0x80000000. Так что, когда ядро RISC-V лезет туда, оно думает, что там обычное классическое ПЗУ. Но для этого нам требуется немного "обучить" SPIFI читать данные, а именно, объяснить ему, какой код команды, и какой её формат ему нужно отправлять флешке. Да, это раньше всё было просто: вот шина адреса, вот шина данных. Выставил адрес - получил результат. А теперь прям каждая флешка - это целый блок со своими командами и протоколом связи!🧐 Набор команд стандартизован и в целом общий между различными флешками и производителями. Но тем не менее возможности конкретных чипов могут различаться. Если внимательно почитать микроновский даташит, можно увидеть, что при подаче питания SPIFI изначально обучен выполнять команду "Read Data Bytes" aka "код 03h". Это самая базовая и самая медленная команда чтения, в которой обмен данными идёт в один поток. Её поддерживают все без исключения флешки. Но это, наверно, не совсем то, чего нам бы хотелось?
В приведённом выше примере мы обучаем SPIFI использовать уже команду "Quad I/O Fast Read" с кодом EBh. При её использовании сам код команды пропихивается по прежнему через один канал, но адрес и данные - уже через четыре канала! Эту команду в принципе поддерживает большая часть современных чипов, но здесь уже возможны нюансы конкретной реализации! Например, в чипе W25Q64 эта команда будет работать только тогда, когда в Регистре статуса 2 выставлен бит QE! Этот самый регистр там тоже выполнен в виде флеш-памяти, так что достаточно один раз тем или иным образом скорректировать его содержимое, и больше его не трогать. Более того, даташит нам рассказывает, что в чипах с исполненем W25Q64JVSSIQ этот бит выставлен уже на заводе-изготовителе - сразу бери и пользуй команду EBh. Но вот, скажем, в исполнении W25Q64FVSSIG этот бит изначально НЕ выставлен! Или вот, для чипа EN25Q80B в даташите написано, мол, что "команда EBh в нём работает всегда и независимо от состояний битов в регистре статуса". В общем, как-то они с этим не загонялись...:sneaky:
Помимо этого, микроновский даташит нам вещает, что SPIFI у нас имеет некие возможности по кэшированию данных, которые он читает из флешки. Эта тема недостаточно раскрыта, но в любом случае, не вижу причин не пользоваться данной фичей. Так что включаем.
И в конце всего - немного ассемблера. Закидываем в регистр ra начало адресного пространства, где сидит наш "костыль"-эмулятор SPIFI блока, и делаем jump по адресу в регистре. Дальше в игру вступает код основной прошивки.
 
Последнее редактирование:

ejsanyo

Active member
Ещё важно отметить такой аппаратный нюанс: для гарантии, что после подачи питания флеха будет сразу адекватно работать, рекомендуется предусмотреть на схеме подтяжку её ног "WP" и "HOLD" к Vcc с помощью внешних резисторов. Это соответствует ногам P2.4 (34) и P2.5 (33) контроллера. Сопротивление некритично, я обычно беру 10 кОм.
 

mscs

Member
Поделюсь своими субъективными суждениями.
Причин для использования ИМС Flash-памяти может быть три:
1. Недостаточный объем встроенной EEPROM МК;
2. Недостаточно длительный гарантированный срок хранения информации во встроенной EEPROM МК;
3. Необходимость обеспечения аппаратной защиты кода программы от искажения/стирания в результате программного сбоя или воздействия помехи в случае, если в процессе работы в энергонезависимую память должны записываться какие-либо данные.
При использовании ИМС Flash-памяти, если от МК требуется максимальная производительность, код программы необходимо копировать во встроенное ОЗУ МК полностью или частично (например, код обработки прерываний).
В двух проектах, один из которых в настоящее время на стадии завершения, а второй временно приостановлен по нетехническим причинам, объем встроенного ОЗУ МК является достаточным для размещения всего кода программы и всех структур данных. ИМС Flash-памяти подключена к МК через интерфейс SPIFI, работающий в режиме SPI (SPIFI_CS, _DATA0, _DATA1, _SCLK). Для МК задан режим старта из внешней памяти. После подачи питающего напряжения или снятия сигнала начальной установки МК начинает вычитывать команды из ИМС Flash-памяти, в начальных адресах которой размещен код специализированного загрузчика. Загрузчик состоит из трех ступеней. Первая загружает код второй во встроенное ОЗУ МК и передает ей управление. Вторая загружает третью с контролем целостности и повторным чтением при обнаружении искажения, например, в результате воздействия помех на линии интерфейса SPI. Получив управление, третья ступень загружает во встроенное ОЗУ МК код программы вместе с инициализированными структурами данных, также с контролем целостности.
В соответствии с требованиями конкретной задачи, вся процедура запуска занимает менее 10 мс. Объем загружаемого кода - порядка 6 килобайт (для второго проекта - порядка 10 килобайт). Гарантированный срок хранения информации в ИМС Flash-памяти - не менее 15 лет. Применена ИМС с аппаратной защитой области кода программы от записи/стирания.
Частота сигнала синхронизации интерфейса SPI - ~16 МГц (ограничена возможностями МК). Чтение кода программы выполняется постранично (по 256 байт) с использованием контроллера прямого доступа к памяти. Параллельно с чтением следующей страницы рассчитывается контрольная сумма считанной ранее страницы с использованием блока вычисления контрольных сумм. Накладные расходы - 1 килобайт Flash-памяти для хранения трех ступеней загрузчика и эталонных контрольных сумм за все используемые страницы. Область ОЗУ, в которой выполняется загрузчик, по завершении процесса загрузки можно использовать для размещения структур данных программы.
 

ejsanyo

Active member
А не желаете ли поделиться своими наработками со всем прогрессивным человечеством? :unsure: Вероятно, вы имеете гораздо больше опыта работы с флешками и с данным контроллером в целом.
 

mscs

Member
К сожалению, проект закрытый. Поделиться исходным текстом не могу.
 

mscs

Member
Выявлены следующие особенности работы узлов.
1. SPIFI. При чтении с использованием КПДП не выставляет последний запрос. Приходится задавать большее количество данных (например, 260 байтов вместо 256), а по завершении пересылки сбрасывать контроллер SPIFI. При записи такой проблемы нет.
2. КПДП. Регистры конроллера ПДП обновляются по первому запросу ПДП. При настройке КПДП значения регистров не изменяются, что на первых порах здорово сбивает с толку.
3. Узел расчета CRC. После записи начального значения нельзя сразу переключаться в режим загрузки данных, иначе результат расчета будет неверным. Необходимо вставить задержку хотя бы в один такт. Компилятор ставит эти команды подряд, во всяком случае, при включенной оптимизации.
4. Коммутационная матрица AHB. При линейном выполнении программы (без сбросов конвейера) из встроенного ОЗУ операции обращения к регистрам периферийных устройств выполняются за пять тактов процессора. Таким образом, максимальная частота меандра, формируемого программно, составляет ~32 МГц / 10 = ~3,2 МГц.
 
Последнее редактирование:

cryptozoy

Member
C:
SPIFI_HandleTypeDef spifi;
SPIFI_CommandTypeDef spifi_enable_qpi;
SPIFI_MemoryCommandTypeDef spifi_qpi_memory_read;
SPIFI_MemoryModeConfig_HandleTypeDef spifi_mem_config;

/* Параметры SPIFI на отправку внешней флэш-памяти команды 0x38 в режиме SPI. */
spifi_enable_qpi.Direction = SPIFI_DIRECTION_INPUT;
spifi_enable_qpi.InterimLength = 0;
spifi_enable_qpi.FieldForm = SPIFI_FIELDFORM_ALL_SERIAL;
spifi_enable_qpi.FrameForm = SPIFI_FRAMEFORM_OPCODE;
spifi_enable_qpi.OpCode = 0x38;

/* Переключаем внешнюю флеш-память командой 0x38 в режим QPI. */
HAL_SPIFI_SendCommand(&spifi, &spifi_enable_qpi, 0, 0, 0);

/* Настраиваем параметры SPIFI на чтение командой 0xEB внешней флэш-памяти EN25Q80B в режиме QPI. */
spifi_qpi_memory_read.InterimLength = 3; /* У флеш-памяти EN25Q80B количество тактов ожидания 6, а это 3 байта */
spifi_qpi_memory_read.FieldForm = SPIFI_FIELDFORM_ALL_PARALLEL;
spifi_qpi_memory_read.FrameForm = SPIFI_FRAMEFORM_OPCODE_3ADDR;
spifi_qpi_memory_read.InterimData = 0;
spifi_qpi_memory_read.OpCode = 0xEB;

/* Переключаем SPIFI в чтение автоматом адресации внешней флэш-памяти в режиме QPI с использованием кэш-памяти. */
spifi_mem_config.CacheEnable = SPIFI_CACHE_ENABLE;
spifi_mem_config.CacheLimit = 0x90000000;
spifi_mem_config.Instance = SPIFI_CONFIG;
spifi_mem_config.Command = spifi_qpi_memory_read;
HAL_SPIFI_MemoryMode_Init(&spifi_mem_config);

Из документации EN25Q80B количество тактов ожидания данных после адреса в QPI режиме 6, а это 3 байта. Проверьте сами. У флеш-памяти W25Q64FV и ей подобным всего 2 такта ожидания, то есть 1 байт. Кто найдёт флеш-память без тактов ожидания в режиме QPI при рабочей частоте 16 МГц, поделитесь ссылкой или документацией.
 
Последнее редактирование:

ejsanyo

Active member
Из документации EN25Q80B количество тактов ожидания данных после адреса в режиме QPI вроде бы 4, а это 2 байта.
Попробовал, и что-то оно, похоже, вообще не выполняет команду 38. Как работало SPIFI_FIELDFORM_OPCODE_SERIAL, так и работает, а с SPIFI_FIELDFORM_ALL_PARALLEL всё подвисает. Или я опять что-то делаю не так...
Ещё заметил такой момент, что в момент сразу после прошивки оно таки работает, а вот если потом передёрнуть питание - нет. Может, скрипт прошивки делает дополнительно ещё какие-то настройки?
 

cryptozoy

Member
C:
/* Настраиваем параметры SPIFI на чтение командой 0xEB внешней флэш-памяти EN25Q80B в режиме QPI. */
spifi_qpi_memory_read.InterimLength = 3; /* У флеш-памяти EN25Q80B количество тактов ожидания 6, а это 3 байта */

Мудрёное переключение в «Enhance Performance Mode» (режим улучшенной производительности). Попробуйте пока 3 байта поставить как и в режиме QSPI Quad Input/Output. Позднее подкину ещё кода для опытов.
 

cryptozoy

Member
C:
/* Настраиваем параметры SPIFI на чтение командой 0xEB внешней флэш-памяти EN25Q80B в режиме QPI. */
spifi_qpi_memory_read.InterimLength = 1;
spifi_qpi_memory_read.FieldForm = SPIFI_FIELDFORM_ALL_PARALLEL;
spifi_qpi_memory_read.FrameForm = SPIFI_FRAMEFORM_OPCODE_3ADDR;
spifi_qpi_memory_read.InterimData = 0xA5;
spifi_qpi_memory_read.OpCode = 0xEB;

В документации на страницах 28-30 объясняется, что режим «Enhance Performance Mode» (режим улучшенной производительности) у флеш-памяти EN25Q80B включается передачей одного байта промежуточных данных, в котором старший полубайт с младшим должны быть инверсными, например: A5h, 5Ah, F0h или 0Fh. Вопрос в том, включается ли он сразу или только в следующем цикле. Инструкция написана мутновато. Надо пробовать. Хотя может быть это никакое не сокращение тактов, а просто сигнал для флешки работать с большими токами ключей, чтобы выдавать данные на максимальных частотах. Поэтому если не запустится, то значит 3 байта промежуточных данных это её судьба. :)
 
Последнее редактирование:

ejsanyo

Active member
В документации на странице 28 объясняется, что режим «Enhance Performance Mode» (режим улучшенной производительности)
Про это я практически сразу понял и сразу поставил InterimLength = 3. И сразу убедился, что если загрузка подвисает, то даже не важно, что там стоит. Проблема-то похоже в том, что не срабатывает команда включения QPI. Смешная ситуация в том, что когда я пытаюсь войти в дебажный режим, как водится, сначала отладчик прошивает код в EEPROM...и после этой самой прошивки всё работает, и HAL_SPIFI_SendCommand возвращает HAL_OK 🤪, и код из внешней флешки нормально выполняется. Не понятно, как отловить баг.
Впрочем, не то чтобы это было прям критично. В "последовательно-параллельном варианте" команды 0xEB основная прошивка, на первый взгляд, исполняется уже как будто-бы не медленнее, чем из Епромки.
 

cryptozoy

Member
У вас случайно не в ОЗУ прошивка идёт? И запуск тоже с него? И ещё, флешка не сбрасывается после перезапуска микроконтроллера, поэтому ей нужно отправлять соответствующие команды. Иначе после переключения в режим QPI она не будет работать в других режимах, так как команды будет принимать по четырём линиям данных. EN25Q80B нужно подавать команду выхода из «Enhance Performance Mode» и QPI, то есть дважды по «0xFF». Или сброс сделать командами «0x66», а затем «0x99». Естественно команды должны передаваться в режиме «SPIFI_FIELDFORM_ALL_PARALLEL».
 
Последнее редактирование:

cryptozoy

Member
В "последовательно-параллельном варианте" команды 0xEB основная прошивка, на первый взгляд, исполняется уже как будто-бы не медленнее, чем из Епромки.
Это работа SPIFI кэш. Но размер её маленький (1 кБайт), поэтому при частых дальних переходах по адресному пространству программы быстродействие будет заметно проседать из-за разрыва последовательного чтения, которое вынуждает повторно отправлять команду чтения, адрес и промежуточные данные (такты ожидания выдачи данных). В режиме QPI просадка эта будет меньше всех, так как команда чтения короче на 6 тактов. Кстати W25Q64FV и ей подобные экономят ещё 4 промежуточных такта по сравнению с EN25Q80B.
 
Последнее редактирование:

mscs

Member
В "последовательно-параллельном варианте" команды 0xEB основная прошивка, на первый взгляд, исполняется уже как будто-бы не медленнее, чем из Епромки.
Предположим, SPIFI кэш не используется, код выполняется из QPI Flash, частота интерфейса максимальная - fCPU / 2 (ограничение МК), чтение последовательное, нет накладных расходов, связанных с выдачей кода команды Flash и адреса. Для команд типа регистр-регистр с 16-разрядными кодами время выполнения составит 8 тактов; с 32-разрядными кодами - 16 тактов.
Использование кэша улучшит картину, но нарушит детерминизм поведения МК: время реакции станет носить вероятностный характер, в результате можно получить редкие сбои, которые очень трудно будет анализировать и отлаживать. Стоит ли создавать предпосылки для такого поведения?
 

cryptozoy

Member
Использование кэша улучшит картину, но нарушит детерминизм поведения МК: время реакции станет носить вероятностный характер, в результате можно получить редкие сбои, которые очень трудно будет анализировать и отлаживать. Стоит ли создавать предпосылки для такого поведения?
Это какие такие «редкие сбои»?
 

mscs

Member
В моей практике бывали случаи, когда приходилось фиксировать фрагменты кода в кэше команд ARM-процессора, запрещая их вытеснение, чтобы обеспечить детерминированное время реакции на прерывание.
 

ejsanyo

Active member
У вас случайно не в ОЗУ прошивка идёт?
С EEPROM. А уж после включения питания запуск-то точно с него идёт.
Вот экспериментировал тут, 100% вижу, что именно после включения питания никакая вообще команда функцией HAL_SPIFI_SendCommand() не выполняется, хоть ты тресни. А вот сразу же после того, как я прошивку залил и OpenOCD сделал запуск выполнения кода - всё выполняется! Может таки SPIFI контроллер совсем не может работать одновременно и в режиме команд и в режиме прозрачной памяти? Может, при подаче питания он вначале сидит во втором режиме, а чтобы его перевести в командный, нужно что-то специфическое станцевать ещё?
 

ejsanyo

Active member
Скиньте исходный код загрузчика, который в данный момент не работает.
Да всё практически то же, что и ранее.
C:
#include "mik32_hal.h"
#include "mik32_hal_spifi.h"

void SPIFI_Config();

void main()
{
    // Конфигурация кэш памяти
    // SPIFI переключаем в полный QPI режим
    SPIFI_Config();

    /* Загрузка из внешней flash по SPIFI */
    asm volatile( "la ra, 0x80000000\n\t"
                    "jalr ra"
                );

    while (1)
    {
    }
}

void SPIFI_Config()
{
    SPIFI_HandleTypeDef spifi;
    SPIFI_CommandTypeDef spifi_enable_qpi;
    SPIFI_MemoryCommandTypeDef spifi_qpi_memory_read;
    SPIFI_MemoryModeConfig_HandleTypeDef spifi_mem_config;

    spifi.Instance = SPIFI_CONFIG;

    //сброс контроллера и его прерываний
    SPIFI_CONFIG->STAT |= SPIFI_CONFIG_STAT_INTRQ_M | SPIFI_CONFIG_STAT_RESET_M;

    //сбрасывает регистры на дефолтное значение
    SPIFI_CONFIG->ADDR = 0x00;
    SPIFI_CONFIG->IDATA = 0x00;
    SPIFI_CONFIG->CLIMIT = 0x00000000;

    spifi_enable_qpi.Direction = SPIFI_DIRECTION_INPUT;
    spifi_enable_qpi.InterimLength = 0;
    spifi_enable_qpi.FieldForm = SPIFI_FIELDFORM_ALL_SERIAL;
    spifi_enable_qpi.FrameForm = SPIFI_FRAMEFORM_OPCODE;
    spifi_enable_qpi.InterimData = 0;
    spifi_enable_qpi.OpCode = 0x38; //команда "Enable QPI"

    //врубаем QPI-режим
    HAL_SPIFI_SendCommand(&spifi, &spifi_enable_qpi, 0, 0, 0, 0, HAL_SPIFI_TIMEOUT);

    //заполняем шаблон команды
    spifi_qpi_memory_read.InterimLength = 3;
    spifi_qpi_memory_read.FieldForm = SPIFI_FIELDFORM_ALL_PARALLEL;
    spifi_qpi_memory_read.FrameForm = SPIFI_FRAMEFORM_OPCODE_3ADDR;
    spifi_qpi_memory_read.InterimData = 0;
    spifi_qpi_memory_read.OpCode = 0xEB; //команда "Quad I/O Fast Read"

    spifi_mem_config.CacheEnable = SPIFI_CACHE_ENABLE;
    spifi_mem_config.CacheLimit = 0x90000000; //не кэшировать всё, чей адрес выше 2,5 гигов?
    spifi_mem_config.Instance = SPIFI_CONFIG;
    spifi_mem_config.Command = spifi_qpi_memory_read;
    HAL_SPIFI_MemoryMode_Init(&spifi_mem_config); //забиваем настройки кэша и команду, которая будет делать чтение из флэхи
}
 
Сверху