Робимо тетріс під FPGA

Всім привіт!

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

Звичайно, гри на FPGA робляться більше для фана і навчання, ніж для якихось реальних «продакшен» завдань, так і від «розробки» ігор я дуже далекий, можна сказати, для мене це новий досвід.

Якщо цікаво, як можна запускати гру без операційної системи, реалізуючи її на самому низькому рівні, за допомогою тригерів і комбінаційної логіки, ласкаво просимо під кат.


Про девките
Нам треба «щось», де наша гра запуститься. Один з найпростіших способів — взяти девкит, де є FPGA і якась периферія для вводу/виводу. У моєму розпорядженні виявилася хустки від Terasic з назвою DE1-SoC.





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

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

Що хочемо отримати
У поняття тетріс можна вкладати різні речі, тому я накидав приблизний ТЗ, чого хотів отримати:
  • Стандартний набір фігурок. Їх поведінка повинна бути максимально схожим на звичне.
  • Гра різнобарвна. За кожною фігуркою закріплений свій колір.
  • Фігурки генерується випадково з рівномірним розподілом.
  • Має бути вікно, в якому відображається наступна фігурка.
  • Повинна бути інформація про стан гри: кількість очок, кількість прибраних ліній, поточний рівень.
  • Очки нараховуються за «прогресивною» шкалою: чим більше за раз прибрав ліній, тим більше очок.
  • Чим вище рівень, тим більше швидкість падіння фігурок.
  • Коректно детектується «кінець гри», є можливість почати нову гру.
  • Enter дій користувача здійснюється з клавіатури (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.2 k 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 шрифтом з висотою в 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, не рекомендую робити тетріс прям в якості першого проекту. Почніть з светодиодиков, сегментних индикторов, годин, та інших класичних речей. Коли це пройдете, то можна спробувати тетріс, або якусь іншу простеньку гру. Сподіваюся, мій проект допоможе в цьому починанні.

Спасибі за увагу! Якщо виникли запитання, задавайте без сумнівів.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.