Idiomas disponibles:

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.

Arquitectura del Mini Piano

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.