River Raid на FPGA

Еще не делали River Raid на FPGA? Ок, тогда я сделаю.
715b70927752428cacc1280fa810bb42.png

Совсем недавно FPGA для меня был черным ящиком, а Verilog казался вообще магией — ну как можно написать программу, по которой построится схема на логических элементах? Изучить это я планировал давно, но без реальной железки даже не хотел начинать.

И вот недавно с Aliexpress ко мне пришло недорогое и неплохое устройство на базе Cyclone IV, но с (на тот момент) фатальным недостатком: документацией на китайском языке. Признаюсь, я впал в уныние и даже просил совета здесь на Хабре. Собравшись с силами, я таки сумел запустить примитивную программу от китайцев. Устройство заморгало светодиодом и я про себя закричал «ура». Покопавшись в остальных примерах, даже я, находясь на начальном уровне, понял, что правду говорят: китайский код ужасен. Учиться на кривом коде я не собирался и, поскольку чесались руки, захотел сходу написать какую-нибудь простенькую программу. Решил, что это будет пинг-понг: алгоритм примитивный, а результат эффектный. Модули работы с VGA и клавиатурой я увидел здесь на хабре в статье о FPGA-Тетрисе (спасибо авторам этих модулей), а остальное уже «дело техники».

На пинг-понг я потратил несколько часов. Честно говоря, не раз хотелось биться головой об стену ибо я в упор не понимал почему некоторые вещи не получаются — сама концепция verilog в разрез шла с любыми, которыми я до этого встречал.

Кстати, очень хорошо помогла фича Quartus-а рисовать логические схемы по исходному коду. Я понял как реализуются «в железе» условия, циклы и т.д.

Закончив пинг-понг, я пришел к выводу, что задача оказалась слишком простой и надо выбрать что-нибудь по-интересней. Понятно, что вторым «проектом» тоже будет игра — ее делать интересно и, как я уже говорил, результат эффектен. Из детства пришло воспоминание как я смотрел на других детей, играющих в River Raid — позволить себе поиграть не мог, дорого. Посмотрев видео игрового процесса на ютубе, приступил к реализации мечты детства. Забегая вперед, вот что у меня получилось в итоге:

Сразу скажу: опыта этой отрасли у меня пара недель, поэтому я не даю советы — могу ошибаться, заблуждаться и т.д. В комментариях прошу опытных товарищей поправить в чем я заблуждаюсь. Данная статья мотивирующая. Как говорят «не боги горшки обжигают» и вы сможете.

С чем у меня возникли трудности?

Нельзя просто взять и записать что-либо в регистр в разных местах кода, если компилятор не может четко обозначить условия: при таких — пишем это, при таких — это. Почему так происходит понятно — вход на запись у регистра один и нельзя подать 2 сигнала без какой-либо логики на один вход.

Мы программисты любим делить код на классы, подпрограммы и т.д. Логично вынести блок формирования одной сущности в один модуль (в терминах verilog), а формирование иной сущности — в иной модуль. Но если оба модуля меняют значения одной и той же переменной?

Решает такие проблемы правильная архитектура. Этот и есть 2я трудность. Насколько мне сейчас видится, правильная архитектура в verilog чуть-ли не важнее чем в классических языках программирования. Помню, когда я реализовал блок работы врагов (самолеты, корабли и т.д.), после компиляции у меня количество задействованных элементов fpga увеличилось на несколько тысяч и это грозило тем, что мне вообще могло не хватить элементов и на минимальный функционал! Пришлось переделывать архитектуру.

Удачное правило define-ить все, даже разрядность переменных. К сожалению, я не полностью придержался этого правила и поэтому, если вы вдруг захотите увеличить разрешение экрана — вам придется поменять пару десятков переменных.

Немного о проекте

03f9f5b3ac6f46f7860d5fc5fcb2ef57.jpg

В устройстве нет ни формирователя видеосигнала ни видеопамяти. Видеосигнал формируется «вручную». Модуль, осуществляющий это, предоставляет 2 динамических параметра: текущие x и y соответствующие позиции на экране в данный момент времени. Чтобы что-либо «нарисовать», нужно постоянно мониторить эти 2 параметра и в нужный момент посылать на RGB монитора определенный цвет пикселя.

Как известно, VGA использует аналоговый сигнал для цвета, но китайцы секономили — подключили по одному цифровому выходу на RGB. В итоге у меня в арсенале всего 8 цветов. Я, конечно, пытался скомпенсировать это разрешением экрана, но все равно приемлемая картинка на 8 цветах не получилась. Пробовал игратся: в один проход рисовать точку одним цветом, в другой — иным, чтобы получить полутона, но ничего толкового не вышло, мигания раздражали.

Еще хочу сказать, что архитектура мне не очень нравится — слабо удалось разбить на модули, поскольку логика «прорисовки экрана» очень тесно переплетается с вычислениями. Возможно, если сделать передышку, то через время я бы (подучился и) пересмотрел бы ее (архитектуру).

Прежде всего я сделал скролинг реки. Для этого есть массив, в котором указывается позиция скролинга и скорость расширения (сужения) реки начиная с этой позиции. Каждый фрейм я «прохожу» по этому массиву с начальной позиции (плавающее окно по сути), модифицируя текущую позицию X берега реки согласно текущей координате Y и текущим данным в этом массиве. Река и острова (для них отдельный массив) у меня симметричны (как и в оригинальной игре) — поэтому правая сторона реки и острова отрисовываются зеркально.

Спрайты для врагов сперва хранил в массивах, но довольно быстро у меня перестало хватать элементов fpga — пришлось перенести в ROM Cyclone IV. С последним есть небольшая проблема. Дело в том, что ROM синхронная, поэтому чтобы выставить адрес пикселя в спрайте, мне нужно за такт (ну или за пол-такта, если использовать negedge) знать координаты текущей точки относительно верхнего левого угла объекта. Это, естественно, осуществимо, надо просто искать пересечение текущей точки на экране с координатами объектов сместив их координаты влево на 1 пиксель при сравнении. Поскольку такие вещи делаются в цикле, эта дополнительная логика накинула бы сотню элементов. Я решил секономить (поскольку элементов оставалось в притык) и не заморачиваться. В итоге спрайт рисуется на один пиксель правее чем он на самом деле есть.

Также присутствует баг наложения врагов друг на друга (с другими объектами все ок) — поскольку ROM для спрайтов на всех врагов одна, то чтобы вычислить адрес какого спрайта использовать в случае пересечения объектов, нужно знать пиксель какого объекта в данный момент прозрачный. Тут довольно сложная логика, поэтому решил тоже не заморачиваться.

Чтобы максимально разбить код на сущности, я реализовал своеобразный конвейер (машину состояний): на первом этапе проверяется стоит ли изменить направление реки, на втором стоит ли поместить в FIFO нового врага, на третьем осуществить перемещения врагов и т.д.

Картинки для спрайтов любезно предоставил Гугл, а mif-файлы формировал скриптом на python.

Мир прорисован совсем немного, примерно на минуту игры, просто устал — проект пилил в нерабочее и несемейное время ночами. Если кто-то сумеет запустить на своих железках — могу дописать

» Проект на Гитхабе

Выражаю благодарность ishevchuk т.к. благодаря ему я понял насколько мощная вещь fpga

Комментарии (0)

© Habrahabr.ru