Игра «Жизнь» в FPGA

в 19:40, , рубрики: fpga, hdmi, игра жизнь, Марсоход3, ненормальное программирование, Параллелизм, параллельное программирование, ПЛИС, метки: ,

Игру жизнь — клеточный автомат уже кажется писали на всех возможных языках программирования.

Меня же интересует технология ПЛИС — и поэтому когда-то я сделал реализацию life для ПЛИС Альтера Cyclone III. Правда поместилось в чип тогда очень мало: всего 32x16 клеток. На таком маленьком поле довольно трудно испытать сложные фигуры.

Сейчас у меня в руках другая плата: тут уже стоит Altera MAX10 с 50-ю тысячами логических элементов. Было интересно, смогу ли я расширить поле хотя бы в 4 раза? В общем задумал сделать хотя бы 64x32.

Результат представлен на этом видео, я называю эту картину: «ружье Госпера убивает самоё себя».

Ниже подробности реализации.
Собственно реализация игры у меня уже была в предыдущем моем проекте для третьего циклона. Весь проект как бы состоит из нескольких частей.

Поле игры жизнь составлено из взаимосвязанных модулей-ячеек написанных на Verilog HDL так, чтобы было возможным за 1 такт вычислить все следующее поколение клеток.
image
Хочется иметь именно такую реализацию, ведь это ПЛИС, значит там можно и нужно делать. Это модель множественных вычислителей, которые работают одновременно параллельно и передают параметры друг другу. Вот этот параллелизм как раз и поражают воображение: поле игры 64x32=2048 параллельных вычислителей работающих в FPGA синхронно! Модуль и вся логика одной ячейки написана на Verilog HDL:

module xcell(
	input wire clk,
	input wire seed_ena,
	input wire life_step,
	input wire in_up_left,
	input wire in_up,
	input wire in_up_right,
	input wire in_left,
	input wire in_right,
	input wire in_down_left,
	input wire in_down,
	input wire in_down_right,
	output reg cell_life
);

wire [3:0]neighbor_number;
assign neighbor_number =
								in_up_left +
								in_up +
								in_up_right +
								in_left +
								in_right +
								in_down_left +
								in_down +
								in_down_right;
	
always @(posedge clk)
	if(seed_ena)
		cell_life <= in_left; 	//do load initial life into cell
	else
	if(life_step)					//recalculate new generation of life
	begin
		if( neighbor_number == 3 )
			cell_life <= 1'b1; //born
		else
		if( neighbor_number < 2 || neighbor_number > 3 )
			cell_life <= 1'b0; //die
	end

endmodule

Потом экземпляры этого модуля многократно создаются и соединяются между собой проводами в единое плоское поле с помощью конструкции generate-endgenerate языка Verilog HDL.

Второй по важности модуль в проекте — это модуль загрузки исходного состояния игры через последовательный порт. Состояние передается в виде текстового файла примерно вот такого вида:

1------**------------------------
2-----*--*-----------------------
3----*----*----------------------
4---*------*---------------------
5---*------*---------------------
6----*----*----------------------
7-----*--*-----------------------
8------**------------------------
9--------------------------------
A--------------------------------
B--------------------------------
C--------------------------------
D--------------------------------
E--------------------------------
F--------------------------------

Значащие символы только '*' (живая клетка) и '-' (нет жизни). Скорость передачи 115200, 8 бит, 1 стоп, без четности. Во время загрузки проекта нужно подержать кнопочку на плате — тогда поле жизни будет засеяно новым состоянием, описанным в текстовом файле.

Ну и последнее — модуль отображения текущего состояния игры. Это текстовый видеоадаптер, в который периодически переписывается состояние ячеек-клеток. Все клетки связаны в циклический сдвиговый регистр, так что можно за WIDTH*HEIGHT тактов прочитать все поле игры и сделать запись в видеоадаптер.

Ну конечно, это все вместе получается довольно мудрено — ведь сама логика игры «жизнь» простая, но обслуживающие модули, модули загрузки и отображения оказываются чуть ли не сложнее самой «жизни».

И вот еще про отображение на экране. Чтобы портировать старый проект на MAX10 и для абсолютно другой платы Марсоход3 придется немного повозиться. Дело в том, что на плате уже нет разъема VGA, с которым было так просто и приятно работать. Теперь на плате стоит HDMI разъем и линии HDMI идут прямо к чипу ПЛИС.

Чтобы управлять линиями HDMI прошлось поизучать довольно много материала. За основу был взят проект на www.fpga4fun.com/HDMI.html Здесь довольно подробно все рассказывается.

HDMI использует последовательную передачу через дифференциальную пару. Всего пар четыре. Три пары передают 8-ми битные цвета R, G, B плюс управляющие сигналы HSYNC и VSYNC. Из-за последовательной передачи для TMDS кодирования требуется рабочая частота в 10 раз выше частоты пикселов на экране. Если частота пикселов 74 МГц при разрешении 1280х720, то для кодирования сигнала уже требуется 740 МГц и это очень много. Ситуацию спасает то факт, что на выходах ПЛИС есть встроенные интерфейс DDIO, то есть сериализатор два-к-одному. Значит максимальная частота в проекте может быть снижена до 370 МГц.

Исходный код модуля HDMI приведен ниже.


module hdmi(
	input wire pixclk,		// 74MHz
	input wire clk_TMDS2,	// 370MHz
	input wire hsync,
	input wire vsync,
	input wire active,
	input wire [7:0]red,
	input wire [7:0]green,
	input wire [7:0]blue,
	output wire TMDS_bh,
	output wire TMDS_bl,
	output wire TMDS_gh,
	output wire TMDS_gl,
	output wire TMDS_rh,
	output wire TMDS_rl
);

wire [9:0] TMDS_red, TMDS_green, TMDS_blue;
TMDS_encoder encode_R(.clk(pixclk), .VD(red  ), .CD(2'b00)        , .VDE(active), .TMDS(TMDS_red));
TMDS_encoder encode_G(.clk(pixclk), .VD(green), .CD(2'b00)        , .VDE(active), .TMDS(TMDS_green));
TMDS_encoder encode_B(.clk(pixclk), .VD(blue ), .CD({vsync,hsync}), .VDE(active), .TMDS(TMDS_blue));

reg [2:0] TMDS_mod5=0;  // modulus 5 counter
reg [4:0] TMDS_shift_bh=0, TMDS_shift_bl=0;
reg [4:0] TMDS_shift_gh=0, TMDS_shift_gl=0;
reg [4:0] TMDS_shift_rh=0, TMDS_shift_rl=0;

wire [4:0] TMDS_blue_l  = {TMDS_blue[9],TMDS_blue[7],TMDS_blue[5],TMDS_blue[3],TMDS_blue[1]};
wire [4:0] TMDS_blue_h  = {TMDS_blue[8],TMDS_blue[6],TMDS_blue[4],TMDS_blue[2],TMDS_blue[0]};
wire [4:0] TMDS_green_l = {TMDS_green[9],TMDS_green[7],TMDS_green[5],TMDS_green[3],TMDS_green[1]};
wire [4:0] TMDS_green_h = {TMDS_green[8],TMDS_green[6],TMDS_green[4],TMDS_green[2],TMDS_green[0]};
wire [4:0] TMDS_red_l   = {TMDS_red[9],TMDS_red[7],TMDS_red[5],TMDS_red[3],TMDS_red[1]};
wire [4:0] TMDS_red_h   = {TMDS_red[8],TMDS_red[6],TMDS_red[4],TMDS_red[2],TMDS_red[0]};

always @(posedge clk_TMDS2)
begin
	TMDS_shift_bh <= TMDS_mod5[2] ? TMDS_blue_h  : TMDS_shift_bh  [4:1];
	TMDS_shift_bl <= TMDS_mod5[2] ? TMDS_blue_l  : TMDS_shift_bl  [4:1];
	TMDS_shift_gh <= TMDS_mod5[2] ? TMDS_green_h : TMDS_shift_gh  [4:1];
	TMDS_shift_gl <= TMDS_mod5[2] ? TMDS_green_l : TMDS_shift_gl  [4:1];
	TMDS_shift_rh <= TMDS_mod5[2] ? TMDS_red_h   : TMDS_shift_rh  [4:1];
	TMDS_shift_rl <= TMDS_mod5[2] ? TMDS_red_l   : TMDS_shift_rl  [4:1];
	TMDS_mod5 <= (TMDS_mod5[2]) ? 3'd0 : TMDS_mod5+3'd1;
end

assign TMDS_bh = TMDS_shift_bh[0];
assign TMDS_bl = TMDS_shift_bl[0];
assign TMDS_gh = TMDS_shift_gh[0];
assign TMDS_gl = TMDS_shift_gl[0];
assign TMDS_rh = TMDS_shift_rh[0];
assign TMDS_rl = TMDS_shift_rl[0];

endmodule

module TMDS_encoder(
	input clk,
	input [7:0] VD,	// video data (red, green or blue)
	input [1:0] CD,	// control data
	input VDE,  	// video data enable, to choose between CD (when VDE=0) and VD (when VDE=1)
	output reg [9:0] TMDS = 0
);

wire [3:0] Nb1s = VD[0] + VD[1] + VD[2] + VD[3] + VD[4] + VD[5] + VD[6] + VD[7];
wire XNOR = (Nb1s>4'd4) || (Nb1s==4'd4 && VD[0]==1'b0);
wire [8:0] q_m = {~XNOR, q_m[6:0] ^ VD[7:1] ^ {7{XNOR}}, VD[0]};

reg [3:0] balance_acc = 0;
wire [3:0] balance = q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7] - 4'd4;
wire balance_sign_eq = (balance[3] == balance_acc[3]);
wire invert_q_m = (balance==0 || balance_acc==0) ? ~q_m[8] : balance_sign_eq;
wire [3:0] balance_acc_inc = balance - ({q_m[8] ^ ~balance_sign_eq} & ~(balance==0 || balance_acc==0));
wire [3:0] balance_acc_new = invert_q_m ? balance_acc-balance_acc_inc : balance_acc+balance_acc_inc;
wire [9:0] TMDS_data = {invert_q_m, q_m[8], q_m[7:0] ^ {8{invert_q_m}}};
wire [9:0] TMDS_code = CD[1] ? (CD[0] ? 10'b1010101011 : 10'b0101010100) : (CD[0] ? 10'b0010101011 : 10'b1101010100);

always @(posedge clk) TMDS <= VDE ? TMDS_data : TMDS_code;
always @(posedge clk) balance_acc <= VDE ? balance_acc_new : 4'h0;

endmodule

module ddio(
	input wire d0,
	input wire d1,
	input wire clk,
	output wire out
	);

reg r_d0;
reg r_d1;
always @(posedge clk)
begin
	r_d0 <= d0;
	r_d1 <= d1;
end
assign out = clk ? r_d0 : r_d1; 
endmodule

Весь проект для платы Марсоход3 можно взять на github: github.com/marsohod4you/FPGA_game_life

Отчет компилятора Altera Quartus Prime:
Flow Status Successful — Thu Apr 28 16:08:48 2016
Quartus Prime Version 15.1.0 Build 185 10/21/2015 SJ Lite Edition
Revision Name max10_50
Top-level Entity Name top
Family MAX 10
Device 10M50SAE144C8GES
Timing Models Preliminary
Total logic elements 29,432 / 49,760 ( 59 % )
Total combinational functions 28,948 / 49,760 ( 58 % )
Dedicated logic registers 2,238 / 49,760 ( 4 % )
Total registers 2254
Total pins 23 / 101 ( 23 % )
Total virtual pins 0
Total memory bits 147,456 / 1,677,312 ( 9 % )
Embedded Multiplier 9-bit elements 0 / 288 ( 0 % )
Total PLLs 1 / 1 ( 100 % )
UFM blocks 0 / 1 ( 0 % )
ADC blocks 0 / 1 ( 0 % )

Вероятно игра «жизнь» уже многим надоела. Однако, на мой взгляд тут есть над чем поразмыслить. Несмотря на свою простоту, в ней заложены интересные принципы взаимосвязанных вычислителей. Вероятно, похожие идеи могут быть использованы в специальных классах задач. Например, размещение компонентов на печатной плате — это сложная комбинаторная задача, которая должна учитывать множество факторов и в том числе длины связей между компонентами. Можно представить себе, что компоненты на печатной плате — это клетки, борющиеся за более удачное расположение на поле жизни под влиянием сил связей между компонентами. Думаю, что со временем такие задачи будут рассчитываться с помощью FPGA.

Автор: nckma

Источник

Поделиться новостью

* - обязательные к заполнению поля