Programando como si fuera 1982. Parte VII – Proyecto: Mini Piano
En los posts anteriores de esta serie desmontamos el C64 pieza a pieza. Registros, memoria, instrucciones, control de flujo y los chips de hardware. Ahora vamos a montarlo todo de vuelta en algo que funcione.
El proyecto es un mini piano de cinco notas. Las teclas Z X C V B tocan Do Re Mi Fa Sol. Cada nota cambia el color del borde de la pantalla, reproduce un sonido en el SID y muestra la letra de la nota en el centro de la pantalla. Lo suficientemente simple para caber en un único post, lo suficientemente complejo para usar los tres chips juntos.
El papel de cada chip es directo. El CIA 1 lee el teclado, el SID genera el sonido y el VIC-II se encarga de lo visual. El loop principal es el director que coordina los tres.
El stub BASIC y el punto de entrada
Los programas assembly en el C64 necesitan un pequeño stub BASIC al inicio para que RUN funcione. Se encuentra en $0801, la dirección donde el intérprete BASIC busca el primer programa, y contiene una única línea que le dice al procesador que salte al código assembly justo después.
Antes de leer este código, hay algo a tener en cuenta. No todo en un archivo fuente assembly es una instrucción del procesador. Algunas líneas son directivas del ensamblador, comandos para el ensamblador en sí y no para el procesador. *=$0801 le dice al ensamblador que coloque los bytes siguientes a partir de la dirección $0801. .WORD $080B le indica que almacene el valor $080B como dos bytes en little-endian. .BYTE $9E almacena un único byte. El procesador nunca lee estas directivas. Para cuando ejecuta el programa, el ensamblador ya ha hecho su trabajo y lo que queda en memoria son solo los bytes en bruto.
*=$0801
.WORD $080B ; puntero a la siguiente línea BASIC
.WORD $000A ; número de línea: 10
.BYTE $9E ; token SYS
.BYTE "2064" ; 2064 = $0810 en decimal
.BYTE $00 ; fin de línea
.WORD $0000 ; fin del programa BASIC
*=$0810 ; el código empieza aquí
Los ensambladores modernos como KickAssembler y ACME se encargan de esto automáticamente, pero vale la pena saber qué está pasando. $9E es el token BASIC para SYS, y los bytes siguientes son el número 2064 como texto, la dirección donde comienza el assembly. Cuando tecleas RUN, BASIC ejecuta SYS 2064 y el control pasa a nuestro código.
Inicialización
Antes de entrar al bucle, configuramos el SID y el VIC-II a un estado conocido. El SID en particular necesita atención: al encenderse, sus registros pueden tener valores aleatorios y generar ruido.
INIT:
; Silencia el SID y configura la voz 1
LDA #$00
STA $D418 ; volumen master = 0 (silencio total)
STA $D404 ; apaga gate y waveform de la voz 1
LDA #$08 ; attack=0 (instantáneo), decay=8 (medio)
STA $D405
LDA #$F4 ; sustain=F (máximo), release=4 (medio)
STA $D406
LDA #$0F
STA $D418 ; volumen master = 15 (máximo)
; Colores iniciales: pantalla negra
LDA #$00
STA $D020 ; borde: negro
STA $D021 ; fondo: negro
; Limpia la pantalla via KERNAL
LDA #$93 ; PETSCII: CLR (limpiar pantalla y volver al inicio)
JSR $FFD2 ; CHROUT -- imprime el carácter de control
RTS
El valor $93 no es un carácter visible. Es un código de control PETSCII que el chip de vídeo interpreta como “limpiar la pantalla”. Enviarlo al CHROUT del KERNAL es la forma más sencilla de empezar con la pantalla limpia sin escribir directamente en los 1.000 bytes de la screen RAM.
El envelope del SID merece una nota. $D405 y $D406 guardan los cuatro parámetros ADSR en nibbles de 4 bits cada uno. Attack=0 significa que la nota alcanza el volumen máximo instantáneamente, ideal para un piano. Release=4 da un decay suave cuando la nota termina, en lugar de cortarla abruptamente.
El bucle principal y la lectura del teclado
El CIA 1 expone el teclado como una matriz de 8×8 bits accesible a través de registros en $DC00 y $DC01. Recorrer la matriz entera manualmente es tedioso, así que para este proyecto usamos la rutina GETIN del KERNAL ($FFE4), que ya hace ese trabajo y devuelve en A el código PETSCII de la última tecla pulsada, o cero si no se pulsó ninguna.
MAIN:
JSR INIT
LOOP:
JSR $FFE4 ; GETIN: A = código de tecla (0 = ninguna)
BEQ LOOP ; A=0, ninguna tecla -- vuelve al inicio
CMP #$5A ; 'Z' en PETSCII = 90 = $5A
BEQ TOCA_DO
CMP #$58 ; 'X' = 88 = $58
BEQ TOCA_RE
CMP #$43 ; 'C' = 67 = $43
BEQ TOCA_MI
CMP #$56 ; 'V' = 86 = $56
BEQ TOCA_FA
CMP #$42 ; 'B' = 66 = $42
BEQ TOCA_SOL
JMP LOOP ; tecla desconocida, ignora
Cada CMP compara el valor en A con el código PETSCII de la tecla. Cuando encuentra una coincidencia, BEQ salta al bloque de esa nota. Si ninguna de las comparaciones coincide, JMP LOOP descarta la tecla y vuelve al inicio.
Configurando y tocando cada nota
Cada bloque de nota hace tres cosas. Configura la frecuencia en el SID, cambia el color del borde en el VIC-II y escribe la letra de la nota en la pantalla.
Las frecuencias del SID se calculan con la fórmula Fn = Fout × 16.777.216 / Fclk. En el C64 PAL, Fclk es aproximadamente 985.248 Hz. Para Do central (261 Hz), el resultado es 4457 = $1169. Cada nota ocupa dos bytes en little-endian, byte bajo primero, byte alto después.
TOCA_DO:
LDA #$69
STA $D400 ; freq low -- $1169 ≈ C4 (261 Hz)
LDA #$11
STA $D401 ; freq high
LDA #$02
STA $D020 ; borde: rojo (color 2)
LDA #$03
STA $05F4 ; 'C' en pantalla -- screen code 3, fila 12 col 20
LDA #$01
STA $D9F4 ; color char: blanco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
TOCA_RE:
LDA #$89
STA $D400 ; $1389 ≈ D4 (293 Hz)
LDA #$13
STA $D401
LDA #$05
STA $D020 ; borde: verde
LDA #$04
STA $05F4 ; 'D' -- screen code 4
LDA #$01
STA $D9F4 ; color char: blanco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
TOCA_MI:
LDA #$F0
STA $D400 ; $15F0 ≈ E4 (329 Hz)
LDA #$15
STA $D401
LDA #$06
STA $D020 ; borde: azul
LDA #$05
STA $05F4 ; 'E' -- screen code 5
LDA #$01
STA $D9F4 ; color char: blanco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
TOCA_FA:
LDA #$3E
STA $D400 ; $173E ≈ F4 (349 Hz)
LDA #$17
STA $D401
LDA #$07
STA $D020 ; borde: amarillo
LDA #$06
STA $05F4 ; 'F' -- screen code 6
LDA #$01
STA $D9F4 ; color char: blanco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
TOCA_SOL:
LDA #$18
STA $D400 ; $1A18 ≈ G4 (392 Hz)
LDA #$1A
STA $D401
LDA #$04
STA $D020 ; borde: morado
LDA #$07
STA $05F4 ; 'G' -- screen code 7
LDA #$01
STA $D9F4 ; color char: blanco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
La dirección $05F4 es la posición central de la pantalla ($0400 + 12 × 40 + 20). La screen RAM del C64 usa screen codes, no PETSCII. A vale 1, B vale 2, C vale 3 y así sucesivamente. La dirección correspondiente en la color RAM es $D9F4 ($D800 + $01F4), donde escribimos el color del carácter.
La subrutina PLAY
PLAY es llamada por todos los bloques de nota. Activa la voz 1 con waveform triangular, espera un tiempo fijo y luego apaga el gate, iniciando la fase de release del envelope.
PLAY:
LDA #$11 ; waveform triangular ($10) + gate ON ($01)
STA $D404 ; la nota empieza aquí
; Delay de ~0,33 segundos
; Cálculo: 255 ext × 255 int × ~5 ciclos = ~326.000 ciclos
; A 985.248 Hz (PAL): ~0,33 segundos
LDX #$FF
WAIT1: LDY #$FF
WAIT2: DEY
BNE WAIT2 ; bucle interno: 255 × 5 ciclos ≈ 1.275 ciclos
DEX
BNE WAIT1 ; bucle externo: repite 255 veces
LDA #$10 ; mantiene waveform triangular, gate OFF
STA $D404 ; inicia el release -- la nota se apaga suavemente
LDA #$00
STA $D020 ; restaura borde a negro
RTS
El delay usa dos bucles anidados contando ciclos de reloj, la forma estándar de crear pausas precisas en assembly del 6510. Para notas más largas, aumenta el valor inicial de LDX. Para más cortas, disminúyelo. Cada unidad de LDX equivale a aproximadamente 1,3 milisegundos.
Cómo ejecutarlo
Guarda el código en un archivo .asm, ensámblalo con KickAssembler o ACME y abre el .prg generado en el emulador VICE. Dentro del emulador, LOAD "PIANO",8,1 seguido de RUN iniciará el programa. Pulsa Z, X, C, V o B.
Si quieres experimentar directamente sin ensamblar, VICE tiene un monitor de lenguaje máquina accesible desde el menú Debug. Puedes escribir los bytes directamente en memoria a partir de $0810 y luego G $0810 para ejecutar, exactamente como se hacía en los años 80.
Lo que puedes intentar ahora
La estructura está hecha. Algunas extensiones naturales:
Más notas. Una octava completa tiene 12 notas. Calcula las frecuencias con la fórmula Fn = Fout × 16.777.216 / 985.248, conviértelas a hexadecimal y añade más bloques siguiendo el mismo patrón.
Diferentes waveforms. El valor $11 en $D404 selecciona onda triangular. Cámbialo a $21 (diente de sierra), $41 (cuadrada) o $81 (ruido). Cada una tiene un timbre completamente diferente. El ruido, por ejemplo, se convierte en percusión.
Sustain mientras la tecla está pulsada. GETIN solo detecta el momento en que la tecla entra en el buffer. Para sustain real, lee directamente la matriz del CIA 1 en $DC01 dentro de un bucle que comprueba si la tecla sigue pulsada antes de apagar el gate.
Dos voces a la vez. El SID tiene tres voces independientes, cada una con sus propios registros de frecuencia, waveform y envelope. Configurar la voz 1 y la voz 2 con frecuencias diferentes da como resultado dos notas simultáneas.
Con esto se cierra la serie. Empezamos en la Parte I sin saber qué era un registro y terminamos aquí: tres chips coordinados por un bucle de pocas decenas de instrucciones, haciendo algo que puedes escuchar y ver. No hay ninguna magia en el camino entre los dos. Solo bytes, direcciones y el procesador haciendo lo que fue diseñado para hacer.