Idiomas disponíveis:

Programando como se fosse 1982. Parte VI – Hardware na Memória

Nos posts anteriores desta série construímos o modelo completo. Registradores, memória, instruções, decisões e loops. Você já sabe como um programa se move pela memória e como o processador executa cada byte que encontra pela frente. O que falta é a parte mais surpreendente da história do C64, e provavelmente a ideia mais elegante de toda a série.

Escrever na memória era acionar hardware

No post sobre o mapa de memória vimos que certas regiões não eram RAM comum. A faixa de $D000 a $DFFF pertencia aos chips de hardware. O VIC-II, o SID e o CIA. Mas o que isso significa na prática só fica claro quando você vê o código.

Para mudar a cor da borda da tela no C64, você escrevia:

LDA #$02    ; 2 = vermelho na paleta do C64
STA $D020   ; escreve no registrador de cor da borda

Duas instruções. Nenhuma chamada de sistema. Nenhum driver. Nenhuma API. Você escreveu um byte num endereço e a borda ficou vermelha imediatamente, no próximo ciclo de clock. Isso é memory-mapped I/O: os chips de hardware viviam no mesmo espaço de endereçamento que a RAM, e escrever nesses endereços não guardava dados. Comandava o hardware diretamente.

Não havia camada entre o código e o efeito. Nenhuma.

Chips de hardware do C64

VIC-II, vídeo sem placa de vídeo

O VIC-II expunha seus 47 registradores na região $D000 a $D02E. Cada um controlava um aspecto do que aparecia na tela. Cores, posição de sprites, modo gráfico, scroll, sincronização com o feixe de vídeo.

A paleta do C64 era fixa em 16 cores, numeradas de 0 a 15. Cor 0 era preto, 1 branco, 2 vermelho e assim por diante. Para mudar o fundo:

LDA #$06    ; 6 = azul
STA $D021   ; registrador de cor do fundo

Sprites seguiam o mesmo padrão. Cada sprite tinha dois registradores para posição, um para X e outro para Y, mais bits de controle espalhados por outros registradores. Para habilitar o sprite 0 e posicioná-lo:

LDA #$01
STA $D015   ; bit 0 = sprite 0 habilitado

LDA #$60    ; posição X = 96
STA $D000   ; registrador X do sprite 0

LDA #$80    ; posição Y = 128
STA $D001   ; registrador Y do sprite 0

Um detalhe que mostra bem o nível de controle disponível: o VIC-II desenhava a tela linha por linha, de cima para baixo, 50 vezes por segundo no padrão PAL. O registrador $D012 informava em qual linha o feixe estava naquele exato momento. Programas que queriam efeitos visuais sincronizados com o raster, como mudar cores no meio da tela, precisavam esperar o feixe chegar na linha certa antes de escrever no registrador. Programadores experientes faziam isso em loops que contavam ciclos de clock, sabendo exatamente quantas instruções cabiam entre uma linha e outra.

SID, um sintetizador dentro de um chip

O SID tinha três vozes independentes. Cada uma podia tocar uma frequência diferente com sua própria forma de onda e envelope de amplitude. Para tocar uma nota, você escrevia a frequência nos registradores da voz, escolhia uma forma de onda e ativava o gate:

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

LDA #$25        ; frequência byte baixo (nota Lá aproximada)
STA $D400
LDA #$1D        ; frequência byte alto
STA $D401

LDA #$A0        ; attack rápido, decay médio
STA $D405
LDA #$F0        ; sustain alto, release longo
STA $D406

LDA #$11        ; forma de onda triangular + gate ON
STA $D404       ; a nota começa a tocar aqui

Oito escritas na memória e você tinha um oscilador triangular tocando uma nota com envelope completo. Para silenciar, desligue o gate. LDA #$10 / STA $D404. Para tocar uma nota diferente, basta mudar os registradores de frequência enquanto o gate está ativo.

O envelope ADSR, Attack, Decay, Sustain, Release, é um conceito vindo da síntese analógica. Define como o volume do som se comporta ao longo do tempo. O tempo que leva para atingir o volume máximo quando uma nota começa, como ele decai depois, qual volume sustenta enquanto a nota está pressionada e quanto tempo leva para silenciar após soltar. Os quatro parâmetros eram divididos entre os dois bytes em $D405 e $D406, quatro bits cada.

CIA, lendo o joystick

O CIA lidava com entradas, e ler o joystick era uma das operações mais comuns em jogos. O registrador $DC00 continha o estado das duas portas de controle:

LDA $DC00       ; lê a porta do joystick
AND #$10        ; isola o bit do botão de fogo
BNE nao_atirou  ; se bit = 1, botão NÃO está pressionado
; chegou aqui: botão está pressionado
nao_atirou:

O detalhe contraintuitivo é a lógica invertida. Bit 0 significa que o botão está pressionado, bit 1 significa solto. Isso se chama active-low, comum em hardware da época onde o pino físico era puxado para baixo quando ativo. Cada direção do joystick correspondia a um bit:

Bit 0 → cima
Bit 1 → baixo
Bit 2 → esquerda
Bit 3 → direita
Bit 4 → botão de fogo

Verificar se o jogador estava indo para cima e para a direita ao mesmo tempo era apenas uma questão de checar os dois bits com operações lógicas. Não havia abstração de “evento de input”. Era leitura de hardware, crua.

O loop principal de um jogo

Com tudo isso em mãos, a estrutura de um jogo no C64 era direta:

game_loop:
        JSR ler_joystick
        JSR atualizar_jogador
        JSR verificar_colisoes
        JSR atualizar_sprites
        JSR esperar_raster      ; sincroniza com $D012
        JMP game_loop

A sub-rotina esperar_raster era o coração da sincronização. Ela ficava num loop lendo $D012 até o feixe chegar a uma linha específica, geralmente abaixo da área visível da tela. Só então o loop avançava para o próximo frame. Isso garantia que todas as atualizações de sprites e cores acontecessem quando o feixe não estava desenhando, evitando o rasgo visual que ocorre quando você atualiza um sprite no meio do feixe passando por ele.

Esse padrão de sincronização manual com o hardware de vídeo é o ancestral direto do VSync que você conhece em engines de jogo modernas. A diferença é que no C64 você implementava à mão, sabendo exatamente o que estava fazendo e por quê.


Neste ponto você conhece os três chips e sabe o que cada endereço faz. O VIC-II muda o que aparece na tela. O SID gera som. O CIA lê o mundo externo. Cada um vive no mapa de memória e responde a escritas como se fossem comandos.

O que falta é ver os três funcionando juntos num programa real, não em exemplos isolados, mas coordenados por um loop que lê uma tecla, toca uma nota e muda uma cor ao mesmo tempo. É exatamente isso que vamos fazer no próximo post.