자바스크립트를 활성화 해주세요

NES CPU(6502) 에뮬레이션 - 1

 ·   ·  ☕ 15 min read  ·  ✍️ Yogo

NES 에뮬레이션을 위해서 C# 환경에서 6502 CPU를 구현 하면서 참조한 지식과 경험을 정리합니다. 이전의 NESDOC1 포스트 내용이 상당수 그대로 반영이 되어있습니다.

들어가며

NES (Nintendo Entertainment System, 아시아에서는 Famicom, 국내 정발 현대 컴보이) 에뮬레이터를 구현해보려고 마음을 먹고 관련 문서를 찾아보면서 가장 먼저 해야하는 했던 것은 NES의 전체적인 시스템 구조에 대한 이해와 더불어 NES의 CPU로 사용된 6502 프로세서 코어에 대한 이해였습니다.

참고로 6502는 모스 테크놀로지에서 1975년에 출시한 프로세서로서2 애플 1~2, 아타리, NES 등 유명한 제품에서 사용되었기 때문에 다행히도 6502에 관련된 문서나 코드가 많이 공개되어 있d어서 참고할 수 있는 자료가 많이 있었습니다.

하지만 코어도 제조사별 또는 개선 여부에 따라 버젼이 나뉘면서 지원하는 명령어 세트(instruction set)가 다르기고 하고, 공개된 여러 문서를 참조해보면 같은 명령어에 대한 설명이라도 명령어 사이클(instruction cycle) 단계에서 참조하는 레지스터나 변경되는 레지스터 설명 차이가 있어서 NES에 사용된 6502 코어를 유사하게 에뮬레이션을 할 수 있는지에 대한 보증을 하기가 어려웠습니다.

대신 검증되고 공개된 에뮬레이터의 소스 코드를 참조하면 되지만 에뮬레이터 간에도 조금씩 다른 차이점을 발견하거나 코드 만으로는 이해할 수 없는 부분이 존재했기 때문에 코어에 대한 정확한 이해없이 코드 만 참조하는 것은 그냥 컨버젼 수준 밖에 되질 않기에 적극적 참조는 피하려고 했습니다.

에뮬레이션을 구현의 주 목표는 단순히 NES 롬파일을 구동시키는 여러 에뮬레이터 중 하나(결과 지향적)를 만드는 것이 아니라 NES 시스템을 가급적 정확히 이해하는 것(절차 치향적, 지적 탐구)이 최우선이었기 때문입니다.

그리고 이러한 목표에는 C#으로 개발중인 에뮬레이터를 차후에는 Python이나 C로 컨버젼하는 작업도 고려하고 있기 때문에 무엇보다 하드웨어 레벨에서의 이해가 많이 필요했습니다.

하지만 맞닥뜨린 사정이 이렇다 보니 다양한 문서과 코드를 참조가 필요했고, 뼈대를 잡고 명령어 세트를 구현하면서 문서 간이나 실제 테스트 코드를 돌리면서 충돌이 나는 부분은 검증된 에뮬레이터 코드와 상호 참조를 하면서 작업을 시작했습니다.

간략한 특징 및 레지스터

이미 Nintendo Entertainment System - CPU를 정리하면서 CPU에 대한 특징을 기술했었습니다. (정확히는 6502 코어가 사용된 Ricoh 2A03/2A07 프로세서)

중복되는 내용이지만 간략한 특징과 레지스터 관련 설명을 다시 기술합니다.

항목 특징
처리능력 8bit
어드레싱 16bit
레지스터 총 6개(A, X, Y, P, SP, PC)

6502는 기본적으로 8비트 프로세서입니다. 16비트 어드레스 버스를 가지므로 64kB 메모리에 직접적인 접근이 가능합니다. 메모리는 리틀엔디언(Little Endian) 체계를 가집니다. 즉, 자료형 크기에 따라 메모리 주소가 낮을수록 LSB(Least Significant Byte)에 가까워지고, 높을수록 MSB(Most Significant Byte)에 가까워 집니다.

기본적으로 A(Accumulator), X(X Index Register), Y(Y Index Register), P(Processor Status), SP(Stack Pointer), PC(Program Counter)와 같이 6개의 레지스터를 포함합니다.

프로그램 카운터(PC)

프로그램 카운터는 다음에 실행할 명령어의 주소를 저장하는 16비트 레지스터입니다. 명령어가 실행되면 프로그램 카운터의 값이 업데이트되며 일반적으로 시퀀스의 다음 명령어로 이동합니다. 값은 분기 및 점프 명령, 프로시저 호출 및 인터럽트의 영향을 받을 수 있습니다.

스택 포인터(SP)

스택은 $0100-$01FF 메모리 위치에 있습니다. 스택 포인터는 $0100에서 오프셋 역할을 하는 8비트 레지스터입니다. 스택은 하향식으로 작동하므로 바이트가 스택에 푸시되면 스택 포인터가 감소하고 스택에서 바이트를 가져오면 스택 포인터가 증가합니다. 스택 영역을 초과하는 경우 별도로 오버플로우가 감지되지 않고 스택 포인터가 $00에서 $FF로 순환 됩니다.

누산기(A)

누산기는 산술 및 논리 연산의 결과를 저장하는 8비트 레지스터입니다. 누산기는 메모리에서 조회된 값으로 설정할 수도 있습니다.

인덱스 레지스터 X(X)

X 레지스터는 일반적으로 특정 주소 지정 모드에 대한 카운터 또는 오프셋으로 사용되는 8비트 레지스터입니다. X 레지스터는 메모리에서 조회된 값으로 설정할 수 있으며 스택 포인터의 값을 가져오거나 설정하는 데 사용할 수 있습니다.

인덱스 레지스터 Y(Y)

Y 레지스터는 X 레지스터와 같은 방식으로 카운터로 사용되거나 오프셋을 저장하는 데 사용되는 8비트 레지스터입니다. X 레지스터와 달리 Y 레지스터는 스택 포인터에 영향을 줄 수 없습니다.

상태 레지스터(P)

상태 레지스터는 연산이 실행 될 때마다 세트 또는 클리어 되는 비트 플래그들의 집합으로 구성되는 레지스터 입니다.

  • Carry Flag (C) - 마지막 명령어가 비트 7에서 오버플로우(overflow) 또는 비트 0에서 언더플로우(underflow)가 발생한 경우 캐리 플래그가 세트됩니다. 예를 들어 255 + 1을 수행하면 결과는 0이 되고 캐리 비트는 세트가 됩니다. 이를 통해 시스템은 첫 번째 바이트에서 계산을 수행하고 캐리를 저장한 다음 두 번째 바이트에서 계산을 수행할 때 해당 캐리를 사용하여 8비트보다 긴 숫자에 대한 계산을 수행할 수 있습니다. 캐리 플래그는 SEC(Set Carry Flag) 명령으로 설정하고 CLC(Clear Carry) 명령으로 Clear할 수 있습니다.

  • Zero Flag(Z) - 마지막 명령어의 결과가 0인 경우 제로 플래그가 세트됩니다. 예를 들어 128 - 127은 0 플래그를 세트 되지 않는 반면 128 - 128은 세트합니다.

  • Interrup Disable (I) - 인터럽트 비활성화 플래그는 시스템이 IRQ에 응답하는 것을 방지하는 데 사용할 수 있습니다. 이것은 SEI(Set Interrupt Disable) 명령에 의해 설정되고 IRQ는 CLI(Clear Interrupt Disable) 명령이 실행될 때까지 무시됩니다.

  • Decimal Mode (D) - 10진수 모드 플래그는 6502를 BCD 모드로 전환하는 데 사용됩니다. 이 플래그는 SED(Set Decimal Flag) 명령으로 설정하고 CLD(Clear Decimal Flag)에 의해 클리어 할 수 있습니다. (NES용 6502에서는 사용되지 않습니다)

  • Break Command (B) - BRK(Break) 명령이 실행되어 IRQ가 발생했음을 나타내는 데 사용됩니다.

  • Overflow Flag (V) - 이전 명령어에서 잘못된 2의 보수 결과를 얻은 경우 오버플로우 플래그가 세트 됩니다. 이것은 양수가 예상되었을 때 음수를 얻었거나 그 반대의 경우를 의미합니다. 예를 들어, 두 개의 양수를 더하면 결과 또한 양수가 되어야 합니다. 그러나 64 + 64는 sign bit로 인해 -128 결과 제공합니다. 따라서 이 경우 오버플로우 플래그가 세트됩니다. 오버플로우 플래그는 비트 6과 7 사이와 비트 7과 캐리 플래그 사이에서 캐리의 배타적 논리합을 취하여 결정됩니다. 자세한 사항은 문서의 Appedix A를 참고하시기 바랍니다.

  • Negative Flag (N) - 바이트의 비트 7은 해당 바이트의 부호를 나타내며 0은 양수이고 1은 음수입니다. 이 부호 비트가 1이면 음수 플래그(부호 플래그라고도 함)가 세트됩니다.

// 상태 레지스터
  7   6   5   4   3   2   1   0
+-------------------------------+
| N | V |   | B | D | I | Z | C |
+-------------------------------+
// 5비트는 unused

어드레싱 모드(Addressing Mode)

6502는 복잡하고(?) 다양한 어드레싱 방식을 지원합니다. 직접적인 어드레싱 외의 다양한 방법을 사용하는 것은 어드레싱 버스 크기(여기서는 16bit)로 제한 된 영역을 벗어난 확장된 주소를 지정하거나 또는 어드레싱 버스 크기보다 상대적으로 적은 메모리를 사용하는 등의 장점을 가질 수 있습니다. 에뮬레이션을 위해서는 명령어 세트 만큼 어드레싱 모드에 대한 이해도 중요하므로 관련 내용을 정리 해보겠습니다. 여기서 메모리 주소는 HEX(16진수) 방식으로 $1F24 와 같이 표현 됩니다.
참조 문서 및 자세한 사항은 Nesdoc의 Appendix E, Easy 65023 또는 Ultimate Commodore 64 Reference4에서 찾아볼 수 있습니다.

Implied(Implicit)

INX(Increment X Register)처럼 피연산자(operand) 위치가 결정되어 있는 명령어들이 있습니다. 이러한 명령어들은 암시적 어드레싱(Implicit Addressing)이라고 합니다.

Accumulator

누산기(Accumlator)를 직접 조작하는 명령어들이 사용하는 어드레싱 모드 입니다.

Immediate

특정 메모리 주소나 위치를 참조하는 것이 아니라 값(value)으로서 사용 합니다!

1
2
3
; 명령어 #$값
LDX #$01    ; X 레지스터에 #$01을 저장 (Immediate)
LDX $01     ; X 레지스터에 $01 번지에 있는 값을 저장 (Zero Page)

Zero Page

Zero Page는 첫 페이지(256 byte) 메모리 $0000 ~ $00FF 위치를 뜻하기도 하며, 해당 어드레싱 모드는 바로 첫 페이지만을 참조하기 위해서 8bit 크기 만큼만 주소영역으로 사용합니다. 상대적으로 짧은 주소 및 적은 operand 사용으로 빠른 접근과 연산을 목적으로 하고 있습니다.

1
2
; 명령어 $주소(8bit)
AND $12 ; $12 번지의 데이터와 AND 연산

Indexed Zero Page (X, Y-Indexed Zero Page)

Zero Page 주소에서 X, Y 레지스터에 저장된 값 만큼 증가한 주소를 참조합니다. Zero Page Y의 경우는 오직 LDX (Load X Regisger)와 STX (Store X Register) 명령어에서만 사용합니다.

그리고 참조할 기본 주소와 X, Y 레지스터에 저장 된 오프셋(offset)을 더한 주소가 Zero Page를 넘어가는 경우 참조할 주소는 페이지 영역을 벗어나지 않고 순환됩니다.

예를 들어 X 레지스터 값이 $01이고 $FF가 참조할 기본 주소인 경우 최종 참조 주소는 $0100이 아닌 $00이 됩니다.

1
2
3
LDX #$01    ; X 레지스터에 #$01을 저장
STA $FF, X  ; $FF 번지에서 X 레지스터 오프셋 만큼 증가한 위치에 A 값을 저장
            ; $FF + $01 is $00, not $0100

Absolute

어드레싱 버스 크기(16bit)의 메모리 주소를 참조합니다.

1
2
; 명령어 $주소(16bit)
STA $2000 # $2000 번지에 A(accumlator) 값을 저장

Indexed Absolute (Absolute X, Y)

Absolute 메모리 주소에서 X, Y 레지스터에 저장된 값 만큼 증가한 주소를 참조합니다.

1
2
3
; 명령어 $주소(16bit), X(or Y)
LDX #$01      ; X 레지스터에 #$01을 저장
STA $2000, X  ; $2000 번지에X 레지스터 값 만큼 증가한 위치($2001)에 A 값을 저장

Absolute Indirect

Indirect는 어드레싱 버스 크기(16bit) 주소 위치에 저장된 값을 참조할 주소로서 해당 위치를 기준으로 16비트 크기 만큼 저장된 값의 메모리 주소를 참조합니다.

예를 들어 $F0 주소 위치에 $01이 저장되어 있고, $F1 주소 위치에 $CC 값이 저장되어 있는 경우, Indirect 어드레싱 모드로 $00F0 위치를 참조하면 실질적으로는 $CC01을 참조하게 됩니다.

다시 말하자면 직접 참조할 메모리 주소에 최종적으로 참조할 메모리 주소가 있다는 뜻입니다.

1
2
3
4
5
6
; 명령어 $주소(16bit)
LDA #$01
STA $F0
LDA #$CC
STA $F1
JMP ($00f0) ; 실제 참조할 주소 $CC01

Relative

Branch 명령어에서 사용되는 어드레싱 입니다. 조건에 따라 프로그램 카운터 증가 값이 달라집니다. 조건에 관계없이 PC는 2만큼 증가하지만, 명령어 조건을 만족하면 추가적으로 PC가 증가합니다.

Indexed Indirect (X-Indexed Zero Page Indirect)

여기서 부터는 두 가지 어드레싱 모드가 합쳐진 형태라 조금 복잡합니다. Nesdoc의 Appendix E에 이미지로 표현이 잘 되어 있으니 그 부분을 참고하시면 좋습니다.

기본적으로 X-Indexed Zero Page 처럼 Zero Page 를 가리키는 주소와 X 레지스터의 오프셋만큼 더한 후 해당 위치에서 16비트 크기 만큼 참조한 값을 주소로 한 위치를 참조합니다.

예를 들면 X 레지스터에 #$01 값이 저장되어 있고, 제로 페이지 $11 위치에는 $23가 $12에 $12가 저장되어 있다고 가정한다면 Indexed Indirect 어드레싱 모드의 명령어가 피연산자 위치를 $10로 지정할 경우 $10 + $01 = $11 (X-Indexed Zero Page) 가 되어 제로 페이지 $11을 기준으로 $11, $12 (Absolute Indirect)로 재 참조하여 $1234 위치를 참조하게 됩니다.

Indirect Indexted (Zero Page Indirect Y-Indexed)

여기서는 위의 어드레싱 절차와 반대로 동작을 수행합니다.

Zero Page를 가리키는 주소에서 16비트 크기 만큼 참조한 값에 Y 레지스터 오프셋 만큼 더한 값을 주소로 한 메모리 영역을 참조합니다.

예를 들어 위 어드레싱과 유사하게 Y 레지스터에 $01이 저장되어 있고, $11에는 $34, $12에는 $12가 저장되어 있다고 가정하고 명령어 피연산자 위치가 $11을 가리키는 경우 $11, $12 즉, $1234 + $01 = $1235 위치를 참조하게 됩니다.

명령어 세트 (Instruction Set)

6502는 56개의 명령어가 있지만 일부 명령어는 여러 어드레싱 모드를 사용하기 때문에 1바이트로 구별 가능한 256개 중 총 151개의 유효한 opcode를 가집니다.

명령어는 어드레싱 모드에 따라 1바이트 ~ 3바이트 길이를 가집니다. 첫 번째 바이트는 opcode이고 나머지 바이트는 피연산자(operand)입니다.

아래는 Illega Opcode를 제외한 명령어 세트 테이블 입니다. 6502 instruction set5를 참조하였습니다.

$x0 $x1 $x2 $x3 $x4 $x5 $x6 $x7 $x8 $x9 $xA $xB $xC $xD $xE $xF
$0x BRK impl ORA X,ind ORA zpg ASL zpg PHP impl ORA # ASL A ORA abs ASL abs
$1x BPL rel ORA ind,Y ORA zpg,X ASL zpg,X CLC impl ORA abs,Y ORA abs,X ASL abs,X
$2x JSR abs AND X,ind BIT zpg AND zpg ROL zpg PLP impl AND # ROL A BIT abs AND abs ROL abs
$3x BMI rel AND ind,Y AND zpg,X ROL zpg,X SEC impl AND abs,Y AND abs,X ROL abs,X
$4x RTI impl EOR X,ind EOR zpg LSR zpg PHA impl EOR # LSR A JMP abs EOR abs LSR abs
$5x BVC rel EOR ind,Y EOR zpg,X LSR zpg,X CLI impl EOR abs,Y EOR abs,X LSR abs,X
$6x RTS impl ADC X,ind ADC zpg ROR zpg PLA impl ADC # ROR A JMP ind ADC abs ROR abs
$7x BVS rel ADC ind,Y ADC zpg,X ROR zpg,X SEI impl ADC abs,Y ADC abs,X ROR abs,X
$8x STA X,ind STY zpg STA zpg STX zpg DEY impl TXA impl STY abs STA abs STX abs
$9x BCC rel STA ind,Y STY zpg,X STA zpg,X STX zpg,Y TYA impl STA abs,Y TXS impl STA abs,X
$Ax LDY # LDA X,ind LDX # LDY zpg LDA zpg LDX zpg TAY impl LDA # TAX impl LDY abs LDA abs LDX abs
$Bx BCS rel LDA ind,Y LDY zpg,X LDA zpg,X LDX zpg,Y CLV impl LDA abs,Y TSX impl LDY abs,X LDA abs,X LDX abs,Y
$Cx CPY # CMP X,ind CPY zpg CMP zpg DEC zpg INY impl CMP # DEX impl CPY abs CMP abs DEC abs
$Dx BNE rel CMP ind,Y CMP zpg,X DEC zpg,X CLD impl CMP abs,Y CMP abs,X DEC abs,X
$Ex CPX # SBC X,ind CPX zpg SBC zpg INC zpg INX impl SBC # NOP impl CPX abs SBC abs INC abs
$Fx BEQ rel SBC ind,Y SBC zpg,X INC zpg,X SED impl SBC abs,Y SBC abs,X INC abs,X
약어 이름 포맷
A Accumulator OPC A
abs absolute OPC $LLHH
abs,X absolute, X-indexed OPC $LLHH,X
abs,Y absolute, Y-indexed OPC $LLHH,Y
# immediate OPC #$BB
impl implied OPC
ind indirect OPC ($LLHH)
X,ind X-indexed, indirect OPC ($LL,X)
ind,Y indirect, Y-indexed OPC ($LL),Y
rel relative OPC $BB
zpg zeropage OPC $LL
zpg,X zeropage, X-indexed OPC $LL,X
zpg,Y zeropage, Y-indexed OPC $LL,Y
Opcode Name
ADC add with carry
AND and (with accumulator)
ASL arithmetic shift left
BCC branch on carry clear
BCS branch on carry set
BEQ branch on equal (zero set)
BIT bit test
BMI branch on minus (negative set)
BNE branch on not equal (zero clear)
BPL branch on plus (negative clear)
BRK break / interrupt
BVC branch on overflow clear
BVS branch on overflow set
CLC clear carry
CLD clear decimal
CLI clear interrupt disable
CLV clear overflow
CMP compare (with accumulator)
CPX compare with X
CPY compare with Y
DEC decrement
DEX decrement X
DEY decrement Y
EOR exclusive or (with accumulator)
INC increment
INX increment X
INY increment Y
JMP jump
JSR jump subroutine
LDA load accumulator
LDX load X
LDY load Y
LSR logical shift right
NOP no operation
ORA or with accumulator
PHA push accumulator
PHP push processor status (SR)
PLA pull accumulator
PLP pull processor status (SR)
ROL rotate left
ROR rotate right
RTI return from interrupt
RTS return from subroutine
SBC subtract with carry
SEC set carry
SED set decimal
SEI set interrupt disable
STA store accumulator
STX store X
STY store Y
TAX transfer accumulator to X
TAY transfer accumulator to Y
TSX transfer stack pointer to X
TXA transfer X to accumulator
TXS transfer X to stack pointer
TYA transfer Y to accumulator

비공식 명령어 (Unofficial/Illegal/Undocumented Opcodes)6

명령어 세트 중에서는 일종의 비공식 명령어 세트가 있습니다. 원래 디자인에서는 공식적으로 사용되지도 않고 문서화되어 있지 않지만 실제로는 다양한 작업을 수행할 수 있습니다. 이러한 명령어는 유용하기도 하지만 예측이 불가능하고 불안정하기도 합니다.

일반적인 상황에서는 이러한 명령어를 무시해도 되겠지만 문제는 일부 게임들이 이 코드를 사용하여 제작되었다는 점입니다. 그래서 모든 illegal opcode를 고려할 필요는 없지만 몇가지 명령어 들은 구현을 해놔야 구동에 문제가 없을 수 있습니다.

여기서는 일단 공식 명령어만 기술하였습니다. 자세한 내용은 참조 페이지를 확인하시길 바랍니다.

명령어 분류

명령어는 동작 방식 또는 피연산자에 따라 다음과 같이 분류 할 수 있습니다. 각 명령어에 대한 상세 설명은 내용 분량 및 참조 페이지 기술이 잘 되어있으므로 링크로 대체합니다. 그리고 링크는 위 테이블의 참조 페이지가 아닌 Ultimate Commodore 64 Reference7 페이지를 참조했습니다.

가져오기/저장(Load/Store Operations)

항목 설명
설명 메모리에서 레지스터를 로드하거나 레지스터의 내용을 메모리에 저장합니다
명령어 LDA, LDX, LDY, STX, STX, STY

레지스터 전송(Register Transfer Operations)

항목 설명
Register Transfer 설명
명령어 TAX, TAY, TSX, TXA, TXS, TYA

Stack

항목 설명
설명 스택을 푸시 또는 풀하거나 X 레지스터를 사용하여 스택 포인터를 조작합니다
명령어 PHA, PHP, PLA, PLP

Logical

항목 설명
설명 누산기 및 메모리에 저장된 값에 대한 논리 연산을 수행합니다
명령어 AND, BIT, EOR, ORA

Arithmetic

항목 설명
설명 레지스터와 메모리에 대한 산술 연산을 수행합니다
명령어 ADC, CMP, CPX, CPY, SBC

Increase/Decrease

항목 설명
설명 X 또는 Y 레지스터 또는 메모리에 저장된 값을 증가 또는 감소시킵니다
명령어 DEC, DEX, DEY, INC, INX, INY

Shifts

항목 설명
설명 누산기 또는 메모리 위치의 비트를 왼쪽이나 오른쪽으로 1비트 이동합니다
명령어 ASL, LSR, ROL, ROR

Jumps/Calls

항목 설명
설명 순차적인 실행 시퀀스를 중단하고, 지정된 주소에서 재게 합니다
명령어 BRK, JMP, JSR, RTI, RTS

Branches

항목 설명
설명 특정한 조건을 만족하면 분기하여 Jumps/Calls 처럼 동작합니다. 조건은 상태 레지스터의 특정 비트와 관련이 있습니다
명령어 BCC, BCS, BEQ, BMI, BNE, BPL, BVC, BVS

Status Register

항목 설명
설명 상태 레지스터에서 플래그를 세트하거나 클리어 합니다
명령어 CLC, CLD, CLI, CLV, SEC, SED, SEI

System Functions

항목 설명
설명 거의 사용하지 않는 기능을 수행합니다
명령어 NOP

참조 페이지 명령어 설명

Ultimate Commodore 64 Reference 페이지에서 명령어 설명을 참조할 시 이해를 돕기 위한 내용을 기술 합니다.

NV-BDIZC
----
ADC - Add Memory to Accumulator with Carry

Operation: A + M + C → A, C

This instruction adds the value of memory and carry from the previous operation to the value of the accumulator and stores the result in the accumulator.

This instruction affects the accumulator; sets the carry flag when the sum of a binary add exceeds 255 or when the sum of a decimal add exceeds 99, otherwise carry is reset. The overflow flag is set when the sign or bit 7 is changed due to the result exceeding +127 or -128, otherwise overflow is reset. The negative flag is set if the accumulator result contains bit 7 on, otherwise the negative flag is reset. The zero flag is set if the accumulator result is 0, otherwise the zero flag is reset.

Note on the MOS 6502:

In decimal mode, the N, V and Z flags are not consistent with the decimal result.

Addressing ModeAssembly Language FormOpcodeNo. BytesNo. Cycles
ImmediateADC #$nn$6922
AbsoluteADC $nnnn$6D34
X-Indexed AbsoluteADC $nnnn,X$7D34+p
Y-Indexed AbsoluteADC $nnnn,Y$7934+p
Zero PageADC $nn$6523
X-Indexed Zero PageADC $nn,X$7524
X-Indexed Zero Page IndirectADC ($nn,X)$6126
Zero Page Indirect Y-IndexedADC ($nn),Y$7125+p

p: =1 if page is crossed.

특정 명령어를 참조하시면 위의 카드형태로 설명이 기술 되어 있는 것을 보실 수 있습니다. 기본적으로 최상단과 중간에 이름과 설명, 우측 상단에는 명령어 실행 시 영향을 받는 상태 레지스터의 플래그를 표시해주고 있습니다. 그리고 지원하는 어드레싱 모드와 연산 수행 시 소요되는 사이클 수를 표시하고 있습니다.

차후 설명하겠지만 소요되는 사이클은 매우 중요한데 CPU 연산 시 소모되는 시간을 정확하게 계산해야 하기 때문입니다. 물론 단순 명령 연산 결과만을 얻기 위해서는 사이클을 무시해도 됩니다. 하지만 에뮬레이터에서는 원래 하드웨어 동작과 똑같거나 거의 유사하게 할 필요가 있으므로 이 사이클을 유지할 수 있도록 별도로 관리를 합니다. 그리고 차후에 설명할 PPU와 타이밍을 맞추기 위해서도 엄격한 사이클 유지가 필요합니다.

그리고 명령이 수핼 될 때마다 소요되는 사이클이 항상 일정하지 않고 if page(or page boundary) is crossed 라는 조건에 따라 추가적으로 사이클이 소요되는 것을 알 수 있습니다. 여기서 page라는 것은 256 바이트 단위의 바운더리를 가지는 메모리 영역을 뜻하고 page가 crossed가 되었다는 것은 16비트 기준 메모리에서 상위 바이트가 변경되었다는 의미를 나타내기도 합니다.

마지막으로 빨간색 박스에 Note가 별도로 기술되어 있는데 재미있는 점은 일종의 하드웨어 버그가 존재하고, 이러한 내용을 알려주는 내용을 담고 있습니다. NES에 사용된 6502는 Decimal 모드를 사용하지 않으므로 무시할 수도 있는 내용이지만 몇몇의 하드웨어 버그 중 하나는 꼭 고려해야하므로 이 부분은 차후에 기술하겠습니다.

.. 다음 포스트에서 ..