es/ASMTutorial
English | Português | Español | 日本語 |
Conceptos Básicos
¿Qué es Assembly?
El lenguaje ensamblador (Assembly o ASM), es utilizado para darle comandos al hardware, este se traduce directamente a una secuencia de números binarios que luego es leído por el hardware para ejecutar la lógica del programa. En estos tutoriales se enseñará ASM 65816 que es el assembly de SNES.
Registros de la SNES
Un registro es un componente de hardware que almacena de manera momentánea un valor específico. Estos se utilizan generalmente para:
- Operaciones aritméticas (como sumar o restar)
- Operaciones lógicas (comparaciones o lógica binaria Bitwise)
- Saltos (ir a otra zona del código)
- Indexación (poder llamar distintos valores de una tabla)
En el SNES en específico tenemos 3 registros, todos pueden ser de 8 (permite valores entre 0 y 255) o 16 bits (permite valores entre 0 y 65535)::
- A: el registro principal, es el único que tiene operaciones aritméticas y lógicas como sumar, restar u operaciones bitwise.
- X e Y: estos 2 registros son de indexación y se usan en general para poder acceder a distintos valores de una tabla o como índices en un bucle.
Constantes
En el SNES puedes utilizar constantes para escribir valores en registros o en direcciones de memoria RAM. Para declarar una constante debe ir primero el carácter “#” y luego el valor de la constante. Estas constantes pueden ser escritas en:
- Binario: Va con un carácter “%” al inicio, solo puede usar dígitos del 0 al 1.
- Ejemplo: #%00001000 (seria el valor 8)
- Hexadecimal: Va con un carácter “$” al inicio, puede usar dígitos del 0 al 9 o de “A” hasta “F”.
- Ejemplo: #$10 (sería el valor 16)
- Decimal: Va sin un carácter al inicio, puede usar dígitos del 0 al 9.
- Ejemplo: #10 (sería el valor 10)
Nota: Dependiendo de si los registros son de 8 o 16 bits, las constantes deben adecuarse a esa cantidad de bits, por ejemplo, #$AA es de 8 bits y #$1000 es de 16 bits, si el registro fuera de 8 bits y cargas un valor de 16 bits esto podría generar errores, lo mismo si a un registro de 16 bits le pones un valor de 8 bits.
Memoria RAM y ROM
En el SNES la información del juego se guarda en la memoria RAM y ROM, la diferencia entre estos 2 tipos de memoria es la capacidad de escritura, la memoria ROM no puede ser modificada, mientras que la RAM si, por esto, la información del juego, como su lógica, gráficos, música, etc… Se guarda en ROM, mientras que la RAM se suele escribir a medida que el juego se ejecuta y se utiliza para las distintas variables que son requeridas por el juego. Las direcciones de ROM y RAM son de 24 bits, por ejemplo:
- $123456
En base a esto se puede dividir la dirección de memoria en 3 partes:
- Bank: Este corresponde al byte más alto de todos, en el ejemplo sería $12. La memoria está dividida en varios Banks de memoria, cada uno es de 32kb, a excepción de si se usa un Hirom, en este caso algunos de los banks serían de 64kb.
- High Byte: Este es el segundo byte más alto, en el ejemplo sería $34.
- Low Byte: este es el byte más bajo, en el ejemplo sería $56
En el SNES los Banks se pueden organizar de la siguiente manera:
- $7E-$7F: son usados por la WRAM (Ram de Trabajo) que es la que guarda las variables en general.
- $00-$3F: ROM.
- $80-$CF: ROM.
- $70-$71: SRAM (RAM Estática) que es la que se utiliza normalmente para guardar la partida.
- Otros bancos pueden ser utilizados por distintos chips de expansión, por ejemplo el SA-1 puede usar el bank $40-$41 BWRAM que básicamente es una WRAM pero más rápida.
Load y Store
Las operaciones Load y Store son las más básicas, Load se utiliza para guardar un valor en un registro, mientras que Store se utiliza para guardar un valor de un registro en una dirección de RAM.
Comandos Load:
- LDA: Guarda un valor en el registro A.
- LDX: Guarda un valor en el registro X.
- LDY: Guarda un valor en el registro Y.
Comandos Store:
- STA: Guarda el valor de A en una dirección de RAM.
- STX: Guarda el valor de X en una dirección de RAM.
- STY: Guarda el valor de Y en una dirección de RAM.
- STZ: Guarda el valor 0 en una dirección de RAM.
En el caso de los comandos Load estos reciben como parámetro el valor que se guardara en el registro, este valor puede ser una constante, una dirección de RAM o de ROM, e incluso una indirección, Ejemplo:
- LDX #$A0: Guarda el valor 0xA0 (160) en el registro X.
- LDY $7890: Guarda el valor de la dirección de RAM $7890 en el registro Y. Por ejemplo si esta dirección tuviera el valor #$01, entonces Y tendría el valor 1.
- LDA ($00): Esto seria una indirección, se abordara a más profundidad más adelante, pero básicamente esto tomaría la dirección de memoria guardada en $00 y $01 (donde $01 seria el High byte y $00 el Low Byte), luego iría a esa dirección de memoria, obtendrá su valor y guardaría ese valor en el registro A.
- Ejemplo:
- Si $00-$01 tuvieran el valor #$5678, LDA ($00) iria a la dirección $5678 y luego tomaría el valor de esa dirección de RAM (que por ejemplo pudiera tener el valor #$10) y entonces A tendría el valor #$10 después de usar ese comando.
- Ejemplo:
En el caso de los comandos Store estos reciben como parámetro una dirección de RAM, ya sea, de manera directa o de forma indirecta, Ejemplo.
- STX $4567: Guardería el valor de X en la dirección de RAM $4567
- STA ($00): Similar al LDA ($00), se tomaría el valor de $00 y $01 para llegar a una dirección de RAM y luego se guardaría el valor de A en esa dirección de RAM.
- Ejemplo:
- Si $00-$01 tuvieran el valor #$5678, STA ($00) iría a la dirección $5678 y luego guardaría el valor de A en la dirección $5678.
- Ejemplo:
Registro de Status y Operaciones Aritméticas y Lógicas
Registro de Status
El Registro de Status también llamado Registro P, es un registro especial de la SNES, este se encarga de guardar el estado actual de la ALU (Unidad Aritmética Lógica), de acá podemos extraer información que nos puede ayudar en combinación de ciertos comandos, también podemos utilizarlo para que los registro A, X e Y puedan cambiar entre modo de 8 bits y 16 bits. El registro P tiene los siguientes bits o flags:
Flag | Valor | Descripción |
---|---|---|
N | #$80 | Si el flag es 1, la última operación dio un resultado con valor negativo. Los números negativos van desde -1 (0xFF) a -128 (0x80), si el valor es 0 entonces el último resultado fue positivo. |
V | #$40 | Si el flag es 1, significa que la última operación de forma errónea paso de un número negativo a uno positivo o viceversa, se explicará a más profundidad cuando hablemos de operaciones aritméticas. |
M | #$20 | Si el flag es 1, significa que el registro A es de 8 bits, sino es de 16 bits. |
X | #$10 | Si el flag es 1, significa que el registro X e Y son de 8 bits, sino son de 16 bits. |
D | #$08 | Si el flag es 1, significa que las operaciones aritméticas se harán como si fuera una operación con números en base 10, sino usa base 16. |
I | #$04 | Si el flag es 1, significa que el hardware está en una Interrupción, esto es algo avanzado y se explicará en otro capitulo a futuro. |
Z | #$02 | Si el flag es 1, significa que el último resultado de una operación dio el valor cero. |
C | #$01 | Si el flag es 1, significa que la última operación superó el límite que el registro podía abarcar, esto se explicará a más profundidad cuando hablemos de operaciones aritméticas. |
Estos serían todos los flags que necesitaremos del registro P. Existen 2 comandos especiales para escribir valores en el registro P.
- REP: Recibe como parámetro una constante, pone en 0, los bits que en el parámetro del comando sean 1.
- SEP: Recibe como parámetro una constante, pone en 1, los bits que en el parámetro del comando sean 1.
Ejemplos de uso:
- REP #$20: Pone en cero el flag M, por lo tanto haría que el registro A sea de 16 bits.
- SEP #$20: Pone en 1 el flag M, por lo tanto haría que el registro A sea de 8 bits.
Con los comandos REP y SEP se puede alterar cualquiera de los flags, aunque también existen otros comandos alternativos que pueden hacer lo mismo para algunos flags específicos. Estos son:
- CLV: Pone el Flag V y lo pone en 0.
- SEC y CLC: SEC pone el Flag C en 1 y CLC lo pone en 0.
- SED y CLD: SED pone el Flag D en 1 y CLD lo pone en 0.
- SEI y CLI: SEI pone el Flag I en 1 y CLI lo pone en 0.
Estos comandos en general no tienen ninguna ventaja en términos de performance con REP y SEP, aunque pueden servir debido a que usan 1 byte en vez de 2, también es más fácil memorizarlos en general.
¿Qué sucede cuando un registro cambia de 16 a 8 bits?
En el caso del registro A, el High byte del registro queda en el mismo valor que cuando estaba en 16 bits y luego todas las operaciones que hagas serán usando el Low Byte del registro A. Ejemplo si A fuera FC08, al pasarlo a 8 bits, seguiría teniendo el valor FC08, y luego si hicieras algo como LDA #$02, solo cambiaría el Low byte, quedando FC02. En el caso del registro X e Y, el High byte pasa a 00 cuando se pasa a 8 bits y luego cualquier operación que se haga afecta solo al Low Byte. Ejemplo, si X fuera FC08, al pasarlo a 8 bits sería 0008, luego si hicieras algo como LDX #$02, solo cambiaría el Low byte, quedando 0002.
Operaciones Aritméticas
Las operaciones aritméticas nos permiten en la SNES hacer sumas y restas, el Super Nintendo no tiene un comando para hacer multiplicaciones o divisiones, sino que usa un sistema aparte para realizar estas operaciones y que se verán a mayor profundidad en otro capítulo. Los comandos aritméticos son:
- ADC: Suma el valor del registro A con el parámetro del comando, si el flag C es 1, se le añade 1 a la suma.
- SBC: Resta el valor del registro A con el parámetro del comando, si el flag C es 0, se le resta 1 más a la resta.
- INC: Incrementa lo que está en el parámetro en 1, INC A incrementa el registro A en 1.
- INX, INY: Incrementa en 1 el registro X (INX) o el registro Y (INY).
- DEC: Decrementa lo que está en el parámetro en 1, INC A Decrementa el registro A en 1.
- DEX, DEY: Decrementa en 1 el registro X (INX) o el registro Y (INY).
Todos estos comandos pueden afectar al flag N si es que el resultado es negativo o no y al flag Z si el resultado es cero o no. El comando ADC y SBC pueden afectar al flag V y C, la forma en que afectan es:
- El flag V se activa cuando una operación (suma o resta) que debiera dar un valor negativo, da un valor positivo, o debiera dar un valor positivo, pero da un valor negativo. Por ejemplo:
- Si al sumar 2 números positivos da un valor negativo, por ejemplo si sumas 0x7F con 0x02, ambos son positivos, pero el resultado es 0x81 que es -127, en este caso se activa el flag V.
- Si al sumar 2 números negativos se pasa da un valor positivo, por ejemplo si sumas 0x80 con 0xFE, ambos son negativos, pero el resultado da 0x7E que es positivo, en este caso se activa el flag V.
- El flag C se activa cuando el resultado de una suma da un valor mayor al límite del registro o cuando al hacer una resta, el valor que se resta es mayor que el valor en el registro A. Por ejemplo:
- A de 8 bits, se suma 0xFE con 0x02, esto da 0x100, pero esto no cabe en 8 bits, así que A seria 0x00 y el flag C se activa.
- A de 8 bits, a 0x03 se le resta 0x06, esto da 0xFD y activará el flag C.
Para la operación ADC lo recomendable sería antes del comando usar el comando CLC para que no añada 1 extra, en el caso de SBC, lo ideal sería antes del comando usar el comando SEC para que no reste 1 extra. Ejemplos
LDA #$01 CLC ADC #$02 ;1 + 2 = 3 LDA #$02 SEC SBC #$01 ;2 - 1 = 1 LDA #$01 SEC ADC #$02 ;1 + 2 + 1 = 4 LDA #$02 CLC SBC #$01 ;2 - 1 - 1 = 0
En algunos casos especificos se puede evitar el SEC y SBC. Sobretodo cuando operas números de 16 bits pero con registros de 8 bits.
LDA !LowByte ;Para este ejemplo !LowByte es 0xFF CLC ADC #$01 STA !Result LDA !HighByte ;Para este ejemplo !HighByte es 0x01 ADC #$00 STA !Result+1
En el ejemplo anterior, se le suma 1 a !LowByte y se guarda en !Result, sin embargo !LowByte es 0xFF así que el resultado debiera dar 0x100, pero A es de 8 bits y no puede almacenar ese número, sin embargo al hacer la suma el flag C se activa y luego podemos sumarle 0 al !HighByte para que de el resultado correcto, al final !Result como variable de 16 bits, tendrá el valor 0x200. Nótese que si la primera suma no hubiera activado el flag C, la segunda suma no haría nada, por lo tanto esto sirve para operar números que tienen un tamaño mayor que el tamaño del registro.
Operaciones Lógicas
Existen comandos llamados Bitwise, todos estos funcionan aplicando operaciones lógicas bit a bit. Los comandos son los siguientes:
- AND: Aplica al registro A la operación AND bit a bit con el parámetro del comando.
- ORA: Aplica al registro A la operación OR bit a bit con el parámetro del comando.
- EOR: Aplica al registro A la operación Exclusive-OR bit a bit con el parámetro del comando.
Todos los comandos lógicos pueden afectar al flag N y Z. Las tablas de verdad de estos comandos son:
V1 | V2 | V1 AND V2 | V1 ORA V2 | V1 EOR V2 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
Veamos algunos ejemplos:
LDA #$13 AND #$15
En este caso A tiene el valor 0x13 que en binario es 00010011b y luego se le aplica AND #$15 que en binario es 00010101b, aplicamos la operación AND bit a bit.
00010011b -> V1 00010101b -> V2 00010001b -> V1 AND V2
Ahora reemplazando el AND por un ORA:
00010011b -> V1 00010101b -> V2 00010111b -> V1 ORA V2
Ahora reemplazando el ORA por un EOR:
00010011b -> V1 00010101b -> V2 00000110b -> V1 EOR V2
En la práctica estos comandos tienen múltiples usos, aunque los más comunes son:
- AND:
- Poner en 0 uno o más bits del registro A.
- Saber si un bit específico está en 1 o no. (Se verá a mayor profundidad en el capítulo sobre saltos condicionales)
- ORA:
- Poner en 1 uno o más bits del registro A.
- Saber si múltiples valores tienen un valor distinto de 0. (Se verá a mayor profundidad en el capítulo sobre saltos condicionales)
- EOR:
- Cambiar el valor de uno o más bits del registro A.
- Saber si un bit específico es igual o distinto a un bit específico del parámetro del comando. (Se verá a mayor profundidad en el capítulo sobre saltos condicionales)
Operaciones Shift y Rotate
Existen algunas operaciones que no es claro catalogarlas como lógicas o aritméticas, sin embargo en algunos casos pueden ser utilizados con estos propósitos. Estos comandos son:
- ASL: Mueve todos los bits del registro A hacia la izquierda. Ejemplo si tienes 0x02, pasaría a ser 0x04, normalmente se usa para multiplicar por 2, ya que mover los bits a la izquierda en base binaria es multiplicar por 2. Si el valor del registro A es negativo, después de usar este comando, el flag C queda activo.
- LSR: Mueve todos los bits del registro A hacia la derecha. Ejemplo si tienes 0x02, pasaría a ser 0x01, normalmente se usa para dividir por 2, ya que mover los bits hacia la derecha en base binaria es dividir por 2. Si el valor del registro A es impar, el flag C queda activo.
- ROL: Similar a ASL pero los bits que estaban prendidos, rotan hacia los bits inferiores. Se explicará en una tabla más adelante.
- ROR: Similar a LSR pero los bits que estaban prendidos, rotan hacia los bits superiores. Se explicará en una tabla más adelante.
Esta tabla muestra cómo varía el valor 0x01 con el flag C en 0 al aplicar ASL varias veces
Veces | Valor | Flag C | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
---|---|---|---|---|---|---|---|---|---|---|
0 | #$01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
1 | #$02 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
2 | #$04 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
3 | #$08 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | #$10 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
5 | #$20 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
6 | #$40 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
7 | #$80 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
8 | #$00 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
9 | #$00 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Esta tabla muestra cómo varía el valor 0x80 con el flag C en 0 al aplicar LSR varias veces
Veces | Valor | Flag C | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
---|---|---|---|---|---|---|---|---|---|---|
0 | #$80 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | #$40 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | #$20 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
3 | #$10 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
4 | #$08 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
5 | #$04 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
6 | #$02 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
7 | #$01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
8 | #$00 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
9 | #$00 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Esta tabla muestra cómo varía el valor 0x01 con el flag C en 0 al aplicar ROL varias veces
Veces | Valor | Flag C | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
---|---|---|---|---|---|---|---|---|---|---|
0 | #$01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
1 | #$02 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
2 | #$04 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
3 | #$08 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | #$10 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
5 | #$20 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
6 | #$40 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
7 | #$80 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
8 | #$00 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
9 | #$01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Esta tabla muestra cómo varía el valor 0x80 con el flag C en 0 al aplicar ROR varias veces
Veces | Valor | Flag C | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
---|---|---|---|---|---|---|---|---|---|---|
0 | #$80 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | #$40 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | #$20 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
3 | #$10 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
4 | #$08 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
5 | #$04 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
6 | #$02 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
7 | #$01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
8 | #$00 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
9 | #$80 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |