РоссияНовостьМодели
Doom, System Shock и Ultima VII на ПЛИС за $15: компьютер из 8640 логических ячеек
Процессор RISC-V на 36 МГц с кешами, 8 МБ памяти, HDMI 720×480 при 60 Гц со стереозвуком 48 кГц, два порта USB и SD-карта. Всё это собрано из 8640 таблиц поиска (LUT) отладочной платы с ПЛИС за 15 долларов. Чтобы погонять систему как следует, я портировал на неё Doom, Descent, Tyrian, System Shock и Ultima VII, и все они идут вполне прилично.
От переводчика: это объединение первой и шестой частей серии m9b. В первой автор описывает устройство компьютера, в шестой рассказывает, как на него переносили игры. Части о процессоре, контроллере памяти, HDMI и USB-хосте остались в оригинале, ссылки на них сохранены.
Видео с платы: Doom, timedemo и игра · Descent, первый уровень · Tyrian на полной скорости · System Shock
В этих статьях я разбираю устройство системы: что вошло в микросхему, на что ушли логические ячейки, на каких частотах удалось запустить разные блоки и как игры подгоняли под получившуюся машину.
Семейство Tang Nano
Платы Tang Nano от Sipeed — это ПЛИС Gowin на плате размером с пластинку жвачки, с портом USB-C для прошивки и набором разъёмов, который зависит от модели. По объёму логики четыре платы различаются почти в двадцать раз:
Площадь каждого квадрата равна числу 4-входовых таблиц поиска (LUT4) на плате. Жёлтые прямоугольники — кристаллы памяти в корпусе. Они нарисованы одного размера, потому что на каждой плате по 8 МБ; у 9K эта память разделена на два кристалла. Cortex-M3 у 4K — аппаратное ядро: оно расположено рядом с логикой, а не собрано из неё.
Плата
ПЛИС
LUT4
Блочная память
Память в корпусе
Аппаратный CPU
Tang Nano 1K
GW1NZ-1
1152
8 КБ
нет
нет
Tang Nano 4K
GW1NSR-4C
4608
20 КБ
8 МБ HyperRAM
Cortex-M3
Tang Nano 9K
GW1NR-9
8640
52 КБ
8 МБ PSRAM (2 кристалла)
нет
Tang Nano 20K
GW2AR-18
20 736
92 КБ
8 МБ SDRAM
нет
Блочная память считается по 2 КБ на блок, без битов чётности.
Основная часть ретрокомпьютерных проектов на Tang Nano живёт на 20K: ядер под неё довольно много. 1K хватает на маленький процессор, но не на компьютер вокруг него, к тому же у неё, в отличие от остальных плат серии, нет разъёма HDMI. У 4K прямо в кремнии есть Cortex-M3, и это была бы отличная основа для небольшого компьютера, но из-за того, как там устроена шина адреса, с ним сложно работать. Про эту плату я, скорее всего, ещё напишу.
У 9K аппаратного процессора нет. Если процессор нужен, его придётся собирать из тех же 8640 ячеек, что и всё остальное. Эту задачу я и выбрал, поставив себе несколько непреложных условий:
- Никаких дополнительных вычислителей. Соблазнительно перепрошить и задействовать BL702, но этого не будет. Работает только ПЛИС.
- Компьютер общего назначения. Игры служат тестами, и специальных аппаратных ускорителей под какую-то одну из них я делать не хочу.
- Настоящий ввод и вывод. HDMI со звуком, USB-клавиатуры и мыши, SD-карта с настоящей файловой системой.
- Портировать готовое. Взять существующий исходный код на C/C++ и перенести его на плату. Немного оптимизации допустимо, но игра должна выглядеть, звучать и вести себя в точности как оригинал.
Вот и всё. Отправной точкой могла бы стать готовая система, сгенерированная LiteX: он эту плату поддерживает. Но производительность результата, увы, довольно ужасная (по крайней мере, на момент написания).
Три кристалла в одном корпусе
Микросхема на плате, GW1NR-LV9QN88P, на самом деле состоит из трёх кристаллов в одном корпусе: самой ПЛИС и двух PSRAM по 32 Мбит (4 МБ каждая). У каждой PSRAM своя 8-битная шина с удвоенной скоростью передачи данных (DDR) до ПЛИС, рассчитанная на частоту до 166 МГц. Ни один из этих проводников не выходит за пределы корпуса: на плате микросхема выглядит как обычная ПЛИС с 88 выводами.
Два кристалла PSRAM сидят на разных шинах, поэтому могут передавать данные одновременно. В m9b оба работают на 144 МГц синхронно, в одном такте.
- 8640 4-входовых таблиц поиска;
- 52 КБ блочной памяти: 26 блоков по 2 КБ;
- 8 МБ PSRAM на двух кристаллах;
- 664 МБ/с — пиковая пропускная способность PSRAM на бумаге: 166 МГц × 2 фронта × 2 кристалла.
Эти цифры задают рамки для всего остального. 52 КБ блочной памяти — меньше, чем было у типичного домашнего компьютера 1980-х. Один кадр VGA, какой используют наши тестовые игры, занимает 320 × 200 байт и не поместился бы даже во все 26 блоков вместе. Поэтому программы и их данные живут в PSRAM, а блочная память уходит на кеши и буферы. 664 МБ/с у PSRAM — число на бумаге: в третьей части я прослеживаю его вплоть до того, что реально достаётся процессору, а это лишь малая доля.
Никакой выделенной аппаратуры для остального нет. Сериализаторы в микросхеме достаточно быстрые для HDMI и памяти, но кодировщик собран из логики. Звук передаётся внутри сигнала HDMI, так что это тоже логика. USB-хоста на плате нет: разъёмы USB подключены прямо к выводам ввода-вывода, и протокол тоже обрабатывает логика.
Система
Вот весь компьютер, разбитый по тактовым доменам.
Пунктиром обведены тактовые домены, жёлтым отмечена блочная память или PSRAM, красным — то, что находится на плате, вне микросхемы. Мост и буферная память передачи связывают домены: мост — процессор и контроллер, у которых общее дерево тактирования, а буферная память — контроллер и видеотракт, у которых оно разное.
Верхний уровень устроен довольно просто. Я взял высокопроизводительное ядро RISC-V (VexiiRiscv) и отдал заметную часть блочной памяти под щедрый кеш (24 КБ из наших 52), потому что задержка DDR-памяти довольно пугающая, и обращаться к ней слишком часто совсем не хочется. Во второй части возможности процессора и устройство подсистемы разобраны гораздо подробнее.
Контроллер PSRAM работает с обоими кристаллами синхронно: он формирует один поток команд и дублирует его на оба кристалла. Физический уровень (PHY) — просто набор сериализаторов и десериализаторов. Внутреннее устройство контроллера описано в третьей части. Кроме того, контроллер PSRAM связан с графическим и звуковым блоком через буферную память передачи, в которой помещаются две строки видеоданных и два звуковых буфера. У видеоблока есть и собственная память для палитры и таблицы строк; процессор пишет в неё напрямую, чтобы сократить задержку. Кодировщик HDMI — простой TMDS-кодировщик, который превращает пиксельные данные в сигнал HDMI. Эту подсистему разбирает четвёртая часть. USB-хост — ядро на микрокоде, которое обрабатывает протокол USB и даёт процессору простой регистровый интерфейс; ему посвящена пятая часть. Набор встроенной периферии завершают UART для отладки и SPI для работы с флеш-памятью и SD-картой.
Тактирование
Все тактовые частоты получаются из единственного кварца платы на 27 МГц через две ФАПЧ (PLL) микросхемы и три её делителя. Некоторые частоты нам неподвластны, и их нужно соблюдать, иначе система не заработает. Одна из них — пиксельная частота стандартного режима VESA (для выбранного мной режима 720×480 это 27 МГц). Значит, для сериализаторов нужна частота 5 × 27 = 135 МГц. Другая — 12 МГц для USB, стандартная для full-speed USB. Все остальные частоты приходится получать делением или умножением этих, причём обе ФАПЧ уже заняты: 12 и 27 не кратны друг другу. В итоге всё оборудование я тактировал кратными частоты USB: 36, 72 и 144 МГц. Хорошо согласованные частоты очень важны, потому что каждое пересечение тактовых доменов стоит задержки.
Сигнал
МГц
Откуда
Почему такое значение
Битовая частота TMDS
135
pll_hdmi: 27 × 5
HDMI передаёт 10 бит на пиксель по каждому каналу: 270 Мбит/с, по обоим фронтам 135 МГц
Пиксельная частота
27
135 ÷ 5
720 × 480 при 59,94 Гц — это режим на 27 МГц. Получена делением битовой частоты, поэтому два такта сериализаторов синфазны по построению
Ввод-вывод PSRAM
144
pll_main: 27 × 16 ÷ 3
Номинал 166 МГц; 144 — самая высокая частота, которая ещё делится до 48 для USB
Контроллер PSRAM
72
pll_main ÷ 2
Сериализаторы ввода-вывода принимают по 4 бита на вывод за такт контроллера
Процессор
36
72 ÷ 2
Столько, сколько процессор выдерживает по таймингам (37,8 МГц в последней сборке), и синхронно с контроллером
USB
12
pll_main ÷ 3, затем ÷ 4
Low-speed USB — это 1,5 Мбит/с с выборкой 8 раз на бит: ровно 12 МГц
Куда уходят ячейки
После размещения и трассировки проект занимает 7560 из 8640 логических ячеек (88%) и 25 из 26 блоков памяти. На деле ограничение жёстче: ячейки упаковываются парами в слайсы, и заняты 97% этих пар. При таком заполнении итоговая частота сборки сильно зависит от того, куда инструменты случайно поставят логику, поэтому каждая сборка заканчивается изучением отчёта о таймингах.
Блок
Логических ячеек
Блоков памяти
Что хранится в блочной памяти
Процессор (с кешами)
≈ 4340
18
L1 данных: 8; теги L1 данных: 2; L1 выборки команд: 4; теги L1 выборки: 1; предсказатель переходов: 1; регистры: 2
Кодировщик HDMI
≈ 630
–
Видеоблок, видеопамять
≈ 490
2 + 4
палитра и таблица строк: 2; буферная память передачи (строки, звук): 4
Контроллер PSRAM, мост
≈ 560
–
строчные буферы моста — в LUT-памяти
USB-хост, два порта
≈ 490
1
ПЗУ микрокода
UART, SPI, чтение флеш-памяти
≈ 270
–
Связующая логика: декодирование адресов ввода-вывода, пересечения доменов, сбросы
≈ 690
–
Цифры по блокам взяты из синтеза и приблизительны: размещение и трассировка оптимизируют проект поверх границ блоков, итог по ним — те самые 7560.
Больше половины микросхемы занимает процессор. Всё остальное я сделал настолько компактным, насколько смог, и по дороге пожертвовал возможностями, которые не были нужны прямо сейчас. Например, в первых версиях был аппаратный текстовый режим, но его пришлось вырезать (заодно на стартовом экране появился более симпатичный пропорциональный шрифт). Ещё я убрал любой синтез звука, оставив только простой вывод волновых данных.
Игры: ретро наоборот
Все пять игр написаны для DOS-компьютеров начала 1990-х. Их современные порты рассчитаны на современную машину. Чтобы запустить их на m9b, часть этой модернизации пришлось откатить и посадить игры на диету по памяти, тактам и звуковому оборудованию, которая местами строже, чем на тех ПК, где они вышли впервые.
Игра
Порт
Скорость на m9b
Что оказалось важнее всего
Doom (1993, shareware)
doomgeneric
≈ 22 fps (timedemo)
процедуры отрисовки столбцов и полос; кеш данных
Descent (1995, shareware)
D1X Rebirth 0.40
≈ 10 fps, было 5
деления в наложении текстур; кабина
Tyrian 2.1 (1995)
OpenTyrian
35 fps, полная скорость, было 17
одна проверка границ на каждый пиксель
System Shock (1994)
Shockolate
≈ 7,4 fps, было 5,8
память: ролики на 17 МБ; невыровненные чтения; деления
Ultima VII (1992), обе игры
Exult 1.12.1
7–13 fps
память: куча 6,2 МБ в объёме меньше 6 МБ
Ultima VII, оригинальный движок
Ultima7_Decomp
8–10 fps (предел самого движка — 10)
блоки кучи, выровненные по слову; спрайты и тайлы по слову за раз
m9b выполняет команды строго по порядку, не имеет блока плавающей точки, работает на 36 МГц, и 8 МБ его памяти скрыты за кешами, промах в которых стоит около 25 тактов (третья часть). Звукового чипа и MIDI-синтезатора у него нет. Зато есть 32-битные аппаратные умножитель и делитель, достаточно быстрая SD-карта и видеоблок, который бесплатно переключает страницы и масштабирует изображение.
Программы именно портированы. Каждая игра — это её исходный код из основного репозитория, урезанный до того, что нужно для сборки, с пометкой на каждом изменении, и собственный платформенный слой вместо SDL.
Все пять игр используют общую небольшую среду выполнения, изначально написанную для Doom:
- Библиотека C и файлы. picolibc, системные вызовы которой отображены на консольный UART и на FatFs, так что open, read и lseek работают с SD-картой. Порты на C++ (физика System Shock, весь Exult) используют libstdc++ из picolibc, включая исключения.
- Обработчик ловушек. Он распределяет прерывания и эмулирует невыровненные загрузки и сохранения: процессор на них выдаёт исключение, а старый игровой код полон таких обращений.
- Видео. Каждая игра рисует свой экран 320 × 200 с 8-битным цветом, как и раньше. Порт копирует его в один из трёх буферов отображения и вычищает из кеша данных, а прерывание обратного хода кадра направляет на него таблицу строк. Каждая строка удваивается по горизонтали и выводится на две или три строки экрана: получается та же картинка 4:3, что давал VGA-монитор (четвёртая часть).
- Звуковые эффекты смешиваются в звуковом прерывании на частоте 12 или 24 кГц вместо 48, и каждое значение повторяется на 4 или 2 выходных отсчёта. Сами эффекты изначально записаны с частотой 11 или 22 кГц, так что ничего не теряется. Смешивание же стоит несколько процентов процессорного времени вместо десятой части.
- Загрузка. Программы хранятся на SD-карте и выбираются в загрузочном меню. Драйвер SD читает последовательности секторов одной командой, по 32 бита за раз: по мере его доработки загрузка уровня в Doom сократилась с 4,6 с до 1,7 с.
Музыку во всех пяти играх писали под синтезатор: FM-чип Yamaha OPL на AdLib или Sound Blaster либо Roland MT-32. Ни один из них не помещается. Открытому ядру OPL2 на этой микросхеме нужно 1337 ячеек и 4 блока памяти, а свободно примерно тысяча ячеек и один блок. Программный синтезатор отнял бы 10–15% процессора ещё до того, как игра что-нибудь нарисует, а эмуляторы в самих портах считают в плавающей точке.
Поэтому музыка один раз рендерится на ПК, а во время игры проигрывается потоком с SD-карты. Для четырёх игр это эмуляция чипа OPL3 из libADLMIDI, которая играет собственными инструментами каждой игры и по правилам громкости её звукового драйвера: банк GENMIDI у Doom, банки HMI у Descent, тембры AIL у System Shock. Tyrian рендерится собственным проигрывателем и эмулятором OPL. Для Ultima VII взяты записи Exult с настоящего Roland MT-32, того синтезатора, под который музыку и писали. Каждый трек превращается в WAV 24 кГц моно с общей громкостью и проигрывается через кольцевой буфер в PSRAM: основной цикл его пополняет, а звуковое прерывание опустошает.
Объёмы получаются немаленькие: 81 МБ для Doom, 69 для Descent, 128 для Tyrian, 95 для System Shock, 208 для Ultima VII. Но SD-карты большие, так что проблемой это не стало.
System Shock потребовал ещё одного шага. Музыка в нём адаптивная: каждая тема состоит из 54 коротких фрагментов или меньше, и музыкальный ИИ запускает их максимум в восьми потоках (основная партия и слои поверх неё), выбирая продолжение по тому, что происходит в игре. Каждый фрагмент отрендерен отдельно (227 файлов), а порт реализует интерфейс музыкального драйвера поверх них: собственный ИИ игры по-прежнему выбирает фрагменты, порт проигрывает до восьми из них одновременно и смешивает.
Основная работа Doom идёт в двух циклах: R_DrawColumn рисует стены и спрайты по вертикальным столбцам, а R_DrawSpan — полы и потолки по горизонтальным полосам. Вместе они занимают 40% кадра.
Изменение (процессор тогда работал на 27 МГц)
2000 кадров
fps
Исходная версия
181,0 с
11,0
Процедуры отрисовки столбцов и полос: локальные переменные, 4 пикселя за итерацию
157,2 с
12,7
Переключение страниц в прерывании обратного хода кадра
144,5 с
13,8
Отключение проверок диапазонов
141,3 с
14,2
Отрисовка прямо в буферы отображения
138,0 с
14,5
Дальше эстафету приняло железо: процессор перешёл на синхронную работу на 36 МГц, у кеша данных появился второй путь (двухпутная ассоциативность), чтение с SD ускорилось, memset и memcpy получили cbo.zero, появился делитель radix-4. Сейчас timedemo идёт примерно на 22 fps. Пока в кеше был один путь, сдвиг буфера на несколько сотен байт менял скорость Doom на 2%: один столбец экрана затрагивает 200 строк кеша, и за них дралось всё остальное, что было горячим.
Descent — настоящая трёхмерная игра с перспективно-корректным наложением текстур, и написанная на C процедура наложения в D1X делит на глубину для каждого пикселя, по 70 тактов и больше на деление. Поначалу наложение текстур занимало две трети времени, а игра шла на 5 fps.
- Наложение текстур теперь вычисляет точные текстурные координаты раз в 16 пикселей по одной обратной величине, а между ними шагает линейно. Обе координаты упакованы в одно слово, как в процедуре отрисовки полос Doom, четыре пикселя за итерацию.
- Деления 64 на 32 бита, которые оригинал делал на ассемблере, теперь занимают два аппаратных деления вместо побитового цикла.
- Кабина — полноэкранный битмап с прозрачным окном под трёхмерный вид, и D1X перерисовывал её со всеми приборами каждый кадр. Теперь её непрозрачные участки рендерятся один раз, перерисовывается она только там, куда может дотянуться трёхмерный вид, а наложение текстур пропускает куски строк, которые кабина всё равно закроет. Кабина и приборы занимали 8% времени, теперь 1%.
- Опрос клавиш при каждом запросе проверял все 256 клавиш и отнимал 6% времени; теперь проверяются только нажатые.
На первом уровне с полной кабиной выходит около 10 fps.
Профиль Tyrian показал, что две трети времени уходят на одну функцию: она рисует три прокручивающихся слоя фона по байту за раз и сверяет каждый пиксель с краями экрана. Теперь для строк, целиком лежащих на экране, проверки пропускаются, и скорость выросла с 17 до 35 fps. Это полная скорость игры, заданная её собственным таймером, и ещё остаётся запас. Музыку Tyrian тоже пришлось рендерить: эмулятор OPL в OpenTyrian считает огибающие в плавающей точке на каждый отсчёт.
Shockolate — это портируемая версия исходников для Macintosh, которые Night Dive выпустила в 2018 году. Версия для Mac, а вслед за ней и порт рассчитывали на гораздо больший объём памяти, чем DOS-версия 1994 года: запуск первого уровня требовал 5,3 МБ кучи, а ролик — 17 МБ. В m9b для игры есть 5,1 МБ кучи.
- Таблица ресурсов содержала запись для каждого из 32 768 возможных идентификаторов, это 1,1 МБ. Идентификаторы System Shock доходят примерно до 5500.
- Составные ресурсы (варианты текстуры разных размеров, кадры существа) читались целиком, даже когда нужен был один элемент, и этот элемент распаковывался в копию. Теперь читается только индекс, а каждый элемент — при первом обращении.
- Ролики загружались целиком (14 МБ на заставку), их звук распаковывался ещё в 2,5 МБ, и всё это оставалось в памяти после просмотра. Теперь они читаются из файла по ходу воспроизведения, и используются ролики низкого разрешения из DOS-версии: декодировать их вчетверо меньше.
Теперь первый уровень занимает 2,8 МБ, ролик — около 0,2 МБ. Скорость выросла за счёт двух вещей. Распаковщик текстур читал слова по нечётным адресам, и на него приходилось 97% невыровненных обращений, которые эмулировал обработчик ловушек. А деления с фиксированной точкой шли через программную 64-битную процедуру вместо аппаратного делителя. После этих исправлений и с делителем radix-4 в процессоре сохранённая игра ускорилась с 5,8 до 7,4 fps. Двойным щелчкам понадобилось отдельное исправление: при 7 fps оба щелчка успевали произойти между двумя опросами мыши, поэтому теперь кнопки опрашиваются в звуковом прерывании вместе с временем нажатия.
Оригинальная Ultima VII — это 16-битный код реального режима на собственном менеджере памяти Origin, и исходников у него нет. Exult заново реализует её движок на C++17 и играет на оригинальных данных. Запуск The Black Gate требовал 6,2 МБ кучи. m9b может выделить ей 5,8 МБ, а распределитель памяти из библиотеки C терял ещё 540 КБ на дырах, пока его не заменил malloc Дага Ли.
- Usecode — скрипты игр, 1,5 МБ байт-кода — читались целиком при старте. Теперь каждая функция читается, когда до неё доходит вызов, а давно не использовавшиеся выгружаются, чтобы держать в памяти не больше 192 КБ.
- Спрайты, прочитанные из файла фигур на 7,3 МБ, оставались в памяти навсегда. Теперь сверх 384 КБ выгружаются давно не использовавшиеся.
- Ландшафт занимал 4 КБ на каждый вид фрагмента карты, а в Тринсике их 120; теперь это 3 байта на тайл. Отрисованные полы кешируются для 24 фрагментов вместо 100.
Через несколько минут игры в Тринсике пик потребления — 4,1 МБ, и ещё остаётся место для музыки MT-32 и звуковых эффектов. Игра идёт на 7–13 fps. На каждый объект Exult делает больше работы, чем движок 1992 года на 386-м, и это следующее, чем стоит заняться.
Оригинальный движок 1992 года тоже работает. Ultima7_Decomp — декомпиляция обеих DOS-игр в переносимый C и C++, а m9b-u7 даёт ей платформенный слой для платы. Игра получает ожидаемую DOS-память в арене на 4 МБ, и правок понадобилось немного: её дальняя куча держит блоки выровненными по слову, так что процессор не выдаёт исключений. Участки спрайтов и тайлы рисуются по слову за раз. При прогулке по Тринсику выходит 8–10 fps при собственном ограничении движка в 10 шагов в секунду. Музыка и звуковые эффекты — те же записи MT-32, что и в порте Exult.
- Timedemo Doom проигрывает фиксированную демозапись и считает контрольную сумму каждого кадра. Так любое изменение железа или программ можно сравнить по времени прогона и при этом доказать, что картинка та же.
- Тестовые обвязки на хосте запускают System Shock и Ultima VII с тем же кодом порта на 32-битном x86 Linux, ограничивая кучу объёмом, как на плате, и заполняя новую память мусором, как это было бы на ней. Диеты по памяти делались там, а потом проверялись на плате.
- Дистанционное управление. Запущенная игра отвечает на запросы скриншотов через последовательный порт и принимает оттуда нажатия клавиш, так что игрой можно управлять и проверять её автоматически, не глядя на экран.
- Профилирование. Профилировочная сборка снимает счётчик команд из прерывания и отправляет гистограмму через последовательный порт.
По нынешним меркам ни одна из пяти игр не быстрая. Но все они работают целиком, с музыкой, на микросхеме, аппаратура которой, скорее всего, проектировалась не под них.
Исходники и благодарности
Исходники, RTL и порты игр лежат на GitHub: m9b-system объединяет SoC (m9b-core) и порты игр, у каждого из которых свой репозиторий.
m9b опирается на работу многих людей. В README каждого репозитория перечислено, что именно в нём используется, в какой версии и что изменено. Оборудование и прошивка:
- SpinalHDL/VexiiRiscv (Charles Papon): процессор, собранный из форка с опциями m9b;
- hdl-util/hdmi (Sameer Puri): кодировщик HDMI со звуком;
- nand2mario/usb_hid_host (на основе работы hi631): USB-хост;
- FatFs (ChaN): файловая система FAT на SD-карте;
- picolibc: библиотека C и скрипт компоновщика программ;
- Wohlstand/libADLMIDI: эмуляция OPL3, которой на ПК рендерится музыка.
Комментарии
Обсуждайте статью, отвечайте и оценивайте комментарии. Вход — через ваш Яндекс ID.
Загружаем обсуждение…