Escolar Documentos
Profissional Documentos
Cultura Documentos
Linguagem Assembly
para o processador 6502
Introdução
Após vários meses coletando informações sobre programação de jogos para o Atari 2600,
resolvi escrever esse tutorial na tentativa de reunir, em um único documento, todas as
informações que consegui.
Esse tutorial não é um guia genérico, ou seja, não é o dono da verdade que tenta fazer com
que as pessoas leiam e pensem que é a única forma das coisas serem feitas. De fato, é só
uma descrição de como eu aprendi (o pouco que sei) e como faço as coisas acontecerem no
Atari. Isso dará um norte para quem ainda não sabe absolutamente nada (e nem por onde
começar) e para quem já sabe será uma ótima fonte de erros para poderem criticar e corrigir
(hehehe).
Já dizia o sábio: “Quando você transcreve o trabalho de alguém, é plágio. Quando você
transcreve o trabalho de várias pessoas, é pesquisa”.
Como eu aprendi lendo textos de várias pessoas, esse tutorial é resultado de uma pesquisa,
portanto não é plágio. Além do mais, em nenhum dos textos havia ressalva sobre
transcrever parte ou todo seu conteúdo. E sejamos francos: se alguma coisa for útil nesse
tutorial, quem me garante que alguém não vai copiá-lo, mudar algumas coisas, dar seu
toque pessoal, e depois fazer a mesma coisa que estou fazendo (e até melhor)?
Portanto, vamos deixar de blá blá blá e começar a tentar entender como o Atari funciona.
• Não citarei também de onde baixar os programas, fontes, ROMs que uso, pois não
faço a mínima idéia de como consegui. O máximo que farei é dar o nome e versão do
programa, daí é só pesquisar na internet e baixar. O único que eu sei é o AtariPaint
que eu mesmo escrevi.
Referência
Esse tutorial é uma tradução tosca e uma compilação dos seguintes textos:
História
Como não poderia deixar de ser, vamos contar um pouco da história do 2600. Essa
lengalenga toda pode ser ignorada, se quiser.
O texto abaixo é um dos poucos que eu lembro onde li. É uma tradução tosca do .html que
vem junto com o emulador do 2600 que eu uso: o Stella.
Então em 1975, a Atari lançou o Home Pong. Outras empresas como a Magnavox e Coleco
seguiram a idéia e lançaram seus próprios consoles. Em 1976, a Fairchild Camera and
Instrument apresentou o sistema Channel F, que foi o primeiro sistema de video game
baseado na utilização de cartuchos. A indústria reconheceu que sistemas de cartuchos
seriam o futuro dos jogos eletrônicos e partiram para essa direção. Em janeiro de 1977, a
RCA lançou o Studio II, outro sistema baseado em cartuchos, apesar de projetado somente
em preto e branco e focado para títulos educacionais. Então, em outubro de 1977, a Atari
lançou o Atari VCS (Video Computer System) com uma oferta inicial de 9 jogos. Esse
sistema foi, mais tarde, chamado de Atari 2600,
A Atari teve as maiores vendas em 1978 e lançou mais jogos como Outlaw, Spacewar e
Breakout. Só que internamente, estava com problemas. Nolan Bushnell, o inventor do Pong
e fundador da Atari, deixou a empresa. Em 1979 a Atari continuou sua linha e lançou mais
12 jogos com sucesso. Entretanto, a Atari agora enfrentava a concorrência da Mattel
Intellivision e Magnavox Odyssey2.
Em 1981, a indústria do video game era praticamente uma competição entre o 2600 e o
Intellivision. Enquanto o Intellivision era tecnologicamente superior, o 2600 continuava líder
em vendas. A Atari lançou a versão home do Asteroids, que foi um grande sucesso.
Inspirado pelo sucesso da Activision, outro grupo de desenvolvimento de software chamado
Imagic foi formado. Contudo, eles não lançaram nenhum jogo até 1982. Outra empresa,
Games by Apollo, foi criada no Texas e lançou vários jogos. A Coleco entrou no mercado em
1982 com o graficamente superior Colecovision. Para combater o novo sistema, a Atari
lançou o 5200, um sistema tecnologicamente comparável. O 2600 caiu $100 no preço para
continuar competitivo. Então uma empresa chamada Arcadia lançou um periférico chamado
Supercharger que rodava jogos em uma fita K7. Isso permitiu carregamentos múltiplos e
expandiu a capacidade do 2600.
A Atari lançou o Pac-Man e E.T. naquele ano. Apesar de ter vendido muitas cópias do Pac-
Man, esse jogo foi considerado uma versão pobre do seu similar arcade.
Depois que a Atari aceitou a produção de jogos por terceiros, várias empresas surgiram,
como a Venturevision, Spectravision, Telesys, CBS, 20th Century Fox, US Games, M
Network, Tigervision, Data Age, Imagic e Coleco. Havia até mesmo uma empresa que
lançou uma linha de jogos X-Rated (pornôs) para o 2600 chamada Mystique.
A Atari ainda lançou alguns jogos em 1985. A Activision lançou o Cosmic Commuter e
Ghostbusters, mas os jogos não venderam bem. Ainda assim, a Atari vendeu 1 milhão de
consoles nesse ano.
A Nintendo trouxe o NES para a América, e como foi um sucesso, mostrou que os video
games ainda teriam um lugar nos Estados Unidos. A Atari então decidiu que talvez seria
uma boa idéia lançar o 7800 que tinha em estoque e produzir mais jogos para o 2600. O
7800 foi lançado com apenas 3 jogos, apesar de ser compatível com o 2600. Eles também
redesenharam o 2600 como 2600Jr. uma máquina com as mesmas características, mas um
novo visual e campanha de marketing.
Em 1987 a Atari lançou novos títulos, como Jr. Pac-Man e também licenciou jogos de outras
empresas como Donkey Kong e Q*Bert. Outros jogos apareceram como Epyx e Exus. Em
1988, a Atari recontratou Nolan Bushnell e anunciou novos títulos, incluindo Secret Quest
(um jogo escrito pelo próprio Bushnell). A Atari continuou fabricando jogos até 1989.
O 6502
O processador utilizado no 2600 é o 6502 (um melhoramento do 6507). Então vamos passar
rapidamente pela arquitetura desse processador.
O 6502 foi fabricado com tecnologia MOS na década de 70 e era o principal processador
numa grande variedade de microcomputadores como o Apple II, BBC Model B, Acorn
Electron, Atari 800 e o Commodore 64, Vic 20 e modelos PET. Ele tem um conjunto
relativamente simples de instruções para os padrões modernos, o que tornava barata sua
produção.
Especificações básicas
Velocidade de clock 1, 2 e até 3 MHz
Tamanho da palavra 8 bits
Entrada/Saída Memória mapeada
Endereçamento
BUS de endereço 16 bits
Formato Little endian
Página de memória
A memória é vista como um conjunto de páginas de 256 bytes. A primeira página (0000h a
00FFh) é chamada de página zero (zero page) e pode ser acessada usando um modo
especial de endereçamento que permite usar instruções menores e, portanto, de execução
mais rápida. Isso se torna útil para armazenar tabelas de valores ou endereços que são
acessados freqüentemente pelo programa.
A segunda página (0100h a 01FFh) é usada para a pilha do sistema. Ela mantém um
histórico de valores, especialmente durante chamadas de subrotinas e não pode ser movida.
Registradores
O 6502 tem somente 6 registradores. 5 deles são de 8 bits e 1 é de 16 bits.
Accumulator
Usado para todas as operações lógicas e aritméticas (exceto incrementos e decrementos).
Os dados devem ser carregados nesse registrador antes de poderem ser manipulados.
Index Register X
Geralmente usado para contadores ou offsets para acessar a memória. Conteúdos podem
ser comparados com locais de memória e incrementados e decrementados. Diferentemente
de outros registradores (inclusive o Y), pode ser usado para obter uma cópia do ponteiro da
pilha ou mudar seu valor.
Index Register Y
Geralmente usado para contadores ou offsets para acessar memória. Conteúdos podem ser
comparados com locais de memória e incrementados ou decrementados.
Program Counter
Contém o endereço da próxima instrução a ser executada. É incrementado automaticamente
pelo hardware, mas pode ser alterado por saltos, loops, condições e chamadas/retornos de
subrotinas.
Stack Pointer
O 6502 usa uma pilha de 256 bytes localizada na página 1 (0100h a 01FFh). O ponteiro da
pilha é um registrador de 8 bits que guarda o byte menos significativo do próximo local livre
na pilha. Isso significa que a pilha não pode ser movida. A pilha começa em 01FFh e
decresce. Quando um byte é colocado na pilha, o ponteiro da pilha é decrementado. Quando
alguma coisa é retirada da pilha, o ponteiro é incrementado. O hardware não detecta o
estouro da pilha. O programador deve assegurar que o programa não use mais que o
espaço da pilha.
Bit 7 6 5 4 3 2 1 0
N V - B D I Z C
Também conhecido como Flags Register. Usado para indicar os resultados de uma
operação. Cada bit no registrador significa uma condição diferente (um estado diferente).
Algumas das instruções permitem testar os valores dos vários bits, setá-los, limpá-los (zerá-
los), colocar todos na pilha e retirar.
Para entendermos melhor, 1 byte tem 8 bits. O oitavo bit é usado como sinal do número.
Bit 7 6 5 4 3 2 1 0
sinal
Bit 7 6 5 4 3 2 1 0
sinal
Valor 0 1 0 0 0 0 0 0
Sendo o bit 7 (sinal) igual a 0 o número é positivo. Somando 64 + 64 temos 128. Isso em
binário é 10000000b. Então:
Bit 7 6 5 4 3 2 1 0
sinal
Valor 1 0 0 0 0 0 0 0
Modos de endereçamento
O 6502 tem 13 modos de endereçamento (formas de como acessar a memória). São eles:
Modo Formato
Imediato #aa
Absoluto aaaa
Zero page aa
Implícito
Absoluto indireto (aaaa)
Absoluto indexado, X aaaa, X
Absoluto indexado, Y aaaa, Y
Zero page indexado, X aa, X
Zero page indexado, Y aa, Y
Indireto indexado (aa, X)
Indexado indireto (aa), Y
Relativo aaaa
Accumulator A
Portanto, se o endereço 2345h contém o valor EAh e o endereço 2346h contém 12h então a
próxima instrução executada será aquela armazenada no endereço 12EAh. Lembrando que
o endereço no 6502 é little endian (baixo/alto).
Se B4h contém EEh e B5h contém 12h então o valor no local da memória 12EEh + Y(6) =
12F4h é obtido e colocado no acumulador.
Essa instrução soma 127 (7Fh = 127d) ao registrador PC atual e então começa a executar
as instruções a partir daquele endereço. Outro exemplo:
Nesse caso, se o registrador PC estivesse com F03Ch, a instrução colocaria nele F035h,
pois ela diz “você está em F03Ch e vá para 7 bytes para trás”. O processador,
obedientemente, subtrai de F03Ch os 7 bytes e encontra F035h. Por fim executa a instrução
nesse novo endereço e seus subseqüentes até encontrar outra instrução de desvio, ou não.
As instruções do 6502
Vimos até agora a arquitetura do processador e os modos de como acessar a memória.
Agora vamos ver as instruções e o que cada uma delas faz. De forma simples e geral temos:
Grupo de carregamento (load) e armazenamento (store) Grupo de mudança (shift) e rotação (rotate)
DEY Decrement the Y register N,Z TSX Transfer Stack pointer to X N,Z
Grupo de transferência TXS Transfer X to Stack pointer
TAX Transfer Accumulator to X N,Z PHA Push Accumulator on Stack
TAY Transfer Accumulator to Y N,z PHP Push Processor Status on Stack
TXA Transfer X to Accumulator N,Z PLA Pull Accumulator from Stack N,Z
TYA Transfer Y to Accumulator N,Z PLP Pull Processor Status from Stack Todos
Grupo lógico Grupo de mudança do Status flag register
Arithmetic Group
Instrução Descrição Modo de Endereçamento Formato Opcode
Logical Group
Instrução Descrição Modo de Endereçamento Formato Opcode
Stack Group
Instrução Descrição Modo de Endereçamento Formato Opcode
Instruções Aritméticas
ADC – ADd to accumulator with Carry (soma ao acumulador com vai 1)
SBC - SuBtract from accumulator with Carry (subtrai do acumulador com empréstimo)
O 6502 tem 2 modos aritméticos: binário e decimal. Ambas, soma e subtração, implementam
o Carry flag a conter “vai 1” e “vem 1” (empréstimos). Note que no caso da subtração, é
necessário setar o Carry flag com o oposto do carry que está sendo subtraído.
SEC
SBC ...
.
.
SBC ...
.
.
Neste caso, setamos o Carry flag primeiro e então fazemos as subtrações.
Exemplo:
C = não importa
64h ; primeira parcela
clc
C = 0
adc 39h ; segunda parcela
9Dh ; soma
Após o CLC o Carry passou (ou permaneceu) com zero. Se ele fosse 1 o resultado seria
diferente (e não o esperado).
C = 1
64h ; primeira parcela
adc 39h ; segunda parcela
9Dh ; soma
01h ; Carry
9Eh ; resultado final
A mesma regra vale para a subtração. Para fixar: se não quiser que o Carry flag interfira no
resultado, deve-se zerá-lo antes de uma adição e setá-lo antes de uma subtração.
Instruções Lógicas
AND - AND memory with accumulator
ORA - OR memory with Accumulator
EOR - Exclusive-OR memory with accumulator
Essas instruções realizam operações lógicas de acordo com as tabelas verdade abaixo. Os
flags afetados são:
Z = 1 se o resultado é zero
N = 1 se o bit 7 do resultado é 1
Instrução AND
0 0 0
0 1 0
1 0 0
1 1 1 Ambos
Instrução ORA
0 0 0
0 1 1 Ou um
1 0 1 ou
1 1 1 ambos
Instrução EOR
0 0 0 Ou um
0 1 1 ou outro
1 0 1 mas não
1 1 0 ambos
A instrução AND é útil para mascarar bits. Por exemplo, para mascarar a parte alta (high
order, ou também nibble superior) de um valor (byte), basta fazer um AND do valor com 0Fh.
Esse grupo inclui todas as instruções que alteram o fluxo do programa ou realizam uma
comparação de valores ou bits.
As instruções de desvio são saltos relativos. Elas causam o desvio para um novo endereço
que é ou 127 bytes além do endereço corrente (PC) ou 128 bytes antes do endereço
corrente (PC). Códigos que usam instruções de desvio são realocáveis e podem rodar em
qualquer lugar na memória. Eles são relativos pois, em vez das instruções dizerem para ir
para um endereço específico elas dizem ao processador mais ou menos assim: “Em relação
ao endereço atual de PC que é onde a execução do programa está atualmente, vá para n
bytes antes (ou n bytes depois, conforme o caso)”.
As 3 instruções de comparação são usadas para setar os bits dos flags. Depois de uma
comparação, freqüentemente desvia-se para um novo local do programa baseado no
resultado dessa comparação. Esse resultado é simplesmente a mudança de determinados
flags. O relacionamento entre os valores comparados e os flags é:
Comparação N Z C
Valor de A, X ou Y < valor da memória 1 0 0
Valor de A, X ou Y = valor da memória 0 1 1
Valor de A, X ou Y > valor da memória 0 0 1
A instrução de teste de bits testa bits da memória com o acumulador, mas não muda nada.
Somente o flag é mudado. É feito um AND lógico do conteúdo do local da memória
especificado com o acumulador e então os flags são setados como segue:
Portanto, se 23h (que, nesse caso, é o endereço de memória) contém o valor 7Fh e o
acumulador contém 80h, ao executar a instrução:
BIT $23
resultará em:
V=1
Z=1
N=0
Antes do AND, o bit 7 do valor 7Fh é zero, portanto o flag N = 0. O bit 6 desse mesmo valor
(ainda antes do AND) é 1, portanto o flag V = 1. O resultado, ou seja, o valor obtido após a
instrução AND é zero, portanto o flag Z = 1.
Use essas instruções para mover os bits pelo acumulador ou memória. O que acontece no
registrador é mais ou menos:
Z = 1 se o resultado é zero
N = 1 se o bit 7 é 1. No LSR ele sempre é zero
Vamos entender melhor essas instruções. Suponha que temos o valor 63h no acumulador.
Em binário fica assim:
63h = 01100011b
Ao executarmos um ASL, por exemplo, os bits são deslocados para a esquerda. Assim:
Como sabemos, 1 byte tem 8 bits então a cada deslocamento o bit 7 (o mais à esquerda)
tem que sair do byte, pois não há mais “vaga” no byte. Esse bit que foi deslocado à
esquerda e “saiu” do byte está agora no Carry flag, ou seja, o Carry flag assumiu o valor 0
(que era o bit 7 antes do ASL). Note que o bit zero (o mais à direita) foi preenchido com zero.
Agora fazemos mais uma vez o ASL, assim:
A mesma coisa vale para o LSR. Só que, em vez de ser o bit 7 (o mais à esquerda), o Carry
flag recebe o valor do bit 0 (o mais à direita) e o bit 7 recebe zero. Vamos rotacionar o
mesmo exemplo para a direita:
Antes do primeiro LSR, não nos importa o que estava no Carry flag. Depois do LSR, o Carry
ficou com o valor do bit 1 (o mais à direita), e o bit 7 recebeu zero. Os outros bits foram
deslocados para a direita. Mais uma vez:
Antes desse LSR o Carry já era 1 devido ao LSR anterior. O bit 0 era 1 e após o LSR, esse 1
foi para o Carry e os outros bits foram deslocados para a direita. O bit 7 foi preenchido com
zero. De novo:
Antes do LSR o Carry era 1 devido ao LSR anterior. O bit 0 era 0 e após o LSR, esse 0 foi
para o Carry e os outros bits foram deslocados para a direita. O bit 7 foi preenchido com
zero.
Além de outras vantagens as instruções ASL e LSR tem uma função interessante:
multiplicação e divisão. Vamos ver a tabela verdade de 0 a 8.
Vamos supor que eu tenha o valor 00000001b, que na tabela é 1d. Agora vou fazer um ASL.
Deslocando os bits para a esquerda, valor do bit 0 passou para o bit 1, tornando-se 2 em
decimal. Agora mais uma vez:
Agora o 2d passou para 4d. Isso quer dizer a cada deslocamento, o valor foi multiplicado por
2 (em relação ao último resultado, ou seja, cada resultado foi multiplicado por 2. Em relação
ao primeiro valor temos a potência de 2).
O mesmo ocorre para o LSR. Deslocando os bits para a direita, temos a divisão de cada
resultado por 2 ou a raiz (com índice sendo potência de 2) do número inicial. Com isso fica
fácil fazer qualquer multiplicação/divisão. Por exemplo, tenho o valor 3d e quero multiplicá-lo
por 5d. O resultado tem que ser 15d. Como fazer?
Chegamos em 12d, agora basta somar o valor inicial 3d que o resultado dá 15d. Então:
12d = 00001100b
clc
adc 03d = 00000011b
---------------------------------
15d = 00001111b
Usamos o CLC para limpar o Carry flag, só para garantir que nenhum “vai 1” ou “vem 1”
pudesse alterar o resultado.
Como podemos observar, tanto no ASL quanto no LSR, os bits dos extremos vão para o
Carry e no próximo ASL ou LSR, os bits dos extremos vão de novo para o Carry e o valor
anterior do Carry é perdido. Exemplo:
O Carry recebeu o zero que estava no bit 0 (mais à direita). Fazendo novamente:
O Carry recebeu o 1 que estava no bit 0 (mais à direita). O valor zero que estava no Carry se
perdeu. De novo:
O Carry recebeu o zero que estava no bit 0 (mais à direita). O valor 1 que estava no Carry se
perdeu. Novamente:
O Carry recebeu o 1 que estava no bit 0 (mais à direita). O valor zero que estava no Carry se
perdeu.
Agora notamos que, o byte inicialmente era 10d (00001010b) agora é zero (00000000b),
pois o bit 7 (mais à esquerda) foi preenchido com zero a cada LSR executado. Essa é a
diferença entre o ASL/LSR e ROL/ROR. Vamos fazer o mesmo usando ROR, supondo que
o Carry é zero inicialmente.
O Carry recebeu o zero que estava no bit 0 (mais à direita) e o bit 7 (mais à esquerda)
recebeu o que estava no Carry antes do ROR. Fazendo novamente:
O Carry recebeu o 1 que estava no bit 0 (mais à direita) e o bit 7 (mais à esquerda) recebeu
o que estava no Carry antes do ROR, ou seja, o valor zero que foi para o Carry no primeiro
ROR. De novo:
O Carry recebeu o zero que estava no bit 0 (mais à direita) e o bit 7 (mais à esquerda)
recebeu o que estava no Carry antes do ROR, ou seja, o valor 1 que foi para o Carry no
segundo ROR. Novamente:
O Carry recebeu o 1 que estava no bit 0 (mais à direita) e o bit 7 (mais à esquerda) recebeu
o que estava no Carry antes do ROR, ou seja, o valor zero que foi para o Carry no terceiro
ROR. Se fizermos mais um ROR, temos:
Com isso, fazemos com que os 4 bits menos significativos (bit 0 ao bit 3, ou seja, os 4 mais
à direita) passassem para os 4 bits mais significativos (bit 4 ao bit 7, ou seja, os mais à
esquerda) e vice-versa. Se fizermos mais 4 RORs, o byte voltará a ter seu valor original.
Então o LSR e o ASL tratam o byte da seguinte forma:
ASL = Carry
Os bits se deslocam da direita para a esquerda, passam pelo Carry e se perdem.
ASL = Carry
Os bits se deslocam da esquerda para a direita, passam pelo Carry e se perdem.
ROR = Carry
ROL = Carry
Instruções de Transferência
TAX - Transfer Accumulator to X (transfere do A para o X)
TAY - Transfer Accumulator to Y (transfere do A para o Y)
TXA - Transfer X to Accumulator (transfere do X para o A)
TYA - Transfer Y to Accumulator (transfere do Y para o A)
Nesse caso como o bit 7 é 1, pois 80h = 1000000b, o N flag será 1. Outro exemplo:
Instruções de Pilha
TSX - Transfer Stack pointer to X (transfere o ponteiro da pilha para o X)
TXS - Transfer X to Stack pointer (transfere o X para o ponteiro da pilha)
PHA - PusH Accumulator on stack (salva o acumulador na pilha)
PHP - PusH Processor status on stack (salva flags na pilha)
PLA - PulL Accumulator from stack (restaura o acumulador da pilha)
PLP - PulL Processor status from stack (restaura flags da pilha)
As instruções TSX e TXS tornam a manipulação da pilha possível. Mais adiante veremos
como recuperar valores salvos na pilha sem descarregá-la, utilizando para isso o TSX. As
instruções que salvam valores na pilha (iniciadas por PH) e as que restauram valores da
pilha (iniciadas por PL) são úteis para salvar/restaurar valores dos registradores e dos flags.
Vejamos como isso é feito. Conforme já dito o registrador S (stack pointer ou ponteiro da
pilha) aponta sempre para o fim da pilha. No caso do 6502, está no final da página de
memória 1. Então o registrador S tem o valor de 1FFh.
Exemplo:
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h 00h 00h 00h
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h 00h 00h 5Bh
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h C3h 99h 5Bh
Nesse momento, o stack pointer aponta para 1FDh, pega o valor que está lá (C3h) e coloca
no registrador A e permanece (o stack pointer) com o valor de 1FDh.
Nesse momento, o stack pointer aponta para 1FEh, pega o valor que está lá (99h) e coloca
no registrador A e permanece (o stack pointer) com o valor de 1FEh.
Nesse momento, o stack pointer aponta para 1FFh, pega o valor que está lá (5Bh) e coloca
no registrador A e permanece (o stack pointer) com o valor de 1FFh.
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h C3h 99h 5Bh
Ainda está com todos os valores. Eles não são zerados quando são restaurados. Eles
permanecem lá até serem sobrescritos. O stack pointer agora aponta para 1FFh. Quando
salvarmos algum valor novamente na pilha, o 6502 sobrescreverá o valor atual em 1FFh,
que é 5Bh, pelo novo valor a ser salvo.
Instruções de Subrotina
JSR - Jump to SubRoutine (vá para subrotina mas terá que voltar)
RTS - ReTurn from Subroutine (retorne da subrotina)
RTI - ReTurn from Interrupt (retorne da interrupção)
Assim como a instrução JMP, a instrução JSR faz a execução do programa saltar de onde
está e recomeçar o processamento em outro lugar (no endereço dado). Diferentemente da
instrução JMP, a instrução JSR salva o endereço da próxima instrução de onde o
processamento estava. Isso, para o 6502 saber para onde voltar. Por exemplo:
Quando um programa é carregado, ele vai para algum lugar. Esse lugar é identificado no
processador. Esse lugar é onde o registrador PC vai percorrer para executar cada instrução.
Supondo que o programa acima foi para o endereço inicial F026h, ficaria assim:
Cada instrução ocupa um certo número de bytes. Por isso, se uma instrução tem apenas 1
byte, ela ocupa somente um offset. Se tiver 2, 2 offsets e assim por diante. Por isso, no
nosso exemplo, os offsets tem valores não consecutivos. Vamos agora analisar o que
acontece quando uma instrução JSR é executada.
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h 00h 00h 00h
Suponha que a pilha esteja vazia como acima (o registrador S apontando para o final dela).
Quando o 6502 executa o JSR acontece o seguinte:
Então:
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h 00h 2Bh F0h
Na pilha foi salvo o offset F02Bh (no formato little endian, ou seja, o byte menos significativo
primeiro) e o stack pointer (registrador S) decrementou em 2.
Agora o 6502 verifica que o label mostraNaTela está no offset FB5Ah, ele então coloca esse
valor no registrador PC dizendo “agora vamos executar as instruções a partir desse novo
endereço”. O 6502 executa então as instruções do offset FB5Ah e subseqüentes até o offset
FB63h quando encontra a instrução RTS. Essa instrução diz “retorne para o lugar de onde
veio”. Mas de onde ele veio? O lugar está na pilha. Então o 6502 restaura o que está na
pilha e coloca no registrador PC. A execução passa então a ser a partir desse novo
endereço e os subseqüentes.
Se invertermos, por exemplo, onde tem TAX colocarmos TAY e onde tem TAY colocarmos
TAX, os registradores X e Y terão seus valores trocados.
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h 00h 2Bh F0h
Agora vamos supor que a rotina chamada mostraNaTela, inicie no offset FB5Ah como no
exemplo do JSR explicado anteriormente. Até aí tudo bem. Nada de anormal. O PC recebe
FB5Ah e passa a executar as instruções a partir desse offset.
Entretanto, vamos supor que dentro dessa rotina temos uma instrução que salva algum valor
na pilha. Veja abaixo.
Nota: Só para efeito de ilustração foi colocado que o LDA #$33 está no offset FB5Bh. Na
verdade o nome da rotina serve apenas para o compilador, sendo totalmente ignorado na
execução. O que o compilador faz é colocar o endereço da instrução que está logo abaixo
do nome da rotina no programa fonte como sendo o destino da instrução JSR chamadora.
Então, na verdade, o LDA #$33 estaria (nesse exemplo) no offset FB5A e o JSR desviaria o
fluxo da execução diretamente para ele.
Ok, retomando a linha de raciocínio, o programa agora executa a rotina mostraNaTela. Ele
carrega o registrador A com o valor 33h e salva na pilha. A pilha fica assim:
Endereço 1F7h 1F8h 1F9h 1FAh 1FBh 1FCh 1FDh 1FEh 1FFh
Valor 00h 00h 00h 00h 00h 00h 33h 2Bh F0h
Daí ele continua suas execuções e, de repente, encontra o RTS. O que vai acontecer? Ele
vai restaurar 2 valores na pilha, pensando que são o offset seguinte ao offset do JSR
chamador (é o que ele sempre faz). Ele pega esse valor e coloca no registrador PC. Daí o
PC continua a execução a partir desse novo endereço. E o que ele vai restaurar dessa vez?
Simples. Ele vai restaurar os valores 33h e 2Bh. Como ele considera o formato little endian,
ele vai colocar no registrador PC o valor 2B33h e vai desviar o fluxo da execução para esse
endereço. O que acontece? Pau (dispensa comentários). Além do mais, o stack pointer
agora aponta para o endereço 1FEh da pilha. Para que não ocorra esse erro, a rotina tinha
que ser escrita assim:
Concluindo: Para toda instrução que salva algo na pilha, deve-se ter instrução que restaura
da pilha. Salvou, restaurou. É como utilizar parêntesis em expressões algébricas: abriu,
fechou. Um para um. Compiladores de linguagens de alto nível reconhecem quando os
delimitadores não estão balanceados (a quantidade de abertura é diferente da quantidade
de fechamento). O mesmo vale para: para todo IF tem um ENDIF, para todo WHILE tem um
WEND, para todo DO tem um LOOP, para todo FOR tem um NEXT, para todo '(' tem um ')',
para toda '{' tem uma '}'. Tudo isso depende da linguagem de programação. Em assembly
não vale a regra: para todo PHA tem um PLA, para todo PHP tem um PLP. Isso só na
cabeça do programador. O compilador não tá nem aí. Se salvou e não restaurou há 99% de
chance de dar pau e o compilador não avisa. Esse 1% restante é que pode-se salvar e
nunca precisar restaurar o valor. Daí fica o lixo na pilha, ocupando espaço. Coisa feia.
Essas são instruções de 1 byte que modificam os flags. CLC e SEC são instruções de uso
particular em adição e subtração respectivamente. Antes de qualquer adição (usando ADC)
fazemos um CLC para limpar o Carry, caso contrário o resultado pode ser 1 a mais do que o
esperado. Para a subtração (usando SBC), usamos SEC para assegurar que o Carry está
setado, pois seu complemento será subtraído do resultado. Em adições ou subtrações de
vários bytes, deve-se fazer um CLC ou SEC somente 1 vez antes da operação inicial. Por
exemplo, para somar um número de 16 bits que está no endereço 2324h devemos:
Outras Instruções
NOP - No Operation (nenhuma ação)
BRK – BreaK (pare)
NOP é justamente isso, não faça nada. Útil para apagar/substituir instruções já existentes,
mantendo o programa com o mesmo tamanho. BRK acarreta numa parada forçada do
processamento normal e o 6502 vai imediatamente iniciar a execução da rotina que está no
endereço $FFFEh e $FFFFh.
É o mesmo que:
LDA #$nn
TAX
Ou ainda:
LDA #$nn
LDX #$nn
Além dos relacionados a seguir, existem outros. Entretanto, muitos dependem da versão do
6502. Os que não funcionam no compilador que utilizaremos não serão relacionados aqui.
ANC
Faz um AND do byte com o acumulador. Se o resultado é negativo então o Carry é setado.
Afeta os flags N, Z, e C.
ANE
Operação exata desconhecida. Leia documentos de referência para maiores informações.
ARR
Faz um AND do byte com o acumulador, então rotaciona 1 bit para a direita no acumulador e
verifica os bits 5 e 6. Afeta os flags N, V, Z e C. Se ambos forem 1, seta o Carry e limpa o
Overflow; se ambos forem 0, limpa o Carry e o Overflow; se somente o bit 5 for 1, seta o
Overflow e limpa o Carry; se somente o bit 6 for 1, seta o Carry e o Overflow
ASR
Faz um AND do byte com o accumulator, então desloca 1 bit para a direita no acumulador.
Afeta os flags N, Z e C
DCP
Subtrai 1 da memória (sem empréstimo). Afeta o flag C.
ISB
Incrementa a memória em 1, então subtrai a memória do acumulador (com empréstimo).
Afeta os flags N, V, Z e C.
LAS
Faz um AND da memória com o stack pointer, transfere o resultado para o acumulador, para
o registrador X e para o próprio stack pointer. Afeta os flags N e Z.
LAX
Carrega o acumulador e o registrador X com o valor da memória. Afeta os flags N e Z.
LXA
Faz um AND do byte com o acumulador, então transfere o acumulador para o registrador X.
Afeta os flags N e Z.
RLA
Rotaciona 1 bit para a esquerda na memória, então faz um AND do acumulador com a
memória. Afeta os flags N, Z e C.
RRA
Rotaciona 1 bit para a direita na memória, então soma a memória ao acumulador (com vai
1). Afeta os flags N, V, Z e C.
SAX
Faz um AND do registrador X com o acumulador e armazena o resultado no registrador X,
então subtrai o byte do registrador X (com empréstimo). Afeta os flags N, Z e C.
SBC
O mesmo que o opcode válido $E9 (SBC #byte). Afeta os flags N,V,Z e C.
SBX
Faz um AND do registrador X com o acumulador e armazena o resultado em X, então
subtrai o byte do registrador X (sem empréstimo). Afeta os flags N, Z e C.
SHA
Faz um AND do registrador X com o acumulador, então faz um AND do resultado com 7 e
armazena na memória. Afeta os flags -.
SHS
Faz um AND do registrador X com o acumulador e armazena o resultado no stack pointer,
então faz um AND do stack pointer com o byte mais singnificativo do endereço de destino do
argumento + 1. Armazena o resultado na memória. Afeta os flags -.
SHX
Faz um AND do registrador X com o byte mais significativo do endereço de destino do
argumento + 1. Armazena o resultado na memória. Afeta os flags -.
SHY
Faz um AND do registrador com o byte mais significativo do endereço de destino do
argumento + 1. Armazena o resultado na memória. Afeta os flags -.
SLO
Desloca para a esquerda 1 bit na memória, então faz um OR do acumulador com a
memória. Afeta os flags N, Z e C.
SRE
Desloca para a direita 1 bit na memória, então faz um EOR do acumulador com a memória.
Afeta os flags N, Z e C.
Só para constar: em inglês, a pronúncia de ROM como RUM esbarra na palavra room que
significa quarto, sala.
Nesse tutorial vamos nos limitar somente à linguagem assembly. Existe o AtariBasic, um
BASIC feito para escrever jogos para Atari, na verdade, escreve-se o programa em BASIC, e
o AtariBasic transforma-o em assembly e depois usa o compilador assembly. Quem quiser
saber a respeito, basta procurar na internet. Nesse tutorial, será o assembly mesmo, puro,
tudo feito na mão mesmo.
Então vamos lá. Partindo do pressuposto que você, que está lendo esse tutorial, já sabe
programar, tem lógica, conhece todas aquelas coisas sobre sistema de numeração e suas
bases e um mínimo sobre operadores lógicos, podemos continuar. Estou dizendo isso
porque, apesar de começar do começo, não vou chegar ao extremo de passar por aquele
processo maçante de explicar sobre numeração binária, octal, decimal e hexadecimal. Nem
sobre os operadores lógicos E, OU, NÃO e OU-EXCLUSIVO a fundo, posso no máximo dar
uma relembrada sucinta no assunto como já foi feito com os operadores lógicos. Essas
coisas já devem ser de conhecimento (e domínio) de qualquer programador.
Vimos até agora a arquitetura, acesso à memória e as instruções do 6502. Agora vamos ter
alguma prática na coisa.
Para aprendermos o assembly do 6502 vamos utilizar o 6502 Macroassembler & Simulator
versão 1.2.5. (vide figuras 1 e 2). Não vamos começar direto programando com a finalidade
de rodar a ROM no emulador do Atari. Resolvi dar uma explicada usando o simulador para
poder mostrar de forma mais fácil como 6502 funciona.
Figura 1
Figura 2
Figura 3
A primeira coisa é saber que no assembly para 6502, as instruções devem estar a pelo
menos 1 espaço da margem esquerda. Se não houver esse espaço, o compilador
considerará como sendo um label.
Agora temos que dizer em que lugar na memória queremos que nosso programa rode. Para
isso usamos a diretiva .ORG (de ORiGin?) (vide figura 4).
Figura 4
Esse programa mostra no lado direito a descrição, sintaxe e exemplo de como usar a
instrução que está sendo inserida. No nosso caso, vamos dizer que queremos que o
programa comece em 1000h. Para isso, na frente do .ORG já digitado (e com um espaço
depois) colocamos $1000.
Exemplos:
.ORG $1000
LDA #$01 ; A = 1
LDX #$02 ; X = 2
LDY #$03 ; Y = 3
PHA ; salva A na pilha
TXA ; A = X
PHA ; salva A na pilha
TYA ; A = Y
PHA ; salva A na pilha
LDA #$01 ; A = 1
ASL ; A = 2
ASL ; A = 4
ASL ; A = 8
ASL ; A = 16
ASL ; A = 32
ASL ; A = 64
ASL ; A = 128 (N flag é 1 indicando número negativo)
ASL ; A = 0 e Carry flag = 1
LDA #@10000000 ; A = 128
LSR ; A = 64
LSR ; A = 32
LSR ; A = 16
LSR ; A = 8
LSR ; A = 4
LSR ; A = 2
LSR ; A = 1
LSR ; A = 0 e Carry flag = 1
PLA ; restaura A
PLA ; restaura A
PLA ; restaura A
BRK ; termina o programa
Para rodar o programa, tecle F7 (compile). Sempre que alterar alguma coisa no programa
deve-se teclar F7. Se houver algum erro, aparecerá um aviso informando a linha do erro. Se
tudo estiver Ok tecle F6 (debug). O simulador coloca uma seta ao lado da primeira linha do
programa, aguardando um comando para executá-la.
Vamos executar passo a passo o programa e ver o que acontece com os registradores e
pilha. Quando F6 é teclado, o programa abre um monte de janelas. Vamos deixar visíveis
somente a 6502 µP Register & Status e a 6502 µP Stack (vide figura 5). Para executar o
programa passo a passo basta teclar F11. Cada vez que teclamos F11 o simulador executa
1 linha do programa. Então vamos executar e observar a janela dos registradores. Para
parar a execução do programa e reiniciar tecle F6 2 vezes.
Figura 5
Observe os flags. Estão todos zerados (desmarcados). Observe que o registrador PC está
com 1000h (figura 6), que é o valor que colocamos na diretiva ORG dizendo que o programa
será alocado a partir desse endereço.
Figura 6
Ao teclar F11 pela primeira vez, a 1ª linha é executada e podemos observar na janela de
registradores que o A está agora com o valor 1. O programa mostra esses valores nos
formatos hexadecimal, binário, decimal e o caracter correspondente. Observe que o PC
agora é 1002h, indicando que a instrução LDA #$01, ocupa 2 bytes de memória. Teclamos
F11 mais 2 vezes. Agora X e Y também estão com seus valores. Observamos que o PC está
com 1006h. As instruções que colocaram valores em X e Y também ocupam 2 bytes de
memória cada uma (figura 7). De agora em diante, observe o PC sempre que tiver
curiosidade. Chamaremos sua atenção para esse registrador somente quando rodarmos
exemplos de salto, desvio e chamada de subrotina.
Figura 7
Agora, antes de teclar F11, observe na janela de registradores o registrador S (figura 7). Ele
está com o valor FFh, ou seja, a pilha está vazia. Tecle F11 e observe esse registrador. Ele
foi decrementado em 1 quando a instrução PHA foi executada. Na frente dele apareceu o
número 01, que é o valor que foi colocado na pilha. Observe também a janela da pilha
(stack). Agora a primeira linha (numerada com 1FFh) contém o valor 01 (figuras 8 e 9).
Figura 8
Figura 9
Tecle F11 para as próximas instruções e observe o que ocorre: A fica com o valor de X
(quando executa TXA), esse valor é salvo na pilha (PHA), A fica com o valor de Y (quando
executa TYA), esse valor é salvo na pilha (PHA) e depois carregamos A com o valor 1
novamente. Agora as instruções ASL vão deslocar os bits do A para a esquerda. Observe na
janela dos registradores o valor de A em binário enquanto tecla F11. O 1 vai se deslocando
para a esquerda. Agora veja que acontece o que foi descrito anteriormente. Quando A for
128, o bit 7 será 1 (o 1 foi deslocado para a esquerda até chegar no bit 7). Isso faz com que
o N flag seja 1, indicando que o número é negativo (figura 10).
Figura 10
Ao teclar F11 novamente, o último ASL faz o A se deslocar para a esquerda. Como o 1 já
está no extremo, ele vai para o Carry flag e o A fica zerado, pois à medida em que os bits
foram deslocados para a esquerda, foram entrando zeros pela direita. Nesse momento,
observe o Carry e o Zero flags. Ambos estão em 1. Indicando que o bit que saiu do
registrador A foi 1 (e está no carry) e o resultado da operação acarretou no registrador A ser
zero (daí o Zero flag ficou 1) – vide figura 11. Simples.
Figura 11
A próxima instrução (PLA) restaura o último valor colocado na pilha. Então A = 3. Teclamos
F11 novamente e agora A = 2 e novamente A = 1. A pilha agora está vazia. Ao executar a
instrução BRK o programa termina. Tecle F6 para fechar o debugger e retornar ao editor.
Agora vamos rodar um programa simples para vermos como funciona o ROR e o ROL.
Digite (ou copie e cole, depende da preguiça) o programa abaixo:
.ORG $1000
Esse 9º bit do resultado vai para o Carry. Então o A ficou com 00000001b e o Carry com 1.
Outro exemplo:
.ORG $1000
Agora faça os mesmos programas alterando a instrução CLD para SED e observe a
diferença. Faça também a subtração lembrando que a instrução é SBC. Veja o
comportamento quando o Carry é 1 e quando é 0.
.ORG $1000
No caso do programa acima, as 4 primeiras linhas querem dizer que “a ordem dos fatores
não altera o produto”. Se eu carregar A com um valor M e fizer em AND com um valor N, ou
carregar A com valor N e fizer um AND com um valor M, dá no mesmo. As 4 linhas
seguintes são exemplo de mascaramento. Muito útil quando queremos eliminar o nibble
inferior ou superior de um byte. Os 2 últimos EORs mostram a propriedade dessa instrução
de inverter bits.. Um byte que recebe 2 EORs é igual ao (resulta no) próprio byte. Para quem
sabe um pouco sobre álgebra booleana, é fácil entender o AND, ORA e EOR.
O AND é: 0 e 0 dá 0
0 e 1 dá 0
1 e 0 dá 0
1 e 1 dá 1
O ORA é: 0 ou 0 dá 0
0 ou 1 dá 1
1 ou 0 dá 1
1 ou 1 dá 1
O EOR é: 0 ou exclusivo 0 dá 0
0 ou exclusivo 1 dá 1
1 ou exclusivo 0 dá 1
1 ou exclusivo 1 dá 0
Casos interessantes
do AND
45h = 01000101b 45h = 01000101b
0Fh = 00001111b F0h = 11110000b
----- --------------- ----- ---------------
05h = 00000101b 40h = 01000000b
O que aconteceu? O AND tem a propriedade de mascarar bits. No caso acima eu tenho o
valor 45h. Fazendo AND 0Fh extraí o 5 e eliminei (mascarei) o 4. Fazendo AND F0h extraí o
4 e eliminei (mascarei) o 5.
do EOR
AAh = 10101010b 55h = 01010101b
FFh = 11111111b FFh = 11111111b
----- --------------- ----- ---------------
55h 01010101b AAh 10101010b
O que aconteceu? Quando se faz um EOR de um bit com 1, o valor do bit é invertido.
do AND e do ORA
LDA #$41 ; A = caracter 'A' (maiúsculo)
ORA #$20 ; A = caracter 'a' (minúsculo)
AND #$DF ; A = caracter 'A' (maiúsculo)
Carregamos o acumulador com o valor 41h que representa o caracter 'A' maiúsculo.
Fazendo o ORA 20h convertemos para minúsculo e fazendo o AND DFh convertemos para
maiúsculo novamente.
.ORG $1000
Mas é complicado lembrar onde guardamos se tivermos que trabalhar com os números dos
endereços. Para isso criamos labels para esses endereços e chamamos de variáveis.
variavelA = $80
variavelX = $81
variavelY = $82
.ORG $1000
No simulador, quando teclar F6 para rodar o programa passo a passo, tecle ALT+3 para
abrir a janela 6502 µP Zero Page e tecle ALT+2 para abrir a janela 6502 µP Memory. Role
(através da scroll bar de cada janela) até encontrar o endereço 80h ou próximos para ver
como os valores são armazenados nesses endereços que são nossas variáveis. Nos nossos
programas, os valores estarão a partir do endereço 80h, mas podem ser alterados, basta
localizá-los depois nas janelas. Se na janela Memory você rolar o conteúdo até o endereço
próximo de 1000h verá um monte de valores diferentes de 00h que são, nada mais nada
menos, que nosso programa compilado e carregado nesse endereço. Veja figura 12.
Nota: o offset é a primeira coluna de cada janela (zero page e memory). Você pode clicar
nas janelas de memória (zero page e memory) com o botão direito do mouse, escolher a
opção “display from address...” e digitar 0x0080 daí as janelas mostrarão a partir do
endereço da primeira variável.
Figura 12
.ORG $1000
variavelA = $80
variavelX = $81
variavelY = $82
.ORG $1000
No programa acima, utilizamos no final o LDA nome-da-variavel para recuperar (ler) valores
das variáveis. Vale lembrar que a forma como o programa está escrito, é só para
exercitarmos os comandos, não sendo necessariamente obrigatório ser dessa forma.
O LDA variavelX carrega no registrador A o conteúdo da memória no local 81h que está
sendo referenciado pelo label variavelX. O TAX transfere esse valor para o registrador X. O
LDA variavelA carrega em A o valor da variavelA e o LDY carrega o valor da variavelY.
Ora, se sabemos que o valor da variavelX vai para o registrador X então fazemos um LDX
direto e não precisamos passar pelo registrador A (não precisa fazer LDA). Com isso
economizamos 1 comando: o TAX. Fazendo LDX, o valor foi para o X direto. Fazendo LDA
depois temos que transferir para o X. Essa economia (de 1 byte) no 2600 é muito
importante, pois como vamos ver mais adiante, a arquitetura dele é bem simples e restrita,
não nos dando o luxo de gastar bytes do jeito que quisermos.
Só para adiantar um pouco o assunto e fazer uma comparação, hoje em dia falamos em
Gigabytes nos HDs dos PCs. No 2600, temos jogos de 1KB, 2KB, 4KB, 8KB, 16KB e 32KB,
dependendo do jogo em si e da ROM usada. As memórias RAM nos PCs hoje também são
tratadas em termos de Gigabytes. No 2600 (pasmem) são apenas 128 bytes. É isso mesmo:
128 bytes e ainda assim você não pode usar toda ela para guardar dados. Os 128 bytes são
compartilhados com a pilha, portanto (como veremos logo) se a pilha atingir a área de dados
e vice-versa, uma catástrofe acontece. É importante ter em mente os limites de chamadas
de subrotinas e a utilização da memória no 2600.
variavelA = $80
variavelX = $81
variavelY = $82
.ORG $1000
O programa acima é semelhante aos anteriores. Ele salva os valores 10h, 20h e 30h nas
variáveis variavelA, variavelX e variavelY respectivamente. Mas para recuperar os valores a
coisa muda um pouco. Vamos ver as 2 primeiras linhas:
Aqui o Y = 00h e a linha seguinte diz ao 6502: “carregue em A o valor que está em variavelA
+ Y”. Como a variavelA é um label que identifica o endereço 80h (nesse exemplo) e Y = 00h,
a tradução fica: “carregue em A o valor que está em 80h + 00h”. Como sabemos 80h + 00h =
80h, então o valor do endereço 80h vai ser colocado em A (A vai ficar com 10h). Depois Y é
incrementado em 01h com a instrução INY. Daí a linha seguinte diz a mesma coisa que a
LDA anterior, só que a tradução agora fica: “carregue em A o valor que está em 80h + 01h”.
Como sabemos 80h + 01h = 81h, então o valor do endereço 81h vai ser colocado em A (A
vai ficar com 20h). A mesma coisa vale para as 2 linhas seguintes, terminando o A com 30h.
Imagine que o LDA variavelA é o start da coisa e que o Y é o ponteiro. Em relação ao start o
ponteiro pega os valores dependendo do seu próprio valor. Simples. Escrevendo o programa
conforme segue, temos o mesmo resultado dos anteriores:
variavelA = $80
variavelX = $81
variavelY = $82
.ORG $1000
O programa acima coloca os valores 10h, 20h e 30h nas variáveis variavelA, variavelX e
variavelY respectivamente. Agora vamos recuperar esses valores, colocá-los nos seus
respectivos registradores, utilizando modo de endereçamento. Lemos o valor que está em
80h + 00h (variavelA = 80h e Y = 00h), que corresponde ao valor (10h) que queremos deixar
no registrador A, só que esbarramos num dilema: mais à frente, carregaremos em A outros
valores, então precisamos salvar esse valor atual. Para isso usamos o PHA e colocamos o
valor na pilha. Daí incrementamos Y em 1 e carregamos o valor que está em 80h + 01h =
81h, que é o valor (20h) que deve ser colocado em X. Daí transferimos para o X usando o
TAX. Incrementamos Y novamente e carregamos em A o valor que está em 80h + 02h =
82h, que é o valor (30h) que deve ficar em Y e transferimos ele para o Y com TAY. Por fim,
restauramos o valor de A com PLA. Observamos que em vez de
LDA variavelA,y
TAX
LDX variavelA,y
LDA variavelA,y
TAY
substituir por
LDY variavelA,y
A resposta é não. Não pode fazer LDY variavelA,y. Um LDY só pode ter X como indexador e
LDX só pode ter Y como indexador. É importante também observar que o Y foi o último
registrador a ter seu valor recuperado, pois como não vamos mais precisar dele como
índice, pode ter seu valor final recuperado.
variavelA = $80
variavelY = $82
.ORG $1000
Figura 13
Depois colocamos o valor #$30h na variavelY. Agora vamos utilizar Y como índice.
Colocamos o valor 00h em Y e depois fazemos LDA (variavelA),Y. Essa linha diz ao 6502 o
seguinte: “O valor que está na variavelA indexada por Y deve ser considerado como um
endereço de memória. Então leia esse valor, cosidere-o como endereço de memória,
recupere o valor que está nesse novo endereço e coloque-o em A”.
Então 6502 lê o valor 0082h, considera-o como um endereço, pega da memória o valor que
está nesse endereço (que é 30h) e coloca em A.
Nota: Observe que usamos outra forma de acessar uma variável com mais de 1 byte. Nesse
tópico, vimos como os índices funcionam para acessar os outros bytes da variável. Nesse
último exemplo, utilizamos o “+1” para acessar o outro byte da variávelA (STA variavelA+1),
ou seja, se a variávelA está em 80h, a variavelA+1 está em 81h. É o mesmo que: sendo X =
00h, STA variavelA,x acessa o endereço 80h e, sendo X = 01h, STA variavelA,x acessa o
endereço 81h. É a mesma coisa. A diferença está na compilação. STA variavelA vai gerar o
opcode 8D80h (físico no programa compilado) enquanto que variavelA+1 vai gerar o opcode
8D81h (físico no programa compilado). Tanto que colocar no lugar do +1 uma variável
simplesmente não funciona. Exemplo:
Até agora trabalhamos com variáveis de apenas 1 byte (exceto no último exemplo). Vamos
nos aprofundar mais em variáveis de mais de 1 byte. Vamos fazer nossa variavelA ocupar 5
bytes nos próximos exemplos. Como vínhamos utilizando:
Variáveis
A X Y
1 byte 1 byte 1 byte
Variáveis
A X Y
1 byte 1 byte 1 byte 1 byte 1 byte 1 byte 1 byte
variavelA = $80
variavelX = $85
variavelY = $86
A variavelA vai de 80h a 84h totalizando 5 bytes. Agora vamos ver como podemos acessá-la
e armazenar/recuperar valores nessa variável. Vamos dar um exemplo de como fazer isso
utilizando os métodos já vistos e vamos dar outros exemplos utilizando o conceito de loop.
Para fazermos loops, vamos utilizar as instruções de comparação e desvio, já vistas
anteriormente.
Primeiramente, vamos fazer do modo já visto (com uma pequena diferença), supondo que
queremos colocar o valor 3E9B436A11h na variavelA, 20h na variavelX e 30h na variavelY.
variavelA = $80
variavelX = $85
variavelY = $86
.ORG $1000
.valorParaColocarEmA
.byte $3E, $9B, $43, $6A, $11
Vamos tentar entender. Introduzimos uma nova diretiva: .byte. Essa diretiva diz
“simplesmente insira esse(s) byte(s) nesse local, não importando o que eles significam”. No
programa acima, colocamos os bytes que para nós significam o valor que queremos colocar
na variavelA e nos referenciamos a eles com o label .valorParaColocarEmA (entendemos
que seja valor para colocar na variavelA, mas resumimos para o nome não ficar extenso).
O programa utiliza o registrador X como indexador. Daí carregamos ele com 0h, lemos de
.valorParaColocarEmA,x e colocamos no registrador A. Logo em seguida, armazenamos na
variavelA,x. Como X nesse primeiro momento é 0h, o valor lido foi de
.valorParaColocarEmA,00h e armazenado em variavelA,00h. Depois incrementamos X em
01h e o valor lido foi de .valorParaColocarEmA,01h e armazenado em variavelA,01h. Depois
incrementamos X em 01h e o valor lido foi de .valorParaColocarEmA,02h e armazenado em
variavelA,02h. Depois incrementamos X em 01h e o valor lido foi de
.valorParaColocarEmA,03h e armazenado em variavelA,03h. Depois incrementamos X em
01h e o valor lido foi de .valorParaColocarEmA,04h e armazenado em variavelA,04h.
Totalizando os 5 bytes. Depois carregamos X e Y com seus valores e armazenamos nas
respectivas variáveis.
variavelA = $80
variavelX = $85
variavelY = $86
.ORG $1000
.pegaProximoByte
LDA .valorParaColocarEmA,x ; A = byte endereçado por X
STA variavelA,x ; armazena na variavelA + X
INX ; X = X + 01h
CPX #$05 ; pergunta X = 05h?
BNE .pegaProximoByte ; não. Vá para a linha .pegaProximoByte
; se X for 05h continua daqui para baixo
LDX #$20 ; X = 20h
LDY #$30 ; Y = 30h
STX variavelX ; armazena X na variavelX
STY variavelY ; armazena Y na variavelY
BRK ; termina o programa
.valorParaColocarEmA
.byte $3E, $9B, $43, $6A, $11 ; valor a ser colocado na variavelA
O programa utiliza o registrador X como indexador. Daí carregamos ele com 00h, lemos de
.valorParaColocarEmA,x e colocamos no registrador A. Logo em seguida, armazenamos na
variavelA,x. Daí X é incrementado em 01h (INX). Agora usando a instrução CMP (de
comparação), comparamos X com 05h. Se o valor de X for 05h, indica que já lemos e
salvamos 5 bytes, caso contrário indica que ainda faltam bytes para ler/salvar. Assim:
quando X é 00h, vou para a linha .pegaProximoByte e leio/salvo o 1º byte. Quando X é 01h,
vou para a linha .pegaProximoByte e leio/salvo o 2º byte. Quando X é 02h, vou para a linha
.pegaProximoByte e leio/salvo o 3º byte. Quando X é 03h, vou para a linha
.pegaProximoByte e leio/salvo o 4º byte. Quando X é 04h, vou para a linha
.pegaProximoByte e leio/salvo o 5º byte. Quando X é 05h não vou mais para linha
.pegaProximoByte e continuo as instruções seguintes.
Relembrando que o CPX quando é igual faz com que o Zero flag seja 1. Daí o BNE é falso,
saindo do loop e quando ele é diferente o Zero flag é 0. Daí o BNE é verdadeiro, perfazendo
o loop. No bom português é mais ou menos assim:
variavelA = $80
variavelX = $85
variavelY = $86
.ORG $1000
LDX #$04 ; X = 4
.pegaProximoByte
LDA .valorParaColocarEmA,x ; A = byte endereçado por X
STA variavelA,x ; armazena na variavelA + X
DEX ; X = X + 1
BPL .pegaProximoByte ; vá para a linha .pegaProximoByte caso
; X seja maior que zero
; se X for < 0 continua daqui para baixo
LDX #$20 ; X = 20h
LDY #$30 ; Y = 30h
STX variavelX ; armazena X na variavelX
STY variavelY ; armazena Y na variavelY
BRK ; termina o programa
.valorParaColocarEmA
.byte $3E, $9B, $43, $6A, $11 ; valor a ser colocado na variavelA
A diferença básica é: não precisamos mais fazer a comparação (utilizar CPX). Por que? X
inicia com 4 e vem decrescendo (DEX). Quando ele chega a zero o 6502 o considera ainda
positivo pois o bit 7 é zero. Quando ele decresce novamente (00h – 01h) ele passa para FFh
acarretando no bit 7 ser 1. Para o 6502, quando o bit 7 é 1, o número é negativo, sendo
então menor que zero. Daí entra em ação a próxima instrução: BPL = Branch on Plus, ou
seja, desvie (ou vá para) caso seja positivo. Então passo a passo o programa executa
assim:
O programa utiliza o registrador X como indexador. Daí carregamos ele com 04h, lemos de
.valorParaColocarEmA,x e colocamos no registrador A. Logo em seguida, armazenamos na
variavelA,x. Daí X é decrementado em 01h (DEX). A instrução DEX altera o Z flag para 1 se
o X for zero e o N flag para 1 se o bit 7 do X for 1. Agora a instrução BPL verifica se o N flag
é zero. Se for 1 indica que já lemos e salvamos 5 bytes, caso contrário indica que ainda
faltam bytes para ler/salvar. Assim: quando X é 04h, vou para a linha .pegaProximoByte e
leio/salvo o 5º byte. Quando X é 03h, vou para a linha .pegaProximoByte e leio/salvo o 4º
byte. Quando X é 02h, vou para a linha .pegaProximoByte e leio/salvo o 3º byte. Quando X é
01h, vou para a linha .pegaProximoByte e leio/salvo o 2º byte. Quando X é 00h, vou para a
linha .pegaProximoByte e leio/salvo o 1º byte. Quando X é FFh não vou mais para linha
.pegaProximoByte. pois agora sou negativo, e continuo as instruções seguintes.
Relembrando que a instrução DEX altera o Z flag para 1 se o X for zero e o N flag para 1 se
o bit 7 do X for 1. A instrução BPL verifica se o N flag é zero. Se for, o desvio é verdadeiro,
pois o número é positivo. Caso contrário, o desvio é falso, saindo do loop. No bom português
é mais ou menos assim:
variavelA = $80
variavelX = $81
variavelY = $86
.ORG $1000
O exemplo acima é simples. A instrução JMP (Jump) diz ao 6502 para pular para o endereço
do label .saltaRestoDoPrograma. A execução então passa a ser a partir daí. Isso quer dizer
que as instruções abaixo da linha da instrução JMP .saltaRestoDoPrograma nunca serão
executadas, em nenhuma hipótese.
variavelA = $80
variavelX = $81
variavelY = $86
.ORG $1000
.somaVariavelAeX ; subrotina
CLD ; limpa modo decimal
CLC ; zera Carry
LDA variavelA ; A = 10h
ADC variavelX ; A = A + 20h
RTS ; retorna ao programa principal
.subtraiVariavelYeA ; subrotina
CLD ; limpa modo decimal
SEC ; seta Carry
LDA variavelY ; A = 30h
SBC variavelA ; A = A - 10h
RTS ; retorna ao programa principal
O programa acima faz a soma de uma variável com o registrador A na primeira subrotina e a
subtração de uma variável com o registrador A na segunda subrotina. Faça a seguinte
modificação no programa: retire o RTS da primeira subrotina. O que vai acontecer? O
programa começa a executar a primeira subrotina e não vai encontrar instrução de retorno.
Ele então vai continuar a execução pela segunda subrotina e encontrará o RTS lá. Daí sim
ele retorna ao programa principal. A chamada e execução da segunda subrotina é normal.
Vimos até agora pelo menos 1 exemplo de cada grupo de instruções. Agora podemos
evoluir um pouco para o âmbito do 2600. Uma pergunta: O 2600 dependia do que além da
energia elétrica, do cartucho e da criança que atormentou o pai para comprar o video game?
Resposta: da Televisão. E uma vez que o 2600 somente gera o sinal para TV e quem monta
a imagem na verdade é o programador, temos que saber como a TV funciona para
podermos saber como programar o 2600 para montar as imagens. Só depois dessa teoria
de TV construiremos os programas e nos familiarizaremos com o emulador que eu uso (o
Stella) inclusive o modo debugger dele.
A televisão
Agora é a parte séria da coisa. Uma televisão não mostra uma imagem movendo-se
continuamente. De fato, a televisão mostra imagens estáticas em sucessões tão rápidas que
o olho humano percebe como se fossem contínuas. E essas imagens estáticas são
compostas por linhas desenhadas uma após a outra.
Figura 14
No cinema a projeção se faz com uma velocidade de 24 quadros por segundo, velocidade
ou freqüência ideal para nos dar a sensação de movimento. Em TV adotou-se um número
um pouco maior: 30 quadros por segundo, os quais são recompostos 60 vezes por segundo
(2 x 30) porque 60 Hertz (60 ciclos por segundo) é a freqüência da rede elétrica do Brasil.
Isso facilita o projeto da televisão: a própria freqüência da rede elétrica sincroniza a imagem
na tela do televisor.
Imaginemos a situação da figura 15. Um pintor está traçando linhas brancas sobre um
tabuleiro preto. O funcionamento do equipamento utilizado para realizar o trabalho da pintura
é dirigido por um assistente que conduz um veículo dotado de uma plataforma, onde ficará o
pintor. O pintor permanece nesta plataforma e realiza seu trabalho com um pincel e um pote
de tinta. O assistente recebe certas instruções: para iniciar, ele tem de colocar a plataforma
de tal modo que o pintor possa pintar da esquerda para a direita e de cima para baixo. No
E:\Atari\SDK\doc\Tutorial.doc Página 62 de 243
Atari 2600 – Programação em Assembly para o processador 6502 20/8/2007
final de um certo tempo, teremos o painel todo pintado. Na televisão, a projeção da imagem
em cada quadro é feita de maneira análoga, recebendo o nome de varredura.
Figura 15
Figura 16
No Brasil, EUA, Japão, etc. adotou-se um padrão para formar a imagem. O padrão é de 30
imagens por segundo; cada imagem possui 525 linhas. Portanto é varrido (525 x 30) 15.750
linhas por segundo. A tela de TV deve ter 525 linhas horizontais (figura 17) e 700 linhas
verticais (figura 18) resultando em (525 x 700) 367.500 pontos (figura 19).
Figura 17 Figura 18
Figura 19
Figura 20
Figura 21
Depois, ainda no mesmo quadro, na outra metade, somente as linhas pares é que são
transmitidas.
Figura 22
A imagem vem sob a forma de corrente elétrica. Após a amplificação, este sinal entra para o
CRT em forma de um feixe de elétrons. O feixe de elétrons é desviado por campos
magnéticos através do YOKE (bobinas defletoras) colocadas no pescoço do tubo (figura 23).
Figura 23
Os dois tipos de deflexão usados com cinescópio são resumidos na figura 24. As placas
defletoras, marcadas com um V deslocam o feixe para cima e para baixo, e as placas
marcadas com um H deslocam o feixe horizontalmente.
Figura 24
A ação conjunta das defletoras verticais e horizontais é mostrada nas figuras 25 e 26.
Figura 25
Figura 26
Existem variações de sistemas de TV. Vamos citar o NTSC e PAL. A diferença está na
freqüência de operação, número de linhas etc. O que determina o número de quadros
(frames) que ela apresenta em cada segundo.
Mas como são essas linhas? Atrás do tubo de imagem da TV existe um canhão (ou 3 no
caso de TV em cores) que emite um (ou 3) feixe(s) de elétrons para a tela. A tela é recoberta
com fósforo que brilha quando recebe esse feixe de elétrons. Esse feixe é muito fino e onde
ele incide, o fósforo brilha.
O feixe começa a percorrer a tela da TV da esquerda para a direita e de cima para baixo.
Cada vez que ele sai da esquerda e vai para a direita, conta-se 1 linha horizontal. Cada vez
que ele chega no lado direito, volta para a esquerda e desce para iniciar uma nova linha
horizontal, conta-se 1 linha vertical. Só que da direita para a esquerda (quando está voltando
para a esquerda), ele está apagado e não incide na tela (chamado de branco horizontal ou
retraço horizontal). Ele somente incide na tela quando está indo da esquerda para a direita.
E:\Atari\SDK\doc\Tutorial.doc Página 67 de 243
Atari 2600 – Programação em Assembly para o processador 6502 20/8/2007
Quando ele chega no final da tela (no canto inferior direito) ele volta para o canto superior
esquerdo e começa tudo novamente (também não desenha na tela nesse momento. É
chamado branco vertical ou retraço vertical). A cada saída do lado superior esquerdo e
chegada no lado inferior direito (percorrendo toda a tela em forma de 'z') damos o nome de
quadro (ou frame). Então no sistema NTSC temos 525 linhas horizontais e no PAL 625
linhas horizontais.
Apesar de dizermos que o sistema NTSC tem 525 linhas e o PAL 625 linhas, a TV tem um
truque chamado interlacing. Consiste em construir a imagem em 2 frames, cada frame
sendo ou as linhas pares da imagem ou as linhas ímpares da imagem. Cada frame é
mostrado a cada 1/30 segundo para o sistema NTSC e a cada 1/25 segundo para o sistema
PAL. A grande sacada nisso tudo é que um único frame de uma imagem de TV é na verdade
somente metade da resolução vertical da imagem. Portanto, um frame NTSC tem 525/2 =
262,5 linhas e o sistema PAL 625/2 = 312,5 linhas. A 0,5 linha extra é usada para indicar se
o frame é o primeiro (linhas pares) ou o segundo (linhas ímpares) da imagem. Então a
quantidade exata de linhas é 262 para NTSC e 312 para PAL. Analogamente vamos ilustrar
uma matriz, na qual cada coluna representa o tempo e cada linha representa... uma linha.
Vamos supor que cada célula é uma luz branca apagada (então as células estarão escuras).
Exemplo com uma matriz representando um display simples:
Tempo > 1 2 3 4
Linha 1
Linha 2
Linha 3
Linha 4
Linha 5
Linha 6
Linha 7
Agora imagine que alguém está percorrendo as células da linha 1 e tempo 1 até a linha 7 e
tempo 4 (da esquerda para a direita e de cima para baixo). Daí quando você disser:
“Acenda!” a célula que está sendo percorrida no momento em que você disse vai se
acender. Você pode fazer isso aleatoriamente ou definir um padrão (que determinará o
formato do que vai aparentar quando as células estiverem acesas). Por exemplo: supondo
que você disse “acenda!” quando estavam sendo percorridas as células: Linha 1 no tempo 3,
Linha 2 no tempo 2, Linha 2 no tempo 3, Linha 3 no tempo 3, Linha 4 no tempo 3, Linha 5 no
tempo 3, Linha 6 no tempo 3, Linha 7 no tempo 2, Linha 7 no tempo 3, Linha 7 no tempo 4.
Tempo > 1 2 3 4
Linha 1
Linha 2
Linha 3
Linha 4
Linha 5
Linha 6
Linha 7
A matriz ficaria assim, dando a idéia do número 1. A tela de uma televisão é uma matriz
enorme com muitas linhas e muitas colunas. O feixe de elétrons fica percorrendo da
esquerda para a direita e de cima para baixo. Quando você der o sinal, o feixe incide no
fósforo e ele se acende. É assim que funciona a TV. Você vai dizer qual ponto (pixel) da TV
irá acender através da programação do 6502 do 2600. Por isso é importante conhecermos a
Figura 27
Cada scanline começa com 68 clocks de branco horizontal (não visto na tela da TV)
seguidos de 160 clocks para percorrer uma linha completa da TV (68 + 160 = 228 clocks
totais). Isso quer dizer que o 2600 tem 160 pixels de resolução horizontal (pixels da tela que
formam cada linha da imagem). Quando o feixe de elétrons chega no fim do scanline, ele
retorna para o lado esquerdo da tela, aguarda 68 clocks de branco horizontal e começa a
desenhar a linha abaixo. Toda a temporização horizontal é o hardware quem controla, mas o
6502 deve “manualmente” controlar a temporização vertical para sinalizar o início do próximo
frame. Quando a última linha do frame anterior é detectada, o 6502 deve gerar 3 linhas de
VSYNC, 37 linhas de VBLANK, 192 linhas de imagem e 30 linhas de overscan. Ambos
VSYNC e VBLANK podem simplesmente ser ligados e desligados no tempo apropriado,
liberando o 6502 para outras tarefas durante sua execução.
A imagem atual da TV é desenhada uma linha por vez, tendo o 6502 que entrar com os
dados daquela linha no Television Interface Adaptor (TIA). É um microchip dedicado que
será detalhado mais adiante. O TIA pode e deve ter somente os dados que pertencem
àquela linha que está sendo desenhada, então o 6502 deve estar “a um passo à frente” do
feixe de elétrons em cada linha. Uma vez que o ciclo de máquina do 6502 ocorre a cada 3
clocks do TIA, o programador tem somente 76 ciclos de máquina por linha (cada linha tem
228 clocks / 3 clocks = 76 ciclos) para construir a imagem (na verdade é menos porque o
6502 deve estar à frente do feixe). Para dar mais tempo para o software, é costumeiro (mas
não necessário), atualizar o TIA a cada 2 scanlines. A porção do programa que constrói a
imagem é chamada de Kernel, uma vez que é a essência ou kernel do jogo.
Vamos detalhar agora a relação entre o 6502 o TIA e a TV para entendermos melhor o
protocolo, ou seja, cada parte do gráfico da figura 27 será desmembrada para podermos ver
o que deve acontecer em cada uma delas, no seu devido tempo.
O 2600
Agora vamos conhecer a arquitetura do 2600. O processador já vimos. Agora vamos
conhecer outros microchips do video game como o TIA e o PIA.
O TIA e o 6502
É atribuição do programador controlar quantos scanlines são enviados para a TV, mas é o
2600 que prepara o sinal (como cor e intensidade) para qualquer scanline. Essa informação
de cor e intensidade é derivada do estado interno do TIA (Television Interface Adapter). O
TIA é responsável por criar o sinal para um único scanline da TV. O TIA desenha os pixels
na tela. Cada pixel é 1 clock do tempo de processamento do TIA, e há exatamente 228
clocks to TIA em cada scanline. Mas um scanline consiste não somente do tempo que se
gasta para escanear o feixe de elétrons através do tubo de imagem, mas também do tempo
que leva para o feixe de elétrons retornar ao início do próximo scanline (o branco horizontal
ou retraço). Dos 228 clocks, 160 são usados para desenhar pixels na tela (dando-nos a
resolução máxima de 160 pixels por linha) e 68 são consumidos durante o retraço.
O clock do 6502 é derivado do clock do TIA pela divisão por 3. Isto é, para cada clock do
6502, 3 clocks do TIA já se passaram. Portanto, há exatamente 228/3 = 76 ciclos do 6502
por scanline. Memorizando: para cada ciclo do 6502, 3 do TIA. Vale lembrar que 76 ciclos
por scanline x 262 linhas por frame x 60 frames por segundo = número de ciclos por
segundo que o 6502 tem no sistema NTSC. Então, à medida que o 6502 está executando as
instruções do programa, o TIA está enviando dados para cada scanline. A cada ciclo de
tempo do 6502, o TIA já enviou 3 ciclos de informação para a TV. Se o TIA estava nos
primeiros 160 ciclos do scanline, então ele estava desenhando pixels na tela. Se ele estava
no clock 160 a 227, então ele estava fazendo o horizontal blank. Sendo assim, uma vez que
o 6502 está “travado” ao TIA através da temporização compartilhada, é possível para o
programador saber exatamente onde, no scanline, o TIA está atualmente desenhando (qual
pixel). E sabendo onde o TIA está, permite-nos mudar o que está desenhando em posições
específicas do scanline. Naturalmente, para alcançar esse tipo de precisão, o programador
tem que saber exatamente quanto tempo o 6502 leva para executar cada instrução. Por
exemplo, uma combinação de carga e salvamento leva no mínimo 5 ciclos do tempo do
6502. 5 ciclos são quantos pixels na tela? Lembrando que 3 clocks por ciclo do 6502 então 3
x 5 = 15 pixels. Essencialmente, se usamos as mais rápidas combinações de carregamento
e salvamento para mudar a cor (por exemplo) do plano de fundo da tela, então o mais rápido
que isso poderia ser feito é a cada 15 pixels (apenas 11 vezes por scanline).
Os registradores
Todas as instruções do TIA são alcançadas pelo endereçamento e escrita nos vários
registradores do microchip. É importante lembrar que um dado é colocado e retido num
registrador até que seja alterado por outra operação de escrita nesse registrador. Por
exemplo, se o registrador de cor de um jogador é setado em vermelho, o jogador será
desenhado na TV na cor vermelha até que o registrador seja mudado. Todos os
registradores são endereçados pelo 6502 como parte de todo o espaço de RAM/ROM.
Todos os registradores têm locais fixos de endereço e nomes de endereço pré-fixados para
referência mais fácil. Na programação do 2600 no PC é comum utilizarmos o arquivo VCS.H
como include no programa principal. Abra esse aquivo em um editor de textos (Notepad por
exemplo) e veja que há vários nomes (siglas/abreviaturas) como VSYNC, WSYNC, GRP1,
PF0, COLUBK, etc. que fazem referência a um determinado endereço. Por exemplo:
O 09h indica o endereço do registrador do TIA que é responsável pela cor de fundo da tela.
Se você for um gênio, pode decorar todos esses números e saber o que cada um é/faz no
TIA. Ou então, utilize o VCS.H em seus programas para, em vez de decorar números,
aprender os nomes. É muito mais fácil. É como você digitar URLs para acessar sites na
internet em vez de digitar os IPs dos sites, qual é mais fácil de lembrar?
Muitos registradores não usam todos os 8 bits e alguns são usados como strobe ou trigger
de eventos. Um registrador strobe executa sua função no instante em que é escrito (o que é
escrito, ou seja, o valor não importa). Os únicos registradores que podem ser lidos são os de
colisão e os da porta de entrada. Esses registradores são convenientemente dispostos de
forma que os bits de maior acesso sempre aparecem na posição 6 ou 7.
Sincronismo
Temporização horizontal
Quando o feixe de elétrons percorre a tela da TV e chega ao lado direito, ele deve ser
desligado e voltar para o lado esquedo da tela, para iniciar o próximo scanline. O TIA cuida
disso automaticamente, independente do 6502. Um oscilador de 3,58MHz gera os pulsos de
clock chamados "color clocks" que correspondem a 1 pulso do TIA. Esse contador conta 160
color clocks para o feixe alcançar o lado direito, então gera um sinal de sincronismo
horizontal (HSYNC) para retornar o feixe para o lado esquerdo. Ele também gera o sinal
para desligar o feixe (branco horizontal) durante o tempo de retorno que é de 68 color
clocks. Então o total de clocks que o feixe gasta para ir e voltar é de 160 + 68 = 228 color
clocks (figura 28).
Figura 28
Temporização vertical
Quando o feixe de elétrons percorreu as 262 linhas, deve-se comandar a TV a apagar o
feixe e posicioná-lo no topo da tela para começar um novo frame. Esse comando em forma
de sinal é chamado de sincronismo vertical (vertical sync) e o TIA deve transmiti-lo por no
mínimo 3 scanlines. Isso é conseguido escrevendo um 1 no bit 1 do registrador VSYNC para
ligá-lo, contar pelo menos 2 scanlines, então escrever um 0 no bit 1 do VSYNC para desligá-
lo (figura 29).
Figura 29
Figura 30
Após isso vem a parte que desenha a imagem na tela (figura 31).
Figura 31
Depois, basta aguardar 30 linhas de overscan e o frame está completo. Daí, num loop
infinito, reinicia-se a partir do vertical sync (figura 32).
Figura 32
A linha em vermelho mostra o loop infinito. Começa no VSYNC, vai até o overscan e volta ao
VSYNC. Antes desse loop, o código do programa normalmente é de configuração e
inicialização, e pode ser de qualquer tamanho (desde que não seja tão grande que esse
código mais o código do jogo, que está dentro do loop, ultrapassem o espaço da ROM,
claro) – figura 33.
Figura 33
Recaptulando:
Vamos fixar bem esse tópico, pois se você não souber o que fazer em cada tempo do frame,
a imagem não vai se formar perfeitamente.
Antes de chegar nesse ponto, você pode escrever seu código do tamanho que quiser (pode
levar o tempo que for). Esse código é para configurar jogadores, inimigos, playfield, placar
etc. Porém quando, no seu programa, você comandar o sincronismo vertical, tudo dali por
diante tem que estar no seu devido tempo.
Um detalhe interessante. Como temos que aguardar 2 ou 3 WSYNCs, significa que o 6502
fica travado até o TIA alcançar o lado direito da tela por 2 ou 3 vezes, ou seja, o 6502 fica
(76 ciclos x 2 WSYNCs = 152 ciclos ou 76 ciclos x 3 WSYNCs = 228 ciclos) à toa nesse
tempo. Podemos então, substituir os WSYNCs por instruções úteis ao programa, ou seja,
fazer alguns cálculos, ter alguma lógica de programa nesse tempo, desde que não
ultrapasse os ciclos correspondentes aos 3 scanlines do sincronismo vertical. Se
ultrapassar, estará invadindo o tempo do VBLANK e o sincronismo se perde.
Figura 34
Depois do VSYNC temos que aguardar 37 VBLANKs (figura 35). Vale a mesma coisa. São
37 scanlines, ou seja, são 37 WSYNCs. Esses WSYNCs são substituídos pela lógica do
programa. Como, nesse ponto, já estamos dentro do loop, aqui "lemos" as chaves do
console (para verificar se ocorreu um select, reset, por exemplo), "lemos" os joysticks e
fazemos cálculos diversos. Então, nesse tempo temos disponíveis 76 ciclos x 37 scanlines =
2.812 ciclos para o programa.
Se seu programa gastar mais, pode colocar parte dele no tempo do overscan (mais abaixo).
Se gastar menos, deve-se então esperar o tempo restante até completar os 37 scanlines.
Isso mesmo. O tempo deve ser exato, nem mais nem menos. Então se seu programa gastar
somente 1.000 ciclos, os outros 1.812 ciclos devem ser aguardados com WSYNC ou então
utilizando o registrador timer para contar o tempo restante. A mesma regra vale para o
VBLANK: se ultrapassar o tempo, o sincronismo se perde. E tem mais: se não usar todo o
tempo, o sincronismo também se perde. Quando o sincronismo se perde, a imagem fica
deformada ou começa no lugar errado da tela, às vezes acarretando na primeira linha da
imagem começar não exatamente no lado esquerdo da tela e sim mais no meio dela.
Veremos como isso é feito mais adiante, quando iniciarmos nossos primeiros programas.
Veremos também como podemos gastar tempo utilizando instruções e o timer para essa
finalidade.
Figura 35
Ok. Depois do vertical blank, sua TV já está na parte em que desenha na tela. Para
desenhar, o programa deve ter um código que corresponda a desenhar na tela e será
executado nesse tempo (figura 36).
Figura 36
A parte do overscan pode ser utilizada para colocar mais lógica de programa. Ela gasta 30
scanlines, então 76 ciclos x 30 linhas = 2.280 ciclos nesse tempo (figura 37).
Figura 37
Portanto, 228 ciclos de VSYNC + 2.812 ciclos de VBLANK + 2.280 ciclos de overscan =
5.320 ciclos para programação.
Vimos alguns registradores do TIA. Vamos ilustrá-los para melhor entendimento. Mais
adiante veremos mais registradores e à medida que forem apresentados serão ilustrados.
Bit 7 6 5 4 3 2 1 0
WSYNC
Bit 7 6 5 4 3 2 1 0
RSYNC
VSYNC
Esse endereço controla o tempo de sincronismo vertical escrevendo no bit 1 do latch.
Bit 7 6 5 4 3 2 1 0
VSYNC
VBLANK
Esse endereço controla o branco vertical, os latches e os transistores nas portas de entrada
escrevendo nos bits 7, 6 e 1.
Bit 7 6 5 4 3 2 1 0
VBLANK
O registrador PF tem 20 bits e eles estão divididos em 3 endereços: PF0, PF1 e PF2. O PF0
tem somente 4 bits e corresponde aos 4 primeiros pixels do playfield, começando do lado
esquerdo da tela da TV. O PF1 corresponde aos 8 pixels seguintes e o PF2 aos últimos 8
pixels que terminam no centro da tela. O registrador PF é scaneado da esquerda para a
direita e onde é 1 a cor do PF é desenhada na tela, e onde é 0 a cor de fundo é desenhada.
Para limpar o playfield, obviamente zeros devem ser escritos no PF0, PF1 e PF2.
Para fazer a metade direita do playfield, tem-se 2 opções: duplicação ou reflexão do lado
esquerdo. Para a duplicação, basta deixar com 0 o bit 0 do registrador CTRLPF (ConTRol
PlayField). Setando o bit 0 para 1, o playfield será refletido.
Bit 7 6 5 4 3 2 1 0
PF0
Bit 7 6 5 4 3 2 1 0
PF1
Bit 7 6 5 4 3 2 1 0
PF2
CTRLPF
Esse endereço é usado para escrever dentro do registrador do playfield.
Se o bit 0 for 1, o playfield é refletido (REF = 1). Se for 0, o playfield é duplicado (REF = 0).
Se o bit 1 for 1, a cor do placar do lado esquerdo assume a cor do jogador 0 e a cor do
placar do lado direito assume a cor do jogador 1. Se for 0, as cores dos jogadores não
interferem nas cores dos placares.
Se o bit 2 for 1, o playfield tem prioridade sobre os jogadores, ou seja, os jogadores se
movem atrás do playfield. Se for 0, a prioridade do playfield é menor que a dos jogadores, ou
seja, os jogadores movem-se na frente do playfield.
Bit 7 6 5 4 3 2 1 0
CTRLPF
ENAM0 e ENAM1
Esse endereço habilita os mísseis.
Bit 7 6 5 4 3 2 1 0
ENAMx
Bola (BL)
O registrador da bola (ENABL) funciona de maneira análoga aos registradores de míssil.
Setando o registrador, habilita o gráfico da bola até o registrador ser desabilitado. A bola
também pode ser expandida com larguras de 1, 2, 4 ou 8 color clocks, bastando para isso
setar os bits 4 e 5 do registrador CTRLPF. A bola pode também ser delayed verticalmente 1
scanline. Por exemplo, se a bola foi habilitada no scanline 95, ela pode ser delayed para não
mostrar na tela até o scanline 96, bastando para isso setar o bit 0 do registrador de delay
vertical (VDELBL). A razão para ter um delay vertical é porque muitos programas atualizam
o TIA a cada 2 scanlines. Isso faz com que todos os movimentos verticais dos objetos
tenham 2 saltos, ou seja, supondo que um objeto está se movendo verticalmente na tela e a
lógica do programa para fazer isso está incrementando esse movimento de 1 em 1, na tela o
objeto se moverá de 2 em 2 linhas, pois o TIA não está sendo atualizado de 1 em 1 e sim de
2 em 2. Exemplo: seu programa tem um contador que é incrementado de 1 em 1 e algum
objeto pega o valor do contador para se posicionar verticalmente na tela. Quando o contador
é 1, o objeto aparece na linha 1 da tela. Quando o contador é 2, o objeto continua na linha 1
da tela, pois o TIA não foi atualizado. Quando o contador é 3, o objeto aparece na linha 3 da
tela, pois agora o TIA foi atualizado. Antes ele estava na linha 1 agora aparece na linha 3 (3
- 1 = 2). O uso do delay vertical permite que os objetos se movam 1 scanline por vez,
corrigindo assim esse problema.
ENABL
Esse endereço habilita a bola.
Bit 7 6 5 4 3 2 1 0
ENABL
Bit 7 6 5 4 3 2 1 0
VDELxx
O jogador é desenhado linha a linha como todos os outros gráficos. A diferença aqui é que
cada scanline do jogador tem 8 bits, enquanto que os mísseis e a bola têm 1 bit. Portanto,
um jogador pode ser imaginado como sendo uma matriz Nx8, onde N é o número de linhas
(que no caso pode ser qualquer número) e 8 é o número de colunas (largura do jogador).
Para desenhar o jogador, deve-se setar os bits correspondentes dos registradores GRP0
(para o jogador 0) e GRP1 (para o jogador 1). Esses registradores de 8 bits são escaneados
do bit 7 ao bit 0, e quando um 1 é encotrado, o pixel do jogador é desenhado na cor que está
definida em COLUP0 (para o jogador 0) e COLUP1 (para o jogador 1), e quando um 0 é
encontrado, o pixel não é desenhado e a cor de fundo da tela (COLUBK) permanece. Para
posicionar o jogador verticalmente, simplesmente deixamos o registrador GRP0 (ou GRP1,
depende do jogador) em zero até que o feixe de elétrons esteja no scanline desejado.
Quando ele estiver no scanline que queremos, basta então colocarmos no registrador os
valores correspondentes para desenhar o jogador. Depois, basta zerar o registrador
novamente.
Para mostrar uma imagem refletida do jogador, basta setar o bit 3 do registrador REFP0
(para o jogador 0) ou REFP1 (para o jogador 1). Ao zerarmos os registradores, os jogadores
correspondentes são mostrados em sua forma original novamente.
Cópias múltiplas dos jogadores assim como seu tamanho são controlados setando 3 bits (0,
1 e 2) nos registradores de número e tamanho NUSIZ0 e NUSIZ1. Esses 3 bits selecionam
de 1 a 3 cópias do jogador, espaçamento entre essas cópias, assim como o tamanho do
jogador (cada pixel do jogador pode ser 1, 2 ou 4 color clocks). Quando múltiplas cópias são
selecionadas, o TIA automaticamente cria o mesmo número de cópias de mísseis para
aquele jogador.
O delay vertical para os jogadores funciona exatamente como para a bola: setando o bit 0
nos registradores VDELP0 e VDELP1. Zerando esses registradores, desabilita o delay
vertical.
GRP0 e GRP1
Esses endereços desenham os jogadores na tela.
Se o REFP0 ou REFP1 for 0 a saída serial se dá pelo bit 7, caso contrário pelo bit 0. REFP0
e REFP1 correspondem ao espelhamento dos jogadores GRP0 e GRP1 respectivamente.
Bit 7 6 5 4 3 2 1 0
GRPx
COR LUMINÂNCIA
Bit 7 6 5 4 3 2 1 0
COLUxx
Figura 38
REFP0 e REFP1
Esses endereços fazem os jogadores serem desenhados de forma normal ou refletida.
Bit 7 6 5 4 3 2 1 0
REFPx
NUSIZ0 e NUSIZ1
Esses endereços controlam o número e o tamanho dos jogadores e mísseis.
Bit 7 6 5 4 3 2 1 0
NUSIZx
Bit 5 4 Tamanho
0 0 1 clock
0 1 2 clocks
1 0 4 clocks
1 1 8 clocks
Os objetos móveis
A todos os 5 objetos móveis (P0, M0, P1, M1, BL) podem ser atribuídos um local horizontal
na tela e movidos para a esquerda ou direita relativamente à sua posição. Posições
verticais, contudo, são tratadas de maneira completamente diferente. Em princípio, esses
objetos aparecem em qualquer scanline que os registradores sejam habilitados. Por
exemplo, supomos que queremos posicionar verticalmente a bola no centro da tela. A tela
tem 192 scanlines e nós queremos que a bola tenha 2 scanlines de espessura. A bola deve
estar desabilitada até o scanline 96, habilitada por 2 scanlines, então desabilitada pelo resto
do frame. Cada tipo de objeto (jogadores, mísseis e bola) tem sua própria característica e
limitações.
Cor e luminosidade
Cor e luminosidade podem ser atribuídas ao plano de fundo (BK), playfield (PF), bola (BL),
jogadores (P0 e P1), mísseis (M0 e M1). Há somente 4 registradores de cor e luminância
para esses 7 objetos, então os objetos formam pares para compartilhar o mesmo registrador
de acordo com a seguinte lista:
Por exemplo, se o registrador COLUP0 for setado para vermelho claro, ambos P0 e M0
serão vermelho claro quando desenhados. Um registrador de cor e luminância é setado
tanto para cor quanto para a luminância utilizando 7 bits desse registrador. Quatro dos bits
selecionam uma das 16 cores possíveis e os outros 3 selecionam um dos 8 níveis de
luminância. Como todos os registradores (exceto os strobe), os dados escritos nesses
registradores são mantidos até serem alterados por outra operação de escrita.
Posicionamento horizontal
O posicionamento horizontal de cada objeto é feito setando seus correspondentes
registradores reset (RESP0, RESP1, RESM0, RESM1, RESBL) os quais são todos
registradores strobe (eles realizam sua função assim que são acessados). Isso faz com que
os objetos sejam posicionados onde quer que o feixe de elétrons esteja quando o registrador
estava resetado. Por exemplo, se o feixe de elétrons estava 60 color clocks em um scanline
quando o RESP0 foi acessado, o jogador 0 será posicionado 60 color clocks do próximo
scanline. Se o P0 está sendo desenhado ou não é função do registrador GRP0, mas se ele
está sendo desenhado, ele será mostrado no color clock 60 do scanline. Zerar esses
registradores em qualquer lugar durante o HSYNC posicionará os objetos no lado esquerdo
da tela (color clock 0). Uma vez que há 3 color clocks por ciclo de máquina, e gasta-se até 5
ciclos de máquina para escrever em um registrador, o programador está limitado a
posicionar os objetos a 15 color clocks de intervalo pela tela. Isso pode ser resolvido com
uma "sintonia fina" através do movimento horizontal (Horizontal Motion) detalhado mais à
frente.
Bit 7 6 5 4 3 2 1 0
RESxx
RESMP0 e RESMP1
Esses endereços são usados para resetar a localização horizontal do míssil para o centro de
seu jogador correspondente. Enquanto seu bit 1 for 1, o míssil permanecerá travado no
centro de seu jogador e não será exibido na tela (desabilitado). Quando o bit 1 for zero, o
míssil é habilitado e pode mover-se independentemente do jogador.
Bit 7 6 5 4 3 2 1 0
RESMxx
Movimento Horizontal
O movimento horizontal permite ao programador mover qualquer um dos 5 objetos
relativamente às suas posições atuais. Cada objeto tem um registrador de movimento
horizontal de 4 bits (HMP0, HMP1, HMM0, HMM1, HMBL) que pode ser carregado com um
valor de +7 a -8 (valores negativos são expressos na forma de complemento de 2). Esse
movimento não é executado até que o registrador HMOVE é acessado. Quando ele é
acessado todos os registradores de movimento movem seus respectivos objetos. Os objetos
podem ser movidos repetidamente simplesmente executando HMOVE. Qualquer objeto que
não se mova deve ter seu registrador zerado. Com o comando de posicionamento horizontal
limitado a posicionar objetos a intervalos de 15 color clocks, os registradores de movimento
preenchem as lacunas movendo os objetos +7 a -8 color clocks. Objetos não podem ser
colocados em qualquer posição de color clock na tela. Todos os registradores de movimento
podem ser resetados simultaneamente setando o registrador de clear de movimento
horizontal HMCLR.
Bit 7 6 5 4 3 2 1 0
HMxx
0 0 1 0 +2
0 0 0 1 +1
0 0 0 0 0 Sem movimento
1 1 1 1 -1
1 1 1 0 -2
1 1 0 1 -3
1 1 0 0 -4 Número de clocks a mover
1 0 1 1 -5 para a direita
1 0 1 0 -6
1 0 0 1 -7
1 0 0 0 -8
HMOVE
Esse endereço faz com que os registradores de movimento horizontal ajam durante o tempo
de branco horizontal no qual ele ocorreu. Ele deve ocorrer no começo do branco horizontal
de forma a dar tempo para a geração de pulsos de clock extras dentro dos contadores de
posição horizontal. Se o movimento é desejado esse comando deve seguir imediatamente
um WSYNC no programa.
Bit 7 6 5 4 3 2 1 0
HMOVE
HMCLR
Esse endereço limpa todos os registradores de movimento horizontal (sem movimento).
Bit 7 6 5 4 3 2 1 0
HMCLR
Prioridade Objetos
1 P0, M0
2 P1, M1
3 BL, PF
4 BK
Prioridade Objetos
1 PF, BL
2 P0, M0
3 P1, M1
4 BK
Mais um controle de prioridade está disponível para ser usado para mostrar o placar.
Quando o bit 1 do registrador CTRLPF é setado, a metade esquerda do playfield assume a
cor do jogador 0, e a metade direita assume a cor do jogador 1. O placar do jogo pode agora
ser mostrado usando o registrador PF, e o placar será da mesma cor do seu jogador
correspondente.
Colisões
O TIA detecta colisões entre quaisquer dos 6 objetos que ele gera (o playfield e os 5 objetos
móveis). Há 15 possíveis colisões entre 2 objetos que são armazenadas em 15 latches de 1
bit. Cada registrador de colisão contém 2 desses latches os quais são lidos pelo 6502 nos
bits 6 e 7. Um 1 indica que a colisão que ele registra ocorreu. Os registradores de colisão
podem ser lidos a qualquer tempo, mas normalmente isso é feito durante o branco vertical,
depois que todas as colisões possíveis tenham ocorrido. Os registradores de colisão podem
ser resetados simultaneamente acessando o registrador CXCLR.
CXCLR
Esse endereço faz com que todos os latches sejam zero (nenhuma colisão).
Bit 7 6 5 4 3 2 1 0
HMCLR
Som
Há 2 circuitos de audio para gerar som. Eles são idênticos mas completamente
independentes e podem ser operados simultaneamente para produzir efeitos de som pelo
alto falante da TV. Cada circuito de audio tem 3 registradores que controlam o gerador de
tom (que tipo de som), uma seleção de freqüência (pitch alto ou baixo do som) e um controle
de volume.
Tom
O gerador de tom é controlado acessando os registradores de controle de audio AUDC0 e
AUDC1. Esses registradores são de 4 bits. Os valores geram diferentes tipos de som.
Alguns são tons puros (como uma flauta), outros têm vários ruídos como um motor de
foguete ou explosão.
AUDC0 e AUDC1
Esses endereços escrevem nos registradores de audio que controlam o ruído e divisão da
saída de audio.
Bit 7 6 5 4 3 2 1 0
AUDCx
Freqüência
A seleção de freqüência é controlada acessando os registradores de áudio freqüência (de 5
bits) AUDF0 e AUDF1. Os valores escritos são usados para dividir uma freqüência de
referência de 30KHz, criando pitch mais alto ou mais baixo de qualquer tipo de som gerado
no gerador de tom. Pela combinação de tons puros gerados pelo gerador e tom com a
seleção de freqüência, uma ampla gama de tons podem ser conseguidos.
AUDF0 e AUDF1
Esses endereços escrevem nos registradores divisores de freqüência de audio.
Bit 7 6 5 4 3 2 1 0
AUDFx
Volume
O volume é controlado acessando os registradores de volume (de 4 bits) AUDV0 e AUDV1.
Zerando esses registradores, desliga-se o som completamente e escrevendo qualquer valor
até 15, aumenta o som de acordo com o valor.
AUDV0 e AUDV1
Esses endereços escrevem nos registradores de volume de áudio.
Bit 7 6 5 4 3 2 1 0
AUDVx
Portas de entrada
Há 6 portas de entrada. Seus estados lógicos podem ser obtidos lendo o bit 7 dos endereços
de porta de entrada INPT0 a INPT5. Elas são divididas em 2 tipos: dumped e latched.
O PIA (6532)
O microchip PIA (Peripheral Interface Adapter) tem 3 funções: um timer programável, 128
bytes de RAM e 2 portas paralelas de 8 bits, ou seja, RAM, portas de entrada/saída (IO
Ports) e Timer: o RIOT
O Timer
O PIA usa o mesmo clock do 6502, de forma que 1 ciclo do PIA ocorre para cada ciclo de
máquina. O PIA pode ser setado para um dos 4 diferentes intervalos, onde cada intervalo é
múltiplo do clock (e portanto ciclos de máquina). Um valor de 1 a 255 é carregado no PIA o
qual será decrementado em 1 a cada intervalo. O timer pode agora ser lido pelo 6502 para
determinar o tempo passado e assim temporizar várias operações de software e mantê-las
sincronizadas com o hardware (TIA).
Setando o timer
O timer é setado escrevendo um valor ou contagem (de 1 a 255) no endereço do intervalo
desejado, de acordo com a seguinte tabela:
Por exemplo, se o valor 100 foi escrito no TIM64T, o timer seria decrementado até 0 em
6400 clocks (64 clocks por intervalo x 100 intervalos), o que poderia ser também 6400 ciclos
de máquina do 6502.
Lendo o timer
O timer pode ser lido tantas vezes quantas forem necessárias depois que ele foi setado
(claro), mas o programador geralmente só está interessado se o timer chegou a 0 ou não. O
timer é lido, simplesmente acessando o INTIM.
programador determinar há quanto tempo atrás o timer zerou no evento que o timer foi lido
depois que passou do 0.
RAM
O PIA tem 128 bytes de RAM localizados no mapa de memória do Stella (do endereço 80h
ao FFh). A pilha do 6502 é normalmente localizada de FFh e anteriores, e as variáveis são
normalmente localizadas de 80h em diante.
As portas de entrada/saída
As 2 portas (Porta A e Porta B) são de 8 bits e podem ser setadas para ou entrada ou saída.
A Porta A é usada para interfacear controladores de mão, mas a Porta B é dedicada a ler o
status das chaves do console do Stella.
Uma vez que o DDR tem seus pinos configurados para entrada e/ou saída, eles podem ser
lidos ou escritos acessando o endereço SWCHA.
Joysticks
2 joysticks podem ser lidos configurando a porta toda como entrada e lendo os dados em
SWCHA de acordo com a seguinte tabela:
Um 0 no bit significa que o joystick foi movido para fechar aquela chave. Se o nibble do
jogador estiver todo com 1, indica que o joystick não está se movendo.
Paddle (pot)
Somente os triggers do paddle são lidos do PIA. Os próprios paddles são lidos de INPT0 a
INPT3 do TIA. Os triggers podem ser lidos no SWCHA de acordo com a tabela seguinte:
Bit Paddle n°
7 P0
6 P1
5/4 não usados
3 P2
2 P3
1/0 não usados
Teclado
O teclado tem 12 botões dispostos em 4 linhas e 3 colunas. Um sinal é enviado para uma
linha, então as colunas são verificadas para saber se um botão está pressionado. Então a
próxima linha é sinalizada e todas as colunas sensibilizadas, etc. até que todo o teclado
tenha sido escaneado. O PIA envia os sinais para as linhas, e as colunas são sensibilizadas
pela leitura do INPT0, INPT1 e INPT4 do TIA. Com a Porta A configurada como porta de
saída, os bits de dados enviarão um sinal para as linhas do teclado de acordo com a
seguinte tabela:
Nota: Um atraso de 400 microsegundos é necessário entre a escrita dessa porta e leitura
das portas de entrada do TIA.
Ciclos de máquina
Em vários trechos do tutorial falamos a respeito de ciclos de máquina e que as instruções do
6502 gastam determinado número de ciclos.
Conceito de contagem
Programar o 2600 requer modificar a percepção de espaço e tempo, pois o 2600 tem um
tipo de física onde espaço é tempo. Um frame é 1/60 segundo. Um scanline é 1/20000
segundo. É importante saber quanto código pode ser executado na quantidade de tempo
que leva para desenhar a tela. A unidade de tempo é ciclo.
X = (ciclos - 20) * 3
onde ciclos é o número de ciclos que passaram desde o branco horizontal. Mas os
registradores são lidos somente a cada 5 ciclos, então a equação deve ser ajustada para
levar isso em conta. Por ora, vamos assumir que arredondaremos para o próximo múltiplo
de 15. Os exemplos envolverão o RESP0, porque já sabemos como ele funciona.
.inicio
sta WSYNC ; começa a contar aqui, comece com zero 0
nop ; 0+2 = [2] gasta 2 ciclos apesar de não fazer nada
lda #0 ; 2+3 = [5] gasta 3 ciclos
sta $FFFF ; 5+4 = [9] gasta 4 ciclos
rol $FFFE,X ; 9+7 = [16] gasta 7 ciclos
rol ; 16+2 = [18] gasta 2 ciclos
sta RESP0 ; 18+3 = *21* gasta 3 ciclos
dey ; 21+2 = [23] gasta 2 ciclos
bne .inicio ; 23+3 = [26] gasta 2 ciclos se falso e 3 se verdadeiro
Os números antes do sinal de '+' é o número de ciclos que se passaram desde o WSYNC.
Os números dentro dos colchetes é o número de ciclos que passaram ao final de cada
instrução. É bom ter um histórico desse número porque escritas nos registradores do TIA
ocorrem nesse ciclo.
Note que o número 21 está com asteriscos, significando uma escrita num registrador do TIA.
(21 - 20) x 3 = 3 e uma vez que é RESP0, nós arredondamos para 15 e é para onde vai o
jogador 0.
Instruções de desvio
Instrução de desvio como BNE e BCC são mais fáceis do que parecem. Todas as instruções
de desvio gastam 2 ciclos. Elas gastam 3 ciclos se, e somente se, o desvio ocorrer. E ainda,
gastam mais 1 ciclo se o desvio ocorrer para além dos limites de página.
Instruções de armazenamento
As instruções STA, STX e STY têm a mesma temporização que as instruções matemáticas,
mas no caso dos endereçamentos Absolute,XY e (Indirect),Y sempre teremos um ciclo extra.
Instruções weenie
Essas instruções não alteram a memória, somente registradores e flags. Elas são CLC,
CLD, CLI, CLV, DEX, DEY, INX, INY, NOP, SEC, SED, SEI, TAX, TAY, TSX, TXA, TSX e
TYA. Elas gastam 2 ciclos.
é dado, elas modificam a memória diretamente. As instruções matemáticas lentas são ASL,
DEC, INC, LSR, ROL e ROR.
ROR ; +2 Accumulator
ROR $99 ; +5 Zero Page
ROR $99,X ; +6 Zero Page,X
ROR $1234 ; +6 Absolute
ROR $1234,X ; +7 Absolute,X
Note que quando essas instruções funcionam com o acumulador, elas diminuem para 2
ciclos e se tornam instruções weenie .
Instruções de pilha
As 2 instruções que colocam valores na pilha (PHA e PHP) gastam 3 ciclos cada. As 2
instruções que recuperam valores da pilha (PLA e PLP) gastam 4 ciclos cada.
Outras instruções
JSR gasta 6 ciclos. JMP gasta 3 ciclos no endereçamento Absolute e 5 ciclos no
endereçamento Absolute Indirect, mas o modo Abolute Indirect é para máquinas que têm um
kernel. RTI e RTS gastam 6 ciclos cada. Mas com somente algumas dezenas de instruções
disponíveis por scanline, não temos tempo para ficar executando subrotinas.
.inicio
sta WSYNC ; a contagem começa aqui
nop ; [0] +2
bit $CC ; [2] +3
bmi .desviaPraCa ; [5] +2 se o desvio não ocorrer
nop ; [7] +2 os NOPs só servem para gastar tempo
nop ; [9] +2
nop ; [11] +2
nop ; [13] +2
nop ; [15] +2
nop ; [17] +2
nop ; [19] +2
.desviaPraCa
lda $F0 ; [21] +3
sta GRP0 ; *24* +3
lda $F1 ; [27] +3
sta ENAM0 ; *30* +3
sta RESP0 ; *33* +3
sta WSYNC ; a contagem deve recomeçar daqui
Vamos contar os ciclos no segundo exemplo mostrando como fica se o desvio ocorrer.
.inicio
sta WSYNC ; a contagem começa aqui
nop ; [0] +2
bit $CC ; [2] +3
bmi .desviaPraCa ; [5] +3 se o desvio ocorrer e vai para .desviaPraCa
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
.desviaPraCa
lda $F0 ; [8] +3
sta GRP0 ; *11* +3
lda $F1 ; [14] +3
sta ENAM0 ; *17* +3
sta RESP0 ; *20* +3
sta WSYNC ; a contagem deve recomeçar daqui
Esse código (que não serve para nada) verifica o bit 8 do local da memória CCh. Se é 1, vai
imediatamente para o label .desviaPraCa (o desvio acontece e gastamos 3 ciclos) e seta a
posição do jogador 0 no ciclo 20 (exemplo 2). Se o teste resultar em 0, então o desvio não
acontece e economizamos 1 ciclo (o desvio gasta só 2 ciclos) e gastamos 14 ciclos
passando pelos NOPs. Agora ele gasta 33 ciclos para posicionar (reset) o jogador 0.
Em rotinas de exibição na tela, como por exemplo placar com 6 dígitos, as 6 instruções
(Indirect),Y aparecem em 1 scanline. Isso adiciona até 6 ciclos extras que podem ou não ser
gastos. Isso pode fazer com que a temporização seja perdida a menos que os dados
estejam arranjados de forma apropriada.
Tenha certeza de que quando colocar dados no programa, disponha-os de forma que eles
nuca cruzem o limite de página ou sempre cruzem o limite de página.
Tão logo você seja capaz de prever quando um ciclo extra será necessário, você terá um
código estável. Assegure que todos os bytes de uma tabela de gráfico estejam na mesma
página de memória.
Nota: Pode-se usar PHA/PHP (1 byte e 3 ciclos) ou PLA/PLP (1 byte e 4 ciclos) de forma
independente, mas deve-se tomar cuidado para não bagunçar a pilha.
Começando a programar
O ambiente
Você pode escrever seus programas no Edit do DOS, no Notepad do Windows ou em
qualquer outro editor que grave arquivos no modo texto, não formatado. Usamos o Crimson
Editor (figura 39).
Figura 39
Ele é free e faz o trabalho. Se optar por usá-lo, é interessante fazer algumas configurações:
No menu Tools -> Preferences, vá no ítem User Tools (figura 40) e configure atalhos para
compilar, executar e debugar seu programa. Essas opções serão criadas no menu e terão
teclas de atalho. Observe a figura 40.
Em Menu Text colocamos o que vai aparecer no menu: &Compile. O “&” faz o C do Compile
ficar sublinhado no menu.
As figuras 41 e 42 mostram os outros atalhos criados. Para quem ainda não entendeu: isso
serve para, quando digitarmos um programa, basta teclarmos Ctrl+1 para compilar, Ctrl+2
para executar (depois de compilado) ou Ctrl+3 para debugar. O Ctrl+3 executa também o
Stella com a diferença da opção –debug (figura 42).
Figura 40
Figura 41
Figura 42
O próximo passo é fazer com que ele reconheça os comandos do 6502. Como ele pretende
ser um IDE, então temos que ensiná-lo. Ele não vem com os comandos do 6502, então, vou
quebrar o galho e mostrar como fazer.
Figura 43
Vá no diretório LINK e crie um arquivo chamado EXTENSION.ASM (figura 44). Crie usando
qualquer editor de texto (que grave no formato texto sem formatação) e digite o seguinte:
LANGSPEC:2600-ASM.SPC
KEYWORDS:2600-ASM.KEY
Figura 44
Figura 45
Salve e arquivo e vá no diretório SPEC (figura 46). Crie um arquivo chamado 2600-
ASM.SPC. Digite o seguinte:
$CASESENSITIVE=NO
$HEXADECIMALMARK=$
$QUOTATIONMARK1="
$QUOTATIONMARK2='
$LINECOMMENT=;
PAIRS1=()
$PAIRS2=[]
$PAIRS3={}
Figura 46
Figura 47
Agora, no mesmo diretório (SPEC), crie um arquivo chamado 2600-ASM.KEY (figura 48).
Figura 48
[-COMMENT-:GLOBAL]
# 6502 ASM LANGUAGE DEFINITION FILE FOR CRIMSON EDITOR
# ATARI 2600 STYLE COMPILER - 10/07/2006
[KEYWORDS0:GLOBAL]
#instruction set
lda ldy ldx sta sty stx pha php pla plp
tax tay txa tya tsx txs bne beq bmi bpl
bcc bcs bvc bvs bit cpx cpy cmp jmp jsr
rts brk rti adc sbc and ora eor lsr lsl
asl ror rol dec dex dey inc iny inx clc
cli clv sec sed sei nop anc ane arr asr
dcp isb las lax lxa rla rra sax sbx sha
shs shx shy slo sre cld
[KEYWORDS1:GLOBAL]
#assembler directive
org rorg include processor
[KEYWORDS2:GLOBAL]
.ascis .byte .db .ascii .word .dw .dbyte .dd .str .string
.dcb .rs .ds .opt .org .start .end .include .macro .endm
.exitm .if .else .endif .error .repeat .rept .endr repeat repend
db dw ds dc dv mac endm if endif equ word byte
seg seg.u dc.bwl ds.bwl dv.bwl hex err rorg echo rend
align subroutine eqm string set mexit ifconst ifnconst if else
endif eif
[KEYWORDS3:GLOBAL]
*=
[KEYWORDS4:GLOBAL]
#operators
* / % + - >> << > >= <
<= == = != & ^| && || ?
[]´$
[KEYWORDS5:GLOBAL]
#registers
A Y X VSYNC VBLANK WSYNC RSYNC NUSIZ0 NUSIZ1 COLUP0
COLUP1 COLUPF COLUBK CTRLPF RESP0 RESP1 PF0 PF1 PF2 RESP0
RESP1 RESM0 RESM1 RESBL AUDC0 AUDC1 AUDF0 AUDF1 AUDV0 AUDV1
GRP0 GRP1 ENAM0 ENAM1 ENABL HMP0 HMP1 HMM0 HMM1 HMBL
VDELP0 VDELP1 DELBL RESMP0 RESMP1 HMOVE HMCLR CXCLR CXM0P CXM1P
CXP0FB CXP1FB CXM0FB CXM1FB CXBLPF CXPPMM INPT0 INPT1 INPT2 INPT3
INPT4 INPT5 SWCHA SWCHB SWBCNT INTIM TIMINT TIM1T TIM8T TIM64T
T1024T TIM1I TIM8I TIM64I T1024I
Figura 49
Agora, para o Crimson Editor ficar bonitinho, vá no menu Tools -> Preferences e vá no ítem
Colors. Altere o esquema de cores a seu gosto e salve. O programa mudará as cores das
palavras (que reconhecerá nos arquivos criados) segundo o esquema. Não se esqueça de
salvar o esquema. A figura 50 mostra meu esquema de cores para General Colors. As
figuras 51 e 52 mostram para Keyword Colors e Miscellaneuos respectivamente.
Figura 50
Figura 51
Figura 52
Figura 53
Figura 54
Pronto. Agora basta criarmos um arquivo novo e começarmos a programar. Nosso ambiente
está configurado.
Figura 55
O cabeçalho
Os programas em assembly para o 2600 comumente têm o seguinte cabeçalho:
processor 6502
include vcs.h
include macro.h
org $F000
A primeira linha diz ao compilador o seguinte: “vou utilizar o conjunto de instruções do 6502”.
A segunda linha, inclui no seu programa (só o compilador vê isso) o conteúdo do arquivo
VCS.H. É interessante ter esse arquivo, caso contrário você deverá declarar os nomes dos
registradores e endereços dentro de seu programa. O VCS.H já traz todos eles. Se tiver
esse arquivo, abra-o no Notepad (por exemplo) e veja seu conteúdo. Só não altere nada
nesse arquivo. A terceira linha diz para incluir macros em seu programa. Mais adiante
veremos o uso de macros. Por enquanto, se quiser omitir a terceira linha, pode. Vamos
convencionar aqui o uso das 3 linhas, só por questão de costume.
Nota: Se os arquivos VCS.H e MACRO.H estiverem em outro diretório, esse deve estar
explícito no include. Nos programas aqui apresentados, meus arquivos VCS.H e MACRO.H
estão num subdiretório, chamado INCLUDES, acima do diretório dos meus programas e os
cabeçalhos serão:
processor 6502
include ..\includes\vcs.h
include ..\includes\macro.h
org $F000
A linha org $F000 diz ao Stella que aloque nosso programa no offset F000h. E daí? Se
temos 64K de endereçamento, então 64K = 65536d = 10000h – F000h = 4K = 4096d =
1000h, que é o tamanho da ROM: 4K. Você escreve jogos de até 4K endereçando
inicialmente em F000h. Se o jogo for maior, utilizamos 2 ROMs de 4K (temos 8K para o
jogo). Para mudar de uma ROM para a outra, utilizamos rotinas de bankswitching, que serão
vistas mais adiante (muito mais adiante). Não veremos aqui como escrever ROMs menores
que 4K. Vamos ver mais as de 4K e depois, no tópico de bankswitching, ROM de até 32K.
O rodapé
Os programas para o 2600 devem ter o seguinte código no final:
org $FFFA
.word label_do_início_do_jogo
.word label_do_início_do_jogo
.word label_do_início_do_jogo
Nota: estamos simplificando as coisas aqui. Na programação do 6502, a primeira linha .word
é para o NMI, a segunda para o RESET e a terceira para o IRQ. Se não temos rotinas
separadas para cada caso, direcionamos o vetor todo para um único local: o início do jogo.
Lembre-se que .word é uma diretiva que gasta 2 bytes (1 word = 2 bytes). Com as 3 linhas
.word declaradas, criamos um vetor cujos valores de cada posição são o endereço
referenciado pelo label do início do jogo (lembra que o endereço tem 16 bits, ou seja, 2 bytes
= 1 word?). Então se temos 3 linhas com a diretiva .word e cada uma gasta 2 bytes temos 6
bytes no total (duh). Isso tudo é para dizer o porquê do org $FFFA. Ora, FFFAh + 6h =
10000h que totaliza nossos 64K de endereçamento. Simples, não?
O programa
Então vamos escrever o seguinte programa (vide figura 56). Ele é uma fusão dos programas
descritos nas páginas 59 e 60 deste tutorial (com umas poucas diferenças).
Figura 56
Se estiver usando o Crimson Editor e ele estiver configurado conforme descrição anterior,
use as teclas de atalho para compilar e debugar o programa digitado. No meu caso, eu usei
Ctrl+1 para compilar, Ctrl+2 para rodar o programa e Ctrl+3 para debugar.
Declarando variáveis
Veja a declaração das variáveis.
variavelA = $80 ;
variavelX = $85 ;
variavelY = $86 ;
A primeira variável está referenciando o endereço 80h da RAM. Se observar o Stella (na
janela do debugger) verá que a RAM começa no endereço 80h e vai até FFh. A segunda
variável referencia o endereço 85h. Isso quer dizer que a primeira variável tem 5 bytes de
tamanho (80h, 81h, 82h, 83h e 84h). No nosso exemplo usamos a variável como sendo de 5
bytes. Você pode declarar uma variável em 80h e outra em 90h, por exemplo, e usar
somente 1 byte de cada uma. Porém isso é um desperdício de bytes. O ideal é usar
variáveis referenciando a endereços consecutivos, sem “espaços”. Lembre-se: só temos 128
bytes de RAM e ainda temos que compartilhá-los com a pilha. Uma outra forma de declarar
variáveis é:
Seg.U Variaveis ;
org $80 ;
;
variavelA ds 5 ;
variavelX ds 1 ;
variavelY ds 1 ;
Aqui declaramos um segumento de nome Variaveis e dizemos que ele começa em 80h
através do org $80. A partir daí não precisamos mais dizer a qual endereço cada variável se
referencia, e sim o “tamanho” de cada uma delas através do DS (Define Storage). No
exemplo acima. A variávelA tem 5 bytes, a variavelX e a variavelY tem 1 byte cada uma. O
compilador já sabe que, a variavelA começa em 80h e a variávelX começa em 85h, uma vez
que a variavelA tem tamanho 5 e a variavelY está em 86h. Se optar por usar a segunda
opção, lembre-se: você declarou um segmento para as variáveis então deve declarar o
segmento de código. Antes do ORG $F000 deve-se colocar SEG nome_do_segmento.
Assim:
Seg.U Variaveis ;
org $80 ;
;
variavelA ds 5 ;
variavelX ds 1 ;
variavelY ds 1 ;
Ao clicarmos pela primeira vez no botão Step (no canto superior direito), o programa é
executado em 1 passo. Cada clique corresponde a 1 passo de execução. Note, na figura 58,
que agora o PC e o registrador X estão destacados (em vermelho). O emulador destaca os
E:\Atari\SDK\doc\Tutorial.doc Página 109 de 243
Atari 2600 – Programação em Assembly para o processador 6502 20/8/2007
registradores (e endereços de memória) que tiveram seus valores alterados. Eles estão
destacados porque, ao clicarmos em Step pela primeira vez, o programa executou a
instrução LDX #$04 e o PC foi para o próximo offset.
Registradores e flags
O seu programa
Figura 57
Figura 58
Sincronizando com a TV
Agora vamos fazer nosso primeiro programa sincronizado com a TV. Ele também não fará
nada, mas através dele veremos a estrutura básica de um programa para o 2600 funcionar.
Vamos tentar ilustrar antes. Conforme a figura 59, antes de iniciar o sincronismo vertical,
podemos fazer qualquer coisa no programa, a qualquer tempo. Ainda não estamos
sincronizados com a TV.
Quando iniciamos o sincronismo (no Vertical Sync), temos que aguardar 3 scanlines. Daí
utilizamos 3 WSYNCs. Como cada scanline gasta 76 ciclos de máquina, temos 3 x 76 = 228
ciclos disponíveis para algum código, mas raramente usamos. Normalmente colocarmos 2
ou 3 WSYNCs e pronto.
Daí começa o Vertical Blank (VBLANK). Ele gasta 37 scanlines. Portanto temos 37 x 76 =
2.812 ciclos de máquina aqui. Dá para escrever muita coisa (código de programa) nesse
tempo. A lógica do programa fica nessa parte.
Depois disso, vem a parte que desenha na tela. São 192 scanlines.
E, por fim, temos o Overscan, que são 30 scanlines. Daí, 30 x 76 = 2.280 ciclos de máquina
aqui. Podemos também escrever muita lógica de programa nessa parte.
Figura 59
Durante o VBLANK e o Overscan, você pode escrever seus códigos não se importando se
eles vão gastar 76 ciclos por scanline. Você só tem que se preocupar com que eles não
ultrapassem seus limites: 2.812 ciclos para o VBLANK e 2.280 ciclos para o Overscan. Por
outro lado, durante os 192 scanlines que desenham na tela, é vital que suas instruções não
ultrapassem 76 ciclos por scanline. Se isso acontecer, o desenho na tela pode não sair
como o esperado.
Mais adiante, veremos esses paus de lógica e matemática. Agora, vamos ver nosso
programa sincronizado com a TV. No primeiro exemplo, ele ainda não mostrará nada na
tela. Será somente a descrição da figura 59.
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;aguarda 37 scanlines
sta WSYNC ;nesses 37 scanlines é que escrevemos a lógica
sta WSYNC ;do jogo
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
lda #$00 ;desliga o VBLANK
sta VBLANK ;
REPEAT 192 ;
sta WSYNC ;aguarda 192 scanlines (aqui desenha na tela)
REPEND ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;aguarda 30 scanlines
sta WSYNC ;aqui podemos ter mais lógica de jogo
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
sta WSYNC ;
jmp .inicioDoFrame ;vai para o próximo frame
;
ORG $FFFA ;
.word .inicioDoJogo ;NMI
.word .inicioDoJogo ;Reset
.word .inicioDoJogo ;IRQ
Entre os labels .inicioDoJogo e .inicioDoFrame podemos escrever qualquer código (no nosso
exemplo, não tem nada). Do label .inicioDoFrame até o VBLANK, temos 37 WSYNCs, que
correspondem ao branco vertical. Aqui fica a lógica do jogo. Quando escrevermos algo de
útil, substituiremos os STA WSYNC por código que faz alguma coisa. Depois do VBLANK
temos o REPEAT. Como já dissemos, a imagem é composta de 192 scanlines. Então, um
dos comandos do DASM, o REPEAT, diz (em tempo de compilação) para repetir o(s)
comando(s) que está(ão) entre REPEAT e REPEND n vezes. No nosso caso, o compilador
repetirá STA WSYNC 192 vezes na ROM. Do REPEND até o JMP temos 30 STA SWYNC
que correspondem ao overscan. O nosso exemplo poderia ser escrito assim:
Isso criará um programa enorme na ROM: 543 bytes. Tanto o exemplo com REPEAT-
REPEND quanto o exemplo expandido (o primeiro exemplo), gastam 543 bytes. O REPEAT-
REPEND facilita somente o código fonte. Para economizarmos bytes da ROM temos que
usar LOOPs. Assim:
O programa acima difere dos anteriores no que diz respeito a executar as instruções STA
WSYNC. Nos exemplos anteriores, temos 37, 192 e 30 linhas de STA WSYNC que estão na
ROM e o processador as executará uma a uma. No exemplo acima, temos apenas 1 STA
WSYNC para cada parte do programa (VBLANK, TELA e Overscan). Mas no VBLANK esse
STA WSYNC é executado 37 vezes pelo loop. Na tela, ele é executado 192 vezes e no
overscan o STA WSYNC é executado 30 vezes pelo loop. Portanto, não precisamos
escrever o STA WSYNC várias vezes. Com o exemplo acima, gastamos apenas 46 bytes da
ROM. Para saber quantos bytes seu programa está gastando da ROM, basta colocar as
seguintes linhas entre as linhas JMP e ORG $FFFA:
Acima, o programa com as linhas ‘echo’ para verificar quantos bytes o programa tem e,
conseqüentemente, o número de bytes restantes na ROM.
Figura 60
Antes de prosseguirmos com programas que realmente fazem alguma coisa, vamos voltar
rapidamente à tela do debugger do Stella para algumas considerações. Veja a RAM
mostrada na figura 61. Quando iniciamos o programa, a RAM tem valores aleatórios. Isso
acontece também no 2600. Devido às características físicas internas dos microchips, ao ligar
a energia elétrica, os latches (flip-flops que são os bits da RAM) podem assumir valor 0 ou 1.
Isso faz com que cada byte da RAM tenha um valor qualquer, o que pode influir no
funcionamento do jogo. Para contornar esse problema, é comum zerar toda a RAM no início
do programa. Para isso, basta endereçar a RAM num loop e colocar o valor zero lá.
Figura 61
O código que usaremos no início dos programas, que seta o processador e zera a RAM é:
Observe que usamos o illegal opcode LAX. Poderíamos usar LDA $00 e LDX $00. Com o
LAX economizamos 2 bytes de ROM. Lembre-se economia de bytes da ROM, RAM e ciclos
de máquina são a chave para qualquer jogo.
O procedimento de zerar a RAM é feito uma vez quando do início do jogo. Vamos inserir o
código acima em um dos exemplos já mostrados.
Compile e rode o programa. Veja que a tela apresentada pelo Stella é igual à mostrada na
figura 62. Dentro do loop correspondente à parte que desenha na tela (figura 59, página 112
desse tutorial), fazemos com que o registrador X seja acrescido em 1 unidade e colocamos o
valor no endereço COLUBK que muda a cor de fundo da tela (página 81 desse tutorial).
Figura 62
Observe que o valor de X é mudado a cada loop então cada linha da tela tem uma cor
diferente. Vamos ver o programa escrito de outra forma:
A instrução que muda o valor de X está fora do loop e a instrução que coloca X como a cor
de fundo da tela está dentro do loop. Isso quer dizer que X terá seu valor incrementado e
esse valor será colocado como cor de fundo da tela 192 vezes, fazendo a tela inteira ter uma
só cor em cada frame. Mas quando o frame muda, a cor muda também. Por exemplo,
supomos que X é 20h. Quando a instrução INX é executada, X passa para 21h e o STX
COLUBK é executado 192 vezes (pois está dentro do loop). Como o registrador endereçado
por COLUBK é um latch, ele retém esse valor até que seja alterado. Daí temos o STA
WSYNC que diz para o 6502 aguardar o fim do scanline. Enquanto o 6502 aguarda o fim do
scanline, o TIA continua desenhando a linha com o valor retido no COLUBK. Então, temos
que 192 linhas da mesma cor = tela toda de uma cor só. Veja as figuras 63 e 64.
Quando os 192 scanlines são desenhados, o loop termina, entra no overscan e vai para o
próximo frame. Daí o X é incrementado novamente e a tela é redesenhada com outra cor. A
tela então se apresenta mudando de cor completamente, em um ciclo infinito. Agora
notamos a diferença entre os 2 últimos programas mostrados: o primeiro deles muda a cor
de fundo a cada linha da tela, o outro muda a cor de fundo a cada frame.
Figura 63
Figura 64
Vamos agora juntar esses 2 efeitos apresentados em um único programa. Ele vai mostrar a
tela com cores diferentes por quadro e depois por linha (figuras 62 e 63/64 alternadamente)
e ficará nesse ciclo indefinidamente.
Primeiramente criamos 2 variáveis. A wFlag que terá 0 ou 1. Se for zero, indica que
queremos mudar a cor por quadro. Se for 1 indica que queremos mudar a cor por linha. A
variávei wContador será incrementada em 1 e será testada. No primeiro teste, se wContador
for diferente de zero, o programa vai para linha .naoMudaTipo. Se wContador for zero, o
programa faz um XOR do valor da wFlag com 1. Isso fará o valor de wFlag ser invertido: se
for 1 passa a ser zero e se for zero passa a ser 1. Daí salvamos o resultado em wFlag para
uma nova comparação no próximo frame.
O processo se inverte quando a variável wContador chegar a zero novamente. Vale ressaltar
que os programas estão sendo escritos sem nos preocuparmos com gasto de bytes e ciclos
de máquina. Como é para vermos o funcionamento da coisa, deve ser o mais didático
possível. As otimizações virão com o tempo.
...
...
.scanLoop ;
sta WSYNC ;aguarda 1 scanline
lda wFlag ;A = 0?
beq .naoMudaCorDaLinha ;sim. não muda cor da linha
inx ;muda cor da linha
;
.naoMudaCorDaLinha ;
stx COLUBK ;muda cor de fundo
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
...
...
Vamos observar os ciclos de máquina gastos nessa parte do programa. Veja a figura 65.
Montamos uma tabela com os ciclos e analisamos a execução.
Figura 65
Recordando do que foi dito na página 92 desse tutorial: “toda instrução de desvio gasta 2
ciclos se o desvio não ocorrer e 3 ciclos se o desvio ocorrer”. Então, vamos supor que o
wFlag é 1, ou seja, em 80h da memória temos o valor 1. O programa vai executar a primeira
linha e gastar 3 ciclos. Executa a segunda e gasta 2 ciclos. Depois a terceira (mais 2 ciclos)
e assim por diante. Quando ele chegar na última linha (BNE $F03C) ele vai ter gasto: 3 + 2 +
2 + 3 + 2 = 12 ciclos (ainda não executou o BNE $F03C). Depois de executar a sexta linha
ele vai ter gasto 12 + 3 = 15 se o desvio ocorrer (se Y for > 0 o desvio ocorre) e 12 + 2 = 14
se o desvio não ocorrer (se Y = 0 o desvio não ocorre).
Se o valor de wFlag for zero, o desvio no BEQ $F041 vai ocorrer, gastando 3 ciclos e não 2.
Daí, quando ele chegar na última linha (BNE $F03C) ele vai ter gasto: 3 + 3 + 3 + 2 = 11
ciclos (ainda não executou o BNE $F03C). Depois de executar a sexta linha ele vai ter gasto
11 + 3 = 14 ciclos se o desvio ocorrer (se Y > 0 o desvio ocorre) e 11 + 2 = 13 se o desvio
não ocorrer (se Y = 0 o desvio não ocorre).
No nosso programa, a diferença de 1 ciclo entre os casos não interferiu. Mas em outros
programas, esse tipo de diferença pode fazer um estrago. Por isso, é bom acostumarmos a
sempre gastar o mesmo número de ciclos tanto para condições verdadeiras quanto para
falsas dentro do scanLoop, ou seja, na parte que desenha a tela.
Figura 66
...
...
.scanLoop ;
lda wFlag ;A = 0?
beq .naoMudaCorDaLinha ;sim. não muda cor da linha
inx ;muda cor da linha
nop ;
nop ;nesse tempo a tela é desenhada com a cor
nop ;do que está em X
nop ;o latch COLUBK retém a cor durante todo
nop ;esse tempo.
nop ;esses NOPs determinam o início da coluna
sty COLUBK ;aqui mudamos o valor do COLUBK
nop ;a tela é desenhada com essa nova cor nesse
nop ;tempo, apresentando uma coluna.
nop ;os 3 NOPS determinam a largura da coluna
;
.naoMudaCorDaLinha ;
stx COLUBK ;muda cor de fundo
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
...
...
A alteração faz com que parte da tela fique com cores diferentes do restante, por coluna, ou
seja, o feixe de elétrons percorre a tela da esquerda para a direita, desenhando a linha em
determinada cor (dada por X em STX COLUBK), e em certo momento, (depois que ele saiu
da esquerda e está indo para a direita da tela), ele passa a desenhar na tela com a cor
determinada por Y em STY COLUBK. A tela fica como na figura 67. Se adicionarmos NOP
antes do STY COLUBK, a coluna vai se deslocar mais para a direita, pois o programa ficará
mais tempo desenhando o que está em X. Se adicionarmos NOP depois do STY COLUBK, a
coluna ficará mais larga, pois o programa ficará mais tempo desenhando o que está em Y.
Para termos a impressão de que a coluna aparece e desaparece basta alterar o código
acima para:
...
...
nop ;o latch COLUBK retém a cor durante todo
nop ;esse tempo.
nop ;esses NOPs determinam o início da coluna
stx COLUBK ;aqui mudamos o valor do COLUBK
nop ;a tela é desenhada com essa nova cor nesse
nop ;tempo, apresentando uma coluna.
nop ;os 3 NOPs determinam a largura da coluna
;
.naoMudaCorDaLinha ;
Figura 67
Vamos animar um pouco as coisas e vermos o que acontece quando colocamos um código
que faz o scroll da tela. Primeiro, vamos fazer scroll do plano de fundo. Para isso altere o
código conforme segue:
...
...
Seg.U Variaveis ;
org $80 ;
;
wFlag ds 1 ;se for 0 muda tela. se for 1 muda cor da linha
wContador ds 1 ;contador
wScroll ds 1 ;faz a imagem “rolar”
...
...
...
...
inc wScroll ;wScroll = wScroll + 1
.scanLoop ;
lda wFlag ;A = 0?
beq .naoMudaCorDaLinha ;sim. não muda cor da linha
tya ;A = Y
adc wScroll ;A = A + wScroll
tax ;X = A
nop ;
nop ;nesse tempo a tela é desenhada com a cor
nop ;do que está em X
nop ;o latch COLUBK retém a cor durante todo
nop ;esse tempo.
nop ;esses NOPs determinam o início da coluna
sty COLUBK ;aqui mudamos o valor do COLUBK
nop ;a tela é desenhada com essa nova cor nesse
nop ;tempo, apresentando uma coluna.
nop ;os 3 NOPs determinam a largura da coluna
;
.naoMudaCorDaLinha ;
stx COLUBK ;muda cor de fundo
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
...
...
O resultado é o mesmo da figura 67, exceto que a maior parte da imagem estará “rolando”
para baixo. A coluna permanece estática. Para fazer a coluna rolar e o restante ficar
estático, basta alterar conforme abaixo:
...
...
stx COLUBK ;aqui mudamos o valor do COLUBK
nop ;a tela é desenhada com essa nova cor nesse
nop ;tempo, apresentando uma coluna.
nop ;os 3 NOPs determinam a largura da coluna
;
.naoMudaCorDaLinha ;
sty COLUBK ;muda cor de fundo
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
...
...
Agora, vamos fazer com que nossa coluna se mova horizontalmente, da direita para a
esquerda (figuras 68 e 69). Para isso faça o seguinte:
...
...
Seg.U Variaveis ;
org $80 ;
;
wFlag ds 1 ;se for 0 muda tela. se for 1 muda cor da linha
wContador ds 1 ;contador
wScroll ds 1 ;faz a imagem “rolar”
wDesloca ds 1 ;desloca coluna
...
...
...
...
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
lda #$08 ;A = 8
sta wDesloca ;wDesloca = A
;
.inicioDoFrame ;início do frame
lda #$02 ;liga o VSYNC
...
...
...
...
inc wScroll ;wScroll = wScroll + 1
bne .scanLoop ;aproveita wScroll para causar delay na coluna
dec wDesloca ;wDesloca = wDesloca - 1
bne .scanLoop ;wDesloca = 0? não, desloque a coluna
lda #$08 ;A = 8
sta wDesloca ;posiciona a coluna na direita da tela
;
.scanLoop ;
tya ;A = Y
adc wScroll ;A = A + wScroll
pha ;salva A
ldx wDesloca ;X = wDesloca
;
.horiz ;
dex ;X = X - 1
bne .horiz ;X = 0? Não, continua decrementando X
pla ;restaura A
tax ;X = A
stx COLUBK ;muda a cor de fundo
nop ;a tela é desenhada com essa nova cor,
nop ;nesse tempo, apresentando uma coluna
nop ;os 3 NOPs determinam a largura da coluna
;
.naoMudaCorDaLinha ;
sty COLUBK ;muda cor de fundo
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
...
...
O que acontece aqui é bem simples: X tem valor que varia de 8 a 1, dado por wDesloca.
Quando o 6502 entra no loop .horiz, X é decrementado até atingir zero. Quanto maior for o
valor de X, mais tempo o 6502 vai ficar no loop, fazendo com que a coluna demore mais
tempo para aparecer na tela. Quando, então, ele sai do loop, a coluna é desenhada. Quanto
maior o valor de X, mais para a direita estará a coluna e quanto menor o valor de X, mais
para a esquerda estará a coluna, pois o loop .horiz demora menos e a coluna é desenhada
em um tempo bem menor depois do último WSYNC.
Para que seja possível visualizar o deslocamento, temos que causar um delay, pois o
deslocamento acontece de forma tão rápida que é difícil vê-lo perfeitamente. Aproveitamos a
variável wScroll. Todas as vezes que ela chega a zero, o sistema então decrementa o
wDesloca, movendo assim a coluna. Enquanto wScroll não é zero, a coluna é redesenhada
na mesma posição. Quando wScroll é zero, wDesloca é decrementado e a coluna passa a
ser desenhada um pouco mais para a esquerda.
Figura 68
Figura 69
Foi dito que após o VSYNC temos 37 scanlines de VBLANK. No nosso primeiro kernel,
gastamos o tempo dos 37 scanlines utilizando 37 linhas STA WSYNC após o VSYNC (vide
páginas 112, 113, 114 e 115 deste tutorial). Como sabemos, 37 scanlines * 76 ciclos = 2.812
ciclos de máquina disponíveis para a lógica do programa antes de entrarmos na área de
desenho da tela (momento em que o feixe de elétrons começa a desenhar na tela).
Vamos destacar os screenshots que vimos até agora. Na figura 70 destacamos parte da
figura 67.
Figura 70
Um dos problemas é a faixa escura no início da tela. A imagem começa a ser desenhada
algumas linhas abaixo da parte superior da tela, ficando uma faixa escura no lugar. Vamos
ver o porque isso está ocorrendo, transcrevendo separadamente as partes do nosso
programa e relacioná-las com as fases do frame.
.inicioDoJogo ;label
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
lda #$08 ;
sta wDesloca ;
O fragmento acima é a parte que inicializa as variáveis, limpa a memória, etc. É onde pode-
se escrever o que quiser sem se preocupar com o tempo.
O fragmento acima é a parte que sincronizamos com a TV (vide figura 71). Temos aí 2 ou 3
scanlines. A partir desse ponto tudo deve estar no seu devido tempo.
Figura 71
Depois do VSYNC, pelo que já vimos, começa o VBLANK. São 37 scanlines de 76 ciclos
cada.
sty VBLANK ;liga VBLANK
ldy #$25 ;25h = 37d
;
.aguardaVBLANK ;
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .aguardaVBLANK ;vai para .aguardaVBLANK se Y > 0
Figura 72
.scanLoop ;
tya ;A = Y
adc wScroll ;A = A + wScroll
pha ;salva A
ldx wDesloca ;X = wDesloca
;
.horiz ;
dex ;X = X - 1
bne .horiz ;X = 0? Não, continua decrementando X
pla ;restaura A
tax ;X = A
stx COLUBK ;muda a cor de fundo
nop ;a tela é desenhada com essa nova cor,
nop ;nesse tempo, apresentando uma coluna
nop ;os 3 NOPs determinam a largura da coluna
;
.naoMudaCorDaLinha ;
sty COLUBK ;muda cor de fundo
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
O fragmento acima é a parte que desenha na tela. Ele pode ter 192 scanlines e, de
preferência, 76 ciclos de instrução por scanline. Corresponde à figura 73.
Figura 73
ldy #$1E ;1Eh = 30d
;
.overScan ;
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .overScan ;vai para .overScan se Y > 0
O fragmento acima é o overscan. Pode-se usar lógica de programa aqui também (figura 74).
Figura 74
jmp .inicioDoFrame ;vai para o próximo frame
O fragmento acima faz com que o 6502 volte ao início do kernel para desenhar um novo
quadro, caracterizando um loop infinito.
Programa Descrição
sta VBLANK liga VBLANK após sincronismo vertical (VSYNC)
ldy #$25
aqui eu estou gastando 37 scanlines * 76 ciclos = 2.812
.aguardaVBLANK ciclos.
sta WSYNC
dey a lógica do programa deveria estar “embutida” nesse tempo
bne .aguardaVBLANK
lda #$00
sta VBLANK
inc wContador
bne .naoMudaTipo
lda #$01
eor wFlag
sta wFlag
.naoMudaTipo
lda wFlag
bne .naoMudaCorDaTela
inx Aqui está a lógica do programa. Ela vem depois dos 37
jmp .naoZeraX scanlines do VBLANK. Isso quer dizer que quando o 6502
executa esse trecho, o TIA já está desenhando na tela.
.naoMudaCorDaTela
ldx #$00
.naoZeraX
ldy #$C0
inc wScroll
bne .scanLoop
dec wDesloca
bne .scanLoop
lda #$08
sta wDesloca
A solução é fazer com que o código da lógica do programa não “invada” o tempo de
desenho da tela. Vamos pensar um pouco: no VBLANK tenho 2.812 ciclos disponíveis. Meu
código gasta N ciclos, então 2.812 – N = tempo restante até entrar no momento de desenhar
na tela. Por exemplo, se meu código gastasse 76 ciclos, então em vez de fazer
que resulta em 38 scalines (37 scanlines do loop + 1 scanline do meu código, considerando
que meu código gasta 76 ciclos), eu faria
ou ainda
meu código +
ldy #$24 ;24h = 36d
;
.aguardaVBLANK ;
sta WSYNC ;aguarda 1 scanline
dey ;Y = Y - 1
bne .aguardaVBLANK ;vai para .aguardaVBLANK se Y > 0
Para cada 76 ciclos de código, devo diminuir 1 scanline do VBLANK. Mas ficar contando
ciclos de máquina é uma tarefa difícil. Temos que deixar o hardware cuidar disso para nós.
Para isso basta utilizarmos o timer.
Utilizando o timer
Conforme visto na página 88 desse tutorial, podemos setar o timer do PIA para que
possamos temporizar operações. Quando setamos o timer, ele passa a contar
regressivamente até chegar a zero. Cada ciclo de máquina corresponde a 1 ciclo do PIA, ou
1 contagem do timer. Portanto, podemos utilizar o timer para contar o tempo restante do
exemplo acima. Daí não precisamos mais nos preocupar em ficar contando quantos ciclos
nossa lógica gasta e subtrair dos scanlines restantes. Apenas devemos continuar nos
preocupando em não gastar mais do que 2.812 ciclos de máquina nessa parte. Para utilizar
o timer, basta escrevermos:
Simples, setamos o timer com 2Bh = 43d. Por que? Vamos fazer as contas:
Segundo consta na página 88 desse tutorial, o TIM64T significa que o timer é um divisor por
64, ou seja, a cada 64 ciclos o timer conta 1. Ora, então se eu preciso que ele conte 2.812
ciclos basta fazer:
Nota: Não importa o valor da casa decimal: sempre devemos arredondar para baixo.
Com o timer setado e começando a contar, vem o nosso código. Se o nosso código gastou
N ciclos, o timer contém o tempo (2Bh * 64) – N. Daí, quando o 6502 chega no loop
.aguardaVBLANK ele lê o que está no timer. Se for maior que zero, ele fica no loop até o
timer atingir zero. Veja que não há necessidade de decrementar nada dentro do loop. Basta
ler o que está no timer (LDA INTIM). Cada vez que se lê esse endereço, ele traz um valor
decrescente. Com isso podemos escrever e editar nosso código, acrescentando ou
excluindo instruções sem ter que nos preocupar com a contagem de ciclos que estamos
gastando. O timer se encarrega de equilibrar as coisas.
Vamos agora, reescrever nosso código, utilizando o timer e vejamos o que acontece.
...
...
.inicioDoFrame ;início do frame
lda #$02 ;liga o VSYNC
sta VSYNC ;
sta WSYNC ;
sta WSYNC ;aguarda 3 scanlines
sta WSYNC ;
ldy #$00 ;desliga VSYNC
sty VSYNC ;
sta VBLANK ;liga VBLANK
lda #$2B ;2Bh = 43d
sta TIM64T ;seta o timer
ldy #$C0 ;C0h = 192d
inc wContador ;wContador = wContador + 1
bne .naoMudaTipo ;se wContador <> 0 vai para .naoMudaTipo
lda #$01 ;A = 1
eor wFlag ;inverte wFlag (se 0 = 1, se 1 = 0)
sta wFlag ;salva wFlag
;
.naoMudaTipo ;
lda wFlag ;A = 0?
bne .naoMudaCorDaTela ;não. não muda a cor da tela
inx ;X = X + 1
jmp .naoZeraX ;
;
.naoMudaCorDaTela ;
ldx #$00 ;X = 0 para iniciar mudança da cor das linhas
;
.naoZeraX ;
inc wScroll ;wScroll = wScroll + 1
bne .aguardaVBLANK ;aproveita wScroll para causar delay na coluna
dec wDesloca ;wDesloca = wDesloca - 1
bne .aguardaVBLANK ;wDesloca = 0? não, desloque a coluna
lda #$08 ;A = 8
sta wDesloca ;posiciona a coluna na direita da tela
;
.aguardaVBLANK ;
lda INTIM ;timer é zero?
bne .aguardaVBLANK ;não. aguarda fim do VBLANK
sta VBLANK ;desliga o VBLANK
;
.scanLoop ;
...
...
Figura 75 Figura 76
Mas ainda assim, há uma faixa escura antes da imagem. Está mais estreita, pois agora
estamos contando certo, mas ainda está lá. Isso acontece porque, o código que sincroniza
com a TV (liga e desliga o VSYNC) gasta ciclos de máquina (ele tem 3 WSYNC, que são
justamente as 3 linhas escuras antes da imagem) e ele está antes de setarmos o timer.
Temos aí 2 opções: diminuir o valor inicial do timer ou colocá-lo antes do VSYNC.
ou
Mas antes de fazermos essa alteração, observe que a primeira linha da coluna aparece
antes do restante dessa coluna (figuras 77 e 78) e acompanha o deslocamento dela.
Figura 77
Figura 78
Os destaques mostram uma barra mais à esquerda da coluna. À medida que a coluna se
desloca para a esquerda, a barra a acompanha. Mas está sempre antes (mais à esquerda)
da coluna. Na verdade, essa barra é a primeira linha da coluna que está defasada em
relação ao resto da coluna. A coluna está sendo desenhada primeiramente em um
determinado ciclo (a primeira linha da coluna é desenhada em um determinado tempo). O
restante da coluna é desenhado em um tempo diferente do primeiro. Esse tempo é em
relação ao último WSYNC, que determina a distância em relação ao lado esquerdo da tela.
O que ocorre é o seguinte: depois que o 6502 executa todo esse código, ele entra no loop
.scanLoop. Daí ele vai executando as instruções, inclusive o loop .horiz que causa um delay,
fazendo a coluna aparecer mais à direita e se deslocar para a esquerda a cada vez que X é
decrementado. Mas na primeira vez que ele entra no .scanLoop, ele está vindo desde o
último WSYNC (que no caso começou quando fez o VSYNC). Do loop .aguardaVBLANK até
o .scanLoop, ele gastou uma certa quantidade de ciclos. Daí ele entra no .scanLoop e vai
executando as instruções, gastando assim mais ciclos. A soma dos ciclos gastos no código
fora do .scanLoop com os ciclos gastos no que está dentro do .scanLoop (na primeira vez
que ele entra no .scanLoop, acumula-se os ciclos) faz a primeira linha aparecer em uma
determinada coluna da tela.
Pela lógia, essa linha deveria aparecer depois do restante da coluna. Mas ela aparece antes
porque, como ele veio acumulando ciclos, quando chega a 76 ciclos, ele passa a contar do
zero, referenciando-se pelo lado esquerdo da tela. Por exemplo, se para executar todas as
instruções ele gastou 100 ciclos, então ele vai desenhar na tela no ciclo 100 – 76 = 27,
comprovando que nosso código está gastando mais de 76 ciclos por scanline na primeira
vez que entra no .scanLoop (vem acumulado do .inicioDoFrame).
Dentro do .scanLoop ele encontra um STA WSYNC. Nesse momento, ele pára a execução e
aguarda o feixe de elétrons voltar para o lado esquerdo da tela. Quando isso ocorre, a
contagem de ciclos é zerada e ele executa novamente as instruções do .scanLoop até
passar novamente pelo STA WSYNC. Como podemos concluir, ele executou somente as
instruções dentro do .scanLoop dessa vez. Somente na primeira vez (antes de entrar no
.scanLoop) ele executou aquelas outras instruções, levando assim, mais tempo para
desenhar a primeira linha (mas ela apareceu antes, dando a impressão de que gastou-se
menos tempo, porque ultrapassou ois 76 ciclos aparecendo então no tempo restante). Já a
partir da segunda linha, ele está executando somente as instruções dentro do .scanLoop,
gastando assim menos tempo em relação ao WSYNC. Essa diferença de tempo faz com que
as linhas sejam desenhadas (a partir da segunda) na mesma posição, pois gastam o mesmo
tempo.
Para resolver isso, basta zerarmos a contagem de ciclos tão logo entremos no .scanLoop.
Assim, teremos a mesma contagem de tempo para todas as linhas.
...
.scanLoop ;
sta WSYNC ;aguarda 1 scanline
tya ;A = Y
adc wScroll ;A = A + wScroll
pha ;salva A
ldx wDesloca ;X = wDesloca
;
.horiz ;
dex ;X = X - 1
bne .horiz ;X = 0? não, continua decrementando X
pla ;restaura A
tax ;X = A
stx COLUBK ;muda a cor de fundo
nop ;a tela é desenhada com essa nova cor,
nop ;nesse tempo, apresentando uma coluna
nop ;os 3 NOPs determinam a largura da coluna
sty COLUBK ;muda cor de fundo
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
...
No programa que estamos escrevendo, o STA WSYNC estava depois das instruções que
desenham na tela. Se houvesse uma mudança em qualquer tempo dentro dos 76 ciclos, ela
seria visível. Mudando o STA WSYNC para o início do loop dizemos que, o que quer que
estivesse sendo desenhado antes de entrar no loop continue sendo até o fim do scanline.
Agora não há mais defasagem de tempo entre as linhas desenhadas (figura 79).
Figura 79
Figura 80
Como podemos observar, a tela começa a ser desenhada a partir da primeira linha da
coluna e não a partir da primeira linha do plano de fundo. Do lado esquerdo, entre as 2
barras vermelhas que destacamos, temos uma linha escura, indicando que nada está sendo
desenhado até aquele momento. Daí temos a primeira linha da coluna. Depois, do lado
direito da coluna, temos o início do desenho do plano de fundo (uma linha verde escuro).
Isso está errado. A linha verde escuro deve começar no lado esquerdo da tela e não depois
da primeira linha da coluna. Só está acontecendo porque dentro do .scanLoop, a instrução
para desenhar a coluna vem antes da instrução para desenhar o plano de fundo.
...
.scanLoop ;
sta WSYNC ;aguarda 1 scanline
sty COLUBK ;muda cor de fundo
tya ;A = Y
adc wScroll ;A = A + wScroll
pha ;salva A
ldx wDesloca ;X = wDesloca
;
.horiz ;
dex ;X = X - 1
bne .horiz ;X = 0? não, continua decrementando X
pla ;restaura A
tax ;X = A
stx COLUBK ;muda a cor de fundo
nop ;a tela é desenhada com essa nova cor,
nop ;nesse tempo, apresentando uma coluna
nop ;os 3 NOPs determinam a largura da coluna
sty COLUBK ;muda cor de fundo
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
...
Ou seja, devemos desenhar o plano de fundo antes de desenhar a coluna. Só que isso
acarreta em outro problema. Veja a figura 81.
Figura 81
Quando a coluna está mais para a direita, o valor de X é alto e o loop .horiz consome muito
tempo para causar o delay na coluna. Isso somado ao tempo total do .scanLoop ultrapassa
os 76 ciclos por scanline. A imagem se deformou. Devemos então conseguir ciclos. Para
isso basta tirar 2 dos 3 NOPs. A coluna ficará mais estreita, mas a imagem volta a se
estabilizar.
...
.aguardaVBLANK ;
lda INTIM ;timer é zero?
bne .aguardaVBLANK ;não. aguarda fim do VBLANK
sta VBLANK ;desliga o VBLANK
;
.scanLoop ;
sta WSYNC ;
sty COLUBK ;muda cor de fundo
tya ;A = Y
adc wScroll ;A = A + wScroll
pha ;salva A
ldx wDesloca ;X = wDesloca
;
.horiz ;
dex ;X = X - 1
bne .horiz ;X = 0? Não, continua decrementando X
pla ;restaura A
tax ;X = A
stx COLUBK ;aqui mudamos o valor do COLUBK
nop ;a tela é desenhada com essa nova cor nesse
;tempo
.naoMudaCorDaLinha ;
sty COLUBK ;muda cor de fundo
dey ;Y = Y - 1
bne .scanLoop ;vai para .scanLoop se Y > 0
sty COLUBK ;Y = 0 então COLUBK = cor preta
lda #$1E ;A = 1Eh = 30d
sta TIM64T ;setamos o timer
;
.overScan ;
lda INTIM ;timer é zero?
bne .overScan ;não. aguarda fim do overscan
jmp .inicioDoFrame ;vai para o próximo frame ...
Note que utlizamos o timer também para a parte do overscan. Assim podemos colocar mais
lógica de programa nesse tempo sem termos que nos preocupar com a contagem de ciclos.
Subtituimos o loop com 30 WSYNCs pelo timer, como fizemos no VBLANK.
Fizemos tudo isso, tivemos todo esse trabalho, para mostrar o que ocorre quando
escrevemos o programa de forma errada e os ciclos não correspondem ao sincronismo. Há
várias formas de corrigir os erros. Essa aqui apresentada pode não ser a melhor, mas para
fins didáticos dá uma boa noção do que acontece quando instruções certas estão no lugar e
tempo errados. Mostra também que quando se muda um STA WSYNC de lugar, muita coisa
pode acontecer com a imagem. Vale a pena testar o STA WSYNC em vários lugares e ver o
que acontece. Em alguns kernels ele vem no início do loop. Em outros ele vem no fim, e
ainda em outros, ele vem em lugar totalmente distinto... depende exclusivamente do que
(quando e quanto) se está desenhando. Não nos esqueçamos de mudar o timer para o início
do quadro. Assim eliminamos as linhas escuras no topo da imagem (figura 85, código na
página 138).
Figura 85
Fazer uma imagem aparecer na tela de forma correta é um tanto quanto trabalhoso. O
programa apresentado ainda tem um bug. Fique observando a tela (o programa sendo
executado) e note que, quando a coluna chega no meio da tela (horizontalmente falando),
toda a imagem sobe 1 (uma) linha.
Aqui mostramos como uma instrução pode consertar, estragar as coisas ou simplesmente
mascarar algo que está errado. O ideal é manter tudo dentro de seus limites. As linhas
escuras no início do quadro poderão ser inevitáveis. Depende do kernel. Diminuindo-se o
valor do timer pode-se suprimi-las. Tudo é uma questão de tentativa e erro. De início, teste
valores, mude instruções de lugar. Quando pegar a prática (e a lógica da coisa) vai ver que é
bem fácil ajustar a apresentação de uma imagem. Nesse tutorial vamos apresentar tudo de
forma, digamos, principiante. Não se assustem (os programadores experts, os lords da
programação para 2600). Esse tutorial é para os iniciantes. Estamos tentando mostrar como
começar. O refinamento caberá a cada um. Nas próximas páginas veremos como desenhar
jogadores, playfield e usar o joystick. Veremos ainda como posicionar um objeto na tela, o
míssil, a bola e testar quando um objeto colide com o outro.
A seguir, uma sugestão de estrutura de um programa. Ela serve para a grande maioria dos
kernels. Nada nos impede de testar variações (que existem aos montes).
O básico de um programa para o 2600 é o apresentado. Mas não é regra. Por exemplo:
• O valor de Y para o scanLoop não é necessariamente 192 linhas. Se você pretende
fazer um jogo que usa somente metade da tela (verticalmente falando), então Y terá o
valor 96.
• Depois do .aguardaVBLANK pode-se ter algum código, mas não pode ser muita
coisa, pois ali já estamos na área de desenho na tela. Se escrevermos muita coisa,
vai aparecer na tela uma ou mais linhas que não fazem parte da imagem, de tamanho
que depende do tempo que leva para executar o que foi escrito até o próximo
WSYNC. Pode-se evitar isso colocando um WSYNC depois desse código, mas daí
desperdiçamos 1 linha da tela. Normalmente, nesse lugar colocamos o HMOVE e
RESPx (que veremos a seguir) para movimentar os objetos horizontalmente na tela.
• O STA WSYNC dentro do .scanLoop não tem que estar no início, depende do que
(quando e quanto) vai ser desenhado.
• O DEY dentro do .scanLoop não tem que estar no fim. Entretanto o teste se toda a
tela foi desenhada deve ser respeitado.
• Não tem que ser necessariamente o registrador Y para o .scanLoop. Pode ser o X ou
ainda uma variável de memória. No caso do X usaríamos DEX no lugar do DEY e no
caso de uma variável usaríamos DEC variável no lugar do DEY.
É interessante também sempre utilizar constantes. Assim para valores temos nomes mais
sugestivos. Por exemplo:
a)
lda #$07
sta COLUBK
b)
COR_CINZA = $07
lda #COR_CINZA
sta COLUBK
Desenhando gráficos
Nesta sessão veremos como desenhar jogadores e playfield. Veremos como posicionar o
jogador na tela, detecção de colisão e utilização do joystick que no nosso caso será o
teclado, a menos que você possua um joystick conectado em seu computador.
Desenhando um jogador
Conforme descrito na página 80 desse tutorial, para desenhar um jogador basta acessarmos
GRP0 para o jogador 0 e GRP1 para o jogador 1. O gráfico nada mais é do que uma matriz
de pontos que, dependendo de quais pontos estão ativos, a imagem se forma (vide exemplo
na página 68 desse tutorial).
Dividimos ele em uma matriz para termos uma noção de cada pixel do jogador:
Temos assim os valores a serem carregados em GRP0 para desenhar o jogador 0. Para
fazer isso, basta criarmos um loop que, a cada passo, lê uma linha dessa matriz de bits e
coloca o byte (cada linha é 1 byte) no GRP0.
Nota: Para desenhar gráficos de forma fácil e rápida utilizaremos o programa AtariPaint
(figura 86), feito em Visual Basic 6 que, diga-se de passagem, eu mesmo escrevi. Pela falta
de criatividade para desenhar, não gostar muito do TiaPaint (programa para DOS que
desenha playfield) e o CAG (programa descontinuado em Java para desenhar objetos para o
Atari) não estar 100% completo, resolvi escrever esse programa para desenhar jogadores,
playfield e sprites diversos. Talvez futuramente eu escreva um editor de programa. Daí será
uma IDE completa.
Figura 86
A figura 87 mostra um objeto desenhado. Note que nesse caso, as células que representam
os olhos e a boca (com um sorriso que parece um quadro de Da Vinci) não estão na cor
preta e sim tiveram sua cor apagada. Daí, essas células serão o bit zero na matriz de bits
conforme a figura 88.
Figura 87
daquela célula será repintada com a nova cor, e não apenas a célula clicada.
Entretanto, se há células apagadas, essas permanecerão apagadas. Somente as
células que possuem a mesma cor da célula clicada serão repintadas.
• Pintar uma célula/linha/região de preto é diferente de apagar. Quando se apaga, a
célula/linha/região apagada representará o zero na matriz de bits e o 2600 mostrará a
cor de fundo (COLUBK) no lugar desse pixel. Quando pinta-se com a cor preta, a
célula/linha/região colorida representará o bit 1 na matriz de bits e o 2600 mostrará a
cor preta independente da cor de fundo.
• O botão Gera montará a matriz de bits para o objeto desenhado (figura 88).
• A caixa de texto abaixo do botão Gera exibe o label padrão para o objeto. Pode-se
alterá-la.
• Nos modos Simétrico e Refletido, basta desenhar a metade esquerda do playfield.
Figura 88
A figura 88 mostra nossa matriz de bits. Pode-se salvar o arquivo e depois incluí-lo no
programa principal ou então copiar o texto e colar no programa principal. A linha colorTable
contém bytes que representam a cor de cada linha do objeto. Como utilizamos apenas o
amarelo, todos os bytes são 0Fh. Assim, no nosso programa não precisamos utilizar essa
tabela de cores. Podemos simplesmente declarar que COLUP0 é 0Fh e pronto. Se nosso
objeto tivesse mais de 1 cor, ou seja, se por exemplo ele fosse um arco-iris, daí sim a linha
colorTable teria valores diversos e faria sentido utilizá-la no programa principal (para cada
linha lida do objeto, lê-se 1 byte da tabela de cores). De qualquer forma, o programa gera o
colorTable. Utilizá-lo ou não depende da necessidade de cada um.
Vamos agora escrever um programa que exibe o jogador 0 e depois analisar alguns pontos.
Utilizando o modelo apresentado nas páginas 144, 145 e 146 desse tutorial, altere as
sessões conforme abaixo:
;=================================================================================
;Constantes
;=================================================================================
COR_AZUL = #$80 ;cor azul
COR_AMARELA = #$1E ;cor amarela
;
;=================================================================================
;=================================================================================
;Variáveis
;=================================================================================
Seg.U Variaveis ;
org $80 ;
;
YPosFromBot ds 1 ;distância em relação a parte de baixo da tela
;
;---------------------------------- ;
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda #COR_AZUL ;cor azul
sta COLUBK ;plano de fundo
lda #COR_AMARELA ;cor amarela
sta COLUP0 ;player0
lda #$80 ;posição em relação a parte inferior da tela
sta YPosFromBot ;salva em YPostFromBot
;
;---------------------------------- ;
;---------------------------------- ;
;Scanlines ;
;---------------------------------- ;
.scanLoop ;
sta WSYNC ;aguarda 1 scanline
tya ;A = Y
sec ;carry = 1
sbc YPosFromBot ;posição em relação à parte inferior da tela
adc #PLAYER0HEIGHT ;adiciona a altura do player
bcc .naoDesenha ;hora de desenhar? não. vá para .naoDesenha
tax ;X = A
lda Player0,x ;carrega Player0[x]
.byte $2C ;pula próxima instrução
;
.naoDesenha ;
lda #$00 ;pára de desenhar
sta GRP0 ;coloca valor em GRP0
dey ;desenhou toda a tela?
bne .scanLoop ;não. desenha próxima linha
;
;<<< zerar registradores aqui
;
lda #$1E ;A = 30 linhas de overscan
sta TIM64T ;seta timer
;---------------------------------- ;
;---------------------------------- ;
;Gráficos ;
;---------------------------------- ;
Player0 ;
.byte %00011000 ; | XX |
.byte %00111100 ; | XXXX |
.byte %01111110 ; | XXXXXX |
.byte %11000111 ; |XX XXX|
.byte %10111111 ; |X XXXXXX|
.byte %11111111 ; |XXXXXXXX|
.byte %11011011 ; |XX XX XX|
.byte %01111110 ; | XXXXXX |
.byte %00111100 ; | XXXX |
.byte %00011000 ; | XX |
;
PLAYER0HEIGHT = *-Player0 ;altura do player (número de linhas)
;---------------------------------- ;
Compile e rode o programa. A tela apresentada deve ser como a da figura 89.
Por que ele é invertido verticalmente (está de cabeça para baixo)? Porque aproveitamos o
loop do .scanLoop para desenhar o gráfico. Como tipicamente o registrador usado para o
loop tem seu valor decrementado, então começamos a ler o gráfico de seu offset final para o
offset inicial. Entretanto, nada nos impede de criarmos um loop crescente e assim começar a
ler o gráfico a partir do offset inicial e desenhá-lo então de cabeça para cima, porém, um
loop crescente gastaria mais tempo de máquina e mais bytes da ROM (alguma coisa em
torno de 2 ou 3 ciclos e bytes) pois temos que realizar a comparação via instrução CMP (ou
CPX ou CPY). E como na programação do 2600 tempo e byte são fundamentais, é melhor
desenharmos de cabeça para baixo do que gastar tempo de máquina e espaço da ROM.
Mas quando Y = 7Fh e subtraímos 80h, o carry passa para 0 já na subtração e A = FFh.
Somamos 0Ah e A fica com 09h e o carry agora é 1. Então, o desvio não ocorre no BCC e a
linha TAX é executada. X agora tem o valor de A e em seguida colocamos em A o valor
endereçado por Player0[X] (que é a última linha da matriz de bits do nosso player). A linha
do programa .BYTE $2C diz ao 6502 para pular a próxima instrução do programa, assim a
linha LDA #$00 não é executada. Com isso, A permanece com o valor que estava (a última
linha da matriz de bits) e o 6502 coloca esse valor em GRP.
Esse processo continua até desenhar todo o player. Após a primeira linha da matriz de bits
ser desenhada, Y está com o valor 75h. Subtraindo-se 80h temos F5h. Adicionando-se 0Ah
do player temos FFh, não alterando o estado do carry (continua com 0). Sendo assim o
desvio BCC ocorre, colocando em A o valor zero e armazenando-o em GRP0.
Figura 89
O teste BCC determina o início (quando começar a desenhar) e o fim (quando parar de
desenhar) o player.
Figura 90
Posicionando o gráfico
Agora vamos fazer nosso jogador aparecer onde quisermos na tela. Para alterar a posição
vertical inicial é só alterarmos o valor de YPosFromBot. Para alterar a posição horizontal
inicial basta alterar o programa conforme segue:
;---------------------------------- ;
;Aguarda VBLANK ;
;---------------------------------- ;
.aguardaVBLANK ;
lda INTIM ;timer = 0?
bne .aguardaVBLANK ;não. aguarda fim do VBLANK
sta VBLANK ;liga o VBLANK
;
;---------------------------------- ;
;Lógica do jogo ;
;---------------------------------- ;
sta WSYNC ;próximo scanline
lda #$8C ;lado direito
sta HMP0 ;move para a direita -8 clocks
and #$0F ;mascara high nibble
tay ;Y = A
;
.posicionaPlayer0 ;
dey ;Y = Y-1
bne .posicionaPlayer0 ;decrementa até atingir a posição
sta RESP0 ;reseta player0 nesse clock
sta HMOVE ;posiciona player
;---------------------------------- ;
Para maiores detalhes sobre os endereços RESPx, HMPx e HMOVE vide páginas 83 e 84
desse tutorial.
As instruções de reset fixam a posição do objeto no lugar da linha horizontal da tela onde o
clock se encontra. Utilizamos um algoritmo para gastar ciclos necessários e chamar um
reset. Com isso o objeto associado será colocado nessa posição quando se escrever nos
seus registradores.
O HMOVE provoca o movimento horizontal do objeto de acordo com sua posição inicial,
segundo os valores de seu registrador de movimento horizontal (HMPx, HMMx, HMBL). Para
que o objeto se mova horizontalmente, não basta escrever nos registradores de movimento
horizontal e sim, deve-se invocar imediatamente depois de um WSYNC, para que comece a
contar exatamente no começo do scanline. Como todo strobe, não importa o valor que se
escreve no HMOVE.
Nota: Não se deve atualizar os registradores HMxxx depois que já se passaram 24 ciclos de
máquina pois resultados inesperados podem ocorrer.
Uma tabela como a mostrada a seguir é necessária para que o algoritmo de posicionamento
funcione.
horizTable
.byte $00,$F0,$E0,$D0,$C0,$B0,$A0,$90
.byte $71,$61,$51,$41,$31,$21,$11,$01,$F1,$E1,$D1,$C1,$B1,$A1,$91
.byte $72,$62,$52,$42,$32,$22,$12,$02,$F2,$E2,$D2,$C2,$B2,$A2,$92
.byte $73,$63,$53,$43,$33,$23,$13,$03,$F3,$E3,$D3,$C3,$B3,$A3,$93
.byte $74,$64,$54,$44,$34,$24,$14,$04,$F4,$E4,$D4,$C4,$B4,$A4,$94
.byte $75,$65,$55,$45,$35,$25,$15,$05,$F5,$E5,$D5,$C5,$B5,$A5,$95
.byte $76,$66,$56,$46,$36,$26,$16,$06,$F6,$E6,$D6,$C6,$B6,$A6,$96
.byte $77,$67,$57,$47,$37,$27,$17,$07,$F7,$E7,$D7,$C7,$B7,$A7,$97
.byte $78,$68,$58,$48,$38,$28,$18,$08,$F8,$E8,$D8,$C8,$B8,$A8,$98
.byte $79,$69,$59,$49,$39,$29,$19,$09,$F9,$E9,$D9,$C9,$B9,$A9,$99
.byte $7A,$6A,$5A,$4A,$3A,$2A,$1A,$0A,$FA,$EA,$DA,$CA,$BA,$AA,$9A
O código acima pega o valor que está na tabela na posição horizTable[33] que é D2h.
Colocamos a posição fina em HMP0. O TIA ignora os 4 bits do low nibble do byte (vide
página 84 desse tutorial que o HMP0 só usa o high nibble – 4 bits altos – do byte) e mantém
os 4 bits altos em HMP0, ou seja, o high nibble fica com o valor Dh que, em um formato de 4
bits é -3. Entretanto, não se faz nada com esse valor.
Agora mascaramos o high nibble do valor que está em A (D2h). Com o mascaramento do
high nibble, A passa a ser 2h (vide página 51 desse tutorial), ou seja, o acumulador agora
contém a posição bruta (ou de baixa precisão) que é o número de vezes que queremos que
haja o ciclo de delay. No nosso caso, 2 vezes.
.posicionaPlayer0 ;
dex ;+2 [16] X = X-1
bpl .posicionaPlayer0 ;+2 [18 + x*5]
Isso gasta 3 ciclos assim que completamos 31 ciclos até esse ponto. 31 ciclos x 3
pixels/ciclo = 93 pixels desde o último WSYNC. Já que o tempo do branco horizontal é de 68
pixels, o player deveria começar a ser desenhado no pixel 93 – 68 = 25. Porém, por alguma
razão, o TIA mostra o player 5 pixels depois do que deveria ser, ou seja, começa a mostrar o
player no pixel 30. Esses 5 pixels já estão contemplados na tabela.
Essa última instrução ativa o movimento fino que havíamos colocado no HMP0. Já que
HMP0 contém -3, o TIA move o player 3 pixels para a direita, até o pixel 33. Pronto. Simples.
Isso funciona para players. Porém o delay de pixels é só de 4 ciclos para mísseis e bola,
assim, para esses objetos em vez de fazer LDA horizTable,X fazemos LDA horizTable+1,X.
;=================================================================================
;Constantes
;=================================================================================
COR_AZUL = #$80 ;cor azul
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posiciona no pixel 33d
;
;=================================================================================
;---------------------------------- ;
;Aguarda VBLANK ;
;---------------------------------- ;
.aguardaVBLANK ;
lda INTIM ;timer = 0?
bne .aguardaVBLANK ;não. aguarda fim do VBLANK
sta VBLANK ;liga o VBLANK
;
;---------------------------------- ;
;Lógica do jogo ;
;---------------------------------- ;
sta WSYNC ;próximo scanline
ldx #PLAYER0HPOS ;posição horizontal do player0
lda horizTable,x ;lê valor na tabela
sta HMP0 ;posiciona player0
and #$0F ;mascara high nibble
tax ;X = A
;
.posicionaPlayer0 ;
dex ;X = X-1
bpl .posicionaPlayer0 ;decrementa até atingir a posição
sta RESP0 ;reseta player0 nesse clock
sta WSYNC ;próximo scanline
sta HMOVE ;posiciona player0
;---------------------------------- ;
;---------------------------------- ;
;Tabelas ;
;---------------------------------- ;
horizTable ;
.byte $00,$F0,$E0,$D0,$C0,$B0,$A0,$90
.byte $71,$61,$51,$41,$31,$21,$11,$01,$F1,$E1,$D1,$C1,$B1,$A1,$91
.byte $72,$62,$52,$42,$32,$22,$12,$02,$F2,$E2,$D2,$C2,$B2,$A2,$92
.byte $73,$63,$53,$43,$33,$23,$13,$03,$F3,$E3,$D3,$C3,$B3,$A3,$93
.byte $74,$64,$54,$44,$34,$24,$14,$04,$F4,$E4,$D4,$C4,$B4,$A4,$94
.byte $75,$65,$55,$45,$35,$25,$15,$05,$F5,$E5,$D5,$C5,$B5,$A5,$95
.byte $76,$66,$56,$46,$36,$26,$16,$06,$F6,$E6,$D6,$C6,$B6,$A6,$96
.byte $77,$67,$57,$47,$37,$27,$17,$07,$F7,$E7,$D7,$C7,$B7,$A7,$97
.byte $78,$68,$58,$48,$38,$28,$18,$08,$F8,$E8,$D8,$C8,$B8,$A8,$98
.byte $79,$69,$59,$49,$39,$29,$19,$09,$F9,$E9,$D9,$C9,$B9,$A9,$99
.byte $7A,$6A,$5A,$4A,$3A,$2A,$1A,$0A,$FA,$EA,$DA,$CA,$BA,$AA,$9A
;
;---------------------------------- ;
Nota: o valor de PLAYER0HPOS deve estar entre 0 e 159 pois, recordando, a resolução
horizontal da tela é de 160 pixels.
Anteriormente dissemos que a tabela não deve ultrapassar um limite de página. Para
verificar se a tabela está ultrapassando um limite de página, basta criarmos uma macro que
fará esse teste em tempo de compilação. Caso o limite de página seja ultrapassado, o
DASM emitirá um erro e abortará a compilação.
;=================================================================================
;Macros
;=================================================================================
MAC CHECKPAGE
IF >. != >{1}
ECHO ""
ECHO "ERRO: Páginas diferentes! (", {1}, ",", ., ")"
ECHO ""
ERR
ENDIF
ENDM
;
;=================================================================================
;---------------------------------- ;
;Tabelas ;
;---------------------------------- ;
horizTable ;
.byte $00,$F0,$E0,$D0,$C0,$B0,$A0,$90
.byte $71,$61,$51,$41,$31,$21,$11,$01,$F1,$E1,$D1,$C1,$B1,$A1,$91
.byte $72,$62,$52,$42,$32,$22,$12,$02,$F2,$E2,$D2,$C2,$B2,$A2,$92
.byte $73,$63,$53,$43,$33,$23,$13,$03,$F3,$E3,$D3,$C3,$B3,$A3,$93
.byte $74,$64,$54,$44,$34,$24,$14,$04,$F4,$E4,$D4,$C4,$B4,$A4,$94
.byte $75,$65,$55,$45,$35,$25,$15,$05,$F5,$E5,$D5,$C5,$B5,$A5,$95
.byte $76,$66,$56,$46,$36,$26,$16,$06,$F6,$E6,$D6,$C6,$B6,$A6,$96
.byte $77,$67,$57,$47,$37,$27,$17,$07,$F7,$E7,$D7,$C7,$B7,$A7,$97
.byte $78,$68,$58,$48,$38,$28,$18,$08,$F8,$E8,$D8,$C8,$B8,$A8,$98
.byte $79,$69,$59,$49,$39,$29,$19,$09,$F9,$E9,$D9,$C9,$B9,$A9,$99
.byte $7A,$6A,$5A,$4A,$3A,$2A,$1A,$0A,$FA,$EA,$DA,$CA,$BA,$AA,$9A
;
CHECKPAGE horizTable ;testa se ultrapassou limite de página
;
;---------------------------------- ;
Figura 91
Para solucionar esse problema, devemos mover nossa tabela para um local onde ela fique
toda dentro de uma única página de memória. Temos 2 formas de fazer isso:
1 – mover a tabela dentro do código do programa de um lugar para outro até encontrar um
local adequado, por exemplo, mover a tabela para o início do código (início da ROM)
2 – fazer com que o compilador alinhe a tabela para a próxima página
;---------------------------------- ;
;Tabelas ;
;---------------------------------- ;
align 256 ;alinha para a próxima página
;
horizTable ;
.byte $00,$F0,$E0,$D0,$C0,$B0,$A0,$90
.byte $71,$61,$51,$41,$31,$21,$11,$01,$F1,$E1,$D1,$C1,$B1,$A1,$91
.byte $72,$62,$52,$42,$32,$22,$12,$02,$F2,$E2,$D2,$C2,$B2,$A2,$92
.byte $73,$63,$53,$43,$33,$23,$13,$03,$F3,$E3,$D3,$C3,$B3,$A3,$93
.byte $74,$64,$54,$44,$34,$24,$14,$04,$F4,$E4,$D4,$C4,$B4,$A4,$94
.byte $75,$65,$55,$45,$35,$25,$15,$05,$F5,$E5,$D5,$C5,$B5,$A5,$95
.byte $76,$66,$56,$46,$36,$26,$16,$06,$F6,$E6,$D6,$C6,$B6,$A6,$96
.byte $77,$67,$57,$47,$37,$27,$17,$07,$F7,$E7,$D7,$C7,$B7,$A7,$97
.byte $78,$68,$58,$48,$38,$28,$18,$08,$F8,$E8,$D8,$C8,$B8,$A8,$98
.byte $79,$69,$59,$49,$39,$29,$19,$09,$F9,$E9,$D9,$C9,$B9,$A9,$99
.byte $7A,$6A,$5A,$4A,$3A,$2A,$1A,$0A,$FA,$EA,$DA,$CA,$BA,$AA,$9A
;
CHECKPAGE horizTable ;testa se ultrapassou limite de página
;
;---------------------------------- ;
Com isso, não importa o tamanho do código que está antes da tabela. Ela sempre será
colocada na página seguinte à página em que o resto código terminou. Isso implica em
desperdício de bytes da ROM. Por exemplo, se nosso código vai de F000h até F06Ch, a
tabela será alocada em F101h, ficando F101h-F06Ch-01h = 94h bytes sem uso.
À medida em que for sendo escrito mais código, esses bytes serão utilizados e a tabela
continuará no mesmo lugar. Entretanto, se nosso código chegar por exemplo em F105h, a
tabela será alocada (em tempo de compilação) para a próxima página.
Nesse tutorial, para fins didáticos e manutenção do modelo de código fonte apresentado,
vamos usar o alinhamento em vez de mudar a tabela de lugar dentro do código.
Figura 92
Utilizando o joystick
Agora vamos colocar interatividade na coisa. Utilizando o teclado (ou joystick se tiver
instalado), vamos mover o jogador por toda a tela. Para isso altere o programa conforme
segue:
;=================================================================================
;Constantes
;=================================================================================
COR_AZUL = #$80 ;cor azul
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posição horizontal inicial do player0
MOVER_ESQUERDA = #$10 ;high nibble = 1
MOVER_DIREITA = #$F0 ;high nibble = -1
;
;=================================================================================
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda #COR_AZUL ;cor azul
sta COLUBK ;plano de fundo
lda #COR_AMARELA ;cor amarela
sta COLUP0 ;player0
lda #$80 ;posição em relação a parte inferior da tela
sta YPosFromBot ;salva em YPostFromBot
sta WSYNC ;próximo scanline
ldx #PLAYER0HPOS ;posição horizontal do player0
lda horizTable,x ;lê valor na tabela
sta HMP0 ;posiciona player0
and #$0F ;mascara high nibble
tax ;X = A
;
.posicionaPlayer0 ;
dex ;X = X-1
bpl .posicionaPlayer0 ;decrementa até atingir a posição
sta RESP0 ;reseta player0 nesse clock
sta WSYNC ;próximo scanline
sta HMOVE ;posiciona player0
;
;---------------------------------- ;
;---------------------------------- ;
;Lógica do jogo ;
;---------------------------------- ;
ldx #$00 ;assegura que o player não se movimentará
lda SWCHA ;lê endereço do joystick
lsr ;acima?
bcs .naoAcima1 ;não. verifica abaixo
;<<<seu código aqui
;
.naoAcima1 ;
lsr ;abaixo?
bcs .naoAbaixo1 ;não. verifica esquerda
;<<<seu código aqui
;
.naoAbaixo1 ;
lsr ;esquerda?
bcs .naoEsquerda1 ;não. verifica direita
;<<<seu código aqui
;
.naoEsquerda1 ;
lsr ;direita?
bcs .naoDireita1 ;não. verifica acima
;<<<seu código aqui
;
.naoDireita1 ;
lsr ;acima?
bcs .naoAcima0 ;não. verifica abaixo
inc YPosFromBot ;YPosFromBot = YPosFromBot+1
;
.naoAcima0 ;
lsr ;abaixo?
bcs .naoAbaixo0 ;não. verifica esquerda
dec YPosFromBot ;YPosFromBot = YPosFromBot-1
;
.naoAbaixo0 ;
lsr ;esquerda?
bcs .naoEsquerda0 ;não. verifica direita
ldx #MOVER_ESQUERDA ;
;
.naoEsquerda0 ;
lsr ;direita?
bcs .naoDireita0 ;não. verifica tiro
ldx #MOVER_DIREITA ;
;
.naoDireita0 ;
lda INPT5 ;botão de tiro pressionado?
bmi .naoBotao1 ;não. verifica botão do outro joystick
;
.naoBotao1 ;
lda INPT4 ;botão de tiro pressionado?
bmi .naoBotao0 ;não. continua
;
.naoBotao0 ;
stx HMP0 ;direção e velocidade do player0
;---------------------------------- ;
;---------------------------------- ;
;Aguarda VBLANK ;
;---------------------------------- ;
.aguardaVBLANK ;
lda INTIM ;timer = 0?
bne .aguardaVBLANK ;não. aguarda fim do VBLANK
sta VBLANK ;liga o VBLANK
;
;---------------------------------- ;
;Lógica do jogo ;
;---------------------------------- ;
sta WSYNC ;próximo scanline
sta HMOVE ;posiciona player0
;
;---------------------------------- ;
Na sessão Lógica do jogo colocamos o código que lê o endereço dos joysticks e então
testamos seus bits. De acordo com cada bit, sabemos qual joystick foi acionado e sua
direção ou botão.
Caso tenha dúvida, leia novamente sobre a instrução LSR nas páginas 26 a 30 e sobre os
joysticks na página 90 desse tutorial.
Vamos dar aqui uma rápida visão do código. Colocamos em A o status de SWCHA e em
cada LSR deslocamos os bits para a direita. Se o bit mais à direita for 1, quando o LSR
executar, esse bit vai para o carry fazendo com que o desvio no BCS ocorra. Caso o bit seja
zero, o LSR faz esse zero ir para o carry e o BCS não ocorre, indicando assim que aquela
direção (ou botão) do joystick foi acionada(o).
;=================================================================================
;Constantes
;=================================================================================
COR_AZUL = #$80 ;cor azul
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posição horizontal inicial do player0
MOVER_ESQUERDA = #$10 ;high nibble = 1
MOVER_DIREITA = #$F0 ;high nibble = -1
ABAIXO1 = #%00000001 ;máscara joystick1 abaixo
ACIMA1 = #%00000010 ;máscara joystick1 acima
ESQUERDA1 = #%00000100 ;máscara joystick1 esquerda
DIREITA1 = #%00001000 ;máscara joystick1 direita
ABAIXO0 = #%00010000 ;máscara joystick0 abaixo
ACIMA0 = #%00100000 ;máscara joystick0 acima
ESQUERDA0 = #%01000000 ;máscara joystick0 esquerda
DIREITA0 = #%10000000 ;máscara joystick0 direita
;
;=================================================================================
;---------------------------------- ;
;Lógica do jogo ;
;---------------------------------- ;
ldx #$00 ;assegura que o player não se movimentará
lda #ACIMA1 ;lê máscara para testar joystick
bit SWCHA ;acima?
bne .naoAcima1 ;não. verifica abaixo
;<<<seu código aqui
;
.naoAcima1 ;
lda #ABAIXO1 ;lê máscara para testar joystick
bit SWCHA ;abaixo?
bne .naoAbaixo1 ;não. verifica esquerda
;<<<seu código aqui
;
.naoAbaixo1 ;
lda #ESQUERDA1 ;lê máscara para testar joystick
bit SWCHA ;esquerda?
bne .naoEsquerda1 ;não. verifica direita
;<<<seu código aqui
;
.naoEsquerda1 ;
lda #DIREITA1 ;lê máscara para testar joystick
bit SWCHA ;direita?
bne .naoDireita1 ;não. verifica acima
;<<<seu código aqui
;
.naoDireita1 ;
lda #ACIMA0 ;lê máscara para testar joystick
bit SWCHA ;acima?
bne .naoAcima0 ;não. verifica abaixo
dec YPosFromBot ;YPosFromBot = YPosFromBot-1
;
.naoAcima0 ;
lda #ABAIXO0 ;lê máscara para testar joystick
bit SWCHA ;abaixo?
bne .naoAbaixo0 ;não. verifica esquerda
inc YPosFromBot ;YPosFromBot = YPosFromBot+1
;
.naoAbaixo0 ;
lda #ESQUERDA0 ;lê máscara para testar joystick
bit SWCHA ;esquerda?
bne .naoEsquerda0 ;não. verifica direita
ldx #MOVER_ESQUERDA ;
;
.naoEsquerda0 ;
lda #DIREITA0 ;lê máscara para testar joystick
bit SWCHA ;direita?
bne .naoDireita0 ;não. verifica tiro
ldx #MOVER_DIREITA ;
;
.naoDireita0 ;
lda INPT5 ;botão de tiro pressionado?
bmi .naoBotao1 ;não. verifica botão do outro joystick
;
.naoBotao1 ;
lda INPT4 ;botão de tiro pressionado?
bmi .naoBotao0 ;não. continua
;
.naoBotao0 ;
stx HMP0 ;direção e velocidade do player0
;---------------------------------- ;
Qual a diferença entre as 2 formas apresentadas? Simples. Na primeira forma, cada LSR
gasta 1 byte de ROM e 2 ciclos de máquina. Na segunda forma, cada LDA gasta 2 bytes de
ROM e também 2 ciclos de máquina. Além disso, cada BIT gasta 3 bytes de ROM e 4 ciclos
de máquina. Em resumo, a primeira forma gasta 1 byte e 2 ciclos em cada LSR, a segunda
forma gasta 5 bytes e 6 ciclos no par LDA e BIT.
Podemos concluir então que a primeira forma é mais econômica (nesse caso).
No nosso exemplo, a primeira forma utiliza o registrador A somente para testar a posição do
joystick. Se precisássemos desse registrador para outra coisa, dentro do teste, teríamos que
salvá-lo (PHA) e depois restaurá-lo (PLA). Daí, dependendo do número de PHAs e PLAs,
essa primeira forma já não seria tão vantajosa.
Já a segunda forma carrega em A as máscaras e lê o SWCHA toda vez que precisa, não
dependendo do que estava antes. Daí não precisamos salvar o que está em A. Tudo
depende da aplicação. Nos nossos exemplos não vamos nos preocupar com otimizações,
pois nosso tutorial tem propósitos didáticos. Por isso vamos gastar ciclos e bytes sem nos
preocuparmos com desperdício. Isso dará uma visão mais ampla e melhor entendimento de
como a coisa funciona. Mais uma vez, as otimizações ficarão por conta de cada um, com o
tempo.
Já para o movimento horizontal a coisa acontece quando escrevemos valores que indicam a
direção e velocidade (veja tabela na página 84 desse tutorial) no HMPx (no nosso exemplo
HMP0 para o player0). Assim que escrevemos em HMOVE, o player se move de acordo
com o que está em HMP0.
Agora vamos fazer com que o player “se vire” para a direção que está se movendo.
.naoAbaixo0 ;
lda #ESQUERDA0 ;lê máscara para testar joystick
bit SWCHA ;esquerda?
bne .naoEsquerda0 ;não. verifica direita
ldx #MOVER_ESQUERDA ;
ldy #$08 ;D3 do REFP0 = 1
sty REFP0 ;espelha o player0
;
.naoEsquerda0 ;
lda #DIREITA0 ;lê máscara para testar joystick
bit SWCHA ;direita?
bne .naoDireita0 ;não. verifica tiro
ldx #MOVER_DIREITA ;
ldy #$00 ;D3 do REFP0 = 0
sty REFP0 ;cancela espelhamento do player0
;
.naoDireita0 ;
Compile e rode o programa. Quando mover o player para a esquerda a imagem deve ser
como a da figura 93. Mova-o para a direita e esquerda alternadamente e veja a mudança
aparente. Isso só é possível de ser notado em gráficos assimétricos, por razões óbvias.
Figura 93
Desenhando um playfield
Vamos desenhar um playfield simples para o nosso jogador. O playfield será como o da
figura 94. Faça as seguintes alterações no programa:
;=================================================================================
;Constantes
;=================================================================================
COR_AZUL = #$80 ;cor azul
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posição horizontal inicial do player0
MOVER_ESQUERDA = #$10 ;high nibble = 1
MOVER_DIREITA = #$F0 ;high nibble = -1
PLAYFIELD_REFLETIDO = #$01 ;playfield refletido
;
;=================================================================================
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda #COR_AZUL ;cor azul
sta COLUBK ;plano de fundo
lda #PLAYFIELD_REFLETIDO ;
sta CTRLPF ;playfield refletido
lda #$80 ;posição em relação a parte inferior da tela
sta YPosFromBot ;salva em YPostFromBot
sta WSYNC ;próximo scanline
ldx #PLAYER0HPOS ;posição horizontal do player0
lda horizTable,x ;lê valor na tabela
sta HMP0 ;posiciona player0
and #$0F ;mascara high nibble
tax ;X = A
;
.posicionaPlayer0 ;
dex ;X = X-1
bpl .posicionaPlayer0 ;decrementa até atingir a posição
sta RESP0 ;reseta player0 nesse clock
sta WSYNC ;próximo scanline
sta HMOVE ;posiciona player0
;
;---------------------------------- ;
.naoBotao0 ;
stx HMP0 ;direção e velocidade do player0
lda #COR_ROSA ;
sta COLUPF ;playfield cor de rosa
lda #COR_AMARELA ;cor amarela
sta COLUP0 ;player0
;---------------------------------- ;
;---------------------------------- ;
;Scanlines ;
;---------------------------------- ;
.scanLoop ;
lda PlayField0-1,y ;lê o playfield0[y]
sta PF0 ;
lda PlayField1-1,y ;lê o playfield1[y]
sta PF1 ;
lda PlayField2-1,y ;lê o playfield2[y]
sta PF2 ;
tya ;A = Y
sec ;carry = 1
sbc YPosFromBot ;posição em relação à parte inferior da tela
adc #PLAYER0HEIGHT ;adiciona a altura do player
bcc .naoDesenha ;hora de desenhar? não. vá para .naoDesenha
tax ;X = A
lda Player0,x ;carrega Player0[x]
.byte $2C ;pula próxima instrução
;
.naoDesenha ;
lda #$00 ;pára de desenhar
sta GRP0 ;coloca valor em GRP0
sta WSYNC ;aguarda 1 scanline
dey ;desenhou toda a tela?
bne .scanLoop ;não. desenha próxima linha
sty COLUPF ;limpa COLUPF
sty COLUP0 ;limpa COLUP0
lda #$1E ;A = 30 linhas de overscan
sta TIM64T ;seta timer
;---------------------------------- ;
Com essas alterações, após compilar e rodar o programa, a tela deve ser como a da figura
95. Mova o player por toda a tela. Note que ele passa “por cima” (na frente) do playfield.
É claro que não devemos nos esquecer de incluir o mapa de bits do playfield na sessão
Gráficos do nosso programa. Se estiver usando o AtariPaint, basta clicar em Gera para que
o programa gere o mapa de bits do playfield (assim como foi feito para o player), vide página
150 desse tutorial o exemplo com o player.
Não vamos incluir a matriz do playfield aqui, pois no nosso exemplo estamos utilizando 192
scanlines * 3 (PF0, PF1 e PF2). Daí, quem se aventurar em imprimir esse tutorial vai gastar
muitas páginas só com o playfield (não estamos usando nenhum algoritmo de otimização em
nossos exemplos, lembre-se que esse tutorial tenta ser o mais didático possível, então...).
Vamos fazer com que ele se mova “por baixo” (por trás) do playfield. Para isso faça a
seguinte alteração:
;=================================================================================
;Constantes
;=================================================================================
COR_AZUL = #$80 ;cor azul
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posição horizontal inicial do player0
MOVER_ESQUERDA = #$10 ;high nibble = 1
MOVER_DIREITA = #$F0 ;high nibble = -1
PLAYFIELD_REFLETIDO = #$01 ;playfield refletido
PLAYFIELD_PRIORIDADE = #$04 ;bit 2 = 1
;
;=================================================================================
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda #COR_AZUL ;cor azul
sta COLUBK ;plano de fundo
Vamos entender o que acontece. De acordo com a página 78 desse tutorial, o endereço
CTRLPF controla várias características do playfield. O bit zero quando é zero diz ao TIA
para desenhar o playfield de modo duplicado. Se for 1, o playfield é desenhado de modo
refletido.
O bit 2 do CTRLPF controla a prioridade do playfield sobre os outros objetos. Se o bit for
zero, o playfield não tem prioridade e os objetos passam na frente (por cima) dele. Se o bit
for 1, o playfield tem a prioridade, então os objetos passam por trás (por baixo) dele.
PLAYFIELD_REFLETIDO = 00000001b
PLAYFIELD_PRIORIDADE = 00000100b
Então 00000001b
or 00000100b
---------------
00000101b
Colocamos em CTRLPF o valor 00000101b que seta então seus bits 0 e 2 (da direita para a
esquerda).
Figura 94
Figura 95
Figura 96
Detectando colisão
Nosso jogador está atravessando o playfield. Vamos então detectar quando o jogador e o
playfield colidem e assim determinar limites de movimento para o jogador.
Figura 97
No nosso caso estamos lidando com o player0 e o playfield. Então para detectarmos sua
colisão basta testarmos o bit 7 do CXP0FB.
.naoDireita1 ;
lsr ;acima?
bcs .naoAcima0 ;não. verifica abaixo
pha ;salva estado do joystick
lda CXP0FB ;lê registrador de colisão
asl ;pega o bit 7
bcs .naoSobe0 ;colidiu? sim. não sobe player na tela
inc YPosFromBot ;YPosFromBot = YPosFromBot+1
.byte $2C ;pula próxima instrução
;
.naoSobe0 ;
dec YPosFromBot ;YPosFromBot = YPosFromBot-1
pla ;restaura estado do joystick
;
.naoAcima0 ;
lsr ;abaixo?
bcs .naoAbaixo0 ;não. verifica esquerda
pha ;salva estado do joystick
lda CXP0FB ;lê registrador de colisão
asl ;pega bit 7
bcs .naoDesce0 ;colidiu? sim. não desce o player na tela
dec YPosFromBot ;YPosFromBot = YPosFromBot-1
O programa tem vários bugs mas como ele é somente para fins didáticos e a idéia está
sendo atendida, vamos deixar com esses bugs. O que queremos aqui é mostrar sobre
detecção de colisão entre objetos.
Movimente o player contra o playfield. Por exemplo para a direita. Quando ele colidir com o
playfield o desvio (BCS) após o ASL ocorrerá e o player retornará um pouco para a
esquerda. O mesmo ocorre para as demais direções: o player sempre será recuado para a
direção oposta. Fizemos assim porque o player precisa estar separado do playfield para que
o movimento volte a ficar liberado. Se quando ocorresse a colisão simplesmente
parássemos o player, ele ficaria em contato com o playfield, acusando uma colisão infinita.
para
Estudar as várias formas de fazer isso é um bom exercício. Aqui é só exemplo (com bug).
Veja uma outra forma para nosso player construída no AtariPaint e vista no Stella (figura 98).
Figura 98
Como nesse exemplo são várias cores por linha, dentro do .scanloop deve-se fazer LDA
colorTable,x e STA COLUP0 entre as linhas TAX e LDA Player0,x conforme segue:
;---------------------------------- ;
;Scanlines ;
;---------------------------------- ;
.scanLoop ;
lda PlayField0-1,y ;lê o playfield0[y]
sta PF0 ;
lda PlayField1-1,y ;lê o playfield1[y]
sta PF1 ;
lda PlayField2-1,y ;lê o playfield2[y]
sta PF2 ;
tya ;A = Y
sec ;carry = 1
sbc YPosFromBot ;posição em relação à parte inferior da tela
adc #PLAYER0HEIGHT ;adiciona a altura do player
bcc .naoDesenha ;hora de desenhar? não. vá para .naoDesenha
tax ;X = A
lda colorTable,x ;lê cor da linha
sta COLUP0 ;define cor da linha do player0
lda Player0,x ;carrega Player0[x]
.byte $2C ;pula próxima instrução
;
.naoDesenha ;
lda #$00 ;pára de desenhar
sta GRP0 ;coloca valor em GRP0
sta WSYNC ;aguarda 1 scanline
dey ;desenhou toda a tela?
bne .scanLoop ;não. desenha próxima linha
sty COLUPF ;limpa COLUPF
sty COLUP0 ;limpa COLUP0
lda #$1E ;A = 30 linhas de overscan
sta TIM64T ;seta timer
;---------------------------------- ;
;---------------------------------- ;
;Gráficos ;
;---------------------------------- ;
Player0 ;
.byte %11111111 ; |XXXXXXXX|
.byte %11011011 ; |XX XX XX|
.byte %01111110 ; | XXXXXX |
.byte %00111100 ; | XXXX |
.byte %00011000 ; | XX |
.byte %00100100 ; | X X |
.byte %01000010 ; | X X |
.byte %01000010 ; | X X |
.byte %00100100 ; | X X |
.byte %00000000 ; | |
PLAYER0HEIGHT = *-Player0 ;
colorTable ;
.byte $1E,$1E,$1E,$1E,$0E,$24,$42,$5E,$D8,$00
;---------------------------------- ;
Assim, para cada linha desenhada, o 6502 lê uma cor da tabela de cores.
Outros objetos
Vamos ver agora de forma bem rápida os outros objetos (bola e míssil). A idéia básica da
coisa é semelhante aos players. Só algumas diferenças.
A bola
Como já vimos na página 79 desse tutorial, podemos habilitar a bola setando o bit 1 do
ENABL e, conforme a página 82, o ENABL compartilha sua cor e luminosidade com o
playfield, ou seja, a bola terá a mesma cor e luminosidade do playfield (o que está em
COLUPF).
;=================================================================================
;Constantes
;=================================================================================
COR_AZUL = #$80 ;cor azul
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posição horizontal inicial do player0
MOVER_ESQUERDA = #$10 ;high nibble = 1
MOVER_DIREITA = #$F0 ;high nibble = -1
PLAYFIELD_REFLETIDO = #$01 ;playfield refletido
PLAYFIELD_PRIORIDADE = #$04 ;bit 2 = 1
LIGA_DESLIGA_BOLA = #$02 ;bit 1 ativo (%00000010)
ALTURA_DA_BOLA = #$01 ;altura da bola (só 1 scanline)
;
;=================================================================================
;=================================================================================
;Variáveis
;=================================================================================
Seg.U Variaveis ;
org $80 ;
;
YposFromBot ds 1 ;distância em relação a parte de baixo da tela
ativaDesativaBola ds 1 ;status da bola
;
;---------------------------------- ;
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda #COR_AZUL ;cor azul
sta COLUBK ;plano de fundo
lda #LIGA_DESLIGA_BOLA ;A = 2
sta ativaDesativaBola ;ativa bola
Na sessão Lógica do Jogo, nesse exemplo mostrada somente a parte alterada, fazemos o
seguinte: ao pressionar o botão do joystick, o desvio no BMI não ocorre e as instruções
abaixo do BMI são executadas.
A primeira instrução abaixo do BMI carrega A com 02h. A instrução seguinte faz um OU-
Exclusivo (XOR) desse valor com o valor que está em ativaDesativaBola (para quem se
esqueceu o que faz o eor, releia as páginas 24, 50 e 51 desse tutorial).
Isso quer dizer que, cada vez que o botão do joystick é pressionado, o valor alterna entre 1 e
0, servindo assim como uma chave liga-desliga.
No loop .scanLoop comparamos a variável ativaDesativaBola com 02h. Se não forem iguais,
então o programa não desenha a bola, caso contrário ele desenhará a bola usando o
mesmo algoritmo para desenhar o player (página 151 desse tutorial). A diferença é que, em
vez de escrevermos em GRP0, escrevemos em ENABL.
O míssil
O míssil é a mesma coisa da bola. Vamos ver o código:
;=================================================================================
;Constantes
;=================================================================================
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posição horizontal inicial do player0
MOVER_ESQUERDA = #$10 ;high nibble = 1
MOVER_DIREITA = #$F0 ;high nibble = -1
LIGA_MISSIL = #$00 ;ativa míssil
DESLIGA_MISSIL = #$02 ;desativa míssil
ALTURA_DO_MISSIL = #$01 ;altura do míssil (só 1 scanline)
;
MISSIL0HPOS = PLAYER0HPOS + $04 ;míssil + metade da largura do player0
;
;
;=================================================================================
;=================================================================================
;Variáveis
;=================================================================================
Seg.U Variaveis ;
org $80 ;
;
YPosFromBot ds 1 ;distância em relação a parte de baixo da tela
YPosFromBotMissil ds 1 ;distância em relação a parte de baixo da tela
;
;---------------------------------- ;
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda #DESLIGA_MISSIL ;A = 2
sta RESMP0 ;desativa míssil
lda #$80 ;posição em relação a parte inferior da tela
sta YPosFromBot ;salva em YPosFromBot
sta WSYNC ;próximo scanline
ldx #PLAYER0HPOS ;posição horizontal do player0
lda horizTable,x ;lê valor na tabela
sta HMP0 ;posiciona player0
and #$0F ;mascara high nibble
tax ;X = A
;
.posicionaPlayer0 ;
dex ;X = X-1
bpl .posicionaPlayer0 ;decrementa até atingir a posição
sta RESP0 ;reseta player0 nesse clock
sta WSYNC ;
ldx #MISSIL0HPOS ;posição horizontal do missil0
lda horizTable,x ;lê valor na tabela
sta HMM0 ;posiciona missil0
and #$0F ;mascara high nibble
tax ;X = A
;
.posicionaMissil0 ;
dex ;X = X-1
bpl .posicionaMissil0 ;decrementa até atingir a posição
sta RESM0 ;posiciona missil0
sta WSYNC ;
sta HMOVE ;
;---------------------------------- ;
;aqui é parte da rotina Lógica do Jogo
.naoBotao1 ;
lda INPT4 ;botão de tiro pressionado?
bmi .naoBotao0 ;não. Continua
lda #LIGA_MISSIL ;A = 0
sta RESMP0 ;ativa míssil
lda YPosFromBot ;A = posição do player0
sta YPosFromBotMissil ;posição do míssil0 = posição do player0
;
.naoBotao0 ;
stx HMP0 ;direção e velocidade do player0
stx HMM0 ;direção e velocidade do missil0
stx CXCLR ;limpa registradores de colisão
inc YPosFromBotMissil ;desloca míssil para cima
bne .naoMovimentaMissil ;se míssil no topo da tela, pára de deslocar
lda #DESLIGA_MISSIL ;A = 2
sta RESMP0 ;desativa míssil
;
.naoMovimentaMissil ;
lda #COR_AMARELA ;A = cor amarela
sta COLUP0 ;para pintar o míssil
;---------------------------------- ;
;Scanlines ;
;---------------------------------- ;
.scanLoop ;
tya ;A = Y
sec ;carry = 1
sbc YPosFromBotMissil ;posição em relação à parte inferior da tela
adc #ALTURA_DO_MISSIL ;adiciona a altura do míssil
bcc .naoDesenhaMissil ;desenhar? não. vá para .naoDesenhaMissil
lda #DESLIGA_MISSIL ;desativa míssil
.byte $2C ;pula próxima instrução
;
.naoDesenhaMissil ;
lda #LIGA_MISSIL ;ativa míssil
sta ENAM0 ;coloca valor em ENAM0
tya ;A = Y
sec ;carry = 1
sbc YPosFromBot ;posição em relação à parte inferior da tela
adc #PLAYER0HEIGHT ;adiciona a altura do player
bcc .naoDesenhaPlayer0 ;desenhar? não. vá para .naoDesenhaPlayer0
tax ;X = A
lda colorTable,x ;lê cor da linha
sta COLUP0 ;define cor da linha do player0
lda Player0,x ;carrega Player0[x]
.byte $2C ;pula próxima instrução
;
.naoDesenhaPlayer0 ;
lda #$00 ;pára de desenhar
sta GRP0 ;coloca valor em GRP0
sta WSYNC ;aguarda 1 scanline
dey ;desenhou toda a tela?
bne .scanLoop ;não. desenha próxima linha
ldy COLUP0 ;reseta cor do player0
lda #$1E ;A = 30 linhas de overscan
sta TIM64T ;seta timer
;---------------------------------- ;
Figura 99
O console
Vamos ver como podemos verificar o estado das chaves do console, ou seja, qual a posição
das chaves de dificuldade A e B (player0 e player1), se o select ou reset foi acionado. O
princípio é o mesmo dos joysticks. Vejamos o código.
;=================================================================================
;Variáveis
;=================================================================================
Seg.U Variaveis ;
org $80 ;
;
YPosFromBot ds 1 ;distância em relação a parte de baixo da tela
YPosFromBotMissil ds 1 ;distância em relação a parte de baixo da tela
wConsole ds 1 ;salva status do console
;
;---------------------------------- ;
;---------------------------------- ;
;Lógica do jogo ;
;---------------------------------- ;
lda SWCHB ;lê console
sta wConsole ;salva status do console
lsr ;reset pressionado?
bcc .inicioDoJogo ;sim. reinicia programa
lsr ;select pressionado?
bcs .selectNaoPressionado ;não. continua execução
;<< aqui vem o código caso o select seja
;<< pressionado
.selectNaoPressionado ;
ldx #$00 ;assegura que o player não se movimentará
lda SWCHA ;lê endereço do joystick
lsr ;abaixo?
bcs .naoAbaixo1 ;não. verifica acima
;<<<seu código aqui
;
.naoAbaixo1 ;
lsr ;acima?
bcs .naoAcima1 ;não. verifica esquerda
;<<<seu código aqui
;
.naoAcima1 ;
lsr ;esquerda?
bcs .naoEsquerda1 ;não. verifica direita
;<<<seu código aqui
;
.naoEsquerda1 ;
lsr ;direita?
bcs .naoDireita1 ;não. verifica acima
;<<<seu código aqui
;
.naoDireita1 ;
lsr ;acima?
bcs .naoAcima0 ;não. verifica abaixo
pha ;salva estado do joystick
lda CXP0FB ;lê registrador de colisão
asl ;pega o bit 7
bcs .naoSobe0 ;colidiu? sim. não sobe player na tela
inc YPosFromBot ;YPosFromBot = YPosFromBot+1
.byte $2C ;pula próxima instrução
;
.naoSobe0 ;
dec YPosFromBot ;YPosFromBot = YPosFromBot-1
Para testar o reset, rode o programa e mova o player para qualquer posição, então tecle F2
(no Stella é a tecla default para o reset). O player deve voltar para a posição inicial.
Nota: Os 2 NOPs são necessários pois, a linha .byte $2C diz para pular a próxima instrução.
Como não temos nenhuma instrução para colocar lá, ou seja, estamos aproveitando o valor
que já vem em X, os 2 NOPs são necessários. Assim, a linha .byte $2C manda pular os 2
NOPs. Se colocássemos apenas 1 NOP (como a lógica manda, pois NOP é uma instrução)
o programa não funcionaria. Faça a experiência: comente 1 dos NOPs, compile e rode o
programa. Faça um disparo e tecle F5 (se estiver usando o Stella). O programa
simplesmente trava.
Som
Para exemplificar a utilização de som, vamos colocar no nosso programa os efeitos sonoros
do jogo Pitfall. Faça as seguintes alterações:
;=================================================================================
;Constantes
;=================================================================================
COR_AMARELA = #$1E ;cor amarela
PLAYER0HPOS = #$21 ;posição horizontal inicial do player0
MOVER_ESQUERDA = #$10 ;high nibble = 1
MOVER_DIREITA = #$F0 ;high nibble = -1
LIGA_MISSIL = #$00 ;ativa míssil
DESLIGA_MISSIL = #$02 ;desativa míssil
ALTURA_DO_MISSIL = #$01 ;altura do míssil (só 1 scanline)
;
MISSIL0HPOS = PLAYER0HPOS + $04 ;míssil + metade da largura do player0
;
;offset dos sons do Pitfall
SOUND_JUMP = #$20 ;Harry pula
SOUND_TREASURE = #$25 ;Harry pega tesouro
SOUND_DEAD = #$31 ;Harry morre
SOUND_FALLING = #$53 ;Harry cai no buraco
;
;=================================================================================
;=================================================================================
;Variáveis
;=================================================================================
Seg.U Variaveis ;
org $80 ;
;
YPosFromBot ds 1 ;distância em relação a parte de baixo da tela
YPosFromBotMissil ds 1 ;distância em relação a parte de baixo da tela
soundIdx ds 1 ;índice da tabela de sons (0 = sem som)
soundDelay ds 1 ;toca uma nota a cada 4 frames
;
;---------------------------------- ;
;aqui é parte da rotina Lógica do Jogo
.naoBotao1 ;
lda INPT4 ;botão de tiro pressionado?
bmi .naoBotao0 ;não. continua
lda #DESLIGA_MISSIL ;A = 2
sta RESMP0 ;centraliza míssil no player
lda #LIGA_MISSIL ;A = 0
sta RESMP0 ;libera míssil
lda YPosFromBot ;
sta YPosFromBotMissil ;posição vertical igual para os 2 objetos
lda #SOUND_FALLING ;tipo de som a emitir
sta soundIdx ;salva
;
.naoBotao0 ;
stx HMP0 ;move player0
lda wConsole ;restaura status do console
asl ;dificuldade do player1
bcc .dificuldade1emB ;dificuldade em A? não. vai para B
;<< aqui vem o código caso a chave de
;<< dificuldade do player1 seja pressionada
.dificuldade1emB ;
asl ;dificuldade do player0
bcc .dificuldade0emB ;dificuldade em A? não. vai para B
ldx #$00 ;míssil não se move horizontalmente
.byte $2C ;pula próxima instrução
;
.dificuldade0emB ;
nop ;assume o mesmo valor de X para o player0, ou
nop ;seja, se MOVER_DIREITA ou MOVER_ESQUERDA
stx HMM0 ;move míssil
stx CXCLR ;limpa latches de colisão
inc YPosFromBotMissil ;míssil sobre pela tela
bne .naoMovimentaMissil ;míssi chegou no topo da tela? Sim. desliga-o
lda #DESLIGA_MISSIL ;desliga míssil
sta RESMP0 ;trava míssil
;
.naoMovimentaMissil ;
lda #COR_AMARELA ;A = cor amarela
sta COLUP0 ;para pintar o míssil
ldy #$00 ;desliga tom
ldx soundIdx ;X = 0?
beq .noSound ;sim. não emite som
inc soundDelay ;soundDelay = soundDelay + 1
lda soundDelay ;A = soundDelay
and #$03 ;momento de emitir som/mudar nota?
bne .skipNext ;não. não toca nota ainda
inc soundIdx ;próxima nota
;
.skipNext: ;
lda soundTab-1,x ;lê nota da tabela
bpl .contSound ;continua emitindo som
sty soundIdx ;pára de emitir som
;
.contSound: ;
sta AUDF0 ;coloca nota em AUDF0
ldy #$01 ;liga tom (4 bit poly)
;
.noSound: ;
sty AUDC0 ;tom
lda #$04 ;volume 4
sta AUDV0 ;seta volume
;---------------------------------- ;
;---------------------------------- ;
;Tabelas ;
;---------------------------------- ;
align 256 ;
;
soundTab ;
.byte $13, $13, $13, $13, $13, $13, $13, $09, $0b, $0b, $0b, $0b, $0b, $0b, $0b, $0b
.byte $0b, $0b, $0b, $0b, $09, $0b, $09, $0b, $0b, $0b, $0b, $0b, $0b, $0b, $8b, $06
.byte $04, $03, $02, $84
.byte $13, $13, $0e, $0b, $09, $09, $09, $0b, $09, $09, $09, $89
.byte $1d, $1d, $1d, $1d, $1d, $1d, $1d, $1d, $1d, $1a, $1a, $19, $19, $19, $19, $19
.byte $19, $1d, $1d, $1d, $1d, $1d, $14, $15, $14, $15, $14, $15, $14, $15, $14, $15
.byte $14, $95
.byte $18, $19, $1a, $1b, $1c, $1d, $1e, $9f
;
CHECKPAGE horizTable ;
;
;---------------------------------- ;
Altere a linha LDA #SOUND_FALLING para uma das outras 3 constantes que se referem ao
som, compile e rode o programa. Pressione o botão de tiro do joystick 0 para disparar um
míssil. O som é emitido nesse momento.
Faça uma experiência: coloque uma instrução INX entre as linhas LDX soundIdx e BEQ
.noSound, compile e rode o programa.
O trecho
E:\Atari\SDK\doc\Tutorial.doc Página 183 de 243
Atari 2600 – Programação em Assembly para o processador 6502 20/8/2007
ldy #$01 ;liga tom (4 bit poly)
;
.noSound: ;
sty AUDC0 ;tom
gera o tom do som. Esse tom será dividido pela freqüência em em AUDF0. Essa divisão é
dada pelos valores lidos na tabela de som, ou seja, damos o tom e as notas são os valores
que dizem para dividir esse tom em várias freqüências. Vamos fazer um teste: altere a linha
LDY #$01 do trecho acima para LDY #$08. Com isso, em vez de termos um tom de 4 bit
poly, teremos um tom de 5 bit poly -> div 6. Compile e rode o programa. Veja a diferença do
tom. Experimente outros valores (de 01h a 08h). O soundDelay nos dá o tempo de duração
das notas. No nosso caso a cada 4 frames uma nova nota é lida. Maiores detalhes sobre
AUDC0, AUDF0 e AUDV0 estão nas páginas 86 e 87 desse tutorial.
Números
Aqui, veremos um exemplo de como mostrar números na tela. Podemos montar números
através do playfield ou mesmo usar os players 0 e 1.
Nos jogos do 2600, é comum vermos placar de 1 (para número de vidas), 2, 3 e até 6
dígitos. No nosso caso vamos exemplificar um contador de 4 dígitos com a utilização dos
players. Vamos começar um novo programa.
;
;---------------------------------- ;
;=================================================================================
;Código
;=================================================================================
Seg Codigo ;
org $F000 ;
;
.inicioDoJogo ;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda #COR_AMARELA ;cor amarela
sta COLUP0 ;cor do player0
; lda #COR_VERMELHA ;cor vermelha
sta COLUP1 ;cor do player1
lda #$00 ;A = 0
ldx #$03 ;número de dígitos
;
.limpaNumeros ;
sta wNumeros,x ;zera números
dex ;X = X-1. X=0?
bpl .limpaNumeros ;não. continua zerando
sta wContador ;zera wContador
lda #$01 ;A = 1
sta NUSIZ0 ;2 cópias (próximas)
sta NUSIZ1 ;2 cópias (próximas)
;---------------------------------- ;
;Loop Principal ;
;---------------------------------- ;
.inicioDoFrame ;início do frame
lda #$2B ;2Bh = 43d
sta TIM64T ;seta o timer
lda #$02 ;liga o VSYNC
sty VSYNC ;
sta WSYNC ;
sta WSYNC ;aguarda 3 scanlines
sta WSYNC ;
ldy #$00 ;desliga VSYNC
sty VSYNC ;
sta VBLANK ;liga o VBLANK
lda wNumeros ;A = centena/milhar atuais
sta COLUBK ;muda cor de fundo
inc wContador ;wContador = wContador+1
lda #$05 ;A = 5 (delay para o incremento dos números)
cmp wContador ;wContador = A?
bne .naoIncrementaNumeros ;não. não incrementa números
ldx #$01 ;X = 4 dígitos
;
.incNumLoop ;esse loop faz o placar contar até 9999
inc wNumeros,x ;incrementa unidade/centena dependendo de X
lda wNumeros,x ;A = unidade/centena dependendo de X
and #$0F ;mascara high nibble
cmp #$0A ;A = 10?
bne .naoIncrementaDezenaMilhar ;nao. sai do loop com o X apontando ainda
repeat 3 ;
nop ;2 * 3 = 6
repend ;
;15
lda (wNumerosPtr+2),y ;5 centena
sta GRP0 ;3
;8
pla ;4
sta GRP1 ;3 unidade
;7
stx GRP0 ;3
sta WSYNC ;próximo scanline
dey ;Y = Y-1
bne .scanLoop ;desenhou as 8 linhas? não. continua desenhando
sty GRP0 ;reset GRP0 (pára de desenhar)
sty GRP1 ;reset GRP1 (pára de desenhar)
sty COLUBK ;reset COLUBK (pára de desenhar)
;
;-----------------------------------;
;Overscan ;
;-----------------------------------;
lda #$23 ;30 scanlines
sta TIM64T ;seta timer
;
.overScan ;
lda INTIM ;timer = 0?
bne .overScan ;não. continua overscan
jmp .inicioDoFrame ;próximo quadro
;-----------------------------------;
;Seta ponteiros para a matriz de números ;
;-----------------------------------;
.setaPonteiros ;
ldx #$01 ;X = 4 dígitos
ldy #$06 ;Y = 16 bits cada ponteiro
;
.setaNumeros ;
txa ;A = X
pha ;salva
lda wNumeros,x ;
;
.segundoDigito ;
pha ;salva número lido
and #$0F ;mascara high nibble
tax ;X = dígito
lda numTable,x ;lê na tabela de números o dígito X
sta wNumerosPtr,y ;salva o endereço no ponteiro (low order)
lda #>numTable ;obtém high order
sta wNumerosPtr+1,y ;salva high order
pla ;restaura número lido
lsr ;divide por 16 para
lsr ;fazer com que o nibble
lsr ;superior passe para
lsr ;o nibble inferior
dey ;
dey ;decrementa ponteiro
cpy #$04 ;já fez a dezena?
beq .segundoDigito ;não. calcule a dezena
cpy #$00 ;já fez o milhar?
beq .segundoDigito ;não. calcule o milhar
pla ;restaura
tax ;X = A
dex ;segundo dígito já montado?
bpl .setaNumeros ;não. recalcule novamente para o segundo dígito
rts ;retorna para a rotina principal
;---------------------------------- ;
;Tabelas ;
;---------------------------------- ;
;
align 256 ;coloca tabela na próxma página
;
HorzTable ;
.byte $00,$F0,$E0,$D0,$C0,$B0,$A0,$90 ;
.byte $71,$61,$51,$41,$31,$21,$11,$01,$F1,$E1,$D1,$C1,$B1,$A1,$91
.byte $72,$62,$52,$42,$32,$22,$12,$02,$F2,$E2,$D2,$C2,$B2,$A2,$92
.byte $73,$63,$53,$43,$33,$23,$13,$03,$F3,$E3,$D3,$C3,$B3,$A3,$93
.byte $74,$64,$54,$44,$34,$24,$14,$04,$F4,$E4,$D4,$C4,$B4,$A4,$94
.byte $75,$65,$55,$45,$35,$25,$15,$05,$F5,$E5,$D5,$C5,$B5,$A5,$95
.byte $76,$66,$56,$46,$36,$26,$16,$06,$F6,$E6,$D6,$C6,$B6,$A6,$96
.byte $77,$67,$57,$47,$37,$27,$17,$07,$F7,$E7,$D7,$C7,$B7,$A7,$97
.byte $78,$68,$58,$48,$38,$28,$18,$08,$F8,$E8,$D8,$C8,$B8,$A8,$98
.byte $79,$69,$59,$49,$39,$29,$19,$09,$F9,$E9,$D9,$C9,$B9,$A9,$99
.byte $7A,$6A,$5A,$4A,$3A,$2A,$1A,$0A,$FA,$EA,$DA,$CA,$BA,$AA,$9A
;
CHECKPAGE HorzTable ;testa se ultrapassou limite de página
;
numTable ;
.byte <zero-1, <um-1, <dois-1, <tres-1, <quatro-1
.byte <cinco-1, <seis-1, <sete-1, <oito-1, <nove-1
;
zero .byte %00111100 ;matriz de bits do número 0
.byte %01100010 ;
.byte %01010010 ;
.byte %01010010 ;
.byte %01001010 ;
.byte %01001010 ;
.byte %01000110 ;
.byte %00111100 ;
;
um .byte %01111110 ;matriz de bits do número 1
.byte %00011000 ;
.byte %00011000 ;
.byte %00011000 ;
.byte %00011000 ;
.byte %00011000 ;
.byte %00111000 ;
.byte %00011000 ;
;
dois .byte %01111110 ;matriz de bits do número 2
.byte %01100000 ;
.byte %00110000 ;
.byte %00011000 ;
.byte %00001100 ;
.byte %00000110 ;
.byte %01100110 ;
.byte %00111100 ;
;
tres .byte %00111100 ;matriz de bits do número 3
.byte %01000010 ;
.byte %00000010 ;
.byte %00011100 ;
.byte %00011100 ;
.byte %00000010 ;
.byte %01000010 ;
.byte %00111100 ;
;
quatro .byte %00001100 ;matriz de bits do número 4
.byte %00001100 ;
.byte %00001100 ;
.byte %01111110 ;
.byte %01001100 ;
.byte %00101100 ;
.byte %00011100 ;
.byte %00001100 ;
;
cinco .byte %00011000 ;matriz de bits do número 5
.byte %01100100 ;
.byte %00000010 ;
.byte %00000010 ;
.byte %01100100 ;
.byte %01011000 ;
.byte %01000000 ;
.byte %01111110 ;
;
seis .byte %00111100 ;matriz de bits do número 6
.byte %01000010 ;
.byte %01000010 ;
.byte %01111100 ;
.byte %00100000 ;
.byte %00010000 ;
.byte %00001000 ;
.byte %00000100 ;
;
sete .byte %00001000 ;matriz de bits do número 7
.byte %00001000 ;
.byte %00001000 ;
.byte %00000100 ;
.byte %00000100 ;
.byte %00000010 ;
.byte %00000010 ;
.byte %01111110 ;
;
oito .byte %00111100 ;matriz de bits do número 8
.byte %01000010 ;
.byte %01000010 ;
.byte %00111100 ;
.byte %00111100 ;
.byte %01000010 ;
.byte %01000010 ;
.byte %00111100 ;
;
nove .byte %00111100 ;matriz de bits do número 9
.byte %00000010 ;
.byte %00000010 ;
.byte %00000010 ;
.byte %00111110 ;
.byte %01000010 ;
.byte %01000010 ;
.byte %00111100 ;
;
CHECKPAGE numTable ;testa se ultrapassou limite de página
;
;---------------------------------- ;
;
echo (*-$F000)d," Bytes da ROM usados." ;
echo ($1000-(*-$F000))d,"Bytes da ROM livres." ;
;
org $FFFA ;
.word .inicioDoJogo ;NMI
.word .inicioDoJogo ;Reset
.word .inicioDoJogo ;IRQ
Entendendo o programa:
Primeiramente criamos uma variável wNumero que conterá os números a serem exibidos.
Essa variável tem tamanho 2 para representar os 4 dígitos. Poderíamos criar a mesma
variável com tamanho 4 e assim cada byte conteria 1 dígito. No nosso caso, criamos uma
variável de tamanho 2 para comportar 4 dígitos, ou seja, cada nibble representará 1 dígito.
1 byte 1 byte
1 nibble (1º dígito) 1 nibble (2º dígito) 1 nibble (3º dígito) 1 nibble (4º dígito)
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
Depois, criamos outra variável que funcionará como ponteiro (para quem conhece Pascal e
C, sabe do que estamos falando). Essa variável tem como tamanho o dobro do número de
dígitos que queremos exibir na tela. Cada par de bytes dessa variável conterá o endereço
correspondente ao número na matriz de bits que será exibida na tela (lembrando que o
endereço é representado por 16 bits, ou seja, 2 bytes). O formato é little endian, então em
cada par, o primeiro byte conterá o offset e o segundo byte a página de memória.
Utilizaremos a variável wContador para causar um delay na contagem dos números e assim
podermos visualizar melhor seu progresso.
Na sessão Inicializações, setamos ambos os players com a cor amarela. Para setar o player
1 com a cor vermelha, basta descomentar a linha LDA #COR_VERMELHA (retirar o ponto e
vírgula do início dessa linha).
No trecho
lda #$00 ;A = 0
ldx #$03 ;número de dígitos
;
.limpaNumeros ;
sta wNumeros,x ;zera números
dex ;X = X-1. X=0?
bpl .limpaNumeros ;não. continua zerando
zeramos a variável wNumero. No caso desse programa, esse trecho pode ser excluído, pois
a memória toda é zerada no início do código (veja na página 117 desse tutorial o trecho que
zera a memória, comum a todos os programas do 2600). Deixaremos o trecho acima por
questões de ilustração.
Em seguida, no trecho
lda #$01 ;A = 1
sta NUSIZ0 ;2 cópias (próximas)
sta NUSIZ1 ;2 cópias (próximas)
Por que fazemos isso? A resposta é simples: precisamos de 4 dígitos mas temos apenas 2
players para representá-los. Então duplicamos o número de players setando o bit 0 dos
registradores NUSIZ0 e NUSIZ1 (vide páginas 81 e 82 desse tutorial). Colocando o valor 1
nos registradores, criamos para cada player, 2 cópias próximas.
Agora vem a parte que incrementa os números, ou seja, mostraremos na tela os números
sendo incrementados de 1, desde 0000 até 9999. Para isso usamos o trecho abaixo.
As linhas
estão aí só para dar um efeito a mais. Elas farão a cor de fundo mudar conforme nossos
números são incrementados. Nossa variável wNumeros é de 2 bytes, sendo que o primeiro
byte conterá os dígitos da centena e milhar e o segundo byte conterá os dígitos da unidade e
dezena. Então a linha LDA wNumeros carrega em A a centena e milhar. Daí setamos a cor
de fundo com STA COLUBK. Se fizéssemos LDA wNumeros+1, a cor mudaria conforme o
valor da unidade e dezena e como resultado, a mudança da cor se daria de forma mais
rápida. Essas linhas são totalmente dispensáveis.
O trecho acima causa um delay na contagem para podermos visualizar melhor a evolução
dos números. Mude o valor na linha LDA #$05 para alterar o delay.
wNumeros = 2 bytes
wNumeros
Byte 1 Byte 2
wNumeros[0]
wNumeros[1]
Valores de X
Como é feito? Ora, utilizando uma das formas de endereçamento. Nossa variável wNumeros
tem 2 bytes. Carregamos 01h em X e então fazemos INC wNumeros,X. É o mesmo que
fazer INC wNumeros[1], onde [1] é o índice. Daí carregamos esse valor em A e em seguida
mascaramos para extrair somente o nibble inferior, ou seja, zeramos o nibble superior.
Então, comparamos esse valor resultante com 0Ah (10d). Se A ainda não é 10d então não
há necessidade de incrementar o nibble superior, daí o desvio BNE é satisfeito.
O trecho acima é executado quando A (do trecho anterior) é 10d. Ele é responsável por
incrementar o nibble superior do byte, ou seja, se considerarmos 4 dígitos incrementa a
dezena e o milhar, dependendo do valor de X. Como X inicialmente é 1 então
incrementaremos a dezena. O desvio não ocorre e o código acima é executado. Carregamos
em A novamente o valor que está em wNumeros[x] e agora mascaramos para extrair o
nibble superior, ou seja, zeramos o nibble inferior. Adicionamos 10h, assim teremos valores
do tipo 00h, 10h, 20h, 30h... ou seja, somente o nibble superior é incrementado. Daí
testamos se esse valor é A0h. Se A ainda não é A0h então não há necessidade de zerar os
números.
O trecho acima é executado quando A (do trecho anterior) é A0h. Ele é responsável por
zerar o número quando a condição anterior não é satisfeita. Daí decrementamos X (que era
1 inicialmente) e X passa a ser 0. Como 0 é positivo, o desvio no BPL ocorre, direcionando o
fluxo novamente para .incNumLoop. Agora o 6502 vai fazer tudo de novo, porém com X
sendo 0, ou seja, agora ele fará os cálculos para o próximo byte (centena e milhar). Simples.
Mas não há uma forma mais fácil de fazer isso? A resposta é sim. No código apresentado,
estamos lidando com os nibbles dos bytes manualmente. Há uma forma simples e prática de
fazer isso automaticamente. O código apresentado se transformará em:
;---------------------------------- ;
;Loop Principal ;
;---------------------------------- ;
.inicioDoFrame ;início do frame
lda #$2B ;2Bh = 43d
sta TIM64T ;seta o timer
lda #$02 ;liga o VSYNC
sty VSYNC ;
sta WSYNC ;
sta WSYNC ;aguarda 3 scanlines
sta WSYNC ;
ldy #$00 ;desliga VSYNC
sty VSYNC ;
sta VBLANK ;liga o VBLANK
lda wNumeros ;A = centena/milhar atuais
sta COLUBK ;muda cor de fundo
inc wContador ;wContador = wContador+1
lda #$10 ;A = 5 (delay para o incremento dos números)
cmp wContador ;wContador = A?
bne .naoIncrementaNumeros ;não. não incrementa números
ldx #$01 ;X = 4 dígitos
php ;salva flags
sed ;seta decimal mode
;
.incrementaCentenaMilhar ;
clc ;limpa carry flag
lda wNumeros,x ;lê par de dígitos de acordo com X
adc #01 ;soma 1
sta wNumeros,x ;armazena novo valor
bne .naoIncCentenaMilhar ;chegou a zero?
dex ;sim. incrementa centena/milhar
jmp .incrementaCentenaMilhar ;incrementa
;
.naoIncCentenaMilhar ;
plp ;restaura flags
lda #$00 ;A = 0
sta wContador ;reseta wContador
;
.naoIncrementaNumeros ;
jsr .setaPonteiros ;muda de banco
;
Muito mais simples. E tudo isso por causa de um flag. O Decimal Mode flag. Quando
setamos esse flag, os valores dos registradores em operações de soma e subtração são
E:\Atari\SDK\doc\Tutorial.doc Página 193 de 243
Atari 2600 – Programação em Assembly para o processador 6502 20/8/2007
tratados como números decimais e não hexadecimais. Assim, se o valor de A for 09d, por
exemplo, ao somarmos 1, em vez de ele ser 0Ah, ele será 10d.
Se você quiser trabalhar com números hexadecimais, porém interpretá-los como decimais
sem utilizar o Decimal flag, basta adicionar 06h ao byte caso ele seja maior que 09h. Por
exemplo:
Convertendo 13h para decimal temos 19d. Então 19d e 19h são iguais (se ignoramos a
base, claro).
Porém:
Convertendo 1Ah para decimal temos 20d. Como resolver? Basta pegarmos o nibble inferior.
Como ele é maior que 09h somamos 06h novamente. Então:
Agora basta fazermos 11h + o nibble superior de 25h = 2h e temos: 11h + 20h = 31h que, se
ignorarmos a base, é igual a 31d (31 = 31).
Nota: Para pegar o nibble superior basta fazer o número AND F0h e para pegar o nibble
inferior basta fazer o número AND 0Fh.
Eles seriam necessários em um programa no qual fosse importante salvar os flags antes de
alterar qualquer um deles (no caso o Decimal Mode). Daí quando terminássemos nossos
cálculos, restauraríamos os flags para o status que eles eram antes dos cálculos.
Veja também que o status dos flags após os cálculos é totalmente descartável, ou seja,
podemos restaurar como eles eram antes porque não dependemos deles fora dos cálculos.
wNumeros
1 byte 1 byte
1 nibble (milhar) 1 nibble (centena) 1 nibble (dezena) 1 nibble (unidade)
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
0 1
X
Agora que a variável wNumeros tem o novo valor, precisamos fazer com que esse valor seja
exibido na tela. A exibição se dá de forma gráfica, então temos uma matriz de bits que
compõe cada número (de 0 a 9). Por exemplo:
é o mesmo que
.byte %01111110 X X X X X X
.byte %01100000 X X
.byte %00110000 X X
.byte %00011000 X X
.byte %00001100 X X
.byte %00000110 X X
.byte %01100110 X X X X
.byte %00111100 X X X X
Cada número é identificado por um label. Esse label está se referindo ao offset do último
byte da matriz de bits que compõe cada número (contando de baixo para cima). No exemplo
do número 2 acima, o label dois está referenciando o primeiro byte (de cima para baixo) da
matriz de bits (ou último byte que representa a base do desenho do número 2, vai da leitura
e interpretação de cada um).
O que importa é que esse label estará indicando um offset e esse offset nos dá a posição na
ROM de onde está nosso número 2. Então para chegarmos até ele, basta colocar em algum
registrador esse offset (veremos logo).
O procedimento (que vai ser detalhado mais adiante) é o seguinte: lemos o valor de
wNumeros, pegamos um dos dígitos, localizamos na matriz de bits o offset desse número,
colocamos em um ponteiro e desenhamos o gráfico a partir desse ponteiro. Básico.
Essa rotina é:
.setaPonteiros ;
ldx #$01 ;X = 4 dígitos
ldy #$06 ;Y = 16 bits cada ponteiro
;
.setaNumeros ;
txa ;A = X
pha ;salva
lda wNumeros,x ;
;
.segundoDigito ;
pha ;salva número lido
and #$0F ;mascara high nibble
tax ;X = dígito
lda numTable,x ;lê na tabela de números o dígito X
sta wNumerosPtr,y ;salva o endereço no ponteiro (low order)
lda #>numTable ;obtém high order
sta wNumerosPtr+1,y ;salva high order
pla ;restaura número lido
lsr ;divide por 16 para
lsr ;fazer com que o nibble
lsr ;superior passe para
lsr ;o nibble inferior
dey ;
dey ;decrementa ponteiro
cpy #$04 ;já fez a dezena?
beq .segundoDigito ;não. calcule a dezena
cpy #$00 ;já fez o milhar?
beq .segundoDigito ;não. calcule o milhar
pla ;restaura
tax ;X = A
dex ;segundo dígito já montado?
bpl .setaNumeros ;não. recalcule novamente para o segundo dígito
rts ;retorna para a rotina principal
Mais uma vez utilizamos X para determinar quem está sendo lido. Isso é feito quando
setamos X com 01h (LDX #$01) e depois lemos wNumeros (LDA wNumeros,x).
Quando o 6502 entra em .segundoDigito, X é 1 então ele fará os cálculos para a unidade.
Primeiro ele salva o byte (com PHA) pois vai precisar usar esse número 2 vezes. Então, ele
extrai o nibble inferior, ou seja, zera o nibble superior do número lido e o transfere para X.
Agora através da linha LDA numTable,X ele coloca em A o offset do número que está em X
e em seguida armazena esse offset em wNumerosPtr, que é o ponteiro para a matriz de bits.
Depois com LDA #>numTable, carrega em A a página de memória onde numTable está e
em seguida armazena em wNumerosPtr+1. Com isso, monta em wNumerosPtr o
offset+página de memória, ou seja, o endereço de 16 bits para a matriz de bits.
Feito isso com o nibble inferior, ele restaura o valor lido (com PLA) e desloca os 4 bits
superiores (nibble superior) para os 4 bits inferiores (nibble inferior). Daí decrementa Y 2
vezes, pois o ponteiro é formado de pares de bits. Se Y for 04h então ele calculará a
dezena, direcionando o fluxo para .segundoDigito com o A carregado com a dezena
deslocada 4 bits para a direita.
Daí quando ele decrementar Y 2 vezes novamente, Y agora terá o valor 02h. O desvio nos 2
BEQs não será satisfeito então ele restaura o valor de X (que era 1 inicialmente) e o
decrementa. Como X agora é 0, ele considera positivo e o desvio BPL ocorre, desviando o
fluxo para .setaNumeros. Agora o A é carregado com o byte de wNumeros que corresponde
a centena/milhar e o mesmo processo anterior é repetido. No final, ele terá montado um
ponteiro de 8 bytes (cada par representando um endereço de memória) e retorna ao
chamador através do RTS.
.scanLoop ;
lax (wNumerosPtr+6),y ;5
lda (wNumerosPtr+4),y ;5
pha ;+3
;=8
lda (wNumerosPtr),y ;5
sta.w GRP1 ;+4 milhar
repeat 3 ;
nop ;+(2 * 3 = 6)
repend ;
;=15
lda (wNumerosPtr+2),y ;5 centena
sta GRP0 ;+3
;=8
pla ;4
sta GRP1 ;+3 unidade
;=7
stx GRP0 ;3
sta WSYNC ;0 próximo scanline
dey ;Y = Y-1
bne .scanLoop ;desenhou as 8 linhas? não. continua desenhando
A figura 100 mostra o resultado desse programa e a figura 101 mostra os números para
players com cores diferentes. Por essa figura, podemos observar que os players são
posicionados de forma intercalada.
Quando setamos o NUSIZx para criar 2 cópias próximas, a distância de uma cópia para a
outra é tal que, ao intercalar o outro player, tem-se a impressão de ser tudo uma coisa só,
devido às distâncias entre eles serem correspodentes a uma largura de player.
Nota: A numTable e a matriz de bits devem estar na mesma página de memória, pois
colocamos em wNumerosPtr+1 a página de memória de numTable e em wNumerosPtr o
valor que está em numTable,X e não seu offset. Na verdade, o valor que está em
numTable,X é o offset do número na matriz de bits, conforme construção abaixo:
numTable ;
.byte <zero-1, <um-1, <dois-1, <tres-1, <quatro-1
.byte <cinco-1, <seis-1, <sete-1, <oito-1, <nove-1
Dessa forma, podemos alterar a ordem dos números na matriz de bits (para compartilhar
bytes por exemplo (detalhes na próxima sessão)) que numTable se encarrega de “colocá-los
na ordem” para que o algoritmo os leia baseado no índice X.
Figura 100
Figura 101
Sharing bytes
No programa anterior, utilizamos apenas 2 bytes para armazenar 4 dígitos. Isso acarretou
em mais código para “montar” e “desmontar” os dígitos, ou seja, mais lógica, mais bytes de
ROM, mais ciclos de máquina. Se utilizássemos 4 bytes, o código ficaria menor, acarretando
em menos bytes de ROM e talvez menos ciclos, entretanto estaríamos gastando mais 2
bytes de RAM.
Quando criamos um programa para o 2600 devemos levar em conta sobre o que podemos
nos dar ao luxo de gastar: ROM ou RAM? E de que forma os ciclos são afetados?
Tudo isso depende: Se nosso programa for muito grande, utilizamos a RAM. Se tivermos
muitas variáveis então economizamos a RAM e deixamos por conta do 6502 executar mais
código. Ainda assim, em um programa como o anterior (que exibe números) é possível
economizar bytes da ROM. Basta compartilhar (share) bytes.
Vamos supor que temos uma matriz de bits que representa números de 0 a 9 em um
formato mais “quadrado” (digital).
Os números acima foram desenhados com 4 colunas (4 bits) cada só para termos uma
melhor visualização. No programa de exibição de números descrito na sessão anterior,
utilizamos os 8 bits dos players.
De posse dessa matriz, nos perguntamos: quais números podem compartilhar a primeira e
última linha com outro(s) número(s)?
Quanto mais quadrado for o desenho, mais compartilhamento de bytes será possível
realizar.
Para quem ainda não entendeu, veja a figura 102, que representa o número 6 e o número 2
com bytes compartilhados:
Figura 102
No caso desses 2 números, economizamos 1 byte, ou seja, para cada par de números
economizamos 1 byte.
Com isso encerramos nossos tópicos de como criar um programa para o 2600. Não
exploramos tudo nesse tutorial, pois, ele é bem básico. Só serve de introdução. É só para
dar uma idéia da coisa.
Nos próximos tópicos veremos alguma coisa sobre bankswitching e vamos hackear uma
ROM utilizando o debugger do Stella.
Bankswitching
Você já está expert na programação de jogos para o 2600 e 4K é pouco para o jogaço que
está escrevendo. Vamos mostrar uma das várias formas de conseguir mais “espaço” na
ROM para programar jogos mais complexos e até mesmo mais de um jogo por cartucho
(lembra-se daqueles cartuchos com 2, 4, 32, etc. jogos?). Mas antes, um pouco de teoria...
É claro que o que vamos descrever aqui está de forma (muito) simplificada, pois se
entrarmos no mérito técnico da coisa, o entendimento seria mais complicado.
Cada byte de um programa é gravado na ROM em uma célula. De forma geral (e para
simplificar o entendimento) vamos dizer que essas células estão enfileiradas. Cada célula
então tem um número de ordem (ou endereço). Esse endereço é acessado pelo 6502
através de seu registrador PC. Uma ROM típica tem 4096 bytes de espaço para
armazenamento, ou seja, 4096 células, cada uma identificada por seu endereço e que pode
ser acessada através do regitrador PC. Lembrando que 4096 bytes = 4K. Então um grupo de
células (4K) é chamado de banco de memória, ou simplesmente banco daqui em diante.
Observemos a figura 103. Nela podemos ver, da direita para a esquerda, a coluna do
programa, a coluna com os opcodes de cada instrução e a coluna de offset. Essa coluna de
offset (com números hexadecimais, ou seja, iniciados por $) é o endereço de cada byte
(instrução) do programa na ROM. São esses números que o registrador PC do 6502
percorrerá e se orientará nos desvios e chamadas de subrotinas.
Podemos observar que a primeira instrução, SEI, tem somente 1 byte (78) e está no
endereço F000h. Como ela só tem 1 byte, então ela ocupa somente 1 célula da ROM e a
próxima instrução então estará no endereço F001h (que no caso da figura é o CLD e seu
opcode é D8). Pela figura, observamos as instruções e quantos bytes elas ocupam. O offset
muda de acordo com o tamanho de cada instrução.
Figura 103
Figura 104
Apesar de serem parecidos, os códigos são diferentes a partir do offset F007h. O que
queremos mostrar aqui é que ambos começam no offset F000h.
Vamos fazer uma analogia: Você está assistindo a um canal de TV. Daí você muda de
canal, ou seja, o que está sendo exibido é diferente, mas a janela de visão (por onde você
está olhando) continua a mesma.
Em relação às 2 figuras (103 e 104) o 6502 “vê” o que é colocado em sua janela de visão, ou
seja, você coloca um cartucho e ele vê o quem dentro dele (o programa), mas sempre no
mesmo lugar: a partir do offset F000h.
Então, no 2600, quando você trocava de cartucho o 6502 “via” o novo programa no mesmo
lugar do anterior.
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
;---------------------------------- ;
;Inicializações ;
;---------------------------------- ;
lda $1FF8,1 ;vai para o banco #0
nop ;2 NOPS para o LDA #$01 do banco #0
nop ;
nop ;2 NOPS para o LDX #$02 do banco #0
nop ;
nop ;2 NOPS para o LDY #$03 do banco #0
nop ;
nop ;3 NOPs para LDA $1FF9,0 do banco #0
nop ;
nop ;
lda #$AA ; <<<< executa daqui quando vem do banco #0
ldx #$BB ;
ldy #$CC ;
jmp .banco1 ;loop infinito
;
org $FFFA ;
.word banco1 ;NMI
.word banco1 ;Reset
.word banco1 ;IRQ
Ao rodar no modo debug, o Stella inicia a execução do banco #1, conforme a figura 105.
Figura 105
Observe na mesma figura, o 1 do Current bank, indicando o banco atual e o total de bancos,
Total banks, igual a 2, ou seja, nossa ROM tem 2 bancos.
O início do código já conhecemos. É aquela parte que zera toda a RAM. Para que não
precisemos executar passo a passo essa parte (um loop de 255 passos, o que demoraria
muito), colocamos um breakpoint em $F009. Para isso, basta clicar no lado esquerdo do
offset $F009 e um quadrado vermelho aparece, conforme a figura 106.
Figura 106
Agora clicamos em Exit (figura 107) para sairmos do modo debug e executarmos o
programa normalmente. Quando o 6502 atingir o offset $F009, o Stella retornará ao modo
debug automaticamente.
Figura 107
Agora que chegamos no breakpoint, observe a figura 108. O registrador PC aponta para o
offset que será executado, no nosso caso o $F009. Então vamos executar passo a passo o
programa. Para isso, clique no botão Step (figura 107). Ao clicar nesse botão, a instrução
que está no offset $F009 será executada. Essa instrução lê um endereço especial que diz
para mudar de banco. Na verdade, é um strobe, pois tanto faz ler ou escrever. O que
importa é: acessou o endereço, alguma coisa acontece. Essa é a natureza dos strobes.
Figura 108
Então, ao clicar em Step, o Stella executa a instrução que diz para ir para o banco #0. Com
isso, a tela fica conforme a figura 109.
Note que o breakpoint continua em $F009 apesar de termos mudado de banco. Isso reforça
o que já dissemos: o 6502 agora “vê” outro programa, mas utiliza a mesma “janela de visão”
para isso.
Observe também na figura que o Current bank agora é 0 e o código do banco #0 está
carregado.
Figura 109
Quando ainda estávamos no banco #1, no offset $F009 onde está nosso breakpoint, o
registrador PC era $F009, ou seja, o registrador PC apontava para o offset que seria
executado, certo? Depois que clicamos em Step, o 6502 executou a instrução e passou para
o próximo offset, ou seja, o registrador PC agora aponta para $F00C (pois a instrução LDA
$1FF8 gasta 3 bytes). Entretanto, a instrução diz para mudar de banco. Com isso, o outro
banco (#0) agora é o banco atual e seu código é colocado no lugar do código do banco #1.
Porém, o registrador PC continua apontando para $F00C, e o 6502 vai continuar executando
o programa a partir desse offset. Imagine que o 6502 fica lendo os endereços não
importando o que esteja neles. Se você troca o conteúdo, ele continua fazendo o trabalho. É
como se você estivesse com os olhos vendados e almoçando. Seu processo é pegar a
E:\Atari\SDK\doc\Tutorial.doc Página 207 de 243
Atari 2600 – Programação em Assembly para o processador 6502 20/8/2007
comida e levá-la até a boca. Várias vezes repetindo esse movimento. Se alguém troca o
prato, você continuará fazendo a mesma coisa, porém o conteúdo é diferente.
Por isso, o que queremos executar no banco #0 deve estar depois do offset $F009. Para
tornar isso possível, colocamos vários NOPs antes do código que queremos executar no
banco #0. Esses NOPs ocupam o mesmo lugar do código do banco #1. Quando o PC chega
em $F009, já gastamos 9 bytes, por isso temos 9 NOPs. Entretanto, a própria instrução em
$F009, que é LDA $1FF8, gasta 3 bytes. Daí quando o PC lê essa instrução ele vai para
$F00C, totalizando 12 bytes. No banco #0, colocamos então, além dos 9 NOPs, mais 3
NOPs correspondentes ao LDA $1FF8 do banco #1. Assim, quando o PC ler o LDA $1FF8
no offset $F009 do banco #1, o Stella carrega o banco #0 e o PC vai para $F00C (figura
110). O código útil do banco #0 inicia-se em $F00C devido aos 12 NOPs anteriores,
conforme a figura 109. Então, ao mudarmos de banco, a execução continua normalmente.
Figura 110
Quando o PC chegar em $F012, ele encontrará a instrução LDA $1FF9 que diz ao Stella
carregar o banco #1 (figura 111).
Figura 111
Ao executar essa instrução o PC vai para o offset $F015 mas o banco #1 já terá sido
carregado, e o que estiver no offset $F015 do banco #1 será executado. Veja a figura 112.
Figura 112
De acordo com a figura 112, voltamos para o banco #1 e as instruções a partir do offset
$F015 estão lá só para efeito de ilustração também. Entretanto, observe os NOPs. Eles
ocupam o mesmo espaço do código útil do banco #0. Vamos colocar os códigos lado a lado
para melhor visualização.
Conte os bytes que cada instrução utiliza da ROM e confronte com a quantidade de NOPs.
As 2 quantidades devem ser iguais.
Com essas ilustrações começamos a ter idéia de como funciona o bankswitching. Agora
vamos ir direto para uma ROM de 8 bancos e ver como mudamos de banco nessa ROM.
O código a seguir é mais ou menos um padrão para a estrutura, exceto pela macro. A macro
startBank é utilizada para garantir que, ao rodar o programa, o banco #0 será sempre
carregado primeiro. Assim saberemos qual é a porta de entrada para o nosso programa
(como o método main do Java ou função main do C). Se quiser fazer sem a macro também
pode. Entretanto, para garantir que o start se dará sempre por um determinado banco, deve-
se colocar o código que muda de banco no início da cada banco (essa frase ficou cheia de
palavras “banco”). A macro é só uma forma de facilitar as coisas na hora da programação. O
programa fonte fica mais enxuto. Entretanto é indiferente para o compilador. Onde ele
encontrar a macro, colocará o código dela. É como se automatizássemos a digitação de
códigos repetidos.
A colocação de um código que diz por qual banco queremos iniciar nosso programa se faz
necessária porque dependendo do emulador, coisas estranhas podem acontecer, como por
exemplo, o start se dar por qualquer banco. Como iniciaremos sempre pelo banco #0, no
próprio banco não precisamos colocar a macro.
Se mudarmos o valor de Y para 01h, por exemplo, o start se dará sempre pelo banco #1 e
assim por diante (até o máximo de 7, totalizando 8 bancos).
Toda essa estrutura pode ficar dentro do mesmo arquivo .ASM para ser compilada.
Observe que temos apenas um único segmento para as variáveis. Isso mesmo. Cada banco
é um programa, porém, compartilham um único segmento de memória.
;=================================================================================
;Constantes
;=================================================================================
;
;=================================================================================
;Variáveis
;=================================================================================
Seg.U Variaveis ;
org $80 ;
;
;=================================================================================
;Bank #0
;=================================================================================
Seg Bank0 ;
org $8000 ;aloca 28K abaixo
rorg $F000 ;realoca para F000h
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
;----------------------------------;
;Seu código aqui ;
;----------------------------------;
;
org $8FFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #1
;=================================================================================
Seg Bank1 ;
org $9000 ;aloca 24K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
;
org $9FFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #2
;=================================================================================
Seg Bank2 ;
org $A000 ;aloca 20K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
;
org $AFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #3
;=================================================================================
Seg Bank3 ;
org $B000 ;aloca 16K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
;
org $BFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #4
;=================================================================================
Seg Bank4 ;
org $C000 ;aloca 12K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
;
org $CFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #5
;=================================================================================
Seg Bank5 ;
org $D000 ;aloca 8K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
;
org $DFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #6
;=================================================================================
Seg Bank6 ;
org $E000 ;aloca 4K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
;
org $EFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #7
;=================================================================================
Seg Bank7 ;
org $F000 ;
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
;
org $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
Agora vamos ver como mudamos de um banco para o outro. Vamos fazer um teste bem
rápido. Do banco #0 vamos mudar para o banco #4 e do banco #4 vamos mudar para o
banco #6. Basta alterar os seguintes bancos:
;=================================================================================
;Bank #0
;=================================================================================
Seg Bank0 ;
org $8000 ;aloca 28K abaixo
rorg $F000 ;realoca para F000h
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
;=================================================================================
;Bank #4
;=================================================================================
Seg Bank4 ;
org $C000 ;aloca 12K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;esses NOPs se referem ao código do banco #0
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
ldy #$06 ;banco #6
lda $1FF4,y ;muda de banco
;
org $CFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #6
;=================================================================================
Seg Bank6 ;
org $E000 ;aloca 4K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;esses NOPs se referem ao código do banco #0
nop ;
nop ;mais o código do banco #4
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
nop ;
lda #$01 ;código do banco #6
;
org $EFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
Ao executar esse código no modo debug, podemos notar a mudança de banco conforme as
figuras 113, 114 e 115. Observe que o código de um banco deve iniciar no offset seguinte ao
offset onde termina o código do banco chamador (inclusive a instrução de mudança de
banco). Há várias formas de fazer bankswitching. Criando tabelas de vetores, alterando o
ponteiro do vetor de IRQ e executando o BRK, colocando o destino na pilha, etc. Vamos ver,
nesse tutorial, a última forma elencada: colocando o destino na pilha.
Figura 113
Figura 114
Figura 115
Vamos alterar o exemplo anterior para eliminarmos a necessidade nos NOPs e executar a
partir de determinado ponto dentro do banco de destino.
;=================================================================================
;Bank #0
;=================================================================================
Seg Bank0 ;
org $8000 ;aloca 28K abaixo
rorg $F000 ;realoca para F000h
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
;----------------------------------;
;Seu código aqui ;
;----------------------------------;
lda #>(.rotinaBanco4-1) ;colocamos a página de memória da rotina
pha ;na pilha (high order)
lda #<(.rotinaBanco4-1) ;colocamos o offset da rotina
pha ;na pilha (low order)
jmp .mudaBanco ;vamos para a rotina de mudar de banco
;=================================================================================
;Bank #4
;=================================================================================
Seg Bank4 ;
org $C000 ;aloca 12K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
ldy #$06 ;banco #6
lda $1FF4,y ;muda de banco
;
org $CF06 ;alocamos em CF06h
rorg $FF06 ;realocamos para FF06h
rts ;retornamos da chamada de subrotina???
;
org $CFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
Antes de fazermos também a alteração para mudar do banco #4 para o banco #6, vamos
verificar o que foi feito. No banco #0, o código:
Faz o seguinte: o compilador coloca em A (no primeiro LDA) o byte alto do endereço onde
está a .rotinaBanco4. Lembramos que um endereço, no 6502, é composto de 16 bits, ou 2
bytes. Daí colocamos esse valor na pilha. Depois, pegamos (com o segundo LDA), o byte
baixo do endereço onde está a .rotinaBanco4 e colocamos também na pilha. Fizemos nessa
ordem porque, lembrando novamente, o endereçamento é na forma little endian, ou seja, o
menor vem primeiro.
vez que não queremos ir para essa subrotina agora, pois ainda estamos no banco #0 e ela
não existe nesse banco. Agora vem a parte do banco #4.
Ao chegar no banco #4, no offset FF06h, o 6502 encontra a instrução RTS. Essa instrução
significa ReTurn from Subroutine, ou seja, retorne da subrotina. Ainda: retorne da subrotina
para o lugar de onde veio. E de onde que ele veio? Do lugar que está na pilha! E qual o
lugar que está na pilha? O endereço da .rotinaBanco4. Então, quando ele executar o RTS,
vai pegar o endereço que está guardado na pilha e setar o registrador PC. Assim nossa
execução vai direto para a .rotinaBanco4. Muito simples.
Veja as figuras 116, 117, 118 e 119. Elas mostram a execução. Quando chegamos no banco
#4, há o código para mudar para o banco #6 (do primeiro exemplo) e então paramos por aí.
Se executarmos o código do banco #4, vamos para o banco #6 com o PC apontando para
um offset diferente de onde começa o código útil do banco #6, ou seja, vamos entrar no
banco #6 e executar alguns NOPs antes de chegar no código útil propriamente dito. De
qualquer forma, do jeito que está podemos ver as 2 formas de mudança de banco
funcionando.
Figura 116
Figura 117
A figura 117 ainda mostra o banco #0, porém no offset FF01h, pronto para mudar de banco.
Figura 118
A figura 118 mostra quando executamos a instrução LDA $1FF4 do banco #0. O Stella muda
para o banco #4 (vide Current bank destacado em vermelho) mas não exibe o código do
banco #4. Daí para sabermos o que vai ser executado, basta digitarmos o comando disasm
na parte esquerda da interface do debug do Stella. Como o banco agora é o #4, ele
disassembla o código do banco #4 a partir do offset dado por PC (que é FF06h). Daí
verificamos que a próxima instrução a ser executada no banco #4 é o RTS. Clicamos
novamente em Step para que essa instrução seja executada.
Figura 119
O RTS pegou o endereço que estava na pilha e fez o PC apontar para ele. Agora estamos
no código útil do banco #4. Se executarmos essas instruções, vamos mudar para o banco #6
e a tela é apresentada conforme a figura 120.
Figura 120
O PC foi para o banco #6 apontado para F008h. Como não fizemos nenhuma alteração no
banco #6 do primeiro exemplo, ele continuou com os NOPs referentes ao antigo código do
banco #4.
Com isso concluimos que, para mudarmos de banco, devemos colocar instruções em cada
banco dizendo para mudar para o banco #X e no banco #X devemos ter instruções que, ou
já são o código útil, ou fazem o fluxo ir para determinado offset. Para que não tenhamos que
escrever em cada banco essas instruções, podemos criar uma macro. A seguir, um exemplo
de mudança de banco com macro e com o recurso de dizermos para onde queremos que o
PC vá no banco de destino.
;=================================================================================
;Macros
;=================================================================================
MAC startBank ;colocar no início de cada banco (exceto no #0)
;
inicioDoBanco ;
ldy #$00 ;
lda $1FF4,y ;startup sempre pelo banco #0
ENDM ;
;
;----------------------------------;
MAC mudaBancos ;colocar no final de cada banco
pha ;salva A
txa ;A = X
pha ;salva A
lda $1FF4,y ;muda de banco
rts ;fake return
ENDM ;
;----------------------------------;
;=================================================================================
;Bank #0
;=================================================================================
Seg Bank0 ;
org $8000 ;aloca 28K abaixo
rorg $F000 ;realoca para F000h
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
;----------------------------------;
;Seu código aqui ;
;----------------------------------;
.rotinaBanco0 ;
lda #>(.rotinaBanco4-1) ;A = high order do endereço da .rotinaBanco4
ldx #<(.rotinaBanco4-1) ;X = low order do endereço da .rotinaBanco4
ldy #$04 ;Y = banco #4
jmp .mudaBanco ;vai para .mudaBanco
;=================================================================================
;Bank #4
;=================================================================================
Seg Bank4 ;
org $C000 ;aloca 12K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
.rotinaBanco4 ;
ldy #$06 ;banco #6
lda $1FF4,y ;muda de banco
;
org $CF06 ;alocamos em CF06h
rorg $FF06 ;realocamos para FF06h
;
mudaBancos ;
;
org $CFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
Com a mudança anterior, simplesmente colocamos o código em uma macro. Daí, setamos A
com o byte alto do endereço da rotina a ser chamada, X com o byte baixo e Y com o número
do banco de destino. No momento da execução, o programa é interpretado como:
A macro deve estar em todos os bancos. No nosso exemplo, ela está no banco #0 e no
banco #4. Assim já é possível notar que, quando estivermos no banco #0 e executarmos a
instrução LDA $1FF4,y o 6502 mudará para o banco #4 e o registrador PC apontará para a
instrução RTS. Ao executar o RTS, o 6502 lê a pilha e vai para a .rotinaBanco4. Escrevendo
o programa dessa forma, podemos agora implementar facilmente no banco #4 o código para
mudar para o banco #6. Assim.
;=================================================================================
;Bank #4
;=================================================================================
Seg Bank4 ;
org $C000 ;aloca 12K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
.rotinaBanco4 ;
lda #>(.rotinaBanco6-1) ;A = high order do endereço da .rotinaBanco6
ldx #<(.rotinaBanco6-1) ;X = low order do endereço da .rotinaBanco6
ldy #$06 ;Y = banco #6
jmp .mudaBanco ;vai para .mudaBanco
;
org $CF06 ;alocamos em CF06h
rorg $FF06 ;realocamos para FF06h
;
mudaBancos ;
;
org $CFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
;=================================================================================
;Bank #6
;=================================================================================
Seg Bank6 ;
org $E000 ;aloca 4K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
.rotinaBanco6 ;
lda #>(.rotinaBanco0-1) ;A = high order do endereço da .rotinaBanco0
ldx #<(.rotinaBanco0-1) ;X = low order do endereço da .rotinaBanco0
ldy #$00 ;Y = banco #0
jmp .mudaBanco ;vai para .mudaBanco
;
org $EF06 ;alocamos em EF06h
rorg $FF06 ;realocamos para FF06h
;
mudaBancos ;
;
org $EFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
Colocamos no banco #6 um código para chamar de volta o banco #0. Loop infinito.
O processo de chamar uma rotina e depois retornar para o chamador é análogo à função da
instrução JSR, ou seja, eu preciso apenas saber quem vou chamar... quanto ao retorno, o
próprio programa se encarrega de saber para onde voltar.
Nos exemplos apresentados, teríamos que nos preocupar não somente em saber qual rotina
chamar, mas também saber para onde voltar. Em outras palavras um banco chamador
chama uma rotina e, depois que a rotina termina sua execução, o banco chamado chama
novamente o banco chamador, passando assim a ser um banco chamador também (essa
frase ficou pior que a frase que tem um monte de palavras “banco”). É um retorno falso.
Vamos apresentar agora uma forma de contornar isso, ou seja, vamos nos preocupar em
saber somente quem queremos chamar. O programa se encarregará de retornar para onde
ocorreu a mudança de banco no banco chamador.
;=================================================================================
;Macros
;=================================================================================
MAC startBank ;colocar no início de cada banco (exceto no #0)
;
inicioDoBanco ;
ldy #$00 ;
lda $1FF4,y ;startup sempre pelo banco #0
ENDM ;
;----------------------------------;
mac mudaBancos ;colocar no fim de cada banco
pha ;salva bancos de origem e destino
tya ;A = high order do endereço da rotina
pha ;salva
txa ;A = low order do endereço da rotina
pha ;salva
tsx ;X = Stack Pointer
inx ;incrementa X para que
inx ;ele aponte para o local da pilha onde
inx ;salvamos os bancos de origem e destino
lda $00,x ;A = bancos de origem e destino (lê da pilha)
and #$0F ;mascara high order
tay ;Y = banco de destino
lda $1FF4,y ;muda de banco
rts ;fake return
endm ;
;----------------------------------;
mac retornaBancos ;
pla ;restaura bancos de origem e destino
lsr ;desloca o nibble que
lsr ;representa o banco de origem
lsr ;para a direita até que ele
lsr ;seja o low nibble
tay ;Y = banco de origem
lda $1FF4,y ;muda de banco
rts ;fake return
endm ;
;----------------------------------;
;=================================================================================
;Bank #0
;=================================================================================
Seg Bank0 ;
org $8000 ;aloca 28K abaixo
rorg $F000 ;realoca para F000h
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
.limpaMemoria ;
sta $00,x ;zera memória
dex ;X = X - 1
bne .limpaMemoria ;X = 0? não. continua limpando a memória
;
;----------------------------------;
;Seu código aqui ;
;----------------------------------;
.rotinaBanco0 ;
lda #$04 ;A = bancos de origem e destino
ldy #>(.rotinaBanco4-1) ;Y = high order do endereço da .rotinaBanco4
ldx #<(.rotinaBanco4-1) ;X = low order do endereço da .rotinaBanco4
jsr .mudaBanco ;muda de banco
lda #$01 ;essas 3 linhas são apenas para
ldx #$02 ;visualizarmos melhor a execução, não tendo
ldy #$03 ;nenhum efeito na rotina de mudança de banco
jmp .rotinaBanco0 ;loop infinito
;=================================================================================
;Bank #4
;=================================================================================
Seg Bank4 ;
org $C000 ;aloca 12K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
.rotinaBanco4 ;
lda #$46 ;A = bancos de origem e destino
;=================================================================================
;Bank #6
;=================================================================================
Seg Bank6 ;
org $E000 ;aloca 4K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
.rotinaBanco6 ;
lda #$07 ;essas 3 linhas são apenas para
ldx #$08 ;visualizarmos melhor a execução, não tendo
ldy #$09 ;nenhum efeito na rotina de mudança de banco
jmp .retornaBanco ;retorna ao banco chamador
;
org $EF06 ;alocamos em EF06h
rorg $FF06 ;realocamos para FF06h
;
.mudaBanco ;
mudaBancos ;macro
;
.retornaBanco ;
retornaBancos ;macro
;
org $EFFA ;
rorg $FFFA ;
.word inicioDoBanco ;NMI
.word inicioDoBanco ;Reset
.word inicioDoBanco ;IRQ
;
Colocamos em A os valores dos bancos de origem e destino, nessa ordem. A macro salva
esse byte no primeiro PHA. Depois, o processo é como nos exemplos anteriores: pegamos o
endereço da rotina a ser executada no banco de destino e salvamos na pilha. A partir do
TSX, vem nova mudança. A instrução TSX coloca em X o valor do registrador S (Stack
Pointer). Supondo que S fosse FFh, após os 3 PHAs, ele estaria com FCh. Esse valor seria
então colocado em X. Depois incrementamos X 3 vezes, ou seja, X agora volta a ser FFh
(lembrando que esses valores são hipotéticos, estamos dando um exemplo aqui). Agora,
através de um modo de endereçamento já visto, acessamos o valor que está em 00h + X.
Ora, se X é FFh então 00h + FFh = FFh. E assim colocamos em A o valor que está em FFh.
Esse valor é o que foi salvo no primeiro PHA da macro, ou seja, o byte que representa os
bancos de origem e destino.
Por fim, zeramos o high nibble (o banco de origem) desse valor em A, utilizando o AND, e
ficamos somente com o low nibble. Depois transferimos esse valor para Y (TAY) e
realizamos a mudança de banco normalmente com LDA $1FF4,y. O RTS seguinte é
executado já no banco de destino, conforme já explicado nos exemplos anterioes. Mas
lembrando… o RTS recupera da pilha o endereço da rotina a ser executada. Esse endereço
foi salvo pela macro através dos 2 PHAs seguintes ao primeiro PHA que salva os bancos de
origem e destino.
A macro mudaBancos salva 3 valores na pilha, sendo que o primeiro o byte corresponde aos
bancos de origem e destino, o segundo byte corresponde à parte alta do endereço da rotina
a ser executada e o terceiro byte corresponde à parte baixa desse endereço. Quando a
macro mudaBancos muda de banco, o banco de destino é carregado e o PC aponta para o
offset seguinte à instrução LDA $1FF4,y, que no caso é um RTS. Ao executar esse RTS, o
6502 é desviado para a rotina, como queríamos. O que ocorre também é que, ao executar o
RTS, a pilha é descarregada em 2 bytes, que correspondem ao endereço da rotina. Então, a
pilha agora tem só o byte que corresponde aos bancos de origem e destino e o registrador S
(Stack Pointer) está apontando para esse byte.
Daí vem a pregunta: e o RTS? Para que serve? Não salvamos nada na pilha que justifique
usá-lo!
Tudo isso tem um preço: gasta pilha e ciclos de máquina. Cada chamada gasta 5 bytes da
pilha e 45 ciclos de máquina desde o JSR do banco de origem até o RTS já no banco de
destino. Deve-se levar em conta em um programa funcional, uma vez que temos pouca
memória disponível e, dependendo do lugar, esses ciclos podem representar um STA
WSYNC a mais que teremos que fazer e com isso perder 1 linha de resolução vertical.
Vamos apresentar agora uma variação da macro que mantém os valores de A, X e Y antes
da mudança/chamada. Quando retornamos ao banco chamador, temos A, X e Y com seus
valores de antes da mudança de banco. Essa macro, como se pode presumir, gasta mais
bytes da ROM, gasta mais RAM e conseqüentemente mais ciclos de máquina.
;=================================================================================
;Macros
;=================================================================================
MAC startBank ;colocar no início de cada banco (exceto no #0)
;
inicioDoBanco ;
ldy #$00 ;
lda $1FF4,y ;startup sempre pelo banco #0
ENDM ;
;----------------------------------;
mac mudaBancos ;colocar no fim de cada banco
pha ;salva A
txa ;
pha ;salva X
tya ;
pha ;salva Y
tsx ;X = stack pointer
inx ;
inx ;
inx ;
inx ;X = offset do JSR chamador
inc $00,x ;offset seguinte
lda ($00,x) ;pega byte que é o banco de origem e destino
pha ;coloca na pilha
and #$0F ;mascara high nibble
tay ;Y = banco de destino
inc $00,x ;offset seguinte
lda ($00,x) ;pega high order do endereço da subrotina
pha ;coloca na pilha
inc $00,x ;offset seguinte
lda ($00,x) ;pega low order do endereço da subrotina
pha ;coloca na pilha
lda $1FF4,y ;muda de banco
rts ;fake return
endm ;
;----------------------------------;
mac retornaBancos ;
pla ;restaura banco de origem
lsr ;desloca o nibble que
lsr ;representa o banco de origem
lsr ;para a direita até que ele
lsr ;seja o low nibble
tay ;Y = banco de origem
lda $1FF4,y ;muda de banco
pla ;
tay ;restaura y
pla ;
tax ;restaura X
pla ;restaura A
rts ;fake return
endm ;
;----------------------------------;
;
;=================================================================================
;Bank #0
;=================================================================================
Seg Bank0 ;
org $8000 ;aloca 28K abaixo
rorg $F000 ;realoca para F000h
;
sei ;seta disable interrupt
cld ;limpa modo decimal
lax $00 ;X = A = 0
;
;=================================================================================
;Bank #4
;=================================================================================
Seg Bank4 ;
org $C000 ;aloca 12K abaixo
rorg $F000 ;realoca para F000h
;
startBank ;assegura startup pelo banco #0
;
;----------------------------------;
; Seu código aqui ;
;----------------------------------;
.rotinaBanco4 ;
lda #$04 ;essas 3 linhas são apenas para
ldx #$05 ;visualizarmos melhor a execução, não tendo
ldy #$06 ;nenhum efeito na rotina de mudança de banco
;
jsr .mudaBanco ;muda de banco
.byte #$46 ;do banco #4 para o banco #6
.byte #>(.rotinaBanco6-1) ;high order do endereço da .rotinaBanco6
.byte #<(.rotinaBanco6-1) ;low order do endereço da .rotinaBanco6
;
jmp .retornaBanco ;retorna ao banco chamador
;
O banco #6 não sofre alterações nesse exemplo. Execute o programa no modo debug e veja
como os valores de A, X e Y são preservados, ou seja, A, X e Y têm determinados valores
antes da mudança de banco e, quando ocorre o retorno, eles têm seus valores restaurados.
Isso implicou em mais gasto de pilha, bytes da ROM e ciclos de máquina. Para verificar
quantos ciclos essas rotinas (de mudança e retorno) estão gastando, basta colocar um STA
WSYNC no início de cada macro. Então no debug, quando executar o STA WSYNC, o
contador de ciclos é zerado (figura 121). Daí basta acompanhar seu valor até o término da
rotina. Por exemplo:
Figura 121
Figura 122
Quando executamos o RTS o contador mostra que foram gastos mais 6 ciclos. Então na
verdade temos o total de 76 + 6 = 82 ciclos que essa rotina de mudança de banco gasta.
Se ela for executada (se uma mudança de banco ocorrer) dentro do .scanLoop (tempo em
que desenhamos na tela), com certeza ela gastará mais de 1 linha de resolução horizontal,
ficando assim uma certa deformidade na imagem. Conforme o caso, seria necessário usar
um STA WSYNC logo após a mudança de banco ocorrer.
Figura 123
Nota: O Stella disassembla como instruções normais. Como o JSR está no seu formato
correto, ou seja, JSR $aaaa (veja os opcodes: 20h é o JSR e 01h FFh é o endereço no
formato little endian), para ele, o próximo byte deve ser uma instrução. Como montamos o
byte 46h, que na nossa intenção são os valores do banco de origem e destino, ele interpreta
46h como o opcode do LSR (vide página 21 desse tutorial). Ora, LSR de opcode 46h espera
1 byte como endereçamento de zero page. Então ele assume que F0h é esse byte (e que na
verdade é a high order do endereço da rotina no banco de destino). Daí restou o 3º byte que
montamos, que é a low order da rotina. O disassembler do Stella interpretou esse byte como
sendo DC $02.
Apesar disso, sabemos que não dará problema, pois esse código não será executado, ou
seja, ele não precisa fazer sentido porque o 6502 não vai executá-lo.
Ao executar o JSR, o 6502 salva na pilha o offset seguinte ao do JSR (que no caso da figura
123 seria salvo o F00Ch – 1 = F00Bh) e é desviado para a rotina de mudança de banco e
quando voltar, vai direto para o JMP seguinte aos bytes montados em vez de voltar para o
F00Ch (F00Bh + 1).
A própria rotina de mudança de bancos seta o retorno para o JMP. Para ela funcionar, deve
fazer isso e aproveitamos para retornar para o lugar certo.
Os 3 primeiros PHAs salvam os registradores para que não tenham seus valores destruídos
quando do retorno do banco de destino para o banco chamador (preservamos seus valores
antes da mudança de banco).
O TSX já vimos. Coloca em X o Stack Pointer. Daí incrementamos X até apontar para o
lugar na pilha onde foi salvo o offset que o JSR .mudaBanco colocou lá. Agora é que vem a
parte interessante da coisa. Utilizando modos de endereçamento (por isso é importante),
fazemos um INC $00,x e um LDA ($00,x) que é diferente do outro exemplo que usávamos
LDA $00,x (sem parêntesis).
O INC $00,x incrementa a low order do offset salvo na pilha quando executamos o JSR
.mudaBanco. Daí, quando fazemos o LDA ($00,x), o 6502 pegará o byte que está no
endereço offset salvo + 1. No nosso programa, depois do JSR temos o .byte #$46. Então A
vai pegar o valor 46h. Daí salvamos esse valor com PHA, pois vamos precisar dele quando
retornarmos do banco. Mascaramos esse valor para ficar somente com o nibble do banco de
destino usando o AND e transferimos para Y (TAY).
Agora fazemos outro INC $00,x. Antes estávamos apontando para o byte dos valores dos
bancos de origem e destino. Agora, estamos apontando para o byte que representa a high
order do endereço da rotina a ser executada no banco de destino. Pegamos o byte com o
LDA ($00,x) e salvamos com PHA. Fazemos novamente o incremento de X para apontar
para a low order do endereço da rotina. Pegamos o byte novamente e salvamos na pilha
com PHA. A seguir, resta-nos mudar de banco e o RTS seguinte faz o 6502 ir para o
endereço salvo na pilha com os 2 últimos PHAs.
O retorno começa com um PLA. Lembra que havíamos salvado o byte com os valores dos
bancos de origem e destino? O PLA recupera esse byte.
O processo que segue é análogo aos outros retornos. Com a diferença que, antes do RTS
final, restauramos da pilha os valores de A, X e Y de antes da mudança de banco.
Daí fica na pilha somente o offset do JSR, porém incrementado pelos INC $00,x. Como os
INC $00,x fizeram ele apontar para o byte da low order do endereço da rotina, quando o
RTS ocorrer, o 6502 vai para esse offset + 1. O offset + 1 é a instrução seguinte àqueles
.byte que colocamos. Por isso o 6502 “pula” esses .byte no programa.
Com isso terminamos aqui nossa pequena lição sobre bankswitching. O que foi apresentado
é só o básico e apenas 1 ou 2 técnicas simples e não otimizadas. Há outras formas de fazer
bankswitching conforme já comentado.
Com o que foi exposto, podemos criar ROMs de mais de 4K para, por exemplo, termos
ROMs com mais de 1 jogo ou um jogo que utilize mais de 1 banco (1 banco para a lógica e
outro para o Kernel, por exemplo).
Hacking
Aqui veremos como hackear uma ROM e com isso conhecer melhor o debugger do Stella. O
jogo será o Beam Rider (figura 124). O objetivo desse hack é ficar com vidas infinitas, ou
seja, os inimigos podem atingir nossa nave que não perderemos vida.
Figura 124
Para começarmos, carregue o jogo no emulador, tecle F2 (default do Stella para o reset) e
observe a quantidade inicial de vidas: 2 no caso (vide figura 125). Antes de começar a jogar
pressione a tecla de aspas simples. Em alguns teclados ela fica do lado do número 1 e
sobre a tecla Tab. O Stella entra no modo debug (figura 126).
Figura 125
Figura 126
Figura 127
Vamos destacar a RAM (figura 127). O que está em vermelho significa que sofreu
alterações, mas isso não é importante nesse caso. Agora, clique em Srch (de search), digite
02 (que é o número de vidas atual) e clique em Ok. Com isso procuramos na RAM todos os
endereços que têm valor 02h. Veja na figura 128 que esses valores ficam destacados
(endereços 85h e FCh).
Figura 128
Agora vamos sair do modo debug (clique em Exit ou tecle a aspas simples novamente ou
ainda digite run no lado esquerdo da tela do debugger do Stella). Comece a jogar (nesse
jogo basta movimentar o joystick ou teclar uma das setas de direção do teclado) e deixe que
a nave seja atingida (pode demorar um pouco até isso acontecer). Não dispare tiros nem
atinja naves inimigas. Afinal, queremos que aconteça o mínimo de mudanças na RAM.
Quando nossa nave for atingida, perderemos 1 vida. Daí o jogo volta a ficar aguardando que
pressionemos uma das teclas de direção (ou movimentemos o joystick) conforme a figura
129 (observe que agora temos 1 só vida). Nesse momento tecle novamente a aspas simples
para entrarmos no modo debug.
Figura 129
Agora é só comparar. Começamos o jogo com 2 vidas. Daí perdemos 1 vida e agora resta 1.
Então procuramos na RAM o valor 01h. Para isso, clique em Cmp (de compare), digite 01 e
clique em Ok. O Stella vai comparar somente os endereços encontrados anteriormente com
search e comparar seus valores com o 01h. Conforme a figura 130, somente 1 dos
endereços ficou destacado. Esse endereço, o 85h (linha 80h e coluna 05h) é um forte
candidato a ser o byte que contém o número de vidas.
Figura 130
Para termos certeza de que é esse o byte do número de vidas, vamos alterar seu valor.
Podemos fazer isso de 3 formas:
• dê um duplo clique no byte, digite um novo valor (por exemplo 09) e tecle enter (tem
que ser o do bloco alfabético, pois o enter do bloco numérico não funciona)
• digite (no lado esquerdo do debugger) o seguinte comando: ram $85 $09
• digite (no lado esquerdo do debugger) o seguinte comando: poke $85 $09
Qualquer uma das 3 formas apresentadas altera o valor da RAM no endereço 85h colocando
lá o valor que queremos (no caso 09h).
Figura 131
Bom… o que pretendemos fazer? Colocar vidas infinitas, ou seja, a quantidade de vidas não
deve chegar a zero. Se pararmos para pensar, um programa faz o seguinte (tipicamente):
decrementarVida
Decrementa essa variável
Isso se deve ao seguinte fato: quando ele decrementa uma variável ele faz uma operação de
escrita na RAM. Quando ele testa uma variável ele faz uma operação de leitura da RAM.
O próximo passo é saber onde esse byte tem seu valor alterado. Para isso, setamos uma
trap que interceptará toda e qualquer operação de escrita nesse local da RAM. Se
tivéssemos escolhido burlar o teste, setaríamos uma trap de leitura. Como escolhemos
burlar o decremento, setaremos uma trap de escrita.
Para setar a trap de escrita, digitamos o seguinte no debugger do Stella (entre no modo
debugger novamente): trapwrite $85
O $85 é o local da RAM que queremos interceptar uma escrita (óbvio). Então, saia do modo
debug e deixe sua nave ser atingida novamente. O Stella volta automaticamente para o
debugger e pára depois da instrução que gerou a trap.
Basta então voltarmos o código (fazer um scroll e ver o offset anterior ao PC) e verificar qual
instrução escreveu em 85h (figura 132). É um STA $85. Poderia ser um DEC $85, que nesse
caso faz mais sentido, ou seja, decrementa o número de vidas. Entretanto, esbarramos em
um STA $85. Pela lógica, concluímos que o número de vidas em 85h da RAM é lido em
algum momento, manipulado e depois atualizado (colocado novo valor em 85h). Portanto,
concluímos que não importa onde e quando esse valor é lido e nem o que é feito com ele. O
que nos importa é: não devemos deixar que o endereço 85h seja atualizado, ou seja, não
devemos deixar o STA $85 ser executado.
Figura 132
Isso é fácil de ser conseguido. Basta colocarmos NOP no lugar. NOP quer dizer No
OPeration (não faz nada). Como STA $85 tem 2 bytes e o NOP só tem 1 byte, devemos
então colocar 2 NOPs. Mas primeiro temos que saber qual é o opcode do NOP. É EAh (vide
página 22 desse tutorial). Devemos saber isso porque o Stella não tem um assembler. Não
podemos entrar com o mnemônico NOP e sim com seu opcode. Temos 2 formas de fazer
isso:
• dê um duplo clique na linha do offset $F671 e digite o opcode do NOP duas vezes e
tecle enter (o do bloco alfabético, não o do bloco numérico) conforme a figura 133.
• digite (no lado esquerdo do debugger) o seguinte comando: rom $f671 $ea $ea
Figura 133
Com o programa alterado (figura 134), saia do modo debug e deixe a nave ser atingida
novamente. Veja que o número de vidas se mantém. Agora temos um Beam Rider com
vidas infinitas.
Figura 134
Por fim, resta-nos salvar as alterações. Isso mesmo. O que fizemos foi no emulador, não na
ROM (arquivo .BIN). Temos que gravar nossas alterações em um novo arquivo .BIN. Para
isso, temos 2 formas:
• clique com o botão direito do mouse sobre o código da ROM e clique em Save ROM
(vide figura 135) e digite o nome da nova ROM.
• digite (no lado esquerdo do debugger) o comando: saverom nome_da_rom.bin
Figura 135
Com esse exemplo podemos ter noção de como patchear qualquer ROM. Por exemplo: em
vez de não deixarmos que o número de vidas fosse atualizado, poderíamos descobrir onde é
feito o teste de colisão entre os objetos. Afinal, após uma colisão o número de vidas é
decrementado. Se não deixarmos que o teste de colisão ocorra, nossa nave poderia colidir a
vontade com quaisquer objetos da tela e o jogo sempre continuaria ininterruptamente.No
caso apresentado, quando há colisão o jogo pára e volta para a tela que aguarda um
movimento do joystick mas o número de vidas não é decrementado. Se optássemos por
burlar o teste de colisão, o número de vidas também não seria decrementado e o jogo não
teria as paradas características de quando há colisão.
E:\Atari\SDK\doc\Tutorial.doc Página 242 de 243
Atari 2600 – Programação em Assembly para o processador 6502 20/8/2007
Conclusão
Esse pequeno tutorial teve o objetivo de reunir em um só lugar o máximo de informação
sobre a programação do 2600. Ainda faltam algumas coisas como técnicas e tópicos
avançados.
Por exemplo: programas com múltiplos sprites, outras formas de bankswitching, contagem
de frames, elaboração de som, scroll horizontal e vertical do playfield, matemática no
assembly, etc.etc.etc.
Por isso não nos preocupamos quanto à otimização de código e retirada de bugs (apenas
mostramos como detectar e tentar corrigir no nosso primeiro programa que mostra alguma
imagem).
Agora, cabe a cada um correr atrás das técnicas e otimizações para criar seu próprio
programa sem bugs.