Как сделать свой 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 МГц.
 
Последнее редактирование:

ejsanyo

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

ejsanyo

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

mscs

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

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); //забиваем настройки кэша и команду, которая будет делать чтение из флэхи
}
 

ejsanyo

Active member
Вы изменили функцию HAL_SPIFI_SendCommand? Откуда у неё появилось ещё два параметра? Она же в оригинале такая:
Это я слил более свежий (наверно) HAL отсюда. Вроде каких-то фундаментальных отличий в нём не появилось: ну разделили буферы, да ввели таймаут...или всё же не всё так просто?
PS Попробовал откатиться на "старый" HAL. К сожалению ничего не изменилось.
 
Последнее редактирование:

mscs

Member
Может, при подаче питания он вначале сидит во втором режиме, а чтобы его перевести в командный, нужно что-то специфическое станцевать ещё?
У меня в загрузчике контроллер SPIFI перед началом выдачи команд чтения из SPI Flash сбрасывается (SPIFI_STA = 0x30) и настраивается (SPIFI_CTRL = 0xD081FFFF).
 

ejsanyo

Active member
У меня в загрузчике контроллер SPIFI перед началом выдачи команд чтения из SPI Flash сбрасывается (SPIFI_STA = 0x30) и настраивается (SPIFI_CTRL = 0xD081FFFF).
Ну вот SPIFI_STA = 0x30 это по сути эквивалентно SPIFI_CONFIG->STAT |= SPIFI_CONFIG_STAT_INTRQ_M | SPIFI_CONFIG_STAT_RESET_M; А вот значения SPIFI_CTRL у вас явно настроены под DMA-обмен, и, конечно, просто так оно не работает...

...а проблема была на самом деле в другом! 😁 Держите код:
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()
{
    uint8_t tmp;

    SPIFI_HandleTypeDef spifi;
    SPIFI_CommandTypeDef spifi_enable_qpi;
    SPIFI_MemoryCommandTypeDef spifi_qpi_memory_read;
    SPIFI_MemoryModeConfig_HandleTypeDef spifi_mem_config;

    spifi.Instance = SPIFI_CONFIG;
    HAL_SPIFI_MspInit(&spifi); //делает ноги флешечным интерфейсом!

    //сброс контроллера и его прерываний
    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, &tmp);

    //заполняем шаблон команды
    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" Полноценный QPI режим

    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); //забиваем настройки кэша и команду, которая будет делать чтение из флэхи
}
И сразу обращаем внимание на HAL_SPIFI_MspInit(&spifi); В котором, как несложно заметить, заложено переназначение ног.
Так что получается, что при подаче питания...просто не все флешечные ноги сразу переключались под SPIFI, а часть оставалось банальными GPIO!
 
Последнее редактирование:

mscs

Member
При запуске МК из SPI Flash выводы P2.0...5 автоматически переводятся в режим 1 (PAD2_CFG имеет начальное значение 0x555). При запуске из EEPROM или из ОЗУ режим указанных выводов по умолчанию 0 (GPIO), требуется перенастройка.
 

ejsanyo

Active member
При запуске МК из SPI Flash выводы P2.0...5 автоматически переводятся в режим 1 (PAD2_CFG имеет начальное значение 0x555). При запуске из EEPROM или из ОЗУ режим указанных выводов по умолчанию 0 (GPIO), требуется перенастройка.
И, кстати, особо жёсткий прикол в том, что когда мы, несмотря ни на что, переходим в режим работы эмулятором памяти, контроллер похоже сам собой переназначает нужные ему ноги.
 

mscs

Member
Переназначает при каждом аппаратном сбросе. Если отладочная приставка использует сигнал nRESET для сброса МК, например.
 

ejsanyo

Active member
Пардон, это я невнимательно ваш код смотрел. Ну значит теперь режим QPI работает без проблем?
Да, наконец-то заработало. Но важнее даже то, что сама отправка команд заработала! Ведь, понятное дело, пишущий во флешку бутлодырь без этого был бы нереализуем.
 
Сверху