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 .

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.
- 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:

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 entrada | Izquierda | Derecho | Arriba | Abajo | Sin cambio de dirección |
---|
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. Mapeo de entradas de joystick y direcciones
- 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 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

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 ):

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:

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:
- Traducimos nuestra lógica ternaria a cables binarios simples para cada dirección;
- En cada ciclo de reloj, verificamos si solo una dirección es
true
(la serpiente no puede ir en diagonal); - 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),
Original | Despué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:
- 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.
- 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. - 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.