Существенным неудобством учебной работы с софт-ядрами на недорогих отладочных FPGA платах является недостаток ресурсов из которых можно синтезировать ОЗУ (Мы точно не будем говорить про отладки Xilix). Нормально с памятью только на отладках с микросхемами Cyclone V (любезно мне подаренными @KeisN13, но их уже больше никому не дарит, а в продаже по гуманной цене их нет). Отладок на которых специально размещен SRAM можно пересчитать по пальцам одной руки, и все они мелкосерийные (у @checkpointнапример можно добыть). Имеющийся на Tang Nanon 9K PSRAM при ближайшем рассмотрении оказывается не такой уж и SRAM, и имеет свои сложности с работой.

В тоже время память по большей части используется как память программ, а имеющихся 16-20Кб синтезируемого ОЗУ было бы вполне достаточно для размещения стека и динамических данных(особенно в тестовых примерах )( а еще я где-то читал, что malloc для микроконтроллеров не комильфо, так что есть повод и кучу не делать). Почему бы для памяти программ не использовать какое-нибудь ПЗУ. К YRV с его узкой 16 битной шиной можно приспособить какую-нибудь микросхему 27C1024, но старые добрые времена с ультрафиолетом и загорелыми блондинками к сожалению прошли. Для PicoRV32 в PicoSoC эта проблема была решена использованием QSPI Flash и механизма XIP(Execute in Place). А почему бы не использовать для этого SD карту? (Будет почти как К574РФ5: достал, прошил, назад поставил).

Искушенный читатель скажет: "SD карту для XIP использовать нельзя, у нее адресация по- секторно", и будет совершенно прав - в отличии от XIP и QSPI Flash, где чтение идет "по-байтово", в случае же с SD картой придется вычитывать сектор в 512 байт целиком. А после некоторой паузы читатель добьет идею фразой: "Вообще-то, QSPI Flash работает по четырем линиям , а протокол работы по четырем линиям для SD карты является проприетарным, с SD картой только обычный SPI". В результате если XIP с QSPI Flash будет использоваться 6 тактов , то для для SD карты это будет... ну в общем да, засада....

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

  always @ (posedge clk) begin
    if (mem_trans[0]) begin
      mem_rdata[31:24] <= mcu_mem_bank3 [mem_addr[13:2]];
      mem_rdata[23:16] <= mcu_mem_bank2 [mem_addr[13:2]];
      mem_rdata[15:8]  <= mcu_mem_bank1 [mem_addr[13:2]];
      mem_rdata[7:0]   <= mcu_mem_bank0 [mem_addr[13:2]];
      end
    if (mem_wr_byte[3]) mcu_mem_bank3 [mem_addr_reg[13:2]] <= mem_wdata[31:24];
    if (mem_wr_byte[2]) mcu_mem_bank2 [mem_addr_reg[13:2]] <= mem_wdata[23:16];
    if (mem_wr_byte[1]) mcu_mem_bank1 [mem_addr_reg[13:2]] <= mem_wdata[15:8];
    if (mem_wr_byte[0]) mcu_mem_bank0 [mem_addr_reg[13:2]] <= mem_wdata[7:0];
    end

Если ОЗУ уменьшить и сделать размером в один сектор 512 байт , то если адрес попадет в этот сектор, ядро будет работать как и работало. А если не попадет, надо считать новый сектор, и там уж как повезет, может и подряд весь сектор отработает. Для управления этим процессом нужен сигнал шины mem_ready (он же HREADY оригинальной AHB-lite). А вся эта конструкция преобразуется в простой FSM , где byte_cnt это номер прочитанного байта из 512 штук. Это конечно не XIP, нам нужна область памяти в 512 байт, но исполнятся теоретически должно. (Конечно можно каждый раз среди 512 байт вычитывать нужное слово и поднимать HREADY, но это уж совсем занудство, всё-таки у нас с памятью не так все плохо).

Проектируемый FSM
Проектируемый FSM

Работу с SD картой я встречал в двух проектах это FPGA-SDcard-Reader и в курсе MIT 6.111. Проект FPGA-SDcard-Reader мне нравится больше, я его использовал для загрузки прошивки через стандартный загрузчик последовательного порта (который давным-давно еще для MIPS Fpga реализовал @YuriPanchul) в проекте YRV Plus. Но с ним есть одна проблема - он использует не SPI, а однобитный режим работы SD карты. В этом режиме надо свободные ноги карточки к высокому сигналу утягивать. Но в отличии от тех же Terasic DExx у Tang Nano 9K SD карточка подключается только в режиме SPI и две ноги DAT1 и DAT2 к ПЛИС не подключены (ну зато вопрос о работе с четырьмя сигналами данных отпадает сам собой).

Поэтому я взял пример из курса MIT6.111 и сделал небольшой тест для Tang Nano. В курсе пример с записью и чтением двух байт, запись мне не нужна , поэтому я сделал только чтение сектора. Давим на кнопку и читаем сектор, на светодиоды выводим первый байт сектора. Вроде все работает. Кроме того, что реализация конечных автоматов в этих примерах заслуживает красного маркера Клиффа Каммингса (действительно, интересно, а за что люди по 200K USD платят если им там такие FSM показывают?), и сам контроллер не стабильно инициализирует SD карту, реально инициализирует ее только после сброса питания (он это делает на частоте проекта, а не на 400КГц как того требует стандарт, на сколько я понял на каких-то платках с Xilix есть возможность управления питанием SD карты и эту неприятность там можно хардкорно исправить). Путем гугления этой проблемы я нашел на GitHub поправленный проект в котором инициализация SD более стабильна.

Единственное отличие от изначально задуманного в том что, конечный автомат имеет еще одно дополнительное состояние - START. Это состояние пинка контроллера SD карты, когда подаем сигнал read (rd), затем в состоянии READ когда начинаем вычитывать сектор сигнал rd опускаем вниз.

И так, сам контроллер SD и FSM у нас есть, имплементируем его в дизайн нашего микроконтроллера с RISC-V ядром YRV. Для этого используем проект basics-graphics-music, там есть процессор YRV Plus для Gowin и реализация индикации и клавиатуры на TM1638 и инфраструктура сборки. В своем репозитории я разместил все в паке labs/99_experimental/99_03_yrv_sd, сам FSM посмотреть тут (студентам не смотреть!). Дополнительно пришлось сделать свою конфигурацию для отладки (tang_nano_9k_tm1638_sd) , чтобы все пины работали на напряжении 3.3. вольта.

Проверим как работает контроллер. Для этого, по традиции, надо написать какой-то Hello World. Для вывода используем индикатор TM1638 и на четыре семисегментника в режиме динамической индикации будем выводить слово HEL(L)O (одну L опустим). В первый порт (port0) BCD-encoded значения сегментов, во второй (port1) номер семисегментника.

    # Инициализация адресов (port0 - сегменты, port1 - разряды)
    li s0, 0xFFFF0000    
    li s1, 0xFFFF0002    

main_loop:
    # --- СИМВОЛ 'H' (Разряд 3) ---
    li t1, 0x8           
    sh t1, 0(s1)         # Занулили
    .rept 100            # Так как это всетаки враппер на TM1638
    nop
    .endr
    li t2, 0b00110111    
    sh t2, 0(s0)         # Записали символ
    .rept 1000           # Пауза 
    nop
    .endr

    # --- СИМВОЛ 'E' (Разряд 2) ---
...

Сделаем так, чтобы вывод на каждый сегмент был в своем секторе SD карты.

Размещение команд на SD карте
Размещение команд на SD карте

Условие успеха - HELO должно было читаемым, таким образом контроллер будет тратить время на чтение сектора с карты, и по количеству оставшихся NOP сможем проанализировать сколько полезной вычислительной нагрузки сможет реализовать контроллер. Исходный код можно посмотреть тут. Для прошивки карточки использовал программу HxD, которую рекомендовали в курсе 6.111.

Hello World в HEX
Hello World в HEX

В общем получилось, что между вывод букв еще входит порядка 1000 команд, что в целом неплохо.HELO читаемо, хотя и в глазах рябит, сброс относительно стабилен, карточка инициализируется без сброса питания. Камера смартфона ловит все четыре символа (правда не каждый раз), а если вытащит SD карту, то ничего не работает.)

Итого — в проектах с софт‑ядрами, где скорость входных сигналов меняется не быстро (миллисекунды), SD карта для хранения исполняемого кода вполне применима. Можно задуматься о реализации кэша, пару килобайт для этого можно выделить, тогда исполнение кода будет более ровным.