Pull to refresh
0
НТЦ Метротек
Разработка и производство Ethernet устройств etc.

Делаем тетрис под FPGA

Reading time 9 min
Views 39K
Всем привет!

imageНа этих долгих новогодних выходных я задался вопросом: насколько легко написать какую-то простенькую игрушку на FPGA с выводом на дисплей и управлением с клавиатуры. Так родилась еще одна реализация тетриса на ПЛИС: yafpgatetris.


Конечно, игры на FPGA делаются больше для фана и обучения, чем для каких-то реальных “продакшен” задач, да и от “разработки” игр я очень далек, можно сказать, для меня это новый опыт.


Если интересно, как можно запускать игру без операционной системы, реализуя её на самом низком уровне, с помощью триггеров и комбинационной логики, добро пожаловать под кат.


О девките


Нам надо «что-то», где наша игра запустится. Один из самых простых способов — взять девкит, где есть FPGA и какая-то периферия для ввода/вывода. В моем распоряжении оказалась платка от Terasic с названием DE1-SoC.





Ну что сказать?
Девкит, как девкит. Много периферии: нам из неё будет интересны разьемы PS/2 и VGA. Для обучения (в школах, либо университетах) самое то. Для своих целей мы его закупили как раз для того, чтобы поиграться (и для обучения студентов), чем для реализации каких-то своих «продакшен» идей.
Если вдруг DE1-SoC (либо похожие платы) вы используете в своих реальных приборах (а не просто поморгать светодиодом) — поделитесь в комментариях, будет интересно.

SoC в названии чипа обозначает то, что в чипе есть и обычная FPGA-логика, и ARM-процессор. Забегая вперед, скажу, что для своей задачи я не использовал ни ARM, ни какой-то софтварный процессор, так что мой проект вы сможете запустить на своих платах с другими FPGA-чипами. Если интересно почитать про поднятие связки FPGA + ARM, и какие бонусы из этого можно получить, советую обратиться к статье моего коллеги Des333.

Что хотим получить


В понятие тетрис можно вкладывать различные вещи, поэтому я набросал примерное ТЗ, чего хотел получить:
  • Стандартный набор фигурок. Их поведение должно быть максимально похожим на привычное.
  • Игра разноцветная. За каждой фигуркой закреплен свой цвет.
  • Фигурки генерируется случайно с равномерным распределением.
  • Должно быть окно, в котором отображается следующая фигурка.
  • Должна быть информация о состоянии игры: количество очков, количество убранных линий, текущий уровень.
  • Очки начисляются по “прогрессивной” шкале: чем больше за раз убрал линий, тем больше очков.
  • Чем выше уровень, тем больше скорость падения фигурок.
  • Корректно детектируется “конец игры”, есть возможность начать новую игру.
  • Ввод действий пользователя осуществляется с клавиатуры (PS/2).
  • Отображение состояния поля и прочего происходит на обычном дисплее через VGA интерфейс.


Схема проекта




Можно выделить три основные части:
  • Ввод пользователем. Принимаем данные от клавиатуры и “воздействуем” на систему.
  • Всё, что относится к самой игре. По факту FSM (finite-state machine), которая принимает “запросы” от игрока, и “делает всё”: генерирует новые фигурки, их двигает, убирает линии, и прочее.
  • Отображение состояния игры. Отрисовываем на дисплей через интерфейс VGA.


PS/2


Если честно, сначала думал обойтись без клавиатуры и использовать клавиши на самом ките, но на удивление никаких проблем с клавиатурой не возникло: всё заработало из коробки.

Для приёма команд с клавиатуры нужен PS/2 контроллер. Я использовал вот этот.

Если чуть обратиться к теории, то для каждой клавиши определен набор кодов, которые посылает клавиатура при её нажатии или отпускании.

Возьмем клавишу “Enter”:
  • Make: 5A
  • Break: F0, 5A.

Посмотрим, как это выглядит внутри FPGA:

Обычное нажатие клавиши:

Как видим, действительно:
  • Нажимаем на клавишу, приходит 5A.
  • Отпускаем: приходит F0, после нее 5A.
  • Снова нажимаем: приходит 5A и так далее.


Если зажмем клавишу, то получим вот это:

Просто приходит команда 5A с какой-то периодичностью.

Нам нужен небольшой набор клавиш:
  • обычные стрелки — для управления фигуркой.
  • n — для начала новой игры.

Для определения типа события, можно обойтись сдвиговым регистром и небольшой примитивной логикой, а всё остальные клавиши просто пропускать мимо. В рамках этой игры я решил, что мне достаточно детектировать события о нажатии клавиши, а отпускание игнорировать. При зажатии клавиши всё равно будут приходить команды о том, что клавиша нажата, а следовательно игрок всё еще хочет, чтобы фигурка двигалась либо переворачивалась. Признаюсь, опасался, что фигурки будут двигаться слишком непривычно, но всё обошлось.

После детектирования интересующего нас “события” кладем его в FIFO, откуда его заберет “прикладная логика” игры. Если у вас не оказалось PS/2 на своей плате, но есть какие-то ключи, тумблеры, то достаточно будет написать логику которая нажатия этих кнопок переведет в “события”, и игра ничего не заметит.

Этот контроллер позволяет подключить мышь, но я не пробовал.

Основная логика игры



С одной стороны, логика тривиальна, и описывается следующей FSM:


(Если честно, не знаю, использует кто-нибудь в продакшене “State Machine Viewer”, если да, поделитесь в комментариях для чего. За всё время разработки под FPGA я его открывал пару раз, да и то в рамках обучения).

FSM «общается» со следующими блоками/модулями:
  • gen_sys_event — таймер, который отсчитывает время, через которое надо автоматически двинуть фигурку вниз.
  • gen_next_block — генератор новой фигурки.
  • check_move — проверка, можно ли выполнить текущий «ход».
  • tetris_stat — накопление «статистики».
  • user_input — считывает событие, которое «произвел» пользователь.

Всё очень похоже на «обычную» реализацию Тетриса, которые написаны на C++/Java/etc: различные модули выполняют роль функций в тех языках. Да и проблемы возникают такие же: дольше всего сидел над переворотом фигурки, ответ подсмотрел в коде quadrapassel. Один из вариантов является то, что можно хранить хранить таблицу всех возможных разворотов (для каждой фигурки четыре варианта).

Весь код написан на Verilog, а если быть более точным — на SystemVerilog. С одной стороны SystemVerilog намного гибче, чем Verilog, а с другой стороны это приводит к тому, что ты не ограничен, и хочешь всё больше и больше различных рюшечек реализовать :).

Я упростил себе жизнь: текущее состояния поля хранится на регистрах (вместо внутренней памяти), и из-за этого (а так же того, что некоторые вещи сделаны неоптимально) образуется много логики, и проект занимает немало ресурсов (около 3.2k ALM из 32k). Если переехать на память, то придется делать некоторые вещи последовательно (например, сдвиг вниз всего поля, когда надо убирать линию, которая заполнилась). Скорее всего я не буду переделывать на использование памяти.

В тестовых целях я собрал я проект под платы DE0/DE1 (братья той платы, которая у меня, но с бюджетными чипами: у них меньше ресурсов, и они более «младшего поколения»): проект по ресурсам влезает. Однако…
Скрытый текст
… прям из коробки не заработает:
  • Квартус будет ругаться на некоторые вещи в qsf файле, т.к. я собирал для 14-м квартусом, где нет Cyclone II/III. Ранние версии квартуса этих вещей не знают: придёться ручками удалить в qsf файле эти строчки, а потом по смыслу такие же галки поставить в GUI квартуса.
  • Не укладывается по частоте: «главная» частота в этом проекте 108 МГц (на нем работает сам main_game_logic и отрисовка на VGA). Чуть забегая вперед, частота 108 МГц — потому что используется разрешение 1280x1024, если использовать 640x480, то там будет частота 25 МГц, и уложится.
  • Возможно, придется перегенерировать мегафункции для PLL и FIFO, т.к. они были созданы для Cyclone V.
  • Вывод на дисплей, возможно, надо чуть подредактировать (выбрать другие цвета), т.к. там на каждый цвет только четыре бита выделено (как я понял), против восьми, как в этой плате.



Отображение на дисплей


Информации о том, как с помощью ПЛИС выводить изображение на дисплей через VGA можно найти немало, например на хабре, поэтому на этом подробно останавливаться не буду.

В этом ките вывод на VGA сделан следующим образом:


Каждый такт VGA_CLK необходимо выставлять новое значения цвета в модели RGB, а затем ЦАП эти значения преобразует в необходимый уровень сигнала.

В качестве контроллера VGA сигналов я взял модуль из демопримеров, которые есть на CD для этого кита. Забавно, что есть понятие CD, но в комплекте с платой никакого CD нет: необходимо скачивать архив из интернета.

Этот «контроллер» Terasic использует и в других китах: он легко гуглится по имени «vga_time_generator». Он удобен тем, что можно настроить его на любой режим работы (640x480, 800x600, etc), и тем, что выдает координаты (pixel_x, pixel_y) текущего пикселя для отображения. Наша задача сводится к тому, чтобы в зависимости от этих координат подставить нужное значения цвета.



Я решил, что 640x480 на большом мониторе смотрится не очень и переехал на 1280x1024, просто передав в модуль нужные значения из стандарта. Дополнительно пришлось изменить значение VGA_CLK: вместо 25.175 МГц стало 108 МГц. Правда, я потом немного жалел об этом, но красота требует жертв.

Рассмотрим, как выводить какие-то примитивные объекты.

Например:
`define RGB_BLACK   24'h00_00_00
`define RGB_ORANGE  24'hFF_A5_00

logic [23:0] vga_data;

localparam START_X = 100;
localparam START_Y = 100;
localparam END_X   = START_X + 200 - 1;
localparam END_Y   = START_Y + 300 - 1;

always_comb
  begin
    vga_data = `RGB_BLACK;

    if( ( pixel_x >= START_X ) && ( pixel_x <= END_X ) &&
        ( pixel_y >= START_Y ) && ( pixel_y <= END_Y ) )
        vga_data = `RGB_ORANGE;
  end

assign { r, g, b } = vga_data;


Выведется оранжевый квадрат размером 200x300 пикселей, причем верхний левый угол будет расположен в точке (100, 100).

Или:
`define RGB_BLACK   24'h00_00_00
`define RGB_ORANGE  24'hFF_A5_00

logic [23:0] vga_data;

localparam MSG_X = 56;
localparam MSG_Y = 5;

logic [0:MSG_Y-1][0:MSG_X-1] msg;

assign msg[0] = 56'b10010011110010000010000001100000001001000110001110001110;
assign msg[1] = 56'b10010010000010000010000010010000001001001001001001001001;
assign msg[2] = 56'b11110011110010000010000010010000001111001111001111001111;
assign msg[3] = 56'b10010010000010000010000010010000001001001001001001001110;
assign msg[4] = 56'b10010011110011110011110001100000001001001001001110001001;

logic [$clog2(MSG_X)-1:0] msg_pix_x;
logic [$clog2(MSG_Y)-1:0] msg_pix_y;

localparam START_MSG_X = 100;
localparam START_MSG_Y = 100;
localparam END_MSG_X   = START_MSG_X + MSG_X - 1;
localparam END_MSG_Y   = START_MSG_Y + MSG_Y - 1;

assign msg_pix_x = pixel_x - START_MSG_X;
assign msg_pix_y = pixel_y - START_MSG_Y;

always_comb
  begin
    vga_data = `RGB_BLACK;

    if( ( pixel_x >= START_MSG_X ) && ( pixel_x <= END_MSG_X ) &&
        ( pixel_y >= START_MSG_Y ) && ( pixel_y <= END_MSG_Y ) )
      begin
        if( msg[ msg_pix_y ][ msg_pix_x ] )
          begin
            vga_data = `RGB_ORANGE;
          end
      end
  end

assign { r, g, b } = vga_data;


Выведется HELLO HABR шрифтом c высотой в 5 пикселей оранжевым цветом на черном фоне. (Приглядитесь к единицам в массиве msg).

Думаю, понятно, как можно отрисовывать какие-то статичные сообщения или поле игры.

Выводим строчки


Для отображения статистики (строк «Score», «Lines», «Level» и их значений) я решил пойти по “классическому” пути. Его можно посмотреть, например, тут.

Допустим, какая-то логика уже определила, какой какой символ (читай, букву или цифру) мы хотим выводить #прямосейчас (в зависимости от pixel_x, pixel_y). Для его отображения используем готовую таблицу шрифта, где единицами будет отмечено какой пиксель необходимо красить цветом шрифта, а нулем — цветом фона, типа:
  "00000000", -- 0
  "00000000", -- 1
  "00010000", -- 2    *
  "00111000", -- 3   ***
  "01101100", -- 4  ** **
  "11000110", -- 5 **   **
  "11000110", -- 6 **   **
  "11111110", -- 7 *******
  "11000110", -- 8 **   **
  "11000110", -- 9 **   **
  "11000110", -- a **   **
  "11000110", -- b **   **
  "00000000", -- c
  "00000000", -- d
  "00000000", -- e
  "00000000", -- f


Во многих проектах (что можно найти в сети) с VGA используется такая таблица (Font ROM), но они рассчитаны на дисплей 640x480: для 1280x1024 это получается мелковато, поэтому необходимо подготовить похожую таблицу, но с “большим” шрифтом.

В этом мне помогла утилита nafe. На входе принимает psf файл, на выходе — текстовый файл с X, в тех пикселях, которые надо отрисовать. С помощью любимого языка (либо чуть переделываем вывод оригинальной программы) меняем X на “1”, а пробелы на “0”, и добавляем заголовок, чтобы сделать mif файл (который потом используется для инициализации ROM).

Самый большой шрифт, что я нашел у себя в формате psf, был 32x16, и, в принципе, для этой задачи его хватило, но я хотел бы чуть больше его сделать. Насколько я понимаю, каких-то ограничений нет, и с помощью этой утилиты можно подготовить ROM с любыми символами (например, русскими буквами).

Однако, для заголовка yafpgatetris и сообщения GAMEOVER этот размер мне показался маленьким, и я решил выводить эти сообщения аналогично строчке HELLO HABR в примере выше. Единственный вопрос — как подготовить msg, т.к. ручками это делать уж очень не хотелось.

Сразу пришло в голову относительно простое велосипедное(?) решение:
  • Набираем текст нужного шрифта и размера в Paint/GIMP.
  • Сохраняем в PNG без сжатия и сглаживания.
  • Используем какую-то готовую библиотеку, чтобы прочитать PNG файл и для каждого пикселя вывести 0, если “цвет белый”, 1 если “цвет черный”.

Получившийся набор нулей и единиц тоже можно положить в ROM (в другую, чем шрифт, конечно).

Немного фотографий


Пара фотографий из серии “разработка в процессе”:
Скрытый текст
Научились выводить поле: фигурки просто падают вниз и все одного цвета.


Добавили статистику и разные цвета. Цвета вырвиглазные :)


Ну, а окончательный вариант — в начале статьи :)

P.S.
Если честно, не знаю, почему при фотографировании такие «разводы» на дисплее, может какую-то настройку не включил в VGA-корке, либо просто не повезло…


Итоги


Исходники:
https://github.com/johan92/yafpgatetris

Видео:


Я попытался сделать проект максимально параметризируемым, и логически разделил на части, поэтому, если захотите на базе моего проекта сделать «гоночки», где надо уворачиваться от других машин, либо змейку, достаточно написать свою main_game_logic и чуть-чуть поправить вывод (если надо).

На разработку ушло где-то около 5 дней, если считать «чистое время»: пришлось повозиться с переворотом фигурки (фактически два раза переписывать алгоритм), много времени ушло на подбор цветов, размеров, выравнивания и расположения сообщений. Внутренний перфекционист всё время требовал от внутреннего дизайнера что-то сдвинуть, что-то увеличить/уменьшить и пр. Для себя я усвоил, что разработка GUI это не моё) В итоге, цвета для фигурок я взял из приложения Тетрис в Вконтакте.

Если вы купили кит и хотите обучиться разработке под FPGA, не рекомендую делать тетрис прям в качестве первого проекта. Начините со светодиодиков, сегментных индикторов, часов, и прочих классических вещей. Когда это пройдете, то можно попробовать тетрис, либо какую-то другую простенькую игру. Надеюсь, мой проект поможет в этом начинании.

Спасибо за внимание! Если появились вопросы, задавайте без сомнений.
Tags:
Hubs:
+57
Comments 24
Comments Comments 24

Articles

Information

Website
metrotek.spb.ru
Registered
Founded
Employees
31–50 employees
Location
Россия