Idiomas disponíveis:

Programando como se fosse 1982. Parte VII – Projeto: Mini Piano

Nos posts anteriores desta série desmontamos o C64 peça por peça. Registradores, memória, instruções, controle de fluxo e os chips de hardware. Agora vamos montar tudo de volta numa coisa que funciona.

O projeto é um mini piano de cinco notas. As teclas Z X C V B tocam Dó Ré Mi Fá Sol. Cada nota muda a cor da borda da tela, toca um som no SID e exibe a letra da nota no centro da tela. Simples o suficiente para caber num único post, complexo o suficiente para usar os três chips juntos.

Arquitetura do Mini Piano

O papel de cada chip é direto. O CIA 1 lê o teclado, o SID gera o som e o VIC-II cuida do visual. O loop principal é o maestro coordenando os três.

O stub BASIC e o ponto de entrada

Programas assembly no C64 precisam de um pequeno stub BASIC no início para que o RUN funcione. Ele fica em $0801, o endereço onde o interpretador BASIC procura o primeiro programa, e contém uma única linha que manda o processador pular para o código assembly logo depois.

Antes de ler este código, há algo a observar. Nem tudo em um arquivo fonte assembly é uma instrução do processador. Algumas linhas são diretivas do assembler, comandos para o assembler em si, não para o processador. *=$0801 diz ao assembler para colocar os bytes seguintes a partir do endereço $0801. .WORD $080B manda armazenar o valor $080B como dois bytes em little-endian. .BYTE $9E armazena um único byte. O processador nunca lê essas diretivas. Quando roda o programa, o assembler já fez seu trabalho e o que resta na memória são apenas os bytes crus.

*=$0801
        .WORD $080B         ; ponteiro para a próxima linha BASIC
        .WORD $000A         ; número de linha: 10
        .BYTE $9E           ; token SYS
        .BYTE "2064"        ; 2064 = $0810 em decimal
        .BYTE $00           ; fim da linha
        .WORD $0000         ; fim do programa BASIC

*=$0810                     ; o código começa aqui

Assemblers modernos como o KickAssembler e o ACME cuidam disso automaticamente, mas vale saber o que está acontecendo. O $9E é o token BASIC para SYS, e os bytes seguintes são o número 2064 em texto, o endereço onde o assembly começa. Quando você digita RUN, o BASIC executa SYS 2064 e o controle passa para o nosso código.

Inicialização

Antes de entrar no loop, configuramos o SID e o VIC-II para um estado conhecido. O SID em particular precisa de atenção: ao ligar, seus registradores podem ter valores aleatórios e gerar ruído.

INIT:
        ; Silencia o SID e configura a voz 1
        LDA #$00
        STA $D418           ; volume master = 0 (silêncio total)
        STA $D404           ; desliga gate e waveform da voz 1

        LDA #$08            ; attack=0 (instantâneo), decay=8 (médio)
        STA $D405
        LDA #$F4            ; sustain=F (máximo), release=4 (médio)
        STA $D406

        LDA #$0F
        STA $D418           ; volume master = 15 (máximo)

        ; Cores iniciais: tela preta
        LDA #$00
        STA $D020           ; borda: preto
        STA $D021           ; fundo: preto

        ; Limpa a tela via KERNAL
        LDA #$93            ; PETSCII: CLR (limpar tela e voltar ao início)
        JSR $FFD2           ; CHROUT -- imprime o caractere de controle
        RTS

O valor $93 não é um caractere visível. É um código de controle PETSCII que o chip de vídeo interpreta como “limpar a tela”. Mandar isso para o CHROUT da KERNAL é a forma mais simples de começar com a tela limpa sem escrever diretamente nos 1.000 bytes da screen RAM.

O envelope do SID merece uma nota. $D405 e $D406 guardam os quatro parâmetros ADSR em nibbles de 4 bits cada. Attack=0 significa que a nota atinge o volume máximo instantaneamente, bom para um piano. Release=4 dá um decay suave quando a nota termina, em vez de cortar abruptamente.

O loop principal e a leitura do teclado

O CIA 1 expõe o teclado como uma matriz de 8×8 bits acessível via registradores em $DC00 e $DC01. Percorrer a matriz inteira manualmente é trabalhoso, então para este projeto usamos a rotina GETIN da KERNAL ($FFE4), que já faz esse trabalho e retorna em A o código PETSCII da última tecla pressionada, ou zero se nenhuma tecla foi acionada.

MAIN:
        JSR INIT

LOOP:
        JSR $FFE4           ; GETIN: A = código da tecla (0 = nenhuma)
        BEQ LOOP            ; A=0, nenhuma tecla -- volta ao início

        CMP #$5A            ; 'Z' em 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 desconhecida, ignora

Cada CMP compara o valor em A com o código PETSCII da tecla. Quando encontra uma correspondência, o BEQ desvia para o bloco da nota. Se nenhuma das comparações bater, o JMP LOOP descarta a tecla e volta ao início.

Configurando e tocando cada nota

Cada bloco de nota faz três coisas. Configura a frequência no SID, muda a cor da borda no VIC-II e escreve a letra da nota na tela.

As frequências do SID são calculadas com a fórmula Fn = Fout × 16.777.216 / Fclk. No C64 PAL, Fclk é aproximadamente 985.248 Hz. Para Dó central (261 Hz), o resultado é 4457 = $1169. Cada nota ocupa dois bytes em little-endian, byte baixo primeiro, byte alto depois.

TOCA_DO:
        LDA #$69
        STA $D400           ; freq low  -- $1169 ≈ C4 (261 Hz)
        LDA #$11
        STA $D401           ; freq high
        LDA #$02
        STA $D020           ; borda: vermelho (cor 2)
        LDA #$03
        STA $05F4           ; 'C' na tela -- screen code 3, linha 12 col 20
        LDA #$01
        STA $D9F4           ; cor do char: branco (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           ; borda: verde
        LDA #$04
        STA $05F4           ; 'D' -- screen code 4
        LDA #$01
        STA $D9F4           ; cor do char: branco (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           ; borda: azul
        LDA #$05
        STA $05F4           ; 'E' -- screen code 5
        LDA #$01
        STA $D9F4           ; cor do char: branco (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           ; borda: amarelo
        LDA #$06
        STA $05F4           ; 'F' -- screen code 6
        LDA #$01
        STA $D9F4           ; cor do char: branco (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           ; borda: roxo
        LDA #$07
        STA $05F4           ; 'G' -- screen code 7
        LDA #$01
        STA $D9F4           ; cor do char: branco (color RAM = screen RAM + $D400)
        JSR PLAY
        JMP LOOP

O endereço $05F4 é a posição central da tela ($0400 + 12 × 40 + 20). A screen RAM do C64 usa screen codes, não PETSCII. A vale 1, B vale 2, C vale 3 e assim por diante. O endereço correspondente na color RAM é $D9F4 ($D800 + $01F4), onde gravamos a cor do caractere.

A sub-rotina PLAY

PLAY é chamada por todos os blocos de nota. Ela ativa a voz 1 com waveform triangular, espera um tempo fixo e depois desliga o gate, iniciando a fase de release do envelope.

PLAY:
        LDA #$11            ; waveform triangular ($10) + gate ON ($01)
        STA $D404           ; a nota começa aqui

        ; 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           ; loop interno: 255 × 5 ciclos ≈ 1.275 ciclos
        DEX
        BNE WAIT1           ; loop externo: repete 255 vezes

        LDA #$10            ; mantém waveform triangular, gate OFF
        STA $D404           ; inicia o release -- nota some suavemente

        LDA #$00
        STA $D020           ; restaura borda para preto
        RTS

O delay usa dois loops aninhados contando ciclos de clock, a forma padrão de criar pausas precisas em assembly do 6510. Para notas mais longas, aumente o valor inicial do LDX. Para mais curtas, diminua. Cada unidade de LDX equivale a aproximadamente 1,3 milissegundos.

Como rodar

Salve o código num arquivo .asm, monte com o KickAssembler ou o ACME e abra o .prg gerado no emulador VICE. Dentro do emulador, LOAD "PIANO",8,1 seguido de RUN vai iniciar o programa. Pressione Z, X, C, V ou B.

Se quiser experimentar diretamente sem montar, o VICE tem um monitor de máquina acessível pelo menu Debug. Você pode digitar os bytes diretamente na memória a partir de $0810 e depois G $0810 para executar, exatamente como se fazia nos anos 80.

O que você pode tentar agora

A estrutura está feita. Algumas extensões naturais:

Mais notas. A oitava completa tem 12 notas. Calcule as frequências com a fórmula Fn = Fout × 16.777.216 / 985.248, converta para hexadecimal e adicione mais blocos seguindo o mesmo padrão.

Waveforms diferentes. O valor $11 em $D404 seleciona onda triangular. Troque para $21 (dente de serra), $41 (quadrada) ou $81 (ruído). Cada uma tem um timbre completamente diferente. O ruído, por exemplo, vira percussão.

Sustain enquanto a tecla está pressionada. GETIN detecta apenas o momento em que a tecla entra no buffer. Para sustain real, leia diretamente a matriz do CIA 1 em $DC01 dentro de um loop verificando se a tecla ainda está pressionada antes de desligar o gate.

Duas vozes ao mesmo tempo. O SID tem três vozes independentes, cada uma com seus próprios registradores de frequência, waveform e envelope. Configurar voz 1 e voz 2 com frequências diferentes resulta em duas notas simultâneas.


Isso encerra a série. Começamos na Parte I sem saber o que era um registrador e terminamos aqui: três chips coordenados por um loop de algumas dezenas de instruções, fazendo algo que você pode ouvir e ver. Não há mágica nenhuma no caminho entre os dois. Só bytes, endereços e o processador fazendo o que foi projetado para fazer.