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.
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.