1. Introdução
Você se lembra do jogo de cobras desde a infância, onde uma cobra corre na tela tentando comer uma maçã? Este artigo descreve nossa implementação do jogo em um FPGA 1 .

Figura 1. Jogabilidade
Primeiro, vamos nos apresentar e explicar a razão pela qual trabalhamos no projeto. Somos três: Tymur Lysenko , Daniil Manakovskiy e Sergey Makarov . Como alunos do primeiro ano da Universidade de Innopolis , tivemos um curso de "Arquitetura de Computadores", que é ministrado profissionalmente e permite que o aluno compreenda a estrutura de baixo nível de um computador. Em algum momento do curso, os instrutores nos deram a oportunidade de desenvolver um projeto para um FPGA para obter pontos adicionais no curso. Nossa motivação não é apenas a nota, mas nosso interesse em ganhar mais experiência no design de hardware, compartilhar os resultados e, finalmente, ter um jogo agradável.
Agora, vamos entrar em detalhes profundos e sombrios.
Visão geral do projeto
Para o nosso projeto, selecionamos um jogo fácil de implementar e divertido, o "Snake". A estrutura da implementação é a seguinte: primeiro, uma entrada é obtida de um joystick SPI, depois processada e, finalmente, uma imagem é enviada para um monitor VGA e uma pontuação é exibida em um display de 7 segmentos (em hexadecimal). Embora a lógica do jogo seja intuitiva e direta, o VGA e o joystick foram desafios interessantes e sua implementação levou a uma boa experiência de jogo.
O jogo tem as seguintes regras. Um jogador começa com a cabeça de uma única cobra. O objetivo é comer maçãs, que são geradas aleatoriamente na tela depois que a anterior foi comida. Além disso, a cobra está sendo estendida por 1 cauda após satisfazer a fome. As caudas se movem uma após a outra, seguindo a cabeça. A cobra está sempre em movimento. Se as bordas da tela forem atingidas, a cobra está sendo transferida para outro lado da tela. Se a cabeça bate no rabo, o jogo acaba.
- Altera Cyclone IV (EP4CE6E22C8N) com 6272 elementos lógicos, relógio interno de 50 MHz, VGA colorido de 3 bits, display de 8 dígitos e 7 segmentos. O FPGA não pode levar uma entrada analógica para seus pinos.
- Joystick SPI (KY-023)
- Um monitor VGA que suporta taxa de atualização de 60 Hz
- Quartus Prime Lite Edition 18.0.0 Build 614
- Verilog HDL IEEE 1364-2001
- Breadboard
- Elementos elétricos:
- 8 conectores macho-fêmea
- 1 conector fêmea-fêmea
- 1 conector macho-macho
- 4 resistores (4,7 KΩ)
Visão geral da arquitetura
A arquitetura do projeto é um fator significativo a considerar. A Figura 2 mostra essa arquitetura do ponto de vista de nível superior:

Figura 2. Visualização de nível superior do design ( pdf )
Como você pode ver, existem muitas entradas, saídas e alguns módulos. Esta seção descreve o que cada elemento significa e especifica quais pinos são usados na placa para as portas.
Entradas principais
As principais entradas necessárias para a implementação são res_x_one , res_x_two , res_y_one , res_y_two , que são usadas para receber uma direção atual de um joystick. A Figura 3 mostra o mapeamento entre seus valores e as direções.
Entrada | Esquerda | Direito | Para cima | Para baixo | Nenhuma mudança de direção |
---|
res_x_one (PIN_30) | 1 | 0 0 | x | x | 1 |
res_x_two (PIN_52) | 1 | 0 0 | x | x | 0 0 |
res_y_one (PIN_39) | x | x | 1 | 0 0 | 1 |
res_y_two (PIN_44) | x | x | 1 | 0 0 | 0 0 |
Figura 3. Mapeamento de entradas e direções do joystick
- clk - o relógio do quadro (PIN_23)
- reset - sinal para reiniciar o jogo e parar de imprimir (PIN_58)
- cor - quando 1, todas as cores possíveis são exibidas na tela e usadas apenas para fins de demonstração (PIN_68)
Módulos principais
O joystick_input é usado para produzir um código de direção com base em uma entrada do joystick.
game_logic
game_logic contém toda a lógica necessária para jogar um jogo. O módulo move uma cobra em uma determinada direção. Além disso, é responsável por comer maçã e detectar colisões. Além disso, ele recebe as coordenadas xey atuais de um pixel na tela e retorna uma entidade colocada na posição.
VGA_Draw
A gaveta define a cor de um pixel para um valor específico com base na posição atual ( iVGA_X, iVGA_Y ) e na entidade atual ( ent ).
VGA_Ctrl
Gera um fluxo de bits de controle para a saída VGA ( V_Sync, H_Sync, R, G, B ).
SSEG_Display 2
SSEG_Display é um driver para gerar a pontuação atual na tela de 7 segmentos.
Vga_clk
O VGA_clk recebe um relógio de 50 MHz e reduz para 25,175 MHz.
game_upd_clk
game_upd_clk é um módulo que gera um relógio especial que aciona uma atualização do estado do jogo.
Saídas
- VGA_B - Pino azul VGA (PIN_144)
- VGA_G - Pino verde VGA (PIN_1)
- VGA_R - Pino vermelho VGA (PIN_2)
- VGA_HS - sincronização horizontal VGA (PIN_142)
- VGA_VS - sincronização vertical VGA (PIN_143)
- sseg_a_to_dp - especifica qual dos 8 segmentos deve ser iluminado (PIN_115, PIN_119, PIN_120, PIN_121, PIN_124, PIN_125, PIN_126, PIN_127)
- sseg_an - especifica qual dos 4 displays de 7 segmentos deve ser usado (PIN_128, PIN_129, PIN_132, PIN_133)
Implementação

Figura 4. Joystick SPI (KY-023)
Ao implementar um módulo de entrada, descobrimos que o stick produz um sinal analógico. O joystick possui 3 posições para cada eixo:
- top - saída de ~ 5V
- mid - saída de ~ 2.5V
- baixa - saída de ~ 0V
A entrada é muito semelhante ao sistema ternário: para o eixo X, temos um estado true
(esquerdo), false
(direito) e undetermined
, onde o joystick não está à esquerda nem à direita. O problema é que a placa FPGA pode processar apenas uma entrada digital. Portanto, não podemos converter essa lógica ternária em binária apenas escrevendo algum código. A primeira solução sugerida foi encontrar um conversor Analógico-Digital, mas decidimos usar nosso conhecimento escolar de física e implementar o divisor de tensão 3 . Para definir os três estados, precisaremos de dois bits: 00 é false
, 01 é undefined
e 11 é true
. Após algumas medições, descobrimos que em nosso quadro, a fronteira entre zero e um é de aproximadamente 1,7V. Assim, construímos o seguinte esquema (imagem criada usando o circuitlab 4 ):

Figura 5. Circuito para ADC para joystick
A implementação física é criada usando os itens do kit Arduino e tem a seguinte aparência:

Figura 6. Implementação do ADC
Nosso circuito recebe uma entrada para cada eixo e produz duas saídas: a primeira vem diretamente do manípulo e se torna zero somente se o joystick gerar zero
. O segundo é 0 no estado undetermined
, mas ainda 1 no true
. Este é o resultado exato que esperávamos.
A lógica do módulo de entrada é:
- Traduzimos nossa lógica ternária em fios binários simples para cada direção;
- A cada ciclo do relógio, verificamos se apenas uma direção é
true
(a cobra não pode ir na diagonal); - Comparamos nossa nova direção com a anterior para impedir que a cobra se coma, não permitindo que o jogador mude a direção na direção oposta.
Parte do código do módulo de entrada reg left, right, up, down; initial begin direction = `TOP_DIR; end always @(posedge clk) begin //1 left = two_resistors_x; right = ~one_resistor_x; up = two_resistors_y; down = ~one_resistor_y; if (left + right + up + down == 3'b001) //2 begin if (left && (direction != `RIGHT_DIR)) //3 begin direction = `LEFT_DIR; end //same code for other directions end end
Saída para VGA
Decidimos fazer uma saída com resolução 640x480 em uma tela de 60Hz rodando a 60 FPS.
O módulo VGA consiste em 2 partes principais: um driver e uma gaveta . O driver gera um fluxo de bits que consiste em sinais de sincronização vertical e horizontal e uma cor que é dada às saídas VGA. Um artigo 5 escrito por @SlavikMIPT descreve os princípios básicos do trabalho com VGA. Nós adaptamos o driver do artigo ao nosso quadro.
Decidimos dividir a tela em uma grade de elementos de 40x30, composta por quadrados de 16x16 pixels. Cada elemento representa 1 entidade do jogo: uma maçã, uma cabeça de cobra, um rabo ou nada.
O próximo passo em nossa implementação foi criar sprites para as entidades.
O ciclone IV possui apenas 3 bits para representar uma cor no VGA (1 para vermelho, 1 para verde e 1 para azul). Devido a essa limitação, precisávamos implementar um conversor para ajustar as cores das imagens às disponíveis. Para isso, criamos um script python que divide um valor RGB de cada pixel por 128.
O script python from PIL import Image, ImageDraw filename = "snake_head" index = 1 im = Image.open(filename + ".png") n = Image.new('RGB', (16, 16)) d = ImageDraw.Draw(n) pix = im.load() size = im.size data = [] code = "sp[" + str(index) + "][{i}][{j}] = 3'b{RGB};\\\n" with open("code_" + filename + ".txt", 'w') as f: for i in range(size[0]): tmp = [] for j in range(size[1]): clr = im.getpixel((i, j)) vg = "{0}{1}{2}".format(int(clr[0] / 128),
Original | Após o script |

| 
|
Figura 7. Comparação entre entrada e saída
O principal objetivo da gaveta é enviar uma cor de pixel ao VGA com base na posição atual ( iVGA_X, iVGA_Y ) e na entidade atual ( ent ). Todos os sprites são codificados, mas podem ser facilmente alterados gerando um novo código usando o script acima.
Lógica da gaveta always @(posedge iVGA_CLK or posedge reset) begin if(reset) begin oRed <= 0; oGreen <= 0; oBlue <= 0; end else begin // DRAW CURRENT STATE if (ent == `ENT_NOTHING) begin oRed <= 1; oGreen <= 1; oBlue <= 1; end else begin // Drawing a particular pixel from sprite oRed <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][0]; oGreen <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][1]; oBlue <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][2]; end end end
Saída para a tela de 7 segmentos
Com o objetivo de permitir que o jogador veja sua pontuação, decidimos produzir uma pontuação do jogo na tela de 7 segmentos. Devido à falta de tempo, usamos o código da documentação do EP4CE6 Starter Board 2 . Este módulo gera um número hexadecimal no display.
Lógica do jogo
Durante o desenvolvimento, tentamos várias abordagens, no entanto, acabamos com a que requer uma quantidade mínima de memória, é fácil de implementar em hardware e pode se beneficiar de cálculos paralelos.
O módulo executa várias funções. Como o VGA desenha um pixel em cada ciclo do relógio, começando do canto superior esquerdo, movendo-se para o canto inferior direito, o módulo VGA_Draw, responsável pela produção de uma cor para um pixel, precisa identificar qual cor usar nas coordenadas atuais. É isso que o módulo de lógica do jogo deve produzir - um código de entidade para as coordenadas fornecidas.
Além disso, ele precisa atualizar o estado do jogo somente depois que a tela cheia foi desenhada. Um sinal produzido pelo módulo game_upd_clk é usado para determinar quando atualizar.
Estado do jogo
O estado do jogo consiste em:
- Coordenadas da cabeça da cobra
- Uma matriz de coordenadas da cauda da cobra. A matriz é limitada por 128 elementos em nossa implementação
- Número de caudas
- Coordenadas de uma maçã
- Bandeira do fim do jogo
- Bandeira do jogo ganho
A atualização do estado do jogo inclui vários estágios:
- Mova a cabeça da cobra para novas coordenadas, com base em uma determinada direção. Se uma coordenada estiver na sua extremidade e precisar ser alterada ainda mais, a cabeça terá que pular para outra extremidade da tela. Por exemplo, uma direção é definida para a esquerda e a coordenada X atual é 0. Portanto, a nova coordenada X deve se tornar igual ao último endereço horizontal.
- Novas coordenadas da cabeça da cobra são testadas contra as coordenadas da maçã:
2.1 Caso sejam iguais e a matriz não esteja cheia, adicione uma nova cauda à matriz e aumente o contador da cauda. Quando o contador atinge seu valor mais alto (128 no nosso caso), a bandeira do jogo ganho está sendo configurada e isso significa que a cobra não pode mais crescer e o jogo ainda continua. A nova cauda é colocada nas coordenadas anteriores da cabeça da cobra. Coordenadas aleatórias para X e Y devem ser tomadas para colocar uma maçã lá.
2.2 Caso não sejam iguais, troque sequencialmente as coordenadas das caudas adjacentes. (n + 1) -ésima cauda deve receber coordenadas de n-ésima, caso a n-ésima cauda tenha sido adicionada antes (n + 1) -ésima. A primeira cauda recebe coordenadas antigas da cabeça. - Verifique se as novas coordenadas da cabeça da cobra coincidem com as coordenadas de qualquer cauda. Se for esse o caso, a bandeira do fim do jogo é levantada e o jogo para.
Geração aleatória de coordenadas
Números aleatórios produzidos tomando bits aleatórios gerados pelos registros de deslocamento de deslocamento linear de realimentação de 6 bits (LFSR) 6 . Para ajustar os números em uma tela, eles estão sendo divididos pelas dimensões da grade do jogo e o restante é obtido.
Conclusão
Após 8 semanas de trabalho, o projeto foi implementado com sucesso. Tivemos alguma experiência em desenvolvimento de jogos e acabamos com uma versão agradável de um jogo "Snake" para um FPGA. O jogo é jogável e nossas habilidades em programação, design de arquitetura e soft-skills melhoraram.
Segmentos reconhecidos
Gostaríamos de expressar nossos agradecimentos e gratidão especial aos professores Muhammad Fahim e Alexander Tormasov por nos fornecerem o conhecimento profundo e a oportunidade de colocá-lo em prática. Agradecemos sinceramente a Vladislav Ostankovich por nos fornecer o hardware essencial usado no projeto e a Temur Kholmatov por ajudar na depuração. Não esqueceríamos de lembrar Anastassiya Boiko desenhando sprites bonitos para o jogo. Além disso, gostaríamos de estender nossas sinceras estima a Rabab Marouf pela revisão e edição deste artigo.
Obrigado por todos aqueles que nos ajudaram a testar o jogo e tentaram estabelecer um recorde. Espero que você goste de jogar!
Referências
[1]: Projeto no Github
[2]: [FPGA] Documentação da placa de iniciação EP4CE6
[3]: Divisor de tensão
[4]: Ferramenta para modelagem de circuitos
[5]: adaptador VGA para FPGA Altera Cyclone III
[6]: Registro de deslocamento de feedback linear (LFSR) na Wikipedia
LFSR em um FPGA - VHDL e código Verilog
Uma textura de maçã
Ideia para gerar números aleatórios
Palnitkar, S. (2003). Verilog HDL: Um Guia para Design e Síntese Digital, Segunda Edição.