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

от автора

1


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

Суть игры заключается в том что есть ползунок управляемый с клавиатуры должен отбивать мячик который перемещается по экрану. В качестве средства отображения был выбран VGA дисплей, а клавиатура была выбрана с простым интерфейсом PS/2, счет самой игры отображается на семисегментном индикаторе.
Отладочная плата

2


DE0-CV это официальная отладочная плата распространяемая Alter’ой, ее цена составляет 150$, а по академической 99$. На самой плате имеем:

— шесть семисегментных индикаторов, 10 светодиодов, 10 переключателей, 4 кнопки

— VGA разъем, PS/2 разъем, слот под micro SD карту

— SDRAM память объемом 64Мбайта

— два GPIO разъема на 35 выводов каждый.
Логика работы

3


В программе можно выделить 4 основных блока. Каждый из которых выполняет определённую функцию.

  • PLL уже готовый ip блок нужный для получения синхронизирующий импульсов нужных для тактирования системы.
  • PS/2 – блок на вход которого приходят сигналы с PS/2 порта и переводятся в коды нажатых клавиш.
  • vga – блок — драйвером для работы с VGA монитором
  • game – непосредственно реализует саму логику игры, на входы приходят сигналы с vga, ps2 и pll блоков .

Сердцем всей программы является PLL, именно благодаря его правильной настройке можно работать с VGA и тактировать другие блоки.

4


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

5


Выводами служащими для обмена данными в протоколе PS/2 являются вывод Data и Clock. Посылка битов состоит из одного стартового бита, 8 бит данных, бита четности и стоп бита. Вывод Clock служит как можно догадаться тактирующим.

Установка битов со стороны устройства происходит по переднему фронту восходящему фронту Clock, а считывание со стороны устройства по нисходящему фронту сигнала. Когда устройство ничего не передает Clock и Data подтянуты к питанию. Затем шина Data и Clock переходит в ноль, что является признаком того что начата отправка сообщения. После чтения 8 бит, идет бит четности и стоп бит который всегда равен единице.

В первом обработчике мы считаем такты для того что бы понять нажата кнопка или нет, если  PS2_CLK_in выставлена в течении 52500000 тактов то кнопка не нажата. Так же тут мы проверяем коды нажатых клавиш, в случае если код нажатой клавиши совпадает с кодом клавиши «стрелки вверх» то выход up переходит в 1, если нажата клавиша «стрелка вниз» то выход down переходит в 1.

<pre>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 </pre> 

В случае если на входе PS2_CLK_in фиксируется, переход от высокого уровня к низкому, то происходит считывания состояния с входа PS2_DAT_in.

<pre>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 </pre> 

Код для тестирования в среде ModelSim приведен ниже:

<pre>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; </pre> 

Диаграммы поведения блока:

6


Работа VGA-блока.
 

Плата DE0 снабжена VGA выходом, в качестве ЦАП для выходов RGB используется простая схема на резисторах.

Для начала работы с VGA нам нужно заглянуть в спецификацию VESA(http://tinyvga.com/vga-timing) и выбрать нужный режим работы.  Соответственно посмотреть частоту необходимую и тайминги. Выберем видорежим 1440×900 60Hz. Необходимая тактовая частота для этого режима 106,5Мгц.

На плате установлен кварц на 50МГц, с помощью специального блока PLL мы можем производить преобразование 50МГц в нужные нам 106,5. Для этого нам необходимо вытащить нужный блок на рабочую область и произвести его настройку.

7


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

8

<pre>	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;  </pre> 

При каждом положительном фронте поступившем на вход pixel_clock, увеличиваем на единицу счетчик pixel_count и в зависимости от его значения выставляется нужный логический уровень на выход горизонтальной синхронизации hsync.

<pre>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 </pre> 

Когда счетчик pixel_count доходит до конца строки, происходит увеличение счетчика строк line_count и соответственно в зависимости от заданных ранее параметров выставляются нужные значения на выход вертикальной синхронизации vsync.

<pre>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  </pre> 

Когда pixel_count и line_count попадают в диапазон принадлежащий видимой части экрана то visible выставляется в высокий уровень, тем самым разрешая блоку game начинать отрисовку игрового поля:

<pre>always @* begin 	visible <= hvisible & vvisible; end </pre> 

Работа game блока.
Переход сигнала pixel_state в логическую единицу означает что получено разрешение на отрисовку игрового поля от vga-блока.  Входные сигналы char_count и line_count информируют нас о координатах точки которая отрисовывается на экране в настоящий момент. Исходя из координат мячика и ракетки закрашиваем нужными цветами зоны которые соответствуют им.

<pre>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 </pre> 

Перерасчет координат мячика и ракетки происходит при восходящем фронте тактового сигнала clk.  Так же если мячик столкнулся со стенкой происходит изменение направления его движения.

<pre>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 </pre> 

Так же в случае если шарик не встретился с ракеткой при приближении к правому краю игрового поля, то счет отображаемый на семисегментных индикаторах увеличится на единицу тк происходит срабатывание на изменение goal, в случае переполнения goal  происходит изменение goal_2 и увеличение на единицу десятичного разряда.

<pre>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 </pre> 

Заключение
Синтезируем полученный проект и получаем статистику по занятым в ПЛИС ресурсам:

9


Реализуя этот проект мы увидели, что с помощью FPGA достаточно просто можно реализовывать сложные интерфейсы такие как VGA, с очень высокие требования к таймингам которые трудно выдержать используя МК. Ссылка на проект: https://github.com/MIPSfpga/pre-mipsfpga/tree/master/pinpong

ссылка на оригинал статьи https://habrahabr.ru/post/281549/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *