Привет Хабр. Изучение 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 вошел недавно, очень извиняюсь перед более опытными людьми которым мой код выжег глаза. Прошу понять, простить, помочь советом.