Atari 2600: Microprocessador 6507 (parte 2)

O processador do Atari 2600 é uma versão resumida do MOS Tech 6502, usando no Apple ][ e no Commodore 64 (de fato, esse último usava o 6510, que é uma versão melhor do 6502!). As diferenças são que o 6507, do Atari, não possui pinagem para lidar com interrupções — nem IRQs, nem NMIs — e muito menos com DMA, o que torna o circuito do Atari muito simples. Ele também possui um barramento de endereços de apenas 13 bits, resultando num espaço de endereçamento máximo de 8 KiB. No mais, ele é essencialmente um 6502.

Embora não tenhamos interrupções e DMA, o 6507 (assim como o 6502) possui um pino chamado RDY (de Ready) que, quando é zerado, faz com que o processador pare de processar instruções. Isso, como veremos no artigo sobre o TIA, será bem útil… Normalmente esse pino é ligado à Vcc e o processador fica processando instruções o tempo todo…

A memória, para o 6502, é dividida em páginas de 256 bytes:

Como um endereço de memória, para o 6502, é um valor de 16 bits, os 8 bits superiores (MSB) correspondem a uma “página” da memória. Assim, o endereço absoluto $F000 está na página $F0 (ou página 240). Esse conceito é importante porque existem duas página especiais para o 6502:

  • Página zero: Endereçar um byte nessa página cria instruções menores porque apenas o LSB do endereço é fornecido no operando;
  • Página 1: É onde fica a pilha usada pelo processador (não tem como mudar isso!).

Como vimos na parte 1, a página 1 é um espelho da página 0 na arquitetura do Atari 2600, sendo que apenas os 128 bytes superiores são usados para RAM. Isso permite endereçamento da RAM via página zero e o ajuste do stack pointer, que usa sempre a página 1.

Registradores:

O 6502 possui apenas 3 registradores que podem ser considerados como “de uso geral”: A, X e Y. Desses, o acumulador (A) é especializado: Todas as instruções lógicas e aritméticas são sempre feitas usando A, nunca usando X ou Y, exceto:

  • X ou Y podem ser usados como índices do operando da instrução que acessa memória;
  • X ou Y podem ser incrementados e decrementados via instruções específicas.

Note que você não pode usar instruções de adição, subtração, AND, OR ou XOR (não existe NOT) com os registradores X ou Y.

Além desses 3, temos o registrador P (de Processor status word), que acomoda os flags N (negativo), V (oVerflow), C (Carry), Z (Zero); bem como os flags de desabilitação de interrupção (I), que é inútil no 6507; o flag de aritmética decimal (D), usada para manipular BCD; e o flag de BREAK (read only), que indica se uma instrução BRK foi executada.

Temos também o registrador S (de Stack pointer), que contém o offset da página 1. Diferente de outros processadores, o 6502 sempre aponta para uma posição não usada da pilha. Instruções como PHA (PusH A) gravam A na posição $0100+S e depois decrementa S. PLA (PulL A) faz o contrário: incrementa S e depois pega o byte apontado por $0100+S e coloca em A.

O único registrador de 16 bits é o PC (Program Counter), que contém o endereço da próxima instrução que será executada.

Instruções e modos de endereçamento:

Uma instrução em assembly, no 6502, corresponde a um mnemônico e um operando (que pode ser opcional para certas instruções). Por exemplo, a instrução de carga de A é LDA (de LoaD Accumulator), mas carregar A com o quê? Muitas instruções desse tipo aceitam os seguintes operandos:

  • Imediato: O valor que será usado faz parte da instrução e segue, imediatamente, o opcode. Por exemplo, se quero carregar A com o valor 2, faço: LDA #2. Esse # ai é a maneira de indicar que estamos lidando com valores imediatos e, em certos assemblers, ele pode ser substituído pelo símbolo =.
  • Página zero: O argumento é um endereço na página zero. LDA $1B carregará A com o byte que está no endereço $001B. Note que não temos o # na frente do valor em hexadecimal, mas um $… esse $ não é uma designação de ponteiro, mas um marcador que diz que o valor que segue é hexadecimal (é o ‘0x’ do C)… Note que a mesma instrução poderia ter sido escrita como LDA 27.
  • Absoluto: O argumento é um endereço absoluto, ou seja, de 16 bits. A sintaxe e semântica é exatamente a mesma do modo “página zero”, acima: LDA $2000. A diferença é que essa instrução usa 2 bytes como operando, ficando maior que a instrução que use “página zero”.
  • Página zero, indexado: Podemos usar o registrador X (e só ele) como um índice que será somado a um endereço base para obter o endereço de onde o byte será lido. LDA $80,X lerá o byte contido em $0080+X e colocará em A. Esse modo não usa jamais o registrador Y. As únicas exceções a essa regra são as instruções LDX (LoaD X) e STX (STore X), que não podem usar o próprio X como índice, então usa Y.
  • Absoluto, indexado: Mesma coisa que o indexado acima, mas permite o uso de X ou Y como indexador. Exemplo: LDA $2000,X ou LDA $2000,Y. No caso de LDX, LDY, STX e STY o mesmo registrador que está sendo manipulado não pode ser usado como índice.
  • Os modos indiretos: Aqui o operando indica o endereço onde o endereço, absoluto, onde o dado está é lido. Como não temos como armazenar ponteiros em registradores (que são todos de 8 bits), então é necessários armazená-los num endereço da página zero (sempre lá!) e usar um dos modos indiretos para acessar os dados apontados. Eis os dois modos de endereçamento: LDA ($00,X) e LDA ($00),Y.O primeiro soma X ao endereço de página zero fornecido na instrução para obter o endereço (na página zero) onde a instrução lerá o endereço que será usado para ler o byte e colocá-lo em A (ufa!)… A segunda, lê o endereço absoluto que está contido na página zero e soma-o a Y, usando-o para ler o byte e colocá-lo em A.

Quanto ao conjunto de instruções, ele é bem simplificado:

As instruções de movimentação são LDA, LDX e LDY para carregar os respectivos registradores; STA, STX e STY para armazenar os respectivos registradores na memória. TAX, TAY, TXA e TYA para mover o conteúdo de A para X, A para Y, X para A e Y para A, respectivamente. Ainda temos TXS e TSX, onde, você já deve ter adivinhado, X é movido para S e S para X, respectivamente.

Temos 4 instruções de manipulação de pilha: PHA e PLA, onde A é empurrado (Push) ou puxado (Pull) da pilha; PHP e PLP, faz o mesmo com o registrador P.

Apenas cinco instruções aritméticas: ADC e SBC, onde A é adicionado com o operando e também com o flag carry (A := A + op + C), o mesmo vale, mas para subtração (A := A – op – C)… Note que não existem instruções de soma e subtração que desconsiderem o carry como argumento de entrada.

Também temos as instruções de comparação CMP, CPX e CPY, que realizam uma subtração entre A, X ou Y, respectivamente, e o operando (sem considerar o carry, como SBC), mas afetam apenas os flags.

Duas instruções, cada, de incremento e decremento dos registradores X e Y: INX, INY, DEX, DEY. Repare que, se quisermos incrementar X ou Y em mais de uma unidade teremos que usar mais de uma dessas instruções, como em:

  ; faz X := X + 4.
  inx
  inx
  inx
  inx

Ou fazer algo assim (se não precisarmos preservar A):

  txa      ; A := X
  clc      ; zera carry
  adc #4   ; A := A + 4
  tax      ; X := A

Ambos fragmentos consomem a mesma quantidade de ciclos de clock, mas a segunda tem as desvantagens de modificar A e de usar 1 byte a mais.

Temos duas outras instruções de incremento e decremento, que afetam apenas conteúdo de memória: INC e DEC.

Instruções lógicas: AND, ORA e EOR, que fazem, respectivamente, AND, OR e XOR entre A e o operando (que pode ser o próprio e, neste caso, é comum que o operando não seja informado. Ainda, temos ASL (Arithmetic Shift Left) e LSR (Logical Shift Right) que fazem shift para a esquerda e direita, respetivamente, colocando 0 no lugar do bit vago… Também temos instruções de rotação ROL e ROR, que fazem rotação de 9 bits (o bit C, dos flags, é usado como o 9º bit [msb]).

Repare que não existe uma instrução NOT, já que essa pode ser obtida facilmente com EOR: EOR #$FF.

Existe uma instrução lógica extra chamada BIT que copia os bits 7 e 6 do operando para os flags N e V, respectivamente, e seta o flag Z com a operação AND entre A e o operando.

Instruções de salto: JMP, JSR, RTS (salto incondicional, salto para sub-rotina [Jump to Sub Routine] — usando a pilha e retorno de sub-rotina [ReTurn from Sub routine] — também usando a pilha), e a série de “pulos” (branch) condicionais: BEQ, BNE, BCC, BCS, BMI, BPL, BVC e BVS. Onde EQ (equal, ou Z=1), CC (Carry cleared, C=0), MI (minor, ou N=1), PL (plus, ou N=0), VC (Overflow cleared, V=0)… CS e VS você pode imaginar o que sejam com base nessa lista, não?

Os branches, além se sempre serem condicionados aos flags, têm como operando um endereço de salto é relativo do registrador PC e que tem apenas 1 byte. Assim, “pulos” condicionais só podem saltar de -128 a +127 bytes em relação ao endereço da próxima instrução. Ou seja, seus loops não podem ser muito grandes sem que você use algum “macete”.

Algumas instruções para manipulação direta de flags, como CLC, SEC, CLD e SED, CLI  e SEI. Mais sobre flags no próximo tópico…

E, por fim, existem instruções que raramente (ou nunca) serão usadas: RTI (ReTurn from Interrupt não tem sentido, num 6507, a não ser que tenhamos uma rotina de “break”) e BRK (que funciona como se fosse uma interrupção que salta para o endereço que conta dos 2 últimos bytes no espaço de endereçamento do processaor).

Uma lista completa das instruções e seus op-codes, bem como os modos de endereçamento permitidos para cada uma delas, pode ser visto aqui.

Instruções que afetam os flags:

Diferente de outros processadores, não são apenas as instruções aritméticas e lógicas que afetam os flags. Por exemplo, as instruções de carga (LDA, LDY, LDX, TAX, TAY, TXA, TYA e TSX), ao carregar A, X ou Y, afetam os flags de sinal (N) e zero (Z):

LDA #0   ; Z=1, N=0
LDX #$80 ; Z=0, N=1

Note que o flag N é sempre a cópia do bit 7 do registrador. Ele pode ser usado para determinar se a representação em complemento 2 resultou num valor negativo ou não.

As únicas instruções que não afetam flags são as de salto (todas, incluindo branches, exceto RTI), push (PHA e PHP), as de armazenamento (STA, STX e STY) e a instrução TXS.

Reset e Power-up:

O 6502 (e o 6507) leem os penúltimos 2 bytes na memória (em $FFFC, no 6502; $1FFC no 6507) e usam esses 2 bytes como o endereço onde o processador vai começar a processar instruções. Este é o “vetor de reset”. Os últimos 2 bytes são o “vetor de interrupção mascarável”, que é usado quando uma interrupção é pedida ou pela instrução BRK (ela empilha o endereço de retorno e o registrador S, depois salta para o endereço contido em $FFFE [$1FFE], como se fosse uma interrupção). O 6502 também usa o vetor de NMI, em $FFFA, mas esse não está presente no 6507.

Não há garantias a respeito do estado inicial do processador depois de um power-up ou reset a não ser pelo flag B estar zerado e o processador realizar essa leitura de $FFFC ($1FFC) e saltar. Todos os outros registradores (A, X, Y, S e P) podem conter “lixo”. Daí, é prudente que, no início do código, algum ajuste seja feito, como, por exemplo:

_init:
  sei   ; desabilita interrupções mascaráveis (I=1).
  cld   ; desabilita o modo bcd (D=0)
  ... continua aqui ...

Mesmo que o 6507 não tenha linhas de interrupção, é conveniente desabilitá-la setando o flag I. E, é claro, queremos trabalhar com valores binários (fazndo D=0).

Exemplo de rotina:

O 6502 não possui instruções especializadas de multiplicação e divisão, então, se quisermos fazer isso, temos que criar nossas próprias rotinas. Eis um exemplo de rotina de multiplicação de dois valores de 8 bits, resultando em um de 16 bits (a rotina não está “otimizada”):

      ORG $80
; Repare que essas "variáveis" estão na página 0!
temp:  .word
a_tmp: .byte

      ORG $1000  ; início da EPROM (4 KiB).

...

;=====================
; Multiplicação SEM sinal de A com X,
; Resposta (em 16 bits) em X (MSB) e A (LSB).
;
: XA = A * X.
;=====================
MULT:
      CMP #0
      BEQ .zero   ; A == 0? sai com X,A=0
      CPX #0      ; Testa se X == 0
      BEQ .zero   ; X == 0? sai com X,A=0

      ; temp = a_tmp = A.
      STA temp
      STA a_tmp

      ; zera MSB de temp.
      LDA #0      ; A := A XOR A.
      STA temp+1

.loop:
      DEX         ; X := X - 1
      BEQ .exit   ; X == 0? Sai do loop.

      ; temp = temp + A.
      LDA a_tmp
      CLC
      ADC temp
      STA temp
      LDA temp+1   ; LDA NÃO afeta Carry!
      ADC #0       ; Aproveita o Carry da soma anterior!
      STA temp+1

      JMP .loop

.exit:
      ; Carrega X e A de temp.
      LDA temp
      LDX temp+1
      RTS

.zero:
      LDA #0
      TAX
      RTS
...