Еще не делали 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)


  1. ishevchuk
    19.10.2016 20:48
    +4

    Рад, что мои статьи как-то влияют на то, что делают другие люди)

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

    Удачи в освоении разработки под FPGA!


  1. mezastel
    19.10.2016 23:14

    Это круто. У меня тоже на плате есть VGA выход, но к сожалению больше нет мониторов, которые поддерживают VGA :)


    1. ef_end_y
      19.10.2016 23:29

      Есть же переходники. На фото монитор справа как раз через переходник подключен


    1. UA3MQJ
      20.10.2016 10:00

      Переходите на HDMI )


      1. mezastel
        20.10.2016 11:09

        Если на HDMI, я захочу UHD выход, а с этим пока плохо — даже Numato Opsis вроде не дает 4К по HDMI.


  1. Shamrel
    20.10.2016 05:11
    +1

    За пару недель такое?
    Завидую!


  1. unabl4
    20.10.2016 09:12

    Спасибо за мотивацию :)


  1. czed
    20.10.2016 11:43
    +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;


    1. ef_end_y
      20.10.2016 11:46

      Я использовал systemverilog из-за возможности использовать структуры и logic. Кстати, был еще один момент, когда я сумел в разных модулях изменить значения одних регистров, причем я не понял как это прокатило. Разбор этой ситуации отложил на потом


      1. czed
        20.10.2016 12:27
        +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 — то такое присваивание будет работать непредсказуемо.


        1. HardWrMan
          26.10.2016 04:16
          +2

          А еще очень полезен опыт проектирования логических схем. Я, имея богатый опыт построения цифровых схем, очень быстро «въехал» во все тонкости описания «железа» в коде. И даже переключил свое сознание в этом направлении на синхронный дизайн: из-за особенности архитектуры ПЛИС в целом (не важно, CPLD это или FPGA), комбинаторика тут работает непредсказуемо из-за непредвиденных задержек сигнала, вносимых путями сигналов, которые образуются после принятия решения фиттером куда что положить в кристалле. Так что, иногда для некоторых магия: почему один и тот же код работает по разному на CPLD/FPGA разной архитектуры.
          А в целом, воплотилась моя мечта детства: менять цифровую схему без паяльника! Жаль, что позволить себе такие средства я смог лишь 20 лет спустя…


          1. ef_end_y
            26.10.2016 09:58
            +1

            У меня та же история, в 80-х я в глаза ни разу не видел компьютер, но умел что-то примитивное создавать на логических элементах. Помню даже случай, когда на триггер не хватало несколько копеек и я впервые в жизни у прохожего попросил их, тот чтобы убедиться, что я не попрошайка, пошел со мной в магазин и я при нем купил микросхему) Потом в приложении к Юному Технику появилась схема компьютера и в месте с ней у меня мечта его собрать. Ну в принципе, будет время — осуществлю мечту в FPGA)


            1. HardWrMan
              26.10.2016 10:16
              +1

              А я уже воплотил на россыпухе. Теперь тоже пилю помалу SoC на CPLD.


          1. czed
            26.10.2016 11:04
            +1

            И даже переключил свое сознание в этом направлении на синхронный дизайн: из-за особенности архитектуры ПЛИС в целом (не важно, CPLD это или FPGA), комбинаторика тут работает непредсказуемо из-за непредвиденных задержек сигнала, вносимых путями сигналов, которые образуются после принятия решения фиттером куда что положить в кристалле.

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


            1. HardWrMan
              26.10.2016 11:17

              Можно даже руками колдовать над Chip Planner'ом, но это не будет правильным мышлением, верно? В пределах конкретного проекта это может быть необходимо, но не должно быть правилом. И конечно, для контроля смотрим в TimeQuest (тут я все буду говорить от имени Altera, но это вроде справедливо и для других производителей), который попытается предсказать на основе введенных параметров работоспособность дизайна на заданной частоте.

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

              И таки да, есть куча примеров когда в серийном производстве полагаются именно на конкретные характеристики конкретных элементов на кристалле. Для этого их даже делают изменяемыми после создания кристалла (например, для подрезки лазером). Тоже плохой дизайн?


              1. czed
                26.10.2016 11:59

                Ну живой пример из практики. У меня просто много знакомых которые чрезмерно увлекаются асинхронным дизайном, клоканьем регистров не штатным клоком и прочими такими делами.
                Человек полгода писал ПЛИС. Код абсолютно дурной — чесслово, я бы писал совсем по другому. Констрейнов там на 1000 строк. Добрая половина их них — указание САПРу игнорить те или иные пути, и обконстренйенные в хлам ансихронные пути. Утилизация небольшая — менее 40% (к слову я набивал FPGA под 85% по LUT) Утрамбовалось, BIST прошло. Через полгода эксплуатации заказчик выявил баг. Простой — исправить, как два пальца — с логической точки зрения. Исправляют уже третий месяц — у них BIST не проходит теперь. Разъехалась логика где-то.
                Хороший дизайн можно модифицировать с минимальными затратами времени. Идеальный (на мой взгляд) дизайн не требует констрейнов вообще (окромя задания пинов и частоты клока естсественно)


              1. czed
                26.10.2016 12:11

                Вот например у студентов часто встречаю такие конструкции:

                always @(posedge CLK1 or posedge CLK2)
                reg <= value

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

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


                1. HardWrMan
                  26.10.2016 12:13

                  Я так не делаю. :) И да, я задаю только клок и пины. А после пытаюсь избавиться от всех (ну или большинства) варнингов.


                  1. czed
                    26.10.2016 12:15

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


                  1. czed
                    26.10.2016 12:18

                    И да, я задаю только клок и пины. А после пытаюсь избавиться от всех (ну или большинства) варнингов.

                    Единственно что я делаю в плане геолокации вентилей в ПЛИС — я задаю регионы. Потому что основная масса продуктов которые пишу — это законченные переносимые IP блоки. Поэтому как-то «красивее» что-ли когда IP занимает какой-то определенный кадратик в кристалле.


                1. ef_end_y
                  26.10.2016 14:03

                  always @(posedge CLK1)
                  reg1 <= A
                  always @(posedge CLK2)
                  reg2 <= A
                  

                  А что тут не так, объясните пожалуйста


                  1. czed
                    26.10.2016 14:15

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


                    1. ef_end_y
                      26.10.2016 14:22

                      Ну, т/е конструкция сама по себе ок, только зависит от интеграции с другими частями системы, мне это важно знать ибо я испугался «может чего-то не понимаю». А так да, про метастабильное состояние я читал и сразу понял, что пример будет с ним)


                      1. czed
                        26.10.2016 14:27

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


        1. HardWrMan
          26.10.2016 04:32

          Забыл добавить, скорее всего синтезатор соберет цепочку мультиплексоров, в ближнем будет выбор между B и вторым мультиплексором по признаку А, а во втором между В1 и выходом триггера по значению А1.

          Ну вот, я прав (извиняюсь за лишнее: встроил тест в рабочий проект):
          image
          image
          Синхронный дизайн. Здесь латчи и комбинаторика как GoTo в ЯВУ. Так что повторюсь: знание и опыт проектирования логических схем очень помогает, ведь в итоге именно их вы и получаете.


  1. desmond_breezey
    20.10.2016 12:43

    А можно ссылку на используемую плату?


    1. ef_end_y
      20.10.2016 12:45
      +1

  1. BlindJoe
    20.10.2016 17:40
    -1

    По поводу видеовыхода, а шим не?


    1. ef_end_y
      20.10.2016 17:45

      Так шим предусматривает, что на выходе импульсы будут сглажены. Если предположить, что это сделает сам монитор, то в принципе можно на обратном фронте синхроимпульса убирать сигнал, получится примитивный шим на 3 значения, т.е. получу 27 цветов вместо 8. В принципе, попробовать можно. Более продвинутый шим программно получить только увеличением частоты — хз как китайская железяка к этому отнесется. Сейчас итак там 108Мгц


      1. BlindJoe
        21.10.2016 17:39

        Ну дык, 4 циклон вещь очень хорошая, на 3м, на 3500ЛЕ делается спектрум 128, делали так же ПАЛ кодер, там вообще шим на шим умножается, http://zx-pk.ru/threads/9342-plis-i-vsjo-chto-s-nimi-svyazano.html куча полезного, простым языком по VHDL.

        >Если предположить, что это сделает сам монитор
        Ну емкость то у ВГА входа, какая-то, но должна быть, ну или в схеме на выходе, китайцы ж не думали, что ВГА это 8 цветов :-)


        1. ef_end_y
          21.10.2016 18:05

          У китайцев есть отдельный VGA-шилд для этой платы. Так что я думаю они специально так сделали чтобы максимально удешевить, платка стоит 39 баксов


          1. BlindJoe
            22.10.2016 12:04

            И что на ней стоит? Цап?


            1. ef_end_y
              23.10.2016 11:11

              Не вкурсе, я видел шилды на картинках, например https://ae01.alicdn.com/kf/HTB1ZzDNKFXXXXX1XFXXq6xXFXXX5/FPGA-development-board-ALTERA-Cyclone-IV-EP4CE-four-generations-SOPC-NIOSII-send-send-remote-control-to.jpg
              Но инфу по ним не нашел еще


      1. czed
        21.10.2016 18:50

        Так шим предусматривает, что на выходе импульсы будут сглажены

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