Construa um jogo PONG com FPGA

Construa um jogo PONG com FPGA Deixe um comentário

Se você gosta de videogames, com certeza já deve ter ouvido falar de Pong, um dos primeiros videogames da história. Desenvolvido pela Atari em 1972, o arcade é extremamente simples, basicamente um simulador de um jogo de tênis, onde dois jogadores competem para marcar mais pontos utilizando uma “raquete” para jogar a bola para fora do campo adversário.

Pong foi um grande sucesso na época e teve grande influência no desenvolvimento da indústria de jogos. Atualmente é incrivelmente simples construir um jogo similar a pong, algumas poucas linhas de código podem facilmente transformar um Arduino em um pong arcade. No entanto, o jogo original não foi programado em um antigo microprocessador, mas sim num hardware específico que foi confeccionado para “executar” toda a lógica de jogo.

Com isso em mente, um dos meus professores lançou um desafio à turma de Eletrônica Digital: construir um jogo similar a Pong utilizando apenas circuitos digitais, sem escrever uma linha de código sequer. Obviamente, um circuito desses pode ser bastante complexo de projetar e confeccionar, principalmente considerando as conexões a serem feitas (imaginem uma selva de jumpers em uma protoboard). Então, esse projeto ficou apenas no simulador. No entanto, há um tempo atrás uma ideia me veio à mente: por que não desenvolver esse circuito utilizando um hardware reconfigurável como uma FPGA?

Construa um jogo PONG com FPGA

Neste post, vamos projetar e desenvolver um Pong Clone simplificado utilizando a FPGA Altera Cyclone II EP2C5T.

Material necessário

Montagem do circuito

O circuito utilizado neste projeto está descrito na imagem abaixo:

Construa um jogo PONG com FPGA

Desenvolvimento

FPGAs e Cyclone II EP2C5T144

Uma FPGA é um dos módulos de hardware reconfigurável disponíveis no mercado e um dos mais utilizados. Diferente de microcontroladores, que podem ser modificados a nível de software, ou seja, pode-se modificar o programa executado pelo microcontrolador, uma FPGA pode ser modificada a nível de hardware: todo o circuito interno pode ser modificado. Essa habilidade dá às FPGAs uma flexibilidade incrivelmente superior à de microprocessadores convencionais, ao custo de um projeto mais complexo e um custo mais elevado.

Construa um jogo PONG com FPGA

A Cyclone II é uma linha de FPGAs de baixo custo desenvolvida pela Altera. Especificamente a EP2C5T144 é uma FPGA da linha Cyclone II bem simples, mas também bastante poderosa, ideal para aqueles que estão começando a utilizar FPGAs. Para saber mais sobre FPGAs, dê uma olhada nos posts: Primeiros passos de FPGA com a Papilio One e Conheça a FPGA Altera Cyclone II – neste último, demonstrei os primeiros passos com a EP2C5T144 e da Linguagem de Descrição de Hardware (HDL, em inglês) VHDL, uma das duas HDLs mais populares disponíveis.

Pong Clone

Como dito anteriormente, a ideia deste projeto é construir um clone de pong utilizando apenas componentes digitais. O pong original é, surpreendentemente, mais complexo do que aparenta e é possível ver o esforço empregado pelos seus desenvolvedores a fim de criar um jogo divertido e desafiante. Você pode ler mais sobre a história do pong na wikipedia. Em nosso caso, vamos simplificar um pouco o projeto. Os requisitos planejados são:

  • Até dois jogadores;
  • Possibilidade de alternar entre controle manual e IA para ambos os jogadores;
  • A bola rebate tanto nas “paredes” superior e inferior quanto nas raquetes;
  • Ao colidir verticalmente a velocidade vertical da bola é invertida;
  • Ao colidir horizontalmente a velocidade horizontal da bola é invertida;
  • A bola deve chegar ao fim do campo adversário para pontuar;
  • A medida que os jogadores pontuam, a velocidade da bola aumenta;
  • O primeiro jogador a fazer 7 pontos vence.

Diagrama em Blocos do Projeto

Como dito no post “Conheça a FPGA Altera Cyclone II”, é útil fazer um diagrama em blocos para auxiliar o desenvolvimento do projeto com FPGA. O diagrama representando o circuito desenvolvido neste artigo está descrito abaixo.

Construa um jogo PONG com FPGA

Vamos analisar individualmente cada módulo de hardware para ver como funciona e como interage com os outros.

Todos os módulos serão descritos em VHDL (não entrarei em detalhes do código pois em projetos de lógica reconfigurável acredito ser mais importante descrever como o hardware funciona). Em meu post anterior há uma introdução rápida aos fundamentos básicos de VHDL, com isso você não deve ter grandes dificuldades em entender os códigos. Para o desenvolvimento e gravação utilizarei a IDE Quartus II, específica para FPGAs da Altera.

O Módulo Clock Enable Source

A EP2C5T tem um cristal oscilador embutido com frequência de 50 MHz. Essa frequência é muito alta para nosso circuito, portanto é necessário reduzi-la para um valor mais apropriado. Uma das formas de se obter um sinal de menor frequência a partir de uma frequência maior é através de um divisor de clock.

Um divisor de clock é um dispositivo capaz de reduzir um sinal de clock dividindo-o por um certo fator. Se esse fator for um múltiplo inteiro de dois, então o divisor se torna um simples contador binário, veja a imagem abaixo:

Construa um jogo PONG com FPGA

A imagem acima retrata um contador binário de dois bits (conta de 0 a 3) que incrementa sua contagem a cada pulso de clock. Repare no bit menos significativo do contador (o bit mais à direita) e note que ele troca de 0 para 1 na metade da frequência do clock, ou seja, é um divisor de clock de fator dois. Cada bit à direita do sinal é dividido novamente por dois, desta forma o bit mais significativo (mais à esquerda) é um divisor de fator 4.

Neste projeto, utilizaremos 5 sinais de clock com valores aproximados de 1.5 kHz, 24 Hz, 12 Hz, 6 Hz e 3 Hz. Todos esses sinais podem ser obtidos, considerando um clock de 50 MHz, com um divisor de clock com 24 bits de profundidade, utilizando os bits 15, 20, 21, 22 e 23 do contador.

No entanto, utilizar diversos sinais de clock obtidos através de um divisor de clock não é recomendado pois não há garantias de que os sinais estarão sincronizados. Todo circuito digital tem um certo atraso de resposta e, como na FPGA os circuitos são montados através da combinação de diversos circuitos menores (LEs), o atraso total final do circuito pode ser bem significativo e levar a erros. Para contornar essa situação, todos os circuitos devem estar conectados ao mesmo sinal de clock e utilizar sinais de enables para definir a frequência em que devem operar.

Sinais de enable devem estar sincronizados com o clock do sistema e produzir um sinal em alto, com duração de um único pulso de clock, na frequência desejada para o circuito. Ao ligar tanto o clock do sistema quanto o sinal de enable, o efeito prático é que o circuito funcionará na frequência especificada.

Construa um jogo PONG com FPGA

Como mostrado na imagem, um gerador de enable também pode ser obtido com o uso de um contador. Porém, ao invés de ligar o sinal de cada bit às saídas, compara-se o valor de uma parte do contador com um certo número binário e a saída se ativa apenas quando ambos os valores são iguais. Na imagem acima, a primeira saída tem uma frequência de metade do sinal de clock; a segunda, um quarto; e a terceira, um oitavo.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity ClkEnSrc is
    port(
        clk_50M : in std_logic;
        rst : in std_logic;
        en : in std_logic;
        en_1k5Hz : out std_logic;
        en_24Hz : out std_logic;
        en_12Hz  : out std_logic;
        en_6Hz  : out std_logic;
        en_3Hz  : out std_logic
    );
end entity;

architecture a_ClkEnSrc of ClkEnSrc is
    constant ZERO : unsigned(23 downto 0) := to_unsigned(0, 24);
    signal cnt : unsigned(23 downto 0);
begin
    process(clk_50M, rst)
    begin
        if rst = '1' then
            cnt <= ZERO;
        elsif (clk_50M'event and clk_50M='1') and en='1' then
            if cnt < 16777215 then
                cnt <= cnt + 1;
            else
                cnt <= ZERO;
            end if;
        else
            cnt <= cnt;
        end if;
    end process;

    en_1k5Hz  <= '1' when (cnt(15 downto 0))=32768   else '0';    -- 1k526Hz    2^-16
    en_24Hz   <= '1' when (cnt(20 downto 0))=1048576 else '0';     -- 23.842Hz    2^-21
    en_12Hz   <= '1' when (cnt(21 downto 0))=2097152 else '0';     -- 11.921Hz    2^-22
    en_6Hz    <= '1' when (cnt(22 downto 0))=4194304 else '0';      --  5.960Hz    2^-23
    en_3Hz    <= '1' when (cnt(23 downto 0))=8388608 else '0';      --  2.980Hz    2^-24
end architecture;

O Módulo Racket

Este módulo é responsável por representar a raquete do jogador, receber os comandos do jogador ou da IA e mover a raquete de acordo (a colisão com a bola é tratada por outro módulo). Uma vez que as raquetes só podem se mover em uma dimensão, a forma mais fácil que encontrei de representar o jogador foi utilizando um registrador de deslocamento.

Construa um jogo PONG com FPGA

O módulo contém duas entradas e oito saídas (uma vez que a matriz de LEDs utilizada como tela do jogo é de 8×8). Inicialmente, apenas as três saídas centrais estão ativas (representando a raquete) e, a cada pulso de clock, todo conteúdo do registrador é deslocado para a esquerda ou para direita, dependendo de qual entrada estiver ativa. Se ambas ou nenhuma estiverem ativas, então o conteúdo do registrador permanece o mesmo e a raquete permanece parada. A raquete não deve ser capaz de atravessar os limites superiores ou inferiores da tela, portanto os bits mais significativos e menos significativos são checados. Caso estejam ativos, o deslocamento na direção correspondente é bloqueado.

library ieee;

use ieee.std_logic_1164.all;




entity Racket is

    port(

        clk, rst, en, l, r : in std_logic;

        q : out std_logic_vector(7 downto 0)

    );

end entity;




architecture a_Racket of Racket is

    signal data : std_logic_vector(7 downto 0) := "00111000";

begin

    process(clk, rst)

    begin

        if rst='1' then

            data <= "00111000";

        elsif (clk'event and clk='1') and en='1' then

            if l='1' and r='0' and data(7)='0' then

                data <= data(6 downto 0) & '0';

            elsif l='0' and r='1' and data(0)='0' then

                data <= '0' & data(7 downto 1);

            else

                data <= data;

            end if;

        else

            data <= data;

        end if;

    end process;

    q <= data;

end architecture;

O Módulo Ball

A bola é um pouco mais complicada do que a raquete, uma vez que pode se mover tanto na vertical quanto na horizontal. Mas uma solução semelhante com registradores de deslocamento pode ser utilizada.

No início da partida, e toda vez que um ponto é marcado, a bola se posiciona no centro da tela. Como a matriz utilizada tem lados pares, não há um centro perfeito, temos que escolher um dos quatro pixels centrais para ser a posição inicial da bola. Ao invés de simplesmente escolher uma das quatro posições de forma fixa, preferi fazer com que, cada vez que a bola é reposicionada, um dos quatro cantos é escolhido de forma aleatória. A direção inicial da bola também é escolhida aleatoriamente.

Construa um jogo PONG com FPGA

Dois registradores de deslocamento são usados para representar as posições vertical e horizontal da bola. A direção é armazenada em dois registradores de 1 bit. Cada vez que a bola atinge um dos limites da tela, o bit da direção correspondente é automaticamente invertido e a bola passa a ir na direção oposta.

A colisão com as raquetes vai ser tratada por outro módulo, portanto adicionei uma entrada que permite inverter a direção horizontal (a bola colide com a raquete apenas horizontalmente) externamente.

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;




entity Ball is

    port(

        en : in std_logic;

        clk : in std_logic;

        rst : in std_logic;

        ball_clear : in std_logic;

        tx  : in std_logic;

        rnd_4 : in std_logic_vector(3 downto 0);

        

        pbx : out std_logic_vector(7 downto 0);

        pby : out std_logic_vector(7 downto 0)

    );

end entity;




architecture a_ball of ball is

    signal ball_x : std_logic_vector(7 downto 0) := "00001000";

    signal ball_y : std_logic_vector(7 downto 0) := "00010000";

    signal x_dir : std_logic := '0';

    signal y_dir : std_logic := '0';

begin

    process(clk, rst)

    begin

        if rst='1' then

            ball_y <= "000" & rnd_4(3) & (not rnd_4(3)) & "000";

            ball_x <= "000" & rnd_4(2) & (not rnd_4(2)) & "000";

            y_dir <= rnd_4(1);

            x_dir <= rnd_4(0);

        elsif (clk'event and clk='1') and en='1' then

            if ball_clear='1' then

                ball_y <= "000" & rnd_4(3) & (not rnd_4(3)) & "000";

                ball_x <= "000" & rnd_4(2) & (not rnd_4(2)) & "000";

                y_dir <= rnd_4(1);

                x_dir <= rnd_4(0);

            else

                if tx='1' then

                    x_dir <= not x_dir;

                    if x_dir='1' then

                        ball_x <= ball_x(6 downto 0) & '0';

                    else

                        ball_x <= '0' & ball_x(7 downto 1);

                    end if;

                else

                    if ((ball_x(6) and not x_dir) or (ball_x(1) and x_dir))='1' then

                        x_dir <= not x_dir;

                    end if;

                    if x_dir='1' then

                        ball_x <= '0' & ball_x(7 downto 1);

                    else

                        ball_x <= ball_x(6 downto 0) & '0';

                    end if;

                end if;

                if ((ball_y(6) and not y_dir) or (ball_y(1) and y_dir))='1' then

                    y_dir <= not y_dir;

                end if;

                if y_dir='1' then

                    ball_y <= '0' & ball_y(7 downto 1);

                else

                    ball_y <= ball_y(6 downto 0) & '0';

                end if;

            end if;

        end if;

    end process;

    pbx <= ball_x;

    pby <= ball_y;

end architecture;

O Módulo RNG

Tanto a posição inicial, quanto a direção inicial da bola, são escolhidas aleatoriamente, portanto precisamos de um gerador de aleatoriedade. Assim como computadores, é praticamente impossível implementar um Gerador Aleatório Verdadeiro utilizando componentes digitais,. Por isso, vamos recorrer a um Gerador Pseudo-Aleatório (em inglês Random Number Generator – RNG) para produzir um sequência de bits determinística (pode ser prevista), mas que parece ser aleatória (muito difícil de prever).

Existem centenas de geradores pseudo-aleatórios, cada qual com suas vantagens e desvantagens. Mas, como nossa aplicação não é crítica, podemos implementar um gerador simples. Escolhi o gerador Fibonacci LFSRs por ser bem fácil de implementar em hardware.

Construa um jogo PONG com FPGA

Note que esse gerador se baseia em deslocar um registrador à esquerda, descartar o bit mais significativo e gerar um novo bit menos significativo a partir dos valores anteriores de certos bits, utilizando operações XOR. Tanto o deslocamento à esquerda (com descarte de bit) quanto as portas XOR podem ser facilmente implementadas em FPGA.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity RNG is
    port(
        clk : in std_logic;
        rst : in std_logic;
        rnd : out std_logic_vector(3 downto 0)
    );
end entity;

architecture a_RNG of RNG is
    signal X  : std_logic_vector(15 downto 0) := "1001001001000101";
    signal X0: std_logic;
begin
    
    X0 <= X(15) xor (X(13) xor (X(12) xor X(10)));
    
    process(clk, rst)
    begin
        if rst='1' then
            X <= "1001001001000101";
        elsif (clk'event and clk='1') then
            X <= X(14 downto 0) & X0;
        else
            X <= X;
        end if;
    end process;
    
    rnd <= X(3 downto 0);
    
end architecture;

Para melhorar a ilusão de aleatoriedade, o gerador constantemente produz novos números na frequência do clock principal (50MHz). Embora o valor inicial seja sempre o mesmo, como o tempo que leva para o circuito necessitar de um novo número aleatório depende dos jogadores (somente é necessário quando um ponto é marcado), as sequências aleatórias são difíceis de prever.

O Módulo Score Register

Este é provavelmente um dos módulos mais simples de todo o projeto: é, basicamente, um contador usado para armazenar a pontuação dos jogadores. Como um jogador é considerado vencedor caso atinja 7 pontos, bastam duas memórias de 3 bits para armazenar todas as pontuações possíveis. Este módulo possui duas entradas de incremento. Esses sinais ficam ativos logo após a bola atingir o canto adversário e instruem o registrador a incrementar a pontuação do jogador correspondente.

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;




entity ScoreRegister is

    port(

        rst : in std_logic;

        clk : in std_logic;

        en  : in std_logic;

        inc_S1 : in std_logic;

        inc_S2 : in std_logic;

        q1 : out std_logic_vector(2 downto 0);

        q2 : out std_logic_vector(2 downto 0)

    );

end entity;




architecture a_ScoreRegister of ScoreRegister is

    constant ZERO : unsigned(2 downto 0) := to_unsigned(0, 3);

    signal Score1, Score2 : unsigned(2 downto 0) := ZERO;

begin

    process(clk, rst)

    begin

        if rst='1' then

            Score1 <= ZERO;

            Score2 <= ZERO;

        elsif (clk'event and clk='1') and en='1' then

            if inc_S1='1' then

                Score1 <= Score1 + 1;

            else

                Score1 <= Score1;

            end if;

            if inc_S2='1' then

                Score2 <= Score2 + 1;

            else

                Score2 <= Score2;

            end if;

        else

            Score1 <= Score1;

            Score2 <= Score2;

        end if;

    end process;

    q1 <= std_logic_vector(Score1);

    q2 <= std_logic_vector(Score2);

end architecture;

O Módulo Control

Como o nome diz, este módulo controla o do fluxo do jogo, sendo responsável por detectar colisões da bola com a raquete, rebater a bola, detectar quando a bola chega ao canto adversário, incrementar as pontuações, reposicionar a bola, controlar a velocidade da bola e finalizar o jogo quando um jogador marcar 7 pontos.

Esse circuito é puramente combinacional (independente de clock), recebendo como entrada as posições das raquetes, da bola, a pontuação de cada jogador e, dependendo do valor das entradas, produz as saídas correspondentes, sendo elas:

  • en_sel – Sinal de controle da seleção do sinal de enable da bola, sendo efetivamente responsável pela seleção da velocidade da bola. A multiplexação do sinal de enable é feita externamente. Este sinal é apenas o sinal de controle da seleção;
  • end_state – Flag de fim de jogo. Fica ativa quando um dos jogadores tem uma pontuação superior ou igual a sete;
  • ball_tx – Sinal de inversão da direção horizontal da bola. Ativa-se quando é detectado uma colisão da bola com a raquete para fazer com que a bola “rebata” e mude de direção;
  • ball_clear – Sinal de clear da bola. Ativado toda vez que um jogador pontua para reposicionar a bola no centro da tela;
  • inc1 e inc2 – Sinais de incremento da pontuação dos jogadores. Ativam-se, respectivamente, quando a bola atinge o canto dos jogadores 2 e 1 para instruir que o registrador de pontuação incremente a pontuação do respectivo jogador.
library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;




entity Control is

    port(

        -- Control

        clk : in std_logic;

        en  : in std_logic;

        rst : in std_logic;

        en_sel : out std_logic_vector(1 downto 0);

        end_state : out  std_logic;

        -- Racket

        racket_1 : in std_logic_vector(7 downto 0);

        racket_2 : in std_logic_vector(7 downto 0);

        -- Ball

        pbx, pby : in std_logic_vector(7 downto 0);

        ball_tx : out std_logic;

        ball_clear : out std_logic;

        -- Score

        score1, score2 : in std_logic_vector(2 downto 0);

        inc1, inc2 : out std_logic

    );

-- en_sel

end entity;




architecture a_Control of Control is

    signal p1_uscore, p2_uscore, max_score : unsigned(2 downto 0);

    signal p1_scored, p2_scored : std_logic;

    signal r1_bounce, r2_bounce : std_logic;

    signal end_game : std_logic;

begin

    p1_uscore <= unsigned(score1);

    p2_uscore <= unsigned(score2);

    max_score <= p1_uscore when p1_uscore > p2_uscore else p2_uscore;




    p1_scored <= pbx(0);

    p2_scored <= pbx(7);




    r1_bounce <= '1' when pbx(6)='1' and (pby and racket_1)/="00000000" else '0';

    r2_bounce <= '1' when pbx(1)='1' and (pby and racket_2)/="00000000" else '0';




    end_game <= '1' when max_score>=to_unsigned(7, 3) else '0';




    en_sel <= "00" when max_score < 2 else 

              "01" when max_score < 4 else

              "10" when max_score < 5 else

              "11" when max_score < 7 else

              "--";




    end_state <= end_game;

    ball_tx <= r1_bounce or r2_bounce;

    ball_clear <= p1_scored or p2_scored;

    inc1 <= p1_scored;

    inc2 <= p2_scored;

end architecture;

O Módulo GPU

Este módulo é responsável por coletar informações de todos os módulos que representam ou armazenam informações que devem ser mostradas ao jogador (como a raquete e a bola) e exibi-las nos displays apropriados.

O Campo do Jogo consiste em uma matriz de LEDs de 8 x 8 totalizando 64 LEDs. Além dela, há outros seis LEDs utilizados para exibir as pontuações dos jogadores, sendo um total de 70 LEDs que devem ser controlados pela FPGA. Embora a EP2C5T nos permita utilizar até 89 pinos, utilizar uma GPIO diretamente conectada a cada LED é uma péssima ideia! Não apenas pela complexidade das conexões (lembra de selva de fios?), mas também por questões de consumo de corrente, pois cada LED precisa de cerca de 15 mA. Considerando o caso extremo onde todos os leds estejam ativos, um total de 1,17 A seriam consumidos apenas pelos LEDs (não consegui encontrar informações se a FPGA suporta tal consumo de corrente).

Então, vamos utilizar um velho truque para lidar com todos esses LEDs: a multiplexação. Sem entrar em grandes detalhes, a multiplexação consiste em ativar apenas um, ou um pequeno conjunto, de LEDs e alternar entre todos os conjuntos ativos em um período inferior a persistência da imagem no olho humano. Desta forma, temos a ilusão que todos os LEDs estão ativos. Dê uma olhada nos projetos Como construir um relógio com Arduino e Aprenda a construir um Relógio Binário com Arduino, ambos utilizam multiplexação para acionamento de LEDs.

Construa um jogo PONG com FPGA

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;




entity GPU is

    port(

        --

        clk : in std_logic;

        en  : in std_logic;

        rst : in std_logic;

        --

        racket_1 : in std_logic_vector(7 downto 0);

        racket_2 : in std_logic_vector(7 downto 0);

        ball_x : in std_logic_vector(7 downto 0);

        ball_y : in std_logic_vector(7 downto 0);

        end_state : in std_logic;

        p1score : in std_logic_vector(2 downto 0);

        p2score : in std_logic_vector(2 downto 0);

        --

        cd : out std_logic_vector(7 downto 0);

        cs : out std_logic_vector(7 downto 0);

        ls : out std_logic

    );

end entity;




architecture a_GPU of GPU is

    type M8x8 is array (7 downto 0) of std_logic_vector(7 downto 0);

    

    constant initial_cnt : integer range -1 to 7 := 7;

    constant initial_sel : std_logic_vector(7 downto 0) := "10000000";

    

    signal colunes : M8x8;

    signal cnt : integer range -1 to 7 := 7;

    signal sel : std_logic_vector(7 downto 0);

begin

    -- Colunes

    colunes(7) <= racket_1 when ball_x(7)='0' else (racket_1 or ball_y);

    colunes(6) <= ball_y   when ball_x(6)='1' else "00000000";

    colunes(5) <= ball_y   when ball_x(5)='1' else "00000000";

    colunes(4) <= ball_y   when ball_x(4)='1' else "00000000";

    colunes(3) <= ball_y   when ball_x(3)='1' else "00000000";

    colunes(2) <= ball_y   when ball_x(2)='1' else "00000000";

    colunes(1) <= ball_y   when ball_x(1)='1' else "00000000";

    colunes(0) <= racket_2 when ball_x(0)='0' else (racket_2 or ball_y);




    process(clk, en)

    begin

        if rst='1' then

            cnt <= initial_cnt;

            sel <= initial_sel;

        elsif (clk'event and clk='1') and en='1' then

            if cnt > -1 then

                cnt <= cnt - 1;

                sel <= '0' & sel(7 downto 1);

            else

                cnt <= initial_cnt;

                sel <= initial_sel;

            end if;

        else

            cnt <= cnt;

            sel <= sel;

        end if;

    end process;




    cd <= (not (p1score&"00"&p2score)) when cnt=-1 and rst='0' else

          (not colunes(cnt))           when rst='0' else

          "11111111";

    cs <= sel when rst='0' else "00000000";

    ls <= '1' when cnt=-1 else '0';




end architecture;

O Módulo AI

Este módulo permite a um jogador humano competir contra o computador. A inteligência artificial é bem simples: se a bola estiver acima da raquete, a AI moverá a raquete para cima; se a bola estiver abaixo, a AI moverá a raquete para baixo.

Apesar da simplicidade, como o sistema tem um tempo de resposta muito menor que um humano, seria impossível vencer o computador. Portanto alguns ajustes precisam feitos: primeiramente o clock da AI foi limitado, a fim de reduzir o tempo de resposta do computador; além disso, fiz com que a AI ficasse “míope”, ou seja, ela só enxerga a bola caso ela esteja em sua metade do campo.

Dois módulos de AI foram incluídos, um para cada raquete, que podem ser ativados ou desativados externamente. Assim, há três modos de jogo: 2-jogadores, 1-jogador e 0-jogadores – este último é utilizado para testes e demonstrações, onde a máquina compete contra si mesma.

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;




entity AI is

    port(

        clk : in std_logic;

        en  : in std_logic;

        rst : in std_logic;

        n_r_l : in std_logic := '0';

        pbx : in std_logic_vector(7 downto 0);

        pby : in std_logic_vector(7 downto 0);

        racket : in std_logic_vector(7 downto 0);

        L : out std_logic;

        R : out std_logic

    );

end entity;




architecture a_AI of AI is

    signal ball_field : std_logic_vector(2 downto 0);

    signal RL, RR : std_logic;

    signal ball_hit : std_logic;

    signal ball_in_field : std_logic;

    signal UnRacket : unsigned(7 downto 0);

    signal UnBall : unsigned(7 downto 0);

    signal move, to_left, to_right : std_logic;

begin

    ball_field <= pbx(2 downto 0) when n_r_l='0' else pbx(7 downto 5);

    

    ball_hit <= '1' when (pby and racket)/="00000000" else '0';

    

    ball_in_field <= '1' when ball_field /= "000" else '0';

    

    UnRacket <= unsigned(racket);

    UnBall <= unsigned(pby);

    

    to_left  <= '1' when (UnBall < UnRacket) else '0';

    to_right <= '1' when (UnBall > UnRacket) else '0';

    

    move <= ball_in_field and (not ball_hit);

    RR <= move and to_left;

    RL <= move and to_right;

    

    L <= RL;

    R <= RR;




end architecture;

O Módulo Debouncing

Se você tem alguma experiência com eletrônica, já deve ter se deparado com o problema de debouncing. Ao pressionar uma tecla mecânica, leva um tempo para os contatos se acomodarem internamente. Durante este período, o sinal produzido pelo botão é instável, podendo variar entre alto e baixo várias vezes e causar erros de leituras.

A fim de evitar problemas causados por essa instabilidade, os circuitos de debouncing filtram os sinais das teclas e garantem um sinal estável. Existem várias formas de solucionar o problema de debouncing, tanto por software quanto por hardware analógico ou digital. A solução adotada neste projeto é baseada em um hardware digital, tratando-se de um circuito amostrador digital.

O circuito amostrador, de tempos em tempos, mede o valor de entrada e produz um valor igual na saída. No entanto, entre esses intervalos, o valor da saída permanece constante, independente de mudanças no valor de entrada.

Construa um jogo PONG com FPGA

Se a amostragem for configurada para uma frequência baixa e consequentemente um alto período, maior que a duração do debouncing, todas as variações do sinal antes da estabilização vão ser ignoradas, resultando em um sinal estável.

library ieee;

use ieee.std_logic_1164.all;




entity Debouncing is

    port(

        inpt        : in  std_logic;

        rst        : in  std_logic;

        clk        : in  std_logic;

        en            : in  std_logic;

        outp        : out std_logic

    );

end entity;




architecture a_Debouncing of Debouncing is

    signal state : std_logic := '0';

    signal pulse_detector : std_logic := '0';

begin

    process(rst, clk)

    begin

        if rst='1' then

            state <= '0';

        elsif (clk'event and clk='1') and en='1' then

            state <= inpt;

        else

            state <= state;

        end if;

    end process;

    outp <= state;

end architecture;

A duração média do período de debouncing é de 6~7 ms, portanto a frequência máxima do amostrador deve ser de 142 Hz. Preferi utilizar um valor muito menor, de 24 Hz para a amostragem, considerando que a velocidade máxima das raquetes são de aproximadamente 12 pixels por segundo (frequência de 12Hz). Essa frequência de amostragem não só garante a remoção do debouncing, como também é compatível com o tempo de resposta do jogo.

Entidade Top-Level

Com todos os módulos descritos, precisamos conectá-los em um único circuito. Isso é feito na entidade top-level, que também é responsável por descrever a interface pela qual o circuito sintetizado na FPGA se comunica com os componentes externos.

No post “Conheça a FPGA Cyclone II” foi necessário declarar cada um dos componentes antes de instanciá-los no circuito. Então, para cada um dos módulos um bloco de código de componente foi criado. No entanto, essa solução não é ideal, pois dificulta a modificação rápida e simplificada dos componentes.

component NOME_DO_COMPONENTE is

    port(

        < Interface do Componente >

    );

end component;

Imaginem que criamos um subcircuito, que é utilizado em diversos módulos que, então, compõem a entidade top-level e, para isso, declaramos um bloco componente em cada um dos módulos para descrever esse subcircuito. Imagine agora que precisamos modificar a interface deste subcircuito. Por exemplo: o circuito foi originalmente pensado para trabalhar com dados de 8-bits, mas o projeto agora necessita de dados de 16-bits. Seria necessário modificar os códigos dos componentes de cada um dos módulos que utilizam o subcircuito.

Uma alternativa melhor é uma abordagem semelhante a criar bibliotecas em linguagens de programação e centralizar as declarações dos componentes em um único arquivo, assim, caso haja necessidade de modificação futura, basta modificar apenas um arquivo. Em VHDL, essa solução é obtida por meio dos uso de pacotes ou packages, conjuntos centralizados de componentes, tipos e funções que podem ser facilmente utilizados pelas entidades VHDL.

A sintaxe para a criação de um pacote é bem simples. Neste projeto, estamos criando um pacote chamado pong_package, onde declaramos todos os módulos de hardware que compõem nosso pong clone. A declaração deste pacote está representada abaixo:

library ieee;

use ieee.std_logic_1164.all;




package pong_package is

    

    component Debouncing is

        port(

            inpt        : in  std_logic;

            rst        : in  std_logic;

            clk        : in  std_logic;

            en            : in  std_logic;

            outp        : out std_logic

        );

    end component;

    

    component Racket is

        port(

            clk, rst, en, l, r : in std_logic;

            q : out std_logic_vector(7 downto 0)

        );

    end component;




    component AI is

        port(

            clk : in std_logic;

            en  : in std_logic;

            rst : in std_logic;

            n_r_l : in std_logic := '0';

            pbx : in std_logic_vector(7 downto 0);

            pby : in std_logic_vector(7 downto 0);

            racket : in std_logic_vector(7 downto 0);

            L : out std_logic;

            R : out std_logic

        );

    end component;

    

    component Ball is

        port(

            en : in std_logic;

            clk : in std_logic;

            rst : in std_logic;

            ball_clear : in std_logic;

            tx  : in std_logic;

            rnd_4 : in std_logic_vector(3 downto 0);

            

            pbx : out std_logic_vector(7 downto 0);

            pby : out std_logic_vector(7 downto 0)

        );

    end component;




    component RNG is

        port(

            clk : in std_logic;

            rst : in std_logic;

            rnd : out std_logic_vector(3 downto 0)

        );

    end component;

    

    component ClkEnSrc is

        port(

            clk_50M : in std_logic;

            rst : in std_logic;

            en : in std_logic;

            en_1k5Hz : out std_logic;

            en_24Hz : out std_logic;

            en_12Hz  : out std_logic;

            en_6Hz  : out std_logic;

            en_3Hz  : out std_logic

        );

    end component;




    component ScoreRegister is

        port(

            rst : in std_logic;

            clk : in std_logic;

            en  : in std_logic;

            inc_S1 : in std_logic;

            inc_S2 : in std_logic;

            q1 : out std_logic_vector(2 downto 0);

            q2 : out std_logic_vector(2 downto 0)

        );

    end component;

    

    component Control is

        port(

            -- Control

            clk : in std_logic;

            en  : in std_logic;

            rst : in std_logic;

            en_sel : out std_logic_vector(1 downto 0);

            end_state : out  std_logic;

            -- Racket

            racket_1 : in std_logic_vector(7 downto 0);

            racket_2 : in std_logic_vector(7 downto 0);

            -- Ball

            pbx, pby : in std_logic_vector(7 downto 0);

            ball_tx : out std_logic;

            ball_clear : out std_logic;

            -- Score

            score1, score2 : in std_logic_vector(2 downto 0);

            inc1, inc2 : out std_logic

        );

    -- en_sel

    end component;

    

    component GPU is

        port(

            --

            clk : in std_logic;

            en  : in std_logic;

            rst : in std_logic;

            --

            racket_1 : in std_logic_vector(7 downto 0);

            racket_2 : in std_logic_vector(7 downto 0);

            ball_x : in std_logic_vector(7 downto 0);

            ball_y : in std_logic_vector(7 downto 0);

            end_state : in std_logic;

            p1score : in std_logic_vector(2 downto 0);

            p2score : in std_logic_vector(2 downto 0);

            --

            cd : out std_logic_vector(7 downto 0);

            cs : out std_logic_vector(7 downto 0);

            ls : out std_logic

        );

    end component;

    

end package;

Para utilizarmos o pacote, precisamos declarar seu uso no arquivo VHDL, da mesma forma que fizemos para os pacotes std_logic_1164 e numeric_std. Por padrão, todos os pacotes adicionados ao ambiente de trabalho estão disponíveis na biblioteca work. Então, da mesma forma que feito para os outros pacotes, declaramos a biblioteca work e, logo em seguida, o uso de todos os componentes disponíveis dentro do pacote pong_package. Veja abaixo o código VHDL da entidade top-level:

library ieee;

use ieee.std_logic_1164.all;

library work;

use work.pong_package.all;

entity PongFPGA is

port(

n_rst : in std_logic := '0';

n_pwr_rst : in std_logic := '0';

clk_50M : in std_logic := '0';

sel_ai_1 : in std_logic := '1';

player_1_up_bt, player_1_down_bt : in std_logic := '1';

sel_ai_2 : in std_logic := '1';

player_2_up_bt, player_2_down_bt : in std_logic := '1';

video_data : out std_logic_vector(7 downto 0);

video_sel  : out std_logic_vector(7 downto 0);

score_sel  : out std_logic;

led_5, led_4, led_2 : out std_logic

);

end entity;

architecture a_PongFPGA of PongFPGA is

signal rst, end_state : std_logic;

signal en_sel : std_logic_vector(1 downto 0);

signal master_en, en_1k5Hz, en_24Hz, en_12Hz, en_6Hz, en_3Hz : std_logic;

signal deboucing_en, racket_en : std_logic;

signal rnd_4 : std_logic_vector(3 downto 0);

signal ball_en, ball_clear, ball_tx : std_logic;

signal pbx, pby : std_logic_vector(7 downto 0);

signal inc_S1, inc_S2 : std_logic;

signal score_1, score_2 : std_logic_vector(2 downto 0);

signal ai_1_up, ai_1_down, player_1_up, player_1_down, racket_1_l, racket_1_r : std_logic;

signal racket_1 : std_logic_vector(7 downto 0);

signal ai_2_up, ai_2_down, player_2_up, player_2_down, racket_2_l, racket_2_r : std_logic;

signal racket_2 : std_logic_vector(7 downto 0);

signal v_data : std_logic_vector(7 downto 0);

signal v_sel : std_logic_vector(7 downto 0);

signal s_sel : std_logic;

begin

rst <= not (n_rst and n_pwr_rst);

master_en <= not end_state;

deboucing_en <= en_24Hz;

racket_en <= en_12Hz and master_en;

ball_en <= en_3Hz    when en_sel="00" else

en_6Hz    when en_sel="01" else

en_6Hz    when en_sel="10" else

en_12Hz   when en_sel="11" else '0';

-- Enable Source

ClkEnSrc0: ClkEnSrc port map(

clk_50M   => clk_50M,

rst       => rst,

en        => '1',

en_1k5Hz  => en_1k5Hz,

en_24Hz   => en_24Hz,

en_12Hz   => en_12Hz,

en_6Hz    => en_6Hz,

en_3Hz    => en_3Hz

);

-- RNG

RNG0: RNG port map(

clk    => clk_50M,

rst    => rst,

rnd    => rnd_4

);

-- Ball

BALL0 : Ball port map(

en         => ball_en and master_en,

clk        => clk_50M,

rst        => rst,

ball_clear => ball_clear,

tx         => ball_tx,

rnd_4      => rnd_4,

pbx        => pbx,

pby        => pby

);

-- Player 1

P1UD : Debouncing port map(

inpt       => not player_1_up_bt,

rst        => rst,

clk        => clk_50M,

en         => deboucing_en,

outp       => player_1_up

);

P1DD : Debouncing port map(

inpt       => not player_1_down_bt,

rst        => rst,

clk        => clk_50M,

en         => deboucing_en,

outp       => player_1_down

);

AI1 : AI port map(

clk    => clk_50M,

en     => deboucing_en,

rst    => rst,

n_r_l  => '1',

pbx    => pbx,

pby    => pby,

racket => racket_1,

L      => ai_1_up,

R      => ai_1_down

);

RACKET1: Racket port map(

clk    => clk_50M,

rst    => rst,

en     => racket_en,

l      => racket_1_l,

r      => racket_1_r,

q      => racket_1

);

-- Player 2

P2UD : Debouncing port map(

inpt       => not player_2_up_bt,

rst        => rst,

clk        => clk_50M,

en         => deboucing_en,

outp       => player_2_up

);

P2DD : Debouncing port map(

inpt       => not player_2_down_bt,

rst        => rst,

clk        => clk_50M,

en         => deboucing_en,

outp       => player_2_down

);

AI2 : AI port map(

clk    => clk_50M,

en     => deboucing_en,

rst    => rst,

n_r_l  => '0',

pbx    => pbx,

pby    => pby,

racket => racket_2,

L      => ai_2_up,

R      => ai_2_down

);

RACKET2: Racket port map(

clk    => clk_50M,

rst    => rst,

en     => racket_en,

l      => racket_2_l,

r      => racket_2_r,

q      => racket_2

);

-- Score

SREG0: ScoreRegister port map(

rst       => rst,

clk       => clk_50M,

en        => ball_en,

inc_S1    => inc_S1,

inc_S2    => inc_S2,

q1        => score_1,

q2        => score_2

);

-- Control

CTRL0: Control port map (

clk            => clk_50M,

en             => en_1k5Hz,

rst            => rst,

en_sel         => en_sel,

end_state      => end_state,

racket_1       => racket_1,

racket_2       => racket_2,

pbx            => pbx,

pby            => pby,

ball_tx        => ball_tx,

ball_clear     => ball_clear,

score1         => score_1,

score2         => score_2,

inc1           => inc_S1,

inc2           => inc_S2

);

-- en_sel

-- GPU

GPU0: GPU port map(

clk       => clk_50M,

en        => en_1k5Hz,

rst       => rst,

racket_1  => racket_1,

racket_2  => racket_2,

ball_x    => pbx,

ball_y    => pby,

end_state => end_state,

p1score   => score_1,

p2score   => score_2,

cd        => v_data,

cs        => v_sel,

ls        => s_sel

);

-- Racket 1 Player IA MUX

racket_1_l <=   player_1_up   when sel_ai_1='0' else

ai_1_up;

racket_1_r <=   player_1_down when sel_ai_1='0' else

ai_1_down;

-- Racket 2 Player IA MUX

racket_2_l <=   player_2_up   when sel_ai_2='0' else

ai_2_up;

racket_2_r <=   player_2_down when sel_ai_2='0' else

ai_2_down;

-- LEDs

led_5 <= not sel_ai_1;

led_4 <= master_en;

led_2 <= not sel_ai_2;

-- Tristate Output

video_data(7) <= 'Z' when v_data(7)='1' else '0';

video_data(6) <= 'Z' when v_data(6)='1' else '0';

video_data(5) <= 'Z' when v_data(5)='1' else '0';

video_data(4) <= 'Z' when v_data(4)='1' else '0';

video_data(3) <= 'Z' when v_data(3)='1' else '0';

video_data(2) <= 'Z' when v_data(2)='1' else '0';

video_data(1) <= 'Z' when v_data(1)='1' else '0';

video_data(0) <= 'Z' when v_data(0)='1' else '0';

video_sel(7)  <= '1' when v_sel(7)='1'  else 'Z';

video_sel(6)  <= '1' when v_sel(6)='1'  else 'Z';

video_sel(5)  <= '1' when v_sel(5)='1'  else 'Z';

video_sel(4)  <= '1' when v_sel(4)='1'  else 'Z';

video_sel(3)  <= '1' when v_sel(3)='1'  else 'Z';

video_sel(2)  <= '1' when v_sel(2)='1'  else 'Z';

video_sel(1)  <= '1' when v_sel(1)='1'  else 'Z';

video_sel(0)  <= '1' when v_sel(0)='1'  else 'Z';

score_sel     <= '1' when s_sel='1'     else 'Z';

end architecture;

Além da interface do circuito (pinos de entrada e saída) e da instanciação e conexão dos componentes criados anteriormente, na entidade top-level criamos os multiplexadores ilustrados no diagrama em blocos, usados para alternar entre diferentes sinais produzidos ou necessitados pelos módulos.

Síntese, Mapeamento de Pinos, Compilação e Gravação

Com todos os módulos devidamente descritos em VHDL, devemos sintetizar o circuito, processo no qual o Quartus II converte os códigos VHDL para um circuito digital. Para isso, pressione o botão de Síntese no painel de ferramentas.

Construa um jogo PONG com FPGA

O processo pode levar alguns segundos e, ao término, uma mensagem indicará se a síntese foi bem sucedida. No caso de erros, verifique a janela na parte inferior do Quartus, onde os erros são mostrados em texto vermelho. O Quartus exibe apenas um erro por vez, portanto corrija o erro mostrado e repita o processo de síntese até que a mensagem de sucesso apareça.

O próximo passo é atribuir cada uma das IOs da entidade Top-Level para pinos físicos na FPGA. Pressione o botão Pin Planner no painel de ferramentas e atribua cada uma das IOs para o respectivo pino na FPGA.

Construa um jogo PONG com FPGA

Construa um jogo PONG com FPGA

Não se esqueça de ativar os resistores de Pull-Up nos pinos de entrada. A tabela abaixo mostra a relação de cada IO, pino e resistor de Pull-Up:

ToDirectionLocationWeak Pull-Up Resistor
clk_50MInputPIN_17
led_2OutputPIN_3
led_4OutputPIN_7
led_5OutputPIN_9
n_pwr_rstInputPIN_73On
n_rstInputPIN_144On
player_1_down_btInputPIN_32On
player_1_up_btInputPIN_30On
player_2_down_btInputPIN_103On
player_2_up_btInputPIN_101On
score_selOutputPIN_96
sel_ai_1InputPIN_25On
sel_ai_2InputPIN_99On
video_data[7]OutputPIN_142
video_data[6]OutputPIN_139
video_data[5]OutputPIN_136
video_data[4]OutputPIN_134
video_data[3]OutputPIN_132
video_data[2]OutputPIN_126
video_data[1]OutputPIN_122
video_data[0]OutputPIN_120
video_sel[7]OutputPIN_143
video_sel[6]OutputPIN_141
video_sel[5]OutputPIN_137
video_sel[4]OutputPIN_135
video_sel[3]OutputPIN_133
video_sel[2]OutputPIN_129
video_sel[1]OutputPIN_125
video_sel[0]OutputPIN_121

Com os pinos atribuídos, passamos para a compilação. Pressione o botão de compilação na barra de ferramentas e aguarde o fim do processo. Pode levar alguns minutos, dependendo do seu computador. Uma mensagem de sucesso deve aparecer ao término.

Construa um jogo PONG com FPGA

Por fim, passamos para o último passo: a gravação do circuito na FPGA. Utilizaremos a interface JTAG para uma gravação temporária. Para mais informações sobre o processo de gravação na Cyclone II, veja o post “Conheça a FPGA Altera Cyclone II”, onde descrevo tanto a gravação temporária, pela interface JTAG, quanto a permanente, pela interface AS.

Construa um jogo PONG com FPGA

Verifique as informações na janela e pressione o botão Start para iniciar a gravação. Ao término, o circuito deve começar a funcionar automaticamente.

Construa um jogo PONG com FPGA

Projeto em funcionamento

Construa um jogo PONG com FPGA

Vimos que é possível construir um jogo pong inteiro utilizando apenas componentes digitais, sem escrever código, bem como a flexibilidade das FPGAs. Embora o projeto de hardwares específicos possa ser mais complexo do que a escrita de códigos para hardwares genéricos (processadores e microcontroladores), de forma geral, circuitos construídos especificamente para um único propósito tendem a ser mais eficientes do que circuitos genéricos. Além disso, em FPGAs não ficamos presos ao hardware pré-existente no microcontrolador, podendo sempre criar mais timers, contadores, GPIOs e outros periféricos sob medida em uma mesma FPGA, ao invés de ficar procurando um novo microcontrolador que atenda a nossos requisitos.

Quanto ao jogo, ainda há alguns pontos a se melhorar, como a inteligência artificial, por exemplo. Mesmo com a limitação de enxergar apenas meio campo é quase impossível vencer a IA atual, algumas outras limitações como um atraso de resposta ou limitar sua frequência de operação devem ser adicionadas a fim de tornar o jogo mais justo para humanos.

Gostou de construir um jogo pong com FPGA? Não esqueça de deixar seu comentário logo abaixo.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *