Commodore 64

El Commodore 64 fue una computadora de 8 bits lanzada en 1982 que gozó de gran popularidad. Este manual busca ser una referencia básica para poder programar juegos para esta máquina. Para ello el manual está dividido en tres documentos, empezando por el presente que contiene los aspectos de bajo nivel y del lenguaje ensamblador, seguido de un manual del lenguaje BASIC V2 y por último un mapa de la memoria del C64.

Especificaciones

Elemento Descripción
CPU MOS Technology 6510
- PAL: 0,9852486 MHz
- NTSC: 1,0227273 MHz
RAM 64 KB
ROM 20 KB dividido en 3 chips
- 8 KB BASIC V2 ($A000-$BFFF)
- 8 KB KERNAL ($E000-$FFFF)
- 4 KB FONTS ($D000-$DFFF)
Gráficos VIC-II (MOS 6567/6569)
- Resolución: 320x200
- Colores: 16
- Sprites: 8 de 24x21 (2 colores) o 12x21 (4 colores)
- Interrupciones del rasterizado
Sonido SID (MOS 6581)
- 3x Osciladores (8 octavas, 16-4.000 Hz)
- 4x Formas de onda (sierra, triángulo, cuadrada y ruido)
- Filtros: paso-bajo (LPF), paso-alto (HPF) y paso-banda (BPF)
- Envolvente (ADSR)
- Modulación por anillo (Ring Modulation)
E/S - 2x CIA 6526 (joystick, teclado, RS-232, GPIO)
- Bus en serie IEEE 488 (disqueteras e impresoras)
- Cintas de casete (datasette)
- Cartuchos de ROM
Disco Commodore 1541 (5¼ SS-SD, 170 KB)
- Formato: GCR
- Máximo de ficheros: 144
- Sectores en total: 683 (664 libres)
- Tamaño de sector: 256 bytes
- Tamaño total: 174.848 bytes
- Tamaño libre: 168.656 bytes (254 bytes/sector)
- Velocidad: 300-400 B/s (C64 DOS)

Para más información consulta la Wikipedia inglesa o la española. También existe una Wiki dedicada al C64 en inglés, donde poder consultar información sobre programar en la plataforma.

Herramientas

El emulador VICE tiene varias configuraciones para el teclado: modo simbólico y modo posicional. Para esta documentación asumimos que se está usando el modo simbólico.

Comandos útiles para trabajar

Si queremos compilar un programa con el CC65 usaremos:

cl65.exe -O -o game.prg -t c64 main.c

Para ejecutar luego el programa con VICE usaremos:

x64sc.exe game.prg

Si queremos generar un disquete virtual usaremos:

c1541 -format game,42 d64 GAME.D64 -attach GAME.D64 -write game.prg game

Lenguaje ensamblador

Las máquinas de 8 bits no son especialmente potentes, por ello es recomendable usar código máquina para desarrollar juegos, en lugar de usar BASIC. Esto implica una mayor dificultad y no disponer de algunas comodidades que sí tiene BASIC, como es el cálculo flotante u operadores de enteros de 16 bits, entre otros. Sin embargo, el aumento de velocidad, a la hora de ejecutar una misma aplicación escrita en BASIC o en ensamblador, es superlativo.

NOTA: Hay una diferencia entre lenguaje máquina y lenguaje ensamblador. El lenguaje máquina o código máquina, es una secuencia de bytes que la CPU puede interpretar. Mientras que el lenguaje ensamblador requiere ser compilado a código máquina, porque son cadenas de texto con las que un humano puede trabajar para programar.

Hola mundo

Para entender, por qué decimos que programar en ensamblador conlleva una mayor dificultad, veamos el “hola mundo” como ejemplo:

*=$0801 ; 10 SYS2064
        BYTE $0B,$08,$0A,$00,$9E,$32,$30,$36,$34,$00,$00,$00

*=$0810
main    jsr $E544 ; KERNAL: Limpiar terminal
        ldx #$00
@loop   lda Message,x
        beq @end
        jsr $E716 ; KERNAL: Poner carácter
        inx
        jmp @loop
@end    rts

Message  TEXT "hola mundo"
         BYTE 13,0

Primero, para poner un comentario se utiliza el punto y coma (;). Segundo, el compilador de ensamblador dispone de una serie de directivas, que permiten configurar diversos parámetros. En el ejemplo se usa la directiva *=, seguida de una dirección de memoria, que permite al programador definir la ubicación del código en la memoria.

Las dos primeras líneas son una directiva con un comentario. La dirección inicial es la $0801, que es la primera posición de memoria donde puede iniciarse un programa BASIC. A continuación, vemos un comentario que dice 10 SYS2064, que sería el programa BASIC que necesitamos para invocar nuestro programa máquina. Pero en lugar de escribirlo a mano, para ejecutar la aplicación, vemos que en la segunda línea hay una serie de bytes escritos en hexadecimal, que da la casualidad que es la codificación del programa BASIC del comentario anterior. De este modo, al cargar el programa e introducir el comando RUN, se ejecutará la instrucción SYS y por lo tanto nuestra aplicación.

El siguiente bloque empieza ubicando el programa en la dirección $0810 (2064). Cada línea del programa puede empezar por una etiqueta identificadora (main, @loop, @end o Message), seguido de una instrucción o una definición de datos. La primera instrucción llama a una subrutina del KERNAL para limpiar la pantalla. Después se inicializa a cero el registro X de la CPU, para iniciar un bucle donde se carga de forma indexada valores almacenados a partir de la dirección que representa Message. Es decir, toda etiqueta identificadora lo que representa es una dirección de memoria. Si el valor es un cero, se activará un flag dentro del registro de flags de la CPU, y podemos saltar a @end con la instrucción beq. Si no se realiza el salto, se invoca otra subrutina del KERNAL para pintar en pantalla un carácter. Por último, se incrementa el valor del registro X y se vuelve al principio del bucle. Cuando termina el bucle, con rts volveremos al programa BASIC.

El bloque final, define una cadena de texto en la posición Message. Salvando las distancias, esto sería como declarar una variable de programa en una región de la memoria. Después de la cadena tenemos dos bytes, el primero para realizar el salto de línea en la pantalla y el último un cero para finalizar la cadena.

NOTA: Para simplificar los ejemplos siguientes, se va a asumir que el inicio del fichero va a ser siempre el mismo cargador de BASIC del hola mundo y que el programa máquina se inicia en la dirección $0810.

Codificación numérica

Para una computadora todo son ceros y unos, pero es el programa que se está ejecutando el que da una semántica concreta a los valores con los que se están trabajando. Esto permite que podamos representar números, letras y otros tipos de información. Pero para lograrlo hace falta establecer una codificación binaria para representar dichos valores.

La forma natural que tenemos para trabajar con números es el sistema decimal, un sistema de numeración que utiliza como base el 10 para descomponer los números en relación a su posición:

123=1×102+2×101+3×100 123 = 1 \times 10^{2} + 2 \times 10^{1} + 3 \times 10^{0}

Mediante el uso de divisiones se puede transformar de una base a otra, pudiendo convertir un número decimal a binario (base 2), octal (base 8) o hexadecimal (base 16). El uso del sistema octal es menos común que el binario o el hexadecimal, pero es conocido su uso en sistemas operativos de la familia UNIX, por ejemplo.

Dec Bin Oct Hex Dec Bin Oct Hex
0 0000 0 0 8 1000 10 8
1 0001 1 1 9 1001 11 9
2 0010 2 2 10 1010 12 A
3 0011 3 3 11 1011 13 B
4 0100 4 4 12 1100 14 C
5 0101 5 5 13 1101 15 D
6 0110 6 6 14 1110 16 E
7 0111 7 7 15 1111 17 F

Como sólo tenemos dígitos para representar del cero al nueve, en bases superiores al 10, utilizamos letras para representar los valores siguientes, de ahí que los números hexadecimales usen el intervalo de letras A-F.

Lo común es que 1 byte son 8 bits y se pueden separar en dos nibbles, es decir, en dos mitades de 4 bits. Se puede representar cada mitad con un solo dígito hexadecimal, para mayor comodidad. Esta división también se utiliza para la codificación BCD, donde cada mitad puede tomar valores entre 0 y 9, de modo que un byte puede representar valores del 0 al 99. Aunque esto claramente hace que se desperdicie espacio en la memoria, en máquinas antiguas era habitual trabajar de esta forma para facilitar la conversión de número a texto en pantalla.

De momento podemos representar el conjunto de los números naturales, pero para poder utilizar números enteros necesitamos representar valores negativos. De las diferentes aproximaciones que existen, la mejor opción es el complemento a dos, que consiste en invertir cada bit y sumarle una unidad para obtener el valor negativo. Para que esto funcione necesitamos trabajar con un tamaño fijo de bits, que nos lleva a la siguiente terminología de tamaños:

Nombre Bits stdint.h CC65 (8/16 bits) C/C++ (32/64 bits)
BYTE 8 int8_t
uint8_t
char
unsigned char
char
unsigned char
WORD 16 int16_t
uint16_t
int
unsigned int
short
unsigned short
DWORD 32 int32_t
uint32_t
long
unsigned long
int
unsigned int
QWORD 64 int64_t
uint64_t
- long long
unsigned long long

En el C64 el ancho del bus de datos es un BYTE, mientras que el ancho de las direcciones de memoria es un WORD. También se usa un WORD como tamaño para los enteros en BASIC, por ejemplo. Teniendo esto en cuenta, los rangos numéricos que podemos representar son:

Tamaño Naturales Enteros
BYTE 0 ⋯ 256 -128 ⋯ 127
WORD 0 ⋯ 65.535 -32.768 ⋯ 32.767
DWORD 0 ⋯ 4.294.967.295 -2.147.483.648 ⋯ 2.147.483.647

Veamos ahora un ejemplo de cómo se codifican los números negativos, con un tamaño de 4 bits para limitar la cantidad de datos:

Binario Naturales Enteros Binario Naturales Enteros
0000 0 0 1000 8 -8
0001 1 1 1001 9 -7
0010 2 2 1010 10 -6
0011 3 3 1011 11 -5
0100 4 4 1100 12 -4
0101 5 5 1101 13 -3
0110 6 6 1110 14 -2
0111 7 7 1111 15 -1

Por ejemplo, si cogemos el número 2, en binario 0010, y lo pasamos a negativo, primero invertimos los bits a 1101, entonces le sumamos 1 y obtenemos 1110. Una ventaja de esta codificación es que podemos usar la suma binaria para sumar números enteros, por ejemplo -2 + 4 sería sumar 1110 y 0100, que daría como resultado 0010 que es 2.

NOTA: Aunque compiladores como el CC65 nos dejen usar números de 32 bits, es recomendable evitarlo en la medida de lo posible, ya que la CPU sólo dispone de operaciones aritméticas para números de 8 bits, por lo que los cálculos se hacen vía software. También se hace por software las operaciones con números de 16 bits, pero su uso se hace imperativo al querer manipular punteros a la memoria.

También podemos utilizar codificaciones para representar números reales, como hace BASIC con los números de coma flotante. Pero dada su complejidad, y su coste computacional, no son recomendables para hacer juegos en plataformas de 8 bits.

Etiquetas, literales y variables

Cada ensamblador tiene algunas peculiaridades propias aunque compartan la plataforma de destino. Por ello nos centramos en el que usa el CBM prg Studio, frente al que usa el CC65.

Como se explicó en el hola mundo, una línea de programa puede empezar con una etiqueta identificadora. Las etiquetas lo que hacen es asociar a un nombre identificador una dirección de memoria, para hacer más fácil al programador el utilizar ciertas secciones del programa. Los nombres de las etiquetas se definen como los nombres de variables en C, pero hay un par de modificadores que se pueden incorporar a estos nombres:

Los valores literales que dispone el lenguaje son:

Tipo Símbolo Ejemplo
Decimal 65
Hexadecimal $ $41
Binario % %01000001
Octal @ 101
PETSCII " "a"
Cod. Pantalla ' 'a'

Los cuatro primeros ejemplos son el número 65 en sus diferentes representaciones de base. Los dos últimos ejemplos son el carácter A, que en códigos PETSCII es el 65 y en códigos de pantalla el 1. La codificación PETSCII es un equivalente al ASCII que la empresa Commodore tenía para sus máquinas. Las rutinas del KERNAL trabajan con esta codificación internamente. Sin embargo, el buffer de pantalla del modo texto trabaja con índices al mapa de caracteres. Por ello, dependiendo de cómo se trabaje, se tiene que decidir usar una codificación o la otra.

Para definir una variable de programa tenemos que usar una etiqueta y una definición de datos, como muestran los siguientes ejemplos:

var1 BYTE 123
var2 WORD 321
var3 TEXT "cadena petscii",0
var4 TEXT 'cadena pantalla',0

Hay más opciones de tipos de datos en la ayuda del compilador, pero esencialmente estos son los más básicos que disponemos. Para las cadenas de texto, se ha optado por añadir un cero al final como terminación, cosa que permite el compilador.

También se pueden declarar variables para el compilador, que podemos utilizar como valores constantes para evitar los famosos números mágicos al programar:

CLRSCR = $E544

*=$0810
main
        jsr CLRSCR
        rts

Estas variables permiten usar diferentes operadores para trabajar con ellas y que el compilador calcule su valor final, pero este uso no es tan común. Sin embargo, hay dos operadores muy importantes que podemos utilizar con literales, etiquetas o variables de compilador, que son:

Porque recordemos que el ancho de palabra de los datos es de 8 bits, por lo tanto necesitaremos estos operadores para trabajar con aquellas expresiones que den un valor de 16 bits. También existen operadores lógicos (and, or, not, xor, <<, >>) y aritméticos (+, -, *, /) que se pueden utilizar con diferentes expresiones para que el compilador calcule el valor final. El mecanismo sería similar al de una plantilla en C++, ya que no es código que se ejecuta con el programa, sino que se ejecuta durante la compilación.

Macros

Se pueden definir macros para unificar aquellas secciones de código que sean duplicados y tener una relativa sensación de modularidad. Para definir una macro hace falta un nombre identificador, de forma similar a las etiquetas, y añadiendo $ al final se puede hacer la macro global al proyecto. Por ejemplo:

CLRSCR = $E544 ; KERNAL: Limpiar terminal
PUTCHR = $E716 ; KERNAL: Poner carácter

defm WriteLine
        ldx #$00
@loop   lda /1,x
        beq @end
        jsr PUTCHR
        inx
        jmp @loop
@end    lda #13
        jsr PUTCHR
endm

*=$0810
main
        jsr CLRSCR
        WriteLine STRING1
        WriteLine STRING2
        rts

STRING1 TEXT "1. hola mundo",0
STRING2 TEXT "2. adios mundo",0

Con las palabras claves defm y endm definimos la macro, poniéndole como nombre WriteLine en el ejemplo. Si queremos poner etiquetas dentro de una macro, estas tendrán que ser etiquetas baratas. Luego con /1 se hace referencia al primer parámetro y podemos tener un máximo de 20. Si no se indica un valor para el parámetro, se asume por defecto el valor 0. Otro detalle es que no se puede llamar a una macro dentro de otra, lo cual puede dificultad su modularidad con operaciones complejas.

Registros de la CPU

Estos son los registros internos de la CPU:

Nombre Tamaño Descripción
A 8 bits Registro acumulador.
X 8 bits Registro X.
Y 8 bits Registro Y.
P 8 bits Registro de estado.
S 8 bits Puntero de la pila.
PC 16 bits Contador de programa.
DDR 8 bits Registro de Dirección de Datos.
Accesible en la dirección: $0000
IOP 8 bits Puerto de Entrada/Salida.
Accesible en la dirección: $0001

Podemos acceder y manipular los registros A, X e Y directamente con las instrucciones de la CPU. La mayor parte de las operaciones aritméticas y lógicas se realizan contra el acumulador, que se denomina así ya que se carga un valor y se manipula con un segundo valor extraído de memoria para hacer la operación, guardando el resultado en el registro, sobrescribiendo el valor previo. Los registros X e Y se utilizan para temas auxiliares como algunos de los modos de direccionamiento a la hora de acceder a la memoria.

El registro de estado de la CPU sirve para poder consultar si se han producido algunos eventos o para modificar el comportamiento de algunas instrucciones. Estos son los flags que hay en el registro:

Bit Flag Nombre 0 1
0 C Acarreo No
1 Z Cero No es cero Cero
2 I IRQ Activadas Desactivadas
3 D Modo BCD Deshabilitado Habilitado
4 B BREAK Hardware Software
5 - - - -
6 V Desbordamiento No
7 N Negativo Positivo Negativo

Al ejecutar algunas operaciones se pueden activar los flags:

Luego para activar o desactivar las interrupciones de tipo IRQ se usa el flag I. Si está activado el flag I y se produce una interrupción, con el flag B sabremos si su origen es hardware o software. Por último, el flag D activa el modo BCD para las operaciones aritméticas.

El contador de programa indica a la CPU la dirección de la siguiente instrucción de programa que ejecutar. Este registro se puede manipular mediante instrucciones de salto durante la ejecución.

El puntero de la pila apunta a la primera posición libre dentro de la pila de programa, que está en la página de memoria $01 y por ello sólo necesitamos 8 bits para indicar el byte bajo de la dirección final. La pila de programa es un segmento de memoria reservado para guardar información sobre el estado de la ejecución cuando se tiene que llamar a una rutina, para rescatar los valores guardados al terminar la rutina llamada. En el C64 la pila empieza apuntando a la posición $FF y va descendiendo hasta la $00. Además de las instrucciones para invocar una rutina y volver de ella, existen otras que nos permiten manipular la pila.

El registro DDR sirve para configurar qué bits son de escritura y/o lectura en el registro IOP, que es un registro para configurar el acceso a la memoria y consultar información sobre el datasette.

Modos de direccionamiento

Los modos de direccionamiento indican cómo se accede a los datos:

Modo Forma Descripción
Implícito Con la propia instrucción se deduce dónde están los datos sobre los que se van a operar.
Acumulador A Es una forma de modo implícito que opera sobre el registro acumulador de la CPU.
Inmediato #VAL El valor del operando acompaña a la instrucción y tiene un byte de tamaño.
Absoluto ABS La instrucción está acompañada por la dirección de memoria sobre la que operar. Primero se encuentra el byte menos significativo (LSB) y después el más significativo (MSB), porque la arquitectura del 6510 es de tipo Little-Endian.
Indexado ABS,X
ABS,Y
Es un modo absoluto que suma a la dirección de memoria el valor del registro X/Y.
Indirecto (ABS) La instrucción está acompañada por la dirección de memoria que contiene un puntero a la dirección final sobre la que operar.
Página Cero ZP Es un modo absoluto pero trabajando con la página cero, por ello sólo necesita un byte para la dirección de memoria.
Indexado Pág. Cero ZP,X
ZP,Y
Es un modo absoluto sobre la página cero que suma a la dirección de memoria el valor del registro X/Y.
Indexado Indirecto (ZP,X) Es un modo indirecto al que, antes de obtener la dirección almacenada en el puntero, se le suma el valor del registro X para calcular la dirección del puntero. También se puede entender como una tabla de punteros a valores.
Indirecto Indexado (ZP),Y Es un modo indirecto al que, después de obtener la dirección almacenada en el puntero, se le suma el valor del registro Y para calcular la dirección final. También se puede entender como un puntero a una tabla de valores.
Relativo REL La instrucción está acompañada por un valor de desplazamiento relativo a la posición actual de memoria. Como el tamaño de valor es de un byte, sólo se pueden indicar rangos entre -128 y 127.

NOTA: Con el modo indirecto no se debe usar como dirección el último byte de la página, por ejemplo $C0FF, pues el byte alto de la dirección no lo consultaría en $C100, sino que lo hace en $C000. Esto ocurre porque el acarreo del incremento no es tenido en cuenta.

Instrucciones de datos

El primer grupo de instrucciones nos permite mover bytes entre la memoria y los registros y viceversa:

Ins. Flags Descripción Ejemplos
LDA
LDX
LDY
N Z Memoria → Registro LDA #$44
LDA $44
LDA $44,X
LDA $4400
LDA $4400,X
LDA $4400,Y
LDA ($44,X)
LDA ($44),Y
LDX #$44
LDX $44
LDX $44,Y
LDX $4400
LDX $4400,Y
LDY #$44
LDY $44
LDY $44,X
LDY $4400
LDY $4400,X
STA
STX
STY
- Registro → Memoria STA $44
STA $44,X
STA $4400
STA $4400,X
STA $4400,Y
STA ($44,X)
STA ($44),Y
STX $44
STX $44,Y
STX $4400
STY $44
STY $44,X
STY $4400

Hay que tener cuidado cuando se realice el indexado a la página cero, porque el acarreo de la suma no afecta al byte más significativo de la dirección de memoria final. El resultado de esta situación es como estar dando “vueltas en círculos”.

También podemos mover bytes entre registros:

Ins. Flags Descripción
TAX N Z A → X
TAY N Z A → X
TXA N Z X → A
TYA N Z Y → A

El registro P de la CPU, que nos indica el estado de los flags, se puede modificar con:

Ins. Flags Descripción
CLC C=0 Borra el flag de carry.
SEC C=1 Activa el flag de carry.
CLI I=0 Desactiva las interrupciones IRQ.
SEI I=1 Activa las interrupciones IRQ.
CLD D=0 Desactiva el modo BCD.
SED D=1 Activa el modo BCD.
CLV V=0 Borra el flag de overflow.

Instrucciones aritméticas

Este segundo grupo de instrucciones nos permite realizar operaciones matemáticas:

Ins. Flags Descripción Ejemplos
ADC N V Z C A += Memoria ADC #$44
ADC $44
ADC $44,X
ADC $4400
ADC $4400,X
ADC $4400,Y
ADC ($44,X)
ADC ($44),Y
SBC N V Z C A -= Memoria SBC #$44
SBC $44
SBC $44,X
SBC $4400
SBC $4400,X
SBC $4400,Y
SBC ($44,X)
SBC ($44),Y

Con ADC se realizan las sumas y con SBC las restas de 8 bits. Si queremos multiplicaciones y divisiones, o si queremos operaciones con operandos de 16 bits, hay que implementarlas vía software. Dependiendo del estado del flag D, se realizarán las operaciones en modo BCD o no. Hay que tener en cuenta que si el flag de carry está activado, se sumará o restará al valor del acumulador. Esto está pensado para realizar operaciones con operandos de varios bytes de tamaño, por lo tanto hay que borrar el flag de carry cada vez que se quiera iniciar una nueva operación matemática, para evitar la intromisión del acarreo de alguna operación previa.

NOTA: El flag de desbordamiento u overflow se utiliza para controlar operaciones de sumas y restas con enteros. Por lo tanto, si estamos sumando dos números y el resultado es superior a 127, se se producirá un desbordamiento. También ocurre lo mismo si estamos restando dos números (o sumando dos números negativos) y el resultado es inferior a -128.

El siguiente bloque de instrucciones sirve para incrementar o decrementar un valor en memoria o un registro:

Ins. Flags Descripción Ejemplos
INC N Z Memoria += 1 INC $44
INC $44,X
INC $4400
INC $4400,X
DEC N Z Memoria -= 1 DEC $44
DEC $44,X
DEC $4400
DEC $4400,X
INX N Z X += 1 INX
INY N Z Y += 1 INY
DEX N Z X -= 1 DEX
DEY N Z Y -= 1 DEY

Es habitual tener que comparar valores entre sí y para ello tenemos:

Ins. Flags Descripción Ejemplos
CMP N Z C A - Memoria CMP #$44
CMP $44
CMP $44,X
CMP $4400
CMP $4400,X
CMP $4400,Y
CMP ($44,X)
CMP ($44),Y
CPX N Z C X - Memoria CPX #$44
CPX $44
CPX $4400
CPY N Z C Y - Memoria CPY #$44
CPY $44
CPY $4400

Se toma el valor del registro, se le resta el valor obtenido desde la memoria. El resultado modifica el estado de los flags de modo que:

Naturales Z C
Reg. = Mem. 1 1
Reg. < Mem. 0 0
Reg. > Mem. 0 1

Si el resultado de la resta da un número entre 128 y 255, el flag N se activará, de lo contrario valdrá cero. Esto hace que para comparar números enteros haga falta algunos pasos más, para comparar primero los signos de cada operando y si coinciden ejecutar CMP para comprobar el flag C.

Este bloque de instrucciones nos permite realizar operaciones lógicas a nivel de bit:

Ins. Flags Descripción Ejemplos
AND N Z Conjunción lógica: A &= Mem AND #$44
AND $44
AND $44,X
AND $4400
AND $4400,X
AND $4400,Y
AND ($44,X)
AND ($44),Y
ORA N Z Disyunción lógica: A |= Mem ORA #$44
ORA $44
ORA $44,X
ORA $4400
ORA $4400,X
ORA $4400,Y
ORA ($44,X)
ORA ($44),Y
EOR N Z Disyunción exclusiva: A ^= Mem EOR #$44
EOR $44
EOR $44,X
EOR $4400
EOR $4400,X
EOR $4400,Y
EOR ($44,X)
EOR ($44),Y

Veamos las tablas de verdad de cada operador lógico para entenderlos mejor:

A B A and B A or B A xor B A xor 1 B xor 1
1 1 1 1 0 0 0
0 1 0 1 1 1 0
1 0 0 1 1 0 1
0 0 0 0 0 1 1

Como se puede observar, la negación lógica se puede alcanzar mediante el uso del operador xor, de modo que tenemos:

not A=A xor 1 \mathit{not}\ A = A\ \mathit{xor}\ 1

Otras operaciones a nivel de bit son los operadores de desplazamiento y rotación de bits:

Ins. Flags Descripción Ejemplos
ASL N Z C Desplazamiento hacia la izquierda:
C = B7; Bi = Bi-1; B0 = 0
ASL A
ASL $44
ASL $44,X
ASL $4400
ASL $4400,X
LSR N=0 Z C Desplazamiento hacia la derecha:
C = B0; Bi = Bi+1; B7 = 0
LSR A
LSR $44
LSR $44,X
LSR $4400
LSR $4400,X
ROL N Z C Rotación hacia la izquierda:
C = B7; Bi = Bi-1; B0 = C
ROL A
ROL $44
ROL $44,X
ROL $4400
ROL $4400,X
ROR N Z C Rotación hacia la derecha:
C = B0; Bi = Bi+1; B7 = C
ROR A
ROR $44
ROR $44,X
ROR $4400
ROR $4400,X

Con el desplazamiento se van moviendo los bits en una dirección para introducir un cero por un lado y sacar al flag de carry el bit que ha salido fuera. Mientras que la rotación, el bit que es empujado fuera se mete en el carry y se vuelve a introducir en el otro extremo.

Instrucciones de la pila

Para manejar la pila de programa tenemos las siguientes instrucciones:

Ins. Flags Descripción Ejemplos
PHA - Meter A PHA
PHP - Meter P PHP
PLA N Z Sacar A PLA
PLP N V B D I Z C Sacar P PLP
TSX N Z S → X TSX
TXS - X → S TXS

Las instrucciones PHA y PHP meten el contenido del registro en la pila, mientras que PLA y PLP sacan un byte de la pila y lo guardan en los registros. Si queremos consultar o modificar el puntero actual a la pila, tenemos que usar las instrucciones TSX y TXS.

Instrucciones de salto

Mediante los flags del registro P podemos saltar condicionalmente con las siguientes instrucciones:

Ins. Flags Descripción Ejemplos
BCC - Salto si C = 0. BCC etiqueta
BCS - Salto si C = 1. BCS etiqueta
BNE - Salto si Z = 0. BNE etiqueta
BEQ - Salto si Z = 1. BEQ etiqueta
BVC - Salto si V = 0. BVC etiqueta
BVS - Salto si V = 1. BVS etiqueta
BPL - Salto si N = 0. BPL etiqueta
BMI - Salto si N = 1. BMI etiqueta

La idea es realizar operaciones de comparación y lógicas e ir saltando en base a los resultados. Salvando mucho las distancias, sería parecido a hacer un IF de BASIC. Hay que tener en cuenta que estas instrucciones usan el direccionamiento relativo, por lo que no pueden saltar más de 127 bytes hacia delante y más de 128 hacia atrás.

Ins. Flags Descripción Ejemplos
JMP - Salto incondicional. JMP $5597
JMP ($5597)
JSR - Salto a rutina. JSR $5597
RTS - Retorno de rutina. RTS

Con la instrucción JMP podemos saltar a cualquier lugar que queramos del programa. La única precaución, cuando se usa el modo de direccionamiento indirecto, es que la dirección no esté en el último byte de la página.

Para poder modularizar el código, en la medida que permite un programa escrito en ensamblador, tenemos las instrucciones JSR y RTS. La primera da un salto a una subrutina dentro de nuestro código, que cuando esta termine de ejecutarse utilizará RTS para volver al punto donde se invocó la rutina. Al utilizar JSR, se guarda en la pila la dirección a la que tiene que saltar cuando ejecute RTS, guardando primero el byte más significativo (MSB) en la pila y después el menos significativo (LSB).

En plataformas modernas, con lenguajes modernos, por debajo un programa usa también subrutinas junto con la pila. Antes de llamar a otra rutina, se guarda en la pila el estado actual, para rescatarlo más tarde, e incluso se guarda en la pila los parámetros de la llamada. Técnicamente podemos hacer también eso con el C64, pero en la realidad nos encontramos con una pila de 256 bytes, que se agotaría fulminantemente de hacer algo así.

Instrucciones de interrupción

Mientras se está ejecutando el programa en la CPU, hay otros chips de E/S que están realizando operaciones de forma paralela. Cuando se cumple alguna condición o se termina alguna tarea concreta, el chip en cuestión se encarga de enviar una señal de interrupción a la CPU. Cuando se recibe esta señal se para lo que se está haciendo, se guarda el estado actual y se hace un salto a la rutina que esté encargada de gestionar las interrupciones. Hay dos tipos de interrupciones:

Para trabajar con interrupciones tenemos las siguientes instrucciones:

Ins. Flags Descripción Ejemplos
CLI I=0 Desactiva las interrupciones IRQ. CLI
SEI I=1 Activa las interrupciones IRQ. SEI
BRK B=1 Interrupción por software. BRK
RTI N V B D I Z C Retorno de interrupción. RTI

Las instrucciones CLI y SEI ya las vimos en una sección anterior, pero nos sirven para activar o desactivar las interrupciones enmascarables. La instrucción RTI es parecida al retorno de subrutina, pero se tiene que utilizar en aquellas rutinas que son invocadas por la CPU al producirse una interrupción. La instrucción BRK fuerza desde el programa a que se produzca una interrupción no enmascarable que se gestiona como si fuera una de tipo IRQ.

Cuando se produce una interrupción IRQ, ya sea por hardware o por software, se guarda el PC en la pila (primero el MSB y después el LSB), se guarda el registro de estado y se ejecuta la rutina apuntada por $FFFE-$FFFF, que es la rutina en $FF48 de la ROM del KERNAL. Esta rutina primero guarda en la pila los registros A, X e Y, y después comprueba si se trata de una interrupción de hardware o software con el flag B. Si es una interrupción hardware se salta a la dirección apuntada por $0314-$0315, si es software la apuntada por $0316-$0317, que por defecto son las rutinas en $EA31 y $FE66 respectivamente.

Hay que tener en cuenta que la instrucción RTI al ejecutarse recupera primero el registro de estado y después el PC de la pila, para continuar la ejecución en la dirección donde se produjo la interrupción. Sin embargo, si se modifica el puntero en $0314-$0315 o $0316-$0317, antes de ejecutar RTI hay que recuperar los registros Y, X y A, para evitar que falle la ejecución al saltar a una dirección equivocada de la memoria. No obstante, cuando redirijamos la rutina de interrupciones a una programada por nosotros, en lugar de utilizar RTI lo habitual es saltar con JMP a la rutina por defecto. Por ejemplo:

*=$0810
main
        sei          ; Desactivar IRQ
        lda #<update ; LSB
        sta $0314
        lda #>update ; MSB
        sta $0315
        cli          ; Activar IRQ
        rts

update
        ; Actualizar pantalla
        inc Victim
        lda Victim
        sta $0400
        ; Rutina por defecto
        jmp $EA31

Victim  BYTE 0

Se desactivan primero las interrupciones, para evitar que se produzca ninguna mientras se modifica el valor del puntero, y una vez modificado se vuelven a activar. Ese es todo el programa por un lado y por el otro está la rutina que gestiona ahora las interrupciones, que actualiza un contador y modifica el buffer de pantalla, para luego proseguir con la rutina por defecto de gestión de interrupciones del KERNAL.

En cuanto a las interrupciones NMI, el mecanismo es similar a las de tipo IRQ, las diferencias es que no se pueden desactivar con el flag I como se ha comentado y que se ejecuta la rutina apuntada por $FFFA-$FFFB, que es la rutina en $FE43 de la ROM del KERNAL. Al final termina ejecutando la rutina apuntada por $0318-$0319, que por defecto es la rutina en $FE47.

Instrucciones auxiliares

En este grupo de instrucciones tenemos las que nos queda y que son de difícil clasificación:

Ins. Flags Descripción Ejemplos
BIT N=B7 V=B6 Z Comprobar bits. BIT $44
BIT $4400
NOP - No operar. NOP

La instrucción BIT es casi idéntica a AND, pero no almacena en el acumulador el resultado de la operación. Además, modifica el flag Z, dependiendo del resultado de la operación, y asigna los bits 6 y 7, a los flags V y N respectivamente.

La instrucción NOP no ejecuta ninguna acción y su única utilidad es rellenar zonas de la memoria de forma segura.

Detalles de bajo nivel

Existen un total de 56 instrucciones disponibles para el chip 6510. La siguiente tabla resume los flags que modifica, así como el código máquina de la instrucción en cada modo, el tamaño de la instrucción y el número de ciclos que tarda en ejecutarse:

Ins. Flags - # ZP ZP,X ZP,Y (ZP,X) (ZP),Y ABS ABS,X ABS,Y (ABS) REL A
ADC N V Z C - $69
2
2
$65
2
3
$75
2
4
- $61
2
6
$71
2
5+
$6D
3
4
$7D
3
4+
$79
3
4+
- - -
AND N Z - $29
2
2
$25
2
3
$35
2
4
- $21
2
6
$31
2
5+
$2D
3
4
$3D
3
4+
$29
3
4+
- - -
ASL N Z C - - $06
2
5
$16
2
6
- - - $0E
3
6
$1E
3
7
- - - $0A
1
2
BCC - - - - - - - - - - - - $90
2
2*+
-
BCS - - - - - - - - - - - - $B0
2
2*+
-
BEQ - - - - - - - - - - - - $F0
2
2*+
-
BIT N=B7 V=B6 Z - $24
2
3
- - - - $2C
2
4
- - - - - -
BMI - - - - - - - - - - - - $30
2
2*+
-
BNE - - - - - - - - - - - - $D0
2
2*+
-
BPL - - - - - - - - - - - - $10
2
2*+
-
BRK B=1 $00
1
7
- - - - - - - - - - - -
BVC - - - - - - - - - - - - $50
2
2*+
-
BVS - - - - - - - - - - - - $70
2
2*+
-
CLC C=0 $18
1
2
- - - - - - - - - - - -
CLD D=0 $D8
1
2
- - - - - - - - - - - -
CLI I=0 $58
1
2
- - - - - - - - - - - -
CLV V=0 $B8
1
2
- - - - - - - - - - - -
CMP N Z C - $C9
2
2
$C5
2
3
$D5
2
4
- $C1
2
6
$D1
2
5+
$CD
3
4
$DD
3
4+
$D9
3
4+
- - -
CPX N Z C - $E0
2
2
$E4
2
3
- - - - $EC
3
4
- - - - -
CPY N Z C - $C0
2
2
$C4
2
3
- - - - $CC
3
4
- - - - -
DEC N Z - - $C6
2
5
$D6
2
6
- - - $CE
3
6
$DE
3
7
- - - -
DEX N Z $CA
1
2
- - - - - - - - - - - -
DEY N Z $88
1
2
- - - - - - - - - - - -
EOR N Z - $49
2
2
$45
2
3
$55
2
4
- $41
2
6
$51
2
5+
$4D
3
4
$5D
3
4+
$59
3
4+
- - -
INC N Z - - $E6
2
5
$F6
2
6
- - - $EE
3
6
$FE
3
7
- - - -
INX N Z $E8
1
2
- - - - - - - - - - - -
INY N Z $C8
1
2
- - - - - - - - - - - -
JMP - - - - - - - - $4C
3
3
- - $6C
3
5
- -
JSR - - - - - - - - $20
3
6
- - - - -
LDA N Z - $A9
2
2
$A5
2
3
$B5
2
4
- $A1
2
6
$B1
2
5+
$AD
3
4
$BD
3
4+
$B9
3
4+
- - -
LDX N Z - $A2
2
2
$A6
2
3
- $B6
2
4
- - $AE
3
4
- $BE
3
4+
- - -
LDY N Z - $A0
2
2
$A4
2
3
$B4
2
4
- - - $AC
3
4
$BC
3
4+
- - - -
LSR N=0 Z C - - $46
2
5
$56
2
6
- - - $4E
3
6
$5E
3
7
- - - $4A
1
2
NOP - $EA
1
2
- - - - - - - - - - - -
ORA N Z - $09
2
2
$05
2
3
$15
2
4
- $01
2
6
$11
2
5+
$0D
3
4
$1D
3
4+
$19
3
4+
- - -
PHA - $48
1
3
- - - - - - - - - - - -
PHP - $08
1
3
- - - - - - - - - - - -
PLA N Z $68
1
4
- - - - - - - - - - - -
PLP N V B D I Z C $28
1
4
- - - - - - - - - - - -
ROL N Z C - - $26
2
5
$36
2
6
- - - $2E
3
6
$3E
3
7
- - - $2A
1
2
ROR N Z C - - $66
2
5
$76
2
6
- - - $6E
3
6
$7E
3
7
- - - $6A
1
2
RTI N V B D I Z C $40
1
6
- - - - - - - - - - - -
RTS - $60
1
6
- - - - - - - - - - - -
SBC N V Z C - $E9
2
2
$E5
2
3
$F5
2
4
- $E1
2
6
$F1
2
5+
$ED
3
4
$FD
3
4+
$E9
3
4+
- - -
SEC C=1 $38
1
2
- - - - - - - - - - - -
SED D=1 $F8
1
2
- - - - - - - - - - - -
SEI I=1 $78
1
2
- - - - - - - - - - - -
STA - - - $85
2
3
$95
2
4
- $81
2
6
$91
2
6
$8D
3
4
$9D
3
5
$99
3
5
- - -
STX - - - $86
2
3
- $96
2
4
- - $8E
3
4
- - - - -
STY - - - $84
2
3
$94
2
4
- - - $8C
3
4
- - - - -
TAX N Z $AA
1
2
- - - - - - - - - - - -
TAY N Z $A8
1
2
- - - - - - - - - - - -
TSX N Z $BA
1
2
- - - - - - - - - - - -
TXA N Z $8A
1
2
- - - - - - - - - - - -
TXS - $9A
1
2
- - - - - - - - - - - -
TYA N Z $98
1
2
- - - - - - - - - - - -

Cuando el tiempo en ciclos tiene un + al final, significa que se ha de sumar un ciclo más si la operación tiene que cambiar de página. El símbolo * significa que se ha de sumar un ciclo más si se ejecuta el salto.

Memoria

El C64 nos permite configurar la memoria para hacer visible los diferentes chips de ROM y RAM del sistema. Para ello tenemos que configurar los bits 0-2 de la dirección de memoria $0001, para configurar los bloques de memoria en $A000-$BFFF, $D000-$DFFF y $E000-$FFFF:

El valor por defecto al arrancar la máquina es el 111, es decir, que la ROM de BASIC, el KERNAL y los chips de E/S son visibles. Es recomendable no desactivar el KERNAL, ni los chips de E/S, para poder trabajar con la máquina, pero en cuanto al interprete de BASIC se puede prescindir del mismo si no necesitamos de ninguna de sus subrutinas.

Por defecto la distribución de la memoria es la siguiente:

Dirección Descripción
$0000-$00FF Página cero.
$0100-$01FF Pila de programa.
$0200-$03FF Operaciones del sistema (BASIC y E/S).
$0400-$07FF Buffer de pantalla y punteros de sprites.
$0800-$9FFF Memoria libre (38.912 bytes).
$A000-$BFFF ROM BASIC.
$C000-$CFFF Memoria libre (4.096 bytes).
$D000-$D3FF Chip VIC-II.
$D400-$D7FF Chip SID.
$D800-$DBFF Buffer de color de pantalla.
$DC00-$DDFF Chips CIA1 y CIA2.
$DE00-$DFFF E/S dispositivos externos opcionales.
$E000-$FFFF ROM KERNAL.

Por defecto tenemos 43.008 bytes de memoria libre disponible, que se pueden ampliar a 51.200 bytes si desactivamos la ROM de BASIC.

Interfaz gráfica

El chip gráfico VIC-II tiene la capacidad de gestionar 16 KB de memoria RAM de los 64 KB que tiene en total el C64. Dentro del puerto A ($DD00) de la CIA2, modificando los bits 0-1, podemos configurar cual de los cuatro bancos de memoria puede acceder el chip:

Esto define los bits 14 y 15 de las direcciones a las que va a acceder el chip VIC-II. Una vez seleccionado el banco, podemos desde los registros en $D011 y $D016 cambiar la configuración de la pantalla:

Bits $D011 $D016
0-2 Desplazamiento horizontal raster. Desplazamiento vertical raster.
3 Altura pantalla (0 = 24; 1 = 25). Ancho pantalla (0 = 38; 1 = 40).
4 Pantalla: 0 = Apagada; 1 = Encendida. Modo: 0 = Monocolor; 1 = Multicolor.
5 Modo: 0 = Carácter; 1 = Bitmap. -
6 Modo fondo extendido: 0 = Desactivado; 1 = Activado. -
7 Bit 8 de la línea actual del raster. -

Como se puede observar hay diferentes modos de pantalla, que se resumen en dos categorías:

Para el modo carácter hace falta el buffer de pantalla, el de color y el juego de caracteres. Para el modo bitmap hace falta el buffer de píxeles y el de color. Algunas de estas regiones se pueden cambiar de ubicación dentro del banco de memoria seleccionado, para ello usaremos el registro en $D018:

Bits Descripción
1-3 Modo carácter: Ubicación del juego de caracteres (bits 11-13).
Modo bitmap: Ubicación del buffer de píxeles (bit 13).
4-7 Ubicación del buffer de pantalla (bits 10-13).

La ubicación del juego de caracteres en la memoria RAM, dependiendo del banco de memoria elegido en $DD00, será:

Valor Banco 0 (11) Banco 1 (10) Banco 2 (01) Banco 3 (00)
000 $0000-$07FF $4000-$47FF $8000-$87FF $C000-$C7FF
001 $0800-$0FFF $4800-$4FFF $8800-$8FFF $C800-$CFFF
010 $1000-$17FF $5000-$57FF $9000-$97FF $D000-$D7FF
011 $1800-$1FFF $5800-$5FFF $9800-$9FFF $D800-$DFFF
100 $2000-$27FF $6000-$67FF $A000-$A7FF $E000-$E7FF
101 $2800-$2FFF $6800-$6FFF $A800-$AFFF $E800-$EFFF
110 $3000-$37FF $7000-$77FF $B000-$B7FF $F000-$F7FF
111 $3800-$3FFF $7800-$7FFF $B800-$BFFF $F800-$FFFF

AVISO: Con los valores 010 y 011 con el banco 0 y 2 la implementación del chip VIC-II selecciona la ROM de caracteres en lugar de la RAM.

La ubicación del buffer de píxeles en la memoria RAM será:

Valor Banco 0 (11) Banco 1 (10) Banco 2 (01) Banco 3 (00)
0xx $0000-$1FFF $4000-$5FFF $8000-$9FFF $C000-$DFFF
1xx $2000-$3FFF $6000-$7FFF $A000-$BFFF $E000-$FFFF

La ubicación del buffer de pantalla en la memoria RAM será:

Valor Banco 0 (11) Banco 1 (10) Banco 2 (01) Banco 3 (00)
0000 $0000-$03FF $4000-$43FF $8000-$83FF $C000-$C3FF
0001 $0400-$07FF $4400-$47FF $8400-$87FF $C400-$C7FF
0010 $0800-$0BFF $4800-$4BFF $8800-$8BFF $C800-$CBFF
0011 $0C00-$0FFF $4C00-$4FFF $8C00-$8FFF $CC00-$CFFF
0100 $1000-$13FF $5000-$53FF $9000-$93FF $D000-$D3FF
0101 $1400-$17FF $5400-$57FF $9400-$97FF $D400-$D7FF
0110 $1800-$1BFF $5800-$5BFF $9800-$9BFF $D800-$DBFF
0111 $1C00-$1FFF $5C00-$5FFF $9C00-$9FFF $DC00-$DFFF
1000 $2000-$23FF $6000-$63FF $A000-$A3FF $E000-$E3FF
1001 $2400-$27FF $6400-$67FF $A400-$A7FF $E400-$E7FF
1010 $2800-$2BFF $6800-$6BFF $A800-$ABFF $E800-$EBFF
1011 $2C00-$2FFF $6C00-$6FFF $AC00-$AFFF $EC00-$EFFF
1100 $3000-$33FF $7000-$73FF $B000-$B3FF $F000-$F3FF
1101 $3400-$37FF $7400-$77FF $B400-$B7FF $F400-$F7FF
1110 $3800-$3BFF $7800-$7BFF $B800-$BBFF $F800-$FBFF
1111 $3C00-$3FFF $7C00-$7FFF $BC00-$BFFF $FC00-$FFFF

En caso de cambiar la ubicación del buffer de pantalla, si queremos utilizar las subrutinas de la terminal tendemos que actualizar el valor en la dirección $0288 (648), que por defecto tiene el valor $04.

Colores del sistema

El C64 tiene 16 colores diferentes, cuyos valores van del 0 al 15, por lo que sólo requieren de 4 bits para definirlos. Por ejemplo:

10  REM VER LISTA DE COLORES
20  TB=1024:CB=55296:T=160
30  FOR I=0 TO 24
40  FOR J=0 TO 39
50  C=INT(J/5)
60  IF I>12 THEN C=C+8
70  POKE TB+(40*I+J),T
80  POKE CB+(40*I+J),C
90  NEXT J
100 NEXT I
110 GET K$
120 IF K$="" THEN 110

Este programa rellena el buffer de pantalla y de color para mostrar los 16 colores que soporta el chip gráfico VIC-II. Como resultado obtenemos la siguiente imagen:

Colores del C64

Además del buffer de color, podemos modificar el color del borde de la pantalla en la dirección $D020 y el del fondo en $D021, independientemente del modo gráfico que esté seleccionado.

Modo carácter

El modo carácter es un grupo de modos gráficos que gestionan el buffer de pantalla usando el carácter como unidad de información. Con este modo se puede representar, gastando poca memoria, los niveles de un juego. Hay tres modos de caracteres: el estándar, el multicolor y el de color de fondo extendido.

Los tres modos implican una representación de la pantalla como un tablero de 40x25 celdas. Para ello hay un buffer de pantalla, que contiene los índices a los caracteres, y un buffer de color para la pantalla. Por defecto, el buffer de pantalla está en la región $0400-$07E7, aunque se puede reubicar si fuera necesario. El buffer de color siempre se encuentra en la región $D800-$DBE7. Ambos búferes tienen un tamaño de 1.000 bytes, de modo que la posición 0 es la esquina superior izquierda y la posición 999 es la esquina inferior derecha.

Cada celda de la pantalla es un byte, por lo tanto podemos disponer de 256 posibles valores que referenciar en el juego de caracteres, desde el 0 al 255. Como cada carácter tiene unas dimensiones de 8x8 píxeles, la resolución de la pantalla es de 320x200 píxeles. No hay que confundir los códigos de pantalla, que son índices dentro del juego de caracteres, con los códigos PETSCII. Algunas rutinas del KERNAL funcionan con PETSCII, por lo que habrá que tenerlo en cuenta a la hora de trabajar, e incluso tener subrutinas de conversión entre ambos tipos de códigos.

El modo estándar es el que está activado por defecto al arrancar la máquina. Este nos permite por cada celda seleccionar un tile y un color. Dentro de la matriz de 8x8 del tile, si el píxel es 0 se utiliza el color de fondo ($D021), si es 1 se utiliza el color almacenado en la celda del buffer de color.

El modo multicolor es similar al estándar, pero nos permite utilizar hasta 4 colores por celda formando bloques de dos píxeles en horizontal. Para aquellos familiarizados con el C64 recordarán los “píxeles gordos” en sus gráficos. La activación de este modo depende del valor que tenga la celda en el buffer de color:

Esto nos permite tener tiles para pintar texto y otros para pintar el mundo del juego, aunque conlleva perder los colores del 8 al 15 para el color de la celda. En este modo, cuando un tile se pinta en modo multicolor, en vez de tener una matriz de 8x8 tenemos una de 4x8, donde cada elemento representa 2x1 píxeles. Ahora los valores de cada “píxel gordo” en la matriz del tile son:

El modo de color de fondo extendido es una variación del modo estándar que nos permite tener cuatro colores de fondo distintos. Para ello, una vez es activado, el chip irá a la celda del buffer de pantalla y comprobará los bits 6-7 para decidir qué color de fondo ha de usar. Los valores posibles son:

La contrapartida de este sistema es que sólo podemos usar los 64 primeros elementos del mapa de caracteres, perdiendo la capacidad de usar los 192 restantes. Esto hace que este modo gráfico no sea especialmente popular.

Para poder ver en acción los diferentes modos tenemos el siguiente ejemplo:

10  REM INICIALIZAR VIC-II
20  POKE 53280,12 : REM COLOR BORDE
30  POKE 53281,0  : REM COLOR FONDO
40  POKE 53282,2  : REM COLOR EXTRA 1
50  POKE 53283,5  : REM COLOR EXTRA 2
60  POKE 53284,3  : REM COLOR EXTRA 3
70  POKE 646,1    : REM COLOR CURSOR
80  GOSUB 670
90  REM PINTAR MENU
100 PRINT "{147}{32*10}(VIC-II TEXT MODES)"
110 PRINT
120 PRINT "{32*4}F1:MONO F2:MULT F3:EXBG F4:BRCL"
130 PRINT "{32*4}F5:BGCL F6:E1CL F7:E2CL F8:E3CL"
140 PRINT "{32*3}0:CHCL 1-8:C0-C7 SHIFT+1-8:C8-C15"
150 REM PINTAR FUENTE
160 POKE 646,15
170 TB=1024  : REM BUFFER PANTALLA
180 CB=55296 : REM BUFFER COLOR
190 C=PEEK(646):T=0
200 FOR I=6 TO 13
210 FOR J=12 TO 27
220 POKE TB+(I*40+J),T
230 POKE CB+(I*40+J),C
240 POKE TB+((I+9)*40+J),T+128
250 POKE CB+((I+9)*40+J),C
260 T=T+1
270 NEXT J,I
280 REM LOGICA MENU
290 MD=0
300 GET K$
310 IF K$="{F1}" THEN GOSUB 670
320 IF K$="{F2}" THEN GOSUB 710
330 IF K$="{F3}" THEN GOSUB 750
340 IF K$="{F4}" THEN MD=1
350 IF K$="{F5}" THEN MD=2
360 IF K$="{F6}" THEN MD=3
370 IF K$="{F7}" THEN MD=4
380 IF K$="{F8}" THEN MD=5
390 IF K$="0" THEN MD=0
400 IF K$>="1" AND K$<="8" THEN C=ASC("1"):GOTO 460
410 IF K$>="!" AND K$<="(" THEN C=ASC("!")-8:GOTO 460
420 IF K$="/" THEN K$="'":C=ASC("!")-8:GOTO 460
430 IF K$="Q" THEN 550
440 GOTO 300
450 REM CAMBIAR EL COLOR
460 C=ASC(K$)-C
470 IF MD=0 THEN POKE 646,C:GOSUB 590
480 IF MD=1 THEN POKE 53280,C
490 IF MD=2 THEN POKE 53281,C
500 IF MD=3 THEN POKE 53282,C
510 IF MD=4 THEN POKE 53283,C
520 IF MD=5 THEN POKE 53284,C
530 GOTO 300
540 REM SALIR DEL PROGRAMA
550 PRINT"{147}"
560 GOSUB 670
570 END
580 REM ACTUALIZAR BUFFER COLOR
590 C=PEEK(646)
600 FOR I=6 TO 13
610 FOR J=12 TO 27
620 POKE CB+(I*40+J),C
630 POKE CB+((I+9)*40+J),C
640 NEXT J,I
650 RETURN
660 REM ACTIVAR MODO MONOCOLOR
670 POKE 53265,(PEEK(53265) AND 159)
680 POKE 53270,(PEEK(53270) AND 239)
690 RETURN
700 REM ACTIVAR MODO MULTICOLOR
710 POKE 53265,(PEEK(53265) AND 159)
720 POKE 53270,(PEEK(53270) AND 239) OR 16
730 RETURN
740 REM ACTIVAR MODO FONDO EXTENDIDO
750 POKE 53265,(PEEK(53265) AND 159) OR 64
760 POKE 53270,(PEEK(53270) AND 239)
770 RETURN

El ejemplo nos permite probar los diferentes modos de caracteres y modificar los diferentes colores de la pantalla. Además de su típica lentitud para actualizar el buffer de color y de pantalla, tiene añadida en la línea 420 una condición para facilitar la ejecución en el emulador VICE cuando el teclado está en modo simbólico, ya que el carácter que espera con SHIFT+7 es la comilla simple (').

Modo bitmap

El modo bitmap es un grupo de modos gráficos que gestionan el buffer de pantalla usando el píxel como unidad de información. Con este modo se puede representar imágenes gráficas más complejas que en la sección anterior, pero su coste de memoria es superior. Hay dos modos bitmap: el estándar y el multicolor.

El tamaño de pantalla en el C64 es de 320x200 píxeles, que son 64.000 píxeles en total. Si quisiéramos que cada uno pudiera representar los 16 colores que tiene la máquina, necesitaríamos 4 bits por píxel y como consecuencia harían falta 32.000 bytes de memoria. Obviamente es un coste excesivo en memoria y por ello los ingenieros que diseñaron el C64 optaron por un método más restringido.

En el modo bitmap tenemos un buffer de píxeles, que será de 320x200 de tamaño, pero cada píxel es de un bit de tamaño, por lo tanto el buffer ocupará 8.000 bytes. Aunque sería lógico pensar que la distribución de los píxeles del buffer es lineal con respecto las coordenadas de pantalla, lo cierto es que no es así sino de la siguiente manera:

Y\X 0 8 304 312
0 B0 (0-7)
B1 (8-15)
B2 (16-23)
B3 (24-31)
B4 (32-39)
B5 (40-47)
B6 (48-55)
B7 (56-63)
B8 (64-71)
B9 (72-79)
B10 (80-87)
B11 (88-95)
B12 (96-103)
B13 (104-111)
B14 (112-119)
B15 (120-127)
B304 (2432-2439)
B305 (2440-2447)
B306 (2448-2455)
B307 (2456-2463)
B308 (2464-2471)
B309 (2472-2479)
B310 (2480-2487)
B311 (2488-2495)
B312 (2496-2503)
B313 (2504-2511)
B314 (2512-2519)
B315 (2520-2527)
B316 (2528-2535)
B317 (2536-2543)
B318 (2544-2551)
B319 (2552-2559)
8 B320 (2560-2567)
B321 (2568-2575)
B322 (2576-2583)
B323 (2584-2591)
B324 (2592-2599)
B325 (2600-2607)
B326 (2608-2615)
B327 (2616-2623)
B328 (2624-2631)
B329 (2632-2639)
B330 (2640-2647)
B331 (2648-2655)
B332 (2656-2663)
B333 (2664-2671)
B334 (2672-2679)
B335 (2680-2687)
B624 (4992-4999)
B625 (5000-5007)
B626 (5008-5015)
B627 (5016-5023)
B628 (5024-5031)
B629 (5032-5039)
B630 (5040-5047)
B631 (5048-5055)
B632 (5056-5063)
B633 (5064-5071)
B634 (5072-5079)
B635 (5080-5087)
B636 (5088-5095)
B637 (5096-5103)
B638 (5104-5111)
B639 (5112-5119)
184 B7360 (58880-58887)
B7361 (58888-58895)
B7362 (58896-58903)
B7363 (58904-58911)
B7364 (58912-58919)
B7365 (58920-58927)
B7366 (58928-58935)
B7367 (58936-58943)
B7368 (58944-58951)
B7369 (58952-58959)
B7370 (58960-58967)
B7371 (58968-58975)
B7372 (58976-58983)
B7373 (58984-58991)
B7374 (58992-58999)
B7375 (59000-59007)
B7664 (61312-61319)
B7665 (61320-61327)
B7666 (61328-61335)
B7667 (61336-61343)
B7668 (61344-61351)
B7669 (61352-61359)
B7670 (61360-61367)
B7671 (61368-61375)
B7672 (61376-61383)
B7673 (61384-61391)
B7674 (61392-61399)
B7675 (61400-61407)
B7676 (61408-61415)
B7677 (61416-61423)
B7678 (61424-61431)
B7679 (61432-61439)
192 B7680 (61440-61447)
B7681 (61448-61455)
B7682 (61456-61463)
B7683 (61464-61471)
B7684 (61472-61479)
B7685 (61480-61487)
B7686 (61488-61495)
B7687 (61496-61503)
B7688 (61504-61511)
B7689 (61512-61519)
B7690 (61520-61527)
B7691 (61528-61535)
B7692 (61536-61543)
B7693 (61544-61551)
B7694 (61552-61559)
B7695 (61560-61567)
B7984 (63872-63879)
B7985 (63880-63887)
B7986 (63888-63895)
B7987 (63896-63903)
B7988 (63904-63911)
B7989 (63912-63919)
B7990 (63920-63927)
B7991 (63928-63935)
B7992 (63936-63943)
B7993 (63944-63951)
B7994 (63952-63959)
B7995 (63960-63967)
B7996 (63968-63975)
B7997 (63976-63983)
B7998 (63984-63991)
B7999 (63992-63999)

Como se puede intuir, la organización del buffer de píxeles sigue un patrón similar a la forma de trabajar del modo de caracteres. Una consecuencia de esto es que el número de colores disponibles en cada celda de 8x8 píxeles es limitado:

Primero, el término nibble se utiliza para designar a una de las dos mitades de un byte. Segundo, podemos deducir que el modo multicolor trabaja de forma similar al visto en el modo carácter, agrupando los píxeles en bloques de dos para definir 4 posibles colores por “píxel gordo”.

No hay que olvidar que se debe configurar, en la dirección $D018, la ubicación del buffer de píxeles dentro del banco de memoria seleccionado del chip gráfico. Esto es importante, porque la configuración por defecto posiblemente apunta al banco 0, situando el buffer de píxeles en los primeros 8.000 bytes de la memoria, donde el sistema tiene varias páginas de la memoria reservadas para diferentes operaciones, como el caso de la pila de programa. También es recomendable ubicar correctamente el buffer de pantalla para evitar colisiones con el de píxeles.

Por motivos de implementación de la arquitectura, el buffer de color no se puede cambiar de ubicación y siempre mantiene la misma dirección. Además, el chip VIC-II sólo puede leer los 4 bits más bajos, como parte de las optimizaciones con las que fue diseñado. Por esta decisión no se puede utilizar su nibble alto para añadir un cuarto color único al modo multicolor.

El siguiente ejemplo nos muestra cómo trabajar con el modo multicolor:

10  REM INICIALIZAR PROGRAMA
20  PRINT "{147}LOADING..."
30  POKE 53280,12     : REM COLOR BORDE
40  POKE 53281,0      : REM COLOR FONDO
50  POKE 646,1        : REM COLOR CURSOR
60  R%(0)=PEEK(56576) : REM PUERTO A CIA2
70  R%(1)=PEEK(53265) : REM REG1 VIC-II
80  R%(2)=PEEK(53270) : REM REG2 VIC-II
90  R%(3)=PEEK(53272) : REM MEM VIC-II
100 REM BORRAR BUFFER PIXEL
110 SB=23552:PB=24576:CB=55296
120 FOR I=0 TO 7999
130 POKE PB+I,0
140 PRINT "{19}LOADING...";STR$(INT(100*I/7999));"%"
150 NEXT I
160 REM MODIFICAR MODO GRAFICO
170 POKE 56576,(R%(0) AND 252) OR 2
180 POKE 53265,(R%(1) AND 223) OR 32
190 POKE 53270,(R%(2) AND 239) OR 16
200 POKE 53272,(R%(3) AND 1) OR 120
210 REM BUCLE SALVA PANTALLAS
220 X=0  : Y=0  : REM COORDENADAS
230 VX=2 : VY=1 : REM MOVIMIENTO
240 C=1  : P=1  : REM COLORES
250 REM CALCULAR POSICION PIXEL
260 CX=INT(X/8) : REM COLUMNA
270 DX=X-CX*8   : REM SUBCOLUMNA
280 CY=INT(Y/8) : REM FILA
290 DY=Y-CY*8   : REM SUBFILA
300 CP=CY*40+CX : REM CELDA
310 REM MODIFICAR BUFFER PIXEL
320 ZZ=PB+CP*8+DY
330 CB%=PEEK(ZZ)
340 IF DX>=0 AND DX<=1 THEN CB%=(CB% AND 63)+P*64
350 IF DX>=2 AND DX<=3 THEN CB%=(CB% AND 207)+P*16
360 IF DX>=4 AND DX<=5 THEN CB%=(CB% AND 243)+P*4
370 IF DX>=6 AND DX<=7 THEN CB%=(CB% AND 252)+P
380 POKE ZZ,CB%
390 REM MODIFICAR BUFFER COLOR
400 IF P=1 THEN ZZ=SB+CP : POKE ZZ,(PEEK(ZZ) AND 15)+C*16
410 IF P=2 THEN ZZ=SB+CP : POKE ZZ,(PEEK(ZZ) AND 240)+C
420 IF P=3 THEN ZZ=CB+CP : POKE ZZ,(PEEK(ZZ) AND 240)+C
430 P=P+1 : IF P>3  THEN P=1
440 C=C+1 : IF C>15 THEN C=1
450 REM MOVER COORDENADAS
460 IF X<=0   THEN X=0   : VX=2
470 IF X>=318 THEN X=318 : VX=-2
480 IF Y<=0   THEN Y=0   : VY=1
490 IF Y>=199 THEN Y=199 : VY=-1
500 X=X+VX : Y=Y+VY
510 REM COMPROBAR TECLADO
520 GET K$
530 IF K$="" THEN 260
540 REM FINALIZAR PROGRAMA
550 POKE 56576,R%(0)
560 POKE 53265,R%(1)
570 POKE 53270,R%(2)
580 POKE 53272,R%(3)

En 20-90 se cambian algunos colores y se guarda la configuración inicial del chip gráfico. En 120-150 se inicializa el buffer de píxeles para vaciarlo. En 170-200 se configura el chip gráfico para activar el modo bitmap multicolor, ubicando el buffer de píxeles en la segunda mitad del banco de memoria 1 ($6000-$7FFF) y el buffer de pantalla justo en los últimos 1024 bytes de la primera mitad ($5C00-$5FFF). En 260-300 se calcula la posición de la celda de la pantalla donde están las coordenadas X e Y. En 320-380 se calcula la posición en la memoria del bloque de píxeles a modificar, se modifica el valor y se guarda en la memoria. En 400-440 se actualiza los colores de pantalla. En 460-500 se mueven las coordenadas de pintado. Por último, en 520-530 se comprueba si se ha pulsado alguna tecla del teclado y en 550-580 se restaura la configuración inicial del chip gráfico.

AVISO: Al trabajar con BASIC hay que tener en cuenta que la memoria ocupada por las cadenas de texto crece desde la dirección $9FFF hacia abajo. Analizando el ejemplo vemos que nos quedan 8.192 bytes para cadenas y 21.503 bytes para código BASIC, variables y arrays, hasta colisionar con el buffer de píxeles. En caso de colisión el programa puede desembocar en comportamientos inesperados y fallar en ejecución. Por ello hay que analizar con detenimiento la ubicación de los búferes del VIC-II.

Gestión de sprites

Los sprites son un mecanismo del chip gráfico para pintar objetos en la pantalla que se pueden mover libremente. De este modo por un lado tenemos el fondo del escenario, representado por el buffer de pantalla o de píxeles, y por el otro las entidades que se mueven por la pantalla.

El C64 dispone de un máximo de 8 sprites por hardware, pero existe una técnica llamada multiplexación de sprites que permite por software aumentar la cantidad de sprites simultáneos por pantalla.

Para poder definir un sprite tenemos que definir su matriz de 24x21 píxeles. Cada píxel se define usando un bit, por lo tanto cada fila de 24 píxeles se define con 3 bytes, y multiplicando por 21 filas necesitamos 63 bytes en total. Sin embargo, para que el VIC-II pueda localizar cualquier sprite en el banco de memoria seleccionado, estas matrices de puntos han de ubicarse en direcciones que sean múltiplo de 64. Haciendo las cuentas nos sale que, dentro de un banco de memoria de 16 KB, pueden almacenarse hasta 256 sprites. Obviamente es imposible tener 256 sprites, porque necesitamos al menos 3 KB para el buffer de pantalla y el juego de caracteres si usamos el modo carácter, con el modo bitmap son 9 KB los que necesitamos.

Sabemos que el buffer de pantalla ocupa 1.000 bytes, pero se ubica en direcciones que son múltiplo de 1.024, es decir, que quedan 24 bytes aparentemente libres. En realidad, los últimos 8 bytes de la región donde se ubica el buffer de pantalla, se utilizan como punteros para que el VIC-II pueda localizar los sprites en el banco de memoria. En la configuración por defecto el buffer de pantalla se encuentra en las direcciones $0400-$07E7, por lo tanto los punteros están en las direcciones $07F8-$07FF, donde la primera dirección corresponde al sprite 0 y la última al sprite 7.

El VIC-II tiene los siguientes registros para configurar los sprites:

Dirección Descripción
$D015 Registro de activación de sprites:
0 = Sprite desactivado.
1 = Sprite activado.
$D017 Registro de doble altura de sprites:
0 = Alto normal.
1 = Alto doble.
$D01B Registro de prioridad de sprites:
0 = Pintar delante de la pantalla.
1 = Pintar detrás de la pantalla.
$D01C Registro de modo multicolor de sprites:
0 = Modo monocolor.
1 = Modo multicolor.
$D01D Registro de doble anchura de sprites:
0 = Ancho normal.
1 = Ancho doble.

El bit 0 corresponde al sprite 0 y así sucesivamente hasta llegar al bit 7 que corresponde al sprite 7. El registro de prioridad determina si el sprite se debe pintar encima del contenido del buffer de pantalla o detrás del mismo, pero por encima del color de fondo. Es decir, que si un sprite se pinta por detrás de la “pantalla”, este sólo será visible en aquellos píxeles que usen el color de fondo. Podemos alterar el ancho y el alto de un sprite duplicando sus dimensiones a la hora de ser pintado. Una vez configurado el sprite lo podemos activar para que sea pintado en pantalla.

Para poder mover por la pantalla los sprites el VIC-II necesita conocer las coordenadas y para ello tiene los siguientes registros:

Dirección Descripción
$D000 Coordenada X del sprite 0 (bits del 0-7).
$D001 Coordenada Y del sprite 0.
$D002 Coordenada X del sprite 1 (bits del 0-7).
$D003 Coordenada Y del sprite 1.
$D004 Coordenada X del sprite 2 (bits del 0-7).
$D005 Coordenada Y del sprite 2.
$D006 Coordenada X del sprite 3 (bits del 0-7).
$D007 Coordenada Y del sprite 3.
$D008 Coordenada X del sprite 4 (bits del 0-7).
$D009 Coordenada Y del sprite 4.
$D00A Coordenada X del sprite 5 (bits del 0-7).
$D00B Coordenada Y del sprite 5.
$D00C Coordenada X del sprite 6 (bits del 0-7).
$D00D Coordenada Y del sprite 6.
$D00E Coordenada X del sprite 7 (bits del 0-7).
$D00F Coordenada Y del sprite 7.
$D010 Bit 8 de la coordenada X de los sprites 0-7.

El eje X va desde el valor 0 al 511, pero como un byte sólo contiene 256 valores hace falta un bit extra, que es el que se almacena en la dirección $D010. El eje Y va desde el valor 0 al 255. Por lo tanto, tenemos coordenadas que van de la posición (0,0), en la esquina superior izquierda, a la (511,255), en la esquina inferior derecha. Sin embargo, no todas las coordenadas son visibles en la pantalla, el área visible va desde la posición (24,50) a la (343,249), que tiene unas dimensiones de 320x200 píxeles.

Otra información que necesita el VIC-II es la información sobre los colores de los sprites. Para ello se usan los siguientes registros:

Dirección Descripción
$D025 Color extra 1 de sprites.
$D026 Color extra 2 de sprites.
$D027 Color del sprite 0.
$D028 Color del sprite 1.
$D029 Color del sprite 2.
$D02A Color del sprite 3.
$D02B Color del sprite 4.
$D02C Color del sprite 5.
$D02D Color del sprite 6.
$D02E Color del sprite 7.

Dependiendo de si el sprite está en modo monocolor o multicolor, tenemos dos o cuatro colores posibles. Como ya sospechará el lector, el modo multicolor agrupa los píxeles en bloques de dos, haciendo uso de los “píxeles gordos” que vimos en los modos gráficos. Por lo tanto, los valores de los píxeles son:

El siguiente ejemplo muestra cómo trabajar con sprites

10  REM INICIALIZAR COLORES
20  POKE 53280,12  : REM COLOR BORDE
30  POKE 53281,0   : REM COLOR FONDO
40  POKE 646,1     : REM COLOR CURSOR
50  POKE 53285,15  : REM COLOR SPRITES EX1
60  POKE 53286,12  : REM COLOR SPRITES EX2
70  POKE 53287,1   : REM COLOR SPRITE 0
80  POKE 53288,2   : REM COLOR SPRITE 1
90  POKE 53289,5   : REM COLOR SPRITE 2
100 POKE 53290,6   : REM COLOR SPRITE 3
110 POKE 53291,1   : REM COLOR SPRITE 4
120 POKE 53292,2   : REM COLOR SPRITE 5
130 POKE 53293,5   : REM COLOR SPRITE 6
140 POKE 53294,6   : REM COLOR SPRITE 7
150 REM INICIALIZAR COORDENADAS
160 POKE 53248,79  : REM SPRITE X0
170 POKE 53249,90  : REM SPRITE Y0
180 POKE 53250,141 : REM SPRITE X1
190 POKE 53251,90  : REM SPRITE Y1
200 POKE 53252,203 : REM SPRITE X2
210 POKE 53253,90  : REM SPRITE Y2
220 POKE 53254,9   : REM SPRITE X3 (265)
230 POKE 53255,90  : REM SPRITE Y3
240 POKE 53256,79  : REM SPRITE X4
250 POKE 53257,197 : REM SPRITE Y4
260 POKE 53258,141 : REM SPRITE X5
270 POKE 53259,197 : REM SPRITE Y5
280 POKE 53260,203 : REM SPRITE X6
290 POKE 53261,197 : REM SPRITE Y6
300 POKE 53262,9   : REM SPRITE X7 (265)
310 POKE 53263,197 : REM SPRITE Y7
320 POKE 53264,136 : REM SPRITE X0-7 (B8)
330 REM CARGAR SPRITE EN MEMORIA
340 FOR I=0 TO 7
350 POKE 2040+I,255
360 NEXT I
370 FOR I=0 TO 62
380 READ A%
390 POKE 16320+I,A%
400 NEXT I
410 REM CONFIGURAR SPRITES
420 POKE 53271,0   : REM ALTO DOBLE SPRITES
430 POKE 53277,0   : REM ANCHO DOBLE SPRITES
440 POKE 53275,0   : REM PRIORIDAD SPRITES
450 POKE 53276,240 : REM MODO COLOR SPRITES
460 POKE 53269,255 : REM ACTIVAR SPRITES
470 REM PINTAR TEXTO PANTALLA
480 PRINT "{147}" : PRINT
490 PRINT "              {white}sprites test"
500 PRINT "{pink}"
510 FOR I=1 TO 18
520 PRINT "      {191*28}"
530 NEXT I
540 PRINT "{white}"
550 REM GESTION TECLADO
560 GET K$
570 IF K$="{F1}" THEN POKE 53271,1
580 IF K$="{F2}" THEN POKE 53271,0
590 IF K$="{F3}" THEN POKE 53277,1
600 IF K$="{F4}" THEN POKE 53277,0
610 IF K$="{F5}" THEN POKE 53275,1
620 IF K$="{F6}" THEN POKE 53275,0
630 IF K$="Q" OR K$=" " THEN 660
640 GOTO 560
650 REM FINALIZAR PROGRAMA
660 POKE 53269,0
670 PRINT "{147}"
680 REM SEÑOR CARTA (255:16320)
690 DATA 255,255,255
700 DATA 192,0,3
710 DATA 160,0,5
720 DATA 144,195,9
730 DATA 136,195,17
740 DATA 132,195,33
750 DATA 130,0,65
760 DATA 145,0,137
770 DATA 168,129,21
780 DATA 160,66,5
790 DATA 160,36,5
800 DATA 160,90,5
810 DATA 152,129,25
820 DATA 129,0,129
830 DATA 130,0,65
840 DATA 132,126,33
850 DATA 136,129,17
860 DATA 144,66,9
870 DATA 160,60,5
880 DATA 192,0,3
890 DATA 255,255,255

Primero se inicializan los colores, coordenadas, la matriz de píxeles del sprite y se termina de configurar todos los ocho sprites del hardware. Segundo se pinta en la pantalla un texto para poder comprobar el cambio de prioridad en el sprite 0. Con la gestión del teclado cambiamos algunas propiedades gráficas del sprite 0. Lo siguiente es desactivar los sprites en 660 y limpiar la pantalla. El último bloque son byte a byte la definición de la matriz de píxeles del sprite.

Interrupciones gráficas

Existen varios elementos que pueden generar interrupciones en el chip VIC-II, para ello necesitamos tener en cuenta las siguientes direcciones de memoria:

Dirección Descripción
$D011
(Bit 7)
Lectura: Línea actual del raster (bit 8).
Escritura: Línea de interrupción del raster (bit 8).
$D012 Lectura: Línea actual del raster (bits 0-7).
Escritura: Línea de interrupción del raster (bits 0-7).
$D019 Registro de estado de interrupciones.
Lectura:
+ Bit 0: 1 = Línea de interrupción del raster alcanzada.
+ Bit 1: 1 = Colisión sprite-fondo.
+ Bit 2: 1 = Colisión sprite-sprite.
+ Bit 7: 1 = Interrupción generada sin admitir.
Escritura:
+ Bit 0: 1 = Admitir interrupción de raster.
+ Bit 1: 1 = Admitir interrupción de colisión sprite-fondo.
+ Bit 2: 1 = Admitir interrupción de colisión sprite-sprite.
$D01A Registro de control de interrupciones:
+ Bit 0: 1 = Activar interrupciones de raster.
+ Bit 1: 1 = Activar interrupciones de colisión sprite-fondo.
+ Bit 2: 1 = Activar interrupciones de colisión sprite-sprite.
$D01E Registro de colisión sprite-sprite.
Lectura: 1 = El sprite ha colisionado con otro sprite.
Escritura: 1 = Activar la detección de colisiones.
$D01F Registro de colisión sprite-fondo.
Lectura: 1 = El sprite ha colisionado con el fondo.
Escritura: 1 = Activar la detección de colisiones.

Podemos controlar mediante interrupciones la localización del raster y las colisiones de los sprites. Una vez configurados los registros del chip gráfico, se deberá configurar la gestión de las interrupciones indicando en $0314-$0315 la dirección de la subrutina encargada de ello. Por defecto el sistema tiene una subrutina en $EA31 para procesar interrupciones.

El raster representa el rayo catódico, en los monitores CRT, que recorre la pantalla actualizando cada punto de la imagen. La actualización recorre cada línea de la pantalla, de izquierda a derecha, y lo hace a una frecuencia de 50/60 Hz (PAL/NTSC). A partir de la línea 250, el rayo está fuera de la zona visible del VIC-II, limitándose a pintar el color del borde de la pantalla. Activando las interrupciones del raster, podemos actualizar la zona visible de la pantalla mientras no se esté actualizando el monitor, evitando así que parpadeen los gráficos de forma molesta. Para lograrlo hay que escribir, en los registros en $D011 (bit 7) y $D012, el número de línea que debe generar la interrupción. Luego se activa el bit 0 del registro $D01A y cuando se invoque la rutina de gestión de interrupciones se consulta el bit 0 de $D019 para saber si ha sido la causa de la interrupción actual, para finalmente escribir un 1 en el bit 0 para comunicar que la interrupción ha sido admitida.

En un acto visionario, varias décadas antes, el chip gráfico del C64 nos alerta si un sprite ha colisionado con otro sprite o con el contenido de la pantalla. La colisión consiste en detectar si uno o más píxeles activos, es decir, que no valgan cero, de un sprite ocupan la misma posición que otros píxeles activos. Para ello, tenemos que activar en $D01E y/o $D01F los sprites que queremos vigilar. Una vez activado podemos consultar estos dos registros para comprobar si ha colisionado los sprites que hemos activado, si necesidad obligatoria de usar interrupciones. Una limitación de este mecanismo es, en caso de haber una colisión múltiple, que no sabremos con qué otro sprite ha colisionado. No obstante, es de gran utilidad esta herramienta. Si queremos utilizar interrupciones con las colisiones, tenemos que activar los bits correspondientes en el registro $D01A. Entonces, cuando se invoque la rutina de gestión de interrupciones, consultaremos en $D019 los bits 1 y 2, para después escribir un 1 en ellos cuando queramos avisar al VIC-II que la interrupción ha sido admitida.

AVISO: A diferencia de otras interrupciones, por ejemplo las provocadas por las CIA1 y CIA2, las generadas por el VIC-II requieren escribir en el registro $D019 el valor 1 en aquellos bits de las interrupciones que hayamos gestionado. Si no se realiza esta acción, se seguirá invocando la rutina de gestión de interrupciones continuamente. Si sólo se van a manejar interrupciones del raster, se puede utilizar la instrucción LSR y comprobar con BCS si en efecto se ha producido una interrupción por el raster. El motivo por el que esto funciona es porque la instrucción LSR, que tiene 6 ciclos de ejecución, en el 4º lee de la memoria el valor, en el 5º escribe el valor leído en la memoria y rota el valor, y en el 6º escribe el nuevo valor obtenido.

Desplazamiento de pantalla

Muchos juegos requieren desplazar la pantalla (scroll) para ir recorriendo los niveles. Dada las limitaciones de velocidad, que había en la época, el C64 dispone de un mecanismo especial para facilitar el movimiento de la pantalla. Entonces, tenemos dos registros en memoria:

Bits $D011 $D016
0-2 Desplazamiento horizontal raster. Desplazamiento vertical raster.
3 Altura pantalla (0 = 24; 1 = 25). Ancho pantalla (0 = 38; 1 = 40).

Ya vimos previamente estos dos registros, pues se utilizan también para configurar los modos gráficos del VIC-II. Ahora lo que nos interesa es que podemos cambiar el tamaño de la pantalla, que por defecto es una tabla de 40x25 celdas. Podemos reducir con el bit 3 de $D011 a 24 filas la pantalla y con el de $D016 a 38 columnas. En ambos registros, los bits 0-2 controlan el desplazamiento de la pantalla a la hora de pintarla en el monitor, que serían de 0 a 7 píxeles, que se traduce en:

Scroll Columnas Filas
0 px Visibles: 1-38
Ocultas: 0 y 39
Visibles: 1-24
Oculta: 0
7 px Visibles: 0-37
Ocultas: 38 y 39
Visibles: 0-23
Oculta: 24

De este modo podemos mover en una dirección determinada y al alcanzar el tope, desplazar el contenido, meter la nueva fila o columna en el lateral correspondiente y restaurar a la posición de desplazamiento inicial, para volver a repetir el ciclo de movimiento:

Dirección Scroll inicial Scroll final
Abajo/Derecha 0 7
Arriba/Izquierda 7 0

Para verlo en acción, tenemos el siguiente ejemplo:

10  REM PINTAR PANTALLA
20  PRINT "{clear}{white}{215*40}";
30  PRINT "{215}{209*38}{215}";
40  FOR I=1 TO 21
50  PRINT "{215}{209}{space*36}{209}{215}";
60  NEXT I
70  PRINT "{215}{209*38}{215}";
80  PRINT "{215*39}";
90  POKE 2023,87
100 POKE 56295,1
110 REM CONFIGURAR PANTALLA
120 POKE 53265,(PEEK(53265) AND 240)
130 POKE 53270,(PEEK(53270) AND 240)
140 REM BUCLE DEL MOVIMIENTO
150 D=1:M=0
160 GET K$
170 IF K$=" " THEN 350
180 IF K$="1" THEN M=1
190 IF K$="2" THEN M=0
200 IF M=0 THEN 160
210 IF D=1 THEN MD=53265:LM=7:GOSUB 280
220 IF D=2 THEN MD=53270:LM=7:GOSUB 280
230 IF D=3 THEN MD=53265:LM=0:GOSUB 280
240 IF D=4 THEN MD=53270:LM=0:GOSUB 280
250 IF D>4 THEN D=1
260 GOTO 160
270 REM MOVER DESPLAZAMIENTO
280 V=PEEK(MD) AND 7
290 IF LM=7 AND V<7 THEN V=V+1
300 IF LM=0 AND V>0 THEN V=V-1
310 POKE MD,(PEEK(MD) AND 248)+V
320 IF V=LM THEN D=D+1
330 RETURN
340 REM FINALIZAR PROGRAMA
350 POKE 53265,(PEEK(53265) AND 240) OR 8
360 POKE 53270,(PEEK(53270) AND 240) OR 8

Después de pintar y configurar la pantalla, podemos con el teclado activar o desactivar el movimiento. Cuando está activado el movimiento (M=1), dependiendo de la dirección se accederá a uno de los dos registros y se tendrá como límite el valor 0 o el 7, se hacen los cálculos para actualizar el desplazamiento actual y si hay que cambiar de dirección, para repetir el bucle. Si se sale de la aplicación, pulsando espacio, se restaura la configuración de pantalla anterior.

Interfaz sonora

El chip SID se encarga de las capacidades sonoras del C64. Debido a sus avanzadas prestaciones, para lo que era común entre las máquinas de la época, gozó de gran popularidad y prestigio. Como resultado, ha tenido una gran influencia en el mundo de la demoscene y del chiptune.

Síntesis de sonido

Antes de explicar cómo funciona el chip de sonido, vamos a introducir algunos conceptos básicos sobre la síntesis de sonido. A modo de resumen, el sonido es un tipo de onda que se propaga por el aire, provocando variaciones en la presión que el cerebro interpreta como sonidos. Las ondas sonoras se pueden expresar como funciones matemáticas, algunas de ellas en relación a la posición y otras al tiempo. Las magnitudes físicas que cambian con el paso del tiempo y se emplean para transportar información se las conoce también como señales. Hay señales que son analógicas, que son funciones continuas, y otras que son digitales, que son funciones discretas. Por lo tanto, el sonido es un tipo de señal analógica, aunque se pueda digitalizar.

Existen varias propiedades que definen una señal. La amplitud o volumen nos indica la fuerza del sonido. La frecuencia es el número de repeticiones por unidad de tiempo que tiene la onda sonora. Además, toda señal se puede descomponer en diferentes frecuencias, teniendo por un lado una frecuencia principal F y por el otro múltiplos de dicha F, que son sus armónicos.

El timbre, o estructura de armónicos, depende de los armónicos que tenga el sonido y el nivel de energía de cada uno de ellos. Cada instrumento musical dispone de un timbre característico propio. El timbre y la forma de onda se relacionan entre sí. Algunas de las formas de onda básicas que tenemos son:

Formas de onda

Otra propiedad de la señal es su dinámica, que define la evolución temporal de cada nota. El volumen no suele ser fijo durante la duración de una nota, sino que puede variar con el tiempo. Este comportamiento se modela usando una envolvente ADSR:

Envolvente del sonido

Dentro del mundo de la síntesis de sonido hay diferentes modelos de generación. Por ejemplo, tenemos la síntesis FM, popularizada por Yamaha, que se incorporó en consolas como la Mega Drive y que fue muy popular en la música de los años 80 con instrumentos como el DX7. Pero el chip SID está en la familia de la síntesis substractiva, que es uno de los modelos más estándar de síntesis en instrumentos musicales, como es el caso del Minimoog, el MS-20 o el Jupiter-8, entre muchos otros. Esta forma de trabajar recibe una onda sonora y modifica su timbre seleccionando un filtro, para eliminar algunas de sus frecuencias. Algunos de los filtros elementales que existen son:

Filtrado de frecuencias

Con esto completamos lo que necesitamos saber sobre síntesis de sonido para trabajar con el chip SID.

Registros del SID

El chip de sonido dispone de tres osciladores independientes, que nos permiten tener tres voces a la vez y que podemos configurar con:

Voz 1 Voz 2 Voz 3 Descripción
$D400-$D401 $D407-$D408 $D40E-$D40F Frecuencia (Frequency).
$D402-$D403 $D409-$D40A $D410-$D411 Ancho de pulso (Pulse Width).
$D404 $D40B $D412 Registros de control:
+ Bit 0: 0 = Desactivar Gate (Release); 1 = Activar Gate (Attack-Decay-Sustain).
+ Bit 1: 1 = Activar sincronización con la voz anterior.
+ Bit 2: 1 = Activar modulación de anillo con la voz anterior (Ring Modulation).
+ Bit 3: 1 = Silenciar voz y reiniciar generador de ruido.
+ Bit 4: 1 = Activar onda triangular (Triangle).
+ Bit 5: 1 = Activar onda en sierra ascendente (Saw).
+ Bit 6: 1 = Activar onda cuadrada (Square).
+ Bit 7: 1 = Activar ruido (Noise).
$D405 $D40C $D413 Attack y Decay:
+ Bits 0-3: Nivel de Decay (0-15).
+ Bits 4-7: Nivel de Attack (0-15).
$D406 $D40D $D414 Sustain y Release:
+ Bits 0-3: Nivel de Release (0-15).
+ Bits 4-7: Volumen de Sustain (0-15).

Primero, hay que tener en cuenta que todos los registros anteriores son sólo de escritura. La única solución para poder consultar el estado, de este tipo de registros, es tener una copia en memoria con la que trabajar y que se envíe a los registros sonoros cuando se tenga que actualizar la información escrita.

Para usar cada voz hay que configurarla con el registro de control, seleccionando la combinación de formas de la onda que queremos tener con los bits 4-7 y abriendo o cerrando la gate del sonido con el bit 0. La “puerta” o gate es la señal con la que en un sintetizador se comunica al sistema si se ha pulsado una nota o no. Cuando se activa se inicia el modo Attack-Decay-Sustain, utilizando la frecuencia indicada en su registro, y cuando se desactiva se pasa al modo Release. Para configurar la envolvente de cada voz, tenemos que tener en cuenta la siguiente tabla de valores:

Nivel Binario Hexadecimal Attack Decay Release
0 0000 0 2 ms 6 ms 6 ms
1 0001 1 8 ms 24 ms 24 ms
2 0010 2 16 ms 48 ms 48 ms
3 0011 3 24 ms 72 ms 72 ms
4 0100 4 38 ms 114 ms 114 ms
5 0101 5 56 ms 168 ms 168 ms
6 0110 6 68 ms 204 ms 204 ms
7 0111 7 80 ms 240 ms 240 ms
8 1000 8 100 ms 300 ms 300 ms
9 1001 9 250 ms 750 ms 750 ms
10 1010 A 500 ms 1,5 s 1,5 s
11 1011 B 800 ms 2,4 s 2,4 s
12 1100 C 1 s 3 s 3 s
13 1101 D 3 s 9 s 9 s
14 1110 E 5 s 15 s 15 s
15 1111 F 8 s 24 s 24 s

Al ser el sustain un nivel de volumen, este tomará valores entre 0 y 15, donde el 0 es silencio y 15 el máximo volumen. Volviendo al registro de control, podemos usar la voz 3 para sincronizar (bit 1) y/o aplicar una modulación de anillo (bit 2) sobre la voz 1. Lo mismo podemos hacer con la voz 2 aplicada a la 1 y la voz 1 aplicada a la 3.

Cuando se selecciona la forma de onda cuadrada, se puede indicar el ancho del pulso, es decir, cuánto porcentaje del ciclo pasa la onda en la parte alta en lugar de la baja. Para ello hay que tener en cuenta la siguiente fórmula:

N=40,95×P N = 40,95 \times P

Donde P es el porcentaje que queremos, de modo que para un ancho del 100% obtenemos el valor 4.095 ($0FFF) y para el 50% sería 2.048 ($0800), etcétera. Hay que tener en cuenta que la parte menos significativa de la palabra iría en la dirección más baja del registro y la más significativa en la más alta.

Dirección Modo L/E Descripción
$D415 Escritura Frecuencia de corte (Cut Off).
Bits 0-2: Bits 0-2 del valor final.
$D416 Escritura Frecuencia de corte (Cut Off).
Bits 0-7: Bits 3-10 del valor final.
$D417 Escritura Control del filtro:
+ Bit 0: 1 = Voz 1 filtrada.
+ Bit 1: 1 = Voz 2 filtrada.
+ Bit 2: 1 = Voz 3 filtrada.
+ Bit 3: 1 = Voz externa filtrada.
+ Bits 4-7: Resonancia del filtro (Resonance).
$D418 Escritura Volumen y modos de filtrado:
+ Bits 0-3: Volumen.
+ Bit 4: 1 = Filtro de paso bajo activado (LPF).
+ Bit 5: 1 = Filtro de paso banda activado (BPF).
+ Bit 6: 1 = Filtro de paso alto activado (HPF).
+ Bit 7: Voz 3 silenciada.

De nuevo todos los registros son sólo de escritura. Podemos configurar el modo de filtrado que queremos, aunque se puede activar una combinación de ellos que compartirán la misma frecuencia de corte, para luego indicar qué voces van a ser filtradas o no. Además de la frecuencia de corte (cut off), que tiene 11 bits, se puede indicar el nivel de resonancia del filtrado. También tenemos un registro para configurar el nivel de volumen global del sonido.

Dirección Modo L/E Descripción
$D419 Lectura Conversor analógico-digital X.
$D41A Lectura Conversor analógico-digital Y.
$D41B Lectura Salida de la voz 3.
$D41C Lectura Salida de la envolvente ADSR para la voz 3.

Por último tenemos estos registros que son sólo de lectura. Los dos primeros se utilizan para la E/S de periféricos que utilizan potenciómetros. El registro $D41B tiene una utilidad secundaria, ya que se puede utilizar como generador de números aleatorios cuando la voz 3 está configurada con la forma de onda de ruido.

Notas musicales

La escala cromática dispone de doce notas:

Nombre 0 1 2 3 4 5 6 7 8 9 10 11
Inglés C C♯/D♭ D D♯/E♭ E F F♯/G♭ G G♯/A♭ A A♯/B♭ B
Latino Do Do♯/Re♭ Re Re♯/Mi♭ Mi Fa Fa♯/Sol♭ Sol Sol♯/La♭ La La♯/Si♭ Si

El símbolo sostenido (♯) añade medio tono a una nota y el bemol (♭) le resta medio tono. Entonces, para calcular las frecuencias de cada nota, tenemos que partir de una frecuencia base para la nota LA en la cuarta octava de un piano (A4), que es la octava central en un piano de 88 teclas (las tres primeras teclas son de la octava cero y la última de la octava ocho). La frecuencia habitual para afinar la nota A4 es la 440 Hz, pero también existen históricamente otras afinaciones que utilizan la 435 Hz por ejemplo. Con esta frecuencia base podemos usar la siguiente fórmula:

f(n)=2n4912×440 Hz f(n) = 2^{\frac{n-49}{12}} \times 440\ \text{Hz}

Donde n es el número de la nota, representando como la nota cero a la tecla A0. Esto nos permite calcular desde C0 hasta C8, pero el chip SID nos permite emitir sonidos desde la octava 0 hasta la 7, porque sus osciladores soportan frecuencias de 16 Hz a 4 kHz, por lo que no necesitamos calcular todas las notas.

También hay otro método para calcular las frecuencias, siempre que tengamos las frecuencias de cada nota en una octava. Para subir una octava una nota, hay que duplicar su frecuencia, y por lo tanto, para bajar una octava hay que dividir a la mitad su frecuencia. De esto obtenemos que:

Octava 0 1 2 3 4 5 6 7 8
0 - 2 4 8 16 32 64 128 256
1 2 - 2 4 8 16 32 64 128
2 4 2 - 2 4 8 16 32 64
3 8 4 2 - 2 4 8 16 32
4 16 8 4 2 - 2 4 8 16
5 32 16 8 4 2 - 2 4 8
6 64 32 16 8 4 2 - 2 4
7 128 64 32 16 8 4 2 - 2
8 256 128 64 32 16 8 4 2 -

La tabla de frecuencias, para el tono estándar (440 Hz) y el francés (435 Hz), es la siguiente:

Nota Frecuencia (440 Hz) Frecuencia (435 Hz) Nota Frecuencia (440 Hz) Frecuencia (435 Hz)
C0 15,4338531642539 15,2584684692055 C4 246,941650628062 244,135495507289
C#0 16,3515978312874 16,1657842195682 C#4 261,625565300599 258,652547513092
D0 17,3239144360545 17,1270517720084 D4 277,182630976872 274,032828352135
D#0 18,354047994838 18,1454792676239 D#4 293,664767917408 290,327668281982
E0 19,4454364826301 19,2244656135093 E4 311,126983722081 307,591449816148
F0 20,6017223070544 20,3676118262924 F4 329,62755691287 325,881789220678
F#0 21,8267644645627 21,5787330501927 F#4 349,228231433004 345,259728803083
G0 23,1246514194772 22,8618712897104 G4 369,994422711634 365,789940635366
G#0 24,4997147488593 24,2213088994405 G#4 391,995435981749 387,540942391048
A0 25,9565435987466 25,6615828760336 A4 415,304697579945 410,585326016537
A#0 27,5 27,1875 A#4 440 435
B0 29,1352350948806 28,8041528778933 B4 466,16376151809 460,866446046293
C1 30,8677063285078 30,5169369384111 C5 493,883301256124 488,270991014577
C#1 32,7031956625748 32,3315684391365 C#5 523,251130601197 517,305095026184
D1 34,647828872109 34,2541035440169 D5 554,365261953744 548,06565670427
D#1 36,7080959896759 36,2909585352478 D#5 587,329535834815 580,655336563965
E1 38,8908729652601 38,4489312270185 E5 622,253967444162 615,182899632296
F1 41,2034446141088 40,7352236525848 F5 659,25511382574 651,763578441356
F#1 43,6535289291255 43,1574661003854 F#5 698,456462866008 690,519457606167
G1 46,2493028389543 45,7237425794207 G5 739,988845423269 731,579881270732
G#1 48,9994294977187 48,442617798881 G#5 783,990871963499 775,081884782095
A1 51,9130871974931 51,3231657520671 A5 830,60939515989 821,170652033073
A#1 55 54,375 A#5 880 870
B1 58,2704701897613 57,6083057557867 B5 932,32752303618 921,732892092587
C2 61,7354126570155 61,0338738768222 C6 987,766602512248 976,541982029154
C#2 65,4063913251497 64,663136878273 C#6 1.046,50226120239 1.034,61019005237
D2 69,295657744218 68,5082070880337 D6 1.108,73052390749 1.096,13131340854
D#2 73,4161919793519 72,5819170704956 D#6 1.174,65907166963 1.161,31067312793
E2 77,7817459305202 76,897862454037 E6 1.244,50793488832 1.230,36579926459
F2 82,4068892282175 81,4704473051696 F6 1.318,51022765148 1.303,52715688271
F#2 87,307057858251 86,3149322007708 F#6 1.396,91292573202 1.381,03891521233
G2 92,4986056779086 91,4474851588415 G6 1.479,97769084654 1.463,15976254146
G#2 97,9988589954373 96,8852355977619 G#6 1.567,981743927 1.550,16376956419
A2 103,826174394986 102,646331504134 A6 1.661,21879031978 1.642,34130406615
A#2 110 108,75 A#6 1.760 1.740
B2 116,540940379522 115,216611511573 B6 1.864,65504607236 1.843,46578418517
C3 123,470825314031 122,067747753644 C7 1.975,5332050245 1.953,08396405831
C#3 130,812782650299 129,326273756546 C#7 2.093,00452240479 2.069,22038010473
D3 138,591315488436 137,016414176067 D7 2.217,46104781498 2.192,26262681708
D#3 146,832383958704 145,163834140991 D#7 2.349,31814333926 2.322,62134625586
E3 155,56349186104 153,795724908074 E7 2.489,01586977665 2.460,73159852919
F3 164,813778456435 162,940894610339 F7 2.637,02045530296 2.607,05431376543
F#3 174,614115716502 172,629864401542 F#7 2.793,82585146403 2.762,07783042467
G3 184,997211355817 182,894970317683 G7 2.959,95538169308 2.926,31952508293
G#3 195,997717990875 193,770471195524 G#7 3.135,96348785399 3.100,32753912838
A3 207,652348789973 205,292663008268 A7 3.322,43758063956 3.284,68260813229
A#3 220 217,5 A#7 3.520 3.480
B3 233,081880759045 230,433223023147 B7 3.729,31009214472 3.686,93156837035

Con todas las notas calculadas, la cosa no se queda ahí para el chip SID, ya que la frecuencia es representada como un valor entero sin signo de 16 bits. Para convertir, una frecuencia a valores admitidos por los registros del chip, usaremos la siguiente fórmula:

N=F×224R=F×16.777.216R N = \dfrac{F \times 2^{24}}{\mathit{R}} = \dfrac{F \times 16.777.216}{\mathit{R}}

Donde F es la frecuencia de la nota y R la frecuencia del reloj de la máquina. Recordemos que la versión PAL tiene una frecuencia de 985.248,6 Hz y la NTSC de 1.022.727,3 Hz. El resultado es que tenemos las siguientes fórmulas para cada modelo:

Modo Fórmula Rango
PAL n = f * (18 * 16777216) / 17734475 0 Hz - 3.848 Hz
NTSC n = f * (14 * 16777216) / 14318182 0 Hz - 3.995 Hz

La tabla de valores para las frecuencias es la siguiente:

Nota Manual PAL (440 Hz) NTSC (440 Hz) PAL (435 Hz) NTSC (435 Hz)
C0 268 ($010C) 263 ($0107) 253 ($00FD) 260 ($0104) 250 ($00FA)
C#0 284 ($011C) 278 ($0116) 268 ($010C) 275 ($0113) 265 ($0109)
D0 301 ($012D) 295 ($0127) 284 ($011C) 292 ($0124) 281 ($0119)
D#0 318 ($013E) 313 ($0139) 301 ($012D) 309 ($0135) 298 ($012A)
E0 337 ($0151) 331 ($014B) 319 ($013F) 327 ($0147) 315 ($013B)
F0 358 ($0166) 351 ($015F) 338 ($0152) 347 ($015B) 334 ($014E)
F#0 379 ($017B) 372 ($0174) 358 ($0166) 367 ($016F) 354 ($0162)
G0 401 ($0191) 394 ($018A) 379 ($017B) 389 ($0185) 375 ($0177)
G#0 425 ($01A9) 417 ($01A1) 402 ($0192) 412 ($019C) 397 ($018D)
A0 451 ($01C3) 442 ($01BA) 426 ($01AA) 437 ($01B5) 421 ($01A5)
A#0 477 ($01DD) 468 ($01D4) 451 ($01C3) 463 ($01CF) 446 ($01BE)
B0 506 ($01FA) 496 ($01F0) 478 ($01DE) 490 ($01EA) 473 ($01D9)
C1 536 ($0218) 526 ($020E) 506 ($01FA) 520 ($0208) 501 ($01F5)
C#1 568 ($0238) 557 ($022D) 536 ($0218) 551 ($0227) 530 ($0212)
D1 602 ($025A) 590 ($024E) 568 ($0238) 583 ($0247) 562 ($0232)
D#1 637 ($027D) 625 ($0271) 602 ($025A) 618 ($026A) 595 ($0253)
E1 675 ($02A3) 662 ($0296) 638 ($027E) 655 ($028F) 631 ($0277)
F1 716 ($02CC) 702 ($02BE) 676 ($02A4) 694 ($02B6) 668 ($029C)
F#1 758 ($02F6) 743 ($02E7) 716 ($02CC) 735 ($02DF) 708 ($02C4)
G1 803 ($0323) 788 ($0314) 759 ($02F7) 779 ($030B) 750 ($02EE)
G#1 851 ($0353) 834 ($0342) 804 ($0324) 825 ($0339) 795 ($031B)
A1 902 ($0386) 884 ($0374) 852 ($0354) 874 ($036A) 842 ($034A)
A#1 955 ($03BB) 937 ($03A9) 902 ($0386) 926 ($039E) 892 ($037C)
B1 1012 ($03F4) 992 ($03E0) 956 ($03BC) 981 ($03D5) 945 ($03B1)
C2 1072 ($0430) 1051 ($041B) 1013 ($03F5) 1039 ($040F) 1001 ($03E9)
C#2 1136 ($0470) 1114 ($045A) 1073 ($0431) 1101 ($044D) 1061 ($0425)
D2 1204 ($04B4) 1180 ($049C) 1137 ($0471) 1167 ($048F) 1124 ($0464)
D#2 1275 ($04FB) 1250 ($04E2) 1204 ($04B4) 1236 ($04D4) 1191 ($04A7)
E2 1351 ($0547) 1324 ($052C) 1276 ($04FC) 1309 ($051D) 1261 ($04ED)
F2 1432 ($0598) 1403 ($057B) 1352 ($0548) 1387 ($056B) 1336 ($0538)
F#2 1517 ($05ED) 1487 ($05CF) 1432 ($0598) 1470 ($05BE) 1416 ($0588)
G2 1607 ($0647) 1575 ($0627) 1517 ($05ED) 1557 ($0615) 1500 ($05DC)
G#2 1703 ($06A7) 1669 ($0685) 1608 ($0648) 1650 ($0672) 1589 ($0635)
A2 1804 ($070C) 1768 ($06E8) 1703 ($06A7) 1748 ($06D4) 1684 ($0694)
A#2 1911 ($0777) 1873 ($0751) 1804 ($070C) 1852 ($073C) 1784 ($06F8)
B2 2025 ($07E9) 1985 ($07C1) 1912 ($0778) 1962 ($07AA) 1890 ($0762)
C3 2145 ($0861) 2103 ($0837) 2025 ($07E9) 2079 ($081F) 2002 ($07D2)
C#3 2273 ($08E1) 2228 ($08B4) 2146 ($0862) 2202 ($089A) 2122 ($084A)
D3 2408 ($0968) 2360 ($0938) 2274 ($08E2) 2333 ($091D) 2248 ($08C8)
D#3 2551 ($09F7) 2500 ($09C4) 2409 ($0969) 2472 ($09A8) 2381 ($094D)
E3 2703 ($0A8F) 2649 ($0A59) 2552 ($09F8) 2619 ($0A3B) 2523 ($09DB)
F3 2864 ($0B30) 2807 ($0AF7) 2704 ($0A90) 2775 ($0AD7) 2673 ($0A71)
F#3 3034 ($0BDA) 2973 ($0B9D) 2864 ($0B30) 2940 ($0B7C) 2832 ($0B10)
G3 3215 ($0C8F) 3150 ($0C4E) 3035 ($0BDB) 3114 ($0C2A) 3000 ($0BB8)
G#3 3406 ($0D4E) 3338 ($0D0A) 3215 ($0C8F) 3300 ($0CE4) 3179 ($0C6B)
A3 3608 ($0E18) 3536 ($0DD0) 3406 ($0D4E) 3496 ($0DA8) 3368 ($0D28)
A#3 3823 ($0EEF) 3746 ($0EA2) 3609 ($0E19) 3704 ($0E78) 3568 ($0DF0)
B3 4050 ($0FD2) 3969 ($0F81) 3824 ($0EF0) 3924 ($0F54) 3780 ($0EC4)
C4 4291 ($10C3) 4205 ($106D) 4051 ($0FD3) 4157 ($103D) 4005 ($0FA5)
C#4 4547 ($11C3) 4455 ($1167) 4292 ($10C4) 4404 ($1134) 4243 ($1093)
D4 4817 ($12D1) 4720 ($1270) 4547 ($11C3) 4666 ($123A) 4495 ($118F)
D#4 5103 ($13EF) 5001 ($1389) 4817 ($12D1) 4944 ($1350) 4763 ($129B)
E4 5407 ($151F) 5298 ($14B2) 5104 ($13F0) 5238 ($1476) 5046 ($13B6)
F4 5728 ($1660) 5613 ($15ED) 5407 ($151F) 5549 ($15AD) 5346 ($14E2)
F#4 6069 ($17B5) 5947 ($173B) 5729 ($1661) 5879 ($16F7) 5664 ($1620)
G4 6430 ($191E) 6300 ($189C) 6070 ($17B6) 6229 ($1855) 6001 ($1771)
G#4 6812 ($1A9C) 6675 ($1A13) 6430 ($191E) 6599 ($19C7) 6357 ($18D5)
A4 7217 ($1C31) 7072 ($1BA0) 6813 ($1A9D) 6992 ($1B50) 6735 ($1A4F)
A#4 7647 ($1DDF) 7492 ($1D44) 7218 ($1C32) 7407 ($1CEF) 7136 ($1BE0)
B4 8101 ($1FA5) 7938 ($1F02) 7647 ($1DDF) 7848 ($1EA8) 7560 ($1D88)
C5 8583 ($2187) 8410 ($20DA) 8102 ($1FA6) 8314 ($207A) 8010 ($1F4A)
C#5 9094 ($2386) 8910 ($22CE) 8584 ($2188) 8809 ($2269) 8486 ($2126)
D5 9634 ($25A2) 9440 ($24E0) 9094 ($2386) 9333 ($2475) 8991 ($231F)
D#5 10207 ($27DF) 10001 ($2711) 9635 ($25A3) 9888 ($26A0) 9525 ($2535)
E5 10814 ($2A3E) 10596 ($2964) 10208 ($27E0) 10476 ($28EC) 10092 ($276C)
F5 11457 ($2CC1) 11226 ($2BDA) 10815 ($2A3F) 11098 ($2B5A) 10692 ($29C4)
F#5 12139 ($2F6B) 11894 ($2E76) 11458 ($2CC2) 11758 ($2DEE) 11328 ($2C40)
G5 12860 ($323C) 12601 ($3139) 12139 ($2F6B) 12458 ($30AA) 12001 ($2EE1)
G#5 13625 ($3539) 13350 ($3426) 12861 ($323D) 13198 ($338E) 12715 ($31AB)
A5 14435 ($3863) 14144 ($3740) 13626 ($353A) 13983 ($369F) 13471 ($349F)
A#5 15294 ($3BBE) 14985 ($3A89) 14436 ($3864) 14815 ($39DF) 14272 ($37C0)
B5 16203 ($3F4B) 15876 ($3E04) 15294 ($3BBE) 15696 ($3D50) 15120 ($3B10)
C6 17167 ($430F) 16820 ($41B4) 16204 ($3F4C) 16629 ($40F5) 16020 ($3E94)
C#6 18188 ($470C) 17820 ($459C) 17167 ($430F) 17618 ($44D2) 16972 ($424C)
D6 19269 ($4B45) 18880 ($49C0) 18188 ($470C) 18665 ($48E9) 17981 ($463D)
D#6 20415 ($4FBF) 20003 ($4E23) 19270 ($4B46) 19775 ($4D3F) 19051 ($4A6B)
E6 21629 ($547D) 21192 ($52C8) 20415 ($4FBF) 20951 ($51D7) 20183 ($4ED7)
F6 22915 ($5983) 22452 ($57B4) 21629 ($547D) 22197 ($56B5) 21384 ($5388)
F#6 24278 ($5ED6) 23787 ($5CEB) 22916 ($5984) 23517 ($5BDD) 22655 ($587F)
G6 25721 ($6479) 25202 ($6272) 24278 ($5ED6) 24915 ($6153) 24002 ($5DC2)
G#6 27251 ($6A73) 26700 ($684C) 25722 ($647A) 26397 ($671D) 25429 ($6355)
A6 28871 ($70C7) 28288 ($6E80) 27251 ($6A73) 27966 ($6D3E) 26942 ($693E)
A#6 30588 ($777C) 29970 ($7512) 28872 ($70C8) 29629 ($73BD) 28544 ($6F80)
B6 32407 ($7E97) 31752 ($7C08) 30589 ($777D) 31391 ($7A9F) 30241 ($7621)
C7 34334 ($861E) 33640 ($8368) 32407 ($7E97) 33258 ($81EA) 32039 ($7D27)
C#7 36376 ($8E18) 35641 ($8B39) 34334 ($861E) 35236 ($89A4) 33944 ($8498)
D7 38539 ($968B) 37760 ($9380) 36376 ($8E18) 37331 ($91D3) 35963 ($8C7B)
D#7 40830 ($9F7E) 40005 ($9C45) 38539 ($968B) 39551 ($9A7F) 38101 ($94D5)
E7 43258 ($A8FA) 42384 ($A590) 40831 ($9F7F) 41902 ($A3AE) 40367 ($9DAF)
F7 45830 ($B306) 44904 ($AF68) 43259 ($A8FB) 44394 ($AD6A) 42767 ($A70F)
F#7 48556 ($BDAC) 47574 ($B9D6) 45831 ($B307) 47034 ($B7BA) 45310 ($B0FE)
G7 51443 ($C8F3) 50403 ($C4E3) 48556 ($BDAC) 49831 ($C2A7) 48004 ($BB84)
G#7 54502 ($D4E6) 53400 ($D098) 51444 ($C8F4) 52794 ($CE3A) 50859 ($C6AB)
A7 57743 ($E18F) 56576 ($DD00) 54503 ($D4E7) 55933 ($DA7D) 53883 ($D27B)
A#7 61176 ($EEF8) 59940 ($EA24) 57743 ($E18F) 59259 ($E77B) 57087 ($DEFF)
B7 64814 ($FD2E) 63504 ($F810) 61177 ($EEF9) 62783 ($F53F) 60482 ($EC42)

Como los cálculos de coma flotante derivan en diferentes grados de errores de precisión, podemos encontrar otras tablas con diferentes valores precalculados. Por ejemplo, tenemos la de la referencia del MOS 6581, o esta versión que incorpora el afinado de 435 Hz, o esta otra versión que incorpora las versiones para PAL y NTSC.

Reproduciendo música

La composición musical usa una notación estándar y dispone del protocolo MIDI para el mundo digital. Sin embargo, a principio de los años 80 el MIDI no era todavía una realidad palpable. Por ello la composición musical en el C64 implica tener un formato propio para describir la música y una implementación para transformar las notas descritas en frecuencias, mientras se controla su duración. Por ejemplo:

10  REM DATOS FRECUENCIAS PAL (OCTAVA 7)
20  DATA 33640,35641,37760,40005,42384,44904
30  DATA 47574,50403,53400,56576,59940,63504
40  REM DATOS CANCION (FRERE JACQUES)
50  REM NOTA=(OCTAVA,NOTA,DURACION)
60  REM (C=0,D=2,E=4,F=5,G=7,A=9,B=11)
70  REM (R=64,B=32,N=16,C=8,SC=4,F=2,SF=1)
80  REM [=== (C4,D4,E4,C4)X2 ===]
90  DATA 4,0,16, 4,2,16, 4,4,16, 4,0,16
100 DATA 4,0,16, 4,2,16, 4,4,16, 4,0,16
110 REM [=== (E4,F4,G4*2)X2 ===]
120 DATA 4,4,16, 4,5,16, 4,7,32
130 DATA 4,4,16, 4,5,16, 4,7,32
140 REM [=== (G4/2,A4/2,G4/2,F4/2,E4,C4)X2 ===]
150 DATA 4,7,8, 4,9,8, 4,7,8, 4,5,8, 4,4,16, 4,0,16
160 DATA 4,7,8, 4,9,8, 4,7,8, 4,5,8, 4,4,16, 4,0,16
170 REM [=== (C4,G4,C4*2)X2 ===]
180 DATA 4,0,16, 3,7,16, 4,0,32
190 DATA 4,0,16, 3,7,16, 4,0,32
200 REM CONFIGURAR SID
210 DIM F(11),G(7)
220 FOR I=0 TO 11
230 READ V:F(I)=V
240 NEXT I
250 FOR I=0 TO 7
260 G(I)=2^(7-I)
270 NEXT I
280 V1=54272      : REM DIR. BASE VOZ 1
290 POKE V1+2,0   : REM ANCHO PULSO (LOW)
300 POKE V1+3,8   : REM ANCHO PULSO (HIGH)
310 POKE V1+4,80  : REM REG. CONTROL
320 POKE V1+5,128 : REM ATTACK/DECAY
330 POKE V1+6,248 : REM SUSTAIN/RELEASE
340 POKE 54296,15 : REM VOLUMEN GLOBAL
350 REM REPRODUCIR CANCION
360 CS=0:MS=32
370 READ OC,NT,LN
380 POKE V1+4,80
390 NF=F(NT)/G(OC)
400 HB=INT(NF/256)
410 POKE V1,NF-HB*256
420 POKE V1+1,HB
430 POKE V1+4,81
440 FOR I=0 TO LN*10:NEXT I
450 CS=CS+1
460 IF CS<MS THEN 370
470 POKE V1+4,80

Este programa reproduce una canción muy sencilla, configurando una voz con una mezcla de la onda cuadrada y la triangular. Se cargan las frecuencias de la séptima octava, para poder dividir entre múltiplos de 2 para calcular el resto de frecuencias. Para reproducirla se lee cada nota de la “partitura”, que indica su octava, la nota dentro de la escala y su duración.

A pesar de lo rudimentario, del método utilizando en el ejemplo, no deja de ser un tanto farragoso describir y reproducir música. El chip SID es capaz de mucho más y por ello se utilizan programas, como CheeseCutter o GoatTracker 2, para componer música en formato SID. Este formato se originó para poder reproducir música con un chip SID emulado vía software en otras máquinas. Los ficheros incorporan en su interior un programa, en código máquina del 6510, para reproducir el sonido.

Esta facilidad no está disponible en BASIC, por lo que hay que recurrir al ensamblador para trabajar con ficheros SID. Si queremos incorporar una canción como parte del ejecutable, tenemos que incluir lo siguiente:

* = $C800
incbin "musica.sid",126

Esto incluye como parte del ejecutable un fichero binario, en la dirección $C800, saltando 126 bytes de su contenido. Estos bytes corresponden con la cabecera que da una descripción útil del contenido del fichero y se usa para los reproductores de música SID en otras plataformas, es por ello que no vamos a necesitar esa parte para el C64. Una vez cargado en memoria el programa, hay que invocar la subrutina que inicializa la canción, para poder reproducirla. Si se trata de un fichero que contiene varias canciones, usando el registro A podemos seleccionar aquella que necesitemos. Una vez inicializada la canción, se configura la rutina de gestión de interrupciones y dentro de esta se invoca la subrutina que actualiza la reproducción de la canción.

Como los ficheros SID tienen en su interior el programa ya compilado, necesitan estar ubicados en la dirección de memoria que indica la cabecera, para poder ejecutarse correctamente. Si esto supusiera un problema, porque necesitamos cambiar de ubicación en la memoria la canción, existe una herramienta llamada SIDreloc que permite reubicar un fichero SID.

Entrada y salida

La gestión a bajo nivel de la E/S del C64 se realiza desde los chips CIA1 y CIA2. Con estos chips podemos controlar múltiples tipos de dispositivos, pero nos vamos a centrar en tres áreas: el teclado, los joysticks y la gestión de temporizadores.

Direcciones de memoria

El chip CIA1 se puede acceder desde las siguientes direcciones de memoria:

Dirección Hexadecimal Descripción
56320 $DC00 Puerto A: Matriz de teclado y joystick 2.
56321 $DC01 Puerto B: Matriz de teclado y joystick 1.
56322 $DC02 Configuración del puerto A.
56323 $DC03 Configuración del puerto B.
56324-56325 $DC04-$DC05 Cuenta atrás A.
56326-56327 $DC06-$DC07 Cuenta atrás B.
56328 $DC08 TOD/Alarma: Décimas.
56329 $DC09 TOD/Alarma: Segundos.
56330 $DC0A TOD/Alarma: Minutos.
56331 $DC0B TOD/Alarma: Horas.
56332 $DC0C E/S de datos por el puerto de usuario.
56333 $DC0D Registro de control de interrupciones.
56334 $DC0E Registro de control de la cuenta atrás A.
56335 $DC0F Registro de control de la cuenta atrás B.

En el mapa de memoria viene indicado cuáles son las posiciones de memoria del CIA2, que sirven para gestionar el puerto serie y el puerto de usuario entre otras cosas. Las interrupciones del CIA1 están conectadas al pin IRQ de la CPU, por lo tanto son enmascarables, mientras que las del CIA2 van al pin NMI y no son enmascarables. Las interrupciones se explican con más detalle en la sección del lenguaje ensamblador.

Teclado

Para poder comprobar si una tecla está pulsada necesitamos consultar la matriz del teclado:

A/B 0 1 2 3 4 5 6 7
0 INST/DEL RETURN ⇐CRSR⇒ F7 F1 F3 F5 ⇑CRSR⇓
1 3 W A 4 Z S E SHIFT (Iz.)
2 5 R D 6 C F T X
3 7 Y G 8 B H U V
4 9 I J 0 M K O N
5 + P L - . : @ ,
6 £ * ; CLR/HOME SHIFT (De.) = /
7 1 CTRL 2 SPACE C= Q RUN/STOP

Esta matriz cubre 64 de las 66 teclas del teclado (RESTORE está asociada al pin NMI y SHIFT LOCK está ligada al SHIFT izquierdo). Las filas son los bits que podemos configurar con el puerto A ($DC00) y las columnas los bits que podemos leer del puerto B ($DC01). Para ello hay que configurar el puerto A de escritura ($DC02 = $FF) y el puerto B de lectura ($DC03 = $00). Luego hay que asignar un 0 al bit que representa la fila correspondiente en el puerto A, para comprobar en el puerto B el estado de las teclas de dicha fila (0 = pulsada; 1 = no pulsada). Por ejemplo:

10  REM INICIALIZAR DATOS
20  DIM K$(7,7), M%(7)
30  N%=1
40  FOR I=0 TO 7
50  M%(I)=N%
60  N%=N%*2
70  NEXT I
80  K$(0,0)="DEL":K$(0,1)="RET":K$(0,2)="LRC":K$(0,3)="F7"
90  K$(0,4)="F1":K$(0,5)="F3":K$(0,6)="F5":K$(0,7)="UDC"
100 K$(1,0)="3":K$(1,1)="W":K$(1,2)="A":K$(1,3)="4"
110 K$(1,4)="Z":K$(1,5)="S":K$(1,6)="E":K$(1,7)="LSH"
120 K$(2,0)="5":K$(2,1)="R":K$(2,2)="D":K$(2,3)="6"
130 K$(2,4)="C":K$(2,5)="F":K$(2,6)="T":K$(2,7)="X"
140 K$(3,0)="7":K$(3,1)="Y":K$(3,2)="G":K$(3,3)="8"
150 K$(3,4)="B":K$(3,5)="H":K$(3,6)="U":K$(3,7)="V"
160 K$(4,0)="9":K$(4,1)="I":K$(4,2)="J":K$(4,3)="0"
170 K$(4,4)="M":K$(4,5)="K":K$(4,6)="O":K$(4,7)="N"
180 K$(5,0)="+":K$(5,1)="P":K$(5,2)="L":K$(5,3)="-"
190 K$(5,4)=".":K$(5,5)=":":K$(5,6)="@":K$(5,7)=","
200 K$(6,0)="{pound}":K$(6,1)="*":K$(6,2)=";":K$(6,3)="HOM"
210 K$(6,4)="RSH":K$(6,5)="=":K$(6,6)="^":K$(6,7)="/"
220 K$(7,0)="1":K$(7,1)="{arrow left}":K$(7,2)="CTR":K$(7,3)="2"
230 K$(7,4)="SPC":K$(7,5)="C=":K$(7,6)="Q":K$(7,7)="STP"
240 REM INICIALIZAR PANTALLA
250 PRINT "{clear}";
260 POKE 646,1    : REM COLOR CURSOR
270 POKE 53281,0  : REM COLOR FONDO
280 POKE 53280,12 : REM COLOR BORDE
290 FOR I=0 TO 7
300 FOR J=0 TO 7
310 PRINT TAB(5*J);K$(I,J);
320 NEXT J:PRINT
330 NEXT I
340 REM CONFIGURAR TECLADO
350 POKE 56322,255
360 POKE 56323,0
370 REM BUCLE PRINCIPAL
380 X=55296 : REM BUFFER DE COLOR
390 FOR I=0 TO 7
400 POKE 56320,255-M%(I)
410 K%=PEEK(56321)
420 FOR J=0 TO 7
430 C%=1:IF (K% AND M%(J))=0 THEN C%=10
440 POKE X,C%:X=X+1
450 POKE X,C%:X=X+1
460 POKE X,C%:X=X+3
470 NEXT J,I
480 GOTO 380

Aquí tenemos un programa BASIC que las líneas 20-230 inicializan la información que va a utilizar, las líneas 250-330 pinta en pantalla una tabla de las teclas de la matriz, las líneas 350-360 configura los puertos de la CIA1, por ultimo, el resto de líneas es un bucle infinito que selecciona en 400 la fila de la matriz a inspeccionar y en 410 obtiene su valor, para comprobar las teclas pulsadas de la fila y cambiar de color el texto si está pulsada la tecla. Lo que se puede observar es que intentar consultar el teclado así desde BASIC es muy lento e impracticable. Mientras que el mismo ejemplo en C, usando el compilador CC65:

#include <stdio.h>
#include <conio.h>

#define POKE(A,X)  (*(unsigned char *)A) = (X)
#define PEEK(A)    (*(unsigned char *)A)
#define POKEW(A,X) (*(unsigned int *)A) = (X)
#define PEEKW(A)   (*(unsigned int *)A)

#define MAX_ROWS 8
#define MAX_COLS 8

void main() {
    unsigned int offset;
    unsigned char aux, row, col, mask[MAX_COLS], keys, color;
    
    aux = 1;
    for(col = 0; col < MAX_COLS; ++col) {
        mask[col] = aux;
        aux *= 2;
    }
    
    POKE(53272, 21); // Fuente mayúsculas
    POKE(  646,  1); // Color cursor
    POKE(53281,  0); // Color fondo
    POKE(53280, 12); // Color borde
    
    clrscr();
    printf("keyboard test!\n");
    printf("\n");
    printf("del  ret  lrc  f7   f1   f3   f5   udc\n");
    printf("3    w    a    4    z    s    e    lsh\n");
    printf("5    r    d    6    c    f    t    x\n");
    printf("7    y    g    8    b    h    u    v\n");
    printf("9    i    j    0    m    k    o    n\n");
    printf("+    p    l    -    .    :    @    ,\n");
    printf("%c    *    ;    hom  rsh  =    %c    /\n", 92, 94);
    printf("1    %c    ctr  2    spc  c=   q    run\n", 95);
    
    POKE(56322, 255); // CIA1 Puerto A en escritura
    POKE(56323,   0); // CIA1 Puerto B en lectura
    
    while(1) {
        offset = 55296 + 80;
        for(row = 0; row < MAX_ROWS; ++row) {
            POKE(56320, 255 - mask[row]);
            keys = PEEK(56321);
            for(col = 0; col < MAX_COLS; ++col) {
                if ((keys & mask[col]) == 0) {
                    color = 10;
                } else {
                    color = 1;
                }
                for(aux = 0; aux < 5; ++aux, ++offset) {
                    POKE(offset, color);
                }
            }
        }
    }
}

Podemos observar que sin embargo sí reacciona de forma fluida la captura de las pulsaciones de las teclas. Esto conlleva a recurrir desde BASIC al comando GET para la entrada por teclado, intentando no forzar la máquina demasiado frente a la lentitud del interprete.

Joysticks

El C64 permite tener dos joysticks conectados, uno en cada puerto de control del lateral de la máquina. Para poder comprobar el estado del joystick del puerto de control 1 usaremos el puerto B ($DC01) y para el joystick del puerto de control 2 usaremos el puerto A ($DC00). En cada puerto podemos comprobar los cinco elementos del joystick (0 = pulsada; 1 = no pulsada):

En cuanto a la configuración de los puertos, es irrelevante ya que en modo escritura también se puede leer los puertos, además si configuramos como lectura el puerto A, no se podrán detectar las pulsaciones del teclado. Para comprenderlo mejor, veamos el siguiente ejemplo:

10  REM INICIALIZAR DATOS
20  DIM M%(4)
30  N%=1
40  FOR I=0 TO 4
50  M%(I)=N%
60  N%=N%*2
70  NEXT I
80  REM INICIALIZAR PANTALLA
90  PRINT "{clear}";
100 POKE 646,1    : REM COLOR CURSOR
110 POKE 53281,0  : REM COLOR FONDO
120 POKE 53280,12 : REM COLOR BORDE
130 PRINT "J1: U D L R F"
140 PRINT "J2: U D L R F"
150 REM CONFIGURAR TECLADO
160 POKE 56322,255
170 POKE 56323,0
180 REM BUCLE PRINCIPAL
190 X=55296+4  : REM BUFFER DE COLOR
200 Y=55296+44 : REM BUFFER DE COLOR
210 J2%=PEEK(56320)
220 J1%=PEEK(56321)
230 FOR I=0 TO 4
240 C%=1:IF (J1% AND M%(I))=0 THEN C%=10
250 POKE X,C%:X=X+2
260 C%=1:IF (J2% AND M%(I))=0 THEN C%=10
270 POKE Y,C%:Y=Y+2
280 NEXT I
290 GET K$
300 IF K$=" " then end
310 GOTO 190

Este es un programa BASIC que primero inicializa los datos en las líneas 20-70, para luego mostrar por pantalla la información sobre los botones de los joysticks en las líneas 90-140. De nuevo se configuran los puertos como en el ejemplo del teclado, para permitir que el interprete pueda luego realizar el comando GET. Por último, llegamos al bucle principal donde se va a cambiar el color del texto dependiendo de si los joysticks están pulsados, para ello en las líneas 210 y 220 se leen los puertos A y B, para comprobar en las líneas 240 y 260 si está pulsado algún botón o no. Finalmente se ejecuta el comando GET, para dar una opción al programa de salir del bucle.

Temporizadores

El C64 permite controlar operaciones de tiempo mediante cuentas atrás (timers) o con el reloj del sistema (TOD). Existen dos cuentas atrás, A ($DC04-$DC05) y B ($DC06-$DC07), que son enteros de 16 bits sin signo, donde el primer byte es la parte menos significativa (low) y el segundo byte la más significativa (high). También existen dos registros de control para la cuenta atrás A ($DC0E) y la cuenta atrás B ($DC0F), que tienen las siguientes operaciones:

Una vez configurada la CIA1, para trabajar con operaciones temporales, se deberá configurar la gestión de las interrupciones indicando en $0314-$0315 la dirección de la subrutina encargada de ello. Por defecto el sistema tiene una subrutina en $EA31 para procesar interrupciones. Se podrá consultar desde la subrutina el registro de interrupciones de la CIA1 ($DC0D), cuyos bits de lectura son:

En cuanto a los bits de escritura tenemos:

Por defecto están activadas las interrupciones, por lo que se podrá leer el registro para comprobar si se ha activado una interrupción o no.

El otro mecanismo es el reloj del sistema que nos da la hora del día (TOD), en formato hora, minutos, segundos y décimas de segundos. Los valores se almacenan en formato BCD, aunque el bit 7 en el byte de la hora se utiliza para indicar si se trata de la hora AM (0) o PM (1). Con el registro de control en $DC0F, si el bit 7 es 1 podemos escribir en los registros de la TOD la hora de la alarma, para poder obtener una interrupción cuando se alcance la hora del día indicada por la alarma. Tanto para la lectura de la TOD, como para la escritura, tiene la CIA un mecanismo de bloqueo (latching) que congela los registros hasta que las décimas sean leídas o escritas, para evitar inconsistencias. Este mecanismo no impide que internamente se siga actualizando la TOD, hasta que se haya escrito o leído entera la información accesible desde la memoria.

AVISO: El interprete y la terminal de BASIC usan internamente los temporizadores, por lo que su uso desde un programa escrito en BASIC puede derivar en problemas inesperados. En caso de necesitar gestionar el tiempo se puede usar la variable TIME. El manejo del tiempo mediante el uso de las CIAs deberá hacerse con programas en código máquina que activen la gestión de las interrupciones.

KERNAL

Esta sección es un listado de algunas rutinas que podemos usar del KERNAL y del interprete de BASIC. Para más información sobre las rutinas del C64, en inglés, puedes consultar la siguiente documentación o el listado de memoria de la ROM de BASIC y la ROM del KERNAL.

Rutinas principales

El KERNAL tiene en la zona final de su memoria una tabla de saltos a las diferentes rutinas de servicio que incorpora. Por ello para cada rutina tenemos dos direcciones: la de la tabla y la dirección real entre paréntesis, siendo esta última donde realmente se encuentra el código de la rutina.

Nombre Dirección Entradas Salidas Usados Descripción
SCINIT $FF81 ($FF5B) - - A, X, Y Inicializar VIC, asignar E/S por defecto (teclado/pantalla), borrar pantalla, configurar flag PAL/NTSC e interrupciones del timer.
IOINIT $FF84 ($FDA3) - - A, X Inicializar CIAs y volumen SID; configurar la memoria; inicializar e iniciar interrupciones del timer.
RAMTAS $FF87 ($FD50) - - A, X, Y Limpiar bloques de memoria $0002-$0101 y $0200-$03FF; iniciar test de memoria y asignar las direcciones de inicio y final de la memoria de programas BASIC; establecer el buffer de pantalla en $0400 y el buffer de datasette en $033C.
RESTOR $FF8A ($FD15) - - - Rellenar la tabla de vectores en $0314-$0333 con los valores por defecto.
VECTOR $FF8D ($FD1A) Carry: 0 = Escribir (TV=TU); 1 = Leer (TU=TV).
X/Y: Puntero tabla de usuario.
- A, Y Lee o escribe los valores de la tabla de vectores en $0314-$0333.
SETMSG $FF90 ($FE18) A: Valor flag. - - Modifica el flag de mostrar errores del sistema ($009D).
LSTNSA $FF93 ($EDB9) A: Dirección secundaria. - A Envía un LISTEN a la dirección secundaria del puerto serie. (Debe llamarse LISTEN previamente.)
TALKSA $FF96 ($EDC7) A: Dirección secundaria. - A Envía un TALK a la dirección secundaria del puerto serie. (Debe llamarse TALK previamente.)
MEMBOT $FF99 ($FE25) Carry: 0 = Escribir; 1 = Leer.
X/Y: Dirección (Carry = 0).
X/Y: Dirección (Carry = 1). X, Y Lee o escribe la dirección de inicio de la memoria de trabajo de BASIC.
MEMTOP $FF9C ($FE34) Carry: 0 = Escribir; 1 = Leer.
X/Y: Dirección (Carry = 0).
X/Y: Dirección (Carry = 1). X, Y Lee o escribe la dirección de final de la memoria de trabajo de BASIC.
SCNKEY $FF9F ($EA87) - - A, X, Y Consulta el teclado, pone en $00CB el código de matriz, en $028D el estado actual de las teclas SHIFT y en el buffer de teclado el código PETSCII.
SETTMO $FFA2 ($FE21) A: Valor de timeout. - - Configura el timeout del puerto serie.
IECIN $FFA5 ($EE13) - A: Byte leído. A Lee un byte del puerto serie. (Debe llamarse TALK y TALKSA previamente.)
IECOUT $FFA8 ($EDDD) A: Byte para escribir. - - Escribe un byte en el puerto serie. (Debe llamarse LISTEN y LSTNSA previamente.)
UNTALK $FFAB ($EDEF) - - A Enviar el comando UNTALK al puerto serie.
UNLSTN $FFAE ($EDFE) - - A Enviar el comando UNLISTEN al puerto serie.
LISTEN $FFB1 ($ED0C) A: Número de dispositivo. - A Enviar el comando LISTEN al puerto serie.
TALK $FFB4 ($ED09) A: Número de dispositivo. - A Enviar el comando TALK al puerto serie.
READST $FFB7 ($FE07) - A: Estado actual del dispositivo. A Obtiene el estado actual del dispositivo de E/S con el valor de la variable STATUS. (Para el RS-232, el estado es borrado.)
SETLFS $FFBA ($FE00) A: Número identificador.
X: Número de dispositivo.
Y: Modo o dirección secundaria.
- - Configura los parámetros del fichero para trabajar.
SETNAM $FFBD ($FDF9) A: Tamaño de la cadena.
X/Y: Puntero a la cadena.
- - Configura el nombre del fichero para trabajar.
OPEN $FFC0 (($031A) = $F34A) - - A, X, Y Abre un fichero. (Debe llamarse SETLFS y SETNAM previamente.)
CLOSE $FFC3 (($031C) = $F291) A: Número identificador. - A, X, Y Cierra un fichero.
CHKIN $FFC6 (($031E) = $F20E) X: Número identificador. - A, X Define un fichero como la entrada por defecto. (Debe llamarse OPEN previamente.)
CHKOUT $FFC9 (($0320) = $F250) X: Número identificador. - A, X Define un fichero como la salida por defecto. (Debe llamarse OPEN previamente.)
CLRCHN $FFCC (($0322) = $F333) - - A, X Cierra los ficheros de E/S por defecto (para el puerto serie se envía UNTALK y/o UNLISTEN). Restaura la E/S por defecto al teclado y pantalla.
CHRIN $FFCF (($0324); $F157) - A: Byte leído. A, Y Lee un byte de la entrada por defecto, pero con el teclado lee una línea de la pantalla. (Si no es el teclado, debe llamarse OPEN y CHKIN previamente.)
CHROUT $FFD2 (($0326); $F1CA) A: Byte para escribir. - - Escribe un byte en la salida por defecto. (Si no es la pantalla, debe llamarse OPEN y CHKOUT previamente.)
LOAD $FFD5 ($F49E) A: 0 = Cargar; 1-255 = Verificar.
X/Y: Dirección de destino (si modo/DS = 0).
Carry: 0 = Éxito; 1 = Errores.
A: Código error KERNAL (Carry = 1).
X/Y: Dirección del último byte cargado/verificado (Carry = 0).
A, X, Y Carga o verifica un fichero. (Debe llamarse SETLFS y SETNAM previamente.)
SAVE $FFD8 ($F5DD) A: Dirección en la página cero del registro con la dirección inicial del bloque de memoria a guardar.
X/Y: Dirección final del bloque de memoria más 1.
Carry: 0 = Éxito; 1 = Errores.
A: Código error KERNAL (Carry = 1).
A, X, Y Guarda un fichero. (Debe llamarse SETLFS y SETNAM previamente.)
SETTIM $FFDB ($F6E4) A/X/Y: Nuevo valor. - - Escribe el valor de TIME ($00A0-$00A2).
RDTIM $FFDE ($F6DD) - A/X/Y: Valor actual. A, X, Y Lee el valor de TIME ($00A0-$00A2).
STOP $FFE1 (($0328) = $F6ED) - Zero: 0 = Sin pulsar; 1 = Pulsada.
Carry: 1 = Pulsada.
A, X Consultar el estado de la tecla RUN/STOP ($0091). Si está pulsada, invocar CLRCHN y limpiar el buffer de teclado.
GETIN $FFE4 (($032A) = $F13E) - A: Byte leído. A, X, Y Lee un byte de la entrada por defecto. (Si no es el teclado, debe llamarse OPEN y CHKIN previamente.)
CLALL $FFE7 (($032C) = $F32F) - - A, X Borra la tabla de ficheros e invoca a CLRCHN.
UDTIM $FFEA ($F69B) - - A, X Actualiza la variable TIME ($00A0-$00A2) y el estado de la tecla RUN/STOP ($0091).
SCREEN $FFED ($E505) - X: Columnas (40).
Y: Filas (25).
X, Y Obtiene el número de filas y columnas de la pantalla.
PLOT $FFF0 ($E50A) Carry: 0 = Escribir; 1 = Leer.
X: Columna (Carry = 0).
Y: Fila (Carry = 0).
X: Columna (Carry = 1).
Y: Fila (Carry = 1).
X, Y Lee o escribe la posición del cursor.
IOBASE $FFF3 ($E500) - X/Y: Dirección base CIA1 ($DC00). X, Y Obtener la dirección base de la CIA1.

Algunas de las rutinas del KERNAL son para trabajar con ficheros. Primero hay que configurar los parámetros del fichero con SETLFS y SETNAM. A continuación se sigue el siguiente esquema:

  1. Abrir el fichero con OPEN.
  2. Usar el fichero como E/S por defecto con CHKIN/CHKOUT.
  3. Operaciones de lectura y escritura con GETIN, CHRIN o CHROUT.
  4. Restaurar la E/S por defecto con CLRCHN
  5. Cerrar el fichero con CLOSE.

Con la rutina READST se podrá obtener en el registro A el estado del dispositivo del fichero con el que se está trabajando, para comprobar si ha habido errores o si se ha llegado al final de fichero, entre otras cosas.

Rutinas de la pantalla

Además de las rutinas SCREEN y PLOT, existen también:

Dirección Entradas Salidas Usados Descripción
$E4DA Y: Columna. - A Poner el color del cursor ($0286) en la posición apuntada por $00F3-$00F4 del buffer de color.
$E518 - - A, X, Y Inicializar VIC, asignar E/S por defecto (teclado/pantalla) y borrar pantalla.
$E544 - - A, X, Y Borrar pantalla.
$E566 - - A, X, Y Mueve el cursor al inicio de la pantalla (esquina superior izquierda).
$E56C - - A, X, Y Actualiza, a la línea actual, el puntero en $00D1-$00D2 al buffer de pantalla y el puntero en $00F3-$00F4 al buffer de color, en base a la fila ($00D6) y columna ($00D3) del cursor.
$E59A - - A, X, Y Inicializar VIC, asignar E/S por defecto (teclado/pantalla) y mover el cursor al inicio de la pantalla.
$E5A0 - - A, X Inicializar VIC y asignar E/S por defecto (teclado/pantalla).
$E5A8 - - A, X Inicializar VIC.
$E632 - A: Byte leído. A Lee un byte de la pantalla. Si la línea de entrada está vacía, aparece el cursor y se introduce una línea de datos.
$E684 - - - Comprueba un código PETSCII. Si es $22 ("), se invierte el modo comillas en $00D4.
$E6B6 - - A, X, Y Recalcula los bytes altos de los punteros ($00D9-$00F1) a las líneas en el buffer de pantalla.
$E716 A: Byte para escribir. - - Escribe un byte en pantalla.
$E8CB - - A, X Comprueba un código PETSCII. Si pertenece a un color, modifica el color del cursor ($0286).
$E8EA - - A, X, Y Desplaza toda la pantalla hacia arriba.
$E965 - - A, X, Y Inserta una línea antes de la actual y desplaza las siguientes líneas de pantalla hacia abajo.
$E9F0 X: Fila. - A Modifica el puntero en $00D1-$00D2 a la línea actual en el buffer de pantalla, consultando el byte alto de la tabla de punteros en $00D9-$00F1.
$E9FF X: Fila. - A, Y Borra una línea de pantalla.
$EA13 A: Carácter
X: Color.
- A, Y Escribe un carácter y color en pantalla, modificando el tiempo de parpadeo del cursor a 2.
$EA24 - - - Modifica el puntero en $00F3-$00F4 a la línea actual en el buffer de color, consultando el puntero $00D1-$00D2 a la línea actual en el buffer de pantalla.

Rutinas del teclado

Además de las rutinas SCNKEY y STOP, existen también:

Dirección Entradas Salidas Usados Descripción
$E5B4 - A: Byte leído. A, X, Y Lee un byte del buffer de teclado, el buffer de las teclas SHIFT y decrementa los punteros al buffer.
$F142 - A: Byte leído (0 = Ninguna tecla pulsada). A, X, Y Lee un byte del buffer de teclado, el buffer de las teclas SHIFT y decrementa los punteros al buffer.
$F6BC - - A, X Actualiza el estado de la tecla RUN/STOP ($0091).

Rutinas del puerto serie

Además de las rutinas LSTNSA, TALKSA, SETTMO, IECIN, IECOUT, UNTALK, UNLSTN, LISTEN y TALK, existen también:

Dirección Entradas Salidas Usados Descripción
$ED40 - - A Flush de la caché de salida ($0095) del puerto serie hacia el puerto serie.
$EE85 - - A Modifica el CLOCK OUT a high.
$EE8E - - A Modifica el CLOCK OUT a low.
$EE97 - - A Modifica el DATA OUT a high.
$EEA0 - - A Modifica el DATA OUT a low.
$EEA9 - Carry: DATA IN
Negative: CLOCK IN
A: CLOCK IN (Bit 7).
A Lee el CLOCK IN y el DATA IN.
$F1AD - A: Byte leído A Lee un byte del puerto serie. Lee $0D (RETURN), si STATUS <> 0.
$F237 A: Número de dispositivo. - A, X Define el puerto serie como entrada por defecto. No se envíe TALK a la dirección secundaria si el bit 7 del modo de apertura vale 1.
$F279 A: Número de dispositivo. - A, X Define el puerto serie como salida por defecto. No se envíe LISTEN a la dirección secundaria si el bit 7 del modo de apertura vale 1.
$F3D5 - - A, Y Abre un fichero en el puerto serie. No se envíe el nombre de fichero si el bit 7 del modo de apertura vale 1 o si el tamaño del nombre de fichero es 0.
$F528 - - A Enviar el comando UNTALK y CLOSE al puerto serie.
$F63F - - A Enviar el comando UNLISTEN y CLOSE al puerto serie.
$F642 - - - Cierra un fichero en el puerto serie. No se envíe CLOSE a la dirección secundaria si el bit 7 del modo de apertura vale 1.

Rutinas del datasette

Estas son las rutinas que existen para manejar el datasette:

Dirección Entradas Salidas Usados Descripción
$F179 - A: Byte leído. A, Y Leer un byte del datasette (sólo ficheros de datos).
$F1DD Carry = 1: Escribir byte; A: Byte para escribir; Carry = 0: Escribir EOF. - - Escribir un byte en el datasette.
$F22A A = 1 - A, X Define el datasette como entrada por defecto.
$F26F A = 1 - A, X Define el datasette como salida por defecto.
$F2C8 - - A, X, Y Cerrar un fichero en el datasette (sólo ficheros de datos). Primero escribe $00 y fin de fichero (EOF). (Previamente se debe meter en la pila el número identificador de fichero.)
$F38B - - A, X, Y Abrir un fichero en el datasette (sólo ficheros de datos). Se lanza un ILLEGAL DEVICE NUMBER si el puntero al buffer del datasette ($00B2-$00B3) está por debajo de $0200.
$F72C - X: Tipo de cabecera; Carry = 1 y Zero = 1: Interrumpido por pulsarse STOP; Carry = 1 y Zero = 0: Interrumpido por alcanzarse el final de cinta (EOT). A, X, Y Leer la cabecera en el datasette.
$F76A A: Tipo de cabecera. Carry = 1 y Zero = 1: Interrumpido por pulsarse STOP. A, X, Y Generar y escribir la cabecera en el datasette.
$F7D0 - Carry = 0: Puntero por debajo de $0200; X/Y: Puntero. X, Y Obtener el puntero al buffer del datasette.
$F7EA - X: Tipo de cabecera; Carry = 1 y Zero = 1: Interrumpido por pulsarse STOP; Carry = 1 y Zero = 0: Interrumpido por alcanzarse el final de cinta (EOT). A, X, Y Buscar un fichero en el datasette. (Debe llamarse SETLFS y SETNAM previamente.)
$F80D - Zero = 1: Buffer lleno; Carry = 1 y Zero = 0: Desbordamiento del buffer. X, Y Incrementar el puntero al buffer del datasette.
$F817 - Carry = 1: Interrumpido por pulsarse STOP. A, Y Esperar a que se pulse PLAY en el datasette.
$F82E - Zero: 0 = Nada pulsado; 1 = PLAY, RECORD, F.FWD y/o REW pulsados. - Detecta si se ha pulsado algún botón en el datasette.
$F838 - Carry = 1: Interrumpido por pulsarse STOP. A, Y Esperar a que se pulse RECORD en el datasette.
$F841 - Carry = 1 y Zero = 1: Interrumpido por pulsarse STOP. A, X, Y Lee un bloque del datasette.
$F84A - Carry = 1 y Zero = 1: Interrumpido por pulsarse STOP. A, X, Y Lee un fichero de programa del datasette.
$F864 - Carry = 1 y Zero = 1: Interrumpido por pulsarse STOP. A, X, Y Escribe un bloque en el datasette.
$F86B - Carry = 1 y Zero = 1: Interrumpido por pulsarse STOP. A, X, Y Escribe un fichero de programa en el datasette.
$FC93 - - A Apaga el motor del datasette; cambia la pantalla hacia atrás; restaura la dirección de ejecución de la rutina de servicio de interrupciones desde $029F-$02A0.
$FCCA - - A Apaga el motor del datasette.

Rutinas del RS-232

Estas son las rutinas que existen para manejar el RS-232:

Dirección Entradas Salidas Usados Descripción
$EF4A - X: Número de bits de datos. A, X Calcular el número de bits de datos, de acuerdo a $0293.
$EFE1 A = 2 - A Definir el RS-232 como salida por defecto.
$F017 - - A, Y Escribir un byte en el RS-232, desde la caché de salida del RS-232.
$F04D A = 2 - A Definir el RS-232 como entrada por defecto.
$F086 - A: Byte leído. - Leer un byte del RS-232.
$F0A4 - - - Esperar al final de la transferencia y desactivar las interrupciones del RS-232. (Para que registros de chips comunes puedan utilizarse por el puerto serie o la E/S del datasette.)
$F14E - A: Byte leído. A Leer un byte del RS-232.
$F1B8 - A: Byte leído. A Leer un byte del RS-232; reintentar ante un byte $00.
$F1DD Carry = 0; A: Byte para escribir. - - Escribir un byte en el RS-232.
$F2AF - - A, X, Y Cerrar un fichero en el RS-232; liberar, si existe, los buffers de E/S del RS-232.
$F409 - - A, X, Y Abrir un fichero en el RS-232; ubicar, si no existen, los buffers de E/S del RS-232.
$F483 - - A, Y Inicializar la CIA2 para abrir/cerrar el RS-232.

Rutinas BASIC de conversión

El interprete de BASIC tiene implementada su propia representación de números de coma flotante y tiene algunas rutinas para trabajar con ellos:

Dirección Descripción
$A96B Lee el número de línea del programa BASIC y lo escribe en $0014-$0015. Si el primer carácter no es un dígito, el resultado será 0. Si el resultado es igual o superior a 64000, se lanzará un SYNTAX ERROR. (NOTA: Necesita que CHRGET haya sido invocado previamente para consultar $0073.)
$A9C4 Convierte el FAC a entero y lo escribe en la variable apuntada por $0049-$004A.
$A9DA Asigna un valor a una variable de cadena, incluida TI$. (NOTA: Consultar también la rutina $AA2C.)
$A9E0 Asigna un valor a TI$ y cambia el TOD. Lee el valor de la cadena apuntada por $0064-$0065.
$AA2C Asigna un valor a una variable de cadena, excluyendo TI$. Lee el valor de la cadena apuntada por $0064-$0065 y escribe el valor en la cadena apuntada por $0049-$004A.
$AD8A Lee el valor de una expresión numérica del programa BASIC y lo escribe en el FAC.
$AD8D Comprueba si una expresión es numérica, si no lo es lanza un TYPE MISMATCH.
$AD8F Comprueba si una expresión es una cadena, si no lo es lanza un TYPE MISMATCH.
$AD9E Lee el valor de una expresión del programa BASIC. Para expresiones numéricas se escribe el valor en el FAC; para expresiones de cadena, el tamaño se escribe en $0061 y la dirección del valor en $0062-$0063.
$AEF1 Lee el valor de una expresión entre paréntesis del programa BASIC. (NOTA: Consultar también la rutina $AD9E.)
$AF28 Lee el nombre de una variable del programa BASIC y carga su valor. La dirección de la variable se escribe en $0064-$0065; para variables numéricas, se escribe su valor en el FAC, en formato de coma flotante; para ST, TI y TI$, toma el valor de sus direcciones de memoria del sistema; para variables que no existen devuelve el valor vacío (0 para números y "" para cadenas).
$AF48 Calcular el valor de TI$.
$AF61 Escribe en el FAC, en formato de coma flotante, el valor de la variable entera apuntada por $0064-$0065.
$AF6E Escribe en el FAC el valor de una variable flotante, incluidas ST y TI. (NOTA: Consultar también la rutina $AFA0.)
$AF7B Calcular el valor de TI en el FAC.
$AF9A Calcular el valor de ST en el FAC.
$AFA0 Escribe en el FAC el valor de la variable flotante apuntada por $0064-$0065, excluyendo ST y TI.
$B08B Lee el nombre de una variable del programa BASIC y la busca. Si la encuentra, la dirección de la variable es escrita en $005F-$0060, la dirección del valor es escrita en $0047-$0048; si no la encuentra, y el invocador es $AF28, devuelve el valor vacío (0 para números y "" para cadenas), sino crea una nueva variable con el valor vacío.
$B128 Crea una nueva variable con el valor vacío (0 para números y "" para cadenas). El nombre de la variable está en $0045-$0046.
$B1B2 Lee un valor entero del programa BASIC y lo escribe en $0064-$0065. Si el valor no está dentro del rango [-32768, 32767], se lanza un ILLEGAL QUANTITY.
$B391 Escribe el entero en A/Y en el FAC en formato de coma flotante.
$B3A2 Escribe el entero en Y en el FAC en formato de coma flotante.
$B794 Escribe el entero en A en el FAC en formato de coma flotante.
$B79B Lee un BYTE del programa BASIC y lo escribe en X. Si el valor no está dentro del rango [0, 255], se lanza un ILLEGAL QUANTITY.
$B7EB Lee un WORD y un BYTE, separados por una coma, del programa BASIC y lo escribe en $0014-$0015 y X.
$B7F7 Convierte el FAC en un valor entero sin signo y lo escribe en $0014-$0015. Si el valor no está dentro del rango [0, 65535], se lanza un ILLEGAL QUANTITY.
$B849 FAC = FAC + 0.5
$B850 FAC = (valor flotante apuntado por A/Y) – FAC
$B853 FAC = ARG – FAC
$B867 FAC = (valor flotante apuntado por A/Y) + FAC
$B86A FAC = ARG + FAC (Requiere un LDA $61 previo.)
$B947 FAC = ComplementoA2(FAC), invierte la mantisa del FAC.
$BA28 FAC := (valor flotante apuntado por A/Y) * FAC
$BA2B FAC = ARG * FAC (Requiere un LDA $61 previo.)
$BA8C ARG = (valor flotante apuntado por A/Y)
$BAE2 FAC = FAC * 10
$BAFE FAC = FAC / 10
$BB0F FAC = (valor flotante apuntado por A/Y) / FAC
$BB12 FAC = ARG / FAC; si el FAC es 0, lanzar un DIVISION BY ZERO. (Requiere un LDA $61 previo.)
$BBA2 FAC = (valor flotante apuntado por A/Y)
$BBC7 Registro Aritmético 4 = FAC
$BBCA Registro Aritmético 3 = FAC
$BBD0 Escribe el FAC en la variable flotante apuntada por $0049-$004A.
$BBD4 Escribe el FAC en la variable flotante apuntada por X/Y.
$BBFC FAC = ARG
$BC0C ARG = Entero(FAC)
$BC0F ARG = FAC
$BC1B FAC = Entero(FAC)
$BC2B Escribe signo del FAC en A: 1 = Positivo; 0 = Cero; 255 = Negativo.
$BC5B Compara el valor flotante apuntado por A/Y con el FAC y lo escribe en A: 1 = FAC menor; 0 = Igual; 255 = FAC mayor.
$BC9B Convierte el FAC a entero y lo escribe en $0064-$0065.
$BCF3 Lee un valor flotante del programa BASIC y lo escribe en el FAC.
$BDCD Escribe en pantalla el entero en A/X en formato de coma flotante.
$BDDD Convierte el FAC a una cadena terminada en 0 almacenada en $0100-$010A.
$BF71 FAC = RaízCuadrada(FAC)
$BF78 FAC = ARG ^ (valor flotante apuntado por A/Y)
$BF7B FAC = ARG ^ FAC
$BFB4 FAC := –FAC