Creamos un juego de plataforma portátil en el microcontrolador Cortex M0 +


Introduccion


(Los enlaces al código fuente y al proyecto KiCAD se proporcionan al final del artículo).

Aunque nacimos en la era de los 8 bits, nuestra primera computadora fue la Amiga 500. Esta es una gran máquina de 16 bits con gráficos y sonido increíbles, lo que la hace ideal para jugar. Las plataformas se han convertido en un género de juego muy popular en esta computadora. Muchos de ellos eran muy coloridos y tenían un desplazamiento de paralaje muy suave. Esto fue posible gracias a programadores talentosos que utilizaron ingeniosamente los coprocesadores Amiga para aumentar la cantidad de colores de pantalla. ¡Eche un vistazo a LionHeart por ejemplo!


Lionheart en Amiga. Esta imagen estática no transmite la belleza de los gráficos.

Desde los años 90, la electrónica ha cambiado mucho, y ahora hay muchos microcontroladores pequeños que le permiten crear cosas increíbles.

Siempre nos han encantado los juegos de plataformas, y hoy, por unos pocos dólares, puedes comprar Raspberry Zero, instalar Linux y escribir un juego de plataformas "bastante fácil".

Pero esta tarea no es para nosotros, ¡no queremos disparar gorriones desde un cañón!

¡Queremos usar microcontroladores con memoria limitada y no un sistema potente en un chip con una GPU integrada! En otras palabras, ¡queremos dificultades!

Por cierto, sobre las posibilidades del video: algunas personas logran exprimir todos los jugos del microcontrolador AVR en sus proyectos (por ejemplo, en el proyecto Uzebox o Craft del desarrollador lft). Sin embargo, para lograr esto, los microcontroladores AVR nos obligan a escribir en ensamblador, y aunque algunos juegos son muy buenos, encontrará serias limitaciones que no le permiten crear un juego en un estilo de 16 bits.

Por lo tanto, decidimos usar un microcontrolador / placa más equilibrado, lo que nos permite escribir código completamente en C.

No es tan poderoso como Arduino Due, pero no tan débil como Arduino Uno. Curiosamente, "Debido" significa "dos" y "Uno" significa "uno". Microsoft nos enseñó a contar correctamente (1, 2, 3, 95, 98, ME, 2000, XP, Vista, 7, 8, 10), ¡y Arduino también fue de esta manera! Utilizaremos el Arduino Zero, que está en el medio entre 1 y 2.

Sí, según Arduino, 1 <0 <2.

En particular, no estamos interesados ​​en la placa en sí, sino en su serie de procesadores. El Arduino Zero tiene un microcontrolador de la serie ATSAMD21 con Cortex M0 + (48 MHz), memoria flash de 256 KB y 32 KB de RAM.

Aunque el Cortex M0 + de 48 MHz supera significativamente el rendimiento del antiguo MC68000 de 7 MHz, el Amiga 500 tenía 512 KB de RAM, sprites de hardware, un tablero de juego dual integrado, Blitter (un motor de transferencia de bloques de imagen basado en DMA con un sistema de reconocimiento de colisión con precisión de píxel incorporado) y transparencia) y Cobre (un coprocesador ráster que le permite realizar operaciones con registros basados ​​en la posición de barrido para crear muchos efectos muy bonitos). SAMD21 no tiene todo este hardware (con la excepción de uno bastante simple en comparación con Blitter DMA), por lo que mucho se representará mediante programación.

Queremos lograr los siguientes parámetros:

  • Resolución 160 x 128 píxeles en una pantalla SPI de 1.8 pulgadas.
  • Gráficos con 16 bits por píxel;
  • La velocidad de fotogramas más alta. Al menos 25 fps a 12 MHz SPI, o 40 fps a 24 MHz;
  • doble campo de juego con desplazamiento de paralaje;
  • todo está escrito en C. Sin código de ensamblador;
  • Reconocimiento de colisiones con precisión de píxeles;
  • superposición de pantalla.

Parece que lograr estos objetivos es bastante difícil. ¡Lo es, especialmente si rechazamos el código en asm!

Por ejemplo, con color de 16 bits, un tamaño de pantalla de 160 × 128 píxeles requerirá 40 KB para el búfer de pantalla, ¡pero solo tenemos 32 KB de RAM! ¡Y todavía necesitamos desplazamiento de paralaje en un campo de juego doble y mucho más, con una frecuencia de al menos 25/40 fps!

Pero nada es imposible para nosotros, ¿verdad?

¡Usamos trucos y funciones integradas de ATSAMD21! Como "hardware" tomamos uChip , que se puede comprar en la Tienda Itaca .


uChip: el corazón de nuestro proyecto!

Tiene las mismas características que el Arduino Zero, pero es mucho más pequeño y también más barato que el Arduino Zero original (sí, puedes comprar un Arduino Zero falso por $ 10 en AliExpress ... pero queremos construir sobre el original). Esto nos permitirá crear una pequeña consola portátil. Puede adaptar este proyecto para Arduino Zero casi sin esfuerzo, solo el resultado será bastante engorroso.

También creamos un pequeño tablero de prueba que implementa una consola portátil para los pobres. Detalles a continuación!


No usaremos el marco Arduino. No es adecuado para optimizar y gestionar equipos. (¡Y no hablemos del IDE!)

En este artículo, describiremos cómo llegamos a la versión final del juego, describiremos todas las optimizaciones y criterios utilizados. El juego en sí aún no está completo, carece de sonido, niveles, etc. ¡Sin embargo, se puede usar como punto de partida para muchos tipos diferentes de juegos!

Además, hay muchas más opciones de optimización, ¡incluso sin ensamblador!

Entonces, ¡comencemos nuestro viaje!

Dificultades


De hecho, el proyecto tiene dos aspectos complejos: tiempos y memoria (RAM y almacenamiento).

El recuerdo


Comencemos con la memoria. Primero, en lugar de almacenar una imagen de gran nivel, usamos mosaicos. De hecho, si analiza cuidadosamente la mayoría de los juegos de plataformas, notará que se crean a partir de una pequeña cantidad de elementos gráficos (mosaicos) que se repiten muchas veces.


Turrican 2 en Amiga. Uno de los mejores juegos de plataformas de todos los tiempos. ¡Puedes ver fácilmente las fichas en él!

El mundo / nivel parece diverso gracias a varias combinaciones de fichas. Esto ahorra mucha memoria en el disco, pero no resuelve el problema de un enorme buffer de cuadros.

¡El segundo truco que utilizamos es posible debido al gran poder computacional de uC y la presencia de DMA! En lugar de almacenar todos los datos del cuadro en la RAM (¿y por qué es necesario?) Crearemos una escena en cada cuadro desde cero. En particular, continuaremos usando buffers, pero de manera que quepan en un bloque horizontal de gráficos de datos con una altura de 16 píxeles.

Tiempos - CPU


Cuando un ingeniero necesita crear algo, primero verifica si esto es posible. ¡Por supuesto, al principio realizamos esta prueba!

Por lo tanto, necesitamos al menos 25 fps en una pantalla de 160 × 128 píxeles. Eso es 512,000 píxeles / s. Como el microcontrolador funciona a una frecuencia de 48 MHz, tenemos al menos 93 ciclos de reloj por píxel. Este valor cae a 58 ciclos si apuntamos a 40 fps.

De hecho, nuestro microcontrolador es capaz de procesar hasta 2 píxeles a la vez, porque cada píxel ocupa 16 bits, y el ATSAMD21 tiene un bus interno de 32 bits, es decir, ¡el rendimiento será aún mejor!

¡Un valor de 93 ciclos de reloj nos dice que la tarea es completamente factible! De hecho, podemos concluir que la CPU sola puede manejar todas las tareas de representación sin DMA. Lo más probable es que esto sea cierto, especialmente cuando se trabaja con ensamblador. Sin embargo, el código será muy difícil de manejar. ¡Y en C tiene que estar muy optimizado! De hecho, Cortex M0 + no es tan amigable con C como Cortex M3, y carece de muchas instrucciones (¡ni siquiera carga / guarda con un incremento / decremento posterior / preliminar!), Que debe implementarse con dos o más instrucciones simples.

Veamos qué debemos hacer para dibujar dos campos de juego (suponiendo que ya conozcamos las coordenadas x e y, etc.).

  • Calcule la ubicación del píxel de primer plano en la memoria flash.
  • Obtenga el valor de píxel.
  • Si es transparente, calcule la posición del píxel de fondo en el flash.
  • Obtenga el valor de píxel.
  • Calcule la ubicación del objetivo.
  • Guardar píxeles en el búfer.

Además, para cada sprite que puede ingresar al búfer, se deben realizar las siguientes operaciones:

  • Calcule la posición de un píxel de sprite en la memoria flash.
  • Obteniendo el valor del píxel.
  • Si no es transparente, calcule la ubicación del búfer de destino.
  • Guardar un píxel en el búfer.

Todas estas operaciones no solo no se implementan como una sola instrucción ASM, sino que cada instrucción ASM requiere dos ciclos al acceder a la memoria RAM / flash.

Además, todavía no tenemos lógica de juego (que, afortunadamente, lleva una pequeña cantidad de tiempo, ya que se calcula una vez por fotograma), reconocimiento de colisión, procesamiento de búfer e instrucciones necesarias para enviar datos a través de SPI.

Por ejemplo, aquí está el pseudocódigo de lo que tenemos que hacer (por ahora, suponemos que el juego no tiene desplazamiento, ¡y el campo de juego tiene un fondo de color constante!) Solo para el primer plano.

Deje que cameraY y cameraX sean las coordenadas de la esquina superior izquierda de la pantalla en el mundo del juego.

Deje que xTilepos e yTilepos sean la posición del mosaico actual en el mapa.

xTilepos = cameraX / 16; // this is a rightward shift of 4 bits. yTilepos = cameraY / 16; destBufferAddress = &buffer[0][0]; for tile = 0...9 nTile = gameMap[yTilepos][xTilepos]; tileDataAddress = &tileData[nTile]; xTilepos = xTilepos + 1; for y = 0…15 for x = 0…15 pixel = *tileDataAddress; tileDataAddress = tileDataAddress + 1; *destBufferAddress = pixel; destBufferAddress = destBufferAddress + 1; next destBufferAddress = destBufferAddress + 144; // point to next row next destBufferAddress = destBufferAddress – ( 160 * 16 - 16); // now point to the position where the next tile will be saved. next 

El número de instrucciones para 2560 píxeles (160 x 16) es de aproximadamente 16k, es decir. 6 por píxel. De hecho, puede dibujar dos píxeles a la vez. Esto reduce a la mitad el número real de instrucciones por píxel, es decir, el número de instrucciones de alto nivel por píxel es aproximadamente 3. Sin embargo, algunas de estas instrucciones de alto nivel se dividirán en dos o más instrucciones de ensamblador, o requerirán al menos dos ciclos para completar porque acceden a la memoria Además, no consideramos restablecer la canalización de la CPU debido a saltos y estados de espera para la memoria flash. Sí, todavía estamos lejos de los 58-93 ciclos a nuestra disposición, pero aún debemos tener en cuenta los antecedentes del campo de juego y los sprites.

Aunque vemos que el problema se puede resolver en una CPU, DMA será mucho más rápido. El acceso directo a la memoria deja aún más espacio para los sprites de pantalla o mejores efectos gráficos (por ejemplo, podemos implementar la mezcla alfa).

¡Veremos que para configurar el DMA para cada mosaico, necesitamos menos de 100 instrucciones C, es decir, menos de 0.5 por píxel! Por supuesto, DMA todavía tendrá que realizar la misma cantidad de transferencias en la memoria, pero el incremento de dirección y la transmisión se realizan sin la intervención de la CPU, que puede hacer algo más (por ejemplo, calcular y generar sprites).

Usando el temporizador SysTick, descubrimos que el tiempo requerido para preparar el DMA para todo el bloque, y luego para completar el DMA, es de aproximadamente 12k ciclos de reloj. Nota: ciclos de reloj! ¡No son instrucciones de alto nivel! El número de ciclos es bastante alto para solo 2560 píxeles, es decir 1.280 palabras de 32 bits. De hecho, obtenemos unos 10 ciclos por palabra de 32 bits. Sin embargo, debe considerar el tiempo requerido para preparar el DMA, así como el tiempo que le toma al DMA cargar los descriptores de transferencia desde la RAM (que esencialmente contienen punteros y la cantidad de bytes transferidos). Además, siempre hay algún tipo de cambio en el bus de memoria (para que la CPU no permanezca inactiva sin datos), y la memoria flash requiere al menos un estado de espera.

Tiempos - SPI


Otro cuello de botella es el SPI. ¿Son suficientes 12 MHz para 25 fps? La respuesta es sí: 12 MHz corresponde a aproximadamente 36 cuadros por segundo. Si usamos 24 MHz, ¡entonces el límite se duplicará!

Por cierto, las especificaciones de la pantalla y el microcontrolador dicen que la velocidad máxima de SPI es, respectivamente, de 15 y 12 MHz. Probamos y nos aseguramos de que se puede aumentar a 24 MHz sin problemas, al menos en la "dirección" que necesitamos (el microcontrolador escribe en la pantalla).

Utilizaremos la popular pantalla SPI de 1.8 pulgadas. Nos aseguramos de que tanto ILI9163 como ST7735 funcionen normalmente con una frecuencia de 12 MHz (al menos con 12 MHz. Se verifica que el ST7735 funciona con una frecuencia de hasta 24 MHz). Si desea utilizar la misma pantalla que en el tutorial "Cómo reproducir videos en Arduino Uno", le recomendamos modificarlo en caso de que desee agregar soporte SD en el futuro. Estamos utilizando la versión de la tarjeta SD para tener mucho espacio para otros elementos, como el sonido o niveles adicionales.

Gráficos


Como ya se mencionó, el juego usa fichas. Cada nivel consistirá en fichas que se repiten de acuerdo con la tabla, que llamamos "gameMap". ¿Qué tan grande será cada azulejo? El tamaño de cada mosaico afecta en gran medida el consumo de memoria, los detalles y la flexibilidad (y, como veremos más adelante, la velocidad también). Los mosaicos demasiado grandes requerirán la creación de un mosaico nuevo para cada pequeña variación que necesitemos. Esto ocupará mucho espacio en el disco.


Dos mosaicos de 32 × 32 píxeles de tamaño (izquierdo y central), que difieren en una pequeña parte (la parte superior derecha del píxel es 16 × 16). Por lo tanto, necesitamos almacenar dos mosaicos diferentes con un tamaño de 32 × 32 píxeles. Si usamos un mosaico de 16 × 16 píxeles (a la derecha), entonces necesitamos almacenar solo dos mosaicos de 16 × 16 (un mosaico completamente blanco y un mosaico a la derecha). Sin embargo, cuando usamos mosaicos de 16 × 16, obtenemos 4 elementos de mapa.

Sin embargo, se requieren menos fichas por pantalla, lo que aumenta la velocidad (ver más abajo) y reduce el tamaño del mapa (es decir, el número de filas y columnas en la tabla) de cada nivel. Las fichas demasiado pequeñas crean el problema opuesto. Las tablas de mapas son cada vez más grandes y la velocidad es más lenta. Por supuesto, no tomaremos decisiones estúpidas. por ejemplo, seleccione mosaicos con un tamaño de 17 × 31 píxeles. Nuestro fiel amigo - grados dos! El tamaño 16 × 16 es casi la "regla de oro", se usa en muchos juegos, ¡y lo elegiremos!

Nuestra pantalla tiene un tamaño de 160 × 128. En otras palabras, necesitamos 10 × 8 mosaicos por pantalla, es decir 80 entradas en la tabla. Para un gran nivel de pantallas de 10 × 10 (o pantallas de 100 × 1), solo se requerirán 8,000 registros (16 KB si usamos 16 bits para la grabación. Más adelante mostraremos por qué decidimos elegir 16 bits para la grabación).

Compare esto con la cantidad de memoria que puede estar ocupada por una imagen grande en toda la pantalla: 40 KB * 100 = 4 MB. Esto es una locura!

Hablemos del sistema de renderizado.

Cada cuadro debe contener (en orden de dibujo):

  • gráficos de fondo (campo de juego posterior)
  • el gráfico de nivel en sí mismo (primer plano).
  • sprites
  • texto / superposición superior.

En particular, realizaremos secuencialmente las siguientes operaciones:

  1. Dibujo de fondo + primer plano (mosaicos)
  2. dibujo de azulejos translúcidos + sprites + superposición superior
  3. enviando datos por SPI.

DMA dibujará el fondo y los mosaicos completamente opacos. Un mosaico completamente opaco es un mosaico en el que no hay píxeles transparentes.


Azulejo parcialmente transparente (izquierda) y completamente opaco (derecha). En un mosaico parcialmente transparente, algunos píxeles (en la parte inferior izquierda) son transparentes y, por lo tanto, se puede ver un fondo a través de esta área.

DMA no puede representar eficazmente mosaicos, sprites y superposiciones parcialmente transparentes. En realidad, el sistema de chip DMA ATSAMD21 simplemente copia los datos y, a diferencia del Amiga Blitter, no verifica la transparencia (establecida por el valor del color). Todos los elementos parcialmente transparentes son dibujados por la CPU.


Los datos se transmiten a la pantalla usando DMA.

Crear una tubería


Como puede ver, si realizamos estas operaciones secuencialmente en un búfer, tomará mucho tiempo. De hecho, mientras DMA se está ejecutando, la CPU no estará ocupada, ¡excepto esperar a que se complete DMA! Esta es una mala manera de implementar un motor gráfico. Además, cuando DMA envía datos a un dispositivo SPI, no utiliza su ancho de banda completo. De hecho, incluso cuando SPI opera a una frecuencia de 24 MHz, los datos se transmiten solo a una frecuencia de 3 MHz, que es bastante pequeña. En otras palabras, DMA no está acostumbrado a su máximo potencial: DMA puede realizar otras tareas sin perder realmente el rendimiento.

Es por eso que implementamos la tubería, que es el desarrollo de la idea del doble buffer (¡usamos tres buffers!). Por supuesto, al final, las operaciones siempre se realizan de forma secuencial. Pero la CPU y DMA realizan simultáneamente diferentes tareas, sin (especialmente) afectarse entre sí.

Esto es lo que sucede al mismo tiempo:

  • El buffer se usa para dibujar datos de fondo usando el canal DMA 1;
  • En otro búfer (que anteriormente estaba lleno de datos de fondo), la CPU dibuja sprites y mosaicos parcialmente transparentes;
  • Luego, se usa otro búfer (que contiene un bloque de datos horizontal completo) para enviar datos a la pantalla a través de SPI usando el canal DMA 0. Por supuesto, el búfer usado para enviar datos a través de SPI se llenó previamente con sprites mientras que SPI envió el bloque anterior y mientras otro búfer lleno de azulejos



DMA


El sistema de chip DMA ATSAMD21 no es comparable a Blitter, pero tiene sus propias características útiles. Gracias a DMA, podemos proporcionar una frecuencia de actualización muy alta, a pesar de tener un campo de juego dual.

La configuración de la transferencia de DMA se almacena en la RAM, en "descriptores de DMA", que le indican a DMA cómo y dónde debe realizar la transferencia actual. Estos descriptores se pueden unir: si hay una conexión (es decir, no hay un puntero nulo), luego de que se complete la transferencia, el DMA recibirá automáticamente el siguiente descriptor. Mediante el uso de múltiples descriptores, DMA puede realizar "transferencias complejas" que son útiles cuando, por ejemplo, el búfer de origen es una secuencia de segmentos no contiguos de bytes contiguos. Sin embargo, lleva tiempo obtener y escribir descriptores, porque necesita guardar / cargar 16 bytes de descriptor desde la RAM.

DMA puede trabajar con datos de diferentes longitudes: bytes, medias palabras (16 bits) y palabras (32 bits). En la especificación, esta longitud se llama "tamaño de latido". Para SPI, nos vemos obligados a utilizar la transferencia de bytes (aunque la especificación REVD actual establece que los chips ATSAMD21 SERCOM tienen FIFO, que, según Microchip, puede aceptar datos de 32 bits, de hecho, parece que no tienen FIFO. La especificación REVD también menciona Registro SERCOM CTRLC, que está ausente tanto en los archivos de encabezado como en la sección de descripción del registro. Afortunadamente, a diferencia de AVR, ATSAMD21 al menos tiene un registro de datos de transmisión amortiguado, por lo que no habrá pausas en la transmisión. Para dibujar fichas, nosotros, por supuesto, usamos 32 bits. Esto le permite copiar dos píxeles por latido. El chip ATSAMD21 DMA también permite que cada latido de origen aumente la dirección de origen o destino en un número fijo de tamaños de latido.

Estos dos aspectos son muy importantes y determinan la forma en que dibujamos los mosaicos.

En primer lugar, si renderizamos un píxel por latido (16 bits), reduciríamos a la mitad el rendimiento de nuestro sistema. ¡No podemos rechazar el ancho de banda completo!

Sin embargo, si dibujamos dos píxeles por latido, el campo del juego solo podrá desplazarse un número par de píxeles, lo que provocará un movimiento suave. Para manejar esto, puede usar un búfer que sea dos o más píxeles más grande. Al enviar datos a la pantalla, utilizaremos el desplazamiento correcto (0 o 1 píxel), dependiendo de si necesitamos mover la "cámara" en un número par o impar de píxeles.

Sin embargo, en aras de la simplicidad, reservamos espacio para 11 mosaicos completos (160 + 16 píxeles), y no para 160 + 2 píxeles. Este enfoque tiene una gran ventaja: no tenemos que calcular y actualizar la dirección del destinatario de cada descriptor de DMA (esto requeriría varias instrucciones, lo que podría generar demasiados cálculos por mosaico). Por supuesto, dibujaremos solo el número mínimo de píxeles, es decir, no más de 162. Sí, al final, gastaremos un poco de memoria adicional (teniendo en cuenta tres buffers, esto es aproximadamente 1500 bytes) para mayor velocidad y simplicidad. También puede realizar más optimizaciones.


Todos los búferes de bloque de 16 líneas (sin descriptores) son visibles en esta animación GIF. A la derecha está lo que realmente se muestra. Los primeros 32 cuadros se muestran en GIF, en el que nos movemos 1 píxel a la derecha en cada cuadro. El área negra del búfer es la parte que no se actualiza, y su contenido simplemente permanece de operaciones anteriores. Cuando la pantalla desplaza un número impar de fotogramas, se dibuja un área de 162 píxeles de ancho en el búfer. Sin embargo, la primera y la última columna de ellas (que se resaltan en la animación) se descartan. Cuando el valor de desplazamiento es un múltiplo de 16 píxeles, las operaciones de dibujo en el búfer comienzan desde la primera columna (x = 0).

¿Qué pasa con el desplazamiento vertical?

Lo trataremos después de mostrar un método para almacenar mosaicos en la memoria flash.

Cómo almacenar azulejos


Un enfoque ingenuo (que nos convendría si solo procesáramos a través de la CPU) sería almacenar los mosaicos en la memoria flash como una secuencia de colores de píxeles. El primer píxel de la primera línea, el segundo, y así sucesivamente, hasta el decimosexto. Luego guardamos el primer píxel de la segunda fila, el segundo, etc.

¿Por qué es una decisión tan ingenua? ¡Porque en este caso, DMA solo puede representar 16 píxeles por descriptor de DMA! Por lo tanto, necesitamos 16 descriptores, cada uno de los cuales necesita 4 + 4 operaciones de acceso a memoria (es decir, para transferir 32 bytes - 8 operaciones de lectura de memoria + 8 operaciones de escritura de memoria - DMA debe realizar 4 lecturas más + 4 escrituras). ¡Esto es bastante ineficiente!

De hecho, para cada descriptor, DMA solo puede incrementar las direcciones de origen y destino en un número fijo de palabras. Después de copiar la primera línea del mosaico en el búfer, la dirección del destinatario no debe aumentarse en 1 palabra, sino en un valor tal que apunte a la siguiente línea del búfer. Esto no es posible porque cada descriptor de transmisión indica solo el incremento de transmisión de tiempo, que no se puede cambiar.

Será mucho más inteligente enviar los primeros dos píxeles de cada línea del mosaico secuencialmente, es decir, los píxeles 0 y 1 de la línea 0, los píxeles 0 y 1 de la línea 1, etc., hasta los píxeles 0 y 1 de la línea 15. Luego enviamos los píxeles 2 y 3 de la línea 0, y así sucesivamente.


¿Cómo se almacena un mosaico?

En la figura anterior, cada número indica el orden en que se almacena el píxel de 16 bits en la matriz de mosaicos.

Esto se puede hacer con un descriptor, pero necesitamos dos cosas:

  • Los mosaicos deben almacenarse de modo que cuando incrementemos la fuente en una palabra, siempre apuntemos a las posiciones correctas de píxeles. En otras palabras, si (r, c) es un píxel en la fila r y la columna c, entonces debemos guardar los píxeles (0,0) (0,1) (1,0) (1,1) (2,0) secuencialmente (2.1) ... (15.0) (15.1) (0.2) (0.3) (1.2) (1.3) ...
  • El búfer debe tener 256 píxeles de ancho (no 160)

El primer objetivo es muy fácil de lograr: simplemente cambie el orden de los datos, puede hacerlo al exportar gráficos a un archivo c (vea la imagen de arriba).

El segundo problema se puede resolver porque DMA le permite aumentar la dirección del destinatario después de cada latido en 512 bytes. Esto tiene dos consecuencias:

  • No podemos enviar datos utilizando un descriptor único sobre un bloque SPI. Este no es un problema muy serio, porque al final leemos un descriptor a través de 160 píxeles. El impacto en el rendimiento será mínimo.
  • El bloque debe tener un tamaño de 256 * 2 * 16 bytes = 8 KB, y habrá un montón de "espacio no utilizado" en él.

Sin embargo, este espacio todavía se puede usar, por ejemplo, para descriptores.

De hecho, cada descriptor tiene un tamaño de 16 bytes. Necesitamos al menos 10 * 8 (¡y en realidad 11 * 8!) Descriptores para mosaicos y 16 descriptores para SPI.

Es por eso que mientras más fichas, mayor es la velocidad. De hecho, si usáramos, por ejemplo, un mosaico de 32 x 32, necesitaríamos menos descriptores por pantalla (320 en lugar de 640). Esto reduciría el desperdicio de recursos.

Mostrar bloque de datos


El búfer de bloque, los descriptores y otros datos se almacenan en un tipo de estructura, que llamamos displayBlock_t.

displayBlock es una matriz de 16 elementos displayLineData_t. Los datos de DisplayLine contienen 176 píxeles más 80 palabras. En estas 80 palabras, almacenamos descriptores de visualización u otros datos de visualización útiles (mediante unión).



Como tenemos 16 líneas, cada mosaico en la posición X usa los primeros 8 descriptores DMA (0 a 7) de las líneas X. Dado que tenemos un máximo de 11 mosaicos (la línea de visualización tiene 176 píxeles de ancho), los mosaicos usan solo los primeros descriptores DMA 11 filas de datos. Los descriptores 8–9 de todas las líneas y los descriptores 0–9 de las líneas 11–15 son gratuitos.

De estos, los descriptores 8 y 9 de las líneas 0..7 se utilizarán para SPI.

Descriptores 0..9 líneas 11-15 (hasta 50 descriptores, aunque usaremos solo 48 de ellos) se utilizarán para el campo de juego de fondo.

La siguiente figura muestra su estructura.


Campo de juego de fondo


El campo de juego de fondo se maneja de manera diferente. En primer lugar, si necesitamos un desplazamiento suave, tendremos que volver al formato de dos píxeles, ya que el primer plano y el fondo se desplazarán a diferentes velocidades. Por lo tanto, el ritmo estará a la mitad. Aunque esto es una desventaja en términos de velocidad, este enfoque facilita la integración. Solo nos queda un pequeño número de descriptores, por lo que no se pueden usar mosaicos pequeños. Además, para simplificar el trabajo y agregar rápidamente paralaje, utilizaremos largos "sectores".

El fondo se dibuja solo si hay al menos un píxel parcialmente transparente. Esto significa que si solo hay un mosaico transparente, se dibujará el fondo. Por supuesto, esto es una pérdida de ancho de banda, pero simplifica todo.

Compare el fondo y los campos de juego frontales:

  • En el fondo, se utilizan sectores, que son mosaicos largos almacenados de forma "ingenua".
  • El fondo tiene su propio mapa, pero horizontalmente se repite. Gracias a esto, se usa menos memoria.
  • El fondo tiene paralaje para cada sector.

Campo de juego delantero


Como se dijo, en cada bloque tenemos hasta 11 mosaicos (10 mosaicos completos, o 9 mosaicos completos y 2 archivos parciales). Cada uno de estos mosaicos, si no está marcado como transparente, se dibuja DMA. Si no es completamente opaco, se agrega a la lista, que se analizará más adelante, cuando se renderizan sprites.

Conectamos dos campos de juego


Los descriptores del campo de juego de fondo (que siempre se calculan) y el campo de juego frontal forman una lista enlazada muy larga. La primera parte dibuja un campo de juego de fondo. La segunda parte dibuja mosaicos sobre el fondo. La longitud de la segunda parte puede ser variable, porque los descriptores DMA de mosaicos parcialmente transparentes están excluidos de la lista. Si el bloque contiene solo mosaicos opacos, DMA se configura de la siguiente manera. para comenzar directamente desde el primer descriptor del primer mosaico.

Sprites y azulejos con transparencia


Las baldosas con transparencia y sprites se procesan casi de la misma manera. Se realiza el análisis de píxeles de mosaico / sprite.Si es negro, entonces es transparente y, por lo tanto, el mosaico de fondo no cambia. Si no es negro, el píxel de fondo se reemplaza por un píxel de sprite / mosaico.

Desplazamiento vertical


Al trabajar con desplazamiento horizontal, dibujamos hasta 11 mosaicos, incluso si al dibujar 11 mosaicos, el primero y el último solo se dibujan parcialmente. Tal representación parcial es posible debido al hecho de que cada descriptor dibuja dos columnas del mosaico, por lo que podemos establecer fácilmente el comienzo y el final de la lista vinculada.

Cuando trabajamos con desplazamiento vertical, necesitamos calcular tanto el registro del receptor como el volumen de transmisión. Deben configurarse varias veces por cuadro. Para evitar este alboroto, simplemente podemos dibujar hasta 9 bloques completos por cuadro (8 si el desplazamiento es un múltiplo de 16).

Equipo


Como dijimos, el corazón del sistema es uChip. ¿Qué hay del resto?

Aquí hay un diagrama! Vale la pena mencionar algunos aspectos.


Llaves


Para optimizar el uso de E / S, usamos un pequeño truco. Tendremos 4 buses de sensores L1-L4 y un cable LC común. 1 y 0 se aplican alternativamente al cable común. En consecuencia, los buses de sensores se tirarán hacia abajo o hacia arriba alternativamente con la ayuda de resistencias de tracción internas. Dos llaves están conectadas entre cada uno de los buses de teclas y un bus común. Se inserta un diodo en serie con estas dos teclas. Cada uno de estos diodos se conmuta en la dirección opuesta, de modo que cada vez que solo se "lee" una tecla.

Dado que no hay un controlador de teclado incorporado (y ningún controlador de teclado incorporado utiliza este método interesante), se sondean rápidamente ocho teclas al comienzo de cada cuadro. Dado que las entradas se deben tirar hacia arriba y hacia abajo, no podemos (y no queremos) usar resistencias externas, por lo que debemos usar las integradas, que pueden tener una resistencia bastante alta (60 kOhm). Esto significa que cuando el bus común cambia de estado, y los buses de datos cambian su estado de extracción hacia arriba / abajo, debe insertar algún retraso para que la resistencia incorporada hacia arriba / abajo cambie el contrato y establezca la capacitancia parásita al nivel deseado. ¡Pero no queremos esperar! Por lo tanto, colocamos el bus común en un estado de alta impedancia (para que no haya desacuerdo), y primero cambiamos los buses del sensor a valores lógicos 1 o 0,configurándolos temporalmente como salida. Más tarde se configuran como entrada tirando hacia arriba o hacia abajo. Dado que la resistencia de salida es del orden de decenas de ohmios, el estado cambia en unos pocos nanosegundos, es decir, cuando el bus del sensor vuelve a la entrada, ya estará en el estado deseado. Después de eso, el bus común cambia a la salida con la polaridad opuesta.

Esto mejora enormemente la velocidad de escaneo y elimina la necesidad de retrasos / instrucciones de nop.

Conexión SPI


Conectamos la SD y la pantalla para que se comuniquen entre sí sin transferir datos al ATSAMD21. Esto puede ser útil si desea reproducir el video.

Las resistencias que conectan MISO y MOSI deben ser bajas. Si son demasiado grandes, entonces el SPI no funcionará, porque la señal será demasiado débil.

Optimización y desarrollo posterior


Uno de los mayores problemas es el uso de RAM. Tres bloques ocupan 8 KB cada uno, dejando solo 8 KB por pila y otras variables. Por el momento, tenemos solo 1.3 KB de RAM libre + 4 KB de pila (4 KB por pila; esto es mucho, tal vez lo reduzcamos).

Sin embargo, puede usar bloques con una altura no de 16, sino de 8 píxeles. Esto aumentará el desperdicio de recursos en los descriptores DMA, pero casi reducirá a la mitad la cantidad de memoria ocupada por el búfer de bloque (tenga en cuenta que el número de descriptores no cambiará si continuamos usando mosaicos de 16 × 16, por lo que tendremos que cambiar la estructura del bloque). Esto puede liberar aproximadamente 7.5 KB de RAM, lo que será muy útil para implementar funciones como una tarjeta modificable con secretos o agregar sonido (aunque el sonido se puede agregar incluso con 1 KB de RAM).

Otro problema es el sprite, pero esta modificación es mucho más simple de realizar, y solo necesita la función createNextFrameScene () para ello. De hecho, estamos creando en RAM una gran matriz con el estado de todos los sprites. Luego, para cada sprite, calculamos si su posición está dentro del área de la pantalla, y luego lo animamos y lo agregamos a la lista de renderizado.

En cambio, puede realizar la optimización. Por ejemplo, en gameMap puedes almacenar no solo el valor del mosaico, sino también una bandera que indica la transparencia del mosaico, establecida en el editor. Esto nos permitirá verificar rápidamente si el mosaico debe representarse: DMA o CPU. Es por eso que utilizamos registros de 16 bits para la tarjeta de mosaico. Si suponemos que tenemos un conjunto de 256 mosaicos (en este momento tenemos menos de 128 mosaicos, pero hay suficiente espacio en la memoria flash para agregar nuevos), entonces hay 7 bits libres que se pueden usar para otros fines. Tres de estos siete bits se pueden usar para indicar si se está almacenando un objeto / objeto. Por ejemplo:

0b000 =
0b001 =
0b010 =
0b011 =
0b100 =
0b101 =
0b110 =
0b111 = , , .


Luego, puede crear una tabla de bits en la RAM en la que cada bit significa si se detecta (por ejemplo, un enemigo) / si (por ejemplo, una bonificación) se recoge / si se activa un determinado objeto (interruptor). En un nivel de 10 × 10 pantallas, esto requerirá 8000 bits, es decir 1 KB de RAM. El bit se restablece cuando se detecta un enemigo o se recoge una bonificación.

En createNextFrameScene (), debemos verificar los bits correspondientes a los mosaicos en el área visible actual. Si tienen un valor de 1:

  • Si esto es un bono, simplemente agréguelo a la lista de sprites para renderizar.
  • Si este es un enemigo, crea un sprite dinámico y reinicia la bandera. En el siguiente cuadro, la escena contendrá un sprite dinámico hasta que el enemigo abandone la pantalla o sea asesinado.

Este enfoque tiene desventajas.

  1. -, ( ). .
  2. -, 80 , , . , 32 . , «/» ( «», .. 0!). «», «» ( ).
  3. -, . ( ), . , .
  4. -, , , , . , , . , , , , !
  5. , (, Unreal Tournament , ).

Sin embargo, de esta manera podemos almacenar y procesar sprites a un nivel mucho más eficiente.

Sin embargo, esta técnica es más relevante para la "lógica del juego" que para el motor gráfico del juego.

Quizás en el futuro implementaremos esta función.

Para resumir


Esperamos que hayas disfrutado este artículo introductorio. Necesitamos explicar muchos más aspectos que serán los temas de futuros artículos.

Mientras tanto, puedes descargar el código fuente completo del juego. Si te gusta, puedes apoyar financieramente al artista ansimuz , que dibujó todos los gráficos y se los dio al mundo de forma gratuita. También aceptamos donaciones .

El juego aún no ha terminado. Queremos agregar sonido, muchos niveles, objetos con los que pueda interactuar y similares. ¡Puedes crear tus propias modificaciones! ¡Esperamos ver nuevos juegos con nuevos gráficos y niveles!

Pronto lanzaremos un editor de mapas, ¡pero por ahora es demasiado rudimentario mostrarlo a la comunidad!

Video


(Nota: ¡debido a la poca iluminación, el video se grabó a una velocidad de cuadro mucho más baja! ¡Pronto actualizaremos el video para que pueda estimar la velocidad máxima a 40 fps!)


Gratitud


Los gráficos del juego (y los mosaicos que se muestran en algunas imágenes) están tomados del activo gratuito "Sunny Land" creado por ansimuz .

Materiales descargables


El código fuente del proyecto es de dominio público, es decir, se proporciona de forma gratuita. Lo compartimos con la esperanza de que sea útil para alguien. ¡No garantizamos que debido a cualquier error / error en el código no habrá problemas!

Diagrama esquemático Proyecto

KiCad

Proyecto Atmel Studio 7 (fuente)

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


All Articles