Создание простой игры на базе FPGA

Привет Хабр. Изучение FPGA я начал совсем недавно. Одним из моих проектов, который был направлен на изучения интерфейсов PS/2 и VGA, была игра в Пин-Понг на одного человека. Одна из реализаций которой работает на плате DE0-CV, которую мне любезно предоставил замечательный проект Silicon Russia в рамках конкурса (http://www.silicon-russia.com/2015/12/11/board-giveaway-for-mipsfpga/).
Суть игры: есть ползунок управляемый с клавиатуры, который должен отбивать мячик, еремещающийся по экрану. В качестве средства отображения был выбран VGA дисплей, а клавиатура была выбрана с простым интерфейсом PS/2. Счет самой игры отображается на семисегментном индикаторе.
Отладочная плата

DE0-CV — это официальная отладочная плата, распространяемая Alter’ой, ее цена составляет 150$, а по академической — 99$. На самой плате имеем:
— шесть семисегментных индикаторов, 10 светодиодов, 10 переключателей, 4 кнопки;
— VGA разъем, PS/2 разъем, слот под micro SD карту;
— SDRAM память объемом 64Мбайта;
— два GPIO разъема на 35 выводов каждый.
Логика работы

В программе можно выделить 4 основных блока. Каждый из которых выполняет определённую функцию.
- PLL — готовый ip блок для получения синхронизирующих импульсов обходимых для тактирования системы.
- PS/2 – блок, на вход которого приходят сигналы с PS/2 порта и переводятся в коды нажатых клавиш.
- vga – блок — драйвер для работы с VGA монитором
- game – непосредственно реализация самой логики игры. На входы приходят сигналы с vga, ps2 и pll блоков .
Сердцем всей программы является PLL. Именно благодаря его правильной настройке можно работать с VGA и тактировать другие блоки.

Контроллер PS/2 клавиатуры
Для управления ракеткой в игре мы используем клавиатуру с PS/2 интерфейсом. Перед тем как перейти к рассмотрению реализации блока, давайте немного пробежимся по протоколу PS/2.

Выводами, служащими для обмена данными в протоколе PS/2, являются вывод Data и Clock. Посылка битов состоит из: одного стартового бита, 8 бит данных, бита четности и стоп бита. Вывод Clock служит, как можно догадаться, тактирующими.
Установка битов со стороны устройства происходит по переднему фронту, восходящему фронту Clock, а считывание — со стороны устройства по нисходящему фронту сигнала. Когда устройство ничего не передает, Clock и Data подтянуты к питанию. Затем шина Data и Clock переходит в ноль, что является признаком того, что начата отправка сообщения. После чтения 8 бит, идет бит четности и стоп бит, который всегда равен единице.
В первом обработчике мы считаем такты для того, что бы понять нажата кнопка или нет. Если PS2_CLK_in выставлена в течении 52500000 тактов — кнопка не нажата. Так же тут мы проверяем коды нажатых клавиш:
— в случае если код нажатой клавиши совпадает с кодом клавиши «стрелки вверх» выход up переходит в 1;
— если нажата клавиша «стрелка вниз»-выход down переходит в 1.
always @(negedge clock)
begin
if(PS2_CLK_in == 1)
count_clk <= count_clk + 1;
else
count_clk <= 0;
if(count_clk>=52500000)
begin
led_out <= 0;
end
else
led_out <= bit;
if(led_out == 8'b01110010)
begin
down <= 1;
up <= 0;
end
else
if(led_out == 8'b01110101)
begin
up <= 1;
down <= 0;
end
else
begin
down <= 0;
up <= 0;
end
end
В случае если на входе PS2_CLK_in фиксируется переход от высокого уровня к низкому, то происходит считывания состояния с входа PS2_DAT_in.
always @(negedge PS2_CLK_in)
begin
if(s == 0) begin
if(count<=7)
begin
bit <= bit|(PS2_DAT_in<<count);
end
if(count == 9)
begin
s <= 1;
end
else
begin
count <= count + 1;
end
end
if(s == 1)
if(PS2_DAT_in == 0)
begin
s <= 0;
count <= 0;
bit <= 0;
end
end
endmodule
Код для тестирования в среде ModelSim приведен ниже:
initial
begin
#0 clock_r=1;
#275 clock_r = 1; //s
repeat( 22 )
begin
#25 clock_r=~clock_r;
end
#100 clock_r = 1;
repeat( 22 )
begin
#25 clock_r=~clock_r;
end
#300 clock_r = 1;
repeat( 22 )
begin
#25 clock_r=~clock_r;
end
#50 clock_r = 1;
repeat( 22 )
begin
#25 clock_r=~clock_r;
end
end
initial
begin
#250 PS2_CLK_r = 1; //s
#50 PS2_CLK_r = 0; //start
#50 PS2_CLK_r = 0; //0
#50 PS2_CLK_r = 1; //1
#50 PS2_CLK_r = 1; //2
#50 PS2_CLK_r = 0; //3
#50 PS2_CLK_r = 1; //4
#50 PS2_CLK_r = 0; //5
#50 PS2_CLK_r = 1; //6
#50 PS2_CLK_r = 1; //7
#50 PS2_CLK_r = 1; //parity bit
#50 PS2_CLK_r = 0; //stop
#50 PS2_CLK_r = 1; //s
#50 PS2_CLK_r = 1; //s
#50 PS2_CLK_r = 0; //start
#50 PS2_CLK_r = 1; //0
#50 PS2_CLK_r = 1; //1
#50 PS2_CLK_r = 0; //2
#50 PS2_CLK_r = 0; //3
#50 PS2_CLK_r = 1; //4
#50 PS2_CLK_r = 0; //5
#50 PS2_CLK_r = 1; //6
#50 PS2_CLK_r = 1; //7
#50 PS2_CLK_r = 1; //parity bit
#50 PS2_CLK_r = 0; //stop
#50 PS2_CLK_r = 1; //s
#250 PS2_CLK_r = 1; //s
#50 PS2_CLK_r = 0; //start
#50 PS2_CLK_r = 0; //0
#50 PS2_CLK_r = 1; //1
#50 PS2_CLK_r = 1; //2
#50 PS2_CLK_r = 1; //3
#50 PS2_CLK_r = 1; //4
#50 PS2_CLK_r = 1; //5
#50 PS2_CLK_r = 1; //6
#50 PS2_CLK_r = 1; //7
#50 PS2_CLK_r = 1; //parity bit
#50 PS2_CLK_r = 0; //stop
#50 PS2_CLK_r = 1; //s
#50 PS2_CLK_r = 0; //start
#50 PS2_CLK_r = 0; //0
#50 PS2_CLK_r = 1; //1
#50 PS2_CLK_r = 1; //2
#50 PS2_CLK_r = 0; //3
#50 PS2_CLK_r = 1; //4
#50 PS2_CLK_r = 0; //5
#50 PS2_CLK_r = 1; //6
#50 PS2_CLK_r = 1; //7
#50 PS2_CLK_r = 1; //parity bit
#50 PS2_CLK_r = 0; //stop
#50 PS2_CLK_r = 1; //s
#50 PS2_CLK_r = 1; //s
#50 PS2_CLK_r = 1; //s
end
assign clock = clock_r;
assign PS2_DAT_in = PS2_CLK_r;
Диаграммы поведения блока:

Работа VGA-блока.
Плата DE0 снабжена VGA выходом, в качестве ЦАП, для выходов RGB, используется простая схема на резисторах.
Для начала работы с VGA нам нужно заглянуть в спецификацию VESA(http://tinyvga.com/vga-timing) и выбрать нужный режим работы. Посмотреть необходимую частоту и тайминги. Выберем видорежим 1440x900 60Hz. Необходимая тактовая частота — 106,5Мгц.
На плате установлен кварц на 50МГц. С помощью специального блока PLL мы можем производить преобразование 50МГц в нужные нам 106,5. Для этого нам необходимо вытащить нужный блок на рабочую область и произвести его настройку

Из документации берем необходимые значения таймингов:

parameter h_front_porch = 80;
parameter h_sync = 152;
parameter h_back_porch = 232;
parameter h_active_pixels = 1440;
parameter v_front_porch = 3;
parameter v_sync = 6;
parameter v_back_porch = 25;
parameter v_active_scanilines = 900;
При каждом положительном фронте поступившем на вход pixel_clock, увеличиваем на единицу счетчик pixel_count и в зависимости от его значения выставляется нужный логический уровень на выход горизонтальной синхронизации hsync.
wire w_hsync = (pixel_count < h_sync);
always @(posedge pixel_clock)
begin
hsync <= (pixel_count < h_sync);
hvisible <= (pixel_count >= (h_sync+h_back_porch)) && (pixel_count < (h_sync+h_back_porch+h_active_pixels));
if(pixel_count < (h_sync+h_back_porch+h_active_pixels+h_front_porch) ) begin
pixel_count <= pixel_count + 1'b1;
char_count <= pixel_count;
end
else
begin
pixel_count <= 0;
end
end
Когда счетчик pixel_count доходит до конца строки, происходит увеличение счетчика строк line_count и, в зависимости от заданных ранее параметров, выставляются нужные значения на выход вертикальной синхронизации vsync.
wire w_hsync_buf = w_hsync&~hsync;
always @(posedge pixel_clock)
begin
if(w_hsync_buf)begin
vsync <= (line_count < v_sync);
vvisible <= (line_count >= (v_sync+v_back_porch)) && (line_count < (v_sync+v_back_porch+v_active_scanilines));
if(line_count < (v_sync+v_back_porch+v_active_scanilines+v_front_porch) )begin
line_count <= line_count + 1'b1;
line_count_out <= line_count;
end
else
begin
line_state <= 0;
line_count <= 0;
end
end
end
Когда pixel_count и line_count попадают в диапазон принадлежащий видимой части экрана то visible выставляется в высокий уровень, тем самым разрешая блоку game начинать отрисовку игрового поля:
always @*
begin
visible <= hvisible & vvisible;
end
Работа game блока.
Переход сигнала pixel_state в логическую единицу означает лучение разрешения на отрисовку игрового поля от vga-блока. Входные сигналы char_count и line_count информируют нас о координатах точки, которая отрисовывается на экране в настоящий момент. Исходя из координат мячика и ракетки, закрашиваем нужными цветами зоны, которые соответствуют им.
always @(pixel_state)
begin
if((char_count>=start_horz) && (char_count<=start_horz+50))begin if((line_count>=i) && (line_count<=i+100)) begin
VGA_BLUE<=6'b111110;
end
else
VGA_BLUE<=6'b000000;
end
else
VGA_BLUE<=6'b000000;
if((ball_x-char_count)*(ball_x-char_count)+(ball_y-line_count)*(ball_y-line_count)<400)
VGA_RED<=5'b11110;
else
VGA_RED<=5'b00000;
end
Перерасчет координат мячика и ракетки происходит при восходящем фронте тактового сигнала clk. Так же, если мячик столкнулся со стенкой, происходит изменение направления его движения.
always @(posedge clk)
begin
if(key_2==0)
begin
if(i<vert_sync+vert_back_porch+vert_addr_time) i=i+1; else i=0; end if(key_0==0) begin if(i>vert_sync+vert_back_porch)
i=i-1;
else
i=vert_sync+vert_back_porch+vert_addr_time;
end
if(flag == 2'b00)
begin
ball_x=ball_x-1;
ball_y=ball_y-1;
end
if(flag == 2'b01)
begin
ball_x=ball_x+1;
ball_y=ball_y+1;
end
if(flag == 2'b10)
begin
ball_x=ball_x-1;
ball_y=ball_y+1;
end
if(flag == 2'b11)
begin
ball_x=ball_x+1;
ball_y=ball_y-1;
end
if(ball_y<=vert_sync+vert_back_porch)
if(flag==2'b00)
flag=2'b10;
else
flag=2'b01;
if(ball_x<=horz_sync+horz_back_porch) if(flag==2'b10) flag = 2'b01; else flag = 2'b11; if(ball_y>=vert_sync+vert_back_porch+vert_addr_time)
if(flag==2'b01)
flag=2'b11;
else
flag=2'b00;
if(ball_x>=start_horz && ball_y>=i && ball_y<=i+100) if(flag==2'b11) flag=2'b00; else flag=2'b10; if(ball_x>=horz_sync+horz_back_porch+horz_addr_time)
begin
if(goal_2==9)
begin
goal_2<=0;
goal<=goal+1;
end
else
goal_2<=goal_2+1;
if(flag==2'b11)
flag<=2'b00;
else
flag<=2'b10;
end
end
В случае:
— если шарик не встретился с ракеткой при приближении к правому краю игрового поля, то счет, отображаемый на семисегментных индикаторах, увеличится на единицу, тк происходит срабатывание на изменение goal;
— переполнения goal — происходит: изменение goal_2 и увеличение на единицу десятичного разряда.
always @(clk)
begin
case(goal)
0: HEX_1 <= 7'b1000000;
1: HEX_1 <= 7'b1111001;
2: HEX_1 <= 7'b0100100;
3: HEX_1 <= 7'b0110000;
4: HEX_1 <= 7'b0011001;
5: HEX_1 <= 7'b0010010;
6: HEX_1 <= 7'b0000010;
7: HEX_1<= 7'b1111000;
8: HEX_1 <= 7'b0000000;
9: HEX_1 <= 7'b0010000;
default: HEX_1 <= 7'b1111111;
endcase
end
always @(clk)
begin
case(goal_2)
0: HEX_2 <= 7'b1000000;
1: HEX_2 <= 7'b1111001;
2: HEX_2 <= 7'b0100100;
3: HEX_2 <= 7'b0110000;
4: HEX_2 <= 7'b0011001;
5: HEX_2 <= 7'b0010010;
6: HEX_2 <= 7'b0000010;
7: HEX_2 <= 7'b1111000;
8: HEX_2 <= 7'b0000000;
9: HEX_2<= 7'b0010000;
default: HEX_2 <= 7'b1111111;
endcase
end
Заключение
Синтезируем полученный проект и получаем статистику по занятым в ПЛИС ресурсам:

Реализуя этот проект, мы увидели, что с помощью FPGA достаточно просто можно реализовывать сложные интерфейсы такие как VGA, с очень высокими требованиями к таймингам которые трудно выдержать используя МК. https://github.com/MIPSfpga/pre-mipsfpga/tree/master/pinpong
PS: в мир FPGA вошел недавно, очень извиняюсь перед более опытными людьми которым мой код выжег глаза. Прошу понять, простить, помочь советом.
Comments 39
Only users with full accounts can post comments. Log in, please.