http://gitlab.matcom.uh.cu/pm1/snakasm.git
Ciencia de la Computación. Curso 2016-17.
Con la idea de preparar un soporte para lo que más adelante será el curso de Sistemas Operativos el colectivo de la asignatura a decidido orientar un proyecto sin dicho recurso (SO).
El objetivo es la implementación de un juego tipo "Snake" pero completamente desarrollado en NASM y sin la utilización de un sistema operativo. Para esto se le brinda un API para la interacción con las partes más primitivas de la computadora, en estos momentos completamente desprovista de drivers ni nada por el estilo.
Antes de empezar a desarrollar es necesario que tenga instalado todos los programas. Para esto desde un terminal ejecute:
$ sudo make install_dependencies
Debe de estar conectado y con acceso a un repositorio. Luego de esto, no debe haber ningún problema con empezar de inmediato con el resto de su proyecto.
Los requerimientos básicos (los que garantizan una calificación de 3) del proyecto son los siguientes:
-
Comenzar
Que el juego arranque.
-
Visualizar Mapa
Este debe tener paredes y espacios en blanco de forma tal que la serpiente pueda moverse y chocar.
-
Movimiento con teclas
4 movimientos básicos, arriba, abajo, derecha e izquierda.
-
Comer y crecer
Eventualmente hacer aparecer "fruta" en una posición válida (alcanzable) del mapa y cuando la cabeza de la serpiente pase por encima de este, hacer que la longitud de la serpiente aumente.
-
Chocar y perder
Si la cabeza de la serpiente se encuentra con un obstáculo (cualquier zona no vacía que no sea una fruta) el juego debe detenerse e indicar que se ha perdido.
-
Puntuación.
Llevar la cuenta de los puntos del jugador y hacérselo saber en todo momento.
Una correcta implementación del anterior grupo de funcionalidades le otorgará 3 puntos. Para alcanzar una nota superior es necesario implementar algunas de las siguientes características opcionales.
Por su nivel de complejidad no todos tienen el mismo "peso":
Cada funcionalidad otorga una cantidad de puntos determinada, en función de su complejidad. A continuación describimos cada una:
-
Dificultad (velocidades, ...) (+0.5)
Tener distintos niveles de dificultad, que afecten la sensación del juego. Una posible idea sería la velocidad de la serpiente.
-
Niveles fijos (+0.5) - Aleatorios jugables (+1)
Tener la opción de jugar distintos niveles en vez de uno fijo. También es posible que estos niveles se generen de manera aleatoria, pero siempre garantizando que sean jugables, que no haya una zona inaccesible en el mapa.
-
Alta puntuación (+0.5)
Durante el período del juego llevar la cuenta de cuáles han sido las puntuaciones más altas, y llevar un scoreboard que se muestre de alguna manera, preferiblemente al terminar el juego.
-
Title screen con ascii art (+0.5)
Tener una pantalla de presentación generada con ASCII art.
-
Tiempo (modificar la puntuación, ...) (+0.5)
El tiempo transcurrido debe afectar el modo de juego. Por ejemplo, ir disminuyendo la puntuación cada 10 segundos si no se comido ninguna fruta.
-
Serpientes adicionales (IA ó 2nd player) (+1) (ambos +1.5)
Tener la posibilidad de jugar con varias serpientes, lo mismo controladas de manera automática que por otro jugador. La implementación de alguno de estos requerimientos aporta 1 punto adicional o (no el
o
de lógica, en todo caso el xor ;-)) 1.5 si se realizan ambos. -
Sonido/Música (+0.5)
Añadir sonido o música al juego.
-
Cualquier otro (+?) (consultar con el profesor)
Como puede verse, es posible lograr más de 5 puntos, calificación que se tendrá en cuenta para la evaluación final de la asignatura.
Es muy probable que para realizar (como es debido) algunos de estos requerimientos se necesite de un poco de investigación al respecto.
- Ej 1. Para poner sonido es necesario leer sobre los "puertos" y cómo pedir e insertar valores.
- Ej 2. Las serpientes controladas automáticamente normalmente se quiere que no tengan un movimiento completamente caótico (solución que es perfectamente válida para conseguir los puntos de este requerimiento).
- Ej 3. Funciones que generen números "random".
Para la correcta implementación de la tarea se le brinda una plantilla que contiene el código necesario para comenzar a cargar un programa muy básico en una máquina virtual, además de las respectivas funciones para leer del teclado, pintar en la pantalla y consultar el tiempo.
snakasm/
├── src/
│ ├── multiboot.asm
│ ├── boot.asm
│ ├── main.asm
│ ├── game.asm
│ ├── keyboard.asm
│ ├── keyboard.mac
│ ├── video.asm
│ ├── video.mac
│ └── timing.asm
├── Makefile
├── README.md
├── ORIENTACIÓN.md
├── linker.ld
├── menu.lst
└── stage2_eltorito
Vale destacar que, aunque estaremos usando una máquina virtual para el desarrollo, este esqueleto contiene todo lo necesario para que al finalizar su proyecto, usted pueda guardar su juego en una memoria y ejecutarlo en su PC real.
En la carpeta src
se encuentra el código que se brinda como base para comenzar el proyecto.
-
game.asm
Se encuentra el ciclo principal del juego, como ejemplo se ve el código de un programa que cuando se presiona la tecla
arriba
la pantalla se pone de color rojo y cuando se presiona "abajo" de color verde. -
kerboard.asm
Se encuentra un procedimiento llamado
scan
que retorna enal
el byte de la última tecla que se presionó y 0 si es la misma. -
keyboard.mac
Valores útiles para el trabajo con el teclado.
-
video.asm
En este fichero está implementado solo el procedimiento
clear
que recibe una palabra que representa el color con el que pintar toda la pantalla. Es tarea de usted implementar el resto de las funciones que sean necesarias para el trabajo con la pantalla.Para manipular la pantalla se utilizará el framebuffer. Básicamente lo que se debe hacer es escribir una dirección de memoria (
0xB8000
) declarada en la macroFBUFFER
. A partir de esta dirección de memoria es posible utilizarCOLS*ROWS
palabras (2 bytes). En cada "casilla" es posible especificar el caracter (1 byte), el foreground (1 nibble) y el background (1 nibble). Ej. Para representar unaa
de color rojo y fondo azul se utilizaríamov ax, 'a' | FG.RED | BG.BLUE
Para escribir en el pixel
i
,j
(fila i, columna j) es necesario modificar el valor de la posición2*(i*COLS + j)
.Se recomienda utilizar lo más que se pueda instrucciones de cadena para disminuir la longitud del código y aumentar su expresividad.
-
video.mac
Macros útiles para el trabajo con el framebuffer. Tamaño de la pantalla y colores para background y foreground.
-
timing.asm
Funciones útiles para el trabajo con el tiempo.
En clases prácticas estaremos entrando más en detalle acerca de este aspecto.
Los demás archivos son utilizados en el proceso de arranque (boot).
-
multiboot.asm
Declara la sección
multiboot
. -
boot.asm
Prepara la pila y salta para la etiqueta
main
. -
main.asm
Desaparece el cursor de la pantalla y salta para la etiqueta
game
.
Existen otros archivos de utilidad en el proyecto no relacionados con el código fuente.
-
linker.ld
Características a tener en cuenta en el proceso de enlace (linker).
-
menu.lst
Configuración de
grub
. -
stage2_eltorito
Binario de
GRUB
para incluirlo dentro de la imagen de disco que se construye. -
README.md
Utilizar este archivo para describir las características propias de su proyecto. Ser lo más claro posible, incluir imágenes y ejemplos.
Este tipo de archivos es característico en todo tipo de proyectos, y utiliza formato Markdown. Normalmente se usa para explicar el software en caso de que otra persona quiera utilizar este código.
-
ORIENTACIÓN.md
Este archivo ;-).
-
.gitignore
Archivos que no se tendrán en cuenta en su repositorio de
git
.
El proceso de compilación y ejecución del código está completamente a cargo de make
. En principio no es necesario cambiar el código del Makefile
, solo si se quisiera añadir alguna modificación, como añadir más directorios a su proyecto.
Para compilar el proyecto solo es necesario ejecutar make
en el directorio del proyecto.
$ make
Cualquier archivo que se añada en el directorio src
automáticamente pasará a formar parte del código fuente de su proyecto, por tanto se ensamblará y enlazará apropiadamente para crear el programa snakasm.elf
sin hacer ninguna modificación en el Makefile
.
QEMU es donde va a correr su programa de manera virtualizada.
Para correr y probar su programa ejecutar make qemu
en una terminal sobre el directorio de su proyecto.
$ make qemu
De manera opcional también se puede preparar un iso y realizar el proceso de arranque utilizando qemu directamente con el iso.
$ make qemu-iso
- Los equipos no deben ser de más de dos personas y se recomienda que no sean menos que esta cantidad.
- No hay ninguna razón para utilizar
C
a no ser que la funcionalidad que se quiera implementar sea lo suficientemente compleja. En cualquier caso consultar con el colectivo de la asignatura su propuesta. - La fecha de entrega es el viernes de la semana 16.
- No invertir mucho tiempo en tareas que parezcan muy complicadas. Proceder en orden ascendente. Plantearse tareas pequeñas que puedan luego ir escalando.
- Comentar el código abundantemente, cuanto más, mejor.
- Organizar convenientemente el código en distintos archivos dentro de la carpeta
src
, de acuerdo con la lógica que desarrollen.
Sistema de control de versiones.
Es altamente recomendada la utilización de esta herramienta dada las ventajas que brinda.
Todo ha sido preparado para que se pueda concentrar en la implementación del proyecto únicamente. De cualquier forma el colectivo de la asignatura está preparado para recibir preguntas de cualquier tipo con respecto al código y las herramientas que se brindan.
Un puerto IO
es una dirección en el bus IO
de la familia de microprocesadores x86
. Este bus permite la comunicación con los dispositivos. Para realizar dicha comunicación, NASM
nos brinda las siguientes instrucciones:
in reg, addr
se utiliza para leer del puerto addr
y escirbir su valor en el registro reg
.
El operando reg
solamente puede ser uno de los registros al
, ax
o eax
y addr
solo puede ser una constante numérica o el registro dx
.
out addr, data
se utiliza para escribir el valor data
en el puerto addr
.
El operando addr
solo puede ser una constante numérica o el registro dx
y data
solamente puede ser uno de los registros al
, ax
o eax
.
links:
CMOS
(Complementary-symmetry Metal-Oxide Semiconductor) es una zona de memoria estática, dividida en varios registros, destinada a almacenear la información del SETUP
del BIOS
(Basic Input Output System). El CMOS se encuentra dentro de un chip que posee una batería independiente, por lo que retiene la información mientras la computadora está apagada. Este chip también posee otro circuito llamado RTC
(Real Time Clock), que cuenta la fecha y la hora, y almacena su valor en varios registros del CMOS.
La comunicación con el CMOS se realiza através de los puertos 0x70 y 0x71. El puerto de direccionamiento (0x70) se utiliza para informar a qué registro del CMOS se quiere acceder, mientras que el puerto de datos (0x71) se utiliza para escribir o leer en el registro seleccionado con 0x70. Los registros del CMOS asociados al RTC son:
Register | Contents |
---|---|
0x00 | Seconds |
0x02 | Minutes |
0x04 | Hours |
0x06 | Weekday |
0x07 | Day of Month |
0x08 | Month |
0x09 | Year |
0x32 | Century (maybe) |
0x0A | Status Register A |
0x0B | Status Register B |
El registro 0x0A notifica en su 5to bit menos significativo (0x80) cuándo está sucediendo la actualización de los registros del RTC (RTC Update In Progress) y por tanto están en un estado inconsistente. Por lo tanto, antes de hacer una consulta a los registros debería esperar a que dicho bit esté activo.
Ej:
wait_in_progress:
mov al, 0XA0
in 0x70, al
test al, 0x80 ; and lógico que sólo modifica los flags (no modifica los operandos)
jnz wait_in_progress
Cada registro tiene un byte de tamaño, por lo que el año no cabe en un solo registro. El siglo es almacenado en el Century Register (0x32) y el resto se almacena en el Tear Register (0x09). Por lo tanto si se quiere obtener el año completo debería calcularse (RealYear = 100 * Century + Year).
Ej:
Las siguientes instrucciones guardan en al
los segundos de la hora actual:
mov al, 0x00
out 0x70, al ; seleccionando el registro 0x00 del CMOS
in al, 0x71 ; leyendo su valor
links:
Si se quisiera esperar (en un programa) un tiempo determinado, se pudiera ejecutar un ciclo hasta que la diferencia en la hora sea dicho tiempo. Pero la fecha proporcionada por los registros del CMOS tiene una resolución en segundos, lo cual es inútil por si solo cuando queremos esperar milisegundos. Para resolver este problema puede ser utilizada instrucción rdtsc
(Read Time Stamp Counter), que almacena en edx:eax
la cantidad de ciclos del reloj que han ocurrido desde que se ha encendido la computadora. A continuación se explica cómo se puede hacer para una cantidad de milisegundos ms
:
- Calcular previamente la cantidad de ciclos que han transcurrido durante un segundo (
tps
) utilizando rtdsc y la hora del CMOS (teniendo en cuenta el wait in progress) - A partir del momento en que se desee esperar, contar, dentro de un ciclo, la cantidad de ciclos del reloj transcurridos (
tc
) - Romper el ciclo cuando se cumpla la condición
1000 * tc / tps >= ms
El teclado es otro dispositivo que se comunica através del bus IO. Este dispositivo es extremadamete complejo y sólo nos centraremos en saber cuál tecla fue presionada. Para esto podemos consultar el puerto IO 0x60.
Ej:
in al, 0x60 ; almacena en al el código de la tecla presionada
links:
Existen otros dispositivos (Memory Mapped Devices
) que, a difirencia de utilizar el bus IO, utilizan la RAM
para su comunicación. La tarjeta gráfica se comunica utilizando una zona de memoria denominada Frame Buffer
, cuyo tamaño varía dependiendo del modo en que se configure. En el proyecto se utilizará la tarjeta gráfica en modo texto, el cual asume que la pantalla es una matriz de texto con 25 filas y 80 columnas. La codificación de la matriz se realiza utilizando 2 bytes por celda y ubicando en la memoria cada fila una a continuación de otra. El framebuffer está ubicado a partir de la dirección 0xB8000 y tiene una extensión de 25 * 80 * 2 bytes, por lo tanto la celda ubicada en la fila r y la columna c se encuentra en la dirección de memoria 0xB8000 + (80 * r + c) * 2. Cada celda se codifica en una palabra (word), el byte menos significativo es exactamente el caracter (chr) que se mostrará en la celda y el más significativo representa el color de la celda. El byte del color almacena en el nibble menos significativo el color del caracter (fg), y en el nible más significativo el color de fondo (bg). Por lo tanto, la palabra (word) correspondiente a una celda puede ser representada de la forma ((bg << 12) | (fg << 8) | chr). Además, el bit más significativo de cada color (fb o bg), al estar activado, indica
que el color se mostrará en su forma más clara.
Color | value |
---|---|
BLACK | 0x0 |
BLUE | 0x1 |
GREEN | 0x2 |
CYAN | 0x3 |
RED | 0x4 |
MAGENTA | 0x5 |
YELLOW | 0x6 |
GRAY | 0x7 |
LIGHT_BLUE = BLUE | 0b1000
links: