[Prologo]
INTRODUCCIÓN AL CÓDIGO MÁQUINA

Prácticamente cualquier usuario de Spectrum ha tenido alguna vez contacto con el código máquina. En este lenguage están escritos los mejores programas comerciales, y algunas veces lo hemos utilizado en las páginas de nuestra revista.

Los menos experimentados se preguntan qué es eso del código máquina, qué tiene que ver con sentencias como DATA, USR, RANDOMIZE, etc. y sobre todo, para qué sirve. A lo largo de este curso, vamos a dar respuesta a estas y otras preguntas.

El código máquina no es sólo otro lenguaje más de programación; se trata de hablarle al ordenador directamente en el lenguaje que él entiende. De esta forma, no estamos sujetos a las restricciones de Basic, que son muchas, y tenemos un dominio completo sobre nuestra máquina.

Normalmente, nadie programa directamente en código máquina, este lenguaje está compuesto únicamente por sucesiones de números y no existe nadie capaz de recordarlos todos. Por esto, se suelen escribir los programas en Assembler y después, traducirlos a código máquina. Esta última tarea, se conoce por el nombre de "ensamblado", y habitualmente, se realiza con la ayuda de un programa llamado "Ensamblador".

En los cuatro primeros capítulos del curso, se estudian algunas nociones previas que serán necesarias en los capítulos posteriores, por lo que no es recomendable pasar a estudiar un capítulo sin haber comprendido totalmente el anterior.

Es muy probable que el gran volumen de lectores nos haga imposible mantener una correspondencia personalizada, pero aún así, nos agradaría que quienes sigan el curso nos escriban contándonos sus progresos o las dificultades que encuentran. Estas cartas nos permitirán ir adaptando las explicaciones a un nivel que satisfaga a todos y permita que nadie se quede "descolgado".

En este mismo capítulo incluimos dos rutinas de utilidad escritas en código máquina que permiten hacer "Scroll" lateral de la pantalla a derecha e izquierda y pixel a pixel.

Las dos rutinas se han ensamblado una a continuación de la otra y son reubicables, es decir, se pueden colocar en cualquier parte de la memoria. Nosotros las hemos ensamblado a partir de la dirección 55000, pero quien disponga de sólo 16K, puede colocarlas en cualquier otra dirección, haciendo unas pequeñas modificaciones en el programa cargador, que explicaremos un poco más adelante.

En la FIGURA 1, reproducimos fotográficamente el listado en Assembler de las dos rutinas. No se preocupe el lector si le suena a "chino", un listado en Assembler no es más dificil de entender que uno en Basic, cuando se conoce. Al final del curso, más de uno será capaz de mejorarlo.

[Figura 1]

El PROGRAMA 1 sirve, lógicamente, para cargar estas rutinas en memoria sin necesidad de Ensamblador. De esta forma, no es necesario saber código máquina para usarlas. Una vez estén en memoria basta teclear:

RANDOMIZE USR 55000

Para que la pantalla se desplace un pixel a la izquierda, y:

RANDOMIZE USR 55030

Para que lo haga hacia la derecha. Para salvar el código en cinta, puede utilizar:

SAVE "Scroll" CODE 55000,60

Y para cargarlo:

CLEAR 54999
LOAD "Scroll" CODE 55000

El PROGRAMA 1 incluye una demostración sobre la forma de utilizar estas rutinas. Quien esté interesado en usarlas en sus programas, puede mirar atentamente las líneas 240 y 250 que resultan suficientemente ilustrativas.

[Programa 1]

Para adaptar las rutinas a la versión de 16K, se deben realizar las siguientes modificaciones en el PROGRAMA 1:

50 CLEAR 31999: LET d=32000
240 IF INKEY$="q" THEN RANDOMIZE USR 32000
250 IF INKEY$="p" THEN RANDOMIZE USR 32030

En este caso, habrá que salvar las rutinas con:

SAVE "Scroll" CODE 32000,60

Y volverlas a cargar con:

CLEAR 31999
LOAD "Scroll" CODE 32000

Antes de seguir leyendo, le pedimos que cargue y ejecute el PROGRAMA 1. Ponga mucho cuidado para no equivocarse a partir de la línea 300, ya que los errores en código máquina suelen tener consecuencias desastrosas.

¿Ya ha ejecutado el programa? Asombroso ¿no? Tenemos pensado otro que hace el Scroll arriba y abajo, ya se lo contaremos.

Ahora vamos a intentar introducirnos en el estudio del código máquina partiendo desde la base más elemental, para que incluso quien no tenga ni la más remota idea de lo que es esto pueda sequirnos. Si no es su caso y es usted capaz de entender sin problemas las explicaciones de los cuatro primeros capítulos no es necesario que lea lo que sigue, aunque tal vez pueda aclararle algunos conceptos.

Manejando una calculadora

Suponemos que todos nuestros lectores han manejado alguna vez una calculadora de bolsillo. El conjunto formado por una calculadora de bolsillo, la persona que la maneja, un lápiz y un papel, pueden ser un símil bastante aproximado de lo que es un ordenador.

Imaginémonos a un amigo con una calculadora y un lápiz; esto equivale más o menos al microprocesador o CPU. Por otro lado está el papel, que equivale a la memoria. Nuestro amigo puede usar el papel para apuntar resultados o datos intermedios de los cálculos, pero nosotros podemos usarlo también, para apuntarle a él los cálculos que queremos que realice. De esta forma, el papel (la memoria) cumple una doble función, por un lado sirve para que el microprocesador (nuestro amigo) anote datos, y por otro lado, sirve para que nosotros le anotemos las instrucciones que tiene que seguir (el programa).

Nuestro amigo no tiene ni idea de como se maneja una calculadora, así que tendremos que decirle, una por otra, las teclas que tiene que pulsar. Podemos decirle: "pulsa la segunda tecla de la tercera fila" o simplemente: "pulsa (2,3)"; esto sería "código máquina". Pero también podemos decirle: "pulsa la tecla 5, luego la tecla «por» y luego la tecla 7"; esto sería "Assembler".

Vamos a "programar" en "Assembler" a nuestro amigo, para que nos calcule el cuadrado de 5 por 7 y nos escriba el resultado en un recuadro de la hoja de papel el que denominamos "archivo de presentación visual". La calculadora puede ser la representada en la FIGURA 2.

[Figura 2]

El programa podría quedar más o menos así:


10 Pulsa "AC"
20 Pulsa "5"
30 Pulsa "POR"
40 Pulsa "7"
50 Pulsa "="
60 Pulsa "CUADRADO"
70 Escribe Resultado
80 Fin

Este programa se lo anotamos en el papel, y le damos la orden de que lo ejecute. Al final, él nos escribe el resultado en el papel.

Podemos sacar aún más partido a nuestro ejemplo. Supongamos que nuestro amigo supiera manejar perfectamente la calculadora, en ese caso, nos bastaría con decirle: "Calcula el cuadrado de 5 por 7 y anota el resultado". En este caso, estaríamos usando un "lenguaje de alto nivel". El Basic es un lenguaje de alto nivel, y lo podemos usar gracias a que nuestro ordenador tiene un "intérprete", lo que hace que "sepa" manejar perfectamente el microprocesador.

Vamos a estudiar detenidamente el proceso. Primero pulsamos "AC", con lo que se borran los anteriores contenidos de la calculadora. A continuación, pulsamos la tecla "5", con lo que aparece en pantalla el número cinco. La pantalla de la calculadora es el registro, y lo que hemos hecho ha sido cargar este registro con el número cinco. Luego definimos la operación a realizar, y cargamos el registro con el segundo operando (siete). Al pulsar "=" se realiza la operación y el resultado aparece de nuevo en ese registro. Finalmente, al pulsar "CUADRADO", elevamos al cuadrado el contenido del registro, y el resultado nos vuelve a aparecer en el mismo.

La pantalla de una calculadora va acumulando los resultados de todas las operaciones que vamos realizando, por eso, podemos llamarla "registro ACUMULADOR". Todos los microprocesadores tienen un registro acumulador, pero a diferencia de las calculadoras, tiene también otros registros que nos pueden servir para diversos fines.

A pesar de tener muchos registros, no son suficientes para almacenar el enorme volumen de datos que maneja un ordenador, por ello se recurre a un sistema de almacenamiento externo que se denomina "memoria" y cumple la misma función que el papel de nuestro ejemplo, sirve para almacenar tanto programas, como datos.

El "papel" que utilizamos como memoria está "cuadriculado" y nuestro microprocesador no sólo tiene "lápiz", sino también "goma" por loque puede escribir y borrar en cualquiera de las cuadriculas; pero sólo borra una cuadrícula cuando tiene que escribir otro dato en ella. Las cuadrículas, a su vez, están numeradas, por lo que en cada caso, se puede acceder a una de ellas en concreto.

Existen más diferencias entre la calculadora y el microprocesador. Este último no realiza las mismas operaciones que una calculadora. Es cierto que puede sumar y restar, pero puede realizar también otro tipo de operaciones como incrementar un registro (sumarle 1), decrementarlo (restarle 1) rotarlo, y fundamentalmente, realizar lo que se denomina "operaciones lógicas" (AND, OR, NOT, EXOR). Además, nos informa continuamente de ciertas características del dato que contiene el acumulador, por ejemplo, nos dice si es cero, si es negativo, y otros cuya utilidad se irá viendo más adelante.

No obstante, la diferencia fundamental entre nuestro ejemplo y un verdadero microprocesador es que este último no trabaja en base 10 (decimal), sino en binario. Afortunadamente, no necesitamos trabajar siempre con números binarios (que son súmamente incómodos) y podremos utilizar números en base 16 (hexadecimales).

Es muy importante adquirir cierta soltura en el manejo de la numeración hexadecimal, por ello, hemos dedicado un capítulo entero a este tema. Con bastante frecuencia, tendremos que convertir números decimales a hexadecimales o viceversa. Para esto se pueden usar los métodos descritos en el citado capítulo, pero resulta bastante tedioso, así que hemos desarrollado un programa que hace ese trabajo por nosotros. Este programa se encuentra en la página 11 del curso (MICROHOBBY número 43). Existen también, calculadoras de bolsillo capaces de operar en estas bases, y representan una gran ayuda a la hora de programar en código máquina.

También hemos dedicado un capítulo a describir el microprocesador Z-80 con el mayor detalle posible, ya que su conocimiento es imprescindible para programarlo. Sería algo así como el "manual" de la calculadora.

Finalmente, y antes de empezar a estudiar las instrucciones, hemos dedicado un capítulo a describir la forma en la que se debe elaborar un programa, independientemente del lenguaje utilizado.

Algebra de Boole

Prácticamente todos los instrumentos matemáticos que se utilizan en la programación de un pequeño ordenador como el Spectrum, forman parte del bagaje cultural de cualquier persona medianamente formada. Excepto, quizá, el álgebra de Boole.

Tal vez por ser de aparición relativamente reciente, tal vez por su escasa utilidad práctica en la realidad habitual, el caso es que el álgebra de Boole no ha sido incluida en el programa de estudios hasta fecha reciente; y lo ha sido dentro de la asignatura de "matemáticas comunes" del C.O.U. desgraciadamente, una de las "Marías".

Programando en Basic, hemos hecho uso de algunos conceptos provenientes del álgebra de Boole; cuando utilizábamos en las sentencias IF...THEN, los operadores OR, AND y NOT para expresar conjunciones y disyunciones lógicas. Al programar en Assembler o código máquina, haremos un uso mucho más profundo y preciso de estos operadores, así como del operador EXOR que no se utiliza en Basic.

Es tan frecuente (y útil) utilizar operadores lógicos en Assembler, como lo puede ser utilizar la suma y la resta en una calculadora de bolsillo. Por ello, es imprescindible tener un cierto conocimiento del álgebra de Boole; que por otro lado, es sumamente sencilla de aprender.

Dado que éste no pretende ser un manual de matemática moderna, no entraremos a definir formalmente lo que constituye un álgebra de Boole. Bástenos saber que un álgebra de Boole se puede construir allá donde tengamos un conjunto de elementos que puedan tomar dos valores (en nuestro caso, "0" y "1") y definamos una relación de equivalencia ("ser igual a") y dos operaciones internas al conjunto, que cumplan una serie de propiedades, similares a las que cumplen la suma y el producto en el álgebra a la que estamos acostumbrados. El hecho de que las operaciones sean internas, quiere decir que al operar dos elementos del conjunto, lo que se obtiene es otro elemento que también pertenece al mismo. Lo que tiene por consecuencia, que siempre que hagamos operaciones lógicas entre "ceros" y "unos", obtendremos indefectiblemente, "ceros" y "unos".

Puesto que un circuito electrónico (y los ordenadores lo son) no pueden trabajar más que con "ceros"y "unos", el álgebra de Boole parece un instrumento especialmente adecuado a la Informática. Como se explica en el capítulo que trata de los sistemas de numeración, agrupamos los "unos" y "ceros" en secuencias de 8 o 16 para componer otros números; pero en definitiva, estaremos trabajando con "ceros" y "unos", y el juego de instrucciones del microprocesador nos permite aplicar operaciones lógicas entre el contenido de los registros.

A continuación, vamos a ver uno a uno los operadores lógicos de nuestro álgebra de Boole.

OPERADOR NOT

No se trata propiamente de un operador lógico pero podemos considerarlo como tal. El operador NOT se aplica sobre un solo elemento del conjunto, y lo convierte en su complementario. Es decir, si aplicamos NOT sobre un "0", obtenemos un "1"; y viceversa, si aplicamos NOT sobre un "1", obtenemos un "0". Su "Tabla de verdad" sería la siguiente:

NOT 0 = 1
NOT 1 = 0

Una "tabla de verdad" equivale, en álgebra de Boole, a la tabla de sumar o multiplicar. Se trata de una representación de todas las soluciones posibles que se pueden obtener con un operador determinado.

Si aplicamos el operador NOT al contenido de un registro, lo que obtenemos es el "complemento" de ese registro, es decir, cambiamos sus "unos" por "ceros" y sus "ceros" por "unos". A esta operación se la denomina "complementar un registro", y utilizamos para ello la instrucción CPL del microprocesador.

Veamos un ejemplo: Supongamos que el registro acumulador contiene el número:

0 1 1 0 1 0 1 0

Que se podría escribir como "6Ah" en hexadecimal (ver capítulo referente a los sistemas de numeración). Si lo complementamos, obtenemos:

1 0 0 1 0 1 0 1

Que podría escribirse como "95h" en hexadecimal. Hemos cambiado los "ceros" por "unos" y los "unos" por "ceros". El número "95h" es el complementario de "6Ah" porque si los sumamos, obtenemos "FFh", que a su vez, es el mayor número posible (todos son "unos").

De esta forma, sería posible construir una "tabla" para la operación NOT en hexadecimal. Esta tabla la hemos representado en la FIGURA 3. Se puede observar que si sumamos cualquier número, con el que resulta de aplicarle el operador NOT, obtenemos "Fh" por eso son "complementarios".

NOT
0 1 2 3 4 5 6 7 8 9 A B C D E F
 
F E D C B A 9 8 7 6 5 4 3 2 1 0
Fig. 3. Tabla hexadecimal de "NOT".

OPERADOR OR

Se trata de una de las operaciones que se usan para definir nuestra áñgebra de Boole, y es equivalente a la "suma" en el sentido de que satisface las mismas propiedades algebraicas.

Cuando operamos dos elementos de nuestro conjunto mediante este operador, obtenemos un "1" si al menos, uno de ellos es "1", o si lo son ambos; y obtenemos "0" en cualquier otro caso. La tabla de verdad del operador "OR" es la siguiente:

0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1

Veamos un ejemplo: Supongamos que el acumulador contiene el número:

0 1 0 0 0 1 1 0

Es decir, "46h". Y le hacemos un "OR" con el número:

1 1 1 0 0 0 1 1

Es decir, "E3h". El resultado sería el número:

1 1 1 0 0 1 1 1

Que se representa en hexadecimal como "E7h". Vemos que hemos puesto un "1" en los lugares donde había "1" en cualquiera de los dos números y "0" en los lugares donde ambos números tenían un "0". La tabla para este operador en hexadecimal se puede ver en la FIGURA 4.

OR
0 1 2 3 4 5 6 7 8 9 A B C D E F
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
0 1 2 3 4 5 6 7 8 9 A B C D E F
1 1 3 3 5 5 7 7 9 9 B B D D F F
2 3 2 3 6 7 6 7 A B A B E F E F
3 3 3 3 7 7 7 7 B B B B F F F F
4 5 6 7 4 5 6 7 C D E F C D E F
5 5 7 7 5 5 7 7 D D F F D D F F
6 7 6 7 6 7 6 7 E F E F E F E F
7 7 7 7 7 7 7 7 F F F F F F F F
8 9 A B C D E F 8 9 A B C D E F
9 9 B B D D F F 9 9 B B D D F F
A B A B E F E F A B A B E F E F
B B B B F F F F B B B B F F F F
C D E F C D E F C D E F C D E F
D D F F D D F F D D F F D D F F
E F E F E F E F E F E F E F E F
F F F F F F F F F F F F F F F F
Fig. 4. Tabla hexadecimal de "OR".

OPERADOR AND

En cierto sentido, se puede considerar que este operador es el opuesto al anterior. Equivale al producto, en cuanto a las propiedades algebraicas que satisface.

Cuando operamos dos elementos de nuestro conjunto ("unos" o "ceros") obtenemos un "1" solamente si ambos elementos son "1"; y un "0" en cualquier otro caso. La Tabla de verdad del operador AND es la siguiente:

0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1

Vamos a ver que ocurre si, con los números del ejemplo anterior, aplicamos la operación AND:

0 1 0 0 0 1 1 0

(46h)

AND

1 1 1 0 0 0 1 1

(E3h)

=

0 1 0 0 0 0 1 0

(42h)

Esta vez, hemos puesto un "1" sólo en los lugares donde ambos números tenían un "1", y hemos puesto "0" en todos los demás lugares. La tabla del operador "AND" en Hexadecimal, está representada en la FIGURA 5.

AND
0 1 2 3 4 5 6 7 8 9 A B C D E F
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
0 0 2 2 0 0 2 2 0 0 2 2 0 0 2 2
0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3
0 0 0 0 4 4 4 4 0 0 0 0 4 4 4 4
0 1 0 1 4 5 4 5 0 1 0 1 4 5 4 5
0 0 2 2 4 4 6 6 0 0 2 2 4 4 6 6
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
0 0 0 0 0 0 0 0 8 8 8 8 8 8 8 8
0 1 0 1 0 1 0 1 8 9 8 9 8 9 8 9
0 0 2 2 0 0 2 2 8 8 A A 8 8 A A
0 1 2 3 0 1 2 3 8 9 A B 8 9 A B
0 0 0 0 4 4 4 4 8 8 8 8 C C C C
0 1 0 1 4 5 4 5 8 9 8 9 C D C D
0 0 2 2 4 4 6 6 8 8 A A C C E E
0 1 2 3 4 5 6 7 8 9 A B C D E F
Fig. 5. Tabla hexadecimal de "AND".

OPERADOR EXOR

No se trata propiamente, de una operación del álgebra de Boole, pero es necesario describirlo dado el gran uso que se hace de él cuando se programa en Assembler. El operador "EXOR" es, en cierta forma, una mezcla de los operadores "AND" y "OR".

Cuando operamos dos elementos de nuestro conjunto mediante este operador, obtenemos un "1", sólo si uno de los dos elementos es "1"; y obtenemos un "0" tanto si ambos son "ceros", como si ambos son "unos". La Tabla de verdad del operador "EXOR" es la siguiente:

0 EXOR 0 = 0
0 EXOR 1 = 1
1 EXOR 0 = 1
1 EXOR 1 = 0

Apliquemos este operador a los números del ejemplo anterior:

0 1 0 0 0 1 1 0

(46h)

EXOR

1 1 1 0 0 0 1 1

(E3h)

=

1 0 1 0 0 1 0 1

(A5h)

Hemos puesto un "0" en los lugares donde ambos números eran iguales (dos "unos" o dos "ceros"), y un "1" donde eran distintos ("cero" y "uno" o "uno" y "cero").

La tabla del operador "EXOR" en hexadecimal, está representada en la FIGURA 6. Como curiosidad importante, cabe señalar que si se realiza una operación "EXOR" de un número consigo mismo, el resultado es siempre "cero"; lo cual es muy útil, ya que si se quiere cargar el número "cero" en el acumulador, se puede utilizar la instrucción "XOR A" ("EXOR" del acumulador consigo mismo) que ocupa la mitad de memoria y es el doble de rápida qu "LD A,0" (cargar el acumulador con cero).

XOR
0 1 2 3 4 5 6 7 8 9 A B C D E F
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
0 1 2 3 4 5 6 7 8 9 A B C D E F
1 0 3 2 5 4 7 6 9 8 B A D C F E
2 3 0 1 6 7 4 5 A B 8 9 E F C D
3 2 1 0 7 6 5 4 B A 9 8 F E D C
4 5 6 7 0 1 2 3 C D E F 8 9 A B
5 4 7 6 1 0 3 2 D C F E 9 8 B A
6 7 4 5 2 3 0 1 E F C D A B 8 9
7 6 5 4 3 2 1 0 F E D C B A 9 8
8 9 A B C D E F 0 1 2 3 4 5 6 7
9 8 B A D C F E 1 0 3 2 5 4 7 6
A B 8 9 E F C D 2 3 0 1 6 7 4 5
B A 9 8 F E D C 3 2 1 0 7 6 5 4
C D E F 8 9 A B 4 5 6 7 0 1 2 3
D C F E 9 8 B A 5 4 7 6 1 0 3 2
E F C D A B 8 9 6 7 4 5 2 3 0 1
F E D C B A 9 8 7 6 5 4 3 2 1 0
Fig. 6. Tabla hexadecimal de "EXOR".

De la misma forma, si se hace un "OR" o un "AND" de un número consigo mismo, éste no varía. En el programa de ejemplo de este capítulo, hemos usado la instrucción "AND A" ("AND" del acumulador consigo mismo) para poner a cero el indicador de acarreo sin que varíe el contenido del acumulador.

El empleo de operadores lógicos nos va a permitir movernos por tablas, calcular direcciones de memoria, poner "máscaras" a algunos bits, y un sinfín de utilidades que justifican la necesidad de adquirir el mayor dominio posible del álgebra de Boole.

[Ejercicios]