River Raid на FPGA

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


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

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

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

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

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



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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

    Выражаю благодарность ishevchuk т.к. благодаря ему я понял насколько мощная вещь fpga
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 34
    • +4
      Рад, что мои статьи как-то влияют на то, что делают другие люди)

      Вы заморочились (в хорошем смысле этого слова) с игрой намного больше, чем я, когда делал тетрис)

      Удачи в освоении разработки под FPGA!
      • 0
        Это круто. У меня тоже на плате есть VGA выход, но к сожалению больше нет мониторов, которые поддерживают VGA :)
        • 0
          Есть же переходники. На фото монитор справа как раз через переходник подключен
          • 0
            Переходите на HDMI )
            • 0
              Если на HDMI, я захочу UHD выход, а с этим пока плохо — даже Numato Opsis вроде не дает 4К по HDMI.
          • +1
            За пару недель такое?
            Завидую!
            • 0
              Спасибо за мотивацию :)
              • +2
                Нельзя просто взять и записать что-либо в регистр в разных местах кода, если компилятор не может четко обозначить условия: при таких — пишем это, при таких — это. Почему так происходит понятно — вход на запись у регистра один и нельзя подать 2 сигнала без какой-либо логики на один вход.

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

                Неправильно:
                always @(posedge CLK) if (A) reg <= B;
                always @(posedge CLK) if (A1) reg <= B1;

                Правильно:
                always @(posedge CKJ)
                if (A) reg <= B;
                else if (A1) reg <= B1;
                • 0
                  Я использовал systemverilog из-за возможности использовать структуры и logic. Кстати, был еще один момент, когда я сумел в разных модулях изменить значения одних регистров, причем я не понял как это прокатило. Разбор этой ситуации отложил на потом
                  • +1
                    Ну глобально тут полезно понимать что происходит с логикой при тех или иных условиях. В частности если взять тот кусок кода, который я пометил как «правильный» то синтезатор сделает следующее.
                    1. Создаст D-триггер, на вход D которого придет некое значение — MUX.
                    2. Значение MUX будет получаться из мультиплексора, и будет выбираться из двух ( B или B1) в зависимости от входного сигнала SEL. Например SEL=0 выбирает B, а SEL=1 выбирает B1
                    3. SEL при этом будет определяться как: SEL = NOT(A).
                    4. На вход CE (clock enable) будет при этом приходить условие определяемое следующим образом:
                    CE = A | NOT(A) & A1.
                    Обратитет внимание что в пункте 4 для срабатывания по условию A1 важно выполнение условия NOT(A).
                    Если код построен так, что NOT(A) не проверяется при выборе A1 — то такое присваивание будет работать непредсказуемо.
                    • +2
                      А еще очень полезен опыт проектирования логических схем. Я, имея богатый опыт построения цифровых схем, очень быстро «въехал» во все тонкости описания «железа» в коде. И даже переключил свое сознание в этом направлении на синхронный дизайн: из-за особенности архитектуры ПЛИС в целом (не важно, CPLD это или FPGA), комбинаторика тут работает непредсказуемо из-за непредвиденных задержек сигнала, вносимых путями сигналов, которые образуются после принятия решения фиттером куда что положить в кристалле. Так что, иногда для некоторых магия: почему один и тот же код работает по разному на CPLD/FPGA разной архитектуры.
                      А в целом, воплотилась моя мечта детства: менять цифровую схему без паяльника! Жаль, что позволить себе такие средства я смог лишь 20 лет спустя…
                      • +1
                        У меня та же история, в 80-х я в глаза ни разу не видел компьютер, но умел что-то примитивное создавать на логических элементах. Помню даже случай, когда на триггер не хватало несколько копеек и я впервые в жизни у прохожего попросил их, тот чтобы убедиться, что я не попрошайка, пошел со мной в магазин и я при нем купил микросхему) Потом в приложении к Юному Технику появилась схема компьютера и в месте с ней у меня мечта его собрать. Ну в принципе, будет время — осуществлю мечту в FPGA)
                      • +1
                        И даже переключил свое сознание в этом направлении на синхронный дизайн: из-за особенности архитектуры ПЛИС в целом (не важно, CPLD это или FPGA), комбинаторика тут работает непредсказуемо из-за непредвиденных задержек сигнала, вносимых путями сигналов, которые образуются после принятия решения фиттером куда что положить в кристалле.

                        Эту непредсказуемость можно контролировать путем задания ограничений на Propagation Delay. Но мое мнение таково — если в дизайне есть ограничения на асинхронные пути (или полная крайность — задание правил LOC — координаты в ПЛИС, для LUT и регистров) — то этот дизайн плохой. Это из той же оперы что при проектировании схемы полагаться на характеристики транзисторов. Не в том смысле что их оценивать, а в том что шаг вправо, шаг влево — и схема отваливается.
                        В общем — больше регистров хороших и годных.
                        • 0
                          Можно даже руками колдовать над Chip Planner'ом, но это не будет правильным мышлением, верно? В пределах конкретного проекта это может быть необходимо, но не должно быть правилом. И конечно, для контроля смотрим в TimeQuest (тут я все буду говорить от имени Altera, но это вроде справедливо и для других производителей), который попытается предсказать на основе введенных параметров работоспособность дизайна на заданной частоте.

                          Мое мнение таково: это как с компиляторами языков программирования, есть разные твики на параметры и степени оптимизаций, но окончательное решение должен принимать человек. А для этого нужно как минимум знать предмет, а не только сам язык HDL.

                          И таки да, есть куча примеров когда в серийном производстве полагаются именно на конкретные характеристики конкретных элементов на кристалле. Для этого их даже делают изменяемыми после создания кристалла (например, для подрезки лазером). Тоже плохой дизайн?
                          • 0
                            Ну живой пример из практики. У меня просто много знакомых которые чрезмерно увлекаются асинхронным дизайном, клоканьем регистров не штатным клоком и прочими такими делами.
                            Человек полгода писал ПЛИС. Код абсолютно дурной — чесслово, я бы писал совсем по другому. Констрейнов там на 1000 строк. Добрая половина их них — указание САПРу игнорить те или иные пути, и обконстренйенные в хлам ансихронные пути. Утилизация небольшая — менее 40% (к слову я набивал FPGA под 85% по LUT) Утрамбовалось, BIST прошло. Через полгода эксплуатации заказчик выявил баг. Простой — исправить, как два пальца — с логической точки зрения. Исправляют уже третий месяц — у них BIST не проходит теперь. Разъехалась логика где-то.
                            Хороший дизайн можно модифицировать с минимальными затратами времени. Идеальный (на мой взгляд) дизайн не требует констрейнов вообще (окромя задания пинов и частоты клока естсественно)
                            • 0
                              Вот например у студентов часто встречаю такие конструкции:
                              always @(posedge CLK1 or posedge CLK2)
                              reg <= value

                              Результатом будет полный треш
                              Объяснить почему?
                              Или такое
                              always @(posedge CLK1)
                              reg1 <= A
                              always @(posedge CLK2)
                              reg2 <= A

                              Тоже очень прикольные вещи происходят в такой конструкции

                              • 0
                                Я так не делаю. :) И да, я задаю только клок и пины. А после пытаюсь избавиться от всех (ну или большинства) варнингов.
                                • 0
                                  Меня просто периодически просят внести какие-то изменения в чей-то код. Поставщик таких задач — компания в которой много
                                  — студентов
                                  — представителей «старой школы»
                                  типичный ВПК короче
                                  И я каждый раз за голову хватаюсь — там такого добра валом.
                                  Часто просто приходится заново переписывать куски кода.
                                  • 0
                                    И да, я задаю только клок и пины. А после пытаюсь избавиться от всех (ну или большинства) варнингов.

                                    Единственно что я делаю в плане геолокации вентилей в ПЛИС — я задаю регионы. Потому что основная масса продуктов которые пишу — это законченные переносимые IP блоки. Поэтому как-то «красивее» что-ли когда IP занимает какой-то определенный кадратик в кристалле.
                                  • 0
                                    always @(posedge CLK1)
                                    reg1 <= A
                                    always @(posedge CLK2)
                                    reg2 <= A
                                    

                                    А что тут не так, объясните пожалуйста
                                    • 0
                                      Зависит от того что там дальше. Ну для определенности давайте возьмем ситуацию когда сигнал «A» синхронизирован с частотой CLK1 и асинхронен по отношению к частоте CLK2.
                                      В этом случае reg1 отработает корректно, а вот на выходе reg2 возможно возникновение метастабильного состояния. Метастабильное состояние характеризуется тем, что фронт сигнала затянут — не просто завален/сглажен — а он буквально размазан во времени Причем даже возможно за период частоты сигнал так и не установится. Дальше представьте что значение reg2 у Вас участвует в логике в нескольких частях схемы и Вы предполагаете что на входе всех компонентов в отдельно взятый момент времени он имеет одно и то же значение. Однако в случае вознкновения метастабильного состояния — какие-то блоки увидят на входе 0, а какие-то 1. И вся логика работы Вашей схемы слетит. Причем слетать оно будет всегда по-разному. Печально здесь то — что никакие констрейны на тайминг эту ситуацию Вам не накроют. И САПР на тайминги ругаться не будет. Он проглотит.
                                      В результате, когда к Вам попадает чужой дизайн, который почему-то дает плавающие отказы (причем всегда разные) — и Вы не ожидаете там таких конструкций — дебажить Вы его будете очень долго.
                                      • 0
                                        Ну, т/е конструкция сама по себе ок, только зависит от интеграции с другими частями системы, мне это важно знать ибо я испугался «может чего-то не понимаю». А так да, про метастабильное состояние я читал и сразу понял, что пример будет с ним)
                                        • 0
                                          Ну это самое адовое что может быть в ПЛИС — поэтому раз уж мы говорим о косяках — я не преминул привести этот пример.
                                          Лечится переходом через 2-3 регистра.
                                          Но лечить такое в уже готовом дизайне — треш. Потому что 2-3 регистровых стадии кардинально меняют всю логику. То есть переписывать приходится все.
                            • 0
                              Забыл добавить, скорее всего синтезатор соберет цепочку мультиплексоров, в ближнем будет выбор между B и вторым мультиплексором по признаку А, а во втором между В1 и выходом триггера по значению А1.

                              Ну вот, я прав (извиняюсь за лишнее: встроил тест в рабочий проект):
                              image
                              image
                              Синхронный дизайн. Здесь латчи и комбинаторика как GoTo в ЯВУ. Так что повторюсь: знание и опыт проектирования логических схем очень помогает, ведь в итоге именно их вы и получаете.
                        • 0
                          А можно ссылку на используемую плату?
                        • –1
                          По поводу видеовыхода, а шим не?
                          • 0
                            Так шим предусматривает, что на выходе импульсы будут сглажены. Если предположить, что это сделает сам монитор, то в принципе можно на обратном фронте синхроимпульса убирать сигнал, получится примитивный шим на 3 значения, т.е. получу 27 цветов вместо 8. В принципе, попробовать можно. Более продвинутый шим программно получить только увеличением частоты — хз как китайская железяка к этому отнесется. Сейчас итак там 108Мгц
                            • 0
                              Ну дык, 4 циклон вещь очень хорошая, на 3м, на 3500ЛЕ делается спектрум 128, делали так же ПАЛ кодер, там вообще шим на шим умножается, http://zx-pk.ru/threads/9342-plis-i-vsjo-chto-s-nimi-svyazano.html куча полезного, простым языком по VHDL.

                              >Если предположить, что это сделает сам монитор
                              Ну емкость то у ВГА входа, какая-то, но должна быть, ну или в схеме на выходе, китайцы ж не думали, что ВГА это 8 цветов :-)
                              • 0
                                У китайцев есть отдельный VGA-шилд для этой платы. Так что я думаю они специально так сделали чтобы максимально удешевить, платка стоит 39 баксов
                                • 0
                                  И что на ней стоит? Цап?
                                  • 0
                                    Не вкурсе, я видел шилды на картинках, например https://ae01.alicdn.com/kf/HTB1ZzDNKFXXXXX1XFXXq6xXFXXX5/FPGA-development-board-ALTERA-Cyclone-IV-EP4CE-four-generations-SOPC-NIOSII-send-send-remote-control-to.jpg
                                    Но инфу по ним не нашел еще
                              • 0
                                Так шим предусматривает, что на выходе импульсы будут сглажены

                                Я никогда не работал с Альтерой, но по аналогии с Xilinx могу предположить, что там можно регулировать slew-rate выводов. Если если можно — ставьте самый низкий. Вариация на тему — может называться Drive Strength. И будут Вам заваленные фронты.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.