Существенным неудобством учебной работы с софт-ядрами на недорогих отладочных 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, но это уж совсем занудство, всё-таки у нас с памятью не так все плохо).

Работу с 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 карты.

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

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