[Из песочницы] Minesweeper на FPGA

70a1ad0cdd144d5c969a2b490c15ef39.JPG Привет всем! Прочитав статью «Делаем тетрис под FPGA», я вспомнил, что у меня завалялся похожий проект, который я когда-то использовал для своеобразного предложения «руки и сердца» своей девушке.

А почему бы не сделать нечто подобное самому?

Откопав исходники, возобновил утерянные знания и решил на базе старого проекта на скорую руку написать простую версию игры «Сапёр» на старенькой ПЛИС Spartan3E. Собственно, о реализации игры «Сапёр» на уровне логических вентилей и основных особенностях разработки на FPGA фирмы Xilinx и пойдет речь в данной статье.

Отладочная платаНесколько лет назад я искал бюджетный вариант отладочной платы с ПЛИС и простейшей обвязкой разными интерфейсами типа VGA, PS/2, наличием светодиодов и LED-дисплеем, а также триггеров-переключателей. Тогда я остановился на простейшем китайском ките, который проще всего было заказать с ebay за $135,00 с учетом доставки. Кстати, комплект пришел неполный, поэтому я оставил гневный отзыв, за что продавец вернул ~20$. Так что плата обошлась мне в ~4000р по старым ценам.d372666d3fe94fc6ae7163f19ad2d55a.jpgОфициальный сайт производителя кита.

Основные особенности девкита:

ПЛИС Spartan3E (XC3S500E-4PQ208C) — 500К логических вентилей, Источник тактовой частоты CLK = 50 MHz, Внешняя память 64M SDRAM, SPI Flash (M25P80) для хранения прошивки ПЛИС, Матрица светодиодов 8×8, линейка светодиодов 8 шт., 8 переключателей и 5 кнопок, Разъемы для подключения LED-дисплеев, Разъем VGA для подключения дисплея, Разъемы PS/2, и т. д. Ресурсы кристалла Spartan3E XC3S500E приведены в таблице: 4832da0405834ff5bb268b23ccf02352.jpg

Из всего разнообразия, для реализации игры «Сапёр» необходимы VGA и PS/2 разъемы. Помимо них я использовал переключатель для глобального сброса (reset) логики внутри ПЛИС.

Основная концепция игры Что было? В старом проекте реализованы следующие штуки: — ввод команд с клавиатуры (управление ШИМ-модулятором и дисплеем); — самописный интерфейс VGA разрешением 640×480; — мигающее сердечко на матрице светодиодов 8×8 на базе ШИМ.Первые два пункта существенно ускорили время разработки игры, поэтому я не стал изобретать велосипед.

Правила для игры:

Управление с клавиатуры: «WSAD» — кнопки-стрелки для перемещения по экрану; «Enter» — проверка поля на наличие/отсутствие мины; «Space» — начать новую игру; «Esc» — завершить текущую игру; «Y/N» — для начала новой игры; Поле 8×8, всего 8 мин на поле; Остальные правила как в обычной игре сапёр; Язык программирования ПЛИС: VHDL.Вот так выглядит готовый проект в программе «PlanAhead» после стадий синтеза и трассировки. Блоки в фиолетовых рамках — занимаемые ресурсы кристалла.

2acbd5499a664a759e910c9fd4a3604d.png

Большой блок: основная логика игры; Средний блок: контроллер PS/2 клавиатуры; Маленький блок: контроллер VGA дисплея.

Иерархия проекта: На одном из первых этапов проектирования необходимо прикинуть, а как же будет выглядеть проект и сколькими компонентами его удобнее описать. Я придумал следующую структуру:

--> Верхний уровень----> Контроллер PS/2----> Контроллер VGA 640×480----> Контроллер игры-------> Блок отрисовки границ прямоугольника,-------> Блок для отрисовки закрашенных полей 8×8-------> Блок для отрисовки мин и цифр на поле-----------> Память для расстановки мин-----------> Память для символов-------> Блок для отрисовки текста и диалоговых сообщений-----------> Память для символов

Так это выглядит в среде «PlanAhead» от Xilinx.

b59c9c607a0249dc84974a2be6222a97.png

Верхний уровеньОн описывает основные порты ввода-вывода, содержит блок синтеза частоты DCM для преобразования входной частоты с 50 МГц в 25 МГц. Код верхнего уровня выглядит следующим образом:

entity top_minesweeper is port (  — PS/2 IO -- PS2_CLK: in std_logic; — CLK from PS/2 keyboard PS2_DATA: in std_logic; — DATA from PS/2 keyboard  — CLOCK 50 MHz -- CLK: in std_logic; — MAIN CLOCK 50 MHz  — VGA SYNC -- VGA_HSYNC: out std_logic; — Horizontal sync VGA_VSYNC: out std_logic; — Vertical sync VGA_R: out std_logic; — RED VGA_G: out std_logic; — GREEN VGA_B: out std_logic; — BLUE  — SWITCHES -- RESET: in std_logic — Asynchronous reset: SW (0) ); end top_minesweeper; Контроллер PS/2За основу взят этот проект. Заработало сразу. Интерфейс последовательной передачи достаточно примитивный: две линии: PS2_CLK и PS2_DATA, по которым идут команды от клавиатуры.Подводный камень — изначально с помощью «Make» кода я генерировал единичный импульс (по фронту), который бы сигнализировал «нажатие» клавиши. Это приводило к имитации повторного нажатия, когда происходило нажатие другой клавиши. Так как байт «Make» и «Break» кодов совпадают, то пришлось сделать условие более явным, учитывая «Break» код.Таблица кодов для PS/2 контроллера приведена по ссылке выше.

Контроллер VGAКогда-то в целях обучения написал самостоятельно, но алгоритм его работы точно такой же, как и у всех VGA-контроллеров. На Хабре тоже есть такой.

552f7313bba5461c84117a337dd62b43.png

Основные особенности: — Частота работы контроллера: 25.175 MHz— Разрешение экрана: 640×480— Частота обновления: 60Hz— Доступная палитра: RGB

К сожалению, отладочная плата не обладает встроенными микросхемами дешифрации цветовой палитры, поэтому доступно всего 3 основных цвета (красный, зеленый, синий) и 5 комбинаций (желтый, пурпурный, голубой, белый и черный). Но это не мешает придумать цветовую схему и даже вывести мигающие изображения! (см. видео в конце)

Контроллер игрыСамый простой способ описания контроллера игры «Сапер» строится на базе конечного автомата (FSM). Необходимо придумать условия автомата, в которых будут обрабатываться те или иные события.

В моем проекте используется 5 основных комбинаций автомата:

WAIT_START (обнуление всех управляющих сигналов, счетчика мин, запуск генератора случайной игры; PLAY (процесс игры: управление кнопками с клавиатуры, поиск мин); CHECK (проверка, если найдена мина — переход в конец игры); GAME_OVER (определяет событие победы или поражения, выводит дополнительные сообщения на дисплей); RST (необязательная стадия — очищает экран, сбрасывает все управляющие сигналы, без возможности запуска новой игры). Память символовНайдена на просторах Интернета. Размеры одного символа 8×16. Пример для символа »1»: »00000000», — 0 »00000000», — 1 »00011000», — 2 »00111000», — 3 »01111000», — 4 ** »00011000», — 5 *** »00011000», — 6 **** »00011000», — 7 ** »00011000», — 8 ** »00011000», — 9 ** »00011000», — a ** »01111110», — b ** »00000000», — c ** »00000000», — d ****** »00000000», — e »00000000», — f Все символы укладываются в одну ячейку блочной памяти RAMB16 кристалла. Память устроена так, что символ состоит из 16 векторов разрядностью 8. Для вывода символов на дисплей необходимо 4 младших разряда шины адреса подключить к вектору координаты Y. Логическая '1' — окрашивает символ в цвет, '0' — цвет фона (черный).Память для расстановки мин на полеЭту часть проекта я модифицировал дольше всего, изобретая различные изощренные решения. В итоге решил сделать следующий компонент в виде ROM-памяти, который выбирает игру.

Кусок кода:

constant N8×8: integer:=8; — константа поля 8×8 constant Ngames: integer:=1; — количество игр

type round_array_3×64xN is array (Ngames*N8×8*N8×8–1 downto 0) of integer range 0 to 7;

constant mem_init0: round_array_3×64xN:=(  — game 0: 1,1,1,0,0,0,0,0, 1,7,1,1,1,1,0,0, 1,1,1,1,7,2,1,0, 0,0,0,1,2,7,1,0, 0,1,1,1,1,1,1,0, 0,1,7,2,7,1,1,1, 0,1,1,2,2,2,2,7, 0,0,0,0,1,7,2,1); Константы N8×8 и Ngames задают размер поля и количество игр. Цифра на поле соответствует мине или количеству мин вокруг нее. Правила очень простые: Цифры 0–6 — определяют количество мин, Цифра 7 — зарезервирована и определяет мину на поле. Почему так? Я не стал придумывать ситуацию, когда вокруг точки могут находиться сразу 7 или 8 мин. Для 8 мин и поля 8×8 это слишком неинтересные решения. К тому же числа от 0 до 7 занимают всего 3 бита, тогда как комбинации от 0 до 8, и 9 для мины занимают уже 4 бита. В этом плане я большой любитель сэкономить внутреннюю логику и трассировочные ресурсы кристалла, даже если этих ресурсов хватит на 5 проектов.Таким образом, все числа укладываются в своеобразный ROM-массив, который можно дописать своими играми. В моем проекте реализовано 32 игры, что занимает чуть меньше 1 блока памяти RAMB16. Следует отметить, что числа задаются в формате integer. Для их перевода в std_logic_vector (2:0) и дальнейшей обработки написана специальная функция. Целочисленный формат упростил запись новых игр и значительно сэкономил время. Многих разработчиков ПЛИС на языке VHDL иногда вводит в ступор ситуация, когда используется целочисленный формат, поскольку конструкции с целочисленным типом не всегда являются синтезируемыми, т.е. их нельзя проверить в реальном железе. Но для ROM-генератора integer является оптимальным выбором.

Для того, чтобы добавить свою расстановку мин — нужно грамотно заполнить поле 8×8 в массив. Вариации игр набивал вручную. Всего в моем проекте 32 различных комбинации расстановки мин.

Блоки отрисовки границ и поля 8×8Изначально я реализовал их на генераторе символов, но потом решил сэкономить ресурсы кристалла, т.к. подумал, что ради закрашенных квадратиков и рамочки нет смысла использовать целую ячейку RAMB16. (Оптимизация по ресурсам!) Поэтому все сделано на мультиплексорах. Подробно останавливаться на этом не буду.

Блок для отрисовки мин и цифрПреобразует данные из памяти набора игр в числа и мины на экране, используя память символов. Изначально хотелось вывести квадратное поле 8×8, но потом мне стало лень переписывать ROM-генератор, и я оставил его прямоугольным.Для этого блока также пришлось создать специальную маску 8×8, с помощью которой по нажатию «Enter» закрашенные ячейки превращались бы в цифру или мину.

Текст и сообщенияТекст написан сплошняком — то есть на экране все пишется сразу, но в зависимости от стадии игры какая-то информация остается невидимой (например, сообщения о поражении или победе). Используется все тот же генератор символов. Размер символа 8×16, поэтому поле дисплея 640×480 можно разбить на секции 80×30, в которых отображаются символы. Как это делается?

Ниже представлен простой пример:

addr_rom <= data_box(6 downto 0) & y_char(3 downto 0) when rising_edge(clk);

x_char_rom: ctrl_8×16_rom — память коэффициентов port map ( clk => clk, addr => addr_rom, data => data_rom);

pr_sel: process (clk, reset) is — данные для отображения на дисплей begin if reset = '0' then data <= '0'; elsif rising_edge(clk) then data <= data_rom(to_integer(unsigned(not x_char(2 downto 0)))); end if; end process;

g_rgb: for ii in 0 to 2 generate — окрашивание данных в цвет begin rgb (ii) <= data and color(ii); end generate; Для начала нужно придумать, как с помощью адреса памяти можно выбирать тот или иной символ. Видно, что адрес состоит из двух векторов «y_char» и «data_box».y_char(3 downto 0) — это младшие разряды вектора координат по оси Y. Эти данные обновляются автоматически и приходят с контроллера VGA.data_box(6 downto 0) — сигнал выбирает, какой символ будет использоваться на поле. Этот вектор необходимо писать самому.

Если записать data_box <= «000001», то в вектор «data_rom» запишется первый символ из генератора. В процессе «pr_sel» происходит преобразование вектора данных в последовательный код. В зависимости от 3 младших битов регистра координаты Х выбирается конкретный бит вектора «data_rom». На первых порах я столкнулся с проблемой зеркального вывода данных на экране. Решение тривиальное — инверсия сигнала x_char.

Выходные данные — сигнал RGB, который поступает на VGA-разъем после логического преобразования с данными из памяти коэффициентов.

Реализация в железе Все это собирается в один большой проект. Для красоты с помощью простого счетчика прикрутил мигание сообщений победы/поражения, а также добавил генератор для выбора случайной игры.К исходникам на VHDL обязательно прикручивается файл *.UCF, в котором описано подключение портов ПЛИС и различные атрибуты. Пример: ## Switches NET «RESET» LOC = «P148» | IOSTANDARD = LVTTL | PULLUP; ## SW<0> NET «ENABLE» LOC = «P142» | IOSTANDARD = LVTTL | PULLUP; ## SW<1>

## VGA ports NET «VGA_R» LOC = «P96» | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST; NET «VGA_G» LOC = «P97» | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST; NET «VGA_B» LOC = «P93» | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST; NET «VGA_HSYNC» LOC = «P90» | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST; NET «VGA_VSYNC» LOC = «P94» | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST;

## CLK 50 MHz NET «CLK» LOC = «P183» | IOSTANDARD = LVCMOS33; NET «CLK» TNM = «CLK_TN»; TIMESPEC TS_CLK = PERIOD «CLK_TN» 20 ns HIGH 50%;

# PS/2 KEYBOARD NET «PS2_CLK» LOC = «P99» | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST; NET «PS2_DATA» LOC = «P100» | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST; С помощью САПР Aldec Active-HDL и Xilinx ISE производится синтез и трассировка проекта ПЛИС. Из-за сложности обработки событий, отладку проводил без написания Testbench, напрямую заливая прошивку в ПЛИС и проверяя вывод на дисплей. Как правило, работало всё сразу. Основные ошибки заключались в синхронизации сигналов. Например, одновременные операции защелкивания адреса и попытки чтения данных. Исправляются такие ошибки быстро введением в нужном месте дополнительной задержки на такт. В тяжелых случаях использовался ChipScope Pro (Core Inserter и Analyzer).

Заключение Мини-игра «Сапёр» успешно заработала на отладочной плате.Размеры поля 8×8, количество мин на поле — 8.Количество игр — 32. Перед стартом расстановка мин выбирается случайно из памяти для поля.Занимаемые ресурсы кристалла (ПЛИС почти пустая): 720e5f8bed5c45c8b83266478c90ad1c.png

ФотоРезультат выглядит примерно вот так:

5a2d693c32684bf8b7ad5647579b48cc.JPG

Ещё фото… Трассировка в FPGA-Editor’e в области контроллера игры: 722e2431d33b4b09b61ce49c35e51417.png

Схематический вид проекта в RTL Schematic:

20c5a9f38d244975a1ef8015783283bb.png

Отладка проекта в ChipScope Pro Analyzer (подсчет количества открытых пустых полей):

1681b39e29e74635bfeb8471ea9426f0.PNG

Исходный код на github.Видео-демонстрация игры[embedded content]

© Habrahabr.ru