El juego Snake para FPGA Cyclone IV (con joystick VGA y SPI)

Introduccion


¿Recuerdas el juego de la serpiente de la infancia, donde una serpiente corre en la pantalla tratando de comer una manzana? Este artículo describe nuestra implementación del juego en un FPGA 1 .


Gameplay.gif
Figura 1. Jugabilidad


Primero, permítanos presentarnos y explicar la razón por la que hemos trabajado en el proyecto. Somos 3: Tymur Lysenko , Daniil Manakovskiy y Sergey Makarov . Como estudiantes de primer año de la Universidad de Innopolis , tuvimos un curso de "Arquitectura de Computadores", que se enseña profesionalmente y permite al alumno comprender la estructura de bajo nivel de una computadora. En algún momento durante el curso, los instructores nos brindaron la oportunidad de desarrollar un proyecto para un FPGA para puntos adicionales en el curso. Nuestra motivación no ha sido solo la calificación, sino nuestro interés por obtener más experiencia en el diseño de hardware, compartir los resultados y, finalmente, tener un juego agradable.


Ahora, entremos en detalles oscuros y profundos.


Resumen del proyecto


Para nuestro proyecto, seleccionamos un juego fácil de implementar y divertido, a saber, la "Serpiente". La estructura de la implementación es la siguiente: en primer lugar, se toma una entrada de un joystick SPI, luego se procesa y, finalmente, se envía una imagen a un monitor VGA y se muestra una puntuación en una pantalla de 7 segmentos (en hexadecimal). Aunque la lógica del juego es intuitiva y directa, VGA y el joystick han sido desafíos interesantes y su implementación ha llevado a una buena experiencia de juego.


El juego tiene las siguientes reglas. Un jugador comienza con una sola cabeza de serpiente. El objetivo es comer manzanas, que se generan aleatoriamente en la pantalla después de comer la anterior. Además, la serpiente se extiende 1 cola después de satisfacer el hambre. Las colas se mueven una tras otra, siguiendo la cabeza. La serpiente siempre se está moviendo. Si se alcanzan los bordes de la pantalla, la serpiente se está transfiriendo a otro lado de la pantalla. Si la cabeza golpea la cola, el juego termina.


Herramientas utilizadas


  • Altera Cyclone IV (EP4CE6E22C8N) con 6272 elementos lógicos, reloj integrado de 50 MHz, VGA a color de 3 bits, pantalla de 8 dígitos y 7 segmentos. El FPGA no puede llevar una entrada analógica a sus pines.
  • Joystick SPI (KY-023)
  • Un monitor VGA que admite una frecuencia de actualización de 60 Hz
  • Quartus Prime Lite Edition 18.0.0 Build 614
  • Verilog HDL IEEE 1364-2001
  • Tablero de pan
  • Elementos electricos:
    • 8 conectores macho-hembra
    • 1 conector hembra-hembra
    • 1 conector macho-macho
    • 4 resistencias (4.7 KΩ)

Resumen de arquitectura


La arquitectura del proyecto es un factor importante a tener en cuenta. La Figura 2 muestra esta arquitectura desde el punto de vista del nivel superior:


Design.png
Figura 2. Vista de nivel superior del diseño ( pdf )


Como puede ver, hay muchas entradas, salidas y algunos módulos. Esta sección describirá lo que significa cada elemento y especificará qué pines se utilizan en la placa para los puertos.


Entradas principales


Las entradas principales necesarias para la implementación son res_x_one , res_x_two , res_y_one , res_y_two , que se utilizan para recibir la dirección actual de un joystick. La Figura 3 muestra el mapeo entre sus valores y las direcciones.


De entradaIzquierdaDerechoArribaAbajoSin cambio de dirección
res_x_one (PIN_30)10 0xx1
res_x_two (PIN_52)10 0xx0 0
res_y_one (PIN_39)xx10 01
res_y_two (PIN_44)xx10 00 0

Figura 3. Mapeo de entradas de joystick y direcciones


Otras entradas


  • clk - el reloj del tablero (PIN_23)
  • restablecer : señal para restablecer el juego y detener la impresión (PIN_58)
  • color : cuando 1, todos los colores posibles se muestran en la pantalla y se usan solo con fines de demostración (PIN_68)

Módulos principales


joystick_input


joystick_input se usa para producir un código de dirección basado en una entrada del joystick.


game_logic


game_logic contiene toda la lógica necesaria para jugar un juego. El módulo mueve una serpiente en una dirección dada. Además, es responsable de comer manzanas y detectar colisiones. Además, recibe las coordenadas xey actuales de un píxel en la pantalla y devuelve una entidad colocada en la posición.


VGA_Draw


El cajón establece un color de un píxel a un valor particular basado en la posición actual ( iVGA_X, iVGA_Y ) y la entidad actual ( ent ).


VGA_Ctrl


Genera un flujo de bits de control para salida VGA ( V_Sync, H_Sync, R, G, B ).


SSEG_Display 2


SSEG_Display es un controlador para generar la puntuación actual en la pantalla de 7 segmentos.


Vga_clk


VGA_clk recibe un reloj de 50MHz y lo reduce a 25.175 MHz.


game_upd_clk


game_upd_clk es un módulo que genera un reloj especial que activa una actualización del estado del juego.


Salidas


  • VGA_B - Pin azul VGA (PIN_144)
  • VGA_G - Pin verde VGA (PIN_1)
  • VGA_R - Pin rojo VGA (PIN_2)
  • VGA_HS - Sincronización horizontal VGA (PIN_142)
  • VGA_VS - Sincronización vertical VGA (PIN_143)
  • sseg_a_to_dp : especifica cuál de los 8 segmentos se encenderá (PIN_115, PIN_119, PIN_120, PIN_121, PIN_124, PIN_125, PIN_126, PIN_127)
  • sseg_an : especifica cuál de las 4 pantallas de 7 segmentos se utilizará (PIN_128, PIN_129, PIN_132, PIN_133)

Implementación


Entrada con joystick SPI


stick.jpg


Figura 4. Joystick SPI (KY-023)


Mientras implementamos un módulo de entrada, descubrimos que el dispositivo produce una señal analógica. El joystick tiene 3 posiciones para cada eje:


  • arriba - ~ 5V de salida
  • salida media - ~ 2.5V
  • bajo - ~ 0V de salida

La entrada es muy similar al sistema ternario: para el eje X, tenemos true (izquierda), false (derecha) y un estado undetermined , donde el joystick no está ni a la izquierda ni a la derecha. El problema es que la placa FPGA solo puede procesar una entrada digital. Por lo tanto, no podemos convertir esta lógica ternaria a binaria simplemente escribiendo algún código. La primera solución sugerida fue encontrar un convertidor analógico-digital, pero luego decidimos usar nuestro conocimiento escolar de física e implementar el divisor de voltaje 3 . Para definir los tres estados, necesitaremos dos bits: 00 es false , 01 undefined está undefined y 11 es true . Después de algunas mediciones, descubrimos que en nuestro tablero, el límite entre cero y uno es de aproximadamente 1.7V. Por lo tanto, creamos el siguiente esquema (imagen creada usando circuitlab 4 ):


Stick_connection.png


Figura 5. Circuito para ADC para joystick


La implementación física se construye utilizando los elementos del kit Arduino y tiene el siguiente aspecto:


stick_imp


Figura 6. Implementación de ADC


Nuestro circuito toma una entrada para cada eje y produce dos salidas: la primera proviene directamente del stick y se convierte en cero solo si el joystick sale a zero . El segundo es 0 en estado undetermined , pero aún 1 en true . Este es el resultado exacto que esperábamos.


La lógica del módulo de entrada es:


  1. Traducimos nuestra lógica ternaria a cables binarios simples para cada dirección;
  2. En cada ciclo de reloj, verificamos si solo una dirección es true (la serpiente no puede ir en diagonal);
  3. Comparamos nuestra nueva dirección con la anterior para evitar que la serpiente se coma al no permitir que el jugador cambie la dirección a la opuesta.

Una parte del código del 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 

Salida a VGA


Decidimos hacer una salida con una resolución de 640x480 en una pantalla de 60Hz que funciona a 60 FPS.


El módulo VGA consta de 2 partes principales: un controlador y un cajón . El controlador genera un flujo de bits que consta de señales de sincronización verticales y horizontales y un color que se otorga a las salidas VGA. Un artículo 5 escrito por @SlavikMIPT describe los principios básicos de trabajar con VGA. Hemos adaptado el controlador del artículo a nuestra placa.


Decidimos dividir la pantalla en una cuadrícula de elementos de 40x30, que consta de cuadrados de 16x16 píxeles. Cada elemento representa 1 entidad del juego: una manzana, la cabeza de una serpiente, una cola o nada.


El siguiente paso en nuestra implementación fue crear sprites para las entidades.


El ciclón IV tiene solo 3 bits para representar un color en VGA (1 para rojo, 1 para verde y 1 para azul). Debido a dicha limitación, necesitábamos implementar un convertidor para adaptar los colores de las imágenes a las disponibles. Para ese propósito, creamos un script de Python que divide un valor RGB de cada píxel por 128.


El script de 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), # an array representation for pixel int(clr[1] / 128), # since clr[*] in range [0, 255], int(clr[2] / 128)) # clr[*]/128 is either 0 or 1 tmp.append(vg) f.write(code.format(i=i, j=j, RGB=vg)) # Verilog code to initialization d.point((i, j), tuple([int(vg[0]) * 255, int(vg[1]) * 255, int(vg[2]) * 255])) # Visualize final image data.append(tmp) n.save(filename + "_3bit.png") for el in data: print(" ".join(el)) 

OriginalDespués del guión



Figura 7. Comparación entre entrada y salida


El objetivo principal del cajón es enviar un color de un píxel a VGA en función de la posición actual ( iVGA_X, iVGA_Y ) y la entidad actual ( ent ). Todos los sprites están codificados pero pueden cambiarse fácilmente generando un nuevo código usando el script anterior.


Lógica del cajón
 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 

Salida a la pantalla de 7 segmentos


Con el fin de permitir que el jugador vea su puntaje, decidimos enviar un puntaje de juego a la pantalla de 7 segmentos. Debido a la escasez de tiempo, utilizamos el código de la Documentación de la placa de inicio EP4CE6 2 . Este módulo emite un número hexadecimal a la pantalla.


Lógica del juego


Durante el desarrollo, probamos varios enfoques, sin embargo, terminamos con el que requiere una cantidad mínima de memoria, es fácil de implementar en hardware y puede beneficiarse de los cálculos paralelos.


El módulo realiza varias funciones. A medida que VGA dibuja un píxel en cada ciclo de reloj, comenzando desde el superior izquierdo hacia el inferior derecho, el módulo VGA_Draw, que es responsable de producir un color para un píxel, necesita identificar qué color usar para las coordenadas actuales. Eso es lo que debería generar el módulo lógico del juego: un código de entidad para las coordenadas dadas.
Además, tiene que actualizar el estado del juego solo después de que se dibuja la pantalla completa. Una señal producida por el módulo game_upd_clk se usa para determinar cuándo actualizar.


Estado del juego


El estado del juego consiste en:


  • Coordenadas de la cabeza de la serpiente.
  • Un conjunto de coordenadas de la cola de la serpiente. La matriz está limitada por 128 elementos en nuestra implementación
  • Número de colas
  • Coordenadas de una manzana
  • Juego sobre bandera
  • Juego ganó bandera

La actualización del estado del juego incluye varias etapas:


  1. Mueva la cabeza de la serpiente a nuevas coordenadas, en función de una dirección determinada. Si resulta que una coordenada está en su borde y necesita ser cambiada aún más, entonces la cabeza tiene que saltar a otro borde de la pantalla. Por ejemplo, una dirección se establece a la izquierda y la coordenada X actual es 0. Por lo tanto, la nueva coordenada X debería ser igual a la última dirección horizontal.
  2. Las nuevas coordenadas de la cabeza de la serpiente se prueban contra las coordenadas de la manzana:
    2.1. En caso de que sean iguales y la matriz no esté llena, agregue una nueva cola a la matriz e incremente el contador de cola. Cuando el contador alcanza su valor más alto (128 en nuestro caso), se está configurando la bandera de juego ganado y eso significa que esa serpiente ya no puede crecer, y el juego aún continúa. La nueva cola se coloca en las coordenadas anteriores de la cabeza de la serpiente. Deben tomarse coordenadas aleatorias para X e Y para colocar una manzana allí.
    2.2. En caso de que no sean iguales, cambie secuencialmente las coordenadas de las colas adyacentes. (n + 1) -th tail debe recibir coordenadas de n-th, en caso de que la enésima cola se haya agregado antes de (n + 1) -th. La primera cola recibe viejas coordenadas de la cabeza.
  3. Comprueba si las nuevas coordenadas de la cabeza de la serpiente coinciden con las coordenadas de cualquier cola. Si ese es el caso, el juego sobre la bandera se levanta y el juego se detiene.

Generación de coordenadas aleatorias


Números aleatorios producidos al tomar bits aleatorios generados por registros de desplazamiento de cambio de retroalimentación lineal de 6 bits (LFSR) 6 . Para ajustar los números en una pantalla, se dividen por las dimensiones de la cuadrícula del juego y se toma el resto.


Conclusión


Después de 8 semanas de trabajo, el proyecto se implementó con éxito. Hemos tenido algo de experiencia en el desarrollo de juegos y terminamos con una versión agradable del juego "Snake" para un FPGA. El juego es jugable y nuestras habilidades en programación, diseño de arquitectura y habilidades blandas han mejorado.


Segmentos reconocidos


Nos gustaría expresar nuestro especial agradecimiento y gratitud a nuestros profesores Muhammad Fahim y Alexander Tormasov por darnos el profundo conocimiento y la oportunidad de ponerlo en práctica. Agradecemos sinceramente a Vladislav Ostankovich por proporcionarnos el hardware esencial utilizado en el proyecto y a Temur Kholmatov por ayudarnos con la depuración. No nos olvidaríamos de recordar a Anastassiya Boiko dibujando hermosos sprites para el juego. Además, nos gustaría extender nuestras sinceras estimaciones a Rabab Marouf por la revisión y edición de este artículo.


Gracias a todos los que nos ayudaron a probar el juego y trataron de establecer un récord. ¡Espero que disfrutes jugando!


Referencias


[1]: Proyecto en el Github
[2]: [FPGA] Documentación de la placa de inicio EP4CE6
[3]: divisor de voltaje
[4]: herramienta para modelar circuitos
[5]: Adaptador VGA para FPGA Altera Cyclone III
[6]: Registro de desplazamiento de retroalimentación lineal (LFSR) en Wikipedia
LFSR en un FPGA - Código VHDL y Verilog
Una textura de manzana
Idea para generar números aleatorios
Palnitkar, S. (2003). Verilog HDL: una guía de diseño y síntesis digital, segunda edición.

Source: https://habr.com/ru/post/es431226/


All Articles